From a788e8b587bcfa2930537e5f4919baf8a2d1da5f Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:14:54 +0300 Subject: [PATCH] feat(MED-161): move medipost privatemessage logic to separate service --- app/api/job/handler/sync-analysis-results.ts | 2 +- app/api/job/medipost-retry-dispatch/route.ts | 3 +- .../cart/montonio-callback/actions.ts | 3 +- lib/services/medipost.service.ts | 379 +---------------- .../medipostPrivateMessage.service.ts | 395 ++++++++++++++++++ 5 files changed, 402 insertions(+), 380 deletions(-) create mode 100644 lib/services/medipost/medipostPrivateMessage.service.ts diff --git a/app/api/job/handler/sync-analysis-results.ts b/app/api/job/handler/sync-analysis-results.ts index e666916..4f394f1 100644 --- a/app/api/job/handler/sync-analysis-results.ts +++ b/app/api/job/handler/sync-analysis-results.ts @@ -1,4 +1,4 @@ -import { readPrivateMessageResponse } from "~/lib/services/medipost.service"; +import { readPrivateMessageResponse } from "~/lib/services/medipost/medipostPrivateMessage.service"; type ProcessedMessage = { messageId: string; diff --git a/app/api/job/medipost-retry-dispatch/route.ts b/app/api/job/medipost-retry-dispatch/route.ts index 1a97cef..7b1a55d 100644 --- a/app/api/job/medipost-retry-dispatch/route.ts +++ b/app/api/job/medipost-retry-dispatch/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import loadEnv from "../handler/load-env"; import validateApiKey from "../handler/validate-api-key"; -import { getOrderedAnalysisIds, sendOrderToMedipost } from "~/lib/services/medipost.service"; +import { getOrderedAnalysisIds } from "~/lib/services/medipost.service"; +import { sendOrderToMedipost } from "~/lib/services/medipost/medipostPrivateMessage.service"; import { retrieveOrder } from "@lib/data/orders"; import { getMedipostDispatchTries } from "~/lib/services/audit.service"; diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index f705399..23d39de 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -8,7 +8,8 @@ import { listProductTypes } from "@lib/data/products"; import { placeOrder, retrieveCart } from "@lib/data/cart"; import { createI18nServerInstance } from "~/lib/i18n/i18n.server"; import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service'; -import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service'; +import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service'; +import { getOrderedAnalysisIds } from '~/lib/services/medipost.service'; import { createNotificationsApi } from '@kit/notifications/api'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import type { AccountWithParams } from '@kit/accounts/api'; diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index ef6eae4..6aa4179 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -7,12 +7,9 @@ import { import { SyncStatus } from '@/lib/types/audit'; import { - AnalysisOrderStatus, GetMessageListResponse, MedipostAction, - MedipostOrderResponse, MedipostPublicMessageResponse, - ResponseUuringuGrupp, UuringuGrupp, } from '@/lib/types/medipost'; import { toArray } from '@/lib/utils'; @@ -20,21 +17,15 @@ import axios from 'axios'; import { Tables } from '@kit/supabase/database'; import { createAnalysisGroup } from './analysis-group.service'; -import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; -import { getAnalysisOrder, updateAnalysisOrderStatus } from './order.service'; -import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service'; +import { getAnalysisElements } from './analysis-element.service'; import { getAnalyses } from './analyses.service'; -import { getAccountAdmin } from './account.service'; import { StoreOrder } from '@medusajs/types'; import { listProducts } from '@lib/data/products'; import { listRegions } from '@lib/data/regions'; import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product'; -import { MedipostValidationError } from './medipost/MedipostValidationError'; -import { logMedipostDispatch } from './audit.service'; -import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service'; import { validateMedipostResponse } from './medipost/medipostValidate.service'; import { parseXML } from './util/xml.service'; -import { createMedipostActionLog, getLatestMessage } from './medipost/medipostMessageBase.service'; +import { getLatestMessage } from './medipost/medipostMessageBase.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -93,163 +84,6 @@ export async function getPublicMessage(messageId: string) { return parseXML(data) as MedipostPublicMessageResponse; } -export async function sendPrivateMessage(messageXml: string) { - const body = new FormData(); - body.append('Action', MedipostAction.SendPrivateMessage); - body.append('User', USER); - body.append('Password', PASSWORD); - body.append('Receiver', RECIPIENT); - body.append('MessageType', 'Tellimus'); - body.append( - 'Message', - new Blob([messageXml], { - type: 'text/xml; charset=UTF-8', - }), - ); - - const { data } = await axios.post(BASE_URL, body); - - await validateMedipostResponse(data); -} - -export async function getLatestPrivateMessageListItem({ - excludedMessageIds, -}: { - excludedMessageIds: string[]; -}) { - const { data } = await axios.get(BASE_URL, { - params: { - Action: MedipostAction.GetPrivateMessageList, - User: USER, - Password: PASSWORD, - }, - }); - - if (data.code && data.code !== 0) { - throw new Error('Failed to get private message list'); - } - - return getLatestMessage({ messages: data?.messages, excludedMessageIds }); -} - -export async function getPrivateMessage(messageId: string) { - const { data } = await axios.get(BASE_URL, { - params: { - Action: MedipostAction.GetPrivateMessage, - User: USER, - Password: PASSWORD, - MessageId: messageId, - }, - headers: { - Accept: 'application/xml', - }, - }); - - await validateMedipostResponse(data, { canHaveEmptyCode: true }); - - return { - message: parseXML(data) as MedipostOrderResponse, - xml: data as string, - }; -} - -export async function deletePrivateMessage(messageId: string) { - const { data } = await axios.get(BASE_URL, { - params: { - Action: MedipostAction.DeletePrivateMessage, - User: USER, - Password: PASSWORD, - MessageId: messageId, - }, - }); - - if (data.code && data.code !== 0) { - throw new Error(`Failed to delete private message (id: ${messageId})`); - } -} - -export async function readPrivateMessageResponse({ - excludedMessageIds, -}: { - excludedMessageIds: string[]; -}): Promise<{ messageId: string | null; hasAnalysisResponse: boolean; hasPartialAnalysisResponse: boolean; hasFullAnalysisResponse: boolean; medusaOrderId: string | undefined; analysisOrderId: number | undefined }> { - let messageId: string | null = null; - let hasAnalysisResponse = false; - let hasPartialAnalysisResponse = false; - let hasFullAnalysisResponse = false; - let medusaOrderId: string | undefined = undefined; - let analysisOrderId: number | undefined = undefined; - - try { - const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds }); - messageId = privateMessage?.messageId ?? null; - - if (!privateMessage || !messageId) { - return { - messageId: null, - hasAnalysisResponse: false, - hasPartialAnalysisResponse: false, - hasFullAnalysisResponse: false, - medusaOrderId: undefined, - analysisOrderId: undefined - }; - } - - const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage( - privateMessage.messageId, - ); - - const messageResponse = privateMessageContent?.Saadetis?.Vastus; - analysisOrderId = Number(privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId); - - const hasInvalidOrderId = isNaN(analysisOrderId) - - if (hasInvalidOrderId || !messageResponse) { - await createMedipostActionLog({ - action: 'sync_analysis_results_from_medipost', - xml: privateMessageXml, - hasAnalysisResults: false, - }); - return { - messageId, - hasAnalysisResponse: false, - hasPartialAnalysisResponse: false, - hasFullAnalysisResponse: false, - medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId, - analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId - }; - } - - const analysisOrder = await getAnalysisOrder({ analysisOrderId: analysisOrderId }) - medusaOrderId = analysisOrder.medusa_order_id; - - let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; - try { - order = await getAnalysisOrder({ medusaOrderId }); - } catch (e) { - await deletePrivateMessage(privateMessage.messageId); - throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); - } - - const status = await syncPrivateMessage({ messageResponse, order }); - - if (status.isPartial) { - await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); - hasAnalysisResponse = true; - hasPartialAnalysisResponse = true; - } else if (status.isCompleted) { - await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); - await deletePrivateMessage(privateMessage.messageId); - hasAnalysisResponse = true; - hasFullAnalysisResponse = true; - } - } catch (e) { - console.warn(`Failed to process private message id=${messageId}, message=${(e as Error).message}`); - } - - return { messageId, hasAnalysisResponse, hasPartialAnalysisResponse, hasFullAnalysisResponse, medusaOrderId, analysisOrderId }; -} - async function saveAnalysisGroup( analysisGroup: UuringuGrupp, supabase: SupabaseClient, @@ -422,215 +256,6 @@ export async function syncPublicMessage( } } -async function syncPrivateMessage({ - messageResponse, - order, -}: { - messageResponse: NonNullable; - order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; -}) { - const supabase = getSupabaseServerAdminClient() - - const { data: analysisOrder, error: analysisOrderError } = await supabase - .schema('medreport') - .from('analysis_orders') - .select('user_id') - .eq('id', order.id); - - if (analysisOrderError || !analysisOrder?.[0]?.user_id) { - throw new Error( - `Could not find analysis order with id ${messageResponse.ValisTellimuseId}`, - ); - } - - const { data: analysisResponse, error } = await supabase - .schema('medreport') - .from('analysis_responses') - .upsert( - { - analysis_order_id: order.id, - order_number: messageResponse.TellimuseNumber, - order_status: AnalysisOrderStatus[messageResponse.TellimuseOlek], - user_id: analysisOrder[0].user_id, - }, - { onConflict: 'order_number', ignoreDuplicates: false }, - ) - .select('id'); - - if (error || !analysisResponse?.[0]?.id) { - throw new Error( - `Failed to insert or update analysis order response (external id: ${messageResponse?.TellimuseNumber})`, - ); - } - const analysisGroups = toArray(messageResponse.UuringuGrupp); - console.info(`Order has results for ${analysisGroups.length} analysis groups`); - - const responses: Omit< - Tables<{ schema: 'medreport' }, 'analysis_response_elements'>, - 'id' | 'created_at' | 'updated_at' - >[] = []; - - const analysisResponseId = analysisResponse[0]!.id; - - for (const analysisGroup of analysisGroups) { - const groupItems = toArray( - analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'], - ); - console.info(`Order has results in group ${analysisGroup.UuringuGruppNimi} for ${groupItems.length} analysis elements`); - for (const item of groupItems) { - const element = item.UuringuElement; - const elementAnalysisResponses = toArray(element.UuringuVastus); - - responses.push( - ...elementAnalysisResponses.map((response) => ({ - analysis_element_original_id: element.UuringId, - analysis_response_id: analysisResponseId, - norm_lower: response.NormAlum?.['#text'] ?? null, - norm_lower_included: - response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah', - norm_status: response.NormiStaatus, - norm_upper: response.NormYlem?.['#text'] ?? null, - norm_upper_included: - response.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah', - response_time: response.VastuseAeg ?? null, - response_value: response.VastuseVaartus, - unit: element.Mootyhik ?? null, - original_response_element: element, - analysis_name: element.UuringNimi || element.KNimetus, - comment: element.UuringuKommentaar ?? '', - })), - ); - } - } - - const { error: deleteError } = await supabase - .schema('medreport') - .from('analysis_response_elements') - .delete() - .eq('analysis_response_id', analysisResponseId); - - if (deleteError) { - throw new Error( - `Failed to clean up response elements for response id ${analysisResponseId}`, - ); - } - - const { error: elementInsertError } = await supabase - .schema('medreport') - .from('analysis_response_elements') - .insert(responses); - - if (elementInsertError) { - throw new Error( - `Failed to insert order response elements for response id ${analysisResponseId}`, - ); - } - - const { data: allOrderResponseElements } = await supabase - .schema('medreport') - .from('analysis_response_elements') - .select('*') - .eq('analysis_response_id', analysisResponseId) - .throwOnError(); - const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; - if (allOrderResponseElements.length !== expectedOrderResponseElements) { - return { isPartial: true }; - } - - const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek]; - return { isCompleted: statusFromResponse === 'COMPLETED' }; -} - -export async function sendOrderToMedipost({ - medusaOrderId, - orderedAnalysisElements, -}: { - medusaOrderId: string; - orderedAnalysisElements: OrderedAnalysisElement[]; -}) { - const medreportOrder = await getAnalysisOrder({ medusaOrderId }); - const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); - - const orderedAnalysesIds = orderedAnalysisElements - .map(({ analysisId }) => analysisId) - .filter(Boolean) as number[]; - const orderedAnalysisElementsIds = orderedAnalysisElements - .map(({ analysisElementId }) => analysisElementId) - .filter(Boolean) as number[]; - - const analyses = await getAnalyses({ ids: orderedAnalysesIds }); - if (analyses.length !== orderedAnalysesIds.length) { - throw new Error(`Got ${analyses.length} analyses, expected ${orderedAnalysesIds.length}`); - } - const analysisElements = await getAnalysisElementsAdmin({ ids: orderedAnalysisElementsIds }); - if (analysisElements.length !== orderedAnalysisElementsIds.length) { - throw new Error(`Got ${analysisElements.length} analysis elements, expected ${orderedAnalysisElementsIds.length}`); - } - - const orderXml = await composeOrderXML({ - analyses, - analysisElements, - person: { - idCode: account.personal_code!, - firstName: account.name ?? '', - lastName: account.last_name ?? '', - phone: account.phone ?? '', - }, - orderId: medreportOrder.id, - orderCreatedAt: new Date(medreportOrder.created_at), - comment: '', - }); - - try { - await sendPrivateMessage(orderXml); - } catch (e) { - const isMedipostError = e instanceof MedipostValidationError; - if (isMedipostError) { - await logMedipostDispatch({ - medusaOrderId, - isSuccess: false, - isMedipostError, - errorMessage: e.response, - }); - await createMedipostActionLog({ - action: 'send_order_to_medipost', - xml: orderXml, - hasAnalysisResults: false, - medusaOrderId, - responseXml: e.response, - hasError: true, - }); - } else { - await logMedipostDispatch({ - medusaOrderId, - isSuccess: false, - isMedipostError, - }); - await createMedipostActionLog({ - action: 'send_order_to_medipost', - xml: orderXml, - hasAnalysisResults: false, - medusaOrderId, - hasError: true, - }); - } - - throw e; - } - await logMedipostDispatch({ - medusaOrderId, - isSuccess: true, - isMedipostError: false, - }); - await createMedipostActionLog({ - action: 'send_order_to_medipost', - xml: orderXml, - hasAnalysisResults: false, - medusaOrderId, - }); - await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); -} - export async function getOrderedAnalysisIds({ medusaOrder, }: { diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts new file mode 100644 index 0000000..afb341b --- /dev/null +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -0,0 +1,395 @@ +'use server'; + +import { + AnalysisOrderStatus, + GetMessageListResponse, + MedipostAction, + MedipostOrderResponse, + ResponseUuringuGrupp, +} from '@/lib/types/medipost'; +import { toArray } from '@/lib/utils'; +import axios from 'axios'; + +import { Tables } from '@kit/supabase/database'; +import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; +import { getAnalysisElementsAdmin } from '../analysis-element.service'; +import { getAnalyses } from '../analyses.service'; +import { createMedipostActionLog, getLatestMessage } from './medipostMessageBase.service'; +import { validateMedipostResponse } from './medipostValidate.service'; +import { getAnalysisOrder, updateAnalysisOrderStatus } from '../order.service'; +import { parseXML } from '../util/xml.service'; +import { composeOrderXML, OrderedAnalysisElement } from '../medipostXML.service'; +import { getAccountAdmin } from '../account.service'; +import { logMedipostDispatch } from '../audit.service'; +import { MedipostValidationError } from './MedipostValidationError'; + +const BASE_URL = process.env.MEDIPOST_URL!; +const USER = process.env.MEDIPOST_USER!; +const PASSWORD = process.env.MEDIPOST_PASSWORD!; +const RECIPIENT = process.env.MEDIPOST_RECIPIENT!; + +export async function getLatestPrivateMessageListItem({ + excludedMessageIds, +}: { + excludedMessageIds: string[]; +}) { + const { data } = await axios.get(BASE_URL, { + params: { + Action: MedipostAction.GetPrivateMessageList, + User: USER, + Password: PASSWORD, + }, + }); + + if (data.code && data.code !== 0) { + throw new Error('Failed to get private message list'); + } + + return getLatestMessage({ messages: data?.messages, excludedMessageIds }); +} + +export async function syncPrivateMessage({ + messageResponse, + order, +}: { + messageResponse: NonNullable; + order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; +}) { + const supabase = getSupabaseServerAdminClient() + + const { data: analysisOrder, error: analysisOrderError } = await supabase + .schema('medreport') + .from('analysis_orders') + .select('user_id') + .eq('id', order.id); + + if (analysisOrderError || !analysisOrder?.[0]?.user_id) { + throw new Error( + `Could not find analysis order with id ${messageResponse.ValisTellimuseId}`, + ); + } + + const { data: analysisResponse, error } = await supabase + .schema('medreport') + .from('analysis_responses') + .upsert( + { + analysis_order_id: order.id, + order_number: messageResponse.TellimuseNumber, + order_status: AnalysisOrderStatus[messageResponse.TellimuseOlek], + user_id: analysisOrder[0].user_id, + }, + { onConflict: 'order_number', ignoreDuplicates: false }, + ) + .select('id'); + + if (error || !analysisResponse?.[0]?.id) { + throw new Error( + `Failed to insert or update analysis order response (external id: ${messageResponse?.TellimuseNumber})`, + ); + } + const analysisGroups = toArray(messageResponse.UuringuGrupp); + console.info(`Order has results for ${analysisGroups.length} analysis groups`); + + const responses: Omit< + Tables<{ schema: 'medreport' }, 'analysis_response_elements'>, + 'id' | 'created_at' | 'updated_at' + >[] = []; + + const analysisResponseId = analysisResponse[0]!.id; + + for (const analysisGroup of analysisGroups) { + const groupItems = toArray( + analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'], + ); + console.info(`Order has results in group ${analysisGroup.UuringuGruppNimi} for ${groupItems.length} analysis elements`); + for (const item of groupItems) { + const element = item.UuringuElement; + const elementAnalysisResponses = toArray(element.UuringuVastus); + + responses.push( + ...elementAnalysisResponses.map((response) => ({ + analysis_element_original_id: element.UuringId, + analysis_response_id: analysisResponseId, + norm_lower: response.NormAlum?.['#text'] ?? null, + norm_lower_included: + response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah', + norm_status: response.NormiStaatus, + norm_upper: response.NormYlem?.['#text'] ?? null, + norm_upper_included: + response.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah', + response_time: response.VastuseAeg ?? null, + response_value: response.VastuseVaartus, + unit: element.Mootyhik ?? null, + original_response_element: element, + analysis_name: element.UuringNimi || element.KNimetus, + comment: element.UuringuKommentaar ?? '', + })), + ); + } + } + + const { error: deleteError } = await supabase + .schema('medreport') + .from('analysis_response_elements') + .delete() + .eq('analysis_response_id', analysisResponseId); + + if (deleteError) { + throw new Error( + `Failed to clean up response elements for response id ${analysisResponseId}`, + ); + } + + const { error: elementInsertError } = await supabase + .schema('medreport') + .from('analysis_response_elements') + .insert(responses); + + if (elementInsertError) { + throw new Error( + `Failed to insert order response elements for response id ${analysisResponseId}`, + ); + } + + const { data: allOrderResponseElements } = await supabase + .schema('medreport') + .from('analysis_response_elements') + .select('*') + .eq('analysis_response_id', analysisResponseId) + .throwOnError(); + const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; + if (allOrderResponseElements.length !== expectedOrderResponseElements) { + return { isPartial: true }; + } + + const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek]; + return { isCompleted: statusFromResponse === 'COMPLETED' }; +} + +export async function readPrivateMessageResponse({ + excludedMessageIds, +}: { + excludedMessageIds: string[]; +}): Promise<{ messageId: string | null; hasAnalysisResponse: boolean; hasPartialAnalysisResponse: boolean; hasFullAnalysisResponse: boolean; medusaOrderId: string | undefined; analysisOrderId: number | undefined }> { + let messageId: string | null = null; + let hasAnalysisResponse = false; + let hasPartialAnalysisResponse = false; + let hasFullAnalysisResponse = false; + let medusaOrderId: string | undefined = undefined; + let analysisOrderId: number | undefined = undefined; + + try { + const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds }); + messageId = privateMessage?.messageId ?? null; + + if (!privateMessage || !messageId) { + return { + messageId: null, + hasAnalysisResponse: false, + hasPartialAnalysisResponse: false, + hasFullAnalysisResponse: false, + medusaOrderId: undefined, + analysisOrderId: undefined + }; + } + + const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage( + privateMessage.messageId, + ); + + const messageResponse = privateMessageContent?.Saadetis?.Vastus; + analysisOrderId = Number(privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId); + + const hasInvalidOrderId = isNaN(analysisOrderId) + + if (hasInvalidOrderId || !messageResponse) { + await createMedipostActionLog({ + action: 'sync_analysis_results_from_medipost', + xml: privateMessageXml, + hasAnalysisResults: false, + }); + return { + messageId, + hasAnalysisResponse: false, + hasPartialAnalysisResponse: false, + hasFullAnalysisResponse: false, + medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId, + analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId + }; + } + + const analysisOrder = await getAnalysisOrder({ analysisOrderId: analysisOrderId }) + medusaOrderId = analysisOrder.medusa_order_id; + + let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; + try { + order = await getAnalysisOrder({ medusaOrderId }); + } catch (e) { + await deletePrivateMessage(privateMessage.messageId); + throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); + } + + const status = await syncPrivateMessage({ messageResponse, order }); + + if (status.isPartial) { + await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); + hasAnalysisResponse = true; + hasPartialAnalysisResponse = true; + } else if (status.isCompleted) { + await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); + await deletePrivateMessage(privateMessage.messageId); + hasAnalysisResponse = true; + hasFullAnalysisResponse = true; + } + } catch (e) { + console.warn(`Failed to process private message id=${messageId}, message=${(e as Error).message}`); + } + + return { messageId, hasAnalysisResponse, hasPartialAnalysisResponse, hasFullAnalysisResponse, medusaOrderId, analysisOrderId }; +} + +export async function deletePrivateMessage(messageId: string) { + const { data } = await axios.get(BASE_URL, { + params: { + Action: MedipostAction.DeletePrivateMessage, + User: USER, + Password: PASSWORD, + MessageId: messageId, + }, + }); + + if (data.code && data.code !== 0) { + throw new Error(`Failed to delete private message (id: ${messageId})`); + } +} + +export async function sendPrivateMessage(messageXml: string) { + const body = new FormData(); + body.append('Action', MedipostAction.SendPrivateMessage); + body.append('User', USER); + body.append('Password', PASSWORD); + body.append('Receiver', RECIPIENT); + body.append('MessageType', 'Tellimus'); + body.append( + 'Message', + new Blob([messageXml], { + type: 'text/xml; charset=UTF-8', + }), + ); + + const { data } = await axios.post(BASE_URL, body); + + await validateMedipostResponse(data); +} + +export async function getPrivateMessage(messageId: string) { + const { data } = await axios.get(BASE_URL, { + params: { + Action: MedipostAction.GetPrivateMessage, + User: USER, + Password: PASSWORD, + MessageId: messageId, + }, + headers: { + Accept: 'application/xml', + }, + }); + + await validateMedipostResponse(data, { canHaveEmptyCode: true }); + + return { + message: parseXML(data) as MedipostOrderResponse, + xml: data as string, + }; +} + +export async function sendOrderToMedipost({ + medusaOrderId, + orderedAnalysisElements, +}: { + medusaOrderId: string; + orderedAnalysisElements: OrderedAnalysisElement[]; +}) { + const medreportOrder = await getAnalysisOrder({ medusaOrderId }); + const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); + + const orderedAnalysesIds = orderedAnalysisElements + .map(({ analysisId }) => analysisId) + .filter(Boolean) as number[]; + const orderedAnalysisElementsIds = orderedAnalysisElements + .map(({ analysisElementId }) => analysisElementId) + .filter(Boolean) as number[]; + + const analyses = await getAnalyses({ ids: orderedAnalysesIds }); + if (analyses.length !== orderedAnalysesIds.length) { + throw new Error(`Got ${analyses.length} analyses, expected ${orderedAnalysesIds.length}`); + } + const analysisElements = await getAnalysisElementsAdmin({ ids: orderedAnalysisElementsIds }); + if (analysisElements.length !== orderedAnalysisElementsIds.length) { + throw new Error(`Got ${analysisElements.length} analysis elements, expected ${orderedAnalysisElementsIds.length}`); + } + + const orderXml = await composeOrderXML({ + analyses, + analysisElements, + person: { + idCode: account.personal_code!, + firstName: account.name ?? '', + lastName: account.last_name ?? '', + phone: account.phone ?? '', + }, + orderId: medreportOrder.id, + orderCreatedAt: new Date(medreportOrder.created_at), + comment: '', + }); + + try { + await sendPrivateMessage(orderXml); + } catch (e) { + const isMedipostError = e instanceof MedipostValidationError; + if (isMedipostError) { + await logMedipostDispatch({ + medusaOrderId, + isSuccess: false, + isMedipostError, + errorMessage: e.response, + }); + await createMedipostActionLog({ + action: 'send_order_to_medipost', + xml: orderXml, + hasAnalysisResults: false, + medusaOrderId, + responseXml: e.response, + hasError: true, + }); + } else { + await logMedipostDispatch({ + medusaOrderId, + isSuccess: false, + isMedipostError, + }); + await createMedipostActionLog({ + action: 'send_order_to_medipost', + xml: orderXml, + hasAnalysisResults: false, + medusaOrderId, + hasError: true, + }); + } + + throw e; + } + await logMedipostDispatch({ + medusaOrderId, + isSuccess: true, + isMedipostError: false, + }); + await createMedipostActionLog({ + action: 'send_order_to_medipost', + xml: orderXml, + hasAnalysisResults: false, + medusaOrderId, + }); + await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); +}