'use server'; import type { PostgrestError } from '@supabase/supabase-js'; import axios from 'axios'; import { GetMessageListResponse, MedipostAction, } from '@/lib/types/medipost'; import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; import type { ResponseUuringuGrupp, MedipostOrderResponse, ResponseUuring, } from '@/packages/shared/src/types/medipost-analysis'; import { toArray } from '@/lib/utils'; import type { AnalysisOrder } from '~/lib/types/analysis-order'; import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element'; 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'; import { getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service'; 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!; const IS_ENABLED_DELETE_PRIVATE_MESSAGE = false as boolean; 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 }); } const logger = (analysisOrder: AnalysisOrder, externalId: string, analysisResponseId: string) => (message: string, error?: PostgrestError | null) => { const messageFormatted = `[${analysisOrder.id}] [${externalId}] [${analysisResponseId}] ${message}`; if (error) { console.info(messageFormatted, error); } else { console.info(messageFormatted); } }; function canCreateAnalysisResponseElement({ existingElements, groupUuring: { UuringuElement: { UuringOlek: status, UuringId: analysisElementOriginalId, }, }, responseValue, log, }: { existingElements: AnalysisResponseElement[]; groupUuring: ResponseUuring; responseValue: number | null; log: ReturnType; }) { const existingAnalysisResponseElement = existingElements.find(({ analysis_element_original_id }) => analysis_element_original_id === analysisElementOriginalId); if (!existingAnalysisResponseElement) { return true; } if (Number(existingAnalysisResponseElement.status) > status) { log(`Analysis response element id=${analysisElementOriginalId} already exists for order in higher status ${existingAnalysisResponseElement.status} than ${status}`); return false; } if (existingAnalysisResponseElement.response_value && !responseValue) { log(`Analysis response element id=${analysisElementOriginalId} already exists for order with response value ${existingAnalysisResponseElement.response_value} but new response has no value`); return false; } return true; } async function getAnalysisResponseElementsForGroup({ analysisResponseId, analysisGroup, log, }: { analysisResponseId: number; analysisGroup: ResponseUuringuGrupp; log: ReturnType; }) { const groupUuringItems = toArray(analysisGroup.Uuring as ResponseUuringuGrupp['Uuring']); log(`Order has results in group '${analysisGroup.UuringuGruppNimi}' for ${groupUuringItems.length} analysis elements`); const existingElements = await getExistingAnalysisResponseElements({ analysisResponseId }); const results: Omit[] = []; for (const groupUuring of groupUuringItems) { const groupUuringElement = groupUuring.UuringuElement; const elementAnalysisResponses = toArray(groupUuringElement.UuringuVastus); const status = groupUuringElement.UuringOlek; log(`Group uuring '${analysisGroup.UuringuGruppNimi}' has status ${status}`); for (const response of elementAnalysisResponses) { const analysisElementOriginalId = groupUuringElement.UuringId; const responseValue = (() => { const valueAsNumber = Number(response.VastuseVaartus); if (isNaN(valueAsNumber)) { return null; } return valueAsNumber; })(); if (!canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })) { continue; } results.push({ analysis_element_original_id: analysisElementOriginalId, 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: responseValue, unit: groupUuringElement.Mootyhik ?? null, original_response_element: groupUuringElement, analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus, comment: groupUuringElement.UuringuKommentaar ?? null, status: status.toString(), }); } } return results; } export async function syncPrivateMessage({ messageResponse, order, }: { messageResponse: NonNullable; order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; }) { const supabase = getSupabaseServerAdminClient(); const externalId = messageResponse.ValisTellimuseId; const orderNumber = messageResponse.TellimuseNumber; const orderStatus = AnalysisOrderStatus[messageResponse.TellimuseOlek]; const log = logger(order, externalId, orderNumber); const { data: analysisOrder } = await supabase .schema('medreport') .from('analysis_orders') .select('id, user_id') .eq('id', order.id) .single() .throwOnError(); const { analysisResponseId } = await upsertAnalysisResponse({ analysisOrderId: order.id, orderNumber, orderStatus, userId: analysisOrder.user_id, }); const analysisGroups = toArray(messageResponse.UuringuGrupp); log(`Order has results for ${analysisGroups.length} analysis groups`); for (const analysisGroup of analysisGroups) { log(`[${analysisGroups.indexOf(analysisGroup) + 1}/${analysisGroups.length}] Syncing analysis group '${analysisGroup.UuringuGruppNimi}'`); const elements = await getAnalysisResponseElementsForGroup({ analysisResponseId, analysisGroup, log, }); for (const element of elements) { const { error } = await supabase .schema('medreport') .from('analysis_response_elements') .insert(element); if (error) { log(`Failed to insert order response elements for response id ${analysisResponseId} (order id: ${analysisOrder.id})`, error); } } } const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId }); const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; if (allOrderResponseElements.length !== expectedOrderResponseElements) { return { isPartial: true }; } return { isCompleted: orderStatus === '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 { messageId: privateMessageId } = privateMessage; const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage( privateMessageId, ); const messageResponse = privateMessageContent?.Saadetis?.Vastus; const medipostExternalOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId; const patientPersonalCode = messageResponse?.Patsient.Isikukood?.toString(); analysisOrderId = Number(medipostExternalOrderId); const hasInvalidOrderId = isNaN(analysisOrderId); if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) { await createMedipostActionLog({ action: 'sync_analysis_results_from_medipost', xml: privateMessageXml, hasAnalysisResults: false, medipostPrivateMessageId: privateMessageId, medusaOrderId, medipostExternalOrderId, hasError: true, }); return { messageId, hasAnalysisResponse: false, hasPartialAnalysisResponse: false, hasFullAnalysisResponse: false, medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId, analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId }; } let analysisOrder: AnalysisOrder; try { analysisOrder = await getAnalysisOrder({ analysisOrderId }) medusaOrderId = analysisOrder.medusa_order_id; } catch (e) { throw new Error(`No analysis order found for Medipost message ValisTellimuseId=${medipostExternalOrderId}`); } const orderPerson = await getAccountAdmin({ primaryOwnerUserId: analysisOrder.user_id }); if (orderPerson.personal_code !== patientPersonalCode) { throw new Error(`Order person personal code does not match Medipost message Patsient.Isikukood=${patientPersonalCode}, orderPerson.personal_code=${orderPerson.personal_code}`); } let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; try { order = await getAnalysisOrder({ medusaOrderId }); } catch (e) { if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) { await deletePrivateMessage(privateMessageId); } throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); } const status = await syncPrivateMessage({ messageResponse, order }); await createMedipostActionLog({ action: 'sync_analysis_results_from_medipost', xml: privateMessageXml, hasAnalysisResults: true, medipostPrivateMessageId: privateMessageId, medusaOrderId, medipostExternalOrderId, }); 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' }); if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) { await deletePrivateMessage(privateMessageId); } 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' }); }