diff --git a/app/api/job/send-open-jobs-emails/route.ts b/app/api/job/send-open-jobs-emails/route.ts index c2083bf..939f3b7 100644 --- a/app/api/job/send-open-jobs-emails/route.ts +++ b/app/api/job/send-open-jobs-emails/route.ts @@ -23,7 +23,7 @@ export const POST = async (request: NextRequest) => { 'Successfully sent out open job notification emails to doctors.', ); await createNotificationLog({ - action: NotificationAction.NEW_JOBS_ALERT, + action: NotificationAction.DOCTOR_NEW_JOBS, status: 'SUCCESS', }); return NextResponse.json( @@ -39,7 +39,7 @@ export const POST = async (request: NextRequest) => { e, ); await createNotificationLog({ - action: NotificationAction.NEW_JOBS_ALERT, + action: NotificationAction.DOCTOR_NEW_JOBS, status: 'FAIL', comment: e?.message, }); diff --git a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx index a568eed..9ce91e8 100644 --- a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx @@ -24,9 +24,9 @@ export default async function AnalysisResultsPage({ }) { const account = await loadCurrentUserAccount(); - const { id: analysisResponseId } = await params; + const { id: analysisOrderId } = await params; - const analysisResponse = await loadUserAnalysis(Number(analysisResponseId)); + const analysisResponse = await loadUserAnalysis(Number(analysisOrderId)); if (!account?.id || !analysisResponse) { return null; diff --git a/lib/services/account.service.ts b/lib/services/account.service.ts index f958c72..87eac94 100644 --- a/lib/services/account.service.ts +++ b/lib/services/account.service.ts @@ -1,6 +1,5 @@ import type { Tables } from '@/packages/supabase/src/database.types'; -import { AccountWithParams } from '@kit/accounts/api'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; @@ -26,6 +25,19 @@ export async function getAccount(id: string): Promise { return data as unknown as AccountWithMemberships; } +export async function getUserContactAdmin(userId: string) { + const { data } = await getSupabaseServerAdminClient() + .schema('medreport') + .from('accounts') + .select('name, last_name, email, preferred_locale') + .eq('primary_owner_user_id', userId) + .eq('is_personal_account', true) + .single() + .throwOnError(); + + return data; +} + export async function getAccountAdmin({ primaryOwnerUserId, }: { diff --git a/lib/services/audit/notificationEntries.service.ts b/lib/services/audit/notificationEntries.service.ts index f83a736..980de2e 100644 --- a/lib/services/audit/notificationEntries.service.ts +++ b/lib/services/audit/notificationEntries.service.ts @@ -2,9 +2,12 @@ import { Database } from '@kit/supabase/database'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; export enum NotificationAction { - DOCTOR_FEEDBACK_RECEIVED = 'DOCTOR_FEEDBACK_RECEIVED', - NEW_JOBS_ALERT = 'NEW_JOBS_ALERT', - PATIENT_RESULTS_RECEIVED_ALERT = 'PATIENT_RESULTS_RECEIVED_ALERT', + DOCTOR_NEW_JOBS = 'DOCTOR_NEW_JOBS', + DOCTOR_PATIENT_RESULTS_RECEIVED = 'DOCTOR_PATIENT_RESULTS_RECEIVED', + PATIENT_DOCTOR_FEEDBACK_RECEIVED = 'PATIENT_DOCTOR_FEEDBACK_RECEIVED', + PATIENT_ORDER_PROCESSING = 'PATIENT_ORDER_PROCESSING', + PATIENT_FIRST_RESULTS_RECEIVED = 'PATIENT_FIRST_RESULTS_RECEIVED', + PATIENT_FULL_RESULTS_RECEIVED = 'PATIENT_FULL_RESULTS_RECEIVED', } export const createNotificationLog = async ({ diff --git a/lib/services/audit/pageView.service.ts b/lib/services/audit/pageView.service.ts index efac5db..aa06aec 100644 --- a/lib/services/audit/pageView.service.ts +++ b/lib/services/audit/pageView.service.ts @@ -37,7 +37,6 @@ export const createPageViewLog = async ({ account_id: accountId, action, changed_by: user.id, - extra_data: extraData, }) .throwOnError(); } catch (error) { diff --git a/lib/services/mailer.service.ts b/lib/services/mailer.service.ts index b4a7ecc..8e2a7ea 100644 --- a/lib/services/mailer.service.ts +++ b/lib/services/mailer.service.ts @@ -13,7 +13,7 @@ type EmailTemplate = { subject: string; }; -type EmailRenderer = (params: T) => Promise; +export type EmailRenderer = (params: T) => Promise; export const sendEmailFromTemplate = async ( renderer: EmailRenderer, diff --git a/packages/database-webhooks/src/server/services/database-webhook-router.service.ts b/packages/database-webhooks/src/server/services/database-webhook-router.service.ts index 5abe3de..9299ae1 100644 --- a/packages/database-webhooks/src/server/services/database-webhook-router.service.ts +++ b/packages/database-webhooks/src/server/services/database-webhook-router.service.ts @@ -1,20 +1,7 @@ import { SupabaseClient } from '@supabase/supabase-js'; -import { - renderAllResultsReceivedEmail, - renderFirstResultsReceivedEmail, -} from '@kit/email-templates'; import { Database } from '@kit/supabase/database'; -import { - getAssignedDoctorAccount, - getDoctorAccounts, -} from '../../../../../lib/services/account.service'; -import { - NotificationAction, - createNotificationLog, -} from '../../../../../lib/services/audit/notificationEntries.service'; -import { sendEmailFromTemplate } from '../../../../../lib/services/mailer.service'; import { RecordChange, Tables } from '../record-change.type'; export function createDatabaseWebhookRouterService( @@ -113,58 +100,13 @@ class DatabaseWebhookRouterService { return; } - let action; - try { - const data = { - analysisOrderId: record.id, - language: 'et', - }; + const { createAnalysisOrderWebhooksService } = await import( + '@kit/notifications/webhooks/analysis-order-notifications.service' + ); - if (record.status === 'PARTIAL_ANALYSIS_RESPONSE') { - action = NotificationAction.NEW_JOBS_ALERT; + const service = createAnalysisOrderWebhooksService(); - const doctorAccounts = await getDoctorAccounts(); - const doctorEmails: string[] = doctorAccounts - .map(({ email }) => email) - .filter((email): email is string => !!email); - - await sendEmailFromTemplate( - renderFirstResultsReceivedEmail, - data, - doctorEmails, - ); - } else if (record.status === 'FULL_ANALYSIS_RESPONSE') { - action = NotificationAction.PATIENT_RESULTS_RECEIVED_ALERT; - const doctorAccount = await getAssignedDoctorAccount(record.id); - const assignedDoctorEmail = doctorAccount?.email; - - if (!assignedDoctorEmail) { - return; - } - - await sendEmailFromTemplate( - renderAllResultsReceivedEmail, - data, - assignedDoctorEmail, - ); - } - - if (action) { - await createNotificationLog({ - action, - status: 'SUCCESS', - relatedRecordId: record.id, - }); - } - } catch (e: any) { - if (action) - await createNotificationLog({ - action, - status: 'FAIL', - comment: e?.message, - relatedRecordId: record.id, - }); - } + return service.handleStatusChangeWebhook(record); } } } diff --git a/packages/email-templates/src/emails/account-delete.email.tsx b/packages/email-templates/src/emails/account-delete.email.tsx index 78fea85..a98c682 100644 --- a/packages/email-templates/src/emails/account-delete.email.tsx +++ b/packages/email-templates/src/emails/account-delete.email.tsx @@ -49,33 +49,28 @@ export async function renderAccountDeleteEmail(props: Props) { - - {previewText} - - + + {previewText} + {t(`${namespace}:hello`, { displayName: props.userDisplayName, })} - {t(`${namespace}:paragraph1`, { productName: props.productName, })} - {t(`${namespace}:paragraph2`)} - {t(`${namespace}:paragraph3`, { productName: props.productName, })} - {t(`${namespace}:paragraph4`, { productName: props.productName, diff --git a/packages/email-templates/src/emails/all-results-received.email.tsx b/packages/email-templates/src/emails/all-results-received.email.tsx index 0243fc4..0083376 100644 --- a/packages/email-templates/src/emails/all-results-received.email.tsx +++ b/packages/email-templates/src/emails/all-results-received.email.tsx @@ -5,7 +5,7 @@ import { Preview, Tailwind, Text, - render + render, } from '@react-email/components'; import { BodyStyle } from '../components/body-style'; @@ -46,11 +46,10 @@ export async function renderAllResultsReceivedEmail({ - - {previewText} - - + + {previewText} + {t(`${namespace}:hello`)} @@ -62,7 +61,6 @@ export async function renderAllResultsReceivedEmail({ > {t(`${namespace}:linkText`)} - {t(`${namespace}:ifLinksDisabled`)}{' '} {`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisOrderId}`} diff --git a/packages/email-templates/src/emails/company-offer.email.tsx b/packages/email-templates/src/emails/company-offer.email.tsx index f13308c..68a5adc 100644 --- a/packages/email-templates/src/emails/company-offer.email.tsx +++ b/packages/email-templates/src/emails/company-offer.email.tsx @@ -55,23 +55,19 @@ export async function renderCompanyOfferEmail({ - - {previewText} - - + + {previewText} + {t(`${namespace}:companyName`)} {companyData.companyName} - {t(`${namespace}:contactPerson`)} {companyData.contactPerson} - {t(`${namespace}:email`)} {companyData.email} - {t(`${namespace}:phone`)} {companyData.phone || 'N/A'} diff --git a/packages/email-templates/src/emails/doctor-summary-received.email.tsx b/packages/email-templates/src/emails/doctor-summary-received.email.tsx index 69ce37e..19b2b65 100644 --- a/packages/email-templates/src/emails/doctor-summary-received.email.tsx +++ b/packages/email-templates/src/emails/doctor-summary-received.email.tsx @@ -2,6 +2,7 @@ import { Body, Head, Html, + Link, Preview, Tailwind, Text, @@ -11,7 +12,6 @@ import { import { BodyStyle } from '../components/body-style'; import CommonFooter from '../components/common-footer'; import { EmailContent } from '../components/content'; -import { EmailButton } from '../components/email-button'; import { EmailHeader } from '../components/header'; import { EmailHeading } from '../components/heading'; import { EmailWrapper } from '../components/wrapper'; @@ -20,12 +20,10 @@ import { initializeEmailI18n } from '../lib/i18n'; export async function renderDoctorSummaryReceivedEmail({ language, recipientName, - orderNr, analysisOrderId, }: { - language?: string; + language: string; recipientName: string; - orderNr: string; analysisOrderId: number; }) { const namespace = 'doctor-summary-received-email'; @@ -35,13 +33,9 @@ export async function renderDoctorSummaryReceivedEmail({ namespace: [namespace, 'common'], }); - const previewText = t(`${namespace}:previewText`, { - orderNr, - }); + const previewText = t(`${namespace}:previewText`); - const subject = t(`${namespace}:subject`, { - orderNr, - }); + const subject = t(`${namespace}:subject`); const html = await render( @@ -54,29 +48,26 @@ export async function renderDoctorSummaryReceivedEmail({ - - {previewText} - - + + {previewText} + - {t(`${namespace}:hello`, { - displayName: recipientName, - })} - - - {t(`${namespace}:summaryReceivedForOrder`, { orderNr })} + {t(`common:helloName`, { name: recipientName })} - - {t(`${namespace}:linkText`, { orderNr })} - - {t(`${namespace}:ifButtonDisabled`)}{' '} - {`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`} + {t(`${namespace}:p1`)}{' '} + + {`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`} + + {t(`${namespace}:p2`)} + {t(`${namespace}:p3`)} + {t(`${namespace}:p4`)} + diff --git a/packages/email-templates/src/emails/first-results-received.email.tsx b/packages/email-templates/src/emails/first-results-received.email.tsx index 4f9f371..40ba596 100644 --- a/packages/email-templates/src/emails/first-results-received.email.tsx +++ b/packages/email-templates/src/emails/first-results-received.email.tsx @@ -46,11 +46,10 @@ export async function renderFirstResultsReceivedEmail({ - - {previewText} - - + + {previewText} + {t(`${namespace}:hello`)} diff --git a/packages/email-templates/src/emails/invite.email.tsx b/packages/email-templates/src/emails/invite.email.tsx index e59ba72..cd91424 100644 --- a/packages/email-templates/src/emails/invite.email.tsx +++ b/packages/email-templates/src/emails/invite.email.tsx @@ -74,20 +74,17 @@ export async function renderInviteEmail(props: Props) { - - {heading} - - + + {heading} + {hello} - - {props.teamLogo && (
@@ -102,20 +99,16 @@ export async function renderInviteEmail(props: Props) {
)} - -
+
{joinTeam}
- {t(`${namespace}:copyPasteLink`)}{' '} {props.link} -
- {t(`${namespace}:invitationIntendedFor`, { invitedUserEmail: props.invitedUserEmail, diff --git a/packages/email-templates/src/emails/new-jobs-available.email.tsx b/packages/email-templates/src/emails/new-jobs-available.email.tsx index 23ca3f4..34fb7d9 100644 --- a/packages/email-templates/src/emails/new-jobs-available.email.tsx +++ b/packages/email-templates/src/emails/new-jobs-available.email.tsx @@ -6,7 +6,7 @@ import { Preview, Tailwind, Text, - render + render, } from '@react-email/components'; import { BodyStyle } from '../components/body-style'; @@ -50,11 +50,10 @@ export async function renderNewJobsAvailableEmail({ - - {previewText} - - + + {previewText} + {t(`${namespace}:hello`)} diff --git a/packages/email-templates/src/emails/order-processing.email.tsx b/packages/email-templates/src/emails/order-processing.email.tsx new file mode 100644 index 0000000..8a7afb0 --- /dev/null +++ b/packages/email-templates/src/emails/order-processing.email.tsx @@ -0,0 +1,90 @@ +import { + Body, + Head, + Html, + Preview, + Tailwind, + Text, + render, +} from '@react-email/components'; + +import { BodyStyle } from '../components/body-style'; +import CommonFooter from '../components/common-footer'; +import { EmailContent } from '../components/content'; +import { EmailHeader } from '../components/header'; +import { EmailHeading } from '../components/heading'; +import { EmailWrapper } from '../components/wrapper'; +import { initializeEmailI18n } from '../lib/i18n'; + +export async function renderOrderProcessingEmail({ + language, + recipientName, + partnerLocation, + isUrine, +}: { + language: string; + recipientName: string; + partnerLocation: string; + isUrine?: boolean; +}) { + const namespace = 'order-processing-email'; + + const { t } = await initializeEmailI18n({ + language, + namespace: [namespace, 'common'], + }); + + const previewText = t(`${namespace}:previewText`); + + const subject = t(`${namespace}:subject`); + + const p2 = t(`${namespace}:p2`); + const p4 = t(`${namespace}:p4`); + const p1Urine = t(`${namespace}:p1Urine`); + + const html = await render( + + + + + + {previewText} + + + + + + + {previewText} + + + {t(`common:helloName`, { name: recipientName })} + + + {t(`${namespace}:heading`)} + + {t(`${namespace}:p1`, { partnerLocation })} + + {t(`${namespace}:p3`)} + + {isUrine && ( + <> + + {t(`${namespace}:p2Urine`)} + + )} + {t(`${namespace}:p5`)} + {t(`${namespace}:p6`)} + + + + + + , + ); + + return { + html, + subject, + }; +} diff --git a/packages/email-templates/src/emails/otp.email.tsx b/packages/email-templates/src/emails/otp.email.tsx index ae6db76..04a8b49 100644 --- a/packages/email-templates/src/emails/otp.email.tsx +++ b/packages/email-templates/src/emails/otp.email.tsx @@ -60,18 +60,17 @@ export async function renderOtpEmail(props: Props) { - - {heading} - - + + {heading} + {mainText} {otpText} -
+
diff --git a/packages/email-templates/src/emails/patient-first-results-received.email.tsx b/packages/email-templates/src/emails/patient-first-results-received.email.tsx new file mode 100644 index 0000000..adeac31 --- /dev/null +++ b/packages/email-templates/src/emails/patient-first-results-received.email.tsx @@ -0,0 +1,81 @@ +import { + Body, + Head, + Html, + Link, + Preview, + Tailwind, + Text, + render, +} from '@react-email/components'; + +import { BodyStyle } from '../components/body-style'; +import CommonFooter from '../components/common-footer'; +import { EmailContent } from '../components/content'; +import { EmailHeader } from '../components/header'; +import { EmailHeading } from '../components/heading'; +import { EmailWrapper } from '../components/wrapper'; +import { initializeEmailI18n } from '../lib/i18n'; + +export async function renderPatientFirstResultsReceivedEmail({ + language, + recipientName, + analysisOrderId, +}: { + language: string; + recipientName: string; + analysisOrderId: number; +}) { + const namespace = 'patient-first-results-received-email'; + + const { t } = await initializeEmailI18n({ + language, + namespace: [namespace, 'common'], + }); + + const previewText = t(`${namespace}:previewText`); + + const subject = t(`${namespace}:subject`); + + const html = await render( + + + + + + {previewText} + + + + + + + {previewText} + + + {t(`common:helloName`, { name: recipientName })} + + + {t(`${namespace}:p1`)}{' '} + + {`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`} + + + {t(`${namespace}:p2`)} + {t(`${namespace}:p3`)} + {t(`${namespace}:p4`)} + + + + + + , + ); + + return { + html, + subject, + }; +} diff --git a/packages/email-templates/src/emails/patient-full-results-received.email.tsx b/packages/email-templates/src/emails/patient-full-results-received.email.tsx new file mode 100644 index 0000000..6f15224 --- /dev/null +++ b/packages/email-templates/src/emails/patient-full-results-received.email.tsx @@ -0,0 +1,82 @@ +import { + Body, + Head, + Html, + Link, + Preview, + Tailwind, + Text, + render, +} from '@react-email/components'; + +import { BodyStyle } from '../components/body-style'; +import CommonFooter from '../components/common-footer'; +import { EmailContent } from '../components/content'; +import { EmailHeader } from '../components/header'; +import { EmailHeading } from '../components/heading'; +import { EmailWrapper } from '../components/wrapper'; +import { initializeEmailI18n } from '../lib/i18n'; + +export async function renderPatientFullResultsReceivedEmail({ + language, + recipientName, + analysisOrderId, +}: { + language: string; + recipientName: string; + analysisOrderId: number; +}) { + const namespace = 'patient-full-results-received-email'; + + const { t } = await initializeEmailI18n({ + language, + namespace: [namespace, 'common'], + }); + + const previewText = t(`${namespace}:previewText`); + + const subject = t(`${namespace}:subject`); + + const html = await render( + + + + + + {previewText} + + + + + + + {previewText} + + + {t(`common:helloName`, { name: recipientName })} + + + + {t(`${namespace}:p1`)}{' '} + + {`${process.env.NEXT_PUBLIC_SITE_URL}/home/analysis-results/${analysisOrderId}`} + + + {t(`${namespace}:p2`)} + {t(`${namespace}:p3`)} + + + + + + + , + ); + + return { + html, + subject, + }; +} diff --git a/packages/email-templates/src/emails/synlab.email.tsx b/packages/email-templates/src/emails/synlab.email.tsx index 29ff7d5..3605ac7 100644 --- a/packages/email-templates/src/emails/synlab.email.tsx +++ b/packages/email-templates/src/emails/synlab.email.tsx @@ -34,7 +34,7 @@ export async function renderSynlabAnalysisPackageEmail(props: Props) { const previewText = t(`${namespace}:previewText`, { analysisPackageName: props.analysisPackageName, }); - + const subject = t(`${namespace}:subject`, { analysisPackageName: props.analysisPackageName, }); @@ -70,15 +70,13 @@ export async function renderSynlabAnalysisPackageEmail(props: Props) { - - {heading} - - + + {heading} + {hello} - {lines.map((line, index) => ( ))} - diff --git a/packages/email-templates/src/index.ts b/packages/email-templates/src/index.ts index 83e3021..cae4d3f 100644 --- a/packages/email-templates/src/index.ts +++ b/packages/email-templates/src/index.ts @@ -7,3 +7,6 @@ export * from './emails/doctor-summary-received.email'; export * from './emails/new-jobs-available.email'; export * from './emails/first-results-received.email'; export * from './emails/all-results-received.email'; +export * from './emails/order-processing.email'; +export * from './emails/patient-first-results-received.email'; +export * from './emails/patient-full-results-received.email'; diff --git a/packages/email-templates/src/locales/en/doctor-summary-received-email.json b/packages/email-templates/src/locales/en/doctor-summary-received-email.json index ebefe9b..ed17242 100644 --- a/packages/email-templates/src/locales/en/doctor-summary-received-email.json +++ b/packages/email-templates/src/locales/en/doctor-summary-received-email.json @@ -1,8 +1,8 @@ { - "subject": "Doctor feedback to order {{orderNr}} received", - "previewText": "A doctor has submitted feedback on your analysis results.", - "hello": "Hello {{displayName}},", - "summaryReceivedForOrder": "A doctor has submitted feedback to your analysis results from order {{orderNr}}.", - "linkText": "View summary", - "ifButtonDisabled": "If clicking the button does not work, copy this link to your browser's url field:" -} \ No newline at end of file + "subject": "Doctor's summary has arrived", + "previewText": "The doctor has prepared a summary of the test results.", + "p1": "The doctor's summary has arrived:", + "p2": "It is recommended to have a comprehensive health check-up regularly, at least once a year, if you wish to maintain an active and fulfilling lifestyle.", + "p3": "MedReport makes it easy, convenient, and fast to view health data in one place and order health check-ups.", + "p4": "SYNLAB customer support phone: 17123" +} diff --git a/packages/email-templates/src/locales/en/order-processing-email.json b/packages/email-templates/src/locales/en/order-processing-email.json new file mode 100644 index 0000000..b3472f0 --- /dev/null +++ b/packages/email-templates/src/locales/en/order-processing-email.json @@ -0,0 +1,13 @@ +{ + "subject": "The referral has been sent to the laboratory. Please go to give samples.", + "heading": "Thank you for your order!", + "previewText": "The referral for tests has been sent to the laboratory.", + "p1": "The referral for tests has been sent to the laboratory digitally. Please go to give samples: {{partnerLocation}}.", + "p2": "If you are unable to go to the selected location to give samples, you may visit any other sampling point convenient for you - see locations and opening hours.", + "p3": "It is recommended to give samples preferably in the morning (before 12:00) and on an empty stomach without drinking or eating (you may drink water).", + "p4": "At the sampling point, please choose in the queue system: under referrals select specialist referral.", + "p5": "If you have any additional questions, please do not hesitate to contact us.", + "p6": "SYNLAB customer support phone: 17123", + "p1Urine": "The tests include a urine test. For the urine test, please collect the first morning urine.", + "p2Urine": "You can buy a sample container at the pharmacy and bring the sample with you (procedure performed at home), or ask for one at the sampling point (procedure performed in the point’s restroom)." +} diff --git a/packages/email-templates/src/locales/en/patient-first-results-received-email.json b/packages/email-templates/src/locales/en/patient-first-results-received-email.json new file mode 100644 index 0000000..1a77006 --- /dev/null +++ b/packages/email-templates/src/locales/en/patient-first-results-received-email.json @@ -0,0 +1,8 @@ +{ + "subject": "The first ordered test results have arrived", + "previewText": "The first test results have arrived.", + "p1": "The first test results have arrived:", + "p2": "We will send the next notification once all test results have been received in the system.", + "p3": "If you have any additional questions, please feel free to contact us.", + "p4": "SYNLAB customer support phone: 17123" +} diff --git a/packages/email-templates/src/locales/en/patient-full-results-received-email.json b/packages/email-templates/src/locales/en/patient-full-results-received-email.json new file mode 100644 index 0000000..8fd6ed2 --- /dev/null +++ b/packages/email-templates/src/locales/en/patient-full-results-received-email.json @@ -0,0 +1,7 @@ +{ + "subject": "All ordered test results have arrived. Awaiting doctor's summary.", + "previewText": "All test results have arrived.", + "p1": "All test results have arrived:", + "p2": "We will send the next notification once the doctor's summary has been prepared.", + "p3": "SYNLAB customer support phone: 17123" +} diff --git a/packages/email-templates/src/locales/et/common.json b/packages/email-templates/src/locales/et/common.json index fc58e08..8b41d33 100644 --- a/packages/email-templates/src/locales/et/common.json +++ b/packages/email-templates/src/locales/et/common.json @@ -4,5 +4,7 @@ "lines2": "E-mail: info@medreport.ee", "lines3": "Klienditugi: +372 5887 1517", "lines4": "www.medreport.ee" - } + }, + "helloName": "Tere, {{name}}", + "hello": "Tere" } \ No newline at end of file diff --git a/packages/email-templates/src/locales/et/doctor-summary-received-email.json b/packages/email-templates/src/locales/et/doctor-summary-received-email.json index e7efdc3..9e81ab5 100644 --- a/packages/email-templates/src/locales/et/doctor-summary-received-email.json +++ b/packages/email-templates/src/locales/et/doctor-summary-received-email.json @@ -1,8 +1,8 @@ { - "subject": "Saabus arsti kokkuvõtte tellimusele {{orderNr}}", - "previewText": "Arst on saatnud kokkuvõtte sinu analüüsitulemustele.", - "hello": "Tere, {{displayName}}", - "summaryReceivedForOrder": "Arst on koostanud selgitava kokkuvõtte sinu tellitud analüüsidele.", - "linkText": "Vaata kokkuvõtet", - "ifButtonDisabled": "Kui nupule vajutamine ei toimi, kopeeri see link oma brauserisse:" + "subject": "Arsti kokkuvõte on saabunud", + "previewText": "Arst on koostanud kokkuvõte analüüsitulemustele.", + "p1": "Arsti kokkuvõte on saabunud:", + "p2": "Põhjalikul terviseuuringul on soovituslik käia regulaarselt, aga vähemalt üks kord aastas, kui soovite säilitada aktiivset ja täisväärtuslikku elustiili.", + "p3": "MedReport aitab lihtsalt, mugavalt ja kiirelt terviseandmeid ühest kohast vaadata ning tellida terviseuuringuid.", + "p4": "SYNLAB klienditoe telefon: 17123" } \ No newline at end of file diff --git a/packages/email-templates/src/locales/et/order-processing-email.json b/packages/email-templates/src/locales/et/order-processing-email.json new file mode 100644 index 0000000..a9e57c0 --- /dev/null +++ b/packages/email-templates/src/locales/et/order-processing-email.json @@ -0,0 +1,13 @@ +{ + "subject": "Saatekiri on saadetud laborisse. Palun mine proove andma.", + "heading": "Täname tellimuse eest!", + "previewText": "Saatekiri uuringute tegemiseks on saadetud laborisse.", + "p1": "Saatekiri uuringute tegemiseks on saadetud laborisse digitaalselt. Palun mine proove andma: {{partnerLocation}}.", + "p2": "Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - vaata asukohti ja lahtiolekuaegasid.", + "p3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).", + "p4": "Proovivõtupunktis valige järjekorrasüsteemis: saatekirjad alt eriarsti saatekiri", + "p5": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.", + "p6": "SYNLAB klienditoe telefon: 17123", + "p1Urine": "Analüüsides on ette nähtud uriinianalüüs. Uriinianalüüsiks võta hommikune esmane uriin.", + "p2Urine": "Proovitopsi võib soetada apteegist ja analüüsi kaasa võtta (teostada protseduur kodus) või küsida proovivõtupunktist (teostada protseduur proovipunkti wc-s)." +} \ No newline at end of file diff --git a/packages/email-templates/src/locales/et/patient-first-results-received-email.json b/packages/email-templates/src/locales/et/patient-first-results-received-email.json new file mode 100644 index 0000000..7d87e78 --- /dev/null +++ b/packages/email-templates/src/locales/et/patient-first-results-received-email.json @@ -0,0 +1,8 @@ +{ + "subject": "Saabusid tellitud uuringute esimesed tulemused", + "previewText": "Esimesed uuringute tulemused on saabunud.", + "p1": "Esimesed uuringute tulemused on saabunud:", + "p2": "Saadame järgmise teavituse, kui kõik uuringute vastused on saabunud süsteemi.", + "p3": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.", + "p4": "SYNLAB klienditoe telefon: 17123" +} \ No newline at end of file diff --git a/packages/email-templates/src/locales/et/patient-full-results-received-email.json b/packages/email-templates/src/locales/et/patient-full-results-received-email.json new file mode 100644 index 0000000..4a1de1a --- /dev/null +++ b/packages/email-templates/src/locales/et/patient-full-results-received-email.json @@ -0,0 +1,7 @@ +{ + "subject": "Kõikide tellitud uuringute tulemused on saabunud. Ootab arsti kokkuvõtet.", + "previewText": "Kõikide uuringute tulemused on saabunud.", + "p1": "Kõikide uuringute tulemused on saabunud:", + "p2": "Saadame järgmise teavituse kui arsti kokkuvõte on koostatud.", + "p3": "SYNLAB klienditoe telefon: 17123" +} \ No newline at end of file diff --git a/packages/email-templates/src/locales/ru/doctor-summary-received-email.json b/packages/email-templates/src/locales/ru/doctor-summary-received-email.json index 09beb43..e233f55 100644 --- a/packages/email-templates/src/locales/ru/doctor-summary-received-email.json +++ b/packages/email-templates/src/locales/ru/doctor-summary-received-email.json @@ -1,8 +1,8 @@ { - "subject": "Получено заключение врача по заказу {{orderNr}}", - "previewText": "Врач отправил заключение по вашим результатам анализа.", - "hello": "Здравствуйте, {{displayName}}", - "summaryReceivedForOrder": "Врач подготовил пояснительное заключение по заказанным вами анализам.", - "linkText": "Посмотреть заключение", - "ifButtonDisabled": "Если кнопка не работает, скопируйте эту ссылку в ваш браузер:" + "subject": "Заключение врача готово", + "previewText": "Врач подготовил заключение по результатам анализов.", + "p1": "Заключение врача готово:", + "p2": "Рекомендуется проходить комплексное обследование регулярно, но как минимум один раз в год, если вы хотите сохранить активный и полноценный образ жизни.", + "p3": "MedReport позволяет легко, удобно и быстро просматривать медицинские данные в одном месте и заказывать обследования.", + "p4": "Телефон службы поддержки SYNLAB: 17123" } \ No newline at end of file diff --git a/packages/email-templates/src/locales/ru/order-processing-email.json b/packages/email-templates/src/locales/ru/order-processing-email.json new file mode 100644 index 0000000..3a5d6ac --- /dev/null +++ b/packages/email-templates/src/locales/ru/order-processing-email.json @@ -0,0 +1,13 @@ +{ + "subject": "Направление отправлено в лабораторию. Пожалуйста, сдайте анализы.", + "heading": "Спасибо за заказ!", + "previewText": "Направление на обследование отправлено в лабораторию.", + "p1": "Направление на обследование было отправлено в лабораторию в цифровом виде. Пожалуйста, сдайте анализы: {{partnerLocation}}.", + "p2": "Если у вас нет возможности прийти в выбранный пункт сдачи анализов, вы можете обратиться в любой удобный для вас пункт – посмотреть адреса и часы работы.", + "p3": "Рекомендуется сдавать анализы утром (до 12:00) натощак, без еды и напитков (разрешается пить воду).", + "p4": "В пункте сдачи анализов выберите в системе очереди: в разделе направлениянаправление от специалиста.", + "p5": "Если у вас возникли дополнительные вопросы, пожалуйста, свяжитесь с нами.", + "p6": "Телефон службы поддержки SYNLAB: 17123", + "p1Urine": "В обследование входит анализ мочи. Для анализа необходимо собрать первую утреннюю мочу.", + "p2Urine": "Контейнер можно приобрести в аптеке и принести образец с собой (процедура проводится дома) или взять контейнер в пункте сдачи (процедура проводится в туалете пункта)." +} diff --git a/packages/email-templates/src/locales/ru/patient-first-results-received-email.json b/packages/email-templates/src/locales/ru/patient-first-results-received-email.json new file mode 100644 index 0000000..975934f --- /dev/null +++ b/packages/email-templates/src/locales/ru/patient-first-results-received-email.json @@ -0,0 +1,8 @@ +{ + "subject": "Поступили первые результаты заказанных исследований", + "previewText": "Первые результаты исследований поступили.", + "p1": "Первые результаты исследований поступили:", + "p2": "Мы отправим следующее уведомление, когда все результаты исследований будут получены в системе.", + "p3": "Если у вас возникнут дополнительные вопросы, пожалуйста, свяжитесь с нами.", + "p4": "Телефон службы поддержки SYNLAB: 17123" +} diff --git a/packages/email-templates/src/locales/ru/patient-full-results-received-email.json b/packages/email-templates/src/locales/ru/patient-full-results-received-email.json new file mode 100644 index 0000000..e47f161 --- /dev/null +++ b/packages/email-templates/src/locales/ru/patient-full-results-received-email.json @@ -0,0 +1,7 @@ +{ + "subject": "Все заказанные результаты исследований поступили. Ожидается заключение врача.", + "previewText": "Все результаты исследований поступили.", + "p1": "Все результаты исследований поступили:", + "p2": "Мы отправим следующее уведомление, когда заключение врача будет подготовлено.", + "p3": "Телефон службы поддержки SYNLAB: 17123" +} \ No newline at end of file diff --git a/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts b/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts index 4553578..610b70a 100644 --- a/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts +++ b/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts @@ -126,7 +126,7 @@ export const giveFeedbackAction = doctorAction( if (isCompleted) { await createNotificationLog({ - action: NotificationAction.DOCTOR_FEEDBACK_RECEIVED, + action: NotificationAction.PATIENT_DOCTOR_FEEDBACK_RECEIVED, status: 'SUCCESS', relatedRecordId: analysisOrderId, }); @@ -136,7 +136,7 @@ export const giveFeedbackAction = doctorAction( } catch (e: any) { if (isCompleted) { await createNotificationLog({ - action: NotificationAction.DOCTOR_FEEDBACK_RECEIVED, + action: NotificationAction.PATIENT_DOCTOR_FEEDBACK_RECEIVED, status: 'FAIL', comment: e?.message, relatedRecordId: analysisOrderId, diff --git a/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema.ts b/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema.ts index 329d846..db8e2be 100644 --- a/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema.ts +++ b/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema.ts @@ -1,9 +1,9 @@ -import z from 'zod/v3'; import { Database } from '@kit/supabase/database'; +import z from 'zod'; export const doctorJobSelectSchema = z.object({ - userId: z.string().uuid(), + userId: z.uuid(), analysisOrderId: z.number(), }); export type DoctorJobSelect = z.infer; diff --git a/packages/features/notifications/package.json b/packages/features/notifications/package.json index 5355d69..df31c57 100644 --- a/packages/features/notifications/package.json +++ b/packages/features/notifications/package.json @@ -11,7 +11,8 @@ "exports": { "./api": "./src/server/api.ts", "./components": "./src/components/index.ts", - "./hooks": "./src/hooks/index.ts" + "./hooks": "./src/hooks/index.ts", + "./webhooks/*": "./src/server/services/webhooks/*.ts" }, "devDependencies": { "@kit/eslint-config": "workspace:*", diff --git a/packages/features/notifications/src/server/services/webhooks/analysis-order-notifications.service.ts b/packages/features/notifications/src/server/services/webhooks/analysis-order-notifications.service.ts new file mode 100644 index 0000000..643b1b7 --- /dev/null +++ b/packages/features/notifications/src/server/services/webhooks/analysis-order-notifications.service.ts @@ -0,0 +1,273 @@ +import { + renderAllResultsReceivedEmail, + renderFirstResultsReceivedEmail, + renderOrderProcessingEmail, + renderPatientFirstResultsReceivedEmail, + renderPatientFullResultsReceivedEmail, +} from '@kit/email-templates'; +import { getLogger } from '@kit/shared/logger'; +import { getFullName } from '@kit/shared/utils'; +import { Database } from '@kit/supabase/database'; +import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; + +import { + getAssignedDoctorAccount, + getDoctorAccounts, + getUserContactAdmin, +} from '~/lib/services/account.service'; +import { + NotificationAction, + createNotificationLog, +} from '~/lib/services/audit/notificationEntries.service'; +import { + EmailRenderer, + sendEmailFromTemplate, +} from '~/lib/services/mailer.service'; + +type AnalysisOrder = Database['medreport']['Tables']['analysis_orders']['Row']; + +export function createAnalysisOrderWebhooksService() { + return new AnalysisOrderWebhooksService(); +} + +class AnalysisOrderWebhooksService { + private readonly namespace = 'analysis_orders.webhooks'; + + async handleStatusChangeWebhook(analysisOrder: AnalysisOrder) { + const logger = await getLogger(); + + const ctx = { + analysisOrderId: analysisOrder.id, + namespace: this.namespace, + }; + + logger.info(ctx, 'Received status change update. Processing...'); + let actions: NotificationAction[] = []; + try { + if (analysisOrder.status === 'PROCESSING') { + actions = [NotificationAction.PATIENT_ORDER_PROCESSING]; + await this.sendProcessingNotification(analysisOrder); + } + + if (analysisOrder.status === 'PARTIAL_ANALYSIS_RESPONSE') { + actions = [ + NotificationAction.PATIENT_FIRST_RESULTS_RECEIVED, + NotificationAction.DOCTOR_NEW_JOBS, + ]; + + await this.sendPartialAnalysisResultsNotifications(analysisOrder); + } + + if (analysisOrder.status === 'FULL_ANALYSIS_RESPONSE') { + actions = [ + NotificationAction.DOCTOR_PATIENT_RESULTS_RECEIVED, + NotificationAction.PATIENT_FULL_RESULTS_RECEIVED, + ]; + await this.sendFullAnalysisResultsNotifications(analysisOrder); + } + + if (actions.length) { + return logger.info(ctx, 'Status change notifications sent.'); + } + + logger.info(ctx, 'Status change processed. No notifications to send.'); + } catch (e: any) { + if (actions.length) + await Promise.all( + actions.map((action) => + createNotificationLog({ + action, + status: 'FAIL', + comment: e?.message, + relatedRecordId: analysisOrder.id, + }), + ), + ); + logger.error( + ctx, + `Error while processing status change: ${JSON.stringify(e)}`, + ); + } + } + + async sendProcessingNotification(analysisOrder: AnalysisOrder) { + const logger = await getLogger(); + const supabase = getSupabaseServerAdminClient(); + + const userContact = await getUserContactAdmin(analysisOrder.user_id); + + if (!userContact?.email) { + await createNotificationLog({ + action: NotificationAction.PATIENT_ORDER_PROCESSING, + status: 'FAIL', + comment: 'No email found for ' + analysisOrder.user_id, + relatedRecordId: analysisOrder.id, + }); + logger.warn( + { analysisOrderId: analysisOrder.id, namespace: this.namespace }, + 'No email found ', + ); + return; + } + + const [{ data: medusaOrder }, { data: analysisElements }] = + await Promise.all([ + supabase + .from('order') + .select('id,metadata') + .eq('id', analysisOrder.medusa_order_id) + .single() + .throwOnError(), + supabase + .schema('medreport') + .from('analysis_elements') + .select('materialGroups:material_groups') + .in('id', analysisOrder.analysis_element_ids ?? []) + .throwOnError(), + ]); + + let isUrine = false; + for (const analysisElement of analysisElements ?? []) { + logger.info({ group: analysisElement.materialGroups ?? [] }); + + const containsUrineSample = (analysisElement.materialGroups ?? [])?.some( + (element) => + (element as { Materjal?: { MaterjaliNimi: string } })?.Materjal + ?.MaterjaliNimi === 'Uriin', + ); + + if (containsUrineSample) { + isUrine = true; + break; + } + } + + const orderMetadata = medusaOrder.metadata as { + partner_location_name?: string; + }; + + await sendEmailFromTemplate( + renderOrderProcessingEmail, + { + language: userContact.preferred_locale ?? 'et', + recipientName: getFullName(userContact.name, userContact.last_name), + partnerLocation: orderMetadata.partner_location_name ?? 'SYNLAB', + isUrine, + }, + userContact.email, + ); + + return createNotificationLog({ + action: NotificationAction.PATIENT_ORDER_PROCESSING, + status: 'SUCCESS', + relatedRecordId: analysisOrder.id, + }); + } + + async sendPatientUpdateNotification( + analysisOrder: AnalysisOrder, + template: EmailRenderer, + action: NotificationAction, + ) { + const logger = await getLogger(); + + const userContact = await getUserContactAdmin(analysisOrder.user_id); + + if (userContact?.email) { + await sendEmailFromTemplate( + template, + { + analysisOrderId: analysisOrder.id, + recipientName: getFullName(userContact.name, userContact.last_name), + language: userContact.preferred_locale ?? 'et', + }, + userContact.email, + ); + await createNotificationLog({ + action, + status: 'SUCCESS', + relatedRecordId: analysisOrder.id, + }); + logger.info( + { analysisOrderId: analysisOrder.id, namespace: this.namespace }, + 'Sent notification email', + ); + } else { + await createNotificationLog({ + action, + status: 'FAIL', + comment: 'No email found for ' + analysisOrder.user_id, + relatedRecordId: analysisOrder.id, + }); + logger.warn( + { analysisOrderId: analysisOrder.id, namespace: this.namespace }, + 'No email found ', + ); + } + } + + async sendPartialAnalysisResultsNotifications(analysisOrder: AnalysisOrder) { + const logger = await getLogger(); + + await this.sendPatientUpdateNotification( + analysisOrder, + renderPatientFirstResultsReceivedEmail, + NotificationAction.PATIENT_FIRST_RESULTS_RECEIVED, + ); + + const doctorAccounts = await getDoctorAccounts(); + const doctorEmails: string[] = doctorAccounts + .map(({ email }) => email) + .filter((email): email is string => !!email); + + await sendEmailFromTemplate( + renderFirstResultsReceivedEmail, + { + analysisOrderId: analysisOrder.id, + language: 'et', + }, + doctorEmails, + ); + + logger.info( + { analysisOrderId: analysisOrder.id, namespace: this.namespace }, + 'Sent out partial analysis results notifications for doctors', + ); + + await createNotificationLog({ + action: NotificationAction.DOCTOR_NEW_JOBS, + status: 'SUCCESS', + relatedRecordId: analysisOrder.id, + }); + } + + async sendFullAnalysisResultsNotifications(analysisOrder: AnalysisOrder) { + await this.sendPatientUpdateNotification( + analysisOrder, + renderPatientFullResultsReceivedEmail, + NotificationAction.PATIENT_FULL_RESULTS_RECEIVED, + ); + + const doctorAccount = await getAssignedDoctorAccount(analysisOrder.id); + const assignedDoctorEmail = doctorAccount?.email; + + if (!assignedDoctorEmail) { + return; + } + + await sendEmailFromTemplate( + renderAllResultsReceivedEmail, + { + analysisOrderId: analysisOrder.id, + language: 'et', + }, + assignedDoctorEmail, + ); + + return createNotificationLog({ + action: NotificationAction.DOCTOR_PATIENT_RESULTS_RECEIVED, + status: 'SUCCESS', + relatedRecordId: analysisOrder.id, + }); + } +}