Merge branch 'develop' into feature/MED-129

This commit is contained in:
Danel Kungla
2025-09-24 15:00:27 +03:00
622 changed files with 19603 additions and 10824 deletions

View File

@@ -1,5 +1,10 @@
'use server';
import { MontonioOrderToken } from '@/app/home/(user)/_components/cart/types';
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
import { placeOrder, retrieveCart } from '@lib/data/cart';
import { listProductTypes } from '@lib/data/products';
import type { StoreOrder } from '@medusajs/types';
import { MontonioOrderToken } from '@/app/home/(user)/_components/cart/types';
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
import { placeOrder, retrieveCart } from '@lib/data/cart';
@@ -24,6 +29,20 @@ const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
const ANALYSIS_TYPE_HANDLE = 'synlab-analysis';
const MONTONIO_PAID_STATUS = 'PAID';
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({
const env = () =>
z
.object({
@@ -47,6 +66,13 @@ const env = () =>
isEnabledDispatchOnMontonioCallback:
process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
});
})
.parse({
emailSender: process.env.EMAIL_SENDER,
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
isEnabledDispatchOnMontonioCallback:
process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
});
const sendEmail = async ({
account,
@@ -60,9 +86,17 @@ const sendEmail = async ({
analysisPackageName: string;
partnerLocationName: string;
language: string;
account: Pick<AccountWithParams, 'name' | 'id'>;
email: string;
analysisPackageName: string;
partnerLocationName: string;
language: string;
}) => {
const client = getSupabaseServerAdminClient();
try {
const { renderSynlabAnalysisPackageEmail } = await import(
'@kit/email-templates'
);
const { renderSynlabAnalysisPackageEmail } = await import(
'@kit/email-templates'
);
@@ -91,10 +125,15 @@ const sendEmail = async ({
account_id: account.id,
body: html,
});
await createNotificationsApi(client).createNotification({
account_id: account.id,
body: html,
});
} catch (error) {
throw new Error(`Failed to send email, message=${error}`);
}
};
};
async function decodeOrderToken(orderToken: string) {
const secretKey = process.env.MONTONIO_SECRET_KEY as string;
@@ -105,6 +144,7 @@ async function decodeOrderToken(orderToken: string) {
if (decoded.paymentStatus !== MONTONIO_PAID_STATUS) {
throw new Error('Payment not successful');
throw new Error('Payment not successful');
}
return decoded;
@@ -114,37 +154,50 @@ async function getCartByOrderToken(decoded: MontonioOrderToken) {
const [, , cartId] = decoded.merchantReferenceDisplay.split(':');
if (!cartId) {
throw new Error('Cart ID not found');
throw new Error('Cart ID not found');
}
const cart = await retrieveCart(cartId);
if (!cart) {
throw new Error('Cart not found');
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 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);
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 ?? '',
}))
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,
};
}
@@ -153,12 +206,17 @@ async function sendAnalysisPackageOrderEmail({
email,
analysisPackageOrder,
}: {
account: AccountWithParams;
email: string;
account: AccountWithParams;
email: string;
analysisPackageOrder: {
partnerLocationName: string;
analysisPackageName: string;
};
partnerLocationName: string;
analysisPackageName: string;
};
}) {
const { language } = await createI18nServerInstance();
const { analysisPackageName, partnerLocationName } = analysisPackageOrder;
@@ -170,6 +228,7 @@ async function sendAnalysisPackageOrderEmail({
partnerLocationName,
language,
});
console.info(`Successfully sent analysis package order email to ${email}`);
} catch (error) {
console.error('Failed to send email', error);
}
@@ -179,6 +238,7 @@ export async function processMontonioCallback(orderToken: string) {
const { account } = await loadCurrentUserAccount();
if (!account) {
throw new Error('Account not found in context');
throw new Error('Account not found in context');
}
try {
@@ -191,8 +251,12 @@ export async function processMontonioCallback(orderToken: string) {
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}`);
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
@@ -231,19 +295,26 @@ export async function processMontonioCallback(orderToken: string) {
if (email) {
if (analysisPackageOrder) {
await sendAnalysisPackageOrderEmail({ account, email, 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`);
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);
console.error('Missing email to send order result email', orderResult);
}
if (env().isEnabledDispatchOnMontonioCallback && orderContainsSynlabItems) {
@@ -263,6 +334,7 @@ export async function processMontonioCallback(orderToken: string) {
return { success: true, orderId };
} catch (error) {
console.error('Failed to place order', error);
console.error('Failed to place order', error);
throw new Error(`Failed to place order, message=${error}`);
}

View File

@@ -1,10 +1,15 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { processMontonioCallback } from './actions';
export default function MontonioCallbackClient({ orderToken, error }: {
export default function MontonioCallbackClient({
orderToken,
error,
}: {
orderToken?: string;
error?: string;
}) {
@@ -33,11 +38,13 @@ export default function MontonioCallbackClient({ orderToken, error }: {
if (result.success) {
return router.push(`/home/order/${result.orderId}/confirmed`);
}
if (result.failedServiceBookings?.length){
router.push(`/home/cart/montonio-callback/error?reasonFailed=${result.failedServiceBookings.map(({reason}) => reason).join(',')}`);
if (result.failedServiceBookings?.length) {
router.push(
`/home/cart/montonio-callback/error?reasonFailed=${result.failedServiceBookings.map(({ reason }) => reason).join(',')}`,
);
}
} catch (error) {
console.error("Failed to place order", error);
console.error('Failed to place order', error);
router.push('/home/cart/montonio-callback/error');
} finally {
setIsProcessing(false);
@@ -48,9 +55,9 @@ export default function MontonioCallbackClient({ orderToken, error }: {
}, [orderToken, error, router, hasProcessed, isProcessing]);
return (
<div className="flex mt-10 justify-center min-h-screen">
<div className="mt-10 flex min-h-screen justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<div className="border-primary mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-b-2"></div>
</div>
</div>
);

View File

@@ -2,9 +2,12 @@ import { use } from 'react';
import Link from 'next/link';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
import { Button } from '@kit/ui/button';
import { Button } from '@kit/ui/button';
import { Alert, AlertDescription } from '@kit/ui/shadcn/alert';
import { AlertTitle } from '@kit/ui/shadcn/alert';

View File

@@ -1,12 +1,14 @@
import MontonioCallbackClient from './client-component';
export default async function MontonioCallbackPage({ searchParams }: {
export default async function MontonioCallbackPage({
searchParams,
}: {
searchParams: Promise<{
'order-token'?: string;
}>;
}) {
const orderToken = (await searchParams)['order-token'];
if (!orderToken) {
return <MontonioCallbackClient error="Order token is missing" />;
}