'use server'; import { SupabaseClient, createClient as createCustomClient, } from '@supabase/supabase-js'; import { getAnalysisGroup, getClientInstitution, getClientPerson, getConfidentiality, getPais, getPatient, getProviderInstitution, getSpecimen, } from '@/lib/templates/medipost-order'; import { SyncStatus } from '@/lib/types/audit'; import { AnalysisOrderStatus, GetMessageListResponse, IMedipostResponseXMLBase, MaterjalideGrupp, 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 { uniqBy } from 'lodash'; import { Tables } from '@kit/supabase/database'; import { createAnalysisGroup } from './analysis-group.service'; import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; import { getOrder } from './order.service'; import { getAnalysisElementsAdmin } from './analysis-element.service'; import { getAnalyses } from './analyses.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!; function parseXML(xml: string) { const parser = new XMLParser({ ignoreAttributes: false }); return parser.parse(xml); } export async function validateMedipostResponse(response: string) { const parsed: IMedipostResponseXMLBase = parseXML(response); if (typeof parsed.ANSWER?.CODE !== 'number' || parsed.ANSWER?.CODE !== 0) { console.error("Bad response", response); throw new Error(`Medipost response is invalid`); } } export async function getMessages() { try { const publicMessage = await getLatestPublicMessageListItem(); if (!publicMessage) { return null; } //Teenused tuleb mappida kokku MedReport teenustega. alusel return getPublicMessage(publicMessage.messageId); } catch (error) { console.error(error); } } export async function getLatestPublicMessageListItem() { const { data } = await axios.get(BASE_URL, { params: { Action: MedipostAction.GetPublicMessageList, User: USER, Password: PASSWORD, Sender: 'syndev', // 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(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); return parseXML(data) as MedipostOrderResponse; } 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[]; }) { let messageIdErrored: string | null = null; try { const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds }); if (!privateMessage) { throw new Error(`No private message found`); } messageIdErrored = privateMessage.messageId; const privateMessageContent = await getPrivateMessage( privateMessage.messageId, ); const messageResponse = privateMessageContent?.Saadetis?.Vastus; if (!messageResponse) { throw new Error(`Invalid data in private message response`); } const status = await syncPrivateMessage({ messageResponse }); if (status === 'COMPLETED') { await deletePrivateMessage(privateMessage.messageId); } } catch (e) { console.error(e); } return { messageIdErrored }; } 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>[] = 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', }); } } export async function composeOrderXML({ person, orderedAnalysisElementsIds, orderedAnalysesIds, orderId, orderCreatedAt, comment, }: { person: { idCode: string; firstName: string; lastName: string; phone: string; }; orderedAnalysisElementsIds: number[]; orderedAnalysesIds: number[]; orderId: string; orderCreatedAt: Date; comment?: string; }) { const analysisElements = await getAnalysisElementsAdmin({ ids: orderedAnalysisElementsIds }); const analyses = await getAnalyses({ ids: orderedAnalysesIds }); const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] = uniqBy( ( analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ?? [] ).concat( analyses?.flatMap( ({ analysis_elements }) => analysis_elements.analysis_groups, ) ?? [], ), 'id', ); const specimenSection = []; const analysisSection = []; let order = 1; 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; }); if (!relatedAnalysisElement) { relatedAnalysisElement = relatedAnalyses?.find( (relatedAnalysis) => relatedAnalysis.analysis_elements.analysis_groups.id === currentGroup.id, )?.analysis_elements; } if (!relatedAnalysisElement || !relatedAnalysisElement.material_groups) { throw new Error( `Failed to find related analysis element for group ${currentGroup.name} (id: ${currentGroup.id})`, ); } for (const group of relatedAnalysisElement?.material_groups as MaterjalideGrupp[]) { const materials = toArray(group.Materjal); const specimenXml = materials.flatMap( ({ MaterjaliNimi, MaterjaliTyyp, MaterjaliTyypOID, Konteiner }) => { return toArray(Konteiner).map((container) => getSpecimen( MaterjaliTyypOID, MaterjaliTyyp, MaterjaliNimi, order, container.ProovinouKoodOID, container.ProovinouKood, ), ); }, ); specimenSection.push(...specimenXml); } const groupXml = getAnalysisGroup( currentGroup.original_id, currentGroup.name, order, relatedAnalysisElement, ); order++; analysisSection.push(groupXml); } return ` ${getPais(USER, RECIPIENT, orderCreatedAt, orderId)} ${orderId} ${getClientInstitution()} ${getProviderInstitution()} ${getClientPerson(person)} ${comment ?? ''} ${getPatient(person)} ${getConfidentiality()} ${specimenSection.join('')} ${analysisSection?.join('')} `; } 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 syncPrivateMessage({ messageResponse, }: { messageResponse: MedipostOrderResponse['Saadetis']['Vastus']; }) { const supabase = getSupabaseServerAdminClient() const status = messageResponse.TellimuseOlek; const order = await getOrder({ medusaOrderId: messageResponse.ValisTellimuseId }); 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[status], 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); const responses: Omit< Tables<{ schema: 'medreport' }, 'analysis_response_elements'>, 'id' | 'created_at' | 'updated_at' >[] = []; for (const analysisGroup of analysisGroups) { const groupItems = toArray( analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'], ); 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: analysisResponse[0]!.id, 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, })), ); } } const { error: deleteError } = await supabase .schema('medreport') .from('analysis_response_elements') .delete() .eq('analysis_response_id', analysisResponse[0].id); if (deleteError) { throw new Error( `Failed to clean up response elements for response id ${analysisResponse[0].id}`, ); } 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 ${analysisResponse[0].id}`, ); } return AnalysisOrderStatus[status]; }