Merge branch 'main' into B2B-95

This commit is contained in:
devmc-ee
2025-06-11 21:31:42 +03:00
8 changed files with 885 additions and 48 deletions

View File

@@ -20,8 +20,10 @@ import {
GetMessageListResponse,
MaterjalideGrupp,
MedipostAction,
MedipostOrderResponse,
MedipostPublicMessageResponse,
Message,
ResponseUuringuGrupp,
UuringuGrupp,
} from '@/lib/types/medipost';
import { toArray } from '@/lib/utils';
@@ -140,12 +142,14 @@ export async function getPrivateMessage(messageId: string) {
},
});
if (data.code && data.code !== 0) {
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})`);
}
const parser = new XMLParser({ ignoreAttributes: false });
return parser.parse(data);
return parsed;
}
export async function deletePrivateMessage(messageId: string) {
@@ -175,8 +179,9 @@ export async function readPrivateMessageResponse() {
privateMessage.messageId,
);
if (privateMessageContent) {
// to be implemented: map and save, only delete if successful
const status = await syncPrivateMessage(privateMessageContent);
if (status === 'COMPLETED') {
await deletePrivateMessage(privateMessage.messageId);
}
} catch (e) {
@@ -307,7 +312,6 @@ async function saveAnalysisGroup(
.upsert(codes, { ignoreDuplicates: false });
if (codesError?.code) {
console.error(codesError); // TODO remove this
throw new Error(
`Failed to insert codes (analysis group id: ${analysisGroup.UuringuGruppId})`,
);
@@ -514,3 +518,113 @@ function getLatestMessage(messages?: Message[]) {
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];
}

View File

@@ -13,25 +13,25 @@ export type GetMessageListResponse = {
};
export enum MedipostAction {
GetPublicMessageList = "GetPublicMessageList",
GetPublicMessage = "GetPublicMessage",
SendPrivateMessage = "SendPrivateMessage",
GetPrivateMessageList = "GetPrivateMessageList",
GetPrivateMessage = "GetPrivateMessage",
DeletePrivateMessage = "DeletePrivateMessage",
GetPublicMessageList = 'GetPublicMessageList',
GetPublicMessage = 'GetPublicMessage',
SendPrivateMessage = 'SendPrivateMessage',
GetPrivateMessageList = 'GetPrivateMessageList',
GetPrivateMessage = 'GetPrivateMessage',
DeletePrivateMessage = 'DeletePrivateMessage',
}
export type VoimalikVaartus = {
VaartusId: string;
Vaartus: "jah" | "ei";
Vaartus: 'jah' | 'ei';
VaartusJarjekord: number;
};
export type Sisendparameeter = {
"@_VastuseTyyp"?: "ARV" | "VABATEKST" | "KODEERITUD" | "AJAHETK";
"@_VastuseKoodistikuOID"?: string;
"@_VastuseKoodistikuNimi"?: string;
"@_URL"?: string;
'@_VastuseTyyp'?: 'ARV' | 'VABATEKST' | 'KODEERITUD' | 'AJAHETK';
'@_VastuseKoodistikuOID'?: string;
'@_VastuseKoodistikuNimi'?: string;
'@_URL'?: string;
UuringIdOID: string;
UuringId: string;
@@ -72,7 +72,7 @@ export type UuringuElement = {
};
export type Uuring = {
tellitav: "JAH" | "EI";
tellitav: 'JAH' | 'EI';
UuringuElement: UuringuElement; //1..1
MaterjalideGrupp?: MaterjalideGrupp | MaterjalideGrupp[]; //0..n If this is not present, then the analysis can't be ordered.
};
@@ -97,13 +97,13 @@ export type Materjal = {
MaterjaliTyyp: string;
MaterjaliNimi: string;
KonteineriOmadus: string;
MaterjaliPaige: { Kohustuslik: "JAH" | "EI" }; //0..1
MaterjaliPaige: { Kohustuslik: 'JAH' | 'EI' }; //0..1
MaterjalJarjekord?: number; //0..1
Konteiner?: Konteiner | Konteiner[]; //0..n
};
export type MaterjalideGrupp = {
vaikimisi: "JAH" | "EI";
vaikimisi: 'JAH' | 'EI';
Materjal: Materjal | Materjal[]; //1..n
};
@@ -114,22 +114,22 @@ export type Teostaja = {
AsutuseNimi: string;
AsutuseKood: string;
AllyksuseNimi: string;
Telefon: string;
Telefon: number;
Aadress: string;
};
Sisendparameeter?: Sisendparameeter | Sisendparameeter[]; //0...n
};
export type MedipostPublicMessageResponse = {
"?xml": {
"@_version": string;
"@_encoding": "UTF-8";
"@_standalone"?: "yes" | "no";
'?xml': {
'@_version': string;
'@_encoding': string;
'@_standalone'?: 'yes' | 'no';
};
ANSWER?: { CODE: number };
Saadetis?: {
Pais: {
Pakett: { "#text": "SL" | "OL" | "AL" | "ME" }; // SL - Teenused, OL - Tellimus (meie poolt saadetav saatekiri), AL - Vastus (saatekirja vastus), ME - Teade
Pakett: { '#text': 'SL' | 'OL' | 'AL' | 'ME' }; // SL - Teenused, OL - Tellimus (meie poolt saadetav saatekiri), AL - Vastus (saatekirja vastus), ME - Teade
Saatja: string;
Saaja: string;
Aeg: string;
@@ -141,11 +141,127 @@ export type MedipostPublicMessageResponse = {
};
};
export const AnalysisOrderStatus = {
1: "QUEUED",
2: "ON_HOLD",
3: "PROCESSING",
4: "COMPLETED",
5: "REJECTED",
6: "CANCELLED",
} as const;
export type UuringuVastus = {
VastuseVaartus: number; // text according to docs, but based on examples and logically, float
VastuseAeg: string;
NormYlem?: {
'#text': number;
'@_kaasaarvatud': string;
}; // 0..1
NormAlum?: {
'#text': number;
'@_kaasaarvatud': string;
};
NormiStaatus: keyof typeof NormStatus;
ProoviJarjenumber: number;
};
export type ResponseUuring = {
UuringuElement: {
UuringIdOID: string;
UuringId: string;
TLyhend: string;
KNimetus: string;
UuringNimi: string;
TellijaUuringId: number;
TeostajaUuringId: string;
UuringOlek: keyof typeof AnalysisOrderStatus;
Mootyhik?: string; // 0..1
Kood: {
HkKood: number;
HkKoodiKordaja: number;
Koefitsient: number;
Hind: number;
};
UuringuVastus?: UuringuVastus | UuringuVastus[]; // 0..n
UuringuKommentaar?: string;
}; // 1..1
UuringuTaitjaAsutuseJnr: number;
};
export type ResponseUuringuGrupp = {
UuringuGruppId: string;
UuringuGruppNimi: string;
Uuring: ResponseUuring | ResponseUuring[]; // 1..n
};
// type for UuringuGrupp is correct, but some of this is generated by an LLM and should be checked if data in use
export type MedipostOrderResponse = {
'?xml': {
'@_version': string;
'@_encoding': string;
'@_standalone': 'yes' | 'no';
};
ANSWER?: { CODE: number };
Saadetis: {
Pais: {
Pakett: {
'#text': string;
'@_versioon': string;
};
Saatja: string;
Saaja: string;
Aeg: string;
SaadetisId: string;
Email: string;
};
Vastus: {
ValisTellimuseId: number;
Asutus: {
'@_tyyp': string; // TEOSTAJA
'@_jarjenumber': string;
AsutuseId: number;
AsutuseNimi: string;
AsutuseKood: string;
AllyksuseNimi?: string;
Telefon: number;
}[]; //@_tyyp = TELLIJA 1..1, @_tyyp = TEOSTAJA 1..n
Personal: {
'@_tyyp': string;
'@_jarjenumber': string;
PersonalOID: string;
PersonalKood: string;
PersonalPerekonnaNimi: string;
PersonalEesNimi: string;
Telefon: number;
};
Patsient: {
IsikukoodiOID: string;
Isikukood: string;
PerekonnaNimi: string;
EesNimi: string;
SynniAeg: string;
SuguOID: string;
Sugu: string;
};
Proov: Array<{
ProovinouIdOID: string;
ProovinouId: string;
MaterjaliTyypOID: string;
MaterjaliTyyp: number;
MaterjaliNimi: string;
Ribakood: string;
Jarjenumber: number;
VotmisAeg: string;
SaabumisAeg: string;
}>;
TellimuseNumber: string;
TellimuseOlek: keyof typeof AnalysisOrderStatus;
UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[];
};
};
};
export const AnalysisOrderStatus: Record<number, string> = {
1: 'QUEUED',
2: 'ON_HOLD',
3: 'PROCESSING',
4: 'COMPLETED',
5: 'REJECTED',
6: 'CANCELLED',
};
export const NormStatus: Record<number, string> = {
1: 'NORMAL',
2: 'WARNING',
3: 'REQUIRES_ATTENTION',
};