feat(MED-161): update sync private message

This commit is contained in:
2025-09-17 11:17:30 +03:00
parent ecc8c2b982
commit 2019c2c1fc
2 changed files with 184 additions and 90 deletions

View File

@@ -1,5 +1,6 @@
import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client"; import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client";
import type { AnalysisResponseElement } from "../types/analysis-response-element"; import type { AnalysisResponseElement } from "../types/analysis-response-element";
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
export async function getExistingAnalysisResponseElements({ export async function getExistingAnalysisResponseElements({
analysisResponseId, analysisResponseId,
@@ -15,3 +16,40 @@ export async function getExistingAnalysisResponseElements({
return data as AnalysisResponseElement[]; 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 };
}

View File

@@ -10,10 +10,12 @@ import {
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
import type { import type {
ResponseUuringuGrupp, ResponseUuringuGrupp,
MedipostOrderResponse MedipostOrderResponse,
ResponseUuring,
} from '@/packages/shared/src/types/medipost-analysis'; } from '@/packages/shared/src/types/medipost-analysis';
import { toArray } from '@/lib/utils'; import { toArray } from '@/lib/utils';
import type { AnalysisOrder } from '~/lib/types/analysis-order'; import type { AnalysisOrder } from '~/lib/types/analysis-order';
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
import { Tables } from '@kit/supabase/database'; import { Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; 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 { getAccountAdmin } from '../account.service';
import { logMedipostDispatch } from '../audit.service'; import { logMedipostDispatch } from '../audit.service';
import { MedipostValidationError } from './MedipostValidationError'; 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 BASE_URL = process.env.MEDIPOST_URL!;
const USER = process.env.MEDIPOST_USER!; const USER = process.env.MEDIPOST_USER!;
@@ -56,68 +58,89 @@ export async function getLatestPrivateMessageListItem({
return getLatestMessage({ messages: data?.messages, excludedMessageIds }); return getLatestMessage({ messages: data?.messages, excludedMessageIds });
} }
export async function syncPrivateMessage({ const logger = (analysisOrder: AnalysisOrder, externalId: string, analysisResponseId: string) => (message: string, error?: PostgrestError | null) => {
messageResponse, const messageFormatted = `[${analysisOrder.id}] [${externalId}] [${analysisResponseId}] ${message}`;
order, if (error) {
}: { console.info(messageFormatted, error);
messageResponse: NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>; } else {
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; console.info(messageFormatted);
}) {
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 function canCreateAnalysisResponseElement({
.schema('medreport') existingElements,
.from('analysis_responses') groupUuring: {
.upsert( UuringuElement: {
{ UuringOlek: status,
analysis_order_id: order.id, UuringId: analysisElementOriginalId,
order_number: messageResponse.TellimuseNumber,
order_status: AnalysisOrderStatus[messageResponse.TellimuseOlek],
user_id: analysisOrder[0].user_id,
}, },
{ onConflict: 'order_number', ignoreDuplicates: false }, },
) responseValue,
.select('id'); log,
}: {
if (error || !analysisResponse?.[0]?.id) { existingElements: AnalysisResponseElement[];
throw new Error( groupUuring: ResponseUuring;
`Failed to insert or update analysis order response (external id: ${messageResponse?.TellimuseNumber})`, responseValue: number | null;
); log: ReturnType<typeof logger>;
}) {
const existingAnalysisResponseElement = existingElements.find(({ analysis_element_original_id }) => analysis_element_original_id === analysisElementOriginalId);
if (!existingAnalysisResponseElement) {
return true;
} }
const analysisGroups = toArray(messageResponse.UuringuGrupp);
console.info(`Order has results for ${analysisGroups.length} analysis groups`);
const responses: Omit< if (Number(existingAnalysisResponseElement.status) > status) {
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>, log(`Analysis response element id=${analysisElementOriginalId} already exists for order in higher status ${existingAnalysisResponseElement.status} than ${status}`);
'id' | 'created_at' | 'updated_at' return false;
>[] = []; }
const analysisResponseId = analysisResponse[0]!.id; 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;
}
for (const analysisGroup of analysisGroups) { return true;
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) => ({ async function getAnalysisResponseElementsForGroup({
analysis_element_original_id: element.UuringId, analysisResponseId,
analysisGroup,
log,
}: {
analysisResponseId: number;
analysisGroup: ResponseUuringuGrupp;
log: ReturnType<typeof logger>;
}) {
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<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id'>[] = [];
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, analysis_response_id: analysisResponseId,
norm_lower: response.NormAlum?.['#text'] ?? null, norm_lower: response.NormAlum?.['#text'] ?? null,
norm_lower_included: norm_lower_included:
@@ -127,47 +150,80 @@ export async function syncPrivateMessage({
norm_upper_included: norm_upper_included:
response.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah', response.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
response_time: response.VastuseAeg ?? null, response_time: response.VastuseAeg ?? null,
response_value: response.VastuseVaartus, response_value: responseValue,
unit: element.Mootyhik ?? null, unit: groupUuringElement.Mootyhik ?? null,
original_response_element: element, original_response_element: groupUuringElement,
analysis_name: element.UuringNimi || element.KNimetus, analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus,
comment: element.UuringuKommentaar ?? '', comment: groupUuringElement.UuringuKommentaar ?? null,
})), status: status.toString(),
); });
} }
} }
const { error: deleteError } = await supabase return results;
}
export async function syncPrivateMessage({
messageResponse,
order,
}: {
messageResponse: NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>;
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') .schema('medreport')
.from('analysis_response_elements') .from('analysis_response_elements')
.delete() .insert(element);
.eq('analysis_response_id', analysisResponseId); if (error) {
log(`Failed to insert order response elements for response id ${analysisResponseId} (order id: ${analysisOrder.id})`, error);
if (deleteError) { }
throw new Error( }
`Failed to clean up response elements for response id ${analysisResponseId}`,
);
} }
const { error: elementInsertError } = await supabase const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId });
.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 expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
if (existingAnalysisResponseElements.length !== expectedOrderResponseElements) { if (allOrderResponseElements.length !== expectedOrderResponseElements) {
return { isPartial: true }; return { isPartial: true };
} }
const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek]; return { isCompleted: orderStatus === 'COMPLETED' };
return { isCompleted: statusFromResponse === 'COMPLETED' };
} }
export async function readPrivateMessageResponse({ export async function readPrivateMessageResponse({