From ce01f9a4c924c573fcafec55d2a40560114be327 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Fri, 10 Oct 2025 14:19:27 +0300 Subject: [PATCH 1/6] MED-198: send notification if new responses --- app/api/job/handler/sync-analysis-results.ts | 11 ++++++ .../medipostPrivateMessage.service.ts | 18 +-------- .../features/user-analyses/src/server/api.ts | 37 +++++++++++++++++++ packages/features/user-analyses/tsconfig.json | 7 +--- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/app/api/job/handler/sync-analysis-results.ts b/app/api/job/handler/sync-analysis-results.ts index b631045..3be7639 100644 --- a/app/api/job/handler/sync-analysis-results.ts +++ b/app/api/job/handler/sync-analysis-results.ts @@ -1,3 +1,6 @@ +import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; + import { readPrivateMessageResponse } from '~/lib/services/medipost/medipostPrivateMessage.service'; type ProcessedMessage = { @@ -16,6 +19,8 @@ type GroupedResults = { export default async function syncAnalysisResults() { console.info('Syncing analysis results'); + const supabase = getSupabaseServerClient(); + const api = createUserAnalysesApi(supabase); const processedMessages: ProcessedMessage[] = []; const excludedMessageIds: string[] = []; @@ -25,6 +30,12 @@ export default async function syncAnalysisResults() { processedMessages.push(result as ProcessedMessage); } + await api.sendAnalysisResultsNotification({ + hasFullAnalysisResponse: result.hasFullAnalysisResponse, + hasPartialAnalysisResponse: result.hasAnalysisResponse, + analysisOrderId: result.analysisOrderId, + }); + if (!result.messageId) { console.info('No more messages to process'); break; diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index 92e8e5d..87dbc77 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -3,9 +3,7 @@ import type { PostgrestError } from '@supabase/supabase-js'; import { GetMessageListResponse, MedipostAction } from '@/lib/types/medipost'; -import { createNotificationsApi } from '@/packages/features/notifications/src/server/api'; import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; -import { pathsConfig } from '@/packages/shared/src/config'; import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; import type { MedipostOrderResponse, @@ -18,7 +16,6 @@ import axios from 'axios'; import { toArray } from '@kit/shared/utils'; import { Tables } from '@kit/supabase/database'; -import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element'; import type { AnalysisOrder } from '~/lib/types/order'; @@ -123,7 +120,7 @@ export async function canCreateAnalysisResponseElement({ if (existingAnalysisResponseElement.response_value && !responseValue) { log( - `Analysis response element id=${analysisElementOriginalId} already exists for order with response value ${existingAnalysisResponseElement.response_value} but new response has no value`, + `Analysis response element id=${analysisElementOriginalId} ${existingAnalysisResponseElement.response_value} but new response has no value`, ); return false; } @@ -271,7 +268,6 @@ export async function syncPrivateMessage({ order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; }) { const supabase = getSupabaseServerAdminClient(); - const { t } = await createI18nServerInstance(); const orderStatus = AnalysisOrderStatus[TellimuseOlek]; @@ -304,7 +300,6 @@ export async function syncPrivateMessage({ log, }); - let newElementsAdded = 0; for (const element of newElements) { try { await upsertAnalysisResponseElement({ @@ -313,7 +308,6 @@ export async function syncPrivateMessage({ analysis_response_id: analysisResponseId, }, }); - newElementsAdded++; } catch (e) { log( `Failed to create order response element for response id ${analysisResponseId}, element id '${element.analysis_element_original_id}' (order id: ${order.id})`, @@ -322,16 +316,6 @@ export async function syncPrivateMessage({ } } - log(`Added ${newElementsAdded} new elements`); - - if (newElementsAdded !== 0) { - await createNotificationsApi(supabase).createNotification({ - account_id: analysisOrder.user_id, - body: t('analysis-results:notification.body'), - link: `${pathsConfig.app.analysisResults}/${order.id}`, - }); - } - return (await hasAllAnalysisResponseElements({ analysisResponseId, order })) ? { isCompleted: orderStatus === 'COMPLETED' } : { isPartial: true }; diff --git a/packages/features/user-analyses/src/server/api.ts b/packages/features/user-analyses/src/server/api.ts index fa738ec..463f2c8 100644 --- a/packages/features/user-analyses/src/server/api.ts +++ b/packages/features/user-analyses/src/server/api.ts @@ -1,10 +1,14 @@ import { SupabaseClient } from '@supabase/supabase-js'; +import { createNotificationsApi } from '@kit/notifications/api'; +import { pathsConfig } from '@kit/shared/config'; import { getLogger } from '@kit/shared/logger'; import type { UuringuVastus } from '@kit/shared/types/medipost-analysis'; import { toArray } from '@kit/shared/utils'; import { Database } from '@kit/supabase/database'; +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; + import type { AnalysisOrder, AnalysisOrderStatus, @@ -488,6 +492,39 @@ class UserAnalysesApi { }) .throwOnError(); } + + async sendAnalysisResultsNotification({ + hasFullAnalysisResponse, + hasPartialAnalysisResponse, + analysisOrderId, + }: { + hasFullAnalysisResponse: boolean; + hasPartialAnalysisResponse: boolean; + analysisOrderId?: number; + }) { + if (!analysisOrderId) { + return; + } + const { data, error: userError } = await this.client.auth.getUser(); + if (userError) { + throw userError; + } + const { user } = data; + const notificationsApi = createNotificationsApi(this.client); + const { t } = await createI18nServerInstance(); + + console.info( + `Order ${analysisOrderId} got new responses -> Sending new notification`, + ); + + if (hasFullAnalysisResponse || hasPartialAnalysisResponse) { + await notificationsApi.createNotification({ + account_id: user.id, + body: t('analysis-results:notification.body'), + link: `${pathsConfig.app.analysisResults}/${analysisOrderId}`, + }); + } + } } export function createUserAnalysesApi(client: SupabaseClient) { diff --git a/packages/features/user-analyses/tsconfig.json b/packages/features/user-analyses/tsconfig.json index 8d5bae9..e2b4bf5 100644 --- a/packages/features/user-analyses/tsconfig.json +++ b/packages/features/user-analyses/tsconfig.json @@ -1,10 +1,7 @@ { - "extends": "@kit/tsconfig/base.json", + "extends": "../../../tsconfig.json", "compilerOptions": { - "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", - "paths": { - "~/lib/utils": ["../../../lib/utils.ts"] - } + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, "include": ["*.ts", "*.tsx", "src"], "exclude": ["node_modules"] From e84541339d201f85092f7af5a655b3be697b3c64 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Fri, 10 Oct 2025 14:42:41 +0300 Subject: [PATCH 2/6] only send not completed jobs --- app/api/job/handler/send-open-jobs-emails.ts | 4 ++++ lib/services/doctor-jobs.service.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/app/api/job/handler/send-open-jobs-emails.ts b/app/api/job/handler/send-open-jobs-emails.ts index 9f09e12..83ea959 100644 --- a/app/api/job/handler/send-open-jobs-emails.ts +++ b/app/api/job/handler/send-open-jobs-emails.ts @@ -7,6 +7,10 @@ import { sendEmailFromTemplate } from '~/lib/services/mailer.service'; export default async function sendOpenJobsEmails() { const analysisResponseIds = await getOpenJobAnalysisResponseIds(); + if (analysisResponseIds.length === 0) { + return; + } + const doctorAccounts = await getDoctorAccounts(); const doctorEmails: string[] = doctorAccounts .map(({ email }) => email) diff --git a/lib/services/doctor-jobs.service.ts b/lib/services/doctor-jobs.service.ts index 2f61c00..db9a34e 100644 --- a/lib/services/doctor-jobs.service.ts +++ b/lib/services/doctor-jobs.service.ts @@ -7,6 +7,7 @@ async function getAssignedOrderIds() { .schema('medreport') .from('doctor_analysis_feedback') .select('analysis_order_id') + .not('status', 'is', 'COMPLETED') .not('doctor_user_id', 'is', null) .throwOnError(); From 2a3f7248a1feab89d6a1fe698fd530c5cef5723e Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Fri, 10 Oct 2025 14:51:05 +0300 Subject: [PATCH 3/6] show ON_HOLD responses, but dont show if they have no responses --- .../src/lib/server/services/doctor-analysis.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts index 8068a32..23f355d 100644 --- a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts +++ b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts @@ -58,6 +58,11 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) { .in('primary_owner_user_id', userIds), ]); + if (!analysisResponseElements || analysisResponseElements?.length === 0) { + console.info(`${analysisResponseIds} has no response elements`); + return []; + } + const doctorUserIds = doctorFeedbackItems ?.map((item) => item.doctor_user_id) @@ -285,7 +290,6 @@ export async function getOpenResponses({ `, { count: 'exact' }, ) - .neq('order_status', 'ON_HOLD') .order('created_at', { ascending: false }); if (assignedIds.length > 0) { From 30e6e47cc7010454a1fd90f9343f8ba06d677731 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Fri, 10 Oct 2025 15:29:40 +0300 Subject: [PATCH 4/6] hasAccountTeamMembership should look at in user_id not account_id --- packages/features/accounts/src/server/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index b934e39..96366bd 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -214,7 +214,7 @@ class AccountsApi { .schema('medreport') .from('accounts_memberships') .select('account_id', { count: 'exact', head: true }) - .eq('account_id', accountId); + .eq('user_id', accountId); if (error) { throw error; From f74c5a2fc66a1794332197908b9eeda35d7134ee Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Fri, 10 Oct 2025 16:14:36 +0300 Subject: [PATCH 5/6] improve doctor email audit logs --- app/api/job/handler/send-open-jobs-emails.ts | 10 ++++++++-- app/api/job/send-open-jobs-emails/route.ts | 5 +++-- .../membership-confirmation-notification.tsx | 2 +- app/select-package/page.tsx | 1 + 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/api/job/handler/send-open-jobs-emails.ts b/app/api/job/handler/send-open-jobs-emails.ts index 83ea959..44d436e 100644 --- a/app/api/job/handler/send-open-jobs-emails.ts +++ b/app/api/job/handler/send-open-jobs-emails.ts @@ -12,9 +12,13 @@ export default async function sendOpenJobsEmails() { } const doctorAccounts = await getDoctorAccounts(); - const doctorEmails: string[] = doctorAccounts + const doctorEmails = doctorAccounts .map(({ email }) => email) - .filter((email): email is string => !!email); + .filter((email) => !!email); + + if (doctorEmails !== null) { + return []; + } await sendEmailFromTemplate( renderNewJobsAvailableEmail, @@ -24,4 +28,6 @@ export default async function sendOpenJobsEmails() { }, doctorEmails, ); + + return doctorAccounts.filter((email) => !!email).map(({ id }) => id); } diff --git a/app/api/job/send-open-jobs-emails/route.ts b/app/api/job/send-open-jobs-emails/route.ts index 0c22957..a18d3c6 100644 --- a/app/api/job/send-open-jobs-emails/route.ts +++ b/app/api/job/send-open-jobs-emails/route.ts @@ -19,13 +19,14 @@ export const POST = async (request: NextRequest) => { } try { - await sendOpenJobsEmails(); + const doctors = await sendOpenJobsEmails(); console.info( - 'Successfully sent out open job notification emails to doctors.', + 'Successfully sent out open job notification emails to doctors', ); await createNotificationLog({ action: NotificationAction.DOCTOR_NEW_JOBS, status: 'SUCCESS', + comment: `doctors that received email: ${doctors}`, }); return NextResponse.json( { diff --git a/app/auth/membership-confirmation/_components/membership-confirmation-notification.tsx b/app/auth/membership-confirmation/_components/membership-confirmation-notification.tsx index 86b8079..1931d21 100644 --- a/app/auth/membership-confirmation/_components/membership-confirmation-notification.tsx +++ b/app/auth/membership-confirmation/_components/membership-confirmation-notification.tsx @@ -25,7 +25,7 @@ const MembershipConfirmationNotification: React.FC<{ descriptionKey="account:membershipConfirmation:successDescription" buttonProps={{ buttonTitleKey: 'account:membershipConfirmation:successButton', - href: pathsConfig.app.home, + href: pathsConfig.app.selectPackage, }} /> ); diff --git a/app/select-package/page.tsx b/app/select-package/page.tsx index 532794f..bba9f72 100644 --- a/app/select-package/page.tsx +++ b/app/select-package/page.tsx @@ -48,6 +48,7 @@ async function SelectPackagePage() { } + countryCode={countryCode} /> Date: Fri, 10 Oct 2025 16:30:06 +0300 Subject: [PATCH 6/6] translations --- .../email-templates/src/locales/et/invite-email.json | 9 +++++++++ public/locales/et/cart.json | 6 +++--- public/locales/et/common.json | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 packages/email-templates/src/locales/et/invite-email.json diff --git a/packages/email-templates/src/locales/et/invite-email.json b/packages/email-templates/src/locales/et/invite-email.json new file mode 100644 index 0000000..1705d0f --- /dev/null +++ b/packages/email-templates/src/locales/et/invite-email.json @@ -0,0 +1,9 @@ +{ + "subject": "Teid on kutsutud tiimi", + "heading": "Liitu tiimiga {{teamName}}", + "hello": "Tere {{invitedUserEmail}},", + "mainText": "{{inviter}} on kutsunud teid ühinema tiimiga {{teamName}} platvormil {{productName}}.", + "joinTeam": "Liitu {{teamName}}", + "copyPasteLink": "või kopeeri ja kleebi see URL teie brauseris:", + "invitationIntendedFor": "See kutse on mõeldud {{invitedUserEmail}} omanikule." +} diff --git a/public/locales/et/cart.json b/public/locales/et/cart.json index ab43940..0120bc0 100644 --- a/public/locales/et/cart.json +++ b/public/locales/et/cart.json @@ -29,9 +29,9 @@ "title": "Kinkekaart või sooduskood", "label": "Lisa promo kood", "apply": "Rakenda", - "subtitle": "Kui soovid, võid lisada promo koodi", - "placeholder": "Sisesta promo kood", - "remove": "Eemalda promo kood", + "subtitle": "Kui soovid, võid lisada promokoodi", + "placeholder": "Sisesta promokood", + "remove": "Eemalda promokood", "appliedCodes": "Rakendatud sooduskoodid:", "removeError": "Sooduskoodi eemaldamine ebaõnnestus", "removeSuccess": "Sooduskood eemaldatud", diff --git a/public/locales/et/common.json b/public/locales/et/common.json index a7f5809..8615a16 100644 --- a/public/locales/et/common.json +++ b/public/locales/et/common.json @@ -63,7 +63,7 @@ "myActions": "Minu toimingud", "healthPackageComparison": { "label": "Tervisepakettide võrdlus", - "description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeks) põhjal tehtud personaalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde." + "description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeks) põhjal tehtud personaalne terviseauditi valik. Terviseuuringute paketile on võimalik lisada üksikuid uuringuid juurde." }, "routes": { "home": "Avaleht",