Move medipostPrivateMessage.service to separate classes, improve logging
This commit is contained in:
@@ -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<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 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<UuringElement, 'UuringOlek' | 'UuringId'>;
|
||||
};
|
||||
responseValue: number | null;
|
||||
log: ReturnType<typeof logger>;
|
||||
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<typeof logger>;
|
||||
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<typeof 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;
|
||||
}
|
||||
|
||||
async function hasAllAnalysisResponseElements({
|
||||
analysisResponseId,
|
||||
order,
|
||||
}: {
|
||||
analysisResponseId: number;
|
||||
order: Pick<AnalysisOrder, 'analysis_element_ids'>;
|
||||
}) {
|
||||
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<MedipostOrderResponse['Saadetis']['Vastus']>,
|
||||
'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,
|
||||
@@ -597,6 +244,7 @@ export async function sendOrderToMedipost({
|
||||
medusaOrderId,
|
||||
responseXml: e.response,
|
||||
hasError: true,
|
||||
medipostPrivateMessageId: `send-order-to-medipost-${Date.now()}`,
|
||||
});
|
||||
} else {
|
||||
console.error(
|
||||
@@ -613,6 +261,7 @@ export async function sendOrderToMedipost({
|
||||
hasAnalysisResults: false,
|
||||
medusaOrderId,
|
||||
hasError: true,
|
||||
medipostPrivateMessageId: `send-order-to-medipost-${Date.now()}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -631,6 +280,7 @@ export async function sendOrderToMedipost({
|
||||
xml: orderXml,
|
||||
hasAnalysisResults: false,
|
||||
medusaOrderId,
|
||||
medipostPrivateMessageId: `send-order-to-medipost-${Date.now()}`,
|
||||
});
|
||||
await createUserAnalysesApi(
|
||||
getSupabaseServerAdminClient(),
|
||||
|
||||
Reference in New Issue
Block a user