Merge branch 'develop' of https://github.com/MR-medreport/MRB2B into MED-103
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { Tables } from '@/packages/supabase/src/database.types';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import type { IUuringElement } from "./medipost.types";
|
||||
import type { IUuringElement } from "./medipost/medipost.types";
|
||||
|
||||
export type AnalysesWithGroupsAndElements = ({
|
||||
analysis_elements: Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Json, Tables } from '@kit/supabase/database';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import type { IMaterialGroup, IUuringElement } from './medipost.types';
|
||||
import type { IMaterialGroup, IUuringElement } from './medipost/medipost.types';
|
||||
|
||||
export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
||||
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
||||
};
|
||||
|
||||
export async function getAnalysisElements({
|
||||
getAll,
|
||||
originalIds,
|
||||
ids,
|
||||
analysisGroupId,
|
||||
}: {
|
||||
getAll?: boolean;
|
||||
originalIds?: string[];
|
||||
ids?: number[];
|
||||
analysisGroupId?: number;
|
||||
}): Promise<AnalysisElement[]> {
|
||||
const query = getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
@@ -19,14 +23,26 @@ export async function getAnalysisElements({
|
||||
.select(`*, analysis_groups(*)`)
|
||||
.order('order', { ascending: true });
|
||||
|
||||
if (Array.isArray(originalIds)) {
|
||||
const hasOriginalIdsFilter = Array.isArray(originalIds);
|
||||
const hasIdsFilter = Array.isArray(ids);
|
||||
const hasAnalysisGroupIdFilter = typeof analysisGroupId === 'number';
|
||||
|
||||
if (!hasOriginalIdsFilter && !hasIdsFilter && !hasAnalysisGroupIdFilter && getAll !== true) {
|
||||
throw new Error('Either originalIds, ids, or analysisGroupId must be provided');
|
||||
}
|
||||
|
||||
if (hasOriginalIdsFilter) {
|
||||
query.in('analysis_id_original', [...new Set(originalIds)]);
|
||||
}
|
||||
|
||||
if (Array.isArray(ids)) {
|
||||
if (hasIdsFilter) {
|
||||
query.in('id', ids);
|
||||
}
|
||||
|
||||
if (hasAnalysisGroupIdFilter) {
|
||||
query.eq('parent_analysis_group_id', analysisGroupId);
|
||||
}
|
||||
|
||||
const { data: analysisElements, error } = await query;
|
||||
|
||||
if (error) {
|
||||
|
||||
68
lib/services/analysis-order.service.ts
Normal file
68
lib/services/analysis-order.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client";
|
||||
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
|
||||
|
||||
import type { AnalysisResponseElement } from "../types/analysis-response-element";
|
||||
|
||||
export async function getExistingAnalysisResponseElements({
|
||||
analysisResponseId,
|
||||
}: {
|
||||
analysisResponseId: number;
|
||||
}): Promise<AnalysisResponseElement[]> {
|
||||
const { data } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_response_elements')
|
||||
.select('*')
|
||||
.eq('analysis_response_id', analysisResponseId)
|
||||
.throwOnError();
|
||||
|
||||
return data as AnalysisResponseElement[];
|
||||
}
|
||||
|
||||
export async function createAnalysisResponseElement({
|
||||
element,
|
||||
}: {
|
||||
element: Omit<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id'>;
|
||||
}) {
|
||||
await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_response_elements')
|
||||
.insert(element)
|
||||
.throwOnError();
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { renderBookTimeFailedEmail } from '@kit/email-templates';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { TimeSlotResponse } from '../../app/home/(user)/_components/booking/booking.context';
|
||||
import { TimeSlotResponse } from '~/home/(user)/_components/booking/booking.context';
|
||||
import { sendEmailFromTemplate } from './mailer.service';
|
||||
import { handleDeleteCartItem } from './medusaCart.service';
|
||||
|
||||
|
||||
@@ -1,791 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
SupabaseClient,
|
||||
createClient as createCustomClient,
|
||||
} from '@supabase/supabase-js';
|
||||
|
||||
import { SyncStatus } from '@/lib/types/audit';
|
||||
import {
|
||||
AnalysisOrderStatus,
|
||||
GetMessageListResponse,
|
||||
IMedipostResponseXMLBase,
|
||||
MedipostAction,
|
||||
MedipostOrderResponse,
|
||||
MedipostPublicMessageResponse,
|
||||
Message,
|
||||
ResponseUuringuGrupp,
|
||||
UuringuGrupp,
|
||||
} from '@/lib/types/medipost';
|
||||
import { toArray } from '@/lib/utils';
|
||||
import axios from 'axios';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { createAnalysisGroup } from './analysis-group.service';
|
||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||
import { getAnalysisOrder, updateAnalysisOrderStatus } from './order.service';
|
||||
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
||||
import { getAnalyses } from './analyses.service';
|
||||
import { getAccountAdmin } from './account.service';
|
||||
import { StoreOrder } from '@medusajs/types';
|
||||
import { listProducts } from '@lib/data/products';
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
import { MedipostValidationError } from './medipost/MedipostValidationError';
|
||||
import { logMedipostDispatch } from './audit.service';
|
||||
import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service';
|
||||
|
||||
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 ANALYSIS_PACKAGE_HANDLE_PREFIX = 'analysis-package-';
|
||||
|
||||
function parseXML(xml: string) {
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
return parser.parse(xml);
|
||||
}
|
||||
|
||||
export async function validateMedipostResponse(response: string, { canHaveEmptyCode = false }: { canHaveEmptyCode?: boolean } = {}) {
|
||||
const parsed: IMedipostResponseXMLBase = parseXML(response);
|
||||
const code = parsed.ANSWER?.CODE;
|
||||
if (canHaveEmptyCode) {
|
||||
if (code && code !== 0) {
|
||||
console.error("Bad response", response);
|
||||
throw new MedipostValidationError(response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof code !== 'number' || (code !== 0 && !canHaveEmptyCode)) {
|
||||
console.error("Bad response", response);
|
||||
throw new MedipostValidationError(response);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMessages() {
|
||||
try {
|
||||
const publicMessage = await getLatestPublicMessageListItem();
|
||||
|
||||
if (!publicMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Teenused tuleb mappida kokku MedReport teenustega. <UuringId> alusel
|
||||
return getPublicMessage(publicMessage.messageId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLatestPublicMessageListItem() {
|
||||
const { data } = await axios.get<GetMessageListResponse>(BASE_URL, {
|
||||
params: {
|
||||
Action: MedipostAction.GetPublicMessageList,
|
||||
User: USER,
|
||||
Password: PASSWORD,
|
||||
Sender: RECIPIENT,
|
||||
// LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created
|
||||
// MessageType check only for messages of certain type
|
||||
},
|
||||
});
|
||||
|
||||
if (data.code && data.code !== 0) {
|
||||
throw new Error('Failed to get public message list');
|
||||
}
|
||||
|
||||
return getLatestMessage({ messages: data?.messages });
|
||||
}
|
||||
|
||||
export async function getPublicMessage(messageId: string) {
|
||||
const { data } = await axios.get(BASE_URL, {
|
||||
params: {
|
||||
Action: MedipostAction.GetPublicMessage,
|
||||
User: USER,
|
||||
Password: PASSWORD,
|
||||
MessageId: messageId,
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/xml',
|
||||
},
|
||||
});
|
||||
await validateMedipostResponse(data);
|
||||
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(
|
||||
analysisGroup: UuringuGrupp,
|
||||
supabase: SupabaseClient,
|
||||
) {
|
||||
const analysisGroupId = await createAnalysisGroup({
|
||||
id: analysisGroup.UuringuGruppId,
|
||||
name: analysisGroup.UuringuGruppNimi,
|
||||
order: analysisGroup.UuringuGruppJarjekord,
|
||||
});
|
||||
|
||||
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
||||
const codes: Partial<Tables<{ schema: 'medreport' }, 'codes'>>[] =
|
||||
analysisGroupCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: analysisGroupId,
|
||||
}));
|
||||
|
||||
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
||||
|
||||
for (const item of analysisGroupItems) {
|
||||
const analysisElement = item.UuringuElement;
|
||||
|
||||
const { data: insertedAnalysisElement, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.upsert(
|
||||
{
|
||||
analysis_id_oid: analysisElement.UuringIdOID,
|
||||
analysis_id_original: analysisElement.UuringId,
|
||||
tehik_short_loinc: analysisElement.TLyhend,
|
||||
tehik_loinc_name: analysisElement.KNimetus,
|
||||
analysis_name_lab: analysisElement.UuringNimi,
|
||||
order: analysisElement.Jarjekord,
|
||||
parent_analysis_group_id: analysisGroupId,
|
||||
material_groups: toArray(item.MaterjalideGrupp),
|
||||
},
|
||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
if (error || !insertedAnalysisElement[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis element (id: ${analysisElement.UuringId}), error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const insertedAnalysisElementId = insertedAnalysisElement[0].id;
|
||||
|
||||
if (analysisElement.Kood) {
|
||||
const analysisElementCodes = toArray(analysisElement.Kood);
|
||||
codes.push(
|
||||
...analysisElementCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_element_id: insertedAnalysisElementId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const analyses = analysisElement.UuringuElement;
|
||||
if (analyses?.length) {
|
||||
for (const analysis of analyses) {
|
||||
const { data: insertedAnalysis, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analyses')
|
||||
.upsert(
|
||||
{
|
||||
analysis_id_oid: analysis.UuringIdOID,
|
||||
analysis_id_original: analysis.UuringId,
|
||||
tehik_short_loinc: analysis.TLyhend,
|
||||
tehik_loinc_name: analysis.KNimetus,
|
||||
analysis_name_lab: analysis.UuringNimi,
|
||||
order: analysis.Jarjekord,
|
||||
parent_analysis_element_id: insertedAnalysisElementId,
|
||||
},
|
||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
if (error || !insertedAnalysis[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis (id: ${analysis.UuringId}) error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const insertedAnalysisId = insertedAnalysis[0].id;
|
||||
if (analysisElement.Kood) {
|
||||
const analysisCodes = toArray(analysis.Kood);
|
||||
|
||||
codes.push(
|
||||
...analysisCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_id: insertedAnalysisId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { error: codesError } = await supabase
|
||||
.schema('medreport')
|
||||
.from('codes')
|
||||
.upsert(codes, { ignoreDuplicates: false });
|
||||
|
||||
if (codesError?.code) {
|
||||
throw new Error(
|
||||
`Failed to insert codes (analysis group id: ${analysisGroup.UuringuGruppId})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncPublicMessage(
|
||||
message?: MedipostPublicMessageResponse | null,
|
||||
) {
|
||||
const supabase = createCustomClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const providers = toArray(message?.Saadetis?.Teenused.Teostaja);
|
||||
const analysisGroups = providers.flatMap((provider) =>
|
||||
toArray(provider.UuringuGrupp),
|
||||
);
|
||||
if (!message || !analysisGroups.length) {
|
||||
return supabase.schema('audit').from('sync_entries').insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
comment: 'No data received',
|
||||
status: SyncStatus.Fail,
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
}
|
||||
|
||||
for (const analysisGroup of analysisGroups) {
|
||||
await saveAnalysisGroup(analysisGroup, supabase);
|
||||
}
|
||||
|
||||
await supabase.schema('audit').from('sync_entries').insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: SyncStatus.Success,
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
await supabase
|
||||
.schema('audit')
|
||||
.from('sync_entries')
|
||||
.insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: SyncStatus.Fail,
|
||||
comment: JSON.stringify(e),
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getLatestMessage({
|
||||
messages,
|
||||
excludedMessageIds,
|
||||
}: {
|
||||
messages?: Message[];
|
||||
excludedMessageIds?: string[];
|
||||
}) {
|
||||
if (!messages?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filtered = messages.filter(({ messageId }) => !excludedMessageIds?.includes(messageId));
|
||||
|
||||
if (!filtered.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filtered.reduce((prev, current) =>
|
||||
Number(prev.messageId) > Number(current.messageId) ? prev : current,
|
||||
{ messageId: '' } as Message,
|
||||
);
|
||||
}
|
||||
|
||||
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({
|
||||
medusaOrder,
|
||||
}: {
|
||||
medusaOrder: StoreOrder;
|
||||
}): Promise<{
|
||||
analysisElementId?: number;
|
||||
analysisId?: number;
|
||||
}[]> {
|
||||
const countryCodes = await listRegions();
|
||||
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
|
||||
|
||||
async function getOrderedAnalysisElements(medusaOrder: StoreOrder) {
|
||||
const originalIds = (medusaOrder?.items ?? [])
|
||||
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
||||
.filter((a) => typeof a === 'string') as string[];
|
||||
const analysisElements = await getAnalysisElements({ originalIds });
|
||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||
}
|
||||
|
||||
async function getOrderedAnalyses(medusaOrder: StoreOrder) {
|
||||
const originalIds = (medusaOrder?.items ?? [])
|
||||
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
||||
.filter((a) => typeof a === 'string') as string[];
|
||||
const analyses = await getAnalyses({ originalIds });
|
||||
return analyses.map(({ id }) => ({ analysisId: id }));
|
||||
}
|
||||
|
||||
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
|
||||
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
|
||||
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
|
||||
if (orderedPackageIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
console.info(`Order has ${orderedPackageIds.length} packages`);
|
||||
const { response: { products: orderedPackagesProducts } } = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, id: orderedPackageIds },
|
||||
});
|
||||
console.info(`Order has ${orderedPackagesProducts.length} packages = ${JSON.stringify(orderedPackageIds, null, 2)}`);
|
||||
if (orderedPackagesProducts.length !== orderedPackageIds.length) {
|
||||
throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`);
|
||||
}
|
||||
|
||||
const ids = getAnalysisElementMedusaProductIds(
|
||||
orderedPackagesProducts.map(({ id, metadata }) => ({
|
||||
metadata,
|
||||
variant: orderedPackages.find(({ product }) => product?.id === id)?.variant,
|
||||
})),
|
||||
);
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const { response: { products: analysisPackagesProducts } } = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, id: ids },
|
||||
});
|
||||
if (analysisPackagesProducts.length !== ids.length) {
|
||||
throw new Error(`Got ${analysisPackagesProducts.length} analysis packages products, expected ${ids.length}`);
|
||||
}
|
||||
|
||||
const originalIds = analysisPackagesProducts
|
||||
.map(({ metadata }) => metadata?.analysisIdOriginal)
|
||||
.filter((id) => typeof id === 'string');
|
||||
if (originalIds.length !== ids.length) {
|
||||
throw new Error(`Got ${originalIds.length} analysis packages products with analysisIdOriginal, expected ${ids.length}`);
|
||||
}
|
||||
const analysisElements = await getAnalysisElements({ originalIds });
|
||||
|
||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||
}
|
||||
|
||||
const [analysisPackageElements, orderedAnalysisElements, orderedAnalyses] = await Promise.all([
|
||||
getOrderedAnalysisPackages(medusaOrder),
|
||||
getOrderedAnalysisElements(medusaOrder),
|
||||
getOrderedAnalyses(medusaOrder),
|
||||
]);
|
||||
|
||||
return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses];
|
||||
}
|
||||
|
||||
export async function createMedipostActionLog({
|
||||
action,
|
||||
xml,
|
||||
hasAnalysisResults = false,
|
||||
medusaOrderId,
|
||||
responseXml,
|
||||
hasError = false,
|
||||
}: {
|
||||
action:
|
||||
| 'send_order_to_medipost'
|
||||
| 'sync_analysis_results_from_medipost'
|
||||
| 'send_fake_analysis_results_to_medipost'
|
||||
| 'send_analysis_results_to_medipost';
|
||||
xml: string;
|
||||
hasAnalysisResults?: boolean;
|
||||
medusaOrderId?: string | null;
|
||||
responseXml?: string | null;
|
||||
hasError?: boolean;
|
||||
}) {
|
||||
await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('medipost_actions')
|
||||
.insert({
|
||||
action,
|
||||
xml,
|
||||
has_analysis_results: hasAnalysisResults,
|
||||
medusa_order_id: medusaOrderId,
|
||||
response_xml: responseXml,
|
||||
has_error: hasError,
|
||||
})
|
||||
.select('id')
|
||||
.throwOnError();
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export interface IMedipostPublicMessageDataParsed {
|
||||
Koefitsient: number;
|
||||
Hind: number;
|
||||
}[];
|
||||
UuringuElement: IUuringElement;
|
||||
UuringuElement?: IUuringElement[];
|
||||
}[];
|
||||
MaterjalideGrupp: IMaterialGroup[];
|
||||
Kood: {
|
||||
68
lib/services/medipost/medipostMessageBase.service.ts
Normal file
68
lib/services/medipost/medipostMessageBase.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
'use server';
|
||||
|
||||
import type { Message } from '@/lib/types/medipost';
|
||||
|
||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||
|
||||
export async function getLatestMessage({
|
||||
messages,
|
||||
excludedMessageIds,
|
||||
}: {
|
||||
messages?: Message[];
|
||||
excludedMessageIds?: string[];
|
||||
}) {
|
||||
if (!messages?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filtered = messages.filter(({ messageId }) => !excludedMessageIds?.includes(messageId));
|
||||
|
||||
if (!filtered.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filtered.reduce((prev, current) =>
|
||||
Number(prev.messageId) > Number(current.messageId) ? prev : current,
|
||||
{ messageId: '' } as Message,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createMedipostActionLog({
|
||||
action,
|
||||
xml,
|
||||
hasAnalysisResults = false,
|
||||
medusaOrderId,
|
||||
responseXml,
|
||||
hasError = false,
|
||||
medipostExternalOrderId,
|
||||
medipostPrivateMessageId,
|
||||
}: {
|
||||
action:
|
||||
| 'send_order_to_medipost'
|
||||
| 'sync_analysis_results_from_medipost'
|
||||
| 'send_fake_analysis_results_to_medipost'
|
||||
| 'send_analysis_results_to_medipost';
|
||||
xml: string;
|
||||
hasAnalysisResults?: boolean;
|
||||
medusaOrderId?: string | null;
|
||||
responseXml?: string | null;
|
||||
hasError?: boolean;
|
||||
medipostExternalOrderId?: string | null;
|
||||
medipostPrivateMessageId?: string | null;
|
||||
}) {
|
||||
await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('medipost_actions')
|
||||
.insert({
|
||||
action,
|
||||
xml,
|
||||
has_analysis_results: hasAnalysisResults,
|
||||
medusa_order_id: medusaOrderId,
|
||||
response_xml: responseXml,
|
||||
has_error: hasError,
|
||||
medipost_external_order_id: medipostExternalOrderId,
|
||||
medipost_private_message_id: medipostPrivateMessageId,
|
||||
})
|
||||
.select('id')
|
||||
.throwOnError();
|
||||
}
|
||||
112
lib/services/medipost/medipostPrivateMessage.service.test.ts
Normal file
112
lib/services/medipost/medipostPrivateMessage.service.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { AnalysisResponseElement } from "~/lib/types/analysis-response-element";
|
||||
import { canCreateAnalysisResponseElement, getAnalysisResponseElementsForGroup } from "./medipostPrivateMessage.service";
|
||||
import { ResponseUuring } from "@/packages/shared/src/types/medipost-analysis";
|
||||
|
||||
type TestExistingElement = Pick<AnalysisResponseElement, 'analysis_element_original_id' | 'status' | 'response_value'>;
|
||||
|
||||
describe('medipostPrivateMessage.service', () => {
|
||||
describe('canCreateAnalysisResponseElement', () => {
|
||||
it('should return true if the analysis response element does not exist', async () => {
|
||||
const existingElements = [] as TestExistingElement[];
|
||||
const groupUuring = {
|
||||
UuringuElement: {
|
||||
UuringOlek: 1,
|
||||
UuringId: '1',
|
||||
},
|
||||
} as const;
|
||||
const responseValue = 1;
|
||||
const log = jest.fn();
|
||||
expect(await canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the analysis response element exists and the status is higher', async () => {
|
||||
const existingElements = [{ analysis_element_original_id: '1', status: '2', response_value: 1 }] as TestExistingElement[];
|
||||
const groupUuring = {
|
||||
UuringuElement: {
|
||||
UuringOlek: 1,
|
||||
UuringId: '1',
|
||||
},
|
||||
} as const;
|
||||
const responseValue = 1;
|
||||
const log = jest.fn();
|
||||
expect(await canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAnalysisResponseElementsForGroup', () => {
|
||||
it('should return single new element', async () => {
|
||||
const analysisGroup = {
|
||||
UuringuGruppNimi: '1',
|
||||
Uuring: [
|
||||
{
|
||||
UuringuElement: {
|
||||
UuringOlek: 1,
|
||||
UuringId: '1',
|
||||
UuringuVastus: [{ VastuseVaartus: '1' }],
|
||||
},
|
||||
},
|
||||
] as unknown as ResponseUuring[],
|
||||
} as const;
|
||||
const existingElements = [] as TestExistingElement[];
|
||||
const log = jest.fn();
|
||||
expect(await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log }))
|
||||
.toEqual([{
|
||||
analysis_element_original_id: '1',
|
||||
analysis_name: undefined,
|
||||
comment: null,
|
||||
norm_lower: null,
|
||||
norm_lower_included: false,
|
||||
norm_status: undefined,
|
||||
norm_upper: null,
|
||||
norm_upper_included: false,
|
||||
response_time: null,
|
||||
response_value: 1,
|
||||
unit: null,
|
||||
original_response_element: {
|
||||
UuringOlek: 1,
|
||||
UuringId: '1',
|
||||
UuringuVastus: [{ VastuseVaartus: '1' }],
|
||||
},
|
||||
status: '1',
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should return no new element if element already exists in higher status', async () => {
|
||||
const analysisGroup = {
|
||||
UuringuGruppNimi: '1',
|
||||
Uuring: [
|
||||
{
|
||||
UuringuElement: {
|
||||
UuringOlek: 1,
|
||||
UuringId: '1',
|
||||
UuringuVastus: [{ VastuseVaartus: '1' }],
|
||||
},
|
||||
},
|
||||
] as unknown as ResponseUuring[],
|
||||
} as const;
|
||||
const existingElements = [{ analysis_element_original_id: '1', status: '2', response_value: 1 }] as TestExistingElement[];
|
||||
const log = jest.fn();
|
||||
expect(await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log }))
|
||||
.toEqual([]);
|
||||
});
|
||||
|
||||
it('should return no new element if element already exists with response value', async () => {
|
||||
const analysisGroup = {
|
||||
UuringuGruppNimi: '1',
|
||||
Uuring: [
|
||||
{
|
||||
UuringuElement: {
|
||||
UuringOlek: 1,
|
||||
UuringId: '1',
|
||||
UuringuVastus: [{ VastuseVaartus: '' }],
|
||||
},
|
||||
},
|
||||
] as unknown as ResponseUuring[],
|
||||
} as const;
|
||||
const existingElements = [{ analysis_element_original_id: '1', status: '1', response_value: 1 }] as TestExistingElement[];
|
||||
const log = jest.fn();
|
||||
expect(await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log }))
|
||||
.toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
515
lib/services/medipost/medipostPrivateMessage.service.ts
Normal file
515
lib/services/medipost/medipostPrivateMessage.service.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
'use server';
|
||||
|
||||
import type { PostgrestError } from '@supabase/supabase-js';
|
||||
import axios from 'axios';
|
||||
|
||||
import {
|
||||
GetMessageListResponse,
|
||||
MedipostAction,
|
||||
} from '@/lib/types/medipost';
|
||||
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
|
||||
import type {
|
||||
ResponseUuringuGrupp,
|
||||
MedipostOrderResponse,
|
||||
UuringElement,
|
||||
} from '@/packages/shared/src/types/medipost-analysis';
|
||||
import { toArray } from '@/lib/utils';
|
||||
import type { AnalysisOrder } from '~/lib/types/analysis-order';
|
||||
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
||||
|
||||
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';
|
||||
import { createAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service';
|
||||
|
||||
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 = false as boolean;
|
||||
|
||||
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: {
|
||||
UuringuElement: {
|
||||
UuringOlek: status,
|
||||
UuringId: analysisElementOriginalId,
|
||||
},
|
||||
},
|
||||
responseValue,
|
||||
log,
|
||||
}: {
|
||||
existingElements: Pick<AnalysisResponseElement, 'analysis_element_original_id' | 'status' | 'response_value'>[];
|
||||
groupUuring: { UuringuElement: Pick<UuringElement, 'UuringOlek' | 'UuringId'> };
|
||||
responseValue: number | null;
|
||||
log: ReturnType<typeof logger>;
|
||||
}) {
|
||||
const existingAnalysisResponseElement = existingElements.find(({ analysis_element_original_id }) => analysis_element_original_id === analysisElementOriginalId);
|
||||
if (!existingAnalysisResponseElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Number(existingAnalysisResponseElement.status) > status) {
|
||||
log(`Analysis response element id=${analysisElementOriginalId} already exists for order in higher status ${existingAnalysisResponseElement.status} than ${status}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export async function getAnalysisResponseElementsForGroup({
|
||||
analysisGroup,
|
||||
existingElements,
|
||||
log,
|
||||
}: {
|
||||
analysisGroup: Pick<ResponseUuringuGrupp, 'UuringuGruppNimi' | 'Uuring'>;
|
||||
existingElements: Pick<AnalysisResponseElement, 'analysis_element_original_id' | 'status' | 'response_value'>[];
|
||||
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 results: Omit<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id' | 'analysis_response_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 vastuseVaartus = response.VastuseVaartus;
|
||||
const responseValue = (() => {
|
||||
const valueAsNumber = Number(vastuseVaartus);
|
||||
if (isNaN(valueAsNumber)) {
|
||||
return null;
|
||||
}
|
||||
return valueAsNumber;
|
||||
})();
|
||||
|
||||
if (!await canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const responseValueIsNumeric = responseValue !== null;
|
||||
const responseValueIsNegative = vastuseVaartus === 'Negatiivne';
|
||||
const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires';
|
||||
|
||||
results.push({
|
||||
analysis_element_original_id: analysisElementOriginalId,
|
||||
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: responseValue,
|
||||
unit: groupUuringElement.Mootyhik ?? null,
|
||||
original_response_element: groupUuringElement,
|
||||
analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus,
|
||||
comment: groupUuringElement.UuringuKommentaar ?? null,
|
||||
status: status.toString(),
|
||||
response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm,
|
||||
response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 createAnalysisResponseElement({
|
||||
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) {
|
||||
await createMedipostActionLog({
|
||||
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 (e) {
|
||||
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 });
|
||||
|
||||
await createMedipostActionLog({
|
||||
action: 'sync_analysis_results_from_medipost',
|
||||
xml: privateMessageXml,
|
||||
hasAnalysisResults: true,
|
||||
medipostPrivateMessageId: privateMessageId,
|
||||
medusaOrderId,
|
||||
medipostExternalOrderId,
|
||||
});
|
||||
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' });
|
||||
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);
|
||||
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: (await 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' });
|
||||
}
|
||||
33
lib/services/medipost/medipostPublicMessage.service.ts
Normal file
33
lib/services/medipost/medipostPublicMessage.service.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
GetMessageListResponse,
|
||||
MedipostAction,
|
||||
} from '@/lib/types/medipost';
|
||||
import axios from 'axios';
|
||||
|
||||
import { getLatestMessage } from './medipostMessageBase.service';
|
||||
|
||||
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 getLatestPublicMessageListItem() {
|
||||
const { data } = await axios.get<GetMessageListResponse>(BASE_URL, {
|
||||
params: {
|
||||
Action: MedipostAction.GetPublicMessageList,
|
||||
User: USER,
|
||||
Password: PASSWORD,
|
||||
Sender: RECIPIENT,
|
||||
// LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created
|
||||
// MessageType check only for messages of certain type
|
||||
},
|
||||
});
|
||||
|
||||
if (data.code && data.code !== 0) {
|
||||
throw new Error('Failed to get public message list');
|
||||
}
|
||||
|
||||
return await getLatestMessage({ messages: data?.messages });
|
||||
}
|
||||
@@ -16,9 +16,9 @@ import { uniqBy } from 'lodash';
|
||||
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { formatDate } from 'date-fns';
|
||||
import { getAnalyses } from './analyses.service';
|
||||
import { getAnalysisElementsAdmin } from './analysis-element.service';
|
||||
import { validateMedipostResponse } from './medipost.service';
|
||||
import { getAnalyses } from '../analyses.service';
|
||||
import { getAnalysisElementsAdmin } from '../analysis-element.service';
|
||||
import { validateMedipostResponse } from './medipostValidate.service';
|
||||
|
||||
const BASE_URL = process.env.MEDIPOST_URL!;
|
||||
const USER = process.env.MEDIPOST_USER!;
|
||||
25
lib/services/medipost/medipostValidate.service.ts
Normal file
25
lib/services/medipost/medipostValidate.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
'use server';
|
||||
|
||||
import type {
|
||||
IMedipostResponseXMLBase,
|
||||
} from '@/packages/shared/src/types/medipost-analysis';
|
||||
|
||||
import { MedipostValidationError } from './MedipostValidationError';
|
||||
import { parseXML } from '../util/xml.service';
|
||||
|
||||
export async function validateMedipostResponse(response: string, { canHaveEmptyCode = false }: { canHaveEmptyCode?: boolean } = {}) {
|
||||
const parsed: IMedipostResponseXMLBase = await parseXML(response);
|
||||
const code = parsed.ANSWER?.CODE;
|
||||
if (canHaveEmptyCode) {
|
||||
if (code && code !== 0) {
|
||||
console.error("Bad response", response);
|
||||
throw new MedipostValidationError(response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof code !== 'number' || (code !== 0 && !canHaveEmptyCode)) {
|
||||
console.error("Bad response", response);
|
||||
throw new MedipostValidationError(response);
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ import { toArray } from '@/lib/utils';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { AnalysisElement } from './analysis-element.service';
|
||||
import { AnalysesWithGroupsAndElements } from './analyses.service';
|
||||
import { AnalysisElement } from '../analysis-element.service';
|
||||
import { AnalysesWithGroupsAndElements } from '../analyses.service';
|
||||
|
||||
const USER = process.env.MEDIPOST_USER!;
|
||||
const RECIPIENT = process.env.MEDIPOST_RECIPIENT!;
|
||||
@@ -76,42 +76,34 @@ export async function composeOrderXML({
|
||||
|
||||
// Collect all materials from all analysis groups
|
||||
for (const currentGroup of analysisGroups) {
|
||||
let relatedAnalysisElement = analysisElements?.find(
|
||||
(element) => element.analysis_groups.id === currentGroup.id,
|
||||
);
|
||||
const relatedAnalyses = analyses?.filter((analysis) => {
|
||||
return analysis.analysis_elements.analysis_groups.id === currentGroup.id;
|
||||
});
|
||||
let relatedAnalysisElements = analysisElements?.filter(({ analysis_groups }) => analysis_groups.id === currentGroup.id);
|
||||
|
||||
if (!relatedAnalysisElement) {
|
||||
relatedAnalysisElement = relatedAnalyses?.find(
|
||||
(relatedAnalysis) =>
|
||||
relatedAnalysis.analysis_elements.analysis_groups.id ===
|
||||
currentGroup.id,
|
||||
)?.analysis_elements;
|
||||
if (!relatedAnalysisElements || relatedAnalysisElements.length === 0) {
|
||||
relatedAnalysisElements = analyses
|
||||
.filter(({ analysis_elements }) => analysis_elements.analysis_groups.id === currentGroup.id)
|
||||
.flatMap(({ analysis_elements }) => analysis_elements);
|
||||
}
|
||||
|
||||
if (!relatedAnalysisElement || !relatedAnalysisElement.material_groups) {
|
||||
if (!relatedAnalysisElements || relatedAnalysisElements.length === 0) {
|
||||
throw new Error(
|
||||
`Failed to find related analysis element for group ${currentGroup.name} (id: ${currentGroup.id})`,
|
||||
`Failed to find related analysis elements for group ${currentGroup.name} (id: ${currentGroup.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const group of relatedAnalysisElement?.material_groups as MaterjalideGrupp[]) {
|
||||
const materials = toArray(group.Materjal);
|
||||
for (const material of materials) {
|
||||
const { MaterjaliNimi, MaterjaliTyyp, MaterjaliTyypOID, Konteiner } = material;
|
||||
const containers = toArray(Konteiner);
|
||||
|
||||
for (const container of containers) {
|
||||
// Use MaterialTyyp as the key for deduplication
|
||||
const materialKey = MaterjaliTyyp;
|
||||
|
||||
if (!uniqueMaterials.has(materialKey)) {
|
||||
uniqueMaterials.set(materialKey, {
|
||||
MaterjaliTyypOID,
|
||||
MaterjaliTyyp,
|
||||
MaterjaliNimi,
|
||||
for (const analysisElement of relatedAnalysisElements) {
|
||||
for (const { Materjal } of analysisElement.material_groups as MaterjalideGrupp[]) {
|
||||
for (const material of toArray(Materjal)) {
|
||||
const { MaterjaliTyyp } = material;
|
||||
|
||||
for (const container of toArray(material.Konteiner)) {
|
||||
if (uniqueMaterials.has(MaterjaliTyyp)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uniqueMaterials.set(MaterjaliTyyp, {
|
||||
MaterjaliTyypOID: material.MaterjaliTyypOID,
|
||||
MaterjaliTyyp: material.MaterjaliTyyp,
|
||||
MaterjaliNimi: material.MaterjaliNimi,
|
||||
ProovinouKoodOID: container.ProovinouKoodOID,
|
||||
ProovinouKood: container.ProovinouKood,
|
||||
order: specimenOrder++,
|
||||
@@ -137,54 +129,55 @@ export async function composeOrderXML({
|
||||
// Generate analysis section with correct specimen references
|
||||
const analysisSection = [];
|
||||
for (const currentGroup of analysisGroups) {
|
||||
let relatedAnalysisElement = analysisElements?.find(
|
||||
let relatedAnalysisElements = analysisElements?.filter(
|
||||
(element) => element.analysis_groups.id === currentGroup.id,
|
||||
);
|
||||
const relatedAnalyses = analyses?.filter((analysis) => {
|
||||
return analysis.analysis_elements.analysis_groups.id === currentGroup.id;
|
||||
});
|
||||
|
||||
if (!relatedAnalysisElement) {
|
||||
relatedAnalysisElement = relatedAnalyses?.find(
|
||||
(relatedAnalysis) =>
|
||||
relatedAnalysis.analysis_elements.analysis_groups.id ===
|
||||
currentGroup.id,
|
||||
)?.analysis_elements;
|
||||
if (!relatedAnalysisElements) {
|
||||
relatedAnalysisElements = analyses
|
||||
.filter(({ analysis_elements }) => analysis_elements.analysis_groups.id === currentGroup.id)
|
||||
.flatMap(({ analysis_elements }) => analysis_elements);
|
||||
}
|
||||
|
||||
if (!relatedAnalysisElement || !relatedAnalysisElement.material_groups) {
|
||||
if (!relatedAnalysisElements || relatedAnalysisElements.length === 0) {
|
||||
throw new Error(
|
||||
`Failed to find related analysis element for group ${currentGroup.name} (id: ${currentGroup.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
// Find the specimen order number for this analysis group
|
||||
let specimenOrderNumber = 1;
|
||||
for (const group of relatedAnalysisElement?.material_groups as MaterjalideGrupp[]) {
|
||||
const materials = toArray(group.Materjal);
|
||||
for (const material of materials) {
|
||||
const materialKey = material.MaterjaliTyyp;
|
||||
const uniqueMaterial = uniqueMaterials.get(materialKey);
|
||||
if (uniqueMaterial) {
|
||||
specimenOrderNumber = uniqueMaterial.order;
|
||||
break; // Use the first material's order number
|
||||
const uuringElementInputs: {
|
||||
analysisElement: Tables<{ schema: 'medreport' }, 'analysis_elements'>,
|
||||
specimenOrderNr: number,
|
||||
}[] = [];
|
||||
for (const analysisElement of relatedAnalysisElements) {
|
||||
for (const group of analysisElement.material_groups as MaterjalideGrupp[]) {
|
||||
const materials = toArray(group.Materjal);
|
||||
for (const material of materials) {
|
||||
const uniqueMaterial = uniqueMaterials.get(material.MaterjaliTyyp);
|
||||
if (!uniqueMaterial) {
|
||||
console.info(`Unique material not found for material: ${material.MaterjaliTyyp}, analysis element: ${analysisElement.id} ${analysisElement.analysis_id_original} ${analysisElement.analysis_name_lab}`);
|
||||
continue;
|
||||
}
|
||||
uuringElementInputs.push({
|
||||
analysisElement,
|
||||
specimenOrderNr: uniqueMaterial.order,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (specimenOrderNumber > 1) break; // Found a specimen, use it
|
||||
}
|
||||
|
||||
const groupXml = getAnalysisGroup(
|
||||
currentGroup.original_id,
|
||||
currentGroup.name,
|
||||
specimenOrderNumber,
|
||||
relatedAnalysisElement,
|
||||
uuringElementInputs,
|
||||
);
|
||||
analysisSection.push(groupXml);
|
||||
}
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
|
||||
${getPais(USER, RECIPIENT, orderId)}
|
||||
${getPais(USER, RECIPIENT, orderId, "OL")}
|
||||
<Tellimus cito="EI">
|
||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||
${getClientInstitution()}
|
||||
@@ -6,10 +6,7 @@ import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
|
||||
import { getCartId } from '@lib/data/cookies';
|
||||
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { requireUserInServerComponent } from '../server/require-user-in-server-component';
|
||||
import { cancelReservation } from './connected-online.service';
|
||||
|
||||
const env = () =>
|
||||
@@ -27,10 +24,8 @@ const env = () =>
|
||||
.min(1),
|
||||
})
|
||||
.parse({
|
||||
medusaBackendPublicUrl:
|
||||
'http://weebhook.site:3000' /* process.env.MEDUSA_BACKEND_PUBLIC_URL! */,
|
||||
siteUrl:
|
||||
'http://weebhook.site:3000' /* process.env.NEXT_PUBLIC_SITE_URL! */,
|
||||
medusaBackendPublicUrl: process.env.MEDUSA_BACKEND_PUBLIC_URL!,
|
||||
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
||||
});
|
||||
|
||||
export async function handleAddToCart({
|
||||
|
||||
90
lib/services/medusaOrder.service.ts
Normal file
90
lib/services/medusaOrder.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
'use server';
|
||||
|
||||
import { getAnalysisElements } from './analysis-element.service';
|
||||
import { getAnalyses } from './analyses.service';
|
||||
import { StoreOrder } from '@medusajs/types';
|
||||
import { listProducts } from '@lib/data/products';
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
|
||||
const ANALYSIS_PACKAGE_HANDLE_PREFIX = 'analysis-package-';
|
||||
|
||||
export async function getOrderedAnalysisIds({
|
||||
medusaOrder,
|
||||
}: {
|
||||
medusaOrder: StoreOrder;
|
||||
}): Promise<{
|
||||
analysisElementId?: number;
|
||||
analysisId?: number;
|
||||
}[]> {
|
||||
const countryCodes = await listRegions();
|
||||
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
|
||||
|
||||
async function getOrderedAnalysisElements(medusaOrder: StoreOrder) {
|
||||
const originalIds = (medusaOrder?.items ?? [])
|
||||
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
||||
.filter((a) => typeof a === 'string') as string[];
|
||||
const analysisElements = await getAnalysisElements({ originalIds });
|
||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||
}
|
||||
|
||||
async function getOrderedAnalyses(medusaOrder: StoreOrder) {
|
||||
const originalIds = (medusaOrder?.items ?? [])
|
||||
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
||||
.filter((a) => typeof a === 'string') as string[];
|
||||
const analyses = await getAnalyses({ originalIds });
|
||||
return analyses.map(({ id }) => ({ analysisId: id }));
|
||||
}
|
||||
|
||||
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
|
||||
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
|
||||
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
|
||||
if (orderedPackageIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
console.info(`Order has ${orderedPackageIds.length} packages`);
|
||||
const { response: { products: orderedPackagesProducts } } = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, id: orderedPackageIds },
|
||||
});
|
||||
console.info(`Order has ${orderedPackagesProducts.length} packages = ${JSON.stringify(orderedPackageIds, null, 2)}`);
|
||||
if (orderedPackagesProducts.length !== orderedPackageIds.length) {
|
||||
throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`);
|
||||
}
|
||||
|
||||
const ids = getAnalysisElementMedusaProductIds(
|
||||
orderedPackagesProducts.map(({ id, metadata }) => ({
|
||||
metadata,
|
||||
variant: orderedPackages.find(({ product }) => product?.id === id)?.variant,
|
||||
})),
|
||||
);
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const { response: { products: analysisPackagesProducts } } = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, id: ids },
|
||||
});
|
||||
if (analysisPackagesProducts.length !== ids.length) {
|
||||
throw new Error(`Got ${analysisPackagesProducts.length} analysis packages products, expected ${ids.length}`);
|
||||
}
|
||||
|
||||
const originalIds = analysisPackagesProducts
|
||||
.map(({ metadata }) => metadata?.analysisIdOriginal)
|
||||
.filter((id) => typeof id === 'string');
|
||||
if (originalIds.length !== ids.length) {
|
||||
throw new Error(`Got ${originalIds.length} analysis packages products with analysisIdOriginal, expected ${ids.length}`);
|
||||
}
|
||||
const analysisElements = await getAnalysisElements({ originalIds });
|
||||
|
||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||
}
|
||||
|
||||
const [analysisPackageElements, orderedAnalysisElements, orderedAnalyses] = await Promise.all([
|
||||
getOrderedAnalysisPackages(medusaOrder),
|
||||
getOrderedAnalysisElements(medusaOrder),
|
||||
getOrderedAnalyses(medusaOrder),
|
||||
]);
|
||||
|
||||
return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses];
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'
|
||||
import type { Tables } from '@kit/supabase/database';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import type { StoreOrder } from '@medusajs/types';
|
||||
|
||||
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||
import type { AnalysisOrder } from '../types/analysis-order';
|
||||
|
||||
export async function createAnalysisOrder({
|
||||
medusaOrder,
|
||||
@@ -103,7 +102,7 @@ export async function getAnalysisOrder({
|
||||
if (error) {
|
||||
throw new Error(`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`);
|
||||
}
|
||||
return order;
|
||||
return order as AnalysisOrder;
|
||||
}
|
||||
|
||||
export async function getAnalysisOrders({
|
||||
|
||||
8
lib/services/util/xml.service.ts
Normal file
8
lib/services/util/xml.service.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
|
||||
export async function parseXML(xml: string) {
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
return parser.parse(xml);
|
||||
}
|
||||
Reference in New Issue
Block a user