import axios from "axios"; import CryptoJS from "crypto-js"; import _ from "lodash"; import { azureHeaders, azureHeadersPaged } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { formatDates } from "../../../components/utils"; import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; var NotifyClient = require("notifications-node-client").NotifyClient; const notifyClient = new NotifyClient(process.env.NOTIFY_API_KEY); const WORDKEY = process.env.HASHKEY; const WEBAPI_URL = process.env.RELAY_ROOT || "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; const encryptDocReference = (documentRef) => { const hash = CryptoJS.HmacSHA256( `documents/download/${documentRef}`, CryptoJS.enc.Hex.parse(WORDKEY) ).toString(CryptoJS.enc.Hex); return `/api/documents/download/${documentRef}?hash=${hash}`; }; const flattenWatchlistEntry = (entry) => { const flattened = { ...entry }; if (entry.pinswg_Contact) { flattened.contactid = entry.pinswg_Contact.contactid; flattened.contact_email = entry.pinswg_Contact.emailaddress1; flattened.firstname = entry.pinswg_Contact.firstname; flattened.lastname = entry.pinswg_Contact.lastname; flattened.pinswg_preferredlanguage = entry.pinswg_Contact.pinswg_preferredlanguage; delete flattened.pinswg_Contact; } return flattened; }; // s const buildNotifyPayloads = (watchlistByEmail) => { const reference = "PEDW-CASE-UPDATES"; const templateId = "217f7f96-2f8a-4034-a27f-9ffcf8d89edb"; const templateIdCY = "09d5cece-65d3-4717-84cd-edab99c8870c"; // const formatDocuments = (docs = []) => // docs // .map((doc) => { // const ref = doc.pinswg_isharedocumentreference || "No Ref"; // const name = doc.pinswg_name || "Untitled"; // const date = // formatDates( // doc.pinswg_documentpublisheddate?.split("T")[0] // ) || "No Date"; // const link = // process.env.NEXTAUTH_URL + doc.pinswg_hashlink || // "[No Link]"; // return ` - ${name} (${date})\n Link: ${link}`; // }) // .join("\n"); const formatDocuments = (docs = [], language = "en") => { // Labels based on language const labels = { en: { noRef: "No Ref", untitled: "Untitled", noDate: "No Date", link: "Link" }, cy: { noRef: "Dim Cyfeirnod", untitled: "Heb Deitl", noDate: "Dim Dyddiad", link: "Dolen" } }; const t = labels[language] || labels.en; return docs .map((doc) => { const ref = doc.pinswg_isharedocumentreference || t.noRef; const name = doc.pinswg_name || t.untitled; const date = formatDates( doc.pinswg_documentpublisheddate?.split("T")[0], language ) || t.noDate; const link = (process.env.NEXTAUTH_URL || "") + doc.pinswg_hashlink || "[No Link]"; return ` - ${name} (${date})\n ${t.link}: ${link}`; }) .join("\n"); }; const formatSipEvents = (events = [], language = "en") => { const labels = { en: { unnamed: "Unnamed Event", noDate: "No Date", on: "on" }, cy: { unnamed: "Digwyddiad Heb Enw", noDate: "Dim Dyddiad", on: "ar" } }; const t = labels[language] || labels.en; return events .map((event) => { const name = event.pinswg_name || t.unnamed; const date = formatDates( event.pinswg_dateeventrequested?.split("T")[0], language ) || t.noDate; return ` - ${name} ${t.on} ${date}`; }) .join("\n"); }; const formatReps = (reps = [], language = "en") => { const labels = { en: { open: "Open", to: "to", close: "Close", na: "N/A" }, cy: { open: "Agor", to: "i", close: "Cau", na: "Dim ar gael" } }; const t = labels[language] || labels.en; return reps .map((rep) => { const start = formatDates( rep.pinswg_consultationopen?.split("T")[0], language ) || t.na; const end = formatDates( rep.pinswg_consultationclose?.split("T")[0], language ) || t.na; return ` - ${t.open}: ${start} ${t.to} ${t.close}: ${end}`; }) .join("\n"); }; const extractNameFromEmail = (email) => email .split("@")[0] .replace(/\./g, " ") .split(/[\s_-]+/) .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); return Object.entries(watchlistByEmail) .map(([email, entries]) => { const contactName = entries[0].firstname + " " + entries[0].lastname; const contactId = entries[0].contactid; const filteredEntries = entries.filter( (entry) => (entry.documents && entry.documents.length > 0) || (entry.sipEvents && entry.sipEvents.length > 0) || (entry.repsPeriods && entry.repsPeriods.length > 0) ); if (filteredEntries.length === 0) return null; const prefLanguage = entries[0].pinswg_preferredlanguage; const personalisation = { contact_name: contactName, case_sections: filteredEntries .map((entry) => { const caseRef = entry[ "_pinswg_watchedcase_value@OData.Community.Display.V1.FormattedValue" ] || "Unknown Case"; const docSection = entry.documents?.length ? `Documents added or updated:\n${formatDocuments( entry.documents, "en" )}\n` : ""; const sipSection = entry.sipEvents?.length ? `SIP Events created:\n${formatSipEvents( entry.sipEvents )}\n` : ""; const repsSection = entry.repsPeriods?.length ? `Consultation Periods:\n${formatReps( entry.repsPeriods )}\n` : ""; const caseUnsubscribelink = `Unsubscribe to updates on this case ${process.env.NEXTAUTH_URL}/unsubscribe/${entry.pinswg_watchlistid}`; return `Case: ${caseRef}\nLink to case:${ process.env.NEXTAUTH_URL }/case/${caseRef}\n\n${[ docSection, sipSection, repsSection, caseUnsubscribelink ] .filter(Boolean) .join("\n")}`; }) .join("\n\n"), unsubscribeLink: `Unsubscribe by selecting this link if you no longer want to receive any updates ${ process.env.NEXTAUTH_URL + "/unsubscribeall/" + contactId }` }; const personalisationCY = { contact_name: contactName, case_sections: filteredEntries .map((entry) => { const caseRef = entry[ "_pinswg_watchedcase_value@OData.Community.Display.V1.FormattedValue" ] || "Achos Anhysbys"; const docSection = entry.documents?.length ? `Dogfennau wedi'u hychwanegu neu eu diweddaru:\n${formatDocuments( entry.documents, "cy" )}\n` : ""; const sipSection = entry.sipEvents?.length ? `Digwyddiadau SIP wedi'u creu:\n${formatSipEvents( entry.sipEvents, "cy" )}\n` : ""; const repsSection = entry.repsPeriods?.length ? `Cyfnodau Ymgynghori:\n${formatReps( entry.repsPeriods, "cy" )}\n` : ""; const caseUnsubscribelink = `Dad-danysgrifio i ddiweddariadau ar yr achos hwn ${process.env.NEXTAUTH_URL}/unsubscribe/${entry.pinswg_watchlistid}`; return `Achos: ${caseRef}\nDolen i'r achos: ${ process.env.NEXTAUTH_URL }/case/${caseRef}\n\n${[ docSection, sipSection, repsSection, caseUnsubscribelink ] .filter(Boolean) .join("\n")}`; }) .join("\n\n"), unsubscribeLink: `Dad-danysgrifiwch drwy ddewis y ddolen hon os nad ydych chi eisiau derbyn unrhyw ddiweddariadau mwyach ${ process.env.NEXTAUTH_URL + "/unsubscribeall/" + contactId }` }; return { email_address: email, template_id: prefLanguage == 846040001 ? templateId : templateIdCY, personalisation: prefLanguage == 846040001 ? personalisation : personalisationCY, reference, oneClickUnsubscribeURL: process.env.NEXTAUTH_URL + (prefLanguage == 846040001 ? "en" : "cy") + "/unsubscribeall/" + contactId }; }) .filter(Boolean); // Remove any nulls from filtered-out users }; export default async function CombinedApiProxy(req, res) { try { const token = await getToken(); // Step 1: Fetch all watchlist entries const watchlistQuery = `pinswg_watchlists?$count=true&$filter=pinswg_emailnotifications ne null&$expand=pinswg_Contact($select=contactid,emailaddress1,firstname,lastname,pinswg_preferredlanguage)&$select=pinswg_emailnotifications,_pinswg_watchedcase_value`; const watchlistRes = await axios.get( WEBAPI_URL + watchlistQuery + hashAPIPath(watchlistQuery), azureHeaders(token.access_token) ); const watchlistData = watchlistRes.data.value.map( flattenWatchlistEntry ); // Step 2: For each entry, gather docs and SIP events const now = new Date(); const twoWeeksAgo = new Date(); twoWeeksAgo.setDate(now.getDate() - 7); const nowStr = now.toISOString(); const twoWeeksAgoStr = twoWeeksAgo.toISOString(); const results = await Promise.all( watchlistData.map(async (entry) => { const incidentID = entry._pinswg_watchedcase_value; // === Fetch documents === let documents = []; try { const docsQuery = `pinswg_documents?$count=true&$filter=pinswg_publishtoweb eq true and _pinswg_documentids_value eq ${incidentID} and pinswg_documentpublisheddate ne null and pinswg_documentpublisheddate ge ${twoWeeksAgoStr} and pinswg_documentpublisheddate le ${nowStr}&$select=pinswg_name,pinswg_isharedocumentlocations,_pinswg_documentids_value,pinswg_isharelabelcasetype,pinswg_isharelabellpaname,pinswg_publishtoweb,pinswg_uploadstatus,pinswg_isharedocumentclassification,pinswg_isharedocumentreference,pinswg_name,pinswg_latestpublishedversion,pinswg_latestpublisheddate,pinswg_documentpublisheddate`; const docsRes = await axios.get( WEBAPI_URL + docsQuery + hashAPIPath(docsQuery), azureHeadersPaged(token.access_token) ); documents = docsRes.data.value.map((doc) => ({ ...doc, pinswg_hashlink: encryptDocReference( doc.pinswg_isharedocumentreference ) })); } catch (err) { consoleLogger( `Document fetch failed for incidentID: ${incidentID}` ); } // === Fetch SIP events === let sipEvents = []; try { const sipsQuery = `pinswg_sipses?$filter=_pinswg_sipscase_value eq ${incidentID}&$select=pinswg_sipsid`; const sipsRes = await axios.get( WEBAPI_URL + sipsQuery + hashAPIPath(sipsQuery), azureHeaders(token.access_token) ); const sipsRecords = _.get(sipsRes, "data.value", []); if (sipsRecords.length > 0) { const sipsId = sipsRecords[0].pinswg_sipsid; const eventsQuery = `pinswg_sipsevents?$count=true&$filter=_pinswg_sipseventsid_value eq ${sipsId} and modifiedon ge ${twoWeeksAgoStr} and modifiedon le ${nowStr}&$select=pinswg_name,pinswg_dateeventrequested,pinswg_typeofevent,createdon,modifiedon`; const eventsRes = await axios.get( WEBAPI_URL + eventsQuery + hashAPIPath(eventsQuery), azureHeaders(token.access_token) ); sipEvents = eventsRes.data.value; } } catch (err) { consoleLogger( `SIP events fetch failed for incidentID: ${incidentID}` ); } // === Fetch Reps open events === let repsPeriods; try { const repsQuery = `pinswg_sipses?$filter=_pinswg_sipscase_value eq ${incidentID} and pinswg_consultationopen ne null &$select=pinswg_consultationopen,pinswg_consultationclose`; const repsRes = await axios.get( WEBAPI_URL + repsQuery + hashAPIPath(repsQuery), azureHeaders(token.access_token) ); repsPeriods = repsRes.data.value; } catch (err) { consoleLogger( `Reps fetch failed for incidentID: ${incidentID}` ); } return { ...entry, repsPeriods, documents, sipEvents }; }) ); // Step 3: Group results by contact_email const groupedByEmail = _.groupBy(results, "contact_email"); const payloads = buildNotifyPayloads(groupedByEmail); // watchlistByEmail should be defined // Iterate and send const sendingResults = []; for (const payload of payloads) { if (!payload) continue; // skip null entries const result = { email: payload.email_address, reference: payload.reference, template_id: payload.template_id, language: payload.pinswg_preferredlanguage == 846040001 ? "en" : "cy" }; try { const response = await notifyClient.sendEmail( payload.template_id, payload.email_address, { personalisation: payload.personalisation, reference: payload.reference } ); result.status = "success"; result.notify_id = response.id; } catch (error) { result.status = "error"; result.error = error.message; } sendingResults.push(result); } return respondSuccess(res, { sendingResults }); } catch (error) { consoleLogger(error); return respondError(res, { status: 500, code: "EMAIL_COMBINED_FETCH_FAILED", message: "An error occurred while retrieving combined data.", details: error.message || error.toString() }); } }