feat(MED-161): move medipost privatemessage logic to separate service

This commit is contained in:
2025-09-17 11:14:54 +03:00
parent 33a6c92841
commit a788e8b587
5 changed files with 402 additions and 380 deletions

View File

@@ -0,0 +1,395 @@
'use server';
import {
AnalysisOrderStatus,
GetMessageListResponse,
MedipostAction,
MedipostOrderResponse,
ResponseUuringuGrupp,
} from '@/lib/types/medipost';
import { toArray } from '@/lib/utils';
import axios from 'axios';
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';
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!;
export async function getLatestPrivateMessageListItem({
excludedMessageIds,
}: {
excludedMessageIds: string[];
}) {
const { data } = await axios.get<GetMessageListResponse>(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 });
}
export async function syncPrivateMessage({
messageResponse,
order,
}: {
messageResponse: NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>;
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
}) {
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
.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;
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);
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 { 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 { data: allOrderResponseElements } = await supabase
.schema('medreport')
.from('analysis_response_elements')
.select('*')
.eq('analysis_response_id', analysisResponseId)
.throwOnError();
const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
if (allOrderResponseElements.length !== expectedOrderResponseElements) {
return { isPartial: true };
}
const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek];
return { isCompleted: statusFromResponse === '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 { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage(
privateMessage.messageId,
);
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
analysisOrderId = Number(privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId);
const hasInvalidOrderId = isNaN(analysisOrderId)
if (hasInvalidOrderId || !messageResponse) {
await createMedipostActionLog({
action: 'sync_analysis_results_from_medipost',
xml: privateMessageXml,
hasAnalysisResults: false,
});
return {
messageId,
hasAnalysisResponse: false,
hasPartialAnalysisResponse: false,
hasFullAnalysisResponse: false,
medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId,
analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId
};
}
const analysisOrder = await getAnalysisOrder({ analysisOrderId: analysisOrderId })
medusaOrderId = analysisOrder.medusa_order_id;
let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
try {
order = await getAnalysisOrder({ medusaOrderId });
} catch (e) {
await deletePrivateMessage(privateMessage.messageId);
throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`);
}
const status = await syncPrivateMessage({ messageResponse, order });
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' });
await deletePrivateMessage(privateMessage.messageId);
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' });
}