'use server'; import { SupabaseClient, createClient as createCustomClient, } from '@supabase/supabase-js'; import { getAnalysisGroup, getClientInstitution, getClientPerson, getConfidentiality, getOrderEnteredByPerson, getPais, getPatient, getProviderInstitution, getSpecimen, } from '@/lib/templates/medipost-order'; import { SyncStatus } from '@/lib/types/audit'; import { AnalysisOrderStatus, GetMessageListResponse, 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'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; const PASSWORD = process.env.MEDIPOST_PASSWORD!; 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(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', }, }); const parser = new XMLParser({ ignoreAttributes: false }); const parsed: MedipostPublicMessageResponse = parser.parse(data); if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) { throw new Error(`Failed to get public message (id: ${messageId})`); } return parsed; } export async function sendPrivateMessage(messageXml: string, receiver: string) { const body = new FormData(); body.append('Action', MedipostAction.SendPrivateMessage); body.append('User', USER); body.append('Password', PASSWORD); body.append('Receiver', receiver); body.append('MessageType', 'Tellimus'); body.append( 'Message', new Blob([messageXml], { type: 'text/xml; charset=UTF-8', }), ); const { data } = await axios.post(BASE_URL, body); if (data.code && data.code !== 0) { throw new Error(`Failed to send private message`); } } export async function getLatestPrivateMessageListItem() { 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(data?.messages); } 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', }, }); const parser = new XMLParser({ ignoreAttributes: false }); const parsed = parser.parse(data); if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) { throw new Error(`Failed to get private message (id: ${messageId})`); } return parsed; } 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() { try { const privateMessage = await getLatestPrivateMessageListItem(); if (!privateMessage) { return null; } const privateMessageContent = await getPrivateMessage( privateMessage.messageId, ); const status = await syncPrivateMessage(privateMessageContent); if (status === 'COMPLETED') { await deletePrivateMessage(privateMessage.messageId); } } catch (e) { console.error(e); } } async function saveAnalysisGroup( analysisGroup: UuringuGrupp, supabase: SupabaseClient, ) { const { data: insertedAnalysisGroup, error } = await supabase .from('analysis_groups') .upsert( { original_id: analysisGroup.UuringuGruppId, name: analysisGroup.UuringuGruppNimi, order: analysisGroup.UuringuGruppJarjekord, }, { onConflict: 'original_id', ignoreDuplicates: false }, ) .select('id'); if (error || !insertedAnalysisGroup[0]?.id) { throw new Error( `Failed to insert analysis group (id: ${analysisGroup.UuringuGruppId}), error: ${error?.message}`, ); } const analysisGroupId = insertedAnalysisGroup[0].id; 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 .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 .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 .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.NEXT_PUBLIC_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', }); } } // TODO use actual parameters export async function composeOrderXML( /* chosenAnalysisElements?: number[], chosenAnalyses?: number[], */ comment?: string, ) { const supabase = createCustomClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!, { auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false, }, }, ); // TODO remove dummy when actual implemetation is present const orderedElements = [1, 75]; const orderedAnalyses = [10, 11, 100]; const createdAnalysisOrder = { id: 4, user_id: 'currentUser.user?.id', analysis_element_ids: orderedElements, analysis_ids: orderedAnalyses, status: AnalysisOrderStatus[1], created_at: new Date(), }; const { data: analysisElements } = (await supabase .from('analysis_elements') .select(`*, analysis_groups(*)`) .in('id', orderedElements)) as { data: ({ analysis_groups: Tables<'analysis_groups'>; } & Tables<'analysis_elements'>)[]; }; const { data: analyses } = (await supabase .from('analyses') .select(`*, analysis_elements(*, analysis_groups(*))`) .in('id', orderedAnalyses)) as { data: ({ analysis_elements: Tables<'analysis_elements'> & { analysis_groups: Tables<'analysis_groups'>; }; } & Tables<'analyses'>)[]; }; const analysisGroups: Tables<'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); } // TODO get actual data when order creation is implemented return ` ${getPais(process.env.MEDIPOST_USER!, process.env.MEDIPOST_RECIPIENT!, createdAnalysisOrder.created_at, createdAnalysisOrder.id)} ${createdAnalysisOrder.id} ${getClientInstitution()} ${getProviderInstitution()} ${getClientPerson()} ${getOrderEnteredByPerson()} ${comment ?? ''} ${getPatient(49610230861, 'Surname', 'First Name', '1996-10-23', 'N')} ${getConfidentiality()} ${specimenSection.join('')} ${analysisSection?.join('')} `; } function getLatestMessage(messages?: Message[]) { if (!messages?.length) { return null; } return messages.reduce((prev, current) => Number(prev.messageId) > Number(current.messageId) ? prev : current, ); } export async function syncPrivateMessage( parsedMessage?: MedipostOrderResponse, ) { const supabase = createCustomClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!, { auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false, }, }, ); const response = parsedMessage?.Saadetis?.Vastus; if (!response) { throw new Error(`Invalid data in private message response`); } const status = response.TellimuseOlek; const { data: analysisOrder, error: analysisOrderError } = await supabase .from('analysis_orders') .select('user_id') .eq('id', response.ValisTellimuseId); if (analysisOrderError || !analysisOrder?.[0]?.user_id) { throw new Error( `Could not find analysis order with id ${response.ValisTellimuseId}`, ); } const { data: analysisResponse, error } = await supabase .from('analysis_responses') .upsert( { analysis_order_id: response.ValisTellimuseId, order_number: response.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: ${response?.TellimuseNumber})`, ); } const analysisGroups = toArray(response.UuringuGrupp); const responses: Omit< Tables<'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, })), ); } } const { error: deleteError } = await supabase .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 .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]; }