feat(MED-161): move medipost privatemessage logic to separate service
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { readPrivateMessageResponse } from "~/lib/services/medipost.service";
|
import { readPrivateMessageResponse } from "~/lib/services/medipost/medipostPrivateMessage.service";
|
||||||
|
|
||||||
type ProcessedMessage = {
|
type ProcessedMessage = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import loadEnv from "../handler/load-env";
|
import loadEnv from "../handler/load-env";
|
||||||
import validateApiKey from "../handler/validate-api-key";
|
import validateApiKey from "../handler/validate-api-key";
|
||||||
import { getOrderedAnalysisIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
|
import { getOrderedAnalysisIds } from "~/lib/services/medipost.service";
|
||||||
|
import { sendOrderToMedipost } from "~/lib/services/medipost/medipostPrivateMessage.service";
|
||||||
import { retrieveOrder } from "@lib/data/orders";
|
import { retrieveOrder } from "@lib/data/orders";
|
||||||
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
|
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { listProductTypes } from "@lib/data/products";
|
|||||||
import { placeOrder, retrieveCart } from "@lib/data/cart";
|
import { placeOrder, retrieveCart } from "@lib/data/cart";
|
||||||
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
||||||
import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service';
|
import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service';
|
||||||
import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
|
import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service';
|
||||||
|
import { getOrderedAnalysisIds } from '~/lib/services/medipost.service';
|
||||||
import { createNotificationsApi } from '@kit/notifications/api';
|
import { createNotificationsApi } from '@kit/notifications/api';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
import type { AccountWithParams } from '@kit/accounts/api';
|
import type { AccountWithParams } from '@kit/accounts/api';
|
||||||
|
|||||||
@@ -7,12 +7,9 @@ import {
|
|||||||
|
|
||||||
import { SyncStatus } from '@/lib/types/audit';
|
import { SyncStatus } from '@/lib/types/audit';
|
||||||
import {
|
import {
|
||||||
AnalysisOrderStatus,
|
|
||||||
GetMessageListResponse,
|
GetMessageListResponse,
|
||||||
MedipostAction,
|
MedipostAction,
|
||||||
MedipostOrderResponse,
|
|
||||||
MedipostPublicMessageResponse,
|
MedipostPublicMessageResponse,
|
||||||
ResponseUuringuGrupp,
|
|
||||||
UuringuGrupp,
|
UuringuGrupp,
|
||||||
} from '@/lib/types/medipost';
|
} from '@/lib/types/medipost';
|
||||||
import { toArray } from '@/lib/utils';
|
import { toArray } from '@/lib/utils';
|
||||||
@@ -20,21 +17,15 @@ import axios from 'axios';
|
|||||||
|
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
import { createAnalysisGroup } from './analysis-group.service';
|
import { createAnalysisGroup } from './analysis-group.service';
|
||||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
import { getAnalysisElements } from './analysis-element.service';
|
||||||
import { getAnalysisOrder, updateAnalysisOrderStatus } from './order.service';
|
|
||||||
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
|
||||||
import { getAnalyses } from './analyses.service';
|
import { getAnalyses } from './analyses.service';
|
||||||
import { getAccountAdmin } from './account.service';
|
|
||||||
import { StoreOrder } from '@medusajs/types';
|
import { StoreOrder } from '@medusajs/types';
|
||||||
import { listProducts } from '@lib/data/products';
|
import { listProducts } from '@lib/data/products';
|
||||||
import { listRegions } from '@lib/data/regions';
|
import { listRegions } from '@lib/data/regions';
|
||||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||||
import { MedipostValidationError } from './medipost/MedipostValidationError';
|
|
||||||
import { logMedipostDispatch } from './audit.service';
|
|
||||||
import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service';
|
|
||||||
import { validateMedipostResponse } from './medipost/medipostValidate.service';
|
import { validateMedipostResponse } from './medipost/medipostValidate.service';
|
||||||
import { parseXML } from './util/xml.service';
|
import { parseXML } from './util/xml.service';
|
||||||
import { createMedipostActionLog, getLatestMessage } from './medipost/medipostMessageBase.service';
|
import { getLatestMessage } from './medipost/medipostMessageBase.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!;
|
||||||
@@ -93,163 +84,6 @@ export async function getPublicMessage(messageId: string) {
|
|||||||
return parseXML(data) as MedipostPublicMessageResponse;
|
return parseXML(data) as MedipostPublicMessageResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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 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 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 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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveAnalysisGroup(
|
async function saveAnalysisGroup(
|
||||||
analysisGroup: UuringuGrupp,
|
analysisGroup: UuringuGrupp,
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
@@ -422,215 +256,6 @@ export async function syncPublicMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getOrderedAnalysisIds({
|
export async function getOrderedAnalysisIds({
|
||||||
medusaOrder,
|
medusaOrder,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
395
lib/services/medipost/medipostPrivateMessage.service.ts
Normal file
395
lib/services/medipost/medipostPrivateMessage.service.ts
Normal 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' });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user