'use server'; import { z } from 'zod'; import jwt from 'jsonwebtoken'; import type { StoreCart, StoreOrder } from "@medusajs/types"; import { initiateMultiPaymentSession, placeOrder } from "@lib/data/cart"; import type { AccountBalanceSummary } from "~/lib/services/accountBalance.service"; import { handleNavigateToPayment } from "~/lib/services/medusaCart.service"; import { loadCurrentUserAccount } from "./load-user-account"; import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service"; import { createAnalysisOrder, getAnalysisOrder } from "~/lib/services/order.service"; import { listProductTypes } from "@lib/data"; import { sendOrderToMedipost } from "~/lib/services/medipost/medipostPrivateMessage.service"; import { AccountWithParams } from "@/packages/features/accounts/src/types/accounts"; import { createI18nServerInstance } from "~/lib/i18n/i18n.server"; import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client"; import { createNotificationsApi } from "@/packages/features/notifications/src/server/api"; const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages'; const ANALYSIS_TYPE_HANDLE = 'synlab-analysis'; const env = () => z .object({ emailSender: z .string({ error: 'EMAIL_SENDER is required', }) .min(1), siteUrl: z .string({ error: 'NEXT_PUBLIC_SITE_URL is required', }) .min(1), isEnabledDispatchOnMontonioCallback: z.boolean({ error: 'MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK is required', }), medusaBackendPublicUrl: z.string({ error: 'MEDUSA_BACKEND_PUBLIC_URL is required', }).min(1), companyBenefitsPaymentSecretKey: z.string({ error: 'COMPANY_BENEFITS_PAYMENT_SECRET_KEY is required', }).min(1), }) .parse({ emailSender: process.env.EMAIL_SENDER, siteUrl: process.env.NEXT_PUBLIC_SITE_URL!, isEnabledDispatchOnMontonioCallback: process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true', medusaBackendPublicUrl: process.env.MEDUSA_BACKEND_PUBLIC_URL!, companyBenefitsPaymentSecretKey: process.env.COMPANY_BENEFITS_PAYMENT_SECRET_KEY!, }); export const initiatePayment = async ({ accountId, balanceSummary, cart, language, }: { accountId: string; balanceSummary: AccountBalanceSummary; cart: StoreCart; language: string; }) => { try { const { montonioPaymentSessionId, companyBenefitsPaymentSessionId, totalByMontonio, totalByBenefits, isFullyPaidByBenefits, } = await initiateMultiPaymentSession(cart, balanceSummary.totalBalance); if (!isFullyPaidByBenefits) { if (!montonioPaymentSessionId) { throw new Error('Montonio payment session ID is missing'); } const url = await handleNavigateToPayment({ language, paymentSessionId: montonioPaymentSessionId, amount: totalByMontonio, currencyCode: cart.currency_code, cartId: cart.id, }); return { url }; } else { // place order if all paid already const { orderId } = await handlePlaceOrder({ cart }); const companyBenefitsOrderToken = jwt.sign({ accountId, companyBenefitsPaymentSessionId, orderId, totalByBenefits, }, env().companyBenefitsPaymentSecretKey, { algorithm: 'HS256', }); const webhookResponse = await fetch(`${env().medusaBackendPublicUrl}/hooks/payment/company-benefits_company-benefits`, { method: 'POST', body: JSON.stringify({ orderToken: companyBenefitsOrderToken, }), headers: { 'Content-Type': 'application/json', }, }); if (!webhookResponse.ok) { throw new Error('Failed to send company benefits webhook'); } return { isFullyPaidByBenefits, orderId }; } } catch (error) { console.error('Error initiating payment', error); } return { url: null } } export async function handlePlaceOrder({ cart, }: { cart: StoreCart; }) { const { account } = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found in context'); } try { 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 { medusaOrderId, email, analysisPackageOrder, analysisItemsOrder } = orderResult; if (email) { if (analysisPackageOrder) { await sendAnalysisPackageOrderEmail({ account, email, analysisPackageOrder, }); } else { console.info(`Order has no analysis package, skipping email.`); } 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 { console.error('Missing email to send order result email', orderResult); } if (env().isEnabledDispatchOnMontonioCallback) { await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); } return { success: true, orderId }; } catch (error) { console.error('Failed to place order', error); throw new Error(`Failed to place order, message=${error}`); } } 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, }); console.info(`Successfully sent analysis package order email to ${email}`); } catch (error) { console.error( `Failed to send analysis package order email to ${email}`, error, ); } } 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, }; } const sendEmail = async ({ account, email, analysisPackageName, partnerLocationName, language, }: { account: Pick; email: string; analysisPackageName: string; partnerLocationName: string; language: string; }) => { const client = getSupabaseServerAdminClient(); try { const { renderSynlabAnalysisPackageEmail } = await import( '@kit/email-templates' ); const { getMailer } = await import('@kit/mailers'); const mailer = await getMailer(); const { html, subject } = await renderSynlabAnalysisPackageEmail({ analysisPackageName, personName: account.name, partnerLocationName, language, }); await mailer .sendEmail({ from: env().emailSender, to: email, subject, html, }) .catch((error) => { throw new Error(`Failed to send email, message=${error}`); }); await createNotificationsApi(client).createNotification({ account_id: account.id, body: html, }); } catch (error) { throw new Error(`Failed to send email, message=${error}`); } };