diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx
index 8826673..d1c8b2d 100644
--- a/app/(marketing)/page.tsx
+++ b/app/(marketing)/page.tsx
@@ -1,15 +1,12 @@
import Link from 'next/link';
+import { MedReportTitle } from '@/components/med-report-title';
import { ArrowRightIcon } from 'lucide-react';
-import {
- CtaButton,
- Hero,
-} from '@kit/ui/marketing';
+import { CtaButton, Hero } from '@kit/ui/marketing';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
-import { MedReportTitle } from '@/components/MedReportTitle';
function Home() {
return (
@@ -25,7 +22,6 @@ function Home() {
cta={}
/>
-
);
}
diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts
index 3f1007f..994581b 100644
--- a/lib/services/medipost.service.ts
+++ b/lib/services/medipost.service.ts
@@ -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];
+}
diff --git a/lib/types/medipost.ts b/lib/types/medipost.ts
index 60d116d..42b9618 100644
--- a/lib/types/medipost.ts
+++ b/lib/types/medipost.ts
@@ -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 = {
+ 1: 'QUEUED',
+ 2: 'ON_HOLD',
+ 3: 'PROCESSING',
+ 4: 'COMPLETED',
+ 5: 'REJECTED',
+ 6: 'CANCELLED',
+};
+export const NormStatus: Record = {
+ 1: 'NORMAL',
+ 2: 'WARNING',
+ 3: 'REQUIRES_ATTENTION',
+};
diff --git a/supabase/database.types.ts b/supabase/database.types.ts
index c432b1d..3671308 100644
--- a/supabase/database.types.ts
+++ b/supabase/database.types.ts
@@ -346,28 +346,128 @@ export type Database = {
}
analysis_orders: {
Row: {
- analysis_group_ids: number[] | null
+ analysis_element_ids: number[] | null
+ analysis_ids: number[] | null
created_at: string
- id: string
+ id: number
status: Database["public"]["Enums"]["analysis_order_status"]
user_id: string
}
Insert: {
- analysis_group_ids?: number[] | null
+ analysis_element_ids?: number[] | null
+ analysis_ids?: number[] | null
created_at?: string
- id: string
+ id?: number
status: Database["public"]["Enums"]["analysis_order_status"]
user_id: string
}
Update: {
- analysis_group_ids?: number[] | null
+ analysis_element_ids?: number[] | null
+ analysis_ids?: number[] | null
created_at?: string
- id?: string
+ id?: number
status?: Database["public"]["Enums"]["analysis_order_status"]
user_id?: string
}
Relationships: []
}
+ analysis_response_elements: {
+ Row: {
+ analysis_element_original_id: string
+ analysis_response_id: number
+ created_at: string
+ id: number
+ norm_lower: number | null
+ norm_lower_included: boolean | null
+ norm_status: number | null
+ norm_upper: number | null
+ norm_upper_included: boolean | null
+ original_response_element: Json
+ response_time: string
+ response_value: Json
+ unit: string | null
+ updated_at: string | null
+ }
+ Insert: {
+ analysis_element_original_id: string
+ analysis_response_id: number
+ created_at?: string
+ id?: number
+ norm_lower?: number | null
+ norm_lower_included?: boolean | null
+ norm_status?: number | null
+ norm_upper?: number | null
+ norm_upper_included?: boolean | null
+ original_response_element: Json
+ response_time: string
+ response_value: Json
+ unit?: string | null
+ updated_at?: string | null
+ }
+ Update: {
+ analysis_element_original_id?: string
+ analysis_response_id?: number
+ created_at?: string
+ id?: number
+ norm_lower?: number | null
+ norm_lower_included?: boolean | null
+ norm_status?: number | null
+ norm_upper?: number | null
+ norm_upper_included?: boolean | null
+ original_response_element?: Json
+ response_time?: string
+ response_value?: Json
+ unit?: string | null
+ updated_at?: string | null
+ }
+ Relationships: [
+ {
+ foreignKeyName: "analysis_response_element_analysis_response_id_fkey"
+ columns: ["analysis_response_id"]
+ isOneToOne: false
+ referencedRelation: "analysis_responses"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
+ analysis_responses: {
+ Row: {
+ analysis_order_id: number
+ created_at: string
+ id: number
+ order_number: string
+ order_status: Database["public"]["Enums"]["analysis_order_status"]
+ updated_at: string | null
+ user_id: string
+ }
+ Insert: {
+ analysis_order_id: number
+ created_at?: string
+ id?: number
+ order_number: string
+ order_status: Database["public"]["Enums"]["analysis_order_status"]
+ updated_at?: string | null
+ user_id: string
+ }
+ Update: {
+ analysis_order_id?: number
+ created_at?: string
+ id?: number
+ order_number?: string
+ order_status?: Database["public"]["Enums"]["analysis_order_status"]
+ updated_at?: string | null
+ user_id?: string
+ }
+ Relationships: [
+ {
+ foreignKeyName: "analysis_responses_analysis_order_id_fkey"
+ columns: ["analysis_order_id"]
+ isOneToOne: false
+ referencedRelation: "analysis_orders"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
billing_customers: {
Row: {
account_id: string
diff --git a/supabase/migrations/20250610092159_add_analysis_order_response_tables.sql b/supabase/migrations/20250610092159_add_analysis_order_response_tables.sql
new file mode 100644
index 0000000..9873f1d
--- /dev/null
+++ b/supabase/migrations/20250610092159_add_analysis_order_response_tables.sql
@@ -0,0 +1,129 @@
+create table "public"."analysis_response_elements" (
+ "id" bigint generated by default as identity not null,
+ "analysis_response_id" bigint not null,
+ "analysis_element_original_id" text not null,
+ "unit" text,
+ "response_value" jsonb not null,
+ "response_time" timestamp with time zone not null,
+ "norm_upper" double precision,
+ "norm_upper_included" boolean,
+ "norm_lower" double precision,
+ "norm_lower_included" boolean,
+ "norm_status" smallint,
+ "original_response_element" jsonb not null,
+ "created_at" timestamp with time zone not null default now(),
+ "updated_at" timestamp with time zone default now()
+);
+
+
+alter table "public"."analysis_response_elements" enable row level security;
+
+create table "public"."analysis_responses" (
+ "id" bigint generated by default as identity not null,
+ "analysis_order_id" bigint not null,
+ "order_number" text not null,
+ "order_status" analysis_order_status not null,
+ "user_id" uuid not null,
+ "created_at" timestamp with time zone not null default now(),
+ "updated_at" timestamp with time zone default now()
+);
+
+alter table "public"."analysis_responses" enable row level security;
+
+CREATE UNIQUE INDEX analysis_response_element_pkey ON public.analysis_response_elements USING btree (id);
+
+CREATE UNIQUE INDEX analysis_response_pkey ON public.analysis_responses USING btree (id);
+
+CREATE UNIQUE INDEX analysis_responses_order_number_key ON public.analysis_responses USING btree (order_number);
+
+alter table "public"."analysis_response_elements" add constraint "analysis_response_element_pkey" PRIMARY KEY using index "analysis_response_element_pkey";
+
+alter table "public"."analysis_responses" add constraint "analysis_response_pkey" PRIMARY KEY using index "analysis_response_pkey";
+
+alter table "public"."analysis_response_elements" add constraint "analysis_response_element_analysis_response_id_fkey" FOREIGN KEY (analysis_response_id) REFERENCES analysis_responses(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
+
+alter table "public"."analysis_response_elements" validate constraint "analysis_response_element_analysis_response_id_fkey";
+
+alter table "public"."analysis_responses" add constraint "analysis_response_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
+
+alter table "public"."analysis_responses" validate constraint "analysis_response_user_id_fkey";
+
+alter table "public"."analysis_responses" add constraint "analysis_responses_order_number_key" UNIQUE using index "analysis_responses_order_number_key";
+
+grant delete on table "public"."analysis_response_elements" to "service_role";
+
+grant insert on table "public"."analysis_response_elements" to "service_role";
+
+grant references on table "public"."analysis_response_elements" to "service_role";
+
+grant select on table "public"."analysis_response_elements" to "service_role";
+
+grant trigger on table "public"."analysis_response_elements" to "service_role";
+
+grant truncate on table "public"."analysis_response_elements" to "service_role";
+
+grant update on table "public"."analysis_response_elements" to "service_role";
+
+grant delete on table "public"."analysis_responses" to "service_role";
+
+grant insert on table "public"."analysis_responses" to "service_role";
+
+grant references on table "public"."analysis_responses" to "service_role";
+
+grant select on table "public"."analysis_responses" to "service_role";
+
+grant trigger on table "public"."analysis_responses" to "service_role";
+
+grant truncate on table "public"."analysis_responses" to "service_role";
+
+grant update on table "public"."analysis_responses" to "service_role";
+
+create policy "select_own"
+on "public"."analysis_response_elements"
+as permissive
+for select
+to authenticated
+using ((( SELECT auth.uid() AS uid) IN ( SELECT analysis_responses.user_id
+ FROM analysis_responses
+ WHERE (analysis_responses.id = analysis_response_elements.analysis_response_id))));
+
+
+create policy "service_role_all"
+on "public"."analysis_response_elements"
+as permissive
+for all
+to service_role
+using (true);
+
+
+create policy "select_own"
+on "public"."analysis_responses"
+as permissive
+for select
+to authenticated
+using ((( SELECT auth.uid() AS uid) = user_id));
+
+
+create policy "service_role_all"
+on "public"."analysis_responses"
+as permissive
+for all
+to service_role
+using (true);
+
+create policy "service_role_all"
+on "public"."analysis_orders"
+as permissive
+for all
+to service_role
+using (true);
+
+
+alter table "public"."analysis_orders" add constraint "analysis_orders_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
+alter table "public"."analysis_orders" drop constraint "analysis_orders_id_fkey";
+alter table "public"."analysis_orders" drop column "id";
+alter table "public"."analysis_orders" add "id" bigint generated by default as identity not null;
+CREATE UNIQUE INDEX analysis_orders_pkey ON public.analysis_orders USING btree (id);
+alter table "public"."analysis_orders" add constraint "analysis_orders_pkey" PRIMARY KEY using index "analysis_orders_pkey";
+alter table "public"."analysis_responses" add constraint "analysis_responses_analysis_order_id_fkey" FOREIGN KEY (analysis_order_id) REFERENCES analysis_orders(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
+alter table "public"."analysis_responses" validate constraint "analysis_responses_analysis_order_id_fkey";