From 7d208b41f2adbfdeba904a495ebb97e9c7f8b931 Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 13:42:44 +0300 Subject: [PATCH 1/2] update naming to be clearer --- app/api/order/medipost-test-response/route.ts | 8 ++++---- .../(dashboard)/cart/montonio-callback/actions.ts | 4 ++-- .../(dashboard)/order/[orderId]/confirmed/page.tsx | 4 ++-- app/home/(user)/(dashboard)/order/[orderId]/page.tsx | 4 ++-- lib/services/medipost.service.ts | 12 ++++++------ lib/services/order.service.ts | 8 ++++---- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/api/order/medipost-test-response/route.ts b/app/api/order/medipost-test-response/route.ts index 2302631..9ce8c41 100644 --- a/app/api/order/medipost-test-response/route.ts +++ b/app/api/order/medipost-test-response/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { getOrder } from "~/lib/services/order.service"; +import { getAnalysisOrder } from "~/lib/services/order.service"; import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service"; import { retrieveOrder } from "@lib/data"; import { getAccountAdmin } from "~/lib/services/account.service"; @@ -14,9 +14,9 @@ export async function POST(request: Request) { const { medusaOrderId } = await request.json(); const medusaOrder = await retrieveOrder(medusaOrderId) - const medreportOrder = await getOrder({ medusaOrderId }); + const analysisOrder = await getAnalysisOrder({ medusaOrderId }); - const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); + const account = await getAccountAdmin({ primaryOwnerUserId: analysisOrder.user_id }); const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder }); console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`); @@ -30,7 +30,7 @@ export async function POST(request: Request) { orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[], orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[], orderId: medusaOrderId, - orderCreatedAt: new Date(medreportOrder.created_at), + orderCreatedAt: new Date(analysisOrder.created_at), }); try { diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index 11eca6f..def72dd 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -7,7 +7,7 @@ import { loadCurrentUserAccount } from "@/app/home/(user)/_lib/server/load-user- import { listProductTypes } from "@lib/data/products"; import { placeOrder, retrieveCart } from "@lib/data/cart"; import { createI18nServerInstance } from "~/lib/i18n/i18n.server"; -import { createOrder } from '~/lib/services/order.service'; +import { createAnalysisOrder } from '~/lib/services/order.service'; import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service'; import { createNotificationsApi } from '@kit/notifications/api'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; @@ -114,7 +114,7 @@ export async function processMontonioCallback(orderToken: string) { const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false }); const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder }); - const orderId = await createOrder({ medusaOrder, orderedAnalysisElements }); + const orderId = await createAnalysisOrder({ medusaOrder, orderedAnalysisElements }); const { productTypes } = await listProductTypes(); const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE); diff --git a/app/home/(user)/(dashboard)/order/[orderId]/confirmed/page.tsx b/app/home/(user)/(dashboard)/order/[orderId]/confirmed/page.tsx index 21e1829..d72c530 100644 --- a/app/home/(user)/(dashboard)/order/[orderId]/confirmed/page.tsx +++ b/app/home/(user)/(dashboard)/order/[orderId]/confirmed/page.tsx @@ -4,7 +4,7 @@ import { PageBody, PageHeader } from '@kit/ui/page'; import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { getOrder } from '~/lib/services/order.service'; +import { getAnalysisOrder } from '~/lib/services/order.service'; import { retrieveOrder } from '@lib/data/orders'; import { pathsConfig } from '@kit/shared/config'; @@ -27,7 +27,7 @@ async function OrderConfirmedPage(props: { }) { const params = await props.params; - const order = await getOrder({ orderId: Number(params.orderId) }).catch(() => null); + const order = await getAnalysisOrder({ orderId: Number(params.orderId) }).catch(() => null); if (!order) { redirect(pathsConfig.app.myOrders); } diff --git a/app/home/(user)/(dashboard)/order/[orderId]/page.tsx b/app/home/(user)/(dashboard)/order/[orderId]/page.tsx index 4b717d5..c84fb99 100644 --- a/app/home/(user)/(dashboard)/order/[orderId]/page.tsx +++ b/app/home/(user)/(dashboard)/order/[orderId]/page.tsx @@ -4,7 +4,7 @@ import { PageBody, PageHeader } from '@kit/ui/page'; import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { getOrder } from '~/lib/services/order.service'; +import { getAnalysisOrder } from '~/lib/services/order.service'; import { retrieveOrder } from '@lib/data/orders'; import { pathsConfig } from '@kit/shared/config'; @@ -27,7 +27,7 @@ async function OrderConfirmedPage(props: { }) { const params = await props.params; - const order = await getOrder({ orderId: Number(params.orderId) }).catch(() => null); + const order = await getAnalysisOrder({ orderId: Number(params.orderId) }).catch(() => null); if (!order) { redirect(pathsConfig.app.myOrders); } diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index 82db51d..a096083 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -24,7 +24,7 @@ import { XMLParser } from 'fast-xml-parser'; import { Tables } from '@kit/supabase/database'; import { createAnalysisGroup } from './analysis-group.service'; import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; -import { getOrder, updateOrderStatus } from './order.service'; +import { getAnalysisOrder, updateAnalysisOrderStatus } from './order.service'; import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service'; import { getAnalyses } from './analyses.service'; import { getAccountAdmin } from './account.service'; @@ -242,7 +242,7 @@ export async function readPrivateMessageResponse({ let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; try { - order = await getOrder({ medusaOrderId }); + order = await getAnalysisOrder({ medusaOrderId }); } catch (e) { await deletePrivateMessage(privateMessage.messageId); throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); @@ -251,11 +251,11 @@ export async function readPrivateMessageResponse({ const status = await syncPrivateMessage({ messageResponse, order }); if (status.isPartial) { - await updateOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); + await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); hasAnalysisResponse = true; hasPartialAnalysisResponse = true; } else if (status.isCompleted) { - await updateOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); + await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); await deletePrivateMessage(privateMessage.messageId); hasAnalysisResponse = true; hasFullAnalysisResponse = true; @@ -588,7 +588,7 @@ export async function sendOrderToMedipost({ medusaOrderId: string; orderedAnalysisElements: OrderedAnalysisElement[]; }) { - const medreportOrder = await getOrder({ medusaOrderId }); + const medreportOrder = await getAnalysisOrder({ medusaOrderId }); const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); const orderedAnalysesIds = orderedAnalysisElements @@ -668,7 +668,7 @@ export async function sendOrderToMedipost({ hasAnalysisResults: false, medusaOrderId, }); - await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); + await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); } export async function getOrderedAnalysisIds({ diff --git a/lib/services/order.service.ts b/lib/services/order.service.ts index 487153a..f1eae85 100644 --- a/lib/services/order.service.ts +++ b/lib/services/order.service.ts @@ -5,7 +5,7 @@ import type { StoreOrder } from '@medusajs/types'; export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>; -export async function createOrder({ +export async function createAnalysisOrder({ medusaOrder, orderedAnalysisElements, }: { @@ -38,7 +38,7 @@ export async function createOrder({ return orderResult.data.id; } -export async function updateOrder({ +export async function updateAnalysisOrder({ orderId, orderStatus, }: { @@ -56,7 +56,7 @@ export async function updateOrder({ .throwOnError(); } -export async function updateOrderStatus({ +export async function updateAnalysisOrderStatus({ orderId, medusaOrderId, orderStatus, @@ -80,7 +80,7 @@ export async function updateOrderStatus({ .throwOnError(); } -export async function getOrder({ +export async function getAnalysisOrder({ medusaOrderId, orderId, }: { From 165d44b13fed45ee39d00ae1cd4c562077c53208 Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 14:02:34 +0300 Subject: [PATCH 2/2] prepare montonio callback logic to send email for individual analysis order - skip confusing error log for orders without analysis packages --- .../cart/montonio-callback/actions.ts | 141 +++++++++++++----- 1 file changed, 102 insertions(+), 39 deletions(-) diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index def72dd..89c8dc3 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -7,13 +7,15 @@ import { loadCurrentUserAccount } from "@/app/home/(user)/_lib/server/load-user- import { listProductTypes } from "@lib/data/products"; import { placeOrder, retrieveCart } from "@lib/data/cart"; import { createI18nServerInstance } from "~/lib/i18n/i18n.server"; -import { createAnalysisOrder } from '~/lib/services/order.service'; +import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service'; import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service'; import { createNotificationsApi } from '@kit/notifications/api'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; -import { AccountWithParams } from '@kit/accounts/api'; +import type { AccountWithParams } from '@kit/accounts/api'; +import type { StoreOrder } from '@medusajs/types'; const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages'; +const ANALYSIS_TYPE_HANDLE = 'synlab-analysis'; const MONTONIO_PAID_STATUS = 'PAID'; const env = () => z @@ -38,14 +40,12 @@ const sendEmail = async ({ account, email, analysisPackageName, - personName, partnerLocationName, language, }: { - account: AccountWithParams, + account: Pick, email: string, analysisPackageName: string, - personName: string, partnerLocationName: string, language: string, }) => { @@ -58,7 +58,7 @@ const sendEmail = async ({ const { html, subject } = await renderSynlabAnalysisPackageEmail({ analysisPackageName, - personName, + personName: account.name, partnerLocationName, language, }); @@ -83,9 +83,7 @@ const sendEmail = async ({ } } -export async function processMontonioCallback(orderToken: string) { - const { language } = await createI18nServerInstance(); - +async function decodeOrderToken(orderToken: string) { const secretKey = process.env.MONTONIO_SECRET_KEY as string; const decoded = jwt.verify(orderToken, secretKey, { @@ -96,50 +94,115 @@ export async function processMontonioCallback(orderToken: string) { throw new Error("Payment not successful"); } + return decoded; +} + +async function getCartByOrderToken(decoded: MontonioOrderToken) { + const [, , cartId] = decoded.merchantReferenceDisplay.split(':'); + if (!cartId) { + throw new Error("Cart ID not found"); + } + const cart = await retrieveCart(cartId); + if (!cart) { + throw new Error("Cart not found"); + } + return cart; +} + +async function getOrderResultParameters(medusaOrder: StoreOrder) { + const { productTypes } = await listProductTypes(); + const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE); + const analysisType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_TYPE_HANDLE); + + const analysisPackageOrderItem = medusaOrder.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id); + const analysisItems = medusaOrder.items?.filter(({ product_type_id }) => product_type_id === analysisType?.id); + + return { + medusaOrderId: medusaOrder.id, + email: medusaOrder.email, + analysisPackageOrder: analysisPackageOrderItem + ? { + partnerLocationName: analysisPackageOrderItem?.metadata?.partner_location_name as string ?? '', + analysisPackageName: analysisPackageOrderItem?.title ?? '', + } + : null, + analysisItemsOrder: Array.isArray(analysisItems) && analysisItems.length > 0 + ? analysisItems.map(({ product }) => ({ + analysisName: product?.title ?? '', + analysisId: product?.metadata?.analysisIdOriginal as string ?? '', + })) + : null, + }; +} + +async function sendAnalysisPackageOrderEmail({ + account, + email, + analysisPackageOrder, +}: { + account: AccountWithParams, + email: string, + analysisPackageOrder: { + partnerLocationName: string, + analysisPackageName: string, + }, +}) { + const { language } = await createI18nServerInstance(); + const { analysisPackageName, partnerLocationName } = analysisPackageOrder; + try { + await sendEmail({ + account: { id: account.id, name: account.name }, + email, + analysisPackageName, + partnerLocationName, + language, + }); + } catch (error) { + console.error("Failed to send email", error); + } +} + +export async function processMontonioCallback(orderToken: string) { const account = await loadCurrentUserAccount(); if (!account) { throw new Error("Account not found in context"); } try { - const [, , cartId] = decoded.merchantReferenceDisplay.split(':'); - if (!cartId) { - throw new Error("Cart ID not found"); - } + const decoded = await decodeOrderToken(orderToken); + const cart = await getCartByOrderToken(decoded); - const cart = await retrieveCart(cartId); - if (!cart) { - throw new Error("Cart not found"); - } - - const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false }); + const medusaOrder = await placeOrder(cart.id, { revalidateCacheTags: false }); const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder }); + + try { + const existingAnalysisOrder = await getAnalysisOrder({ medusaOrderId: medusaOrder.id }); + console.info(`Analysis order already exists for medusaOrderId=${medusaOrder.id}, orderId=${existingAnalysisOrder.id}`); + return { success: true, orderId: existingAnalysisOrder.id }; + } catch { + // ignored + } + const orderId = await createAnalysisOrder({ medusaOrder, orderedAnalysisElements }); + const orderResult = await getOrderResultParameters(medusaOrder); - const { productTypes } = await listProductTypes(); - const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE); - const analysisPackageOrderItem = medusaOrder.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id); + const { medusaOrderId, email, analysisPackageOrder, analysisItemsOrder } = orderResult; - const orderResult = { - medusaOrderId: medusaOrder.id, - email: medusaOrder.email, - partnerLocationName: analysisPackageOrderItem?.metadata?.partner_location_name as string ?? '', - analysisPackageName: analysisPackageOrderItem?.title ?? '', - orderedAnalysisElements, - }; + if (email) { + if (analysisPackageOrder) { + await sendAnalysisPackageOrderEmail({ account, email, analysisPackageOrder }); + } else { + console.info(`Order has no analysis package, skipping email.`); + } - const { medusaOrderId, email, partnerLocationName, analysisPackageName } = orderResult; - const personName = account.name; - - if (email && analysisPackageName) { - try { - await sendEmail({ account, email, analysisPackageName, personName, partnerLocationName, language }); - } catch (error) { - console.error("Failed to send email", error); + if (analysisItemsOrder) { + // @TODO send email for separate analyses + console.warn(`Order has analysis items, but no email template exists yet`); + } else { + console.info(`Order has no analysis items, skipping email.`); } } else { - // @TODO send email for separate analyses - console.error("Missing email or analysisPackageName", orderResult); + console.error("Missing email to send order result email", orderResult); } await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });