diff --git a/.env.development b/.env.development index c2104de..ce65402 100644 --- a/.env.development +++ b/.env.development @@ -59,3 +59,37 @@ MONTONIO_API_URL=https://sandbox-stargate.montonio.com # JOBS JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197 + + +MEDUSA_BACKEND_URL=http://5.181.51.38:9000 +MEDUSA_BACKEND_PUBLIC_URL=http://5.181.51.38:9000 +NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_0ec86252438b38ce18d5601f7877e4395d7e0a6afa8687dfea8d37af33015633 + +NEXT_PUBLIC_SUPABASE_URL=http://5.181.51.38:54321 +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU + +NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1Njk4NDYyOCwiZXhwIjoyMDcyNTYwNjI4fQ.1UZR7AqSD9bOy1gtZRGhOCNoESsw2W-DoFDDsNNMwoE + +MEDUSA_BACKEND_URL=https://backoffice-test.medreport.ee +MEDUSA_BACKEND_PUBLIC_URL=https://backoffice-test.medreport.ee +MEDUSA_SECRET_API_KEY=sk_5ac1c1c12c144cd744b6c881050d459e339ddf6a3d14eda271a0cc4f9d3812cb +NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_e740b9ca22b31c4b44862044f001dbcf8f46d47d40f430733d0c75bef14d2d6a + +#MEDUSA_BACKEND_URL=https://backoffice.medreport.ee +#MEDUSA_BACKEND_PUBLIC_URL=https://backoffice.medreport.ee +#MEDUSA_SECRET_API_KEY=sk_fdb1808fbabf62979cc46316aa997378ffbb87882883e8f5c3ee47cee39dcac5 +#NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_068d930c33fea53608a410d84a51935f6ce2ccec5bef8e0ecf75eaee602ac486 + +# PROD +NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0 +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0NjUyODEyMywiZXhwIjoyMDYyMTA0MTIzfQ.KVcnkZ21Pd0XkJho23dZqFHawVTLQqfvF7l2RxsELLk +MEDIPOST_URL=https://medipost2.medisoft.ee:8443/Medipost/MedipostServlet +MEDIPOST_USER=medreport +MEDIPOST_PASSWORD=85MXFFDB7 +MEDIPOST_RECIPIENT=HTI +MEDIPOST_MESSAGE_SENDER=medreport +MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK=false diff --git a/app/api/job/handler/sync-analysis-results.ts b/app/api/job/handler/sync-analysis-results.ts index 06e80d5..054123e 100644 --- a/app/api/job/handler/sync-analysis-results.ts +++ b/app/api/job/handler/sync-analysis-results.ts @@ -1,7 +1,7 @@ import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; -import { readPrivateMessageResponse } from '~/lib/services/medipost/medipostPrivateMessage.service'; +import MedipostResultsSyncService from '~/lib/services/medipost/medipostResultsSync.service'; type ProcessedMessage = { messageId: string; @@ -21,20 +21,21 @@ export default async function syncAnalysisResults() { console.info('Syncing analysis results'); const supabase = getSupabaseServerAdminClient(); const api = createUserAnalysesApi(supabase); + const syncService = new MedipostResultsSyncService(); const processedMessages: ProcessedMessage[] = []; const excludedMessageIds: string[] = []; while (true) { - const result = await readPrivateMessageResponse({ excludedMessageIds }); + const result = await syncService.readPrivateMessageResponse({ excludedMessageIds }); if (result.messageId) { processedMessages.push(result as ProcessedMessage); } - await api.sendAnalysisResultsNotification({ - hasFullAnalysisResponse: result.hasFullAnalysisResponse, - hasPartialAnalysisResponse: result.hasAnalysisResponse, - analysisOrderId: result.analysisOrderId, - }); + // await api.sendAnalysisResultsNotification({ + // hasFullAnalysisResponse: result.hasFullAnalysisResponse, + // hasPartialAnalysisResponse: result.hasAnalysisResponse, + // analysisOrderId: result.analysisOrderId, + // }); if (!result.messageId) { console.info('No more messages to process'); diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index 55376ae..3a5263a 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -1,12 +1,8 @@ 'use server'; -import type { PostgrestError } from '@supabase/supabase-js'; - -import { GetMessageListResponse, MedipostAction } from '@/lib/types/medipost'; +import { MedipostAction } from '@/lib/types/medipost'; import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; -import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; import type { - MedipostOrderResponse, ResponseUuringuGrupp, UuringElement, } from '@/packages/shared/src/types/medipost-analysis'; @@ -14,77 +10,27 @@ import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/se import axios from 'axios'; import { toArray } from '@kit/shared/utils'; -import { Tables } from '@kit/supabase/database'; import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element'; -import type { AnalysisOrder } from '~/lib/types/order'; import { getAccountAdmin } from '../account.service'; import { getAnalyses } from '../analyses.service'; import { getAnalysisElementsAdmin } from '../analysis-element.service'; -import { - getExistingAnalysisResponseElements, - upsertAnalysisResponse, - upsertAnalysisResponseElement, -} from '../analysis-order.service'; import { logMedipostDispatch } from '../audit.service'; import { getAnalysisOrder } from '../order.service'; -import { parseXML } from '../util/xml.service'; import { MedipostValidationError } from './MedipostValidationError'; import { - getLatestMessage, upsertMedipostActionLog, } from './medipostMessageBase.service'; import { validateMedipostResponse } from './medipostValidate.service'; import { OrderedAnalysisElement, composeOrderXML } from './medipostXML.service'; +import type { Logger } from './types'; 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 = - process.env.MEDIPOST_ENABLE_DELETE_RESPONSE_PRIVATE_MESSAGE_ON_READ === - 'true'; - -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 await 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); - } - }; - export async function canCreateAnalysisResponseElement({ existingElements, groupUuring: { @@ -101,7 +47,7 @@ export async function canCreateAnalysisResponseElement({ UuringuElement: Pick; }; responseValue: number | null; - log: ReturnType; + log: Logger; }) { const existingAnalysisResponseElement = existingElements.find( ({ analysis_element_original_id }) => @@ -138,7 +84,7 @@ export async function getAnalysisResponseElementsForGroup({ AnalysisResponseElement, 'analysis_element_original_id' | 'status' | 'response_value' >[]; - log: ReturnType; + log: Logger; }) { const groupUuringItems = toArray( analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'], @@ -211,284 +157,6 @@ export async function getAnalysisResponseElementsForGroup({ return results; } -async function getNewAnalysisResponseElements({ - analysisGroups, - existingElements, - log, -}: { - analysisGroups: ResponseUuringuGrupp[]; - existingElements: AnalysisResponseElement[]; - log: ReturnType; -}) { - 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; -} - -async function hasAllAnalysisResponseElements({ - analysisResponseId, - order, -}: { - analysisResponseId: number; - order: Pick; -}) { - const allOrderResponseElements = await getExistingAnalysisResponseElements({ - analysisResponseId, - }); - const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; - return allOrderResponseElements.length >= expectedOrderResponseElements; -} - -export async function syncPrivateMessage({ - messageResponse: { - ValisTellimuseId: externalId, - TellimuseNumber: orderNumber, - TellimuseOlek, - UuringuGrupp, - }, - order, -}: { - messageResponse: Pick< - NonNullable, - 'ValisTellimuseId' | 'TellimuseNumber' | 'TellimuseOlek' | 'UuringuGrupp' - >; - order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; -}) { - const supabase = getSupabaseServerAdminClient(); - - const orderStatus = AnalysisOrderStatus[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 existingElements = await getExistingAnalysisResponseElements({ - analysisResponseId, - }); - - const analysisGroups = toArray(UuringuGrupp); - log(`Order has results for ${analysisGroups.length} analysis groups`); - const newElements = await 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 hasAllAnalysisResponseElements({ analysisResponseId, order })) - ? { isCompleted: orderStatus === 'COMPLETED' } - : { isPartial: true }; -} - -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) { - 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 { - 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 { - if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) { - await 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 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 createUserAnalysesApi( - getSupabaseServerAdminClient(), - ).updateAnalysisOrderStatus({ - medusaOrderId, - orderStatus: 'PARTIAL_ANALYSIS_RESPONSE', - }); - hasAnalysisResponse = true; - hasPartialAnalysisResponse = true; - } else if (status.isCompleted) { - await createUserAnalysesApi( - getSupabaseServerAdminClient(), - ).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); @@ -508,27 +176,6 @@ export async function sendPrivateMessage(messageXml: string) { 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: (await parseXML(data)) as MedipostOrderResponse, - xml: data as string, - }; -} - export async function sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements, diff --git a/lib/services/medipost/medipostResultsSync.service.ts b/lib/services/medipost/medipostResultsSync.service.ts new file mode 100644 index 0000000..dca079a --- /dev/null +++ b/lib/services/medipost/medipostResultsSync.service.ts @@ -0,0 +1,389 @@ +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, + }; + } + +} diff --git a/lib/services/medipost/types.ts b/lib/services/medipost/types.ts new file mode 100644 index 0000000..68611a6 --- /dev/null +++ b/lib/services/medipost/types.ts @@ -0,0 +1,3 @@ +import type { PostgrestError } from '@supabase/supabase-js'; + +export type Logger = (message: string, error?: PostgrestError | null) => void; diff --git a/lib/services/order.service.ts b/lib/services/order.service.ts index a7128e3..c4d485e 100644 --- a/lib/services/order.service.ts +++ b/lib/services/order.service.ts @@ -72,6 +72,7 @@ export async function getAnalysisOrder({ const { data: order, error } = await query.single(); if (error) { + console.error("Get analysis order error", error); throw new Error( `Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`, ); diff --git a/packages/supabase/src/get-service-role-key.ts b/packages/supabase/src/get-service-role-key.ts index eaa5c00..1f1e494 100644 --- a/packages/supabase/src/get-service-role-key.ts +++ b/packages/supabase/src/get-service-role-key.ts @@ -26,8 +26,8 @@ export function getServiceRoleKey() { */ export function warnServiceRoleKeyUsage() { if (process.env.NODE_ENV !== 'production') { - console.warn( - `[Dev Only] This is a simple warning to let you know you are using the Supabase Service Role. Make sure it's the right call.`, - ); + // console.warn( + // `[Dev Only] This is a simple warning to let you know you are using the Supabase Service Role. Make sure it's the right call.`, + // ); } } diff --git a/run-test-sync-local.sh b/run-test-sync-local.sh old mode 100644 new mode 100755 index 509be1c..76c3075 --- a/run-test-sync-local.sh +++ b/run-test-sync-local.sh @@ -1,6 +1,6 @@ #!/bin/bash -MEDUSA_ORDER_ID="order_01K1TQQHZGPXKDHAH81TDSNGXR" +MEDUSA_ORDER_ID="order_01K9SMB00HJ1W37S1HM0DN2SFV" # HOSTNAME="https://test.medreport.ee" # JOBS_API_TOKEN="fd26ec26-70ed-11f0-9e95-431ac3b15a84" @@ -33,7 +33,7 @@ function sync_analysis_groups_store() { # Requirements # 1. Sync analysis groups from Medipost to B2B -sync_analysis_groups +#sync_analysis_groups # 2. Optional - sync all Medipost analysis groups from B2B to Medusa (or add manually) #sync_analysis_groups_store @@ -41,7 +41,7 @@ sync_analysis_groups # 3. Set up products configurations in Medusa so B2B "Telli analüüs" page shows the product and you can do payment flow # 4. After payment is done, run `send_medipost_test_response` to send the fake test results to Medipost -# send_medipost_test_response +send_medipost_test_response # 5. Run `sync_analysis_results` to sync the all new Medipost results to B2B # sync_analysis_results