Merge pull request #84 from MR-medreport/keycloak-improvements-0909

improve namings; improve logging for orders without analysis packages
This commit is contained in:
2025-09-09 11:55:43 +00:00
committed by GitHub
6 changed files with 120 additions and 57 deletions

View File

@@ -1,5 +1,5 @@
import { NextResponse } from "next/server"; 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 { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { retrieveOrder } from "@lib/data"; import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service"; import { getAccountAdmin } from "~/lib/services/account.service";
@@ -14,9 +14,9 @@ export async function POST(request: Request) {
const { medusaOrderId } = await request.json(); const { medusaOrderId } = await request.json();
const medusaOrder = await retrieveOrder(medusaOrderId) 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 }); const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`); 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[], orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[], orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
orderId: medusaOrderId, orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at), orderCreatedAt: new Date(analysisOrder.created_at),
}); });
try { try {

View File

@@ -7,13 +7,15 @@ import { loadCurrentUserAccount } from "@/app/home/(user)/_lib/server/load-user-
import { listProductTypes } from "@lib/data/products"; import { listProductTypes } from "@lib/data/products";
import { placeOrder, retrieveCart } from "@lib/data/cart"; import { placeOrder, retrieveCart } from "@lib/data/cart";
import { createI18nServerInstance } from "~/lib/i18n/i18n.server"; import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
import { createOrder } from '~/lib/services/order.service'; import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service';
import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service'; import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
import { createNotificationsApi } from '@kit/notifications/api'; import { createNotificationsApi } from '@kit/notifications/api';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; 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_PACKAGES_TYPE_HANDLE = 'analysis-packages';
const ANALYSIS_TYPE_HANDLE = 'synlab-analysis';
const MONTONIO_PAID_STATUS = 'PAID'; const MONTONIO_PAID_STATUS = 'PAID';
const env = () => z const env = () => z
@@ -38,14 +40,12 @@ const sendEmail = async ({
account, account,
email, email,
analysisPackageName, analysisPackageName,
personName,
partnerLocationName, partnerLocationName,
language, language,
}: { }: {
account: AccountWithParams, account: Pick<AccountWithParams, 'name' | 'id'>,
email: string, email: string,
analysisPackageName: string, analysisPackageName: string,
personName: string,
partnerLocationName: string, partnerLocationName: string,
language: string, language: string,
}) => { }) => {
@@ -58,7 +58,7 @@ const sendEmail = async ({
const { html, subject } = await renderSynlabAnalysisPackageEmail({ const { html, subject } = await renderSynlabAnalysisPackageEmail({
analysisPackageName, analysisPackageName,
personName, personName: account.name,
partnerLocationName, partnerLocationName,
language, language,
}); });
@@ -83,9 +83,7 @@ const sendEmail = async ({
} }
} }
export async function processMontonioCallback(orderToken: string) { async function decodeOrderToken(orderToken: string) {
const { language } = await createI18nServerInstance();
const secretKey = process.env.MONTONIO_SECRET_KEY as string; const secretKey = process.env.MONTONIO_SECRET_KEY as string;
const decoded = jwt.verify(orderToken, secretKey, { const decoded = jwt.verify(orderToken, secretKey, {
@@ -96,50 +94,115 @@ export async function processMontonioCallback(orderToken: string) {
throw new Error("Payment not successful"); 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(); const account = await loadCurrentUserAccount();
if (!account) { if (!account) {
throw new Error("Account not found in context"); throw new Error("Account not found in context");
} }
try { try {
const [, , cartId] = decoded.merchantReferenceDisplay.split(':'); const decoded = await decodeOrderToken(orderToken);
if (!cartId) { const cart = await getCartByOrderToken(decoded);
throw new Error("Cart ID not found");
}
const cart = await retrieveCart(cartId); const medusaOrder = await placeOrder(cart.id, { revalidateCacheTags: false });
if (!cart) {
throw new Error("Cart not found");
}
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder }); const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
const { productTypes } = await listProductTypes(); try {
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE); const existingAnalysisOrder = await getAnalysisOrder({ medusaOrderId: medusaOrder.id });
const analysisPackageOrderItem = medusaOrder.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id); console.info(`Analysis order already exists for medusaOrderId=${medusaOrder.id}, orderId=${existingAnalysisOrder.id}`);
return { success: true, orderId: existingAnalysisOrder.id };
} catch {
// ignored
}
const orderResult = { const orderId = await createAnalysisOrder({ medusaOrder, orderedAnalysisElements });
medusaOrderId: medusaOrder.id, const orderResult = await getOrderResultParameters(medusaOrder);
email: medusaOrder.email,
partnerLocationName: analysisPackageOrderItem?.metadata?.partner_location_name as string ?? '',
analysisPackageName: analysisPackageOrderItem?.title ?? '',
orderedAnalysisElements,
};
const { medusaOrderId, email, partnerLocationName, analysisPackageName } = orderResult; const { medusaOrderId, email, analysisPackageOrder, analysisItemsOrder } = orderResult;
const personName = account.name;
if (email && analysisPackageName) { if (email) {
try { if (analysisPackageOrder) {
await sendEmail({ account, email, analysisPackageName, personName, partnerLocationName, language }); await sendAnalysisPackageOrderEmail({ account, email, analysisPackageOrder });
} catch (error) { } else {
console.error("Failed to send email", error); 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 { } else {
// @TODO send email for separate analyses console.error("Missing email to send order result email", orderResult);
console.error("Missing email or analysisPackageName", orderResult);
} }
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });

View File

@@ -4,7 +4,7 @@ import { PageBody, PageHeader } from '@kit/ui/page';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n'; 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 { retrieveOrder } from '@lib/data/orders';
import { pathsConfig } from '@kit/shared/config'; import { pathsConfig } from '@kit/shared/config';
@@ -27,7 +27,7 @@ async function OrderConfirmedPage(props: {
}) { }) {
const params = await props.params; 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) { if (!order) {
redirect(pathsConfig.app.myOrders); redirect(pathsConfig.app.myOrders);
} }

View File

@@ -4,7 +4,7 @@ import { PageBody, PageHeader } from '@kit/ui/page';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n'; 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 { retrieveOrder } from '@lib/data/orders';
import { pathsConfig } from '@kit/shared/config'; import { pathsConfig } from '@kit/shared/config';
@@ -27,7 +27,7 @@ async function OrderConfirmedPage(props: {
}) { }) {
const params = await props.params; 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) { if (!order) {
redirect(pathsConfig.app.myOrders); redirect(pathsConfig.app.myOrders);
} }

View File

@@ -24,7 +24,7 @@ import { XMLParser } from 'fast-xml-parser';
import { Tables } from '@kit/supabase/database'; import { Tables } from '@kit/supabase/database';
import { createAnalysisGroup } from './analysis-group.service'; import { createAnalysisGroup } from './analysis-group.service';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; 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 { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
import { getAnalyses } from './analyses.service'; import { getAnalyses } from './analyses.service';
import { getAccountAdmin } from './account.service'; import { getAccountAdmin } from './account.service';
@@ -242,7 +242,7 @@ export async function readPrivateMessageResponse({
let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
try { try {
order = await getOrder({ medusaOrderId }); order = await getAnalysisOrder({ medusaOrderId });
} catch (e) { } catch (e) {
await deletePrivateMessage(privateMessage.messageId); await deletePrivateMessage(privateMessage.messageId);
throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); 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 }); const status = await syncPrivateMessage({ messageResponse, order });
if (status.isPartial) { if (status.isPartial) {
await updateOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' });
hasAnalysisResponse = true; hasAnalysisResponse = true;
hasPartialAnalysisResponse = true; hasPartialAnalysisResponse = true;
} else if (status.isCompleted) { } else if (status.isCompleted) {
await updateOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' });
await deletePrivateMessage(privateMessage.messageId); await deletePrivateMessage(privateMessage.messageId);
hasAnalysisResponse = true; hasAnalysisResponse = true;
hasFullAnalysisResponse = true; hasFullAnalysisResponse = true;
@@ -588,7 +588,7 @@ export async function sendOrderToMedipost({
medusaOrderId: string; medusaOrderId: string;
orderedAnalysisElements: OrderedAnalysisElement[]; orderedAnalysisElements: OrderedAnalysisElement[];
}) { }) {
const medreportOrder = await getOrder({ medusaOrderId }); const medreportOrder = await getAnalysisOrder({ medusaOrderId });
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
const orderedAnalysesIds = orderedAnalysisElements const orderedAnalysesIds = orderedAnalysisElements
@@ -668,7 +668,7 @@ export async function sendOrderToMedipost({
hasAnalysisResults: false, hasAnalysisResults: false,
medusaOrderId, medusaOrderId,
}); });
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
} }
export async function getOrderedAnalysisIds({ export async function getOrderedAnalysisIds({

View File

@@ -5,7 +5,7 @@ import type { StoreOrder } from '@medusajs/types';
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>; export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
export async function createOrder({ export async function createAnalysisOrder({
medusaOrder, medusaOrder,
orderedAnalysisElements, orderedAnalysisElements,
}: { }: {
@@ -38,7 +38,7 @@ export async function createOrder({
return orderResult.data.id; return orderResult.data.id;
} }
export async function updateOrder({ export async function updateAnalysisOrder({
orderId, orderId,
orderStatus, orderStatus,
}: { }: {
@@ -56,7 +56,7 @@ export async function updateOrder({
.throwOnError(); .throwOnError();
} }
export async function updateOrderStatus({ export async function updateAnalysisOrderStatus({
orderId, orderId,
medusaOrderId, medusaOrderId,
orderStatus, orderStatus,
@@ -80,7 +80,7 @@ export async function updateOrderStatus({
.throwOnError(); .throwOnError();
} }
export async function getOrder({ export async function getAnalysisOrder({
medusaOrderId, medusaOrderId,
orderId, orderId,
}: { }: {