From 0081e8948b3234738eac73813103b7da99842020 Mon Sep 17 00:00:00 2001 From: Karli Date: Mon, 8 Sep 2025 23:42:50 +0300 Subject: [PATCH 01/13] move most isikukood.js usage to utils --- app/home/(user)/_components/dashboard.tsx | 21 +++---- .../_lib/server/load-analysis-packages.ts | 25 +------- lib/templates/medipost-order.ts | 8 +-- lib/utils.ts | 61 +++++++++++++++++-- packages/features/accounts/src/server/api.ts | 11 +--- 5 files changed, 75 insertions(+), 51 deletions(-) diff --git a/app/home/(user)/_components/dashboard.tsx b/app/home/(user)/_components/dashboard.tsx index 53722d9..356688b 100644 --- a/app/home/(user)/_components/dashboard.tsx +++ b/app/home/(user)/_components/dashboard.tsx @@ -16,7 +16,6 @@ import { } from 'lucide-react'; import { pathsConfig } from '@kit/shared/config'; -import { getPersonParameters } from '@kit/shared/utils'; import { Button } from '@kit/ui/button'; import { Card, @@ -30,7 +29,7 @@ import { cn } from '@kit/ui/utils'; import { isNil } from 'lodash'; import { BmiCategory } from '~/lib/types/bmi'; -import { +import PersonalCode, { bmiFromMetric, getBmiBackgroundColor, getBmiStatus, @@ -145,21 +144,19 @@ export default function Dashboard({ 'id' >[]; }) { - const params = getPersonParameters(account.personal_code!); - const bmiStatus = getBmiStatus(bmiThresholds, { - age: params?.age || 0, - height: account.accountParams?.height || 0, - weight: account.accountParams?.weight || 0, - }); + const height = account.accountParams?.height || 0; + const weight = account.accountParams?.weight || 0; + const { age = 0, gender } = PersonalCode.parsePersonalCode(account.personal_code!); + const bmiStatus = getBmiStatus(bmiThresholds, { age, height, weight }); return ( <>
{cards({ - gender: params?.gender, - age: params?.age, - height: account.accountParams?.height, - weight: account.accountParams?.weight, + gender, + age, + height, + weight, bmiStatus, smoking: account.accountParams?.isSmoker, }).map( diff --git a/app/home/(user)/_lib/server/load-analysis-packages.ts b/app/home/(user)/_lib/server/load-analysis-packages.ts index ca3fd5b..3fe0291 100644 --- a/app/home/(user)/_lib/server/load-analysis-packages.ts +++ b/app/home/(user)/_lib/server/load-analysis-packages.ts @@ -1,5 +1,4 @@ import { cache } from 'react'; -import Isikukood, { Gender } from 'isikukood'; import { listProductTypes, listProducts } from "@lib/data/products"; import { listRegions } from '@lib/data/regions'; @@ -8,6 +7,7 @@ import type { StoreProduct } from '@medusajs/types'; import { loadCurrentUserAccount } from './load-user-account'; import { AccountWithParams } from '@/packages/features/accounts/src/server/api'; import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package'; +import PersonalCode from '~/lib/utils'; async function countryCodesLoader() { const countryCodes = await listRegions().then((regions) => @@ -32,27 +32,8 @@ function userSpecificVariantLoader({ if (!personalCode) { throw new Error('Personal code not found'); } - const parsed = new Isikukood(personalCode); - const ageRange = (() => { - const age = parsed.getAge(); - if (age >= 18 && age <= 29) { - return '18-29'; - } - if (age >= 30 && age <= 39) { - return '30-39'; - } - if (age >= 40 && age <= 49) { - return '40-49'; - } - if (age >= 50 && age <= 59) { - return '50-59'; - } - if (age >= 60) { - return '60'; - } - throw new Error('Age range not supported'); - })(); - const gender = parsed.getGender() === Gender.MALE ? 'M' : 'F'; + + const { gender, ageRange } = PersonalCode.parsePersonalCode(personalCode); return ({ product, diff --git a/lib/templates/medipost-order.ts b/lib/templates/medipost-order.ts index 10ce573..818e57d 100644 --- a/lib/templates/medipost-order.ts +++ b/lib/templates/medipost-order.ts @@ -1,7 +1,7 @@ import { format } from 'date-fns'; -import Isikukood, { Gender } from 'isikukood'; import { Tables } from '@/packages/supabase/src/database.types'; import { DATE_FORMAT, DATE_TIME_FORMAT } from '@/lib/constants'; +import PersonalCode from '../utils'; const isProd = process.env.NODE_ENV === 'production'; @@ -73,15 +73,15 @@ export const getPatient = ({ lastName: string, firstName: string, }) => { - const isikukood = new Isikukood(idCode); + const { dob, gender } = PersonalCode.parsePersonalCode(idCode); return ` 1.3.6.1.4.1.28284.6.2.2.1 ${idCode} ${lastName} ${firstName} - ${format(isikukood.getBirthday(), DATE_FORMAT)} + ${format(dob, DATE_FORMAT)} 1.3.6.1.4.1.28284.6.2.3.16.2 - ${isikukood.getGender() === Gender.MALE ? 'M' : 'N'} + ${gender === 'M' ? 'M' : 'N'} `; }; diff --git a/lib/utils.ts b/lib/utils.ts index 36307b7..90442fa 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -91,8 +91,61 @@ export function getBmiBackgroundColor(bmiStatus: BmiCategory | null): string { } export function getGenderStringFromPersonalCode(personalCode: string) { - const person = new Isikukood(personalCode); - if (person.getGender() === Gender.FEMALE) return 'common:female'; - if (person.getGender() === Gender.MALE) return 'common:male'; - return 'common:unknown'; + switch (PersonalCode.parsePersonalCode(personalCode).gender) { + case 'F': + return 'common:female'; + case 'M': + return 'common:male'; + default: + return 'common:unknown'; + } +} + +type AgeRange = '18-29' | '30-39' | '40-49' | '50-59' | '60'; +export default class PersonalCode { + static getPersonalCode(personalCode: string | null) { + if (!personalCode) { + return null; + } + if (personalCode.toLowerCase().startsWith('ee')) { + return personalCode.substring(2); + } + return personalCode; + } + + static parsePersonalCode(personalCode: string): { + ageRange: AgeRange; + gender: 'M' | 'F'; + dob: Date; + age: number; + } { + const parsed = new Isikukood(personalCode); + const ageRange = (() => { + const age = parsed.getAge(); + if (age >= 18 && age <= 29) { + return '18-29'; + } + if (age >= 30 && age <= 39) { + return '30-39'; + } + if (age >= 40 && age <= 49) { + return '40-49'; + } + if (age >= 50 && age <= 59) { + return '50-59'; + } + if (age >= 60) { + return '60'; + } + throw new Error('Age range not supported'); + })(); + const gender = parsed.getGender() === Gender.MALE ? 'M' : 'F'; + + return { + ageRange, + gender, + dob: parsed.getBirthday(), + age: parsed.getAge(), + } + } } diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index f28a490..d1faaef 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -3,6 +3,7 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { Database } from '@kit/supabase/database'; import { AnalysisResultDetails, UserAnalysis } from '../types/accounts'; +import PersonalCode from '~/lib/utils'; export type AccountWithParams = Database['medreport']['Tables']['accounts']['Row'] & { @@ -71,15 +72,7 @@ class AccountsApi { const { personal_code, ...rest } = data; return { ...rest, - personal_code: (() => { - if (!personal_code) { - return null; - } - if (personal_code.toLowerCase().startsWith('ee')) { - return personal_code.substring(2); - } - return personal_code; - })(), + personal_code: PersonalCode.getPersonalCode(personal_code), }; } From 2c638758066fe2e6f3c0a4bd0d9f1b3e0f778588 Mon Sep 17 00:00:00 2001 From: Karli Date: Mon, 8 Sep 2025 23:43:03 +0300 Subject: [PATCH 02/13] fix typo --- .../server/load-team-account-health-details.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/home/[account]/_lib/server/load-team-account-health-details.ts b/app/home/[account]/_lib/server/load-team-account-health-details.ts index 0c4ff72..1705770 100644 --- a/app/home/[account]/_lib/server/load-team-account-health-details.ts +++ b/app/home/[account]/_lib/server/load-team-account-health-details.ts @@ -31,11 +31,11 @@ export const getAccountHealthDetailsFields = ( >[], members: Database['medreport']['Functions']['get_account_members']['Returns'], ): AccountHealthDetailsField[] => { - const avarageWeight = + const averageWeight = memberParams.reduce((sum, r) => sum + r.weight!, 0) / memberParams.length; - const avarageHeight = + const averageHeight = memberParams.reduce((sum, r) => sum + r.height!, 0) / memberParams.length; - const avarageAge = + const averageAge = members.reduce((sum, r) => { const person = new Isikukood(r.personal_code); return sum + person.getAge(); @@ -48,11 +48,11 @@ export const getAccountHealthDetailsFields = ( const person = new Isikukood(r.personal_code); return person.getGender() === 'female'; }).length; - const averageBMI = bmiFromMetric(avarageWeight, avarageHeight); + const averageBMI = bmiFromMetric(averageWeight, averageHeight); const bmiStatus = getBmiStatus(bmiThresholds, { - age: avarageAge, - height: avarageHeight, - weight: avarageWeight, + age: averageAge, + height: averageHeight, + weight: averageWeight, }); const malePercentage = members.length ? (numberOfMaleMembers / members.length) * 100 @@ -76,7 +76,7 @@ export const getAccountHealthDetailsFields = ( }, { title: 'teams:healthDetails.avgAge', - value: avarageAge.toFixed(0), + value: averageAge.toFixed(0), Icon: Clock, iconBg: 'bg-success', }, From 0e063cd5dc115d7db52b50e5bf980338c98124be Mon Sep 17 00:00:00 2001 From: Karli Date: Mon, 8 Sep 2025 23:43:24 +0300 Subject: [PATCH 03/13] 'medreport.update_account' should also update email --- packages/features/auth/src/server/api.ts | 1 + packages/supabase/src/database.types.ts | 1 + ...08145900_update_account_email_keycloak.sql | 28 +++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 supabase/migrations/20250908145900_update_account_email_keycloak.sql diff --git a/packages/features/auth/src/server/api.ts b/packages/features/auth/src/server/api.ts index 6f8ead2..223e82c 100644 --- a/packages/features/auth/src/server/api.ts +++ b/packages/features/auth/src/server/api.ts @@ -68,6 +68,7 @@ class AuthApi { p_name: data.firstName, p_last_name: data.lastName, p_personal_code: data.personalCode, + p_email: data.email || '', p_phone: data.phone || '', p_city: data.city || '', p_has_consent_personal_data: data.userConsent, diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index a4f8cc1..047f243 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -2053,6 +2053,7 @@ export type Database = { p_personal_code: string p_phone: string p_uid: string + p_email: string } Returns: undefined } diff --git a/supabase/migrations/20250908145900_update_account_email_keycloak.sql b/supabase/migrations/20250908145900_update_account_email_keycloak.sql new file mode 100644 index 0000000..9e44e06 --- /dev/null +++ b/supabase/migrations/20250908145900_update_account_email_keycloak.sql @@ -0,0 +1,28 @@ +CREATE OR REPLACE FUNCTION medreport.update_account(p_name character varying, p_last_name text, p_personal_code text, p_phone text, p_city text, p_has_consent_personal_data boolean, p_uid uuid, p_email character varying) + RETURNS void + LANGUAGE plpgsql +AS $function$begin + update medreport.accounts + set name = coalesce(p_name, name), + last_name = coalesce(p_last_name, last_name), + personal_code = coalesce(p_personal_code, personal_code), + phone = coalesce(p_phone, phone), + city = coalesce(p_city, city), + has_consent_personal_data = coalesce(p_has_consent_personal_data, + has_consent_personal_data), + email = coalesce(p_email, email) + where id = p_uid; +end;$function$ +; + +grant +execute on function medreport.update_account( + p_name character varying, + p_last_name text, + p_personal_code text, + p_phone text, + p_city text, + p_has_consent_personal_data boolean, + p_uid uuid, + p_email character varying) to authenticated, +service_role; From 5b91ece1ec02baf9855c86a19b5e9c3bd411e608 Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 00:08:57 +0300 Subject: [PATCH 04/13] sort compare packages modal analyses by title --- app/home/(user)/_components/compare-packages-modal.tsx | 2 +- app/home/(user)/_lib/server/load-analysis-packages.ts | 1 + .../features/medusa-storefront/src/lib/data/products.ts | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/home/(user)/_components/compare-packages-modal.tsx b/app/home/(user)/_components/compare-packages-modal.tsx index 7c163bd..90ed81c 100644 --- a/app/home/(user)/_components/compare-packages-modal.tsx +++ b/app/home/(user)/_components/compare-packages-modal.tsx @@ -128,7 +128,7 @@ const ComparePackagesModal = async ({ return ( - + {title}{' '} {description && (} />)} diff --git a/app/home/(user)/_lib/server/load-analysis-packages.ts b/app/home/(user)/_lib/server/load-analysis-packages.ts index 3fe0291..ca4dab3 100644 --- a/app/home/(user)/_lib/server/load-analysis-packages.ts +++ b/app/home/(user)/_lib/server/load-analysis-packages.ts @@ -70,6 +70,7 @@ async function analysisPackageElementsLoader({ queryParams: { id: analysisElementMedusaProductIds, limit: 100, + order: "title", }, }); diff --git a/packages/features/medusa-storefront/src/lib/data/products.ts b/packages/features/medusa-storefront/src/lib/data/products.ts index a8ea25d..4b1e250 100644 --- a/packages/features/medusa-storefront/src/lib/data/products.ts +++ b/packages/features/medusa-storefront/src/lib/data/products.ts @@ -14,7 +14,12 @@ export const listProducts = async ({ regionId, }: { pageParam?: number - queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { "type_id[0]"?: string; id?: string[], category_id?: string } + queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { + "type_id[0]"?: string; + id?: string[], + category_id?: string; + order?: 'title'; + } countryCode?: string regionId?: string }): Promise<{ From 3bdc1cfefc46e4e3b24f2c1dcc5480b1e1d9b5a7 Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 00:09:15 +0300 Subject: [PATCH 05/13] update personal code util --- app/home/(user)/_components/dashboard.tsx | 4 ++-- lib/utils.ts | 25 +++++++++++------------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/home/(user)/_components/dashboard.tsx b/app/home/(user)/_components/dashboard.tsx index 356688b..d2f8007 100644 --- a/app/home/(user)/_components/dashboard.tsx +++ b/app/home/(user)/_components/dashboard.tsx @@ -59,7 +59,7 @@ const cards = ({ }) => [ { title: 'dashboard:gender', - description: gender ?? 'dashboard:male', + description: gender ?? '-', icon: , iconBg: 'bg-success', }, @@ -153,7 +153,7 @@ export default function Dashboard({ <>
{cards({ - gender, + gender: gender.label, age, height, weight, diff --git a/lib/utils.ts b/lib/utils.ts index 90442fa..d8fa393 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -90,17 +90,6 @@ export function getBmiBackgroundColor(bmiStatus: BmiCategory | null): string { } } -export function getGenderStringFromPersonalCode(personalCode: string) { - switch (PersonalCode.parsePersonalCode(personalCode).gender) { - case 'F': - return 'common:female'; - case 'M': - return 'common:male'; - default: - return 'common:unknown'; - } -} - type AgeRange = '18-29' | '30-39' | '40-49' | '50-59' | '60'; export default class PersonalCode { static getPersonalCode(personalCode: string | null) { @@ -115,7 +104,7 @@ export default class PersonalCode { static parsePersonalCode(personalCode: string): { ageRange: AgeRange; - gender: 'M' | 'F'; + gender: { label: string; value: string }; dob: Date; age: number; } { @@ -139,7 +128,17 @@ export default class PersonalCode { } throw new Error('Age range not supported'); })(); - const gender = parsed.getGender() === Gender.MALE ? 'M' : 'F'; + const gender = (() => { + const gender = parsed.getGender(); + switch (gender) { + case Gender.FEMALE: + return { label: 'common:female', value: 'F' }; + case Gender.MALE: + return { label: 'common:male', value: 'M' }; + default: + throw new Error('Gender not supported'); + } + })(); return { ageRange, From 353e5c3c4b3ef07013174d4587f71f296a5bbc89 Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 00:31:55 +0300 Subject: [PATCH 06/13] update types --- packages/supabase/src/database.types.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 047f243..a09d6b8 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -1257,6 +1257,26 @@ export type Database = { }, ] } + medipost_actions: { + Row: { + created_at: string + id: number + action: string + xml: string + has_analysis_results: boolean + medusa_order_id: string + response_xml: string + has_error: boolean + } + Insert: { + action: string + xml: string + has_analysis_results: boolean + medusa_order_id: string + response_xml: string + has_error: boolean + } + } medreport_product_groups: { Row: { created_at: string From c2896d77b0d7a7f6630ea7dc9f0c67d6c18e6fe6 Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 00:09:38 +0300 Subject: [PATCH 07/13] prefill phone field in update-account form --- app/auth/update-account/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/auth/update-account/page.tsx b/app/auth/update-account/page.tsx index fc94740..a40b13e 100644 --- a/app/auth/update-account/page.tsx +++ b/app/auth/update-account/page.tsx @@ -37,7 +37,7 @@ async function UpdateAccount() { } return account?.email ?? user?.email ?? ''; })(), - phone: account?.phone ?? '', + phone: account?.phone ?? '+372', city: account?.city ?? '', weight: account?.accountParams?.weight ?? 0, height: account?.accountParams?.height ?? 0, From f00899c4563852198105dcfd0bb5356a9d64c89c Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 00:33:43 +0300 Subject: [PATCH 08/13] move medipost xml to separate service to be unit tested --- lib/services/analyses.service.ts | 10 +- lib/services/medipost.service.ts | 152 ++++------------------------ lib/services/medipostXML.service.ts | 136 +++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 134 deletions(-) create mode 100644 lib/services/medipostXML.service.ts diff --git a/lib/services/analyses.service.ts b/lib/services/analyses.service.ts index 0127e09..790b201 100644 --- a/lib/services/analyses.service.ts +++ b/lib/services/analyses.service.ts @@ -2,7 +2,7 @@ import type { Tables } from '@/packages/supabase/src/database.types'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import type { IUuringElement } from "./medipost.types"; -type AnalysesWithGroupsAndElements = ({ +export type AnalysesWithGroupsAndElements = ({ analysis_elements: Tables<{ schema: 'medreport' }, 'analysis_elements'> & { analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>; }; @@ -105,7 +105,13 @@ export const createMedusaSyncSuccessEntry = async () => { }); } -export async function getAnalyses({ ids, originalIds }: { ids?: number[], originalIds?: string[] }): Promise { +export async function getAnalyses({ + ids, + originalIds, +}: { + ids?: number[]; + originalIds?: string[]; +}): Promise { const query = getSupabaseServerAdminClient() .schema('medreport') .from('analyses') diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index b6aec5d..4171977 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -5,23 +5,11 @@ import { createClient as createCustomClient, } from '@supabase/supabase-js'; -import { - getAnalysisGroup, - getClientInstitution, - getClientPerson, - getConfidentiality, - getOrderEnteredPerson, - getPais, - getPatient, - getProviderInstitution, - getSpecimen, -} from '@/lib/templates/medipost-order'; import { SyncStatus } from '@/lib/types/audit'; import { AnalysisOrderStatus, GetMessageListResponse, IMedipostResponseXMLBase, - MaterjalideGrupp, MedipostAction, MedipostOrderResponse, MedipostPublicMessageResponse, @@ -32,7 +20,6 @@ import { 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'; @@ -47,6 +34,7 @@ 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!; @@ -451,122 +439,6 @@ export async function syncPublicMessage( } } -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 }); - if (analysisElements.length !== orderedAnalysisElementsIds.length) { - throw new Error(`Got ${analysisElements.length} analysis elements, expected ${orderedAnalysisElementsIds.length}`); - } - - const analyses = await getAnalyses({ ids: orderedAnalysesIds }); - if (analyses.length !== orderedAnalysesIds.length) { - throw new Error(`Got ${analyses.length} analyses, expected ${orderedAnalysesIds.length}`); - } - - 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()} - ${getOrderEnteredPerson()} - ${comment ?? ''} - ${getPatient(person)} - ${getConfidentiality()} - ${specimenSection.join('')} - ${analysisSection?.join('')} - -`; -} - function getLatestMessage({ messages, excludedMessageIds, @@ -714,20 +586,36 @@ export async function sendOrderToMedipost({ orderedAnalysisElements, }: { medusaOrderId: string; - orderedAnalysisElements: { analysisElementId?: number; analysisId?: number }[]; + orderedAnalysisElements: OrderedAnalysisElement[]; }) { const medreportOrder = await getOrder({ 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 ?? '', }, - orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[], - orderedAnalysesIds: orderedAnalysisElements.map(({ analysisId }) => analysisId).filter(Boolean) as number[], orderId: medusaOrderId, orderCreatedAt: new Date(medreportOrder.created_at), comment: '', diff --git a/lib/services/medipostXML.service.ts b/lib/services/medipostXML.service.ts new file mode 100644 index 0000000..ad6fb04 --- /dev/null +++ b/lib/services/medipostXML.service.ts @@ -0,0 +1,136 @@ +'use server'; + +import { + getAnalysisGroup, + getClientInstitution, + getClientPerson, + getConfidentiality, + getOrderEnteredPerson, + getPais, + getPatient, + getProviderInstitution, + getSpecimen, +} from '@/lib/templates/medipost-order'; +import { + MaterjalideGrupp, +} from '@/lib/types/medipost'; +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'; + +const USER = process.env.MEDIPOST_USER!; +const RECIPIENT = process.env.MEDIPOST_RECIPIENT!; + +export type OrderedAnalysisElement = { + analysisElementId?: number; + analysisId?: number; +} + +export async function composeOrderXML({ + analyses, + analysisElements, + person, + orderId, + orderCreatedAt, + comment, +}: { + analyses: AnalysesWithGroupsAndElements; + analysisElements: AnalysisElement[]; + person: { + idCode: string; + firstName: string; + lastName: string; + phone: string; + }; + orderId: string; + orderCreatedAt: Date; + comment?: string; +}) { + 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()} + ${getOrderEnteredPerson()} + ${comment ?? ''} + ${getPatient(person)} + ${getConfidentiality()} + ${specimenSection.join('')} + ${analysisSection?.join('')} + +`; +} From 06154f24bfcedf827d52c2cb56172464c9aa79de Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 00:35:21 +0300 Subject: [PATCH 09/13] fix: deduplicate specimen elements in medipost XML generation - Fix duplicate elements when multiple analysis elements use same material type - Ensure analysis elements reference correct specimen order numbers - Move XML composition logic to separate service for better separation of concerns --- lib/services/medipostXML.service.ts | 99 ++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/lib/services/medipostXML.service.ts b/lib/services/medipostXML.service.ts index ad6fb04..3b55506 100644 --- a/lib/services/medipostXML.service.ts +++ b/lib/services/medipostXML.service.ts @@ -62,9 +62,19 @@ export async function composeOrderXML({ 'id', ); - const specimenSection = []; - const analysisSection = []; - let order = 1; + // First, collect all unique materials across all analysis groups + const uniqueMaterials = new Map(); + + let specimenOrder = 1; + + // Collect all materials from all analysis groups for (const currentGroup of analysisGroups) { let relatedAnalysisElement = analysisElements?.find( (element) => element.analysis_groups.id === currentGroup.id, @@ -89,31 +99,86 @@ export async function composeOrderXML({ 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( + 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, - order, - container.ProovinouKoodOID, - container.ProovinouKood, - ), - ); - }, - ); + ProovinouKoodOID: container.ProovinouKoodOID, + ProovinouKood: container.ProovinouKood, + order: specimenOrder++, + }); + } + } + } + } + } - specimenSection.push(...specimenXml); + // Generate specimen section from unique materials + const specimenSection = Array.from(uniqueMaterials.values()).map(material => + getSpecimen( + material.MaterjaliTyypOID, + material.MaterjaliTyyp, + material.MaterjaliNimi, + material.order, + material.ProovinouKoodOID, + material.ProovinouKood, + ) + ); + + // Generate analysis section with correct specimen references + const analysisSection = []; + 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})`, + ); + } + + // 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 + } + } + if (specimenOrderNumber > 1) break; // Found a specimen, use it } const groupXml = getAnalysisGroup( currentGroup.original_id, currentGroup.name, - order, + specimenOrderNumber, relatedAnalysisElement, ); - order++; analysisSection.push(groupXml); } From 1d641211b6a1622f47456d1bd828ecfda0f57e8f Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 01:13:35 +0300 Subject: [PATCH 10/13] update for new type --- app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts | 1 - lib/templates/medipost-order.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index 9ef4799..11eca6f 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -112,7 +112,6 @@ export async function processMontonioCallback(orderToken: string) { throw new Error("Cart not found"); } - const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false }); const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder }); const orderId = await createOrder({ medusaOrder, orderedAnalysisElements }); diff --git a/lib/templates/medipost-order.ts b/lib/templates/medipost-order.ts index 818e57d..19e5b79 100644 --- a/lib/templates/medipost-order.ts +++ b/lib/templates/medipost-order.ts @@ -81,7 +81,7 @@ export const getPatient = ({ ${firstName} ${format(dob, DATE_FORMAT)} 1.3.6.1.4.1.28284.6.2.3.16.2 - ${gender === 'M' ? 'M' : 'N'} + ${gender.value === 'M' ? 'M' : 'N'} `; }; From 596d0e9eee1ca7c9c27997ed9b3b0486b58328d2 Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 01:13:47 +0300 Subject: [PATCH 11/13] fix missing month for DoB in medipost xml --- lib/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/constants.ts b/lib/constants.ts index 7092724..93f3bca 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -1,2 +1,2 @@ export const DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; -export const DATE_FORMAT = "yyyy-mm-dd"; +export const DATE_FORMAT = "yyyy-MM-dd"; From fd943202955c5dfb503c7df5c74c49e9af2e081d Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 01:16:23 +0300 Subject: [PATCH 12/13] use `analysisElementMedusaProductIds` from order product selected variant if it exists --- lib/services/medipost.service.ts | 17 +++++++++++------ utils/medusa-product.ts | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index 4171977..82db51d 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -566,7 +566,7 @@ async function syncPrivateMessage({ ); } - const { data: allOrderResponseElements} = await supabase + const { data: allOrderResponseElements } = await supabase .schema('medreport') .from('analysis_response_elements') .select('*') @@ -714,7 +714,12 @@ export async function getOrderedAnalysisIds({ throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`); } - const ids = getAnalysisElementMedusaProductIds(orderedPackagesProducts); + const ids = getAnalysisElementMedusaProductIds( + orderedPackagesProducts.map(({ id, metadata }) => ({ + metadata, + variant: orderedPackages.find(({ product }) => product?.id === id)?.variant, + })), + ); if (ids.length === 0) { return []; } @@ -755,10 +760,10 @@ export async function createMedipostActionLog({ hasError = false, }: { action: - | 'send_order_to_medipost' - | 'sync_analysis_results_from_medipost' - | 'send_fake_analysis_results_to_medipost' - | 'send_analysis_results_to_medipost'; + | '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; diff --git a/utils/medusa-product.ts b/utils/medusa-product.ts index 6cbbb3b..c505608 100644 --- a/utils/medusa-product.ts +++ b/utils/medusa-product.ts @@ -1,17 +1,27 @@ -export const getAnalysisElementMedusaProductIds = (products: ({ +import { StoreProduct } from "@medusajs/types"; + +type Product = { metadata?: { analysisElementMedusaProductIds?: string; } | null; -} | null)[]) => { + variant?: { + metadata?: { + analysisElementMedusaProductIds?: string; + } | null; + } | null; +} | null; + +export const getAnalysisElementMedusaProductIds = (products: Pick[]) => { if (!products) { return []; } const mapped = products .flatMap((product) => { - const value = product?.metadata?.analysisElementMedusaProductIds?.replaceAll("'", '"'); + const value = (product as Product)?.metadata?.analysisElementMedusaProductIds?.replaceAll("'", '"'); + const value_variant = (product as Product)?.variant?.metadata?.analysisElementMedusaProductIds?.replaceAll("'", '"'); try { - return JSON.parse(value as string); + return [...JSON.parse(value as string), ...JSON.parse(value_variant as string)]; } catch (e) { console.error("Failed to parse analysisElementMedusaProductIds from analysis package, possibly invalid format", e); return []; From c0e9cf5e25a0ea5f1d897cf40e1399eeee734316 Mon Sep 17 00:00:00 2001 From: Karli Date: Tue, 9 Sep 2025 01:39:46 +0300 Subject: [PATCH 13/13] fix value with new types --- app/home/(user)/_lib/server/load-analysis-packages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/home/(user)/_lib/server/load-analysis-packages.ts b/app/home/(user)/_lib/server/load-analysis-packages.ts index ca4dab3..597b95f 100644 --- a/app/home/(user)/_lib/server/load-analysis-packages.ts +++ b/app/home/(user)/_lib/server/load-analysis-packages.ts @@ -33,7 +33,7 @@ function userSpecificVariantLoader({ throw new Error('Personal code not found'); } - const { gender, ageRange } = PersonalCode.parsePersonalCode(personalCode); + const { ageRange, gender: { value: gender } } = PersonalCode.parsePersonalCode(personalCode); return ({ product,