import axios from 'axios'; import type { PostgrestError, SupabaseClient } from "@supabase/supabase-js"; import type { Database, Tables } from '@kit/supabase/database'; import { toArray } from '@kit/shared/utils'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import type { GetMessageListResponse } from '~/lib/types/medipost'; import { MedipostAction } from '~/lib/types/medipost'; import type { AnalysisOrder } from "~/lib/types/order"; import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element'; import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; import type { MedipostOrderResponse, ResponseUuringuGrupp, } from '@/packages/shared/src/types/medipost-analysis'; import { createUserAnalysesApi } from "@/packages/features/user-analyses/src/server/api"; import { getAnalysisResponseElementsForGroup } from "./medipostPrivateMessage.service"; import { getAnalysisOrder } from "../order.service"; import { getLatestMessage, upsertMedipostActionLog } from "./medipostMessageBase.service"; import { getAccountAdmin } from "../account.service"; import { getExistingAnalysisResponseElements, upsertAnalysisResponse, upsertAnalysisResponseElement } from "../analysis-order.service"; import { validateMedipostResponse } from './medipostValidate.service'; import { parseXML } from '../util/xml.service'; import type { Logger } from './types'; interface ISyncResult { messageId: string | null; hasAnalysisResponse: boolean; hasPartialAnalysisResponse: boolean; hasFullAnalysisResponse: boolean; medusaOrderId: string | undefined; analysisOrderId: number | undefined; } const ERROR_RESPONSE: ISyncResult = { messageId: null, hasAnalysisResponse: false, hasPartialAnalysisResponse: false, hasFullAnalysisResponse: false, medusaOrderId: undefined, analysisOrderId: undefined, }; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; const PASSWORD = process.env.MEDIPOST_PASSWORD!; const IS_ENABLED_DELETE_PRIVATE_MESSAGE = false; // process.env.MEDIPOST_ENABLE_DELETE_RESPONSE_PRIVATE_MESSAGE_ON_READ === // 'true'; 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); } }; export default class MedipostResultsSyncService { private readonly client: SupabaseClient; private readonly userAnalysesApi: ReturnType; constructor() { this.client = getSupabaseServerAdminClient(); this.userAnalysesApi = createUserAnalysesApi(this.client); } public async readPrivateMessageResponse({ excludedMessageIds, }: { excludedMessageIds: string[]; }): Promise { 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 this.getLatestPrivateMessageListItem({ excludedMessageIds, }); messageId = privateMessage?.messageId ?? null; if (!privateMessage || !messageId) { return ERROR_RESPONSE; } const { messageId: privateMessageId } = privateMessage; const { message: privateMessageContent, xml: privateMessageXml } = await this.getPrivateMessage(privateMessageId); const messageResponse = privateMessageContent?.Saadetis?.Vastus; if (!messageResponse) { console.info(`Skipping private message id=${privateMessageId} because it has no response`); return ERROR_RESPONSE; } const medipostExternalOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId; console.info("PATSIENT", JSON.stringify(messageResponse?.Patsient, null, 2)); const patientPersonalCode = messageResponse?.Patsient.Isikukood?.toString(); analysisOrderId = Number(medipostExternalOrderId); const hasInvalidOrderId = isNaN(analysisOrderId); if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) { console.log({ privateMessageContent, saadetis: privateMessageContent?.Saadetis, messageResponse, }); console.error( `Invalid order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}, patientPersonalCode=${patientPersonalCode}`, ); // await upsertMedipostActionLog({ // action: 'sync_analysis_results_from_medipost', // xml: privateMessageXml, // hasAnalysisResults: false, // medipostPrivateMessageId: privateMessageId, // medusaOrderId, // medipostExternalOrderId, // hasError: true, // }); return { ...ERROR_RESPONSE, messageId, ...(!hasInvalidOrderId && { medusaOrderId, analysisOrderId }), }; } let analysisOrder: AnalysisOrder; try { analysisOrder = await getAnalysisOrder({ analysisOrderId }); medusaOrderId = analysisOrder.medusa_order_id; } catch (e) { console.error("Get analysis order error", e); await this.deletePrivateMessage(privateMessageId); 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}`, ); } const status = await this.syncPrivateMessage({ messageResponse, order: analysisOrder, }); console.info( `Successfully synced analysis results from Medipost message privateMessageId=${privateMessageId}`, ); await upsertMedipostActionLog({ action: 'sync_analysis_results_from_medipost', xml: privateMessageXml, hasAnalysisResults: true, medipostPrivateMessageId: privateMessageId, medusaOrderId, medipostExternalOrderId, }); if (status.isPartial) { await this.userAnalysesApi.updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE', }); hasAnalysisResponse = true; hasPartialAnalysisResponse = true; } else if (status.isCompleted) { await this.userAnalysesApi.updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE', }); await this.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, }; } private async syncPrivateMessage({ messageResponse: { ValisTellimuseId: externalId, TellimuseNumber: orderNumber, TellimuseOlek, UuringuGrupp, }, order, }: { messageResponse: Pick< NonNullable, 'ValisTellimuseId' | 'TellimuseNumber' | 'TellimuseOlek' | 'UuringuGrupp' >; order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; }) { const orderStatus = AnalysisOrderStatus[TellimuseOlek]; const log = logger(order, externalId, orderNumber); const { data: analysisOrder } = await this.client .schema('medreport') .from('analysis_orders') .select('id, user_id') .eq('id', order.id) .single() .throwOnError(); console.info("ANALYSIS ORDER", JSON.stringify(analysisOrder, null, 2)); throw new Error("early return"); const { analysisResponseId } = await upsertAnalysisResponse({ analysisOrderId: order.id, orderNumber, orderStatus, userId: analysisOrder.user_id, }); const existingElements = await getExistingAnalysisResponseElements({ analysisResponseId, }); const analysisGroups = toArray(UuringuGrupp); log(`Order has results for ${analysisGroups.length} analysis groups`); const newElements = await this.getNewAnalysisResponseElements({ analysisGroups, existingElements, log, }); for (const element of newElements) { try { await upsertAnalysisResponseElement({ element: { ...element, analysis_response_id: analysisResponseId, }, }); } catch (e) { log( `Failed to create order response element for response id ${analysisResponseId}, element id '${element.analysis_element_original_id}' (order id: ${order.id})`, e as PostgrestError, ); } } return (await this.hasAllAnalysisResponseElements({ analysisResponseId, order })) ? { isCompleted: orderStatus === 'COMPLETED' } : { isPartial: true }; } private async 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 await getLatestMessage({ messages: data?.messages, excludedMessageIds, }); } private async getNewAnalysisResponseElements({ analysisGroups, existingElements, log, }: { analysisGroups: ResponseUuringuGrupp[]; existingElements: AnalysisResponseElement[]; log: Logger; }) { const newElements: Omit< AnalysisResponseElement, 'created_at' | 'updated_at' | 'id' | 'analysis_response_id' >[] = []; for (const analysisGroup of analysisGroups) { log( `[${analysisGroups.indexOf(analysisGroup) + 1}/${analysisGroups.length}] Syncing analysis group '${analysisGroup.UuringuGruppNimi}'`, ); const elements = await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log, }); newElements.push(...elements); } return newElements; } private async hasAllAnalysisResponseElements({ analysisResponseId, order, }: { analysisResponseId: number; order: Pick; }) { const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId, }); const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; return allOrderResponseElements.length >= expectedOrderResponseElements; } private async deletePrivateMessage(messageId: string) { if (!IS_ENABLED_DELETE_PRIVATE_MESSAGE) { console.info(`Skipping delete private message id=${messageId} because deleting is not enabled`); return; } 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})`); } } private async 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: (await parseXML(data)) as MedipostOrderResponse, xml: data as string, }; } }