diff --git a/lib/services/analysis-order.service.ts b/lib/services/analysis-order.service.ts index e76ca16..2d0f77e 100644 --- a/lib/services/analysis-order.service.ts +++ b/lib/services/analysis-order.service.ts @@ -1,5 +1,6 @@ import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client"; import type { AnalysisResponseElement } from "../types/analysis-response-element"; +import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; export async function getExistingAnalysisResponseElements({ analysisResponseId, @@ -15,3 +16,40 @@ export async function getExistingAnalysisResponseElements({ return data as AnalysisResponseElement[]; } + +export async function upsertAnalysisResponse({ + analysisOrderId, + orderNumber, + orderStatus, + userId, +}: { + analysisOrderId: number; + orderNumber: string; + orderStatus: typeof AnalysisOrderStatus[keyof typeof AnalysisOrderStatus]; + userId: string; +}) { + const { data: analysisResponse } = await getSupabaseServerAdminClient() + .schema('medreport') + .from('analysis_responses') + .upsert( + { + analysis_order_id: analysisOrderId, + order_number: orderNumber, + order_status: orderStatus, + user_id: userId, + }, + { onConflict: 'order_number', ignoreDuplicates: false }, + ) + .select('id') + .throwOnError(); + + + const analysisResponseId = analysisResponse?.[0]?.id; + if (!analysisResponseId) { + throw new Error( + `Failed to insert or update analysis order response (order id: ${analysisOrderId}, order number: ${orderNumber})`, + ); + } + + return { analysisResponseId }; +} diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index d667b8e..ba19fc2 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -10,10 +10,12 @@ import { import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; import type { ResponseUuringuGrupp, - MedipostOrderResponse + 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'; @@ -27,7 +29,7 @@ import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service'; import { getAccountAdmin } from '../account.service'; import { logMedipostDispatch } from '../audit.service'; import { MedipostValidationError } from './MedipostValidationError'; -import { getExistingAnalysisResponseElements } from '../analysis-order.service'; +import { getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -56,6 +58,111 @@ export async function getLatestPrivateMessageListItem({ 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, @@ -63,111 +170,60 @@ export async function syncPrivateMessage({ messageResponse: NonNullable; order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; }) { - const supabase = getSupabaseServerAdminClient() + const supabase = getSupabaseServerAdminClient(); - const { data: analysisOrder, error: analysisOrderError } = await supabase + 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('user_id') - .eq('id', order.id); + .select('id, user_id') + .eq('id', order.id) + .single() + .throwOnError(); - if (analysisOrderError || !analysisOrder?.[0]?.user_id) { - throw new Error( - `Could not find analysis order with id ${messageResponse.ValisTellimuseId}`, - ); - } + const { analysisResponseId } = await upsertAnalysisResponse({ + analysisOrderId: order.id, + orderNumber, + orderStatus, + userId: analysisOrder.user_id, + }); - 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; + log(`Order has results for ${analysisGroups.length} analysis groups`); 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); + log(`[${analysisGroups.indexOf(analysisGroup) + 1}/${analysisGroups.length}] Syncing analysis group '${analysisGroup.UuringuGruppNimi}'`); - 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 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 { 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 existingAnalysisResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId }); + const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId }); const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; - if (existingAnalysisResponseElements.length !== expectedOrderResponseElements) { + if (allOrderResponseElements.length !== expectedOrderResponseElements) { return { isPartial: true }; } - const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek]; - return { isCompleted: statusFromResponse === 'COMPLETED' }; + return { isCompleted: orderStatus === 'COMPLETED' }; } export async function readPrivateMessageResponse({