390 lines
12 KiB
TypeScript
390 lines
12 KiB
TypeScript
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<Database>;
|
|
private readonly userAnalysesApi: ReturnType<typeof createUserAnalysesApi>;
|
|
|
|
constructor() {
|
|
this.client = getSupabaseServerAdminClient();
|
|
this.userAnalysesApi = createUserAnalysesApi(this.client);
|
|
}
|
|
|
|
public async readPrivateMessageResponse({
|
|
excludedMessageIds,
|
|
}: {
|
|
excludedMessageIds: string[];
|
|
}): Promise<ISyncResult> {
|
|
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<MedipostOrderResponse['Saadetis']['Vastus']>,
|
|
'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<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,
|
|
});
|
|
}
|
|
|
|
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<AnalysisOrder, 'analysis_element_ids'>;
|
|
}) {
|
|
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,
|
|
};
|
|
}
|
|
|
|
}
|