'use server'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; import { listProductTypes } from '@lib/data'; import { initiateMultiPaymentSession, placeOrder } from '@lib/data/cart'; import type { StoreCart, StoreOrder } from '@medusajs/types'; import jwt from 'jsonwebtoken'; import { z } from 'zod'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { bookAppointment } from '~/lib/services/connected-online.service'; import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service'; import { handleNavigateToPayment } from '~/lib/services/medusaCart.service'; import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service'; import { createAnalysisOrder, getAnalysisOrder, } from '~/lib/services/order.service'; import { getOrderedTtoServices } from '~/lib/services/reservation.service'; import { FailureReason } from '~/lib/types/connected-online'; import { loadCurrentUserAccount } from './load-user-account'; 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, benefitsAmount, cart, language, }: { accountId: string; benefitsAmount: number; cart: StoreCart; language: string; }) => { try { const { montonioPaymentSessionId, companyBenefitsPaymentSessionId, totalByMontonio, totalByBenefits, isFullyPaidByBenefits, } = await initiateMultiPaymentSession(cart, benefitsAmount); if (!isFullyPaidByBenefits) { if (!montonioPaymentSessionId) { throw new Error('Montonio payment session ID is missing'); } const props = await handleNavigateToPayment({ language, paymentSessionId: montonioPaymentSessionId, amount: totalByMontonio, currencyCode: cart.currency_code, cartId: cart.id, }); return { ...props, isFullyPaidByBenefits }; } 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, unavailableLineItemIds: [] }; } } catch (error) { console.error('Error initiating payment', error); } return { url: null, isFullyPaidByBenefits: false, orderId: null, unavailableLineItemIds: [], }; }; 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, }); const orderContainsSynlabItems = !!orderedAnalysisElements?.length; 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 } let orderId: number | undefined = undefined; if (orderContainsSynlabItems) { orderId = await createAnalysisOrder({ medusaOrder, orderedAnalysisElements, }); } const orderResult = await getOrderResultParameters(medusaOrder); const { medusaOrderId, email, analysisPackageOrder, analysisItemsOrder } = orderResult; const orderedTtoServices = await getOrderedTtoServices({ medusaOrder }); let bookServiceResults: { success: boolean; reason?: FailureReason; serviceId?: number; }[] = []; if (orderedTtoServices?.length) { const bookingPromises = orderedTtoServices.map((service) => bookAppointment( service.service_id, service.clinic_id, service.service_user_id, service.sync_user_id, service.start_time, ), ); bookServiceResults = await Promise.all(bookingPromises); } // TODO: SEND EMAIL 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 }); } if (bookServiceResults.some(({ success }) => success === false)) { const failedServiceBookings = bookServiceResults.filter( ({ success }) => success === false, ); return { success: false, failedServiceBookings, orderId, }; } 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; }) => { 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}`); }); } catch (error) { throw new Error(`Failed to send email, message=${error}`); } };