From a229ab4d316e4cc1b99ecf36a2325fb76598cfa2 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:14:33 +0300 Subject: [PATCH 01/27] feat(MED-161): move medipost xml validate to separate service --- lib/services/medipost.service.ts | 26 ++----------------- .../medipost/medipostValidate.service.ts | 25 ++++++++++++++++++ lib/services/medipostTest.service.ts | 2 +- lib/services/util/xml.service.ts | 8 ++++++ 4 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 lib/services/medipost/medipostValidate.service.ts create mode 100644 lib/services/util/xml.service.ts diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index 6fb4302..d7113d8 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -9,7 +9,6 @@ import { SyncStatus } from '@/lib/types/audit'; import { AnalysisOrderStatus, GetMessageListResponse, - IMedipostResponseXMLBase, MedipostAction, MedipostOrderResponse, MedipostPublicMessageResponse, @@ -19,7 +18,6 @@ import { } from '@/lib/types/medipost'; import { toArray } from '@/lib/utils'; import axios from 'axios'; -import { XMLParser } from 'fast-xml-parser'; import { Tables } from '@kit/supabase/database'; import { createAnalysisGroup } from './analysis-group.service'; @@ -35,6 +33,8 @@ import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product'; import { MedipostValidationError } from './medipost/MedipostValidationError'; import { logMedipostDispatch } from './audit.service'; import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service'; +import { validateMedipostResponse } from './medipost/medipostValidate.service'; +import { parseXML } from './util/xml.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -43,28 +43,6 @@ const RECIPIENT = process.env.MEDIPOST_RECIPIENT!; const ANALYSIS_PACKAGE_HANDLE_PREFIX = 'analysis-package-'; -function parseXML(xml: string) { - const parser = new XMLParser({ ignoreAttributes: false }); - return parser.parse(xml); -} - -export async function validateMedipostResponse(response: string, { canHaveEmptyCode = false }: { canHaveEmptyCode?: boolean } = {}) { - const parsed: IMedipostResponseXMLBase = parseXML(response); - const code = parsed.ANSWER?.CODE; - if (canHaveEmptyCode) { - if (code && code !== 0) { - console.error("Bad response", response); - throw new MedipostValidationError(response); - } - return; - } - - if (typeof code !== 'number' || (code !== 0 && !canHaveEmptyCode)) { - console.error("Bad response", response); - throw new MedipostValidationError(response); - } -} - export async function getMessages() { try { const publicMessage = await getLatestPublicMessageListItem(); diff --git a/lib/services/medipost/medipostValidate.service.ts b/lib/services/medipost/medipostValidate.service.ts new file mode 100644 index 0000000..f255c46 --- /dev/null +++ b/lib/services/medipost/medipostValidate.service.ts @@ -0,0 +1,25 @@ +'use server'; + +import { + IMedipostResponseXMLBase, +} from '@/lib/types/medipost'; + +import { MedipostValidationError } from './MedipostValidationError'; +import { parseXML } from '../util/xml.service'; + +export async function validateMedipostResponse(response: string, { canHaveEmptyCode = false }: { canHaveEmptyCode?: boolean } = {}) { + const parsed: IMedipostResponseXMLBase = parseXML(response); + const code = parsed.ANSWER?.CODE; + if (canHaveEmptyCode) { + if (code && code !== 0) { + console.error("Bad response", response); + throw new MedipostValidationError(response); + } + return; + } + + if (typeof code !== 'number' || (code !== 0 && !canHaveEmptyCode)) { + console.error("Bad response", response); + throw new MedipostValidationError(response); + } +} diff --git a/lib/services/medipostTest.service.ts b/lib/services/medipostTest.service.ts index 4b1d1ca..18dd35a 100644 --- a/lib/services/medipostTest.service.ts +++ b/lib/services/medipostTest.service.ts @@ -18,7 +18,7 @@ import { Tables } from '@kit/supabase/database'; import { formatDate } from 'date-fns'; import { getAnalyses } from './analyses.service'; import { getAnalysisElementsAdmin } from './analysis-element.service'; -import { validateMedipostResponse } from './medipost.service'; +import { validateMedipostResponse } from './medipost/medipostValidate.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; diff --git a/lib/services/util/xml.service.ts b/lib/services/util/xml.service.ts new file mode 100644 index 0000000..c3eb02c --- /dev/null +++ b/lib/services/util/xml.service.ts @@ -0,0 +1,8 @@ +'use server'; + +import { XMLParser } from 'fast-xml-parser'; + +export function parseXML(xml: string) { + const parser = new XMLParser({ ignoreAttributes: false }); + return parser.parse(xml); +} From 33a6c92841eb597b45ecf3161c725bd8a530b90d Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:14:45 +0300 Subject: [PATCH 02/27] feat(MED-161): create medipost common service --- app/api/order/medipost-test-response/route.ts | 3 +- lib/services/medipost.service.ts | 59 +---------------- .../medipost/medipostMessageBase.service.ts | 63 +++++++++++++++++++ 3 files changed, 66 insertions(+), 59 deletions(-) create mode 100644 lib/services/medipost/medipostMessageBase.service.ts diff --git a/app/api/order/medipost-test-response/route.ts b/app/api/order/medipost-test-response/route.ts index 262c1f0..fb7e8b0 100644 --- a/app/api/order/medipost-test-response/route.ts +++ b/app/api/order/medipost-test-response/route.ts @@ -3,7 +3,8 @@ import { getAnalysisOrder } from "~/lib/services/order.service"; import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service"; import { retrieveOrder } from "@lib/data"; import { getAccountAdmin } from "~/lib/services/account.service"; -import { createMedipostActionLog, getOrderedAnalysisIds } from "~/lib/services/medipost.service"; +import { createMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service"; +import { getOrderedAnalysisIds } from "~/lib/services/medipost.service"; export async function POST(request: Request) { // const isDev = process.env.NODE_ENV === 'development'; diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index d7113d8..ef6eae4 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -12,7 +12,6 @@ import { MedipostAction, MedipostOrderResponse, MedipostPublicMessageResponse, - Message, ResponseUuringuGrupp, UuringuGrupp, } from '@/lib/types/medipost'; @@ -35,6 +34,7 @@ import { logMedipostDispatch } from './audit.service'; import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service'; import { validateMedipostResponse } from './medipost/medipostValidate.service'; import { parseXML } from './util/xml.service'; +import { createMedipostActionLog, getLatestMessage } from './medipost/medipostMessageBase.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -422,29 +422,6 @@ export async function syncPublicMessage( } } -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, - ); -} - async function syncPrivateMessage({ messageResponse, order, @@ -733,37 +710,3 @@ export async function getOrderedAnalysisIds({ return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses]; } - -export async function createMedipostActionLog({ - action, - xml, - hasAnalysisResults = false, - medusaOrderId, - responseXml, - hasError = false, -}: { - action: - | '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; - responseXml?: string | null; - hasError?: boolean; -}) { - await getSupabaseServerAdminClient() - .schema('medreport') - .from('medipost_actions') - .insert({ - action, - xml, - has_analysis_results: hasAnalysisResults, - medusa_order_id: medusaOrderId, - response_xml: responseXml, - has_error: hasError, - }) - .select('id') - .throwOnError(); -} diff --git a/lib/services/medipost/medipostMessageBase.service.ts b/lib/services/medipost/medipostMessageBase.service.ts new file mode 100644 index 0000000..07aeddc --- /dev/null +++ b/lib/services/medipost/medipostMessageBase.service.ts @@ -0,0 +1,63 @@ +'use server'; + +import type { Message } from '@/lib/types/medipost'; + +import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; + +export 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 createMedipostActionLog({ + action, + xml, + hasAnalysisResults = false, + medusaOrderId, + responseXml, + hasError = false, + }: { + action: + | '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; + responseXml?: string | null; + hasError?: boolean; + }) { + await getSupabaseServerAdminClient() + .schema('medreport') + .from('medipost_actions') + .insert({ + action, + xml, + has_analysis_results: hasAnalysisResults, + medusa_order_id: medusaOrderId, + response_xml: responseXml, + has_error: hasError, + }) + .select('id') + .throwOnError(); + } + \ No newline at end of file From a788e8b587bcfa2930537e5f4919baf8a2d1da5f Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:14:54 +0300 Subject: [PATCH 03/27] feat(MED-161): move medipost privatemessage logic to separate service --- app/api/job/handler/sync-analysis-results.ts | 2 +- app/api/job/medipost-retry-dispatch/route.ts | 3 +- .../cart/montonio-callback/actions.ts | 3 +- lib/services/medipost.service.ts | 379 +---------------- .../medipostPrivateMessage.service.ts | 395 ++++++++++++++++++ 5 files changed, 402 insertions(+), 380 deletions(-) create mode 100644 lib/services/medipost/medipostPrivateMessage.service.ts diff --git a/app/api/job/handler/sync-analysis-results.ts b/app/api/job/handler/sync-analysis-results.ts index e666916..4f394f1 100644 --- a/app/api/job/handler/sync-analysis-results.ts +++ b/app/api/job/handler/sync-analysis-results.ts @@ -1,4 +1,4 @@ -import { readPrivateMessageResponse } from "~/lib/services/medipost.service"; +import { readPrivateMessageResponse } from "~/lib/services/medipost/medipostPrivateMessage.service"; type ProcessedMessage = { messageId: string; diff --git a/app/api/job/medipost-retry-dispatch/route.ts b/app/api/job/medipost-retry-dispatch/route.ts index 1a97cef..7b1a55d 100644 --- a/app/api/job/medipost-retry-dispatch/route.ts +++ b/app/api/job/medipost-retry-dispatch/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import loadEnv from "../handler/load-env"; import validateApiKey from "../handler/validate-api-key"; -import { getOrderedAnalysisIds, sendOrderToMedipost } from "~/lib/services/medipost.service"; +import { getOrderedAnalysisIds } from "~/lib/services/medipost.service"; +import { sendOrderToMedipost } from "~/lib/services/medipost/medipostPrivateMessage.service"; import { retrieveOrder } from "@lib/data/orders"; import { getMedipostDispatchTries } from "~/lib/services/audit.service"; diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index f705399..23d39de 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -8,7 +8,8 @@ import { listProductTypes } from "@lib/data/products"; import { placeOrder, retrieveCart } from "@lib/data/cart"; import { createI18nServerInstance } from "~/lib/i18n/i18n.server"; import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service'; -import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service'; +import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service'; +import { getOrderedAnalysisIds } from '~/lib/services/medipost.service'; import { createNotificationsApi } from '@kit/notifications/api'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import type { AccountWithParams } from '@kit/accounts/api'; diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index ef6eae4..6aa4179 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -7,12 +7,9 @@ import { import { SyncStatus } from '@/lib/types/audit'; import { - AnalysisOrderStatus, GetMessageListResponse, MedipostAction, - MedipostOrderResponse, MedipostPublicMessageResponse, - ResponseUuringuGrupp, UuringuGrupp, } from '@/lib/types/medipost'; import { toArray } from '@/lib/utils'; @@ -20,21 +17,15 @@ import axios from 'axios'; import { Tables } from '@kit/supabase/database'; import { createAnalysisGroup } from './analysis-group.service'; -import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; -import { getAnalysisOrder, updateAnalysisOrderStatus } from './order.service'; -import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service'; +import { getAnalysisElements } from './analysis-element.service'; import { getAnalyses } from './analyses.service'; -import { getAccountAdmin } from './account.service'; import { StoreOrder } from '@medusajs/types'; import { listProducts } from '@lib/data/products'; 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'; import { validateMedipostResponse } from './medipost/medipostValidate.service'; import { parseXML } from './util/xml.service'; -import { createMedipostActionLog, getLatestMessage } from './medipost/medipostMessageBase.service'; +import { getLatestMessage } from './medipost/medipostMessageBase.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -93,163 +84,6 @@ export async function getPublicMessage(messageId: string) { 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, { canHaveEmptyCode: true }); - - return { - message: parseXML(data) as MedipostOrderResponse, - xml: data as string, - }; -} - -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[]; -}): Promise<{ messageId: string | null; hasAnalysisResponse: boolean; hasPartialAnalysisResponse: boolean; hasFullAnalysisResponse: boolean; medusaOrderId: string | undefined; analysisOrderId: number | undefined }> { - let messageId: string | null = null; - let hasAnalysisResponse = false; - let hasPartialAnalysisResponse = false; - let hasFullAnalysisResponse = false; - let medusaOrderId: string | undefined = undefined; - let analysisOrderId: number | undefined = undefined; - - try { - const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds }); - messageId = privateMessage?.messageId ?? null; - - if (!privateMessage || !messageId) { - return { - messageId: null, - hasAnalysisResponse: false, - hasPartialAnalysisResponse: false, - hasFullAnalysisResponse: false, - medusaOrderId: undefined, - analysisOrderId: undefined - }; - } - - const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage( - privateMessage.messageId, - ); - - const messageResponse = privateMessageContent?.Saadetis?.Vastus; - analysisOrderId = Number(privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId); - - const hasInvalidOrderId = isNaN(analysisOrderId) - - if (hasInvalidOrderId || !messageResponse) { - await createMedipostActionLog({ - action: 'sync_analysis_results_from_medipost', - xml: privateMessageXml, - hasAnalysisResults: false, - }); - return { - messageId, - hasAnalysisResponse: false, - hasPartialAnalysisResponse: false, - hasFullAnalysisResponse: false, - medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId, - analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId - }; - } - - const analysisOrder = await getAnalysisOrder({ analysisOrderId: analysisOrderId }) - medusaOrderId = analysisOrder.medusa_order_id; - - let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; - try { - order = await getAnalysisOrder({ medusaOrderId }); - } catch (e) { - await deletePrivateMessage(privateMessage.messageId); - throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); - } - - const status = await syncPrivateMessage({ messageResponse, order }); - - if (status.isPartial) { - await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); - hasAnalysisResponse = true; - hasPartialAnalysisResponse = true; - } else if (status.isCompleted) { - await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); - await deletePrivateMessage(privateMessage.messageId); - hasAnalysisResponse = true; - hasFullAnalysisResponse = true; - } - } catch (e) { - console.warn(`Failed to process private message id=${messageId}, message=${(e as Error).message}`); - } - - return { messageId, hasAnalysisResponse, hasPartialAnalysisResponse, hasFullAnalysisResponse, medusaOrderId, analysisOrderId }; -} - async function saveAnalysisGroup( analysisGroup: UuringuGrupp, supabase: SupabaseClient, @@ -422,215 +256,6 @@ export async function syncPublicMessage( } } -async function syncPrivateMessage({ - messageResponse, - order, -}: { - messageResponse: NonNullable; - order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; -}) { - const supabase = getSupabaseServerAdminClient() - - 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[messageResponse.TellimuseOlek], - 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); - console.info(`Order has results for ${analysisGroups.length} analysis groups`); - - const responses: Omit< - Tables<{ schema: 'medreport' }, 'analysis_response_elements'>, - 'id' | 'created_at' | 'updated_at' - >[] = []; - - const analysisResponseId = analysisResponse[0]!.id; - - for (const analysisGroup of analysisGroups) { - const groupItems = toArray( - analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'], - ); - console.info(`Order has results in group ${analysisGroup.UuringuGruppNimi} for ${groupItems.length} analysis elements`); - 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: analysisResponseId, - 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, - comment: element.UuringuKommentaar ?? '', - })), - ); - } - } - - const { error: deleteError } = await supabase - .schema('medreport') - .from('analysis_response_elements') - .delete() - .eq('analysis_response_id', analysisResponseId); - - if (deleteError) { - throw new Error( - `Failed to clean up response elements for response id ${analysisResponseId}`, - ); - } - - 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 ${analysisResponseId}`, - ); - } - - const { data: allOrderResponseElements } = await supabase - .schema('medreport') - .from('analysis_response_elements') - .select('*') - .eq('analysis_response_id', analysisResponseId) - .throwOnError(); - const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; - if (allOrderResponseElements.length !== expectedOrderResponseElements) { - return { isPartial: true }; - } - - const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek]; - return { isCompleted: statusFromResponse === 'COMPLETED' }; -} - -export async function sendOrderToMedipost({ - medusaOrderId, - orderedAnalysisElements, -}: { - medusaOrderId: string; - orderedAnalysisElements: OrderedAnalysisElement[]; -}) { - const medreportOrder = await getAnalysisOrder({ 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 ?? '', - }, - orderId: medreportOrder.id, - orderCreatedAt: new Date(medreportOrder.created_at), - comment: '', - }); - - try { - await sendPrivateMessage(orderXml); - } catch (e) { - const isMedipostError = e instanceof MedipostValidationError; - if (isMedipostError) { - await logMedipostDispatch({ - medusaOrderId, - isSuccess: false, - isMedipostError, - errorMessage: e.response, - }); - await createMedipostActionLog({ - action: 'send_order_to_medipost', - xml: orderXml, - hasAnalysisResults: false, - medusaOrderId, - responseXml: e.response, - hasError: true, - }); - } else { - await logMedipostDispatch({ - medusaOrderId, - isSuccess: false, - isMedipostError, - }); - await createMedipostActionLog({ - action: 'send_order_to_medipost', - xml: orderXml, - hasAnalysisResults: false, - medusaOrderId, - hasError: true, - }); - } - - throw e; - } - await logMedipostDispatch({ - medusaOrderId, - isSuccess: true, - isMedipostError: false, - }); - await createMedipostActionLog({ - action: 'send_order_to_medipost', - xml: orderXml, - hasAnalysisResults: false, - medusaOrderId, - }); - await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); -} - export async function getOrderedAnalysisIds({ medusaOrder, }: { diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts new file mode 100644 index 0000000..afb341b --- /dev/null +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -0,0 +1,395 @@ +'use server'; + +import { + AnalysisOrderStatus, + GetMessageListResponse, + MedipostAction, + MedipostOrderResponse, + ResponseUuringuGrupp, +} from '@/lib/types/medipost'; +import { toArray } from '@/lib/utils'; +import axios from 'axios'; + +import { Tables } from '@kit/supabase/database'; +import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; +import { getAnalysisElementsAdmin } from '../analysis-element.service'; +import { getAnalyses } from '../analyses.service'; +import { createMedipostActionLog, getLatestMessage } from './medipostMessageBase.service'; +import { validateMedipostResponse } from './medipostValidate.service'; +import { getAnalysisOrder, updateAnalysisOrderStatus } from '../order.service'; +import { parseXML } from '../util/xml.service'; +import { composeOrderXML, OrderedAnalysisElement } from '../medipostXML.service'; +import { getAccountAdmin } from '../account.service'; +import { logMedipostDispatch } from '../audit.service'; +import { MedipostValidationError } from './MedipostValidationError'; + +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!; + +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 syncPrivateMessage({ + messageResponse, + order, +}: { + messageResponse: NonNullable; + order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; +}) { + const supabase = getSupabaseServerAdminClient() + + 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[messageResponse.TellimuseOlek], + 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); + console.info(`Order has results for ${analysisGroups.length} analysis groups`); + + const responses: Omit< + Tables<{ schema: 'medreport' }, 'analysis_response_elements'>, + 'id' | 'created_at' | 'updated_at' + >[] = []; + + const analysisResponseId = analysisResponse[0]!.id; + + for (const analysisGroup of analysisGroups) { + const groupItems = toArray( + analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'], + ); + console.info(`Order has results in group ${analysisGroup.UuringuGruppNimi} for ${groupItems.length} analysis elements`); + 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: analysisResponseId, + 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, + comment: element.UuringuKommentaar ?? '', + })), + ); + } + } + + const { error: deleteError } = await supabase + .schema('medreport') + .from('analysis_response_elements') + .delete() + .eq('analysis_response_id', analysisResponseId); + + if (deleteError) { + throw new Error( + `Failed to clean up response elements for response id ${analysisResponseId}`, + ); + } + + 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 ${analysisResponseId}`, + ); + } + + const { data: allOrderResponseElements } = await supabase + .schema('medreport') + .from('analysis_response_elements') + .select('*') + .eq('analysis_response_id', analysisResponseId) + .throwOnError(); + const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; + if (allOrderResponseElements.length !== expectedOrderResponseElements) { + return { isPartial: true }; + } + + const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek]; + return { isCompleted: statusFromResponse === 'COMPLETED' }; +} + +export async function readPrivateMessageResponse({ + excludedMessageIds, +}: { + excludedMessageIds: string[]; +}): Promise<{ messageId: string | null; hasAnalysisResponse: boolean; hasPartialAnalysisResponse: boolean; hasFullAnalysisResponse: boolean; medusaOrderId: string | undefined; analysisOrderId: number | undefined }> { + let messageId: string | null = null; + let hasAnalysisResponse = false; + let hasPartialAnalysisResponse = false; + let hasFullAnalysisResponse = false; + let medusaOrderId: string | undefined = undefined; + let analysisOrderId: number | undefined = undefined; + + try { + const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds }); + messageId = privateMessage?.messageId ?? null; + + if (!privateMessage || !messageId) { + return { + messageId: null, + hasAnalysisResponse: false, + hasPartialAnalysisResponse: false, + hasFullAnalysisResponse: false, + medusaOrderId: undefined, + analysisOrderId: undefined + }; + } + + const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage( + privateMessage.messageId, + ); + + const messageResponse = privateMessageContent?.Saadetis?.Vastus; + analysisOrderId = Number(privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId); + + const hasInvalidOrderId = isNaN(analysisOrderId) + + if (hasInvalidOrderId || !messageResponse) { + await createMedipostActionLog({ + action: 'sync_analysis_results_from_medipost', + xml: privateMessageXml, + hasAnalysisResults: false, + }); + return { + messageId, + hasAnalysisResponse: false, + hasPartialAnalysisResponse: false, + hasFullAnalysisResponse: false, + medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId, + analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId + }; + } + + const analysisOrder = await getAnalysisOrder({ analysisOrderId: analysisOrderId }) + medusaOrderId = analysisOrder.medusa_order_id; + + let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; + try { + order = await getAnalysisOrder({ medusaOrderId }); + } catch (e) { + await deletePrivateMessage(privateMessage.messageId); + throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); + } + + const status = await syncPrivateMessage({ messageResponse, order }); + + if (status.isPartial) { + await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); + hasAnalysisResponse = true; + hasPartialAnalysisResponse = true; + } else if (status.isCompleted) { + await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); + await deletePrivateMessage(privateMessage.messageId); + hasAnalysisResponse = true; + hasFullAnalysisResponse = true; + } + } catch (e) { + console.warn(`Failed to process private message id=${messageId}, message=${(e as Error).message}`); + } + + return { messageId, hasAnalysisResponse, hasPartialAnalysisResponse, hasFullAnalysisResponse, medusaOrderId, analysisOrderId }; +} + +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 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 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, { canHaveEmptyCode: true }); + + return { + message: parseXML(data) as MedipostOrderResponse, + xml: data as string, + }; +} + +export async function sendOrderToMedipost({ + medusaOrderId, + orderedAnalysisElements, +}: { + medusaOrderId: string; + orderedAnalysisElements: OrderedAnalysisElement[]; +}) { + const medreportOrder = await getAnalysisOrder({ 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 ?? '', + }, + orderId: medreportOrder.id, + orderCreatedAt: new Date(medreportOrder.created_at), + comment: '', + }); + + try { + await sendPrivateMessage(orderXml); + } catch (e) { + const isMedipostError = e instanceof MedipostValidationError; + if (isMedipostError) { + await logMedipostDispatch({ + medusaOrderId, + isSuccess: false, + isMedipostError, + errorMessage: e.response, + }); + await createMedipostActionLog({ + action: 'send_order_to_medipost', + xml: orderXml, + hasAnalysisResults: false, + medusaOrderId, + responseXml: e.response, + hasError: true, + }); + } else { + await logMedipostDispatch({ + medusaOrderId, + isSuccess: false, + isMedipostError, + }); + await createMedipostActionLog({ + action: 'send_order_to_medipost', + xml: orderXml, + hasAnalysisResults: false, + medusaOrderId, + hasError: true, + }); + } + + throw e; + } + await logMedipostDispatch({ + medusaOrderId, + isSuccess: true, + isMedipostError: false, + }); + await createMedipostActionLog({ + action: 'send_order_to_medipost', + xml: orderXml, + hasAnalysisResults: false, + medusaOrderId, + }); + await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); +} From 19631fb2db958702529d0af1332d9686dd233e9b Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:15:08 +0300 Subject: [PATCH 04/27] feat(MED-161): delete unused functions --- lib/services/medipost.service.ts | 216 ------------------------------- 1 file changed, 216 deletions(-) diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index 6aa4179..fc9b447 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -1,30 +1,17 @@ 'use server'; -import { - SupabaseClient, - createClient as createCustomClient, -} from '@supabase/supabase-js'; - -import { SyncStatus } from '@/lib/types/audit'; import { GetMessageListResponse, MedipostAction, - MedipostPublicMessageResponse, - UuringuGrupp, } from '@/lib/types/medipost'; -import { toArray } from '@/lib/utils'; import axios from 'axios'; -import { Tables } from '@kit/supabase/database'; -import { createAnalysisGroup } from './analysis-group.service'; import { getAnalysisElements } from './analysis-element.service'; import { getAnalyses } from './analyses.service'; import { StoreOrder } from '@medusajs/types'; import { listProducts } from '@lib/data/products'; import { listRegions } from '@lib/data/regions'; import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product'; -import { validateMedipostResponse } from './medipost/medipostValidate.service'; -import { parseXML } from './util/xml.service'; import { getLatestMessage } from './medipost/medipostMessageBase.service'; const BASE_URL = process.env.MEDIPOST_URL!; @@ -34,21 +21,6 @@ const RECIPIENT = process.env.MEDIPOST_RECIPIENT!; const ANALYSIS_PACKAGE_HANDLE_PREFIX = 'analysis-package-'; -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: { @@ -68,194 +40,6 @@ export async function getLatestPublicMessageListItem() { 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; -} - -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 getOrderedAnalysisIds({ medusaOrder, }: { From 0d08592a9af2e42e041d73162f17daa6401262fd Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:15:16 +0300 Subject: [PATCH 05/27] feat(MED-161): move medipost publicmessage logic to separate service --- app/api/job/handler/sync-analysis-groups.ts | 2 +- lib/services/medipost.service.ts | 31 ----------------- .../medipost/medipostPublicMessage.service.ts | 33 +++++++++++++++++++ 3 files changed, 34 insertions(+), 32 deletions(-) create mode 100644 lib/services/medipost/medipostPublicMessage.service.ts diff --git a/app/api/job/handler/sync-analysis-groups.ts b/app/api/job/handler/sync-analysis-groups.ts index 8509bbb..d2d5115 100644 --- a/app/api/job/handler/sync-analysis-groups.ts +++ b/app/api/job/handler/sync-analysis-groups.ts @@ -7,7 +7,7 @@ import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry import { getLastCheckedDate } from '~/lib/services/sync-entries.service'; import { createAnalysisElement } from '~/lib/services/analysis-element.service'; import { createCodes } from '~/lib/services/codes.service'; -import { getLatestPublicMessageListItem } from '~/lib/services/medipost.service'; +import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service'; import type { ICode } from '~/lib/types/code'; function toArray(input?: T | T[] | null): T[] { diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index fc9b447..5b3396f 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -1,45 +1,14 @@ 'use server'; -import { - GetMessageListResponse, - MedipostAction, -} from '@/lib/types/medipost'; -import axios from 'axios'; - import { getAnalysisElements } from './analysis-element.service'; import { getAnalyses } from './analyses.service'; import { StoreOrder } from '@medusajs/types'; import { listProducts } from '@lib/data/products'; import { listRegions } from '@lib/data/regions'; import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product'; -import { getLatestMessage } from './medipost/medipostMessageBase.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!; const ANALYSIS_PACKAGE_HANDLE_PREFIX = 'analysis-package-'; -export async function getLatestPublicMessageListItem() { - const { data } = await axios.get(BASE_URL, { - params: { - Action: MedipostAction.GetPublicMessageList, - User: USER, - Password: PASSWORD, - Sender: RECIPIENT, - // 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 getOrderedAnalysisIds({ medusaOrder, }: { diff --git a/lib/services/medipost/medipostPublicMessage.service.ts b/lib/services/medipost/medipostPublicMessage.service.ts new file mode 100644 index 0000000..21b21e4 --- /dev/null +++ b/lib/services/medipost/medipostPublicMessage.service.ts @@ -0,0 +1,33 @@ +'use server'; + +import { + GetMessageListResponse, + MedipostAction, +} from '@/lib/types/medipost'; +import axios from 'axios'; + +import { getLatestMessage } from './medipostMessageBase.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!; + +export async function getLatestPublicMessageListItem() { + const { data } = await axios.get(BASE_URL, { + params: { + Action: MedipostAction.GetPublicMessageList, + User: USER, + Password: PASSWORD, + Sender: RECIPIENT, + // 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 }); +} From 000cad7f05f3c939241c569a995896a58933f53f Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:15:26 +0300 Subject: [PATCH 06/27] feat(MED-161): move medipost services to medipost package --- app/api/job/handler/sync-analysis-groups.ts | 2 +- app/api/job/medipost-retry-dispatch/route.ts | 2 +- app/api/job/test-medipost-responses/route.ts | 4 ++-- app/api/order/medipost-test-response/route.ts | 4 ++-- .../(user)/(dashboard)/cart/montonio-callback/actions.ts | 2 +- lib/services/analyses.service.ts | 2 +- lib/services/analysis-element.service.ts | 2 +- lib/services/{ => medipost}/medipost.types.ts | 0 lib/services/medipost/medipostPrivateMessage.service.ts | 2 +- lib/services/{ => medipost}/medipostTest.service.ts | 6 +++--- lib/services/{ => medipost}/medipostXML.service.ts | 4 ++-- .../{medipost.service.ts => medusaOrder.service.ts} | 0 12 files changed, 15 insertions(+), 15 deletions(-) rename lib/services/{ => medipost}/medipost.types.ts (100%) rename lib/services/{ => medipost}/medipostTest.service.ts (97%) rename lib/services/{ => medipost}/medipostXML.service.ts (97%) rename lib/services/{medipost.service.ts => medusaOrder.service.ts} (100%) diff --git a/app/api/job/handler/sync-analysis-groups.ts b/app/api/job/handler/sync-analysis-groups.ts index d2d5115..c9bd2e0 100644 --- a/app/api/job/handler/sync-analysis-groups.ts +++ b/app/api/job/handler/sync-analysis-groups.ts @@ -2,7 +2,7 @@ import axios from 'axios'; import { XMLParser } from 'fast-xml-parser'; import fs from 'fs'; import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service'; -import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost.types'; +import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost/medipost.types'; import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry } from '~/lib/services/analyses.service'; import { getLastCheckedDate } from '~/lib/services/sync-entries.service'; import { createAnalysisElement } from '~/lib/services/analysis-element.service'; diff --git a/app/api/job/medipost-retry-dispatch/route.ts b/app/api/job/medipost-retry-dispatch/route.ts index 7b1a55d..3ccf0f9 100644 --- a/app/api/job/medipost-retry-dispatch/route.ts +++ b/app/api/job/medipost-retry-dispatch/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import loadEnv from "../handler/load-env"; import validateApiKey from "../handler/validate-api-key"; -import { getOrderedAnalysisIds } from "~/lib/services/medipost.service"; +import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service"; import { sendOrderToMedipost } from "~/lib/services/medipost/medipostPrivateMessage.service"; import { retrieveOrder } from "@lib/data/orders"; import { getMedipostDispatchTries } from "~/lib/services/audit.service"; diff --git a/app/api/job/test-medipost-responses/route.ts b/app/api/job/test-medipost-responses/route.ts index 7c2944d..619756f 100644 --- a/app/api/job/test-medipost-responses/route.ts +++ b/app/api/job/test-medipost-responses/route.ts @@ -1,9 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { getAnalysisOrdersAdmin } from "~/lib/services/order.service"; -import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service"; +import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service"; import { retrieveOrder } from "@lib/data"; import { getAccountAdmin } from "~/lib/services/account.service"; -import { getOrderedAnalysisIds } from "~/lib/services/medipost.service"; +import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service"; import loadEnv from "../handler/load-env"; import validateApiKey from "../handler/validate-api-key"; diff --git a/app/api/order/medipost-test-response/route.ts b/app/api/order/medipost-test-response/route.ts index fb7e8b0..5142972 100644 --- a/app/api/order/medipost-test-response/route.ts +++ b/app/api/order/medipost-test-response/route.ts @@ -1,10 +1,10 @@ import { NextResponse } from "next/server"; import { getAnalysisOrder } from "~/lib/services/order.service"; -import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service"; +import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service"; import { retrieveOrder } from "@lib/data"; import { getAccountAdmin } from "~/lib/services/account.service"; import { createMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service"; -import { getOrderedAnalysisIds } from "~/lib/services/medipost.service"; +import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service"; export async function POST(request: Request) { // const isDev = process.env.NODE_ENV === 'development'; diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index 23d39de..5650f0c 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -9,7 +9,7 @@ import { placeOrder, retrieveCart } from "@lib/data/cart"; import { createI18nServerInstance } from "~/lib/i18n/i18n.server"; import { createAnalysisOrder, getAnalysisOrder } from '~/lib/services/order.service'; import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service'; -import { getOrderedAnalysisIds } from '~/lib/services/medipost.service'; +import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service'; import { createNotificationsApi } from '@kit/notifications/api'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import type { AccountWithParams } from '@kit/accounts/api'; diff --git a/lib/services/analyses.service.ts b/lib/services/analyses.service.ts index 790b201..7eabeac 100644 --- a/lib/services/analyses.service.ts +++ b/lib/services/analyses.service.ts @@ -1,6 +1,6 @@ import type { Tables } from '@/packages/supabase/src/database.types'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; -import type { IUuringElement } from "./medipost.types"; +import type { IUuringElement } from "./medipost/medipost.types"; export type AnalysesWithGroupsAndElements = ({ analysis_elements: Tables<{ schema: 'medreport' }, 'analysis_elements'> & { diff --git a/lib/services/analysis-element.service.ts b/lib/services/analysis-element.service.ts index 635320c..9049254 100644 --- a/lib/services/analysis-element.service.ts +++ b/lib/services/analysis-element.service.ts @@ -1,6 +1,6 @@ import { Json, Tables } from '@kit/supabase/database'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; -import type { IMaterialGroup, IUuringElement } from './medipost.types'; +import type { IMaterialGroup, IUuringElement } from './medipost/medipost.types'; export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements'> & { analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>; diff --git a/lib/services/medipost.types.ts b/lib/services/medipost/medipost.types.ts similarity index 100% rename from lib/services/medipost.types.ts rename to lib/services/medipost/medipost.types.ts diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index afb341b..ab4d406 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -18,7 +18,7 @@ import { createMedipostActionLog, getLatestMessage } from './medipostMessageBase import { validateMedipostResponse } from './medipostValidate.service'; import { getAnalysisOrder, updateAnalysisOrderStatus } from '../order.service'; import { parseXML } from '../util/xml.service'; -import { composeOrderXML, OrderedAnalysisElement } from '../medipostXML.service'; +import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service'; import { getAccountAdmin } from '../account.service'; import { logMedipostDispatch } from '../audit.service'; import { MedipostValidationError } from './MedipostValidationError'; diff --git a/lib/services/medipostTest.service.ts b/lib/services/medipost/medipostTest.service.ts similarity index 97% rename from lib/services/medipostTest.service.ts rename to lib/services/medipost/medipostTest.service.ts index 18dd35a..a7ce270 100644 --- a/lib/services/medipostTest.service.ts +++ b/lib/services/medipost/medipostTest.service.ts @@ -16,9 +16,9 @@ import { uniqBy } from 'lodash'; import { Tables } from '@kit/supabase/database'; import { formatDate } from 'date-fns'; -import { getAnalyses } from './analyses.service'; -import { getAnalysisElementsAdmin } from './analysis-element.service'; -import { validateMedipostResponse } from './medipost/medipostValidate.service'; +import { getAnalyses } from '../analyses.service'; +import { getAnalysisElementsAdmin } from '../analysis-element.service'; +import { validateMedipostResponse } from './medipostValidate.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; diff --git a/lib/services/medipostXML.service.ts b/lib/services/medipost/medipostXML.service.ts similarity index 97% rename from lib/services/medipostXML.service.ts rename to lib/services/medipost/medipostXML.service.ts index 77e00eb..335a011 100644 --- a/lib/services/medipostXML.service.ts +++ b/lib/services/medipost/medipostXML.service.ts @@ -18,8 +18,8 @@ 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'; +import { AnalysisElement } from '../analysis-element.service'; +import { AnalysesWithGroupsAndElements } from '../analyses.service'; const USER = process.env.MEDIPOST_USER!; const RECIPIENT = process.env.MEDIPOST_RECIPIENT!; diff --git a/lib/services/medipost.service.ts b/lib/services/medusaOrder.service.ts similarity index 100% rename from lib/services/medipost.service.ts rename to lib/services/medusaOrder.service.ts From cee37178dfa73a0814be6eb0b50d85e53c95c1ff Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:15:44 +0300 Subject: [PATCH 07/27] feat(MED-161): create analysis-order service --- lib/services/analysis-order.service.ts | 17 +++++++++++++++++ .../medipost/medipostPrivateMessage.service.ts | 10 +++------- lib/types/analysis-response-element.ts | 3 +++ 3 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 lib/services/analysis-order.service.ts create mode 100644 lib/types/analysis-response-element.ts diff --git a/lib/services/analysis-order.service.ts b/lib/services/analysis-order.service.ts new file mode 100644 index 0000000..e76ca16 --- /dev/null +++ b/lib/services/analysis-order.service.ts @@ -0,0 +1,17 @@ +import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client"; +import type { AnalysisResponseElement } from "../types/analysis-response-element"; + +export async function getExistingAnalysisResponseElements({ + analysisResponseId, +}: { + analysisResponseId: number; +}) { + const { data } = await getSupabaseServerAdminClient() + .schema('medreport') + .from('analysis_response_elements') + .select('*') + .eq('analysis_response_id', analysisResponseId) + .throwOnError(); + + return data as AnalysisResponseElement[]; +} diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index ab4d406..b6216b8 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -22,6 +22,7 @@ import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service'; import { getAccountAdmin } from '../account.service'; import { logMedipostDispatch } from '../audit.service'; import { MedipostValidationError } from './MedipostValidationError'; +import { getExistingAnalysisResponseElements } from '../analysis-order.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -152,14 +153,9 @@ export async function syncPrivateMessage({ ); } - const { data: allOrderResponseElements } = await supabase - .schema('medreport') - .from('analysis_response_elements') - .select('*') - .eq('analysis_response_id', analysisResponseId) - .throwOnError(); + const existingAnalysisResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId }); const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; - if (allOrderResponseElements.length !== expectedOrderResponseElements) { + if (existingAnalysisResponseElements.length !== expectedOrderResponseElements) { return { isPartial: true }; } diff --git a/lib/types/analysis-response-element.ts b/lib/types/analysis-response-element.ts new file mode 100644 index 0000000..db5b20f --- /dev/null +++ b/lib/types/analysis-response-element.ts @@ -0,0 +1,3 @@ +import type { Database } from "@/packages/supabase/src/database.types"; + +export type AnalysisResponseElement = Database['medreport']['Tables']['analysis_response_elements']['Row']; From 7af6aa438ec0ce6e88da4509a476153b40ff10f9 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:15:55 +0300 Subject: [PATCH 08/27] feat(MED-161): store more fields about medipost analysis response --- lib/types/medipost.ts | 2 +- packages/supabase/src/database.types.ts | 16 +++++++++++++--- ...21841_log_medipost_action_additional_info.sql | 11 +++++++++++ ...175433_analysis_response_element_negative.sql | 2 ++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 supabase/migrations/20250914221841_log_medipost_action_additional_info.sql create mode 100644 supabase/migrations/20250916175433_analysis_response_element_negative.sql diff --git a/lib/types/medipost.ts b/lib/types/medipost.ts index 583fc33..4f7bc0f 100644 --- a/lib/types/medipost.ts +++ b/lib/types/medipost.ts @@ -145,7 +145,7 @@ export type MedipostPublicMessageResponse = IMedipostResponseXMLBase & { }; export type UuringuVastus = { - VastuseVaartus: number; // text according to docs, but based on examples and logically, float + VastuseVaartus: string; // numeric or text like 'Negatiivne' VastuseAeg: string; NormYlem?: { '#text': number; diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index a09d6b8..e3f2af6 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -686,7 +686,9 @@ export type Database = { norm_upper_included: boolean | null original_response_element: Json response_time: string - response_value: number + response_value: number | null + response_value_is_negative?: boolean | null + status: string unit: string | null updated_at: string | null } @@ -704,7 +706,9 @@ export type Database = { norm_upper_included?: boolean | null original_response_element: Json response_time: string - response_value: number + response_value: number | null + response_value_is_negative?: boolean | null + status: string unit?: string | null updated_at?: string | null } @@ -722,7 +726,9 @@ export type Database = { norm_upper_included?: boolean | null original_response_element?: Json response_time?: string - response_value?: number + response_value?: number | null + response_value_is_negative?: boolean | null + status: string unit?: string | null updated_at?: string | null } @@ -1264,6 +1270,8 @@ export type Database = { action: string xml: string has_analysis_results: boolean + medipost_external_order_id: string + medipost_private_message_id: string medusa_order_id: string response_xml: string has_error: boolean @@ -1272,6 +1280,8 @@ export type Database = { action: string xml: string has_analysis_results: boolean + medipost_external_order_id: string + medipost_private_message_id: string medusa_order_id: string response_xml: string has_error: boolean diff --git a/supabase/migrations/20250914221841_log_medipost_action_additional_info.sql b/supabase/migrations/20250914221841_log_medipost_action_additional_info.sql new file mode 100644 index 0000000..54799af --- /dev/null +++ b/supabase/migrations/20250914221841_log_medipost_action_additional_info.sql @@ -0,0 +1,11 @@ +-- store extra audit info +ALTER TABLE medreport.medipost_actions ADD COLUMN medipost_private_message_id TEXT; +ALTER TABLE medreport.medipost_actions ADD COLUMN medipost_external_order_id TEXT; + +-- store status info for frontend texts +ALTER TABLE medreport.analysis_response_elements ADD COLUMN status TEXT; + +-- cancelled orders +ALTER TABLE medreport.analysis_response_elements ALTER COLUMN response_time DROP NOT NULL; +ALTER TABLE medreport.analysis_response_elements ALTER COLUMN response_value DROP NOT NULL; +ALTER TABLE medreport.analysis_response_elements ALTER COLUMN comment DROP NOT NULL; diff --git a/supabase/migrations/20250916175433_analysis_response_element_negative.sql b/supabase/migrations/20250916175433_analysis_response_element_negative.sql new file mode 100644 index 0000000..49c1649 --- /dev/null +++ b/supabase/migrations/20250916175433_analysis_response_element_negative.sql @@ -0,0 +1,2 @@ +ALTER TABLE medreport.analysis_response_elements +ADD COLUMN response_value_is_negative BOOLEAN; From 46774b286e30e4c91e4b17ef5d9aaab11e99aae9 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:16:00 +0300 Subject: [PATCH 09/27] feat(MED-161): return with type in `getAnalysisOrder` --- app/home/(user)/_components/order/order-details.tsx | 2 +- app/home/(user)/_components/orders/order-block.tsx | 2 +- app/home/(user)/_components/orders/order-items-table.tsx | 2 +- lib/services/order.service.ts | 5 ++--- lib/types/analysis-order.ts | 3 +++ 5 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 lib/types/analysis-order.ts diff --git a/app/home/(user)/_components/order/order-details.tsx b/app/home/(user)/_components/order/order-details.tsx index 1188566..e10f3bc 100644 --- a/app/home/(user)/_components/order/order-details.tsx +++ b/app/home/(user)/_components/order/order-details.tsx @@ -1,6 +1,6 @@ import { Trans } from '@kit/ui/trans'; import { formatDate } from 'date-fns'; -import { AnalysisOrder } from "~/lib/services/order.service"; +import type { AnalysisOrder } from "~/lib/types/analysis-order"; export default function OrderDetails({ order }: { order: AnalysisOrder diff --git a/app/home/(user)/_components/orders/order-block.tsx b/app/home/(user)/_components/orders/order-block.tsx index 003b0c6..ccffd9e 100644 --- a/app/home/(user)/_components/orders/order-block.tsx +++ b/app/home/(user)/_components/orders/order-block.tsx @@ -1,4 +1,4 @@ -import { AnalysisOrder } from "~/lib/services/order.service"; +import type { AnalysisOrder } from "~/lib/types/analysis-order"; import { Trans } from '@kit/ui/makerkit/trans'; import { StoreOrderLineItem } from "@medusajs/types"; import OrderItemsTable from "./order-items-table"; diff --git a/app/home/(user)/_components/orders/order-items-table.tsx b/app/home/(user)/_components/orders/order-items-table.tsx index b1e4852..fc27d81 100644 --- a/app/home/(user)/_components/orders/order-items-table.tsx +++ b/app/home/(user)/_components/orders/order-items-table.tsx @@ -18,7 +18,7 @@ import { } from '@kit/ui/table'; import { Trans } from '@kit/ui/trans'; -import { AnalysisOrder } from '~/lib/services/order.service'; +import type { AnalysisOrder } from '~/lib/types/analysis-order'; import { logAnalysisResultsNavigateAction } from './actions'; diff --git a/lib/services/order.service.ts b/lib/services/order.service.ts index eced8f9..4e934fd 100644 --- a/lib/services/order.service.ts +++ b/lib/services/order.service.ts @@ -2,8 +2,7 @@ import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client' import type { Tables } from '@kit/supabase/database'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; import type { StoreOrder } from '@medusajs/types'; - -export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>; +import type { AnalysisOrder } from '../types/analysis-order'; export async function createAnalysisOrder({ medusaOrder, @@ -103,7 +102,7 @@ export async function getAnalysisOrder({ if (error) { throw new Error(`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`); } - return order; + return order as AnalysisOrder; } export async function getAnalysisOrders({ diff --git a/lib/types/analysis-order.ts b/lib/types/analysis-order.ts new file mode 100644 index 0000000..a7ce721 --- /dev/null +++ b/lib/types/analysis-order.ts @@ -0,0 +1,3 @@ +import type { Tables } from '@kit/supabase/database'; + +export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>; From c51808d8995c60b738aa88d4e10dd62be1e8e679 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:16:09 +0300 Subject: [PATCH 10/27] feat(MED-161): separate doctor analysis view --- app/doctor/_components/analysis-doctor.tsx | 149 ++++++++++++++++++ app/doctor/_components/analysis-level-bar.tsx | 134 ++++++++++++++++ .../_components/doctor-analysis-wrapper.tsx | 3 +- 3 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 app/doctor/_components/analysis-doctor.tsx create mode 100644 app/doctor/_components/analysis-level-bar.tsx diff --git a/app/doctor/_components/analysis-doctor.tsx b/app/doctor/_components/analysis-doctor.tsx new file mode 100644 index 0000000..4178237 --- /dev/null +++ b/app/doctor/_components/analysis-doctor.tsx @@ -0,0 +1,149 @@ +'use client'; + +import React, { ReactElement, ReactNode, useMemo, useState } from 'react'; + +import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts'; +import { format } from 'date-fns'; +import { Info } from 'lucide-react'; + +import { Trans } from '@kit/ui/trans'; +import { cn } from '@kit/ui/utils'; + +import { AnalysisElement } from '~/lib/services/analysis-element.service'; + +import AnalysisLevelBar, { + AnalysisLevelBarSkeleton, + AnalysisResultLevel, +} from './analysis-level-bar'; + +export type AnalysisResultForDisplay = Pick< + UserAnalysisElement, + | 'norm_status' + | 'response_value' + | 'unit' + | 'norm_lower_included' + | 'norm_upper_included' + | 'norm_lower' + | 'norm_upper' + | 'response_time' +>; + +export enum AnalysisStatus { + NORMAL = 0, + MEDIUM = 1, + HIGH = 2, +} + +const AnalysisDoctor = ({ + analysisElement, + results, + startIcon, + endIcon, + isCancelled, +}: { + analysisElement: Pick; + results?: AnalysisResultForDisplay; + isCancelled?: boolean; + startIcon?: ReactElement | null; + endIcon?: ReactNode | null; +}) => { + const name = analysisElement.analysis_name_lab || ''; + const status = results?.norm_status || AnalysisStatus.NORMAL; + const value = results?.response_value || 0; + const unit = results?.unit || ''; + const normLowerIncluded = results?.norm_lower_included || false; + const normUpperIncluded = results?.norm_upper_included || false; + const normLower = results?.norm_lower || 0; + const normUpper = results?.norm_upper || 0; + + const [showTooltip, setShowTooltip] = useState(false); + const analysisResultLevel = useMemo(() => { + if (!results) { + return null; + } + + const isUnderNorm = value < normLower; + if (isUnderNorm) { + switch (status) { + case AnalysisStatus.MEDIUM: + return AnalysisResultLevel.LOW; + default: + return AnalysisResultLevel.VERY_LOW; + } + } + switch (status) { + case AnalysisStatus.MEDIUM: + return AnalysisResultLevel.HIGH; + case AnalysisStatus.HIGH: + return AnalysisResultLevel.VERY_HIGH; + default: + return AnalysisResultLevel.NORMAL; + } + }, [results, value, normLower]); + + return ( +
+
+
+ {startIcon ||
} + {name} + {results?.response_time && ( +
{ + e.stopPropagation(); + setShowTooltip(!showTooltip); + }} + onMouseLeave={() => setShowTooltip(false)} + > + {' '} + +
+ )} +
+ {results ? ( + <> +
+
{value}
+
{unit}
+
+
+ {normLower} - {normUpper} +
+ +
+
+ + {endIcon ||
} + + ) : (isCancelled ? null : ( + <> +
+
+ +
+
+
+ + + ))} +
+
+ ); +}; + +export default AnalysisDoctor; diff --git a/app/doctor/_components/analysis-level-bar.tsx b/app/doctor/_components/analysis-level-bar.tsx new file mode 100644 index 0000000..7bac5f4 --- /dev/null +++ b/app/doctor/_components/analysis-level-bar.tsx @@ -0,0 +1,134 @@ +import { useMemo } from 'react'; + +import { ArrowDown } from 'lucide-react'; + +import { cn } from '@kit/ui/utils'; +import { AnalysisResultForDisplay } from './analysis-doctor'; + +export enum AnalysisResultLevel { + VERY_LOW = 0, + LOW = 1, + NORMAL = 2, + HIGH = 3, + VERY_HIGH = 4, +} + +const Level = ({ + isActive = false, + color, + isFirst = false, + isLast = false, + arrowLocation, +}: { + isActive?: boolean; + color: 'destructive' | 'success' | 'warning' | 'gray-200'; + isFirst?: boolean; + isLast?: boolean; + arrowLocation?: number; +}) => { + return ( +
+ {isActive && ( +
+ +
+ )} +
+ ); +}; + +export const AnalysisLevelBarSkeleton = () => { + return ( +
+ +
+ ); +}; + +const AnalysisLevelBar = ({ + normLowerIncluded = true, + normUpperIncluded = true, + level, + results, +}: { + normLowerIncluded?: boolean; + normUpperIncluded?: boolean; + level: AnalysisResultLevel; + results: AnalysisResultForDisplay; +}) => { + + const { norm_lower: lower, norm_upper: upper, response_value: value } = results; + const arrowLocation = useMemo(() => { + if (value < lower!) { + return 0; + } + + if (normLowerIncluded || normUpperIncluded) { + return 50; + } + + const calculated = ((value - lower!) / (upper! - lower!)) * 100; + + if (calculated > 100) { + return 100; + } + + return calculated; + }, [value, upper, lower]); + + const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(() => [ + level === AnalysisResultLevel.VERY_LOW, + level === AnalysisResultLevel.LOW, + level === AnalysisResultLevel.HIGH, + level === AnalysisResultLevel.VERY_HIGH, + ], [level, value, upper, lower]); + + const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh; + + return ( +
+ {normLowerIncluded && ( + <> + + + + )} + + + + {normUpperIncluded && ( + <> + + + + )} +
+ ); +}; + +export default AnalysisLevelBar; diff --git a/app/doctor/_components/doctor-analysis-wrapper.tsx b/app/doctor/_components/doctor-analysis-wrapper.tsx index fec4c35..d2f0813 100644 --- a/app/doctor/_components/doctor-analysis-wrapper.tsx +++ b/app/doctor/_components/doctor-analysis-wrapper.tsx @@ -14,6 +14,7 @@ import { import { Trans } from '@kit/ui/trans'; import Analysis from '~/home/(user)/(dashboard)/analysis-results/_components/analysis'; +import AnalysisDoctor from './analysis-doctor'; export default function DoctorAnalysisWrapper({ analysisData, @@ -65,7 +66,7 @@ export default function DoctorAnalysisWrapper({ {analysisData.latestPreviousAnalysis && (
- From e5822fd55dbd38a0057d33af4f4e55659f414f3b Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:16:18 +0300 Subject: [PATCH 11/27] feat(MED-161): improve query --- .../job/handler/sync-analysis-groups-store.ts | 2 +- lib/services/analysis-element.service.ts | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/api/job/handler/sync-analysis-groups-store.ts b/app/api/job/handler/sync-analysis-groups-store.ts index ccf9c0a..7f6ee7b 100644 --- a/app/api/job/handler/sync-analysis-groups-store.ts +++ b/app/api/job/handler/sync-analysis-groups-store.ts @@ -165,7 +165,7 @@ async function createProducts({ medusa.admin.product.list({ category_id: allCategories.map(({ id }) => id), }), - getAnalysisElements({}), + getAnalysisElements({ getAll: true }), getAnalysisPackagesType(), getProductDefaultFields({ medusa }), ]) diff --git a/lib/services/analysis-element.service.ts b/lib/services/analysis-element.service.ts index 9049254..e4f3aff 100644 --- a/lib/services/analysis-element.service.ts +++ b/lib/services/analysis-element.service.ts @@ -7,11 +7,15 @@ export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements }; export async function getAnalysisElements({ + getAll, originalIds, ids, + analysisGroupId, }: { + getAll?: boolean; originalIds?: string[]; ids?: number[]; + analysisGroupId?: number; }): Promise { const query = getSupabaseServerAdminClient() .schema('medreport') @@ -19,14 +23,26 @@ export async function getAnalysisElements({ .select(`*, analysis_groups(*)`) .order('order', { ascending: true }); - if (Array.isArray(originalIds)) { + const hasOriginalIdsFilter = Array.isArray(originalIds); + const hasIdsFilter = Array.isArray(ids); + const hasAnalysisGroupIdFilter = typeof analysisGroupId === 'number'; + + if (!hasOriginalIdsFilter && !hasIdsFilter && !hasAnalysisGroupIdFilter && getAll !== true) { + throw new Error('Either originalIds, ids, or analysisGroupId must be provided'); + } + + if (hasOriginalIdsFilter) { query.in('analysis_id_original', [...new Set(originalIds)]); } - if (Array.isArray(ids)) { + if (hasIdsFilter) { query.in('id', ids); } + if (hasAnalysisGroupIdFilter) { + query.eq('parent_analysis_group_id', analysisGroupId); + } + const { data: analysisElements, error } = await query; if (error) { From 7870b8840a92cf5e7e9be4edba091f4a882d8b50 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:16:27 +0300 Subject: [PATCH 12/27] feat(MED-161): allow syncing new analyses for existing groups --- app/api/job/handler/sync-analysis-groups.ts | 78 ++++++++++++++------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/app/api/job/handler/sync-analysis-groups.ts b/app/api/job/handler/sync-analysis-groups.ts index c9bd2e0..59703fc 100644 --- a/app/api/job/handler/sync-analysis-groups.ts +++ b/app/api/job/handler/sync-analysis-groups.ts @@ -3,9 +3,9 @@ import { XMLParser } from 'fast-xml-parser'; import fs from 'fs'; import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service'; import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost/medipost.types'; -import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry } from '~/lib/services/analyses.service'; +import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry, getAnalyses } from '~/lib/services/analyses.service'; import { getLastCheckedDate } from '~/lib/services/sync-entries.service'; -import { createAnalysisElement } from '~/lib/services/analysis-element.service'; +import { AnalysisElement, createAnalysisElement, getAnalysisElements } from '~/lib/services/analysis-element.service'; import { createCodes } from '~/lib/services/codes.service'; import { getLatestPublicMessageListItem } from '~/lib/services/medipost/medipostPublicMessage.service'; import type { ICode } from '~/lib/types/code'; @@ -82,42 +82,60 @@ export default async function syncAnalysisGroups() { const codes: ICode[] = []; for (const analysisGroup of analysisGroups) { const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId); + let groupExistingAnalysisElements: AnalysisElement[] = []; + let analysisGroupId: number; if (existingAnalysisGroup) { - console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists`); - continue; + console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists, only creating new analysis elements`); + groupExistingAnalysisElements = await getAnalysisElements({ analysisGroupId: existingAnalysisGroup.id }); + analysisGroupId = existingAnalysisGroup.id; + } else { + analysisGroupId = await createAnalysisGroup({ + id: analysisGroup.UuringuGruppId, + name: analysisGroup.UuringuGruppNimi, + order: analysisGroup.UuringuGruppJarjekord, + }); + + const analysisGroupCodes = toArray(analysisGroup.Kood); + codes.push( + ...analysisGroupCodes.map((kood) => ({ + hk_code: kood.HkKood, + hk_code_multiplier: kood.HkKoodiKordaja, + coefficient: kood.Koefitsient, + price: kood.Hind, + analysis_group_id: analysisGroupId, + analysis_element_id: null, + analysis_id: null, + })), + ); } - // SAVE ANALYSIS GROUP - const analysisGroupId = await createAnalysisGroup({ - id: analysisGroup.UuringuGruppId, - name: analysisGroup.UuringuGruppNimi, - order: analysisGroup.UuringuGruppJarjekord, - }); - - const analysisGroupCodes = toArray(analysisGroup.Kood); - codes.push( - ...analysisGroupCodes.map((kood) => ({ - hk_code: kood.HkKood, - hk_code_multiplier: kood.HkKoodiKordaja, - coefficient: kood.Koefitsient, - price: kood.Hind, - analysis_group_id: analysisGroupId, - analysis_element_id: null, - analysis_id: null, - })), - ); - const analysisGroupItems = toArray(analysisGroup.Uuring); for (const item of analysisGroupItems) { - const analysisElement = item.UuringuElement; + const analysisElement = item.UuringuElement!; + const isExistingAnalysisElement = groupExistingAnalysisElements + .find(({ analysis_id_original }) => analysis_id_original === analysisElement.UuringId); + if (isExistingAnalysisElement) { + console.info(`Analysis element '${analysisElement.UuringNimi}' already exists`); + continue; + } const insertedAnalysisElementId = await createAnalysisElement({ - analysisElement, + analysisElement: analysisElement!, analysisGroupId, materialGroups: toArray(item.MaterjalideGrupp), }); + if (Array.isArray(analysisElement.UuringuElement)) { + for (const nestedAnalysisElement of analysisElement.UuringuElement) { + await createAnalysisElement({ + analysisElement: nestedAnalysisElement, + analysisGroupId, + materialGroups: toArray(item.MaterjalideGrupp), + }); + } + } + if (analysisElement.Kood) { const analysisElementCodes = toArray(analysisElement.Kood); codes.push( @@ -135,7 +153,15 @@ export default async function syncAnalysisGroups() { const analyses = analysisElement.UuringuElement; if (analyses?.length) { + const existingAnalyses = await getAnalyses({ originalIds: analyses.map(({ UuringId }) => UuringId) }); + for (const analysis of analyses) { + const isExistingAnalysis = existingAnalyses.find(({ analysis_id_original }) => analysis_id_original === analysis.UuringId); + if (isExistingAnalysis) { + console.info(`Analysis '${analysis.UuringNimi}' already exists`); + continue; + } + const insertedAnalysisId = await createAnalysis(analysis, analysisGroupId); if (analysis.Kood) { From 20a497938ee073b5636f0c15d3faadab30333475 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:16:35 +0300 Subject: [PATCH 13/27] feat(MED-161): delete unused loader --- .../(user)/_lib/server/load-user-analyses.ts | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 app/home/(user)/_lib/server/load-user-analyses.ts diff --git a/app/home/(user)/_lib/server/load-user-analyses.ts b/app/home/(user)/_lib/server/load-user-analyses.ts deleted file mode 100644 index 388bfec..0000000 --- a/app/home/(user)/_lib/server/load-user-analyses.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { cache } from 'react'; - -import { createAccountsApi } from '@kit/accounts/api'; -import { UserAnalysis } from '@kit/accounts/types/accounts'; -import { getSupabaseServerClient } from '@kit/supabase/server-client'; - -export type UserAnalyses = Awaited>; - -/** - * @name loadUserAnalyses - * @description - * Load the user's analyses. It's a cached per-request function that fetches the user workspace data. - * It can be used across the server components to load the user workspace data. - */ -export const loadUserAnalyses = cache(analysesLoader); - -async function analysesLoader(): Promise { - const client = getSupabaseServerClient(); - const api = createAccountsApi(client); - - return api.getUserAnalyses(); -} From afac628bcfabbba9e0609f25930687c200a553ff Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:16:42 +0300 Subject: [PATCH 14/27] feat(MED-161): move analysis types separately from accounts types --- packages/features/accounts/src/server/api.ts | 24 +-- .../features/accounts/src/types/accounts.ts | 58 -------- .../accounts/src/types/analysis-orders.ts | 3 + .../accounts/src/types/analysis-results.ts | 138 ++++++++++++++++++ 4 files changed, 154 insertions(+), 69 deletions(-) create mode 100644 packages/features/accounts/src/types/analysis-orders.ts create mode 100644 packages/features/accounts/src/types/analysis-results.ts diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index d1faaef..d29e52b 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -1,22 +1,24 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { Database } from '@kit/supabase/database'; - -import { AnalysisResultDetails, UserAnalysis } from '../types/accounts'; +import type { UuringElement, UuringuVastus } from '@kit/shared/types/medipost-analysis'; import PersonalCode from '~/lib/utils'; +import type { AnalysisResultDetails, AnalysisResultDetailsMapped, UserAnalysis } from '../types/analysis-results'; +import type { AnalysisOrder } from '../types/analysis-orders'; + export type AccountWithParams = Database['medreport']['Tables']['accounts']['Row'] & { accountParams: - | (Pick< - Database['medreport']['Tables']['account_params']['Row'], - 'weight' | 'height' - > & { - isSmoker: - | Database['medreport']['Tables']['account_params']['Row']['is_smoker'] - | null; - }) + | (Pick< + Database['medreport']['Tables']['account_params']['Row'], + 'weight' | 'height' + > & { + isSmoker: + | Database['medreport']['Tables']['account_params']['Row']['is_smoker'] | null; + }) + | null; }; /** @@ -25,7 +27,7 @@ export type AccountWithParams = * @param {SupabaseClient} client - The Supabase client instance. */ class AccountsApi { - constructor(private readonly client: SupabaseClient) {} + constructor(private readonly client: SupabaseClient) { } /** * @name getAccount diff --git a/packages/features/accounts/src/types/accounts.ts b/packages/features/accounts/src/types/accounts.ts index 86d51f3..02700b2 100644 --- a/packages/features/accounts/src/types/accounts.ts +++ b/packages/features/accounts/src/types/accounts.ts @@ -1,15 +1,5 @@ -import * as z from 'zod'; - import { Database } from '@kit/supabase/database'; -export type UserAnalysisElement = - Database['medreport']['Tables']['analysis_response_elements']['Row']; -export type UserAnalysisResponse = - Database['medreport']['Tables']['analysis_responses']['Row'] & { - elements: UserAnalysisElement[]; - }; -export type UserAnalysis = UserAnalysisResponse[]; - export type ApplicationRole = Database['medreport']['Tables']['accounts']['Row']['application_role']; export enum ApplicationRoleEnum { @@ -17,51 +7,3 @@ export enum ApplicationRoleEnum { Doctor = 'doctor', SuperAdmin = 'super_admin', } - -export const ElementSchema = z.object({ - unit: z.string(), - norm_lower: z.number(), - norm_upper: z.number(), - norm_status: z.number(), - analysis_name: z.string(), - response_time: z.string(), - response_value: z.number(), - norm_lower_included: z.boolean(), - norm_upper_included: z.boolean(), -}); -export type Element = z.infer; - -export const OrderSchema = z.object({ - status: z.string(), - medusa_order_id: z.string(), - created_at: z.coerce.date(), -}); -export type Order = z.infer; - -export const SummarySchema = z.object({ - id: z.number(), - value: z.string(), - status: z.string(), - user_id: z.string(), - created_at: z.coerce.date(), - created_by: z.string(), - updated_at: z.coerce.date().nullable(), - updated_by: z.string(), - doctor_user_id: z.string().nullable(), - analysis_order_id: z.number(), -}); -export type Summary = z.infer; - -export const AnalysisResultDetailsSchema = z.object({ - id: z.number(), - analysis_order_id: z.number(), - order_number: z.string(), - order_status: z.string(), - user_id: z.string(), - created_at: z.coerce.date(), - updated_at: z.coerce.date().nullable(), - elements: z.array(ElementSchema), - order: OrderSchema, - summary: SummarySchema.nullable(), -}); -export type AnalysisResultDetails = z.infer; diff --git a/packages/features/accounts/src/types/analysis-orders.ts b/packages/features/accounts/src/types/analysis-orders.ts new file mode 100644 index 0000000..4ef4027 --- /dev/null +++ b/packages/features/accounts/src/types/analysis-orders.ts @@ -0,0 +1,3 @@ +import { Tables } from '@kit/supabase/database'; + +export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>; diff --git a/packages/features/accounts/src/types/analysis-results.ts b/packages/features/accounts/src/types/analysis-results.ts new file mode 100644 index 0000000..64ddb83 --- /dev/null +++ b/packages/features/accounts/src/types/analysis-results.ts @@ -0,0 +1,138 @@ +import * as z from 'zod'; + +import { Database } from '@kit/supabase/database'; + +export type UserAnalysisElement = + Database['medreport']['Tables']['analysis_response_elements']['Row']; +export type UserAnalysisResponse = + Database['medreport']['Tables']['analysis_responses']['Row'] & { + elements: UserAnalysisElement[]; + }; +export type UserAnalysis = UserAnalysisResponse[]; + +const ElementSchema = z.object({ + unit: z.string(), + norm_lower: z.number(), + norm_upper: z.number(), + norm_status: z.number(), + analysis_name: z.string(), + response_time: z.string(), + response_value: z.number(), + response_value_is_negative: z.boolean(), + norm_lower_included: z.boolean(), + norm_upper_included: z.boolean(), + status: z.string(), + analysis_element_original_id: z.string(), + original_response_element: z.object({ + + }), +}); + +const OrderSchema = z.object({ + status: z.string(), + medusa_order_id: z.string(), + created_at: z.coerce.date(), +}); + +const DoctorAnalysisFeedbackSchema = z.object({ + id: z.number(), + status: z.string(), + user_id: z.string(), + created_at: z.coerce.date(), + created_by: z.string(), +}); + +const SummarySchema = z.object({ + id: z.number(), + value: z.string(), + status: z.string(), + user_id: z.string(), + created_at: z.coerce.date(), + created_by: z.string(), + updated_at: z.coerce.date().nullable(), + updated_by: z.string(), + doctor_user_id: z.string().nullable(), + analysis_order_id: z.number(), + doctor_analysis_feedback: z.array(DoctorAnalysisFeedbackSchema), +}); + +export const AnalysisResultDetailsSchema = z.object({ + id: z.number(), + analysis_order_id: z.number(), + order_number: z.string(), + order_status: z.string(), + user_id: z.string(), + created_at: z.coerce.date(), + updated_at: z.coerce.date().nullable(), + elements: z.array(ElementSchema), + order: OrderSchema, + summary: SummarySchema.nullable(), +}); +export type AnalysisResultDetails = z.infer; + +export type AnalysisResultDetailsElementResults = { + unit: string | null; + normLower: number | null; + normUpper: number | null; + normStatus: number | null; + responseTime: string | null; + responseValue: number | null; + responseValueIsNegative: boolean | null; + normLowerIncluded: boolean; + normUpperIncluded: boolean; + status: string; + analysisElementOriginalId: string; + nestedElements: { + analysisElementOriginalId: string; + normLower?: number | null; + normLowerIncluded: boolean; + normStatus: number; + normUpper?: number | null; + normUpperIncluded: boolean; + responseTime: string; + responseValue: number; + status: number; + unit: string; + }[]; + labComment?: string | null; +}; + +export type AnalysisResultDetailsElement = { + analysisIdOriginal: string; + isWaitingForResults: boolean; + analysisName: string; + results: AnalysisResultDetailsElementResults; +}; + +export type AnalysisResultDetailsMapped = { + id: number; + order: { + status: string; + medusaOrderId: string; + createdAt: Date | string; + }; + elements: { + id: string; + unit: string; + norm_lower: number; + norm_upper: number; + norm_status: number; + analysis_name: string; + response_time: string; + response_value: number; + norm_lower_included: boolean; + norm_upper_included: boolean; + status: string; + analysis_element_original_id: string; + }[]; + orderedAnalysisElementIds: number[]; + orderedAnalysisElements: AnalysisResultDetailsElement[]; + summary: { + id: number; + status: string; + user_id: string; + created_at: Date; + created_by: string; + value?: string; + } | null; +}; From e7650e3c32b64a5aed02b97edaa1d1f1a0b2e173 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:16:50 +0300 Subject: [PATCH 15/27] feat(MED-161): move type to types file --- .../(dashboard)/cart/montonio-callback/actions.ts | 2 +- app/home/(user)/_components/dashboard.tsx | 2 +- .../(user)/_lib/server/load-analysis-packages.ts | 4 ++-- .../_components/account-preferences-form.tsx | 2 +- .../_components/account-settings-form.tsx | 2 +- packages/features/accounts/src/server/api.ts | 15 +-------------- packages/features/accounts/src/types/accounts.ts | 14 ++++++++++++++ packages/shared/package.json | 3 ++- 8 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index 5650f0c..7e7b22e 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -12,7 +12,7 @@ import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMess import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service'; import { createNotificationsApi } from '@kit/notifications/api'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; -import type { AccountWithParams } from '@kit/accounts/api'; +import type { AccountWithParams } from '@kit/accounts/types/accounts'; import type { StoreOrder } from '@medusajs/types'; const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages'; diff --git a/app/home/(user)/_components/dashboard.tsx b/app/home/(user)/_components/dashboard.tsx index f8b2e79..51faecd 100644 --- a/app/home/(user)/_components/dashboard.tsx +++ b/app/home/(user)/_components/dashboard.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; -import type { AccountWithParams } from '@/packages/features/accounts/src/server/api'; +import type { AccountWithParams } from '@kit/accounts/types/accounts'; import { Database } from '@/packages/supabase/src/database.types'; import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons'; import { diff --git a/app/home/(user)/_lib/server/load-analysis-packages.ts b/app/home/(user)/_lib/server/load-analysis-packages.ts index f7fffb9..596a041 100644 --- a/app/home/(user)/_lib/server/load-analysis-packages.ts +++ b/app/home/(user)/_lib/server/load-analysis-packages.ts @@ -5,8 +5,8 @@ import { listRegions } from '@lib/data/regions'; import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product'; 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 type { AccountWithParams } from '@kit/accounts/types/accounts'; +import type { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package'; import PersonalCode from '~/lib/utils'; async function countryCodesLoader() { diff --git a/app/home/(user)/settings/_components/account-preferences-form.tsx b/app/home/(user)/settings/_components/account-preferences-form.tsx index aa19746..f36adb3 100644 --- a/app/home/(user)/settings/_components/account-preferences-form.tsx +++ b/app/home/(user)/settings/_components/account-preferences-form.tsx @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { Trans } from 'react-i18next'; -import { AccountWithParams } from '@kit/accounts/api'; +import type { AccountWithParams } from '@kit/accounts/types/accounts'; import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data'; import { Button } from '@kit/ui/button'; import { Card, CardDescription, CardTitle } from '@kit/ui/card'; diff --git a/app/home/(user)/settings/_components/account-settings-form.tsx b/app/home/(user)/settings/_components/account-settings-form.tsx index 8513798..5aba272 100644 --- a/app/home/(user)/settings/_components/account-settings-form.tsx +++ b/app/home/(user)/settings/_components/account-settings-form.tsx @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { Trans } from 'react-i18next'; -import { AccountWithParams } from '@kit/accounts/api'; +import type { AccountWithParams } from '@kit/accounts/types/accounts'; import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data'; import { Button } from '@kit/ui/button'; import { diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index d29e52b..187da61 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -6,20 +6,7 @@ import PersonalCode from '~/lib/utils'; import type { AnalysisResultDetails, AnalysisResultDetailsMapped, UserAnalysis } from '../types/analysis-results'; import type { AnalysisOrder } from '../types/analysis-orders'; - -export type AccountWithParams = - Database['medreport']['Tables']['accounts']['Row'] & { - accountParams: - | (Pick< - Database['medreport']['Tables']['account_params']['Row'], - 'weight' | 'height' - > & { - isSmoker: - | Database['medreport']['Tables']['account_params']['Row']['is_smoker'] - | null; - }) - | null; - }; +import { AccountWithParams } from '../types/accounts'; /** * Class representing an API for interacting with user accounts. diff --git a/packages/features/accounts/src/types/accounts.ts b/packages/features/accounts/src/types/accounts.ts index 02700b2..7259cfb 100644 --- a/packages/features/accounts/src/types/accounts.ts +++ b/packages/features/accounts/src/types/accounts.ts @@ -7,3 +7,17 @@ export enum ApplicationRoleEnum { Doctor = 'doctor', SuperAdmin = 'super_admin', } + +export type AccountWithParams = + Database['medreport']['Tables']['accounts']['Row'] & { + accountParams: + | (Pick< + Database['medreport']['Tables']['account_params']['Row'], + 'weight' | 'height' + > & { + isSmoker: + | Database['medreport']['Tables']['account_params']['Row']['is_smoker'] + | null; + }) + | null; + }; diff --git a/packages/shared/package.json b/packages/shared/package.json index 27fc7b2..a0fc483 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -16,7 +16,8 @@ "./events": "./src/events/index.tsx", "./components/*": "./src/components/*.tsx", "./registry": "./src/registry/index.ts", - "./config": "./src/config/index.ts" + "./config": "./src/config/index.ts", + "./types/*": "./src/types/*.ts" }, "devDependencies": { "@kit/eslint-config": "workspace:*", From 4ac261fd0d5c1a5e7e6ed16fed9633e2888203b5 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:16:59 +0300 Subject: [PATCH 16/27] feat(MED-161): move analyses logic to separate package from `kit/accounts` --- app/home/(user)/(dashboard)/page.tsx | 4 +- app/home/[account]/page.tsx | 6 +- package.json | 1 + packages/features/accounts/src/server/api.ts | 103 ------ .../features/user-analyses/eslint.config.mjs | 3 + packages/features/user-analyses/package.json | 33 ++ .../features/user-analyses/src/server/api.ts | 307 ++++++++++++++++++ .../src/types/analysis-orders.ts | 3 + .../src/types/analysis-results.ts | 138 ++++++++ packages/features/user-analyses/tsconfig.json | 11 + pnpm-lock.yaml | 186 +++++------ 11 files changed, 594 insertions(+), 201 deletions(-) create mode 100644 packages/features/user-analyses/eslint.config.mjs create mode 100644 packages/features/user-analyses/package.json create mode 100644 packages/features/user-analyses/src/server/api.ts create mode 100644 packages/features/user-analyses/src/types/analysis-orders.ts create mode 100644 packages/features/user-analyses/src/types/analysis-results.ts create mode 100644 packages/features/user-analyses/tsconfig.json diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index ca9fc22..de4d375 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation'; import { toTitleCase } from '@/lib/utils'; -import { createAccountsApi } from '@/packages/features/accounts/src/server/api'; +import { createUserAnalysesApi } from '@kit/user-analyses/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; import { PageBody, PageHeader } from '@kit/ui/page'; @@ -27,7 +27,7 @@ async function UserHomePage() { const client = getSupabaseServerClient(); const { account } = await loadCurrentUserAccount(); - const api = createAccountsApi(client); + const api = createUserAnalysesApi(client); const bmiThresholds = await api.fetchBmiThresholds(); if (!account) { diff --git a/app/home/[account]/page.tsx b/app/home/[account]/page.tsx index 8bc2ade..8279c92 100644 --- a/app/home/[account]/page.tsx +++ b/app/home/[account]/page.tsx @@ -2,10 +2,10 @@ import { use } from 'react'; -import { createAccountsApi } from '@/packages/features/accounts/src/server/api'; import { CompanyGuard } from '@/packages/features/team-accounts/src/components'; import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; +import { createUserAnalysesApi } from '@kit/user-analyses/api'; import { PageBody } from '@kit/ui/page'; @@ -35,10 +35,10 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) { const account = use(params).account; const client = getSupabaseServerClient(); const teamAccountsApi = createTeamAccountsApi(client); - const accountsApi = createAccountsApi(client); + const userAnalysesApi = createUserAnalysesApi(client); const teamAccount = use(teamAccountsApi.getTeamAccount(account)); const { memberParams, members } = use(teamAccountsApi.getMembers(account)); - const bmiThresholds = use(accountsApi.fetchBmiThresholds()); + const bmiThresholds = use(userAnalysesApi.fetchBmiThresholds()); const companyParams = use( teamAccountsApi.getTeamAccountParams(teamAccount.id), ); diff --git a/package.json b/package.json index 33ec639..823ccc7 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@kit/supabase": "workspace:*", "@kit/team-accounts": "workspace:*", "@kit/ui": "workspace:*", + "@kit/user-analyses": "workspace:*", "@makerkit/data-loader-supabase-core": "^0.0.10", "@makerkit/data-loader-supabase-nextjs": "^1.2.5", "@marsidev/react-turnstile": "^1.1.0", diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index 187da61..015e0f4 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -1,11 +1,8 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { Database } from '@kit/supabase/database'; -import type { UuringElement, UuringuVastus } from '@kit/shared/types/medipost-analysis'; import PersonalCode from '~/lib/utils'; -import type { AnalysisResultDetails, AnalysisResultDetailsMapped, UserAnalysis } from '../types/analysis-results'; -import type { AnalysisOrder } from '../types/analysis-orders'; import { AccountWithParams } from '../types/accounts'; /** @@ -207,89 +204,6 @@ class AccountsApi { return response.data?.customer_id; } - async getUserAnalysis( - analysisOrderId: number, - ): Promise { - const authUser = await this.client.auth.getUser(); - const { data, error: userError } = authUser; - - if (userError) { - console.error('Failed to get user', userError); - throw userError; - } - - const { user } = data; - - const { data: analysisResponse } = await this.client - .schema('medreport') - .from('analysis_responses') - .select( - `*, - elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time), - order:analysis_order_id(medusa_order_id, status, created_at), - summary:analysis_order_id(doctor_analysis_feedback(*))`, - ) - .eq('user_id', user.id) - .eq('analysis_order_id', analysisOrderId) - .throwOnError(); - - const responseWithElements = analysisResponse?.[0]; - if (!responseWithElements) { - return null; - } - - const feedback = responseWithElements.summary.doctor_analysis_feedback?.[0]; - - return { - ...responseWithElements, - summary: - feedback?.status === 'COMPLETED' - ? responseWithElements.summary.doctor_analysis_feedback?.[0] - : null, - }; - } - - async getUserAnalyses(): Promise { - const authUser = await this.client.auth.getUser(); - const { data, error: userError } = authUser; - - if (userError) { - console.error('Failed to get user', userError); - throw userError; - } - - const { user } = data; - - const { data: analysisResponses } = await this.client - .schema('medreport') - .from('analysis_responses') - .select('*') - .eq('user_id', user.id); - - if (!analysisResponses) { - return null; - } - - const analysisResponseIds = analysisResponses.map((r) => r.id); - - const { data: analysisResponseElements } = await this.client - .schema('medreport') - .from('analysis_response_elements') - .select('*') - .in('analysis_response_id', analysisResponseIds); - - if (!analysisResponseElements) { - return null; - } - - return analysisResponses.map((r) => ({ - ...r, - elements: analysisResponseElements.filter( - (e) => e.analysis_response_id === r.id, - ), - })); - } - async hasAccountTeamMembership(accountId?: string) { if (!accountId) { return false; @@ -307,23 +221,6 @@ class AccountsApi { return (count ?? 0) > 0; } - - async fetchBmiThresholds() { - // Fetch BMI - const { data, error } = await this.client - .schema('medreport') - .from('bmi_thresholds') - .select( - 'age_min,age_max,underweight_max,normal_min,normal_max,overweight_min,strong_min,obesity_min', - ) - .order('age_min', { ascending: true }); - - if (error) { - console.error('Error fetching BMI thresholds:', error); - throw error; - } - return data; - } } export function createAccountsApi(client: SupabaseClient) { diff --git a/packages/features/user-analyses/eslint.config.mjs b/packages/features/user-analyses/eslint.config.mjs new file mode 100644 index 0000000..97563ae --- /dev/null +++ b/packages/features/user-analyses/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintConfigBase from '@kit/eslint-config/base.js'; + +export default eslintConfigBase; diff --git a/packages/features/user-analyses/package.json b/packages/features/user-analyses/package.json new file mode 100644 index 0000000..5d40175 --- /dev/null +++ b/packages/features/user-analyses/package.json @@ -0,0 +1,33 @@ +{ + "name": "@kit/user-analyses", + "private": true, + "version": "0.1.0", + "scripts": { + "clean": "git clean -xdf .turbo node_modules", + "format": "prettier --check \"**/*.{ts,tsx}\"", + "lint": "eslint .", + "typecheck": "tsc --noEmit" + }, + "exports": { + "./api": "./src/server/api.ts", + "./types/*": "./src/types/*.ts" + }, + "dependencies": { + "nanoid": "^5.1.5" + }, + "devDependencies": { + "@kit/eslint-config": "workspace:*", + "@kit/prettier-config": "workspace:*", + "@kit/shared": "workspace:*", + "@kit/supabase": "workspace:*", + "@kit/tsconfig": "workspace:*" + }, + "prettier": "@kit/prettier-config", + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + } +} diff --git a/packages/features/user-analyses/src/server/api.ts b/packages/features/user-analyses/src/server/api.ts new file mode 100644 index 0000000..b01047c --- /dev/null +++ b/packages/features/user-analyses/src/server/api.ts @@ -0,0 +1,307 @@ +import { SupabaseClient } from '@supabase/supabase-js'; + +import { Database } from '@kit/supabase/database'; +import type { UuringElement, UuringuVastus } from '@kit/shared/types/medipost-analysis'; + +import type { AnalysisResultDetails, AnalysisResultDetailsMapped, UserAnalysis } from '../types/analysis-results'; +import type { AnalysisOrder } from '../types/analysis-orders'; + +/** + * Class representing an API for interacting with user accounts. + * @constructor + * @param {SupabaseClient} client - The Supabase client instance. + */ +class UserAnalysesApi { + constructor(private readonly client: SupabaseClient) { } + + async getAnalysisOrder({ + medusaOrderId, + analysisOrderId, + }: { + medusaOrderId?: string; + analysisOrderId?: number; + }) { + const query = this.client + .schema('medreport') + .from('analysis_orders') + .select('*') + if (medusaOrderId) { + query.eq('medusa_order_id', medusaOrderId); + } else if (analysisOrderId) { + query.eq('id', analysisOrderId); + } else { + throw new Error('Either medusaOrderId or orderId must be provided'); + } + + const { data: order, error } = await query.single(); + if (error) { + throw new Error(`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`); + } + return order as AnalysisOrder; + } + + async getUserAnalysis( + analysisOrderId: number, + ): Promise { + const authUser = await this.client.auth.getUser(); + const { data, error: userError } = authUser; + + if (userError) { + console.error('Failed to get user', userError); + throw userError; + } + + const { user } = data; + + const analysisOrder = await this.getAnalysisOrder({ analysisOrderId }); + const orderedAnalysisElementIds = analysisOrder.analysis_element_ids ?? []; + if (orderedAnalysisElementIds.length === 0) { + console.error('No ordered analysis element ids found for analysis order id=', analysisOrderId); + return null; + } + const { data: orderedAnalysisElements, error: orderedAnalysisElementsError } = await this.client + .schema('medreport') + .from('analysis_elements') + .select('analysis_id_original,analysis_name_lab') + .in('id', orderedAnalysisElementIds); + if (orderedAnalysisElementsError) { + console.error('Failed to get ordered analysis elements for analysis order id=', analysisOrderId, orderedAnalysisElementsError); + throw orderedAnalysisElementsError; + } + + const orderedAnalysisElementOriginalIds = orderedAnalysisElements.map(({ analysis_id_original }) => analysis_id_original); + if (orderedAnalysisElementOriginalIds.length === 0) { + console.error('No ordered analysis element original ids found for analysis order id=', analysisOrderId); + return null; + } + + const { data: analysisResponse } = await this.client + .schema('medreport') + .from('analysis_responses') + .select( + `*, + elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time,status,analysis_element_original_id,original_response_element,response_value_is_negative), + summary:analysis_order_id(doctor_analysis_feedback(*))`, + ) + .eq('user_id', user.id) + .eq('analysis_order_id', analysisOrderId) + .throwOnError(); + + const responseWithElements = analysisResponse?.[0] as AnalysisResultDetails | null; + if (!responseWithElements) { + return null; + } + + const analysisResponseElements = responseWithElements.elements; + + const feedback = responseWithElements.summary?.doctor_analysis_feedback?.[0]; + + const mappedOrderedAnalysisElements = orderedAnalysisElements.map(({ analysis_id_original, analysis_name_lab }) => { + return this.getOrderedAnalysisElements({ + analysisIdOriginal: analysis_id_original, + analysisNameLab: analysis_name_lab, + analysisResponseElements, + }); + }).sort((a, b) => a.analysisName.localeCompare(b.analysisName)); + const nestedAnalysisElementIds = mappedOrderedAnalysisElements.map(({ results }) => results?.nestedElements.map(({ analysisElementOriginalId }) => analysisElementOriginalId)).flat(); + if (nestedAnalysisElementIds.length > 0) { + const { data: nestedAnalysisElements, error: nestedAnalysisElementsError } = await this.client + .schema('medreport') + .from('analysis_elements') + .select('*') + .in('id', nestedAnalysisElementIds); + console.info('analysisResponse nestedAnalysisElementIds', nestedAnalysisElementIds) + if (!nestedAnalysisElementsError && nestedAnalysisElements) { + for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) { + const { results } = mappedOrderedAnalysisElement; + if (!results) { + continue; + } + for (const nestedElement of results.nestedElements) { + const { analysisElementOriginalId } = nestedElement; + const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId); + if (!nestedAnalysisElement) { + continue; + } + results.nestedElements.push({ + ...nestedAnalysisElement, + analysisElementOriginalId, + analysisName: nestedAnalysisElement.analysis_name_lab, + }); + } + } + + mappedOrderedAnalysisElements.forEach(({ results }) => { + results?.nestedElements.forEach(({ analysisElementOriginalId }) => { + const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId); + if (nestedAnalysisElement) { + results?.nestedElements.push({ + ...nestedAnalysisElement, + analysisElementOriginalId, + analysisName: nestedAnalysisElement.analysis_name_lab, + }); + } + }); + }); + } + } + + return { + id: analysisOrderId, + order: { + status: analysisOrder.status, + medusaOrderId: analysisOrder.medusa_order_id, + createdAt: new Date(analysisOrder.created_at), + }, + orderedAnalysisElementIds, + orderedAnalysisElements: mappedOrderedAnalysisElements, + summary: + feedback?.status === 'COMPLETED' + ? (responseWithElements.summary?.doctor_analysis_feedback?.[0] ?? null) + : null, + }; + } + + getOrderedAnalysisElements({ + analysisIdOriginal, + analysisNameLab, + analysisResponseElements, + }: { + analysisIdOriginal: string; + analysisNameLab: string; + analysisResponseElements: AnalysisResultDetails['elements']; + }) { + const elementResponse = analysisResponseElements.find((element) => element.analysis_element_original_id === analysisIdOriginal); + if (!elementResponse) { + return { + analysisIdOriginal, + isWaitingForResults: true, + analysisName: analysisNameLab, + }; + } + const labComment = elementResponse.original_response_element?.UuringuKommentaar; + return { + analysisIdOriginal, + isWaitingForResults: false, + analysisName: analysisNameLab, + results: { + nestedElements: (() => { + const nestedElements = elementResponse.original_response_element?.UuringuElement as UuringElement[] | undefined; + if (!nestedElements) { + return []; + } + return nestedElements.map((element) => { + const elementVastus = element.UuringuVastus as UuringuVastus | undefined; + return { + status: element.UuringOlek, + unit: element.Mootyhik, + normLower: elementVastus?.NormAlum?.['#text'], + normUpper: elementVastus?.NormYlem?.['#text'], + normStatus: elementVastus?.NormiStaatus, + responseTime: elementVastus?.VastuseAeg, + responseValue: elementVastus?.VastuseVaartus, + responseValueIsNegative: elementVastus?.VastuseVaartus === 'Negatiivne', + normLowerIncluded: elementVastus?.NormAlum?.['@_kaasaarvatud'] === 'JAH', + normUpperIncluded: elementVastus?.NormYlem?.['@_kaasaarvatud'] === 'JAH', + analysisElementOriginalId: element.UuringId, + }; + }); + })(), + labComment, + //originalResponseElement: elementResponse.original_response_element ?? null, + unit: elementResponse.unit, + normLower: elementResponse.norm_lower, + normUpper: elementResponse.norm_upper, + normStatus: elementResponse.norm_status, + responseTime: elementResponse.response_time, + responseValue: elementResponse.response_value, + responseValueIsNegative: elementResponse.response_value_is_negative === true, + normLowerIncluded: elementResponse.norm_lower_included, + normUpperIncluded: elementResponse.norm_upper_included, + status: elementResponse.status, + analysisElementOriginalId: elementResponse.analysis_element_original_id, + } + }; + } + + // @TODO unused currently + async getUserAnalyses(): Promise { + const authUser = await this.client.auth.getUser(); + const { data, error: userError } = authUser; + + if (userError) { + console.error('Failed to get user', userError); + throw userError; + } + + const { user } = data; + + const { data: analysisResponses } = await this.client + .schema('medreport') + .from('analysis_responses') + .select('*') + .eq('user_id', user.id); + + if (!analysisResponses) { + return null; + } + + const analysisResponseIds = analysisResponses.map((r) => r.id); + + const { data: analysisResponseElements } = await this.client + .schema('medreport') + .from('analysis_response_elements') + .select('*') + .in('analysis_response_id', analysisResponseIds); + + if (!analysisResponseElements) { + return null; + } + + return analysisResponses.map((r) => ({ + ...r, + elements: analysisResponseElements.filter( + (e) => e.analysis_response_id === r.id, + ), + })); + } + + async hasAccountTeamMembership(accountId?: string) { + if (!accountId) { + return false; + } + + const { count, error } = await this.client + .schema('medreport') + .from('accounts_memberships') + .select('account_id', { count: 'exact', head: true }) + .eq('account_id', accountId); + + if (error) { + throw error; + } + + return (count ?? 0) > 0; + } + + async fetchBmiThresholds() { + // Fetch BMI + const { data, error } = await this.client + .schema('medreport') + .from('bmi_thresholds') + .select( + 'age_min,age_max,underweight_max,normal_min,normal_max,overweight_min,strong_min,obesity_min', + ) + .order('age_min', { ascending: true }); + + if (error) { + console.error('Error fetching BMI thresholds:', error); + throw error; + } + return data; + } +} + +export function createUserAnalysesApi(client: SupabaseClient) { + return new UserAnalysesApi(client); +} diff --git a/packages/features/user-analyses/src/types/analysis-orders.ts b/packages/features/user-analyses/src/types/analysis-orders.ts new file mode 100644 index 0000000..4ef4027 --- /dev/null +++ b/packages/features/user-analyses/src/types/analysis-orders.ts @@ -0,0 +1,3 @@ +import { Tables } from '@kit/supabase/database'; + +export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>; diff --git a/packages/features/user-analyses/src/types/analysis-results.ts b/packages/features/user-analyses/src/types/analysis-results.ts new file mode 100644 index 0000000..64ddb83 --- /dev/null +++ b/packages/features/user-analyses/src/types/analysis-results.ts @@ -0,0 +1,138 @@ +import * as z from 'zod'; + +import { Database } from '@kit/supabase/database'; + +export type UserAnalysisElement = + Database['medreport']['Tables']['analysis_response_elements']['Row']; +export type UserAnalysisResponse = + Database['medreport']['Tables']['analysis_responses']['Row'] & { + elements: UserAnalysisElement[]; + }; +export type UserAnalysis = UserAnalysisResponse[]; + +const ElementSchema = z.object({ + unit: z.string(), + norm_lower: z.number(), + norm_upper: z.number(), + norm_status: z.number(), + analysis_name: z.string(), + response_time: z.string(), + response_value: z.number(), + response_value_is_negative: z.boolean(), + norm_lower_included: z.boolean(), + norm_upper_included: z.boolean(), + status: z.string(), + analysis_element_original_id: z.string(), + original_response_element: z.object({ + + }), +}); + +const OrderSchema = z.object({ + status: z.string(), + medusa_order_id: z.string(), + created_at: z.coerce.date(), +}); + +const DoctorAnalysisFeedbackSchema = z.object({ + id: z.number(), + status: z.string(), + user_id: z.string(), + created_at: z.coerce.date(), + created_by: z.string(), +}); + +const SummarySchema = z.object({ + id: z.number(), + value: z.string(), + status: z.string(), + user_id: z.string(), + created_at: z.coerce.date(), + created_by: z.string(), + updated_at: z.coerce.date().nullable(), + updated_by: z.string(), + doctor_user_id: z.string().nullable(), + analysis_order_id: z.number(), + doctor_analysis_feedback: z.array(DoctorAnalysisFeedbackSchema), +}); + +export const AnalysisResultDetailsSchema = z.object({ + id: z.number(), + analysis_order_id: z.number(), + order_number: z.string(), + order_status: z.string(), + user_id: z.string(), + created_at: z.coerce.date(), + updated_at: z.coerce.date().nullable(), + elements: z.array(ElementSchema), + order: OrderSchema, + summary: SummarySchema.nullable(), +}); +export type AnalysisResultDetails = z.infer; + +export type AnalysisResultDetailsElementResults = { + unit: string | null; + normLower: number | null; + normUpper: number | null; + normStatus: number | null; + responseTime: string | null; + responseValue: number | null; + responseValueIsNegative: boolean | null; + normLowerIncluded: boolean; + normUpperIncluded: boolean; + status: string; + analysisElementOriginalId: string; + nestedElements: { + analysisElementOriginalId: string; + normLower?: number | null; + normLowerIncluded: boolean; + normStatus: number; + normUpper?: number | null; + normUpperIncluded: boolean; + responseTime: string; + responseValue: number; + status: number; + unit: string; + }[]; + labComment?: string | null; +}; + +export type AnalysisResultDetailsElement = { + analysisIdOriginal: string; + isWaitingForResults: boolean; + analysisName: string; + results: AnalysisResultDetailsElementResults; +}; + +export type AnalysisResultDetailsMapped = { + id: number; + order: { + status: string; + medusaOrderId: string; + createdAt: Date | string; + }; + elements: { + id: string; + unit: string; + norm_lower: number; + norm_upper: number; + norm_status: number; + analysis_name: string; + response_time: string; + response_value: number; + norm_lower_included: boolean; + norm_upper_included: boolean; + status: string; + analysis_element_original_id: string; + }[]; + orderedAnalysisElementIds: number[]; + orderedAnalysisElements: AnalysisResultDetailsElement[]; + summary: { + id: number; + status: string; + user_id: string; + created_at: Date; + created_by: string; + value?: string; + } | null; +}; diff --git a/packages/features/user-analyses/tsconfig.json b/packages/features/user-analyses/tsconfig.json new file mode 100644 index 0000000..8d5bae9 --- /dev/null +++ b/packages/features/user-analyses/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@kit/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", + "paths": { + "~/lib/utils": ["../../../lib/utils.ts"] + } + }, + "include": ["*.ts", "*.tsx", "src"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77d0440..041d098 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: '@kit/ui': specifier: workspace:* version: link:packages/ui + '@kit/user-analyses': + specifier: workspace:* + version: link:packages/features/user-analyses '@makerkit/data-loader-supabase-core': specifier: ^0.0.10 version: 0.0.10(@supabase/postgrest-js@1.19.4)(@supabase/supabase-js@2.49.4) @@ -478,10 +481,10 @@ importers: dependencies: '@keystatic/core': specifier: 0.5.47 - version: 0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@keystatic/next': specifier: ^5.0.4 - version: 5.0.4(@keystatic/core@0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 5.0.4(@keystatic/core@0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@markdoc/markdoc': specifier: ^0.5.1 version: 0.5.4(@types/react@19.1.4)(react@19.1.0) @@ -1073,6 +1076,28 @@ importers: specifier: ^2.0.3 version: 2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + packages/features/user-analyses: + dependencies: + nanoid: + specifier: ^5.1.5 + version: 5.1.5 + devDependencies: + '@kit/eslint-config': + specifier: workspace:* + version: link:../../../tooling/eslint + '@kit/prettier-config': + specifier: workspace:* + version: link:../../../tooling/prettier + '@kit/shared': + specifier: workspace:* + version: link:../../shared + '@kit/supabase': + specifier: workspace:* + version: link:../../supabase + '@kit/tsconfig': + specifier: workspace:* + version: link:../../../tooling/typescript + packages/i18n: dependencies: i18next: @@ -1272,7 +1297,7 @@ importers: dependencies: '@sentry/nextjs': specifier: ^9.19.0 - version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3) + version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3) import-in-the-middle: specifier: 1.13.2 version: 1.13.2 @@ -11457,7 +11482,7 @@ snapshots: '@juggle/resize-observer@3.4.0': {} - '@keystar/ui@0.7.19(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@keystar/ui@0.7.19(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 '@emotion/css': 11.13.5 @@ -11550,18 +11575,18 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: - next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - supports-color - '@keystatic/core@0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@keystatic/core@0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 '@braintree/sanitize-url': 6.0.4 '@emotion/weak-memoize': 0.3.1 '@floating-ui/react': 0.24.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@internationalized/string': 3.2.7 - '@keystar/ui': 0.7.19(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@keystar/ui': 0.7.19(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@markdoc/markdoc': 0.4.0(@types/react@19.1.4)(react@19.1.0) '@react-aria/focus': 3.20.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -11632,13 +11657,13 @@ snapshots: - next - supports-color - '@keystatic/next@5.0.4(@keystatic/core@0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@keystatic/next@5.0.4(@keystatic/core@0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@keystatic/core': 0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@keystatic/core': 0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/react': 19.1.4 chokidar: 3.6.0 - next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) server-only: 0.0.1 @@ -17232,7 +17257,7 @@ snapshots: '@sentry/core@9.46.0': {} - '@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3)': + '@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.34.0 @@ -17245,7 +17270,7 @@ snapshots: '@sentry/vercel-edge': 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)) '@sentry/webpack-plugin': 3.5.0(webpack@5.101.3) chalk: 3.0.0 - next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) resolve: 1.22.8 rollup: 4.35.0 stacktrace-parser: 0.1.11 @@ -19173,8 +19198,8 @@ snapshots: '@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.9.2) eslint: 8.10.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.10.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.10.0) eslint-plugin-react: 7.37.5(eslint@8.10.0) eslint-plugin-react-hooks: 5.2.0(eslint@8.10.0) @@ -19193,8 +19218,8 @@ snapshots: '@typescript-eslint/parser': 8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.34.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.34.0(jiti@2.5.1)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.34.0(jiti@2.5.1)) @@ -19219,22 +19244,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@8.10.0): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1 - eslint: 8.10.0 - get-tsconfig: 4.10.1 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.14 - unrs-resolver: 1.7.11 - optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0) - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.34.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -19245,62 +19255,48 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.7.11 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.10.0): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.1 + eslint: 8.10.0 + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.14 + unrs-resolver: 1.7.11 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.9.2) eslint: 8.10.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.10.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.34.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.34.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.34.0(jiti@2.5.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -19311,7 +19307,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.10.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.10.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -19329,6 +19325,35 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.34.0(jiti@2.5.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-jsx-a11y@6.10.2(eslint@8.10.0): dependencies: aria-query: 5.3.2 @@ -21269,31 +21294,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@next/env': 15.5.2 - '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001723 - postcss: 8.4.31 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - styled-jsx: 5.1.6(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react@19.1.0) - optionalDependencies: - '@next/swc-darwin-arm64': 15.5.2 - '@next/swc-darwin-x64': 15.5.2 - '@next/swc-linux-arm64-gnu': 15.5.2 - '@next/swc-linux-arm64-musl': 15.5.2 - '@next/swc-linux-x64-gnu': 15.5.2 - '@next/swc-linux-x64-musl': 15.5.2 - '@next/swc-win32-arm64-msvc': 15.5.2 - '@next/swc-win32-x64-msvc': 15.5.2 - '@opentelemetry/api': 1.9.0 - babel-plugin-react-compiler: 19.1.0-rc.2 - sharp: 0.34.3 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - no-case@3.0.4: dependencies: lower-case: 2.0.2 From 4560d2df1da9f6f2339e2bf141c99a0e97f420a6 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:17:08 +0300 Subject: [PATCH 17/27] feat(MED-161): use shared types --- .../medipostPrivateMessage.service.ts | 8 +- .../medipost/medipostValidate.service.ts | 4 +- lib/types/medipost.ts | 131 +---------------- .../shared/src/types/medipost-analysis.ts | 132 ++++++++++++++++++ 4 files changed, 140 insertions(+), 135 deletions(-) create mode 100644 packages/shared/src/types/medipost-analysis.ts diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index b6216b8..f8477f7 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -1,12 +1,14 @@ 'use server'; import { - AnalysisOrderStatus, GetMessageListResponse, MedipostAction, - MedipostOrderResponse, - ResponseUuringuGrupp, } from '@/lib/types/medipost'; +import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; +import type { + ResponseUuringuGrupp, + MedipostOrderResponse +} from '@/packages/shared/src/types/medipost-analysis'; import { toArray } from '@/lib/utils'; import axios from 'axios'; diff --git a/lib/services/medipost/medipostValidate.service.ts b/lib/services/medipost/medipostValidate.service.ts index f255c46..655673b 100644 --- a/lib/services/medipost/medipostValidate.service.ts +++ b/lib/services/medipost/medipostValidate.service.ts @@ -1,8 +1,8 @@ 'use server'; -import { +import type { IMedipostResponseXMLBase, -} from '@/lib/types/medipost'; +} from '@/packages/shared/src/types/medipost-analysis'; import { MedipostValidationError } from './MedipostValidationError'; import { parseXML } from '../util/xml.service'; diff --git a/lib/types/medipost.ts b/lib/types/medipost.ts index 4f7bc0f..b8228fb 100644 --- a/lib/types/medipost.ts +++ b/lib/types/medipost.ts @@ -1,11 +1,4 @@ -export interface IMedipostResponseXMLBase { - '?xml': { - '@_version': string; - '@_encoding': string; - '@_standalone': 'yes' | 'no'; - }; - ANSWER?: { CODE: number }; -} +import type { IMedipostResponseXMLBase } from "@/packages/shared/src/types/medipost-analysis"; export type Message = { messageId: string; @@ -143,125 +136,3 @@ export type MedipostPublicMessageResponse = IMedipostResponseXMLBase & { }; }; }; - -export type UuringuVastus = { - VastuseVaartus: string; // numeric or text like 'Negatiivne' - 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 = IMedipostResponseXMLBase & { - Saadetis: { - Pais: { - Pakett: { - '#text': string; - '@_versioon': string; - }; - Saatja: string; - Saaja: string; - Aeg: string; - SaadetisId: string; - Email: string; - }; - Vastus?: { - ValisTellimuseId: string; - 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[]; - }; - Tellimus?: { - ValisTellimuseId: string; - } - }; -}; - -export const AnalysisOrderStatus = { - 1: 'QUEUED', - 2: 'ON_HOLD', - 3: 'PROCESSING', - 4: 'COMPLETED', - 5: 'REJECTED', - 6: 'CANCELLED', -} as const; -export const NormStatus: Record = { - 0: 'NORMAL', - 1: 'WARNING', - 2: 'REQUIRES_ATTENTION', -} as const; diff --git a/packages/shared/src/types/medipost-analysis.ts b/packages/shared/src/types/medipost-analysis.ts new file mode 100644 index 0000000..c50e402 --- /dev/null +++ b/packages/shared/src/types/medipost-analysis.ts @@ -0,0 +1,132 @@ +export const NormStatus: Record = { + 0: 'NORMAL', + 1: 'WARNING', + 2: 'REQUIRES_ATTENTION', +} as const; + +export const AnalysisOrderStatus = { + 1: 'QUEUED', + 2: 'ON_HOLD', + 3: 'PROCESSING', + 4: 'COMPLETED', + 5: 'REJECTED', + 6: 'CANCELLED', +} as const; + +export type UuringuVastus = { + VastuseVaartus: string; // numeric or text like 'Negatiivne' + VastuseAeg: string; + NormYlem?: { + '#text': number; + '@_kaasaarvatud': string; + }; // 0..1 + NormAlum?: { + '#text': number; + '@_kaasaarvatud': string; + }; + NormiStaatus: keyof typeof NormStatus; + ProoviJarjenumber: number; +}; + +export type UuringElement = { + 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; +} + +export type ResponseUuring = { + UuringuElement: UuringElement; + UuringuTaitjaAsutuseJnr: number; +}; + +export type ResponseUuringuGrupp = { + UuringuGruppId: string; + UuringuGruppNimi: string; + Uuring: ResponseUuring | ResponseUuring[]; // 1..n +}; + +export interface IMedipostResponseXMLBase { + '?xml': { + '@_version': string; + '@_encoding': string; + '@_standalone': 'yes' | 'no'; + }; + ANSWER?: { CODE: number }; +} + +export type MedipostOrderResponse = IMedipostResponseXMLBase & { + Saadetis: { + Pais: { + Pakett: { + '#text': string; + '@_versioon': string; + }; + Saatja: string; + Saaja: string; + Aeg: string; + SaadetisId: string; + Email: string; + }; + Vastus?: { + ValisTellimuseId: string; + 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[]; + }; + Tellimus?: { + ValisTellimuseId: string; + } + }; +}; From b729ad9ffacac38ad4b76af262498212e43c5fb9 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:17:15 +0300 Subject: [PATCH 18/27] feat(MED-161): add feature flag for delete private messages --- .../medipostPrivateMessage.service.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index f8477f7..2d05d03 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -31,6 +31,8 @@ const USER = process.env.MEDIPOST_USER!; const PASSWORD = process.env.MEDIPOST_PASSWORD!; const RECIPIENT = process.env.MEDIPOST_RECIPIENT!; +const IS_ENABLED_DELETE_PRIVATE_MESSAGE = false as boolean; + export async function getLatestPrivateMessageListItem({ excludedMessageIds, }: { @@ -169,7 +171,14 @@ export async function readPrivateMessageResponse({ excludedMessageIds, }: { excludedMessageIds: string[]; -}): Promise<{ messageId: string | null; hasAnalysisResponse: boolean; hasPartialAnalysisResponse: boolean; hasFullAnalysisResponse: boolean; medusaOrderId: string | undefined; analysisOrderId: number | undefined }> { +}): Promise<{ + messageId: string | null; + hasAnalysisResponse: boolean; + hasPartialAnalysisResponse: boolean; + hasFullAnalysisResponse: boolean; + medusaOrderId: string | undefined; + analysisOrderId: number | undefined; +}> { let messageId: string | null = null; let hasAnalysisResponse = false; let hasPartialAnalysisResponse = false; @@ -224,7 +233,9 @@ export async function readPrivateMessageResponse({ try { order = await getAnalysisOrder({ medusaOrderId }); } catch (e) { - await deletePrivateMessage(privateMessage.messageId); + if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) { + await deletePrivateMessage(privateMessage.messageId); + } throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); } @@ -236,7 +247,9 @@ export async function readPrivateMessageResponse({ hasPartialAnalysisResponse = true; } else if (status.isCompleted) { await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); - await deletePrivateMessage(privateMessage.messageId); + if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) { + await deletePrivateMessage(privateMessage.messageId); + } hasAnalysisResponse = true; hasFullAnalysisResponse = true; } From ecc8c2b982033e8dc7b59fd7c2f07b8806eb6838 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:17:23 +0300 Subject: [PATCH 19/27] feat(MED-161): update results sync validations --- .../medipost/medipostMessageBase.service.ts | 71 ++++++++++--------- .../medipostPrivateMessage.service.ts | 46 +++++++++--- 2 files changed, 75 insertions(+), 42 deletions(-) diff --git a/lib/services/medipost/medipostMessageBase.service.ts b/lib/services/medipost/medipostMessageBase.service.ts index 07aeddc..5faf012 100644 --- a/lib/services/medipost/medipostMessageBase.service.ts +++ b/lib/services/medipost/medipostMessageBase.service.ts @@ -28,36 +28,41 @@ export function getLatestMessage({ } export async function createMedipostActionLog({ - action, - xml, - hasAnalysisResults = false, - medusaOrderId, - responseXml, - hasError = false, - }: { - action: - | '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; - responseXml?: string | null; - hasError?: boolean; - }) { - await getSupabaseServerAdminClient() - .schema('medreport') - .from('medipost_actions') - .insert({ - action, - xml, - has_analysis_results: hasAnalysisResults, - medusa_order_id: medusaOrderId, - response_xml: responseXml, - has_error: hasError, - }) - .select('id') - .throwOnError(); - } - \ No newline at end of file + action, + xml, + hasAnalysisResults = false, + medusaOrderId, + responseXml, + hasError = false, + medipostExternalOrderId, + medipostPrivateMessageId, +}: { + action: + | '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; + responseXml?: string | null; + hasError?: boolean; + medipostExternalOrderId?: string | null; + medipostPrivateMessageId?: string | null; +}) { + await getSupabaseServerAdminClient() + .schema('medreport') + .from('medipost_actions') + .insert({ + action, + xml, + has_analysis_results: hasAnalysisResults, + medusa_order_id: medusaOrderId, + response_xml: responseXml, + has_error: hasError, + medipost_external_order_id: medipostExternalOrderId, + medipost_private_message_id: medipostPrivateMessageId, + }) + .select('id') + .throwOnError(); +} diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index 2d05d03..d667b8e 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -1,5 +1,8 @@ 'use server'; +import type { PostgrestError } from '@supabase/supabase-js'; +import axios from 'axios'; + import { GetMessageListResponse, MedipostAction, @@ -10,7 +13,7 @@ import type { MedipostOrderResponse } from '@/packages/shared/src/types/medipost-analysis'; import { toArray } from '@/lib/utils'; -import axios from 'axios'; +import type { AnalysisOrder } from '~/lib/types/analysis-order'; import { Tables } from '@kit/supabase/database'; import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; @@ -201,20 +204,27 @@ export async function readPrivateMessageResponse({ }; } + const { messageId: privateMessageId } = privateMessage; const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage( - privateMessage.messageId, + privateMessageId, ); const messageResponse = privateMessageContent?.Saadetis?.Vastus; - analysisOrderId = Number(privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId); + const medipostExternalOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId; + const patientPersonalCode = messageResponse?.Patsient.Isikukood?.toString(); + analysisOrderId = Number(medipostExternalOrderId); - const hasInvalidOrderId = isNaN(analysisOrderId) + const hasInvalidOrderId = isNaN(analysisOrderId); - if (hasInvalidOrderId || !messageResponse) { + if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) { await createMedipostActionLog({ action: 'sync_analysis_results_from_medipost', xml: privateMessageXml, hasAnalysisResults: false, + medipostPrivateMessageId: privateMessageId, + medusaOrderId, + medipostExternalOrderId, + hasError: true, }); return { messageId, @@ -226,21 +236,39 @@ export async function readPrivateMessageResponse({ }; } - const analysisOrder = await getAnalysisOrder({ analysisOrderId: analysisOrderId }) - medusaOrderId = analysisOrder.medusa_order_id; + let analysisOrder: AnalysisOrder; + try { + analysisOrder = await getAnalysisOrder({ analysisOrderId }) + medusaOrderId = analysisOrder.medusa_order_id; + } catch (e) { + throw new Error(`No analysis order found for Medipost message ValisTellimuseId=${medipostExternalOrderId}`); + } + + const orderPerson = await getAccountAdmin({ primaryOwnerUserId: analysisOrder.user_id }); + if (orderPerson.personal_code !== patientPersonalCode) { + throw new Error(`Order person personal code does not match Medipost message Patsient.Isikukood=${patientPersonalCode}, orderPerson.personal_code=${orderPerson.personal_code}`); + } let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; try { order = await getAnalysisOrder({ medusaOrderId }); } catch (e) { if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) { - await deletePrivateMessage(privateMessage.messageId); + await deletePrivateMessage(privateMessageId); } throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); } const status = await syncPrivateMessage({ messageResponse, order }); + await createMedipostActionLog({ + action: 'sync_analysis_results_from_medipost', + xml: privateMessageXml, + hasAnalysisResults: true, + medipostPrivateMessageId: privateMessageId, + medusaOrderId, + medipostExternalOrderId, + }); if (status.isPartial) { await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); hasAnalysisResponse = true; @@ -248,7 +276,7 @@ export async function readPrivateMessageResponse({ } else if (status.isCompleted) { await updateAnalysisOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) { - await deletePrivateMessage(privateMessage.messageId); + await deletePrivateMessage(privateMessageId); } hasAnalysisResponse = true; hasFullAnalysisResponse = true; From 2019c2c1fce1b2b2caa59e5ee9bf42a1758752df Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:17:30 +0300 Subject: [PATCH 20/27] feat(MED-161): update sync private message --- lib/services/analysis-order.service.ts | 38 +++ .../medipostPrivateMessage.service.ts | 236 +++++++++++------- 2 files changed, 184 insertions(+), 90 deletions(-) diff --git a/lib/services/analysis-order.service.ts b/lib/services/analysis-order.service.ts index e76ca16..2d0f77e 100644 --- a/lib/services/analysis-order.service.ts +++ b/lib/services/analysis-order.service.ts @@ -1,5 +1,6 @@ import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client"; import type { AnalysisResponseElement } from "../types/analysis-response-element"; +import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; export async function getExistingAnalysisResponseElements({ analysisResponseId, @@ -15,3 +16,40 @@ export async function getExistingAnalysisResponseElements({ return data as AnalysisResponseElement[]; } + +export async function upsertAnalysisResponse({ + analysisOrderId, + orderNumber, + orderStatus, + userId, +}: { + analysisOrderId: number; + orderNumber: string; + orderStatus: typeof AnalysisOrderStatus[keyof typeof AnalysisOrderStatus]; + userId: string; +}) { + const { data: analysisResponse } = await getSupabaseServerAdminClient() + .schema('medreport') + .from('analysis_responses') + .upsert( + { + analysis_order_id: analysisOrderId, + order_number: orderNumber, + order_status: orderStatus, + user_id: userId, + }, + { onConflict: 'order_number', ignoreDuplicates: false }, + ) + .select('id') + .throwOnError(); + + + const analysisResponseId = analysisResponse?.[0]?.id; + if (!analysisResponseId) { + throw new Error( + `Failed to insert or update analysis order response (order id: ${analysisOrderId}, order number: ${orderNumber})`, + ); + } + + return { analysisResponseId }; +} diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index d667b8e..ba19fc2 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -10,10 +10,12 @@ import { import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; import type { ResponseUuringuGrupp, - MedipostOrderResponse + MedipostOrderResponse, + ResponseUuring, } from '@/packages/shared/src/types/medipost-analysis'; import { toArray } from '@/lib/utils'; import type { AnalysisOrder } from '~/lib/types/analysis-order'; +import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element'; import { Tables } from '@kit/supabase/database'; import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; @@ -27,7 +29,7 @@ import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service'; import { getAccountAdmin } from '../account.service'; import { logMedipostDispatch } from '../audit.service'; import { MedipostValidationError } from './MedipostValidationError'; -import { getExistingAnalysisResponseElements } from '../analysis-order.service'; +import { getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -56,6 +58,111 @@ export async function getLatestPrivateMessageListItem({ return getLatestMessage({ messages: data?.messages, excludedMessageIds }); } +const logger = (analysisOrder: AnalysisOrder, externalId: string, analysisResponseId: string) => (message: string, error?: PostgrestError | null) => { + const messageFormatted = `[${analysisOrder.id}] [${externalId}] [${analysisResponseId}] ${message}`; + if (error) { + console.info(messageFormatted, error); + } else { + console.info(messageFormatted); + } +}; + +function canCreateAnalysisResponseElement({ + existingElements, + groupUuring: { + UuringuElement: { + UuringOlek: status, + UuringId: analysisElementOriginalId, + }, + }, + responseValue, + log, +}: { + existingElements: AnalysisResponseElement[]; + groupUuring: ResponseUuring; + responseValue: number | null; + log: ReturnType; +}) { + const existingAnalysisResponseElement = existingElements.find(({ analysis_element_original_id }) => analysis_element_original_id === analysisElementOriginalId); + if (!existingAnalysisResponseElement) { + return true; + } + + if (Number(existingAnalysisResponseElement.status) > status) { + log(`Analysis response element id=${analysisElementOriginalId} already exists for order in higher status ${existingAnalysisResponseElement.status} than ${status}`); + return false; + } + + if (existingAnalysisResponseElement.response_value && !responseValue) { + log(`Analysis response element id=${analysisElementOriginalId} already exists for order with response value ${existingAnalysisResponseElement.response_value} but new response has no value`); + return false; + } + + return true; +} + + +async function getAnalysisResponseElementsForGroup({ + analysisResponseId, + analysisGroup, + log, +}: { + analysisResponseId: number; + analysisGroup: ResponseUuringuGrupp; + log: ReturnType; +}) { + const groupUuringItems = toArray(analysisGroup.Uuring as ResponseUuringuGrupp['Uuring']); + log(`Order has results in group '${analysisGroup.UuringuGruppNimi}' for ${groupUuringItems.length} analysis elements`); + + const existingElements = await getExistingAnalysisResponseElements({ analysisResponseId }); + + const results: Omit[] = []; + + for (const groupUuring of groupUuringItems) { + const groupUuringElement = groupUuring.UuringuElement; + const elementAnalysisResponses = toArray(groupUuringElement.UuringuVastus); + + const status = groupUuringElement.UuringOlek; + log(`Group uuring '${analysisGroup.UuringuGruppNimi}' has status ${status}`); + + for (const response of elementAnalysisResponses) { + const analysisElementOriginalId = groupUuringElement.UuringId; + const responseValue = (() => { + const valueAsNumber = Number(response.VastuseVaartus); + if (isNaN(valueAsNumber)) { + return null; + } + return valueAsNumber; + })(); + + if (!canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })) { + continue; + } + + results.push({ + analysis_element_original_id: analysisElementOriginalId, + analysis_response_id: analysisResponseId, + 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: responseValue, + unit: groupUuringElement.Mootyhik ?? null, + original_response_element: groupUuringElement, + analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus, + comment: groupUuringElement.UuringuKommentaar ?? null, + status: status.toString(), + }); + } + } + + return results; +} + export async function syncPrivateMessage({ messageResponse, order, @@ -63,111 +170,60 @@ export async function syncPrivateMessage({ messageResponse: NonNullable; order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; }) { - const supabase = getSupabaseServerAdminClient() + const supabase = getSupabaseServerAdminClient(); - const { data: analysisOrder, error: analysisOrderError } = await supabase + const externalId = messageResponse.ValisTellimuseId; + const orderNumber = messageResponse.TellimuseNumber; + const orderStatus = AnalysisOrderStatus[messageResponse.TellimuseOlek]; + + const log = logger(order, externalId, orderNumber); + + const { data: analysisOrder } = await supabase .schema('medreport') .from('analysis_orders') - .select('user_id') - .eq('id', order.id); + .select('id, user_id') + .eq('id', order.id) + .single() + .throwOnError(); - if (analysisOrderError || !analysisOrder?.[0]?.user_id) { - throw new Error( - `Could not find analysis order with id ${messageResponse.ValisTellimuseId}`, - ); - } + const { analysisResponseId } = await upsertAnalysisResponse({ + analysisOrderId: order.id, + orderNumber, + orderStatus, + userId: analysisOrder.user_id, + }); - const { data: analysisResponse, error } = await supabase - .schema('medreport') - .from('analysis_responses') - .upsert( - { - analysis_order_id: order.id, - order_number: messageResponse.TellimuseNumber, - order_status: AnalysisOrderStatus[messageResponse.TellimuseOlek], - 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); - console.info(`Order has results for ${analysisGroups.length} analysis groups`); - const responses: Omit< - Tables<{ schema: 'medreport' }, 'analysis_response_elements'>, - 'id' | 'created_at' | 'updated_at' - >[] = []; - - const analysisResponseId = analysisResponse[0]!.id; + log(`Order has results for ${analysisGroups.length} analysis groups`); for (const analysisGroup of analysisGroups) { - const groupItems = toArray( - analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'], - ); - console.info(`Order has results in group ${analysisGroup.UuringuGruppNimi} for ${groupItems.length} analysis elements`); - for (const item of groupItems) { - const element = item.UuringuElement; - const elementAnalysisResponses = toArray(element.UuringuVastus); + log(`[${analysisGroups.indexOf(analysisGroup) + 1}/${analysisGroups.length}] Syncing analysis group '${analysisGroup.UuringuGruppNimi}'`); - responses.push( - ...elementAnalysisResponses.map((response) => ({ - analysis_element_original_id: element.UuringId, - analysis_response_id: analysisResponseId, - 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, - comment: element.UuringuKommentaar ?? '', - })), - ); + const elements = await getAnalysisResponseElementsForGroup({ + analysisResponseId, + analysisGroup, + log, + }); + + for (const element of elements) { + const { error } = await supabase + .schema('medreport') + .from('analysis_response_elements') + .insert(element); + if (error) { + log(`Failed to insert order response elements for response id ${analysisResponseId} (order id: ${analysisOrder.id})`, error); + } } } - const { error: deleteError } = await supabase - .schema('medreport') - .from('analysis_response_elements') - .delete() - .eq('analysis_response_id', analysisResponseId); - - if (deleteError) { - throw new Error( - `Failed to clean up response elements for response id ${analysisResponseId}`, - ); - } - - 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 ${analysisResponseId}`, - ); - } - - const existingAnalysisResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId }); + const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId }); const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; - if (existingAnalysisResponseElements.length !== expectedOrderResponseElements) { + if (allOrderResponseElements.length !== expectedOrderResponseElements) { return { isPartial: true }; } - const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek]; - return { isCompleted: statusFromResponse === 'COMPLETED' }; + return { isCompleted: orderStatus === 'COMPLETED' }; } export async function readPrivateMessageResponse({ From bfadf56173f29acc2adc7f18785909541181e26a Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:17:36 +0300 Subject: [PATCH 21/27] feat(MED-161): update analysis results view --- app/doctor/_components/analysis-level-bar.tsx | 4 +- .../_components/doctor-analysis-wrapper.tsx | 3 +- .../analysis-results/[id]/page.tsx | 16 +- .../_components/analysis-level-bar.tsx | 210 ++++++++++++------ .../analysis-results/_components/analysis.tsx | 160 ++++++------- .../(user)/_lib/server/load-user-analysis.ts | 8 +- lib/services/analysis-order.service.ts | 17 +- lib/services/medipost/medipost.types.ts | 2 +- .../medipost/medipostMessageBase.service.ts | 2 +- .../medipostPrivateMessage.service.ts | 143 +++++++----- .../medipost/medipostPublicMessage.service.ts | 2 +- .../medipost/medipostValidate.service.ts | 2 +- lib/services/util/xml.service.ts | 2 +- .../accounts/src/types/analysis-results.ts | 1 + .../features/user-analyses/src/server/api.ts | 12 +- .../src/types/analysis-results.ts | 2 + packages/supabase/src/database.types.ts | 3 + public/locales/en/analysis-results.json | 7 + public/locales/et/analysis-results.json | 7 + public/locales/ru/analysis-results.json | 7 + ...analysis_response_element_within_range.sql | 2 + 21 files changed, 384 insertions(+), 228 deletions(-) create mode 100644 supabase/migrations/20250917073453_analysis_response_element_within_range.sql diff --git a/app/doctor/_components/analysis-level-bar.tsx b/app/doctor/_components/analysis-level-bar.tsx index 7bac5f4..e9080db 100644 --- a/app/doctor/_components/analysis-level-bar.tsx +++ b/app/doctor/_components/analysis-level-bar.tsx @@ -48,7 +48,7 @@ const Level = ({ export const AnalysisLevelBarSkeleton = () => { return ( -
+
); @@ -95,7 +95,7 @@ const AnalysisLevelBar = ({ const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh; return ( -
+
{normLowerIncluded && ( <>
- diff --git a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx index c69d0b6..386d584 100644 --- a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx @@ -52,6 +52,8 @@ export default async function AnalysisResultsPage({ ); } + const orderedAnalysisElements = analysisResponse.orderedAnalysisElements; + return ( <> @@ -80,7 +82,7 @@ export default async function AnalysisResultsPage({

@@ -88,7 +90,7 @@ export default async function AnalysisResultsPage({ i18nKey={`orders:status.${analysisResponse.order.status}`} />
@@ -102,13 +104,9 @@ export default async function AnalysisResultsPage({
)}
- {analysisResponse.elements ? ( - analysisResponse.elements.map((element, index) => ( - + {orderedAnalysisElements ? ( + orderedAnalysisElements.map((element, index) => ( + )) ) : (
diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx index 71a7036..dadc42b 100644 --- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx @@ -3,14 +3,12 @@ import { useMemo } from 'react'; import { ArrowDown } from 'lucide-react'; import { cn } from '@kit/ui/utils'; -import { AnalysisResultForDisplay } from './analysis'; +import { AnalysisResultDetailsElementResults } from '@/packages/features/accounts/src/types/analysis-results'; export enum AnalysisResultLevel { - VERY_LOW = 0, - LOW = 1, - NORMAL = 2, - HIGH = 3, - VERY_HIGH = 4, + NORMAL = 0, + WARNING = 1, + CRITICAL = 2, } const Level = ({ @@ -19,17 +17,19 @@ const Level = ({ isFirst = false, isLast = false, arrowLocation, + normRangeText, }: { isActive?: boolean; color: 'destructive' | 'success' | 'warning' | 'gray-200'; isFirst?: boolean; isLast?: boolean; arrowLocation?: number; + normRangeText?: string | null; }) => { return (
92.5 && { left: '92.5%' }), + ...(arrowLocation < 7.5 && { left: '7.5%' }), + } + } : {})} >
)} + + {color === 'success' && typeof normRangeText === 'string' && ( +

+ {normRangeText} +

+ )}
); }; export const AnalysisLevelBarSkeleton = () => { return ( -
+
); }; const AnalysisLevelBar = ({ - normLowerIncluded = true, - normUpperIncluded = true, level, results, + normRangeText, }: { - normLowerIncluded?: boolean; - normUpperIncluded?: boolean; level: AnalysisResultLevel; - results: AnalysisResultForDisplay; + results: AnalysisResultDetailsElementResults; + normRangeText: string | null; }) => { - const { norm_lower: lower, norm_upper: upper, response_value: value } = results; + const { normLower: lower, normUpper: upper, responseValue: value, normStatus } = results; + const normLowerIncluded = results?.normLowerIncluded || false; + const normUpperIncluded = results?.normUpperIncluded || false; + + // Calculate arrow position based on value within normal range const arrowLocation = useMemo(() => { - if (value < lower!) { - return 0; - } - - if (normLowerIncluded || normUpperIncluded) { + // If no response value, center the arrow + if (value === null || value === undefined) { return 50; } - const calculated = ((value - lower!) / (upper! - lower!)) * 100; - - if (calculated > 100) { - return 100; + // If no normal ranges defined, center the arrow + if (lower === null && upper === null) { + return 50; } - - return calculated; + + // If only upper bound exists + if (lower === null && upper !== null) { + if (value <= upper) { + return Math.min(75, (value / upper) * 75); // Show in left 75% of normal range + } + return 100; // Beyond upper bound + } + + // If only lower bound exists + if (upper === null && lower !== null) { + if (value >= lower) { + // Value is in normal range (above lower bound) + // Position proportionally in the normal range section + const normalizedPosition = Math.min((value - lower) / (lower * 0.5), 1); // Use 50% of lower as scale + return normalizedPosition * 100; + } + // Value is below lower bound - position in the "below normal" section + const belowPosition = Math.max(0, Math.min(1, value / lower)); + return belowPosition * 100; + } + + // Both bounds exist + if (lower !== null && upper !== null) { + if (value < lower) { + return 0; // Below normal range + } + if (value > upper) { + return 100; // Above normal range + } + // Within normal range + return ((value - lower) / (upper - lower)) * 100; + } + + return 50; // Fallback }, [value, upper, lower]); - const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(() => [ - level === AnalysisResultLevel.VERY_LOW, - level === AnalysisResultLevel.LOW, - level === AnalysisResultLevel.HIGH, - level === AnalysisResultLevel.VERY_HIGH, - ], [level, value, upper, lower]); + // Determine level states based on normStatus + const isNormal = level === AnalysisResultLevel.NORMAL; + const isWarning = level === AnalysisResultLevel.WARNING; + const isCritical = level === AnalysisResultLevel.CRITICAL; + const isPending = level === null; - const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh; + // If pending results, show gray bar + if (isPending) { + return ( +
+ +
+ ); + } + + // Show appropriate levels based on available norm bounds + const hasLowerBound = lower !== null; + const isLowerBoundZero = hasLowerBound && lower === 0; + console.info('isLowerBoundZero', results.analysisElementOriginalId, { isLowerBoundZero, hasLowerBound, lower }); + const hasUpperBound = upper !== null; + + // Determine which section the value falls into + const isValueBelowLower = hasLowerBound && value !== null && value < lower!; + const isValueAboveUpper = hasUpperBound && value !== null && value > upper!; + const isValueInNormalRange = !isValueBelowLower && !isValueAboveUpper; + + const [first, second, third] = useMemo(() => { + if (!hasLowerBound) { + return [ + { + isActive: isNormal, + color: "success", + isFirst: true, + normRangeText, + ...(isNormal ? { arrowLocation } : {}), + }, + { + isActive: isWarning, + color: "warning", + ...(isWarning ? { arrowLocation } : {}), + }, + { + isActive: isCritical, + color: "destructive", + isLast: true, + ...(isCritical ? { arrowLocation } : {}), + }, + ] as const; + } + + return [ + { + isActive: isWarning, + color: "warning", + isFirst: true, + ...(isWarning ? { arrowLocation } : {}), + }, + { + isActive: isNormal, + color: "success", + normRangeText, + ...(isNormal ? { arrowLocation } : {}), + }, + { + isActive: isCritical, + color: "destructive", + isLast: true, + ...(isCritical ? { arrowLocation } : {}), + }, + ] as const; + }, [isValueBelowLower, isValueAboveUpper, isValueInNormalRange, arrowLocation, normRangeText, isNormal, isWarning, isCritical]); return ( -
- {normLowerIncluded && ( - <> - - - - )} - - - - {normUpperIncluded && ( - <> - - - - )} +
+ + +
); }; diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx index c88e9c3..d19d0d1 100644 --- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx @@ -1,33 +1,19 @@ 'use client'; -import React, { ReactElement, ReactNode, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; -import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts'; +import { AnalysisResultDetailsElement } from '@/packages/features/accounts/src/types/analysis-results'; import { format } from 'date-fns'; import { Info } from 'lucide-react'; import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; -import { AnalysisElement } from '~/lib/services/analysis-element.service'; - import AnalysisLevelBar, { - AnalysisLevelBarSkeleton, AnalysisResultLevel, } from './analysis-level-bar'; -export type AnalysisResultForDisplay = Pick< - UserAnalysisElement, - | 'norm_status' - | 'response_value' - | 'unit' - | 'norm_lower_included' - | 'norm_upper_included' - | 'norm_lower' - | 'norm_upper' - | 'response_time' ->; - export enum AnalysisStatus { NORMAL = 0, MEDIUM = 1, @@ -35,26 +21,45 @@ export enum AnalysisStatus { } const Analysis = ({ - analysisElement, - results, - startIcon, - endIcon, - isCancelled, + element, }: { - analysisElement: Pick; - results?: AnalysisResultForDisplay; - isCancelled?: boolean; - startIcon?: ReactElement | null; - endIcon?: ReactNode | null; + element: AnalysisResultDetailsElement; }) => { - const name = analysisElement.analysis_name_lab || ''; - const status = results?.norm_status || AnalysisStatus.NORMAL; - const value = results?.response_value || 0; + const { t } = useTranslation(); + + const name = element.analysisName || ''; + const results = element.results; + + const hasIsWithinNorm = results?.responseValueIsWithinNorm !== null; + const hasIsNegative = results?.responseValueIsNegative !== null; + + const value = (() => { + if (!results) { + return null; + } + + const { responseValue, responseValueIsNegative, responseValueIsWithinNorm } = results; + if (responseValue === null || responseValue === undefined) { + if (hasIsNegative) { + if (responseValueIsNegative) { + return t('analysis-results:results.value.negative'); + } + return t('analysis-results:results.value.positive'); + } + if (hasIsWithinNorm) { + if (responseValueIsWithinNorm) { + return t('analysis-results:results.value.isWithinNorm'); + } + return t('analysis-results:results.value.isNotWithinNorm'); + } + return null; + } + + return responseValue; + })(); const unit = results?.unit || ''; - const normLowerIncluded = results?.norm_lower_included || false; - const normUpperIncluded = results?.norm_upper_included || false; - const normLower = results?.norm_lower || 0; - const normUpper = results?.norm_upper || 0; + const normLower = results?.normLower; + const normUpper = results?.normUpper; const [showTooltip, setShowTooltip] = useState(false); const analysisResultLevel = useMemo(() => { @@ -62,32 +67,34 @@ const Analysis = ({ return null; } - const isUnderNorm = value < normLower; - if (isUnderNorm) { - switch (status) { - case AnalysisStatus.MEDIUM: - return AnalysisResultLevel.LOW; - default: - return AnalysisResultLevel.VERY_LOW; - } + if (results.responseValue === null || results.responseValue === undefined) { + return null; } - switch (status) { - case AnalysisStatus.MEDIUM: - return AnalysisResultLevel.HIGH; - case AnalysisStatus.HIGH: - return AnalysisResultLevel.VERY_HIGH; + + const normStatus = results.normStatus; + + switch (normStatus) { + case 1: + return AnalysisResultLevel.WARNING; + case 2: + return AnalysisResultLevel.CRITICAL; + case 0: default: return AnalysisResultLevel.NORMAL; } - }, [results, value, normLower]); + }, [results]); + + const isCancelled = Number(results?.status) === 5; + const hasNestedElements = results?.nestedElements.length > 0; + + const normRangeText = normLower !== null ? `${normLower} - ${normUpper || ''}` : null; return (
-
+
- {startIcon ||
} {name} - {results?.response_time && ( + {results?.responseTime && (
{ @@ -105,42 +112,41 @@ const Analysis = ({ > {': '} - {format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')} + {format(new Date(results.responseTime), 'dd.MM.yyyy HH:mm')}
)}
- {results ? ( + + {isCancelled && ( +
+ +
+ )} + + {isCancelled || !results || hasNestedElements ? null : ( <>
{value}
{unit}
-
- {normLower} - {normUpper} -
- -
-
- - {endIcon ||
} + {!(hasIsNegative || hasIsWithinNorm) && ( + <> +
+ {normRangeText} +
+ +
+
+ + + )} - ) : (isCancelled ? null : ( - <> -
-
- -
-
-
- - - ))} + )}
); diff --git a/app/home/(user)/_lib/server/load-user-analysis.ts b/app/home/(user)/_lib/server/load-user-analysis.ts index 09efd46..77c40bd 100644 --- a/app/home/(user)/_lib/server/load-user-analysis.ts +++ b/app/home/(user)/_lib/server/load-user-analysis.ts @@ -1,8 +1,8 @@ import { cache } from 'react'; -import { createAccountsApi } from '@kit/accounts/api'; -import { AnalysisResultDetails } from '@kit/accounts/types/accounts'; +import { AnalysisResultDetailsMapped } from '@kit/accounts/types/analysis-results'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; export type UserAnalyses = Awaited>; @@ -15,9 +15,9 @@ export const loadUserAnalysis = cache(analysisLoader); async function analysisLoader( analysisOrderId: number, -): Promise { +): Promise { const client = getSupabaseServerClient(); - const api = createAccountsApi(client); + const api = createUserAnalysesApi(client); return api.getUserAnalysis(analysisOrderId); } diff --git a/lib/services/analysis-order.service.ts b/lib/services/analysis-order.service.ts index 2d0f77e..cd57b9a 100644 --- a/lib/services/analysis-order.service.ts +++ b/lib/services/analysis-order.service.ts @@ -1,12 +1,13 @@ import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client"; -import type { AnalysisResponseElement } from "../types/analysis-response-element"; import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; +import type { AnalysisResponseElement } from "../types/analysis-response-element"; + export async function getExistingAnalysisResponseElements({ analysisResponseId, }: { analysisResponseId: number; -}) { +}): Promise { const { data } = await getSupabaseServerAdminClient() .schema('medreport') .from('analysis_response_elements') @@ -17,6 +18,18 @@ export async function getExistingAnalysisResponseElements({ return data as AnalysisResponseElement[]; } +export async function createAnalysisResponseElement({ + element, +}: { + element: Omit; +}) { + await getSupabaseServerAdminClient() + .schema('medreport') + .from('analysis_response_elements') + .insert(element) + .throwOnError(); +} + export async function upsertAnalysisResponse({ analysisOrderId, orderNumber, diff --git a/lib/services/medipost/medipost.types.ts b/lib/services/medipost/medipost.types.ts index 3f14de8..ed2cbca 100644 --- a/lib/services/medipost/medipost.types.ts +++ b/lib/services/medipost/medipost.types.ts @@ -68,7 +68,7 @@ export interface IMedipostPublicMessageDataParsed { Koefitsient: number; Hind: number; }[]; - UuringuElement: IUuringElement; + UuringuElement?: IUuringElement[]; }[]; MaterjalideGrupp: IMaterialGroup[]; Kood: { diff --git a/lib/services/medipost/medipostMessageBase.service.ts b/lib/services/medipost/medipostMessageBase.service.ts index 5faf012..ef452ec 100644 --- a/lib/services/medipost/medipostMessageBase.service.ts +++ b/lib/services/medipost/medipostMessageBase.service.ts @@ -4,7 +4,7 @@ import type { Message } from '@/lib/types/medipost'; import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; -export function getLatestMessage({ +export async function getLatestMessage({ messages, excludedMessageIds, }: { diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index ba19fc2..f6356e3 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -11,7 +11,7 @@ import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analys import type { ResponseUuringuGrupp, MedipostOrderResponse, - ResponseUuring, + UuringElement, } from '@/packages/shared/src/types/medipost-analysis'; import { toArray } from '@/lib/utils'; import type { AnalysisOrder } from '~/lib/types/analysis-order'; @@ -29,7 +29,7 @@ import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service'; import { getAccountAdmin } from '../account.service'; import { logMedipostDispatch } from '../audit.service'; import { MedipostValidationError } from './MedipostValidationError'; -import { getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service'; +import { createAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -55,7 +55,7 @@ export async function getLatestPrivateMessageListItem({ throw new Error('Failed to get private message list'); } - return getLatestMessage({ messages: data?.messages, excludedMessageIds }); + return await getLatestMessage({ messages: data?.messages, excludedMessageIds }); } const logger = (analysisOrder: AnalysisOrder, externalId: string, analysisResponseId: string) => (message: string, error?: PostgrestError | null) => { @@ -67,7 +67,7 @@ const logger = (analysisOrder: AnalysisOrder, externalId: string, analysisRespon } }; -function canCreateAnalysisResponseElement({ +export async function canCreateAnalysisResponseElement({ existingElements, groupUuring: { UuringuElement: { @@ -78,8 +78,8 @@ function canCreateAnalysisResponseElement({ responseValue, log, }: { - existingElements: AnalysisResponseElement[]; - groupUuring: ResponseUuring; + existingElements: Pick[]; + groupUuring: { UuringuElement: Pick }; responseValue: number | null; log: ReturnType; }) { @@ -102,21 +102,19 @@ function canCreateAnalysisResponseElement({ } -async function getAnalysisResponseElementsForGroup({ - analysisResponseId, +export async function getAnalysisResponseElementsForGroup({ analysisGroup, + existingElements, log, }: { - analysisResponseId: number; - analysisGroup: ResponseUuringuGrupp; + analysisGroup: Pick; + existingElements: Pick[]; log: ReturnType; }) { const groupUuringItems = toArray(analysisGroup.Uuring as ResponseUuringuGrupp['Uuring']); log(`Order has results in group '${analysisGroup.UuringuGruppNimi}' for ${groupUuringItems.length} analysis elements`); - const existingElements = await getExistingAnalysisResponseElements({ analysisResponseId }); - - const results: Omit[] = []; + const results: Omit[] = []; for (const groupUuring of groupUuringItems) { const groupUuringElement = groupUuring.UuringuElement; @@ -127,21 +125,25 @@ async function getAnalysisResponseElementsForGroup({ for (const response of elementAnalysisResponses) { const analysisElementOriginalId = groupUuringElement.UuringId; + const vastuseVaartus = response.VastuseVaartus; const responseValue = (() => { - const valueAsNumber = Number(response.VastuseVaartus); + const valueAsNumber = Number(vastuseVaartus); if (isNaN(valueAsNumber)) { return null; } return valueAsNumber; })(); - if (!canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })) { + if (!await canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })) { continue; } + const responseValueIsNumeric = responseValue !== null; + const responseValueIsNegative = vastuseVaartus === 'Negatiivne'; + const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires'; + results.push({ analysis_element_original_id: analysisElementOriginalId, - analysis_response_id: analysisResponseId, norm_lower: response.NormAlum?.['#text'] ?? null, norm_lower_included: response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah', @@ -156,6 +158,8 @@ async function getAnalysisResponseElementsForGroup({ analysis_name: groupUuringElement.UuringNimi || groupUuringElement.KNimetus, comment: groupUuringElement.UuringuKommentaar ?? null, status: status.toString(), + response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm, + response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative, }); } } @@ -163,18 +167,55 @@ async function getAnalysisResponseElementsForGroup({ return results; } -export async function syncPrivateMessage({ - messageResponse, +async function getNewAnalysisResponseElements({ + analysisGroups, + existingElements, + log, +}: { + analysisGroups: ResponseUuringuGrupp[]; + existingElements: AnalysisResponseElement[]; + log: ReturnType; +}) { + const newElements: Omit[] = []; + for (const analysisGroup of analysisGroups) { + log(`[${analysisGroups.indexOf(analysisGroup) + 1}/${analysisGroups.length}] Syncing analysis group '${analysisGroup.UuringuGruppNimi}'`); + const elements = await getAnalysisResponseElementsForGroup({ + analysisGroup, + existingElements, + log, + }); + newElements.push(...elements); + } + return newElements; +} + +async function hasAllAnalysisResponseElements({ + analysisResponseId, order, }: { - messageResponse: NonNullable; + analysisResponseId: number; + order: Pick; +}) { + const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId }); + const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; + return allOrderResponseElements.length === expectedOrderResponseElements; +} + +export async function syncPrivateMessage({ + messageResponse: { + ValisTellimuseId: externalId, + TellimuseNumber: orderNumber, + TellimuseOlek, + UuringuGrupp, + }, + order, +}: { + messageResponse: Pick, 'ValisTellimuseId' | 'TellimuseNumber' | 'TellimuseOlek' | 'UuringuGrupp'>; order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; }) { const supabase = getSupabaseServerAdminClient(); - const externalId = messageResponse.ValisTellimuseId; - const orderNumber = messageResponse.TellimuseNumber; - const orderStatus = AnalysisOrderStatus[messageResponse.TellimuseOlek]; + const orderStatus = AnalysisOrderStatus[TellimuseOlek]; const log = logger(order, externalId, orderNumber); @@ -193,37 +234,28 @@ export async function syncPrivateMessage({ userId: analysisOrder.user_id, }); - const analysisGroups = toArray(messageResponse.UuringuGrupp); + const existingElements = await getExistingAnalysisResponseElements({ analysisResponseId }); + const analysisGroups = toArray(UuringuGrupp); log(`Order has results for ${analysisGroups.length} analysis groups`); + const newElements = await getNewAnalysisResponseElements({ analysisGroups, existingElements, log }); - for (const analysisGroup of analysisGroups) { - log(`[${analysisGroups.indexOf(analysisGroup) + 1}/${analysisGroups.length}] Syncing analysis group '${analysisGroup.UuringuGruppNimi}'`); - - const elements = await getAnalysisResponseElementsForGroup({ - analysisResponseId, - analysisGroup, - log, - }); - - for (const element of elements) { - const { error } = await supabase - .schema('medreport') - .from('analysis_response_elements') - .insert(element); - if (error) { - log(`Failed to insert order response elements for response id ${analysisResponseId} (order id: ${analysisOrder.id})`, error); - } + for (const element of newElements) { + try { + await createAnalysisResponseElement({ + element: { + ...element, + analysis_response_id: analysisResponseId, + }, + }); + } catch (e) { + log(`Failed to create order response element for response id ${analysisResponseId}, element id '${element.analysis_element_original_id}' (order id: ${order.id})`, e as PostgrestError); } } - const allOrderResponseElements = await getExistingAnalysisResponseElements({ analysisResponseId }); - const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; - if (allOrderResponseElements.length !== expectedOrderResponseElements) { - return { isPartial: true }; - } - - return { isCompleted: orderStatus === 'COMPLETED' }; + return await hasAllAnalysisResponseElements({ analysisResponseId, order }) + ? { isCompleted: orderStatus === 'COMPLETED' } + : { isPartial: true }; } export async function readPrivateMessageResponse({ @@ -297,6 +329,9 @@ export async function readPrivateMessageResponse({ analysisOrder = await getAnalysisOrder({ analysisOrderId }) medusaOrderId = analysisOrder.medusa_order_id; } catch (e) { + if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) { + await deletePrivateMessage(privateMessageId); + } throw new Error(`No analysis order found for Medipost message ValisTellimuseId=${medipostExternalOrderId}`); } @@ -305,17 +340,7 @@ export async function readPrivateMessageResponse({ throw new Error(`Order person personal code does not match Medipost message Patsient.Isikukood=${patientPersonalCode}, orderPerson.personal_code=${orderPerson.personal_code}`); } - let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; - try { - order = await getAnalysisOrder({ medusaOrderId }); - } catch (e) { - if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) { - await deletePrivateMessage(privateMessageId); - } - throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); - } - - const status = await syncPrivateMessage({ messageResponse, order }); + const status = await syncPrivateMessage({ messageResponse, order: analysisOrder }); await createMedipostActionLog({ action: 'sync_analysis_results_from_medipost', @@ -394,7 +419,7 @@ export async function getPrivateMessage(messageId: string) { await validateMedipostResponse(data, { canHaveEmptyCode: true }); return { - message: parseXML(data) as MedipostOrderResponse, + message: (await parseXML(data)) as MedipostOrderResponse, xml: data as string, }; } diff --git a/lib/services/medipost/medipostPublicMessage.service.ts b/lib/services/medipost/medipostPublicMessage.service.ts index 21b21e4..c6c823e 100644 --- a/lib/services/medipost/medipostPublicMessage.service.ts +++ b/lib/services/medipost/medipostPublicMessage.service.ts @@ -29,5 +29,5 @@ export async function getLatestPublicMessageListItem() { throw new Error('Failed to get public message list'); } - return getLatestMessage({ messages: data?.messages }); + return await getLatestMessage({ messages: data?.messages }); } diff --git a/lib/services/medipost/medipostValidate.service.ts b/lib/services/medipost/medipostValidate.service.ts index 655673b..aa85192 100644 --- a/lib/services/medipost/medipostValidate.service.ts +++ b/lib/services/medipost/medipostValidate.service.ts @@ -8,7 +8,7 @@ import { MedipostValidationError } from './MedipostValidationError'; import { parseXML } from '../util/xml.service'; export async function validateMedipostResponse(response: string, { canHaveEmptyCode = false }: { canHaveEmptyCode?: boolean } = {}) { - const parsed: IMedipostResponseXMLBase = parseXML(response); + const parsed: IMedipostResponseXMLBase = await parseXML(response); const code = parsed.ANSWER?.CODE; if (canHaveEmptyCode) { if (code && code !== 0) { diff --git a/lib/services/util/xml.service.ts b/lib/services/util/xml.service.ts index c3eb02c..a9d156e 100644 --- a/lib/services/util/xml.service.ts +++ b/lib/services/util/xml.service.ts @@ -2,7 +2,7 @@ import { XMLParser } from 'fast-xml-parser'; -export function parseXML(xml: string) { +export async function parseXML(xml: string) { const parser = new XMLParser({ ignoreAttributes: false }); return parser.parse(xml); } diff --git a/packages/features/accounts/src/types/analysis-results.ts b/packages/features/accounts/src/types/analysis-results.ts index 64ddb83..e2b5e66 100644 --- a/packages/features/accounts/src/types/analysis-results.ts +++ b/packages/features/accounts/src/types/analysis-results.ts @@ -78,6 +78,7 @@ export type AnalysisResultDetailsElementResults = { responseTime: string | null; responseValue: number | null; responseValueIsNegative: boolean | null; + responseValueIsWithinNorm: boolean | null; normLowerIncluded: boolean; normUpperIncluded: boolean; status: string; diff --git a/packages/features/user-analyses/src/server/api.ts b/packages/features/user-analyses/src/server/api.ts index b01047c..6b74201 100644 --- a/packages/features/user-analyses/src/server/api.ts +++ b/packages/features/user-analyses/src/server/api.ts @@ -80,7 +80,7 @@ class UserAnalysesApi { .from('analysis_responses') .select( `*, - elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time,status,analysis_element_original_id,original_response_element,response_value_is_negative), + elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time,status,analysis_element_original_id,original_response_element,response_value_is_negative,response_value_is_within_norm), summary:analysis_order_id(doctor_analysis_feedback(*))`, ) .eq('user_id', user.id) @@ -192,6 +192,10 @@ class UserAnalysesApi { } return nestedElements.map((element) => { const elementVastus = element.UuringuVastus as UuringuVastus | undefined; + const responseValue = elementVastus?.VastuseVaartus; + const responseValueIsNumeric = !isNaN(Number(responseValue)); + const responseValueIsNegative = responseValue === 'Negatiivne'; + const responseValueIsWithinNorm = responseValue === 'Normi piires'; return { status: element.UuringOlek, unit: element.Mootyhik, @@ -199,8 +203,9 @@ class UserAnalysesApi { normUpper: elementVastus?.NormYlem?.['#text'], normStatus: elementVastus?.NormiStaatus, responseTime: elementVastus?.VastuseAeg, - responseValue: elementVastus?.VastuseVaartus, - responseValueIsNegative: elementVastus?.VastuseVaartus === 'Negatiivne', + response_value: responseValueIsNegative || !responseValueIsNumeric ? null : (responseValue ?? null), + response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative, + response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm, normLowerIncluded: elementVastus?.NormAlum?.['@_kaasaarvatud'] === 'JAH', normUpperIncluded: elementVastus?.NormYlem?.['@_kaasaarvatud'] === 'JAH', analysisElementOriginalId: element.UuringId, @@ -216,6 +221,7 @@ class UserAnalysesApi { responseTime: elementResponse.response_time, responseValue: elementResponse.response_value, responseValueIsNegative: elementResponse.response_value_is_negative === true, + responseValueIsWithinNorm: elementResponse.response_value_is_within_norm === true, normLowerIncluded: elementResponse.norm_lower_included, normUpperIncluded: elementResponse.norm_upper_included, status: elementResponse.status, diff --git a/packages/features/user-analyses/src/types/analysis-results.ts b/packages/features/user-analyses/src/types/analysis-results.ts index 64ddb83..3176499 100644 --- a/packages/features/user-analyses/src/types/analysis-results.ts +++ b/packages/features/user-analyses/src/types/analysis-results.ts @@ -19,6 +19,7 @@ const ElementSchema = z.object({ response_time: z.string(), response_value: z.number(), response_value_is_negative: z.boolean(), + response_value_is_within_norm: z.boolean(), norm_lower_included: z.boolean(), norm_upper_included: z.boolean(), status: z.string(), @@ -78,6 +79,7 @@ export type AnalysisResultDetailsElementResults = { responseTime: string | null; responseValue: number | null; responseValueIsNegative: boolean | null; + responseValueIsWithinNorm: boolean | null; normLowerIncluded: boolean; normUpperIncluded: boolean; status: string; diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index e3f2af6..6907fac 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -688,6 +688,7 @@ export type Database = { response_time: string response_value: number | null response_value_is_negative?: boolean | null + response_value_is_within_norm?: boolean | null status: string unit: string | null updated_at: string | null @@ -708,6 +709,7 @@ export type Database = { response_time: string response_value: number | null response_value_is_negative?: boolean | null + response_value_is_within_norm?: boolean | null status: string unit?: string | null updated_at?: string | null @@ -728,6 +730,7 @@ export type Database = { response_time?: string response_value?: number | null response_value_is_negative?: boolean | null + response_value_is_within_norm?: boolean | null status: string unit?: string | null updated_at?: string | null diff --git a/public/locales/en/analysis-results.json b/public/locales/en/analysis-results.json index 571b1fb..02a2195 100644 --- a/public/locales/en/analysis-results.json +++ b/public/locales/en/analysis-results.json @@ -7,9 +7,16 @@ "noAnalysisElements": "No analysis orders found", "noAnalysisOrders": "No analysis orders found", "analysisDate": "Analysis result date", + "cancelled": "Cancelled", "results": { "range": { "normal": "Normal range" + }, + "value": { + "negative": "Negative", + "positive": "Positive", + "isWithinNorm": "Within norm", + "isNotWithinNorm": "Not within norm" } }, "orderTitle": "Order number {{orderNumber}}", diff --git a/public/locales/et/analysis-results.json b/public/locales/et/analysis-results.json index 9efe9bd..1fabbf8 100644 --- a/public/locales/et/analysis-results.json +++ b/public/locales/et/analysis-results.json @@ -7,9 +7,16 @@ "noAnalysisElements": "Veel ei ole tellitud analüüse", "noAnalysisOrders": "Veel ei ole analüüside tellimusi", "analysisDate": "Analüüsi vastuse kuupäev", + "cancelled": "Tühistatud", "results": { "range": { "normal": "Normaalne vahemik" + }, + "value": { + "negative": "Negatiivne", + "positive": "Positiivne", + "isWithinNorm": "Normi piires", + "isNotWithinNorm": "Normi piirest väljas" } }, "orderTitle": "Tellimus {{orderNumber}}", diff --git a/public/locales/ru/analysis-results.json b/public/locales/ru/analysis-results.json index 8a032b3..c79497e 100644 --- a/public/locales/ru/analysis-results.json +++ b/public/locales/ru/analysis-results.json @@ -7,9 +7,16 @@ "noAnalysisElements": "Анализы еще не заказаны", "noAnalysisOrders": "Пока нет заказов на анализы", "analysisDate": "Дата результата анализа", + "cancelled": "Отменен", "results": { "range": { "normal": "Нормальный диапазон" + }, + "value": { + "negative": "Отрицательный", + "positive": "Положительный", + "isWithinNorm": "В норме", + "isNotWithinNorm": "Не в норме" } }, "orderTitle": "Заказ {{orderNumber}}" diff --git a/supabase/migrations/20250917073453_analysis_response_element_within_range.sql b/supabase/migrations/20250917073453_analysis_response_element_within_range.sql new file mode 100644 index 0000000..657e79b --- /dev/null +++ b/supabase/migrations/20250917073453_analysis_response_element_within_range.sql @@ -0,0 +1,2 @@ +ALTER TABLE medreport.analysis_response_elements +ADD COLUMN response_value_is_within_norm BOOLEAN; From 64acdfcbbb04aecbff4550e4817fa34e9fbc4c23 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:17:43 +0300 Subject: [PATCH 22/27] feat(MED-161): add test analysis results page for different cases --- .../analysis-results/test/page.tsx | 99 ++ .../analysis-results/test/test-responses.ts | 1116 +++++++++++++++++ 2 files changed, 1215 insertions(+) create mode 100644 app/home/(user)/(dashboard)/analysis-results/test/page.tsx create mode 100644 app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts diff --git a/app/home/(user)/(dashboard)/analysis-results/test/page.tsx b/app/home/(user)/(dashboard)/analysis-results/test/page.tsx new file mode 100644 index 0000000..ddd1f4c --- /dev/null +++ b/app/home/(user)/(dashboard)/analysis-results/test/page.tsx @@ -0,0 +1,99 @@ +'use client'; + +import React, { useState } from 'react'; + +import { PageBody, PageHeader } from '@kit/ui/page'; +import { Trans } from '@kit/ui/trans'; +import { Button } from '@kit/ui/shadcn/button'; +import Modal from "@modules/common/components/modal" + +import Analysis from '../_components/analysis'; +import { analysisResponses } from './test-responses'; + +export default function AnalysisResultsPage() { + const [openBlocks, setOpenBlocks] = useState([]); + + return ( + <> + + +
+
+

+ Analüüsi tulemused demo +

+
+
+ +
+ {analysisResponses.map(({ id, orderedAnalysisElements }, index) => { + const isOpen = openBlocks.includes(id); + const closeModal = () => setOpenBlocks(openBlocks.filter((block) => block !== id)); + return ( +
+
+

AnalysisOrderId: {id}

+ +
+
OrderedAnalysisElements
+ + {isOpen && ( + +
+ +

NormiStaatus

+
    +
  • 0 - testi väärtus jääb normaalväärtuste piirkonda või on määramata,
  • +
  • 1 - testi väärtus jääb hoiatava (tähelepanu suunava) märkega piirkonda,
  • +
  • 2 - testi väärtus on normaalväärtuste piirkonnast väljas või kõrgendatud tähelepanu nõudvas piirkonnas.
  • +
+ +

UuringOlek

+
    +
  • 1 - Järjekorras,
  • +
  • 2 - Ootel,
  • +
  • 3 - Töös,
  • +
  • 4 - Lõpetatud,
  • +
  • 5 - Tagasi lükatud,
  • +
  • 6 - Tühistatud,
  • +
+ +
+                            {JSON.stringify(orderedAnalysisElements, null, 2)}
+                          
+
+
+ )} +
+ + {orderedAnalysisElements ? ( + orderedAnalysisElements.map((element, index) => ( + + )) + ) : ( +
+ +
+ )} +
+
+
+ ) + })} +
+
+ + ); +} diff --git a/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts new file mode 100644 index 0000000..0801825 --- /dev/null +++ b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts @@ -0,0 +1,1116 @@ +import { AnalysisResultDetailsMapped } from "@/packages/features/accounts/src/types/analysis-results"; + +type AnalysisTestResponse = Omit; + +const empty1: AnalysisTestResponse = { + "id": 1, + "orderedAnalysisElements": [], +}; + +const big1: AnalysisTestResponse = { + "id": 2, + "orderedAnalysisElements": [ + { + "analysisIdOriginal": "1744-2", + "isWaitingForResults": false, + "analysisName": "ALAT", + "results": { + "nestedElements": [], + "unit": "U/l", + "normLower": null, + "normUpper": 45, + "normStatus": 2, + "responseTime": "2024-02-29T10:42:25+00:00", + "responseValue": 84, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "1744-2" + } + }, + { + "analysisIdOriginal": "1920-8", + "isWaitingForResults": false, + "analysisName": "ASAT", + "results": { + "nestedElements": [], + "unit": "U/l", + "normLower": 15, + "normUpper": 45, + "normStatus": 0, + "responseTime": "2024-02-29T10:20:55+00:00", + "responseValue": 45, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "1920-8" + } + }, + { + "analysisIdOriginal": "1988-5", + "isWaitingForResults": false, + "analysisName": "CRP", + "results": { + "nestedElements": [], + "unit": "mg/l", + "normLower": null, + "normUpper": 5, + "normStatus": 0, + "responseTime": "2024-02-29T10:18:49+00:00", + "responseValue": 0.79, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "1988-5" + } + }, + { + "analysisIdOriginal": "57747-8", + "isWaitingForResults": false, + "analysisName": "Erütrotsüüdid", + "results": { + "nestedElements": [], + "unit": null, + "normLower": null, + "normUpper": 5, + "normStatus": 0, + "responseTime": "2024-02-29T10:13:01+00:00", + "responseValue": null, + "responseValueIsNegative": true, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "57747-8" + } + }, + { + "analysisIdOriginal": "2276-4", + "isWaitingForResults": false, + "analysisName": "Ferritiin", + "results": { + "nestedElements": [], + "unit": "µg/l", + "normLower": 28, + "normUpper": 370, + "normStatus": 0, + "responseTime": "2024-02-29T10:46:54+00:00", + "responseValue": 204.1, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "2276-4" + } + }, + { + "analysisIdOriginal": "14771-0", + "isWaitingForResults": false, + "analysisName": "Glükoos", + "results": { + "nestedElements": [], + "unit": "mmol/l", + "normLower": 4.1, + "normUpper": 6, + "normStatus": 0, + "responseTime": "2024-02-29T10:06:24+00:00", + "responseValue": 5.4, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "14771-0" + } + }, + { + "analysisIdOriginal": "59156-0", + "isWaitingForResults": false, + "analysisName": "Glükoos", + "results": { + "nestedElements": [], + "unit": null, + "normLower": null, + "normUpper": 2, + "normStatus": 0, + "responseTime": "2024-02-29T10:13:01+00:00", + "responseValue": null, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": false, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "59156-0" + } + }, + { + "analysisIdOriginal": "13955-0", + "isWaitingForResults": false, + "analysisName": "HCV Ab", + "results": { + "nestedElements": [], + "unit": null, + "normLower": null, + "normUpper": null, + "normStatus": 0, + "responseTime": "2024-02-29T13:44:48+00:00", + "responseValue": null, + "responseValueIsNegative": true, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "13955-0" + } + }, + { + "analysisIdOriginal": "14646-4", + "isWaitingForResults": false, + "analysisName": "HDL kolesterool", + "results": { + "nestedElements": [], + "unit": "mmol/l", + "normLower": 1, + "normUpper": null, + "normStatus": 1, + "responseTime": "2024-02-29T10:20:55+00:00", + "responseValue": 0.8, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "14646-4" + } + }, + { + "analysisIdOriginal": "2000-8", + "isWaitingForResults": false, + "analysisName": "Kaltsium", + "results": { + "nestedElements": [], + "unit": "mmol/l", + "normLower": 2.1, + "normUpper": 2.55, + "normStatus": 0, + "responseTime": "2024-02-29T10:12:10+00:00", + "responseValue": 2.49, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "2000-8" + } + }, + { + "analysisIdOriginal": "59158-6", + "isWaitingForResults": false, + "analysisName": "Ketokehad", + "results": { + "nestedElements": [], + "unit": null, + "normLower": null, + "normUpper": 0.5, + "normStatus": 0, + "responseTime": "2024-02-29T10:13:01+00:00", + "responseValue": null, + "responseValueIsNegative": true, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "59158-6" + } + }, + { + "analysisIdOriginal": "14647-2", + "isWaitingForResults": false, + "analysisName": "Kolesterool", + "results": { + "nestedElements": [], + "unit": "mmol/l", + "normLower": null, + "normUpper": 5, + "normStatus": 1, + "responseTime": "2024-02-29T10:20:34+00:00", + "responseValue": 5.7, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "14647-2" + } + }, + { + "analysisIdOriginal": "14682-9", + "isWaitingForResults": false, + "analysisName": "Kreatiniin", + "results": { + "nestedElements": [], + "unit": "µmol/l", + "normLower": 64, + "normUpper": 111, + "normStatus": 0, + "responseTime": "2024-02-29T10:19:00+00:00", + "responseValue": 89, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "14682-9" + } + }, + { + "analysisIdOriginal": "22748-8", + "isWaitingForResults": false, + "analysisName": "LDL kolesterool", + "results": { + "nestedElements": [], + "unit": "mmol/l", + "normLower": null, + "normUpper": 3, + "normStatus": 1, + "responseTime": "2024-02-29T10:21:15+00:00", + "responseValue": 4.3, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "22748-8" + } + }, + { + "analysisIdOriginal": "58805-3", + "isWaitingForResults": false, + "analysisName": "Leukotsüüdid", + "results": { + "nestedElements": [], + "unit": null, + "normLower": null, + "normUpper": 10, + "normStatus": 0, + "responseTime": "2024-02-29T10:13:01+00:00", + "responseValue": null, + "responseValueIsNegative": true, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "58805-3" + } + }, + { + "analysisIdOriginal": "2601-3", + "isWaitingForResults": false, + "analysisName": "Magneesium", + "results": { + "nestedElements": [], + "unit": "mmol/l", + "normLower": 0.66, + "normUpper": 1.07, + "normStatus": 0, + "responseTime": "2024-02-29T10:17:26+00:00", + "responseValue": 0.82, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "2601-3" + } + }, + { + "analysisIdOriginal": "70204-3", + "isWaitingForResults": false, + "analysisName": "Mitte-HDL kolesterool", + "results": { + "nestedElements": [], + "labComment": "Mitte-paastu veri <3,9 mmol/L", + "unit": "mmol/l", + "normLower": null, + "normUpper": 3.8, + "normStatus": 1, + "responseTime": "2024-02-29T10:20:55+00:00", + "responseValue": 4.9, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "70204-3" + } + }, + { + "analysisIdOriginal": "14798-3", + "isWaitingForResults": false, + "analysisName": "Raud", + "results": { + "nestedElements": [], + "unit": "µmol/l", + "normLower": 11.6, + "normUpper": 31.3, + "normStatus": 0, + "responseTime": "2024-02-29T10:21:16+00:00", + "responseValue": 16.5, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "14798-3" + } + }, + { + "analysisIdOriginal": "14927-8", + "isWaitingForResults": false, + "analysisName": "Triglütseriidid", + "results": { + "nestedElements": [], + "labComment": "Mitte-paastu veri <2,0 mmol/L", + "unit": "mmol/l", + "normLower": null, + "normUpper": 1.7, + "normStatus": 1, + "responseTime": "2024-02-29T10:21:16+00:00", + "responseValue": 1.89, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "14927-8" + } + }, + { + "analysisIdOriginal": "3016-3", + "isWaitingForResults": false, + "analysisName": "TSH", + "results": { + "nestedElements": [], + "unit": "mIU/l", + "normLower": 0.4, + "normUpper": 4, + "normStatus": 0, + "responseTime": "2024-02-29T10:49:02+00:00", + "responseValue": 1.27, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "3016-3" + } + }, + { + "analysisIdOriginal": "22664-7", + "isWaitingForResults": false, + "analysisName": "Uurea", + "results": { + "nestedElements": [], + "unit": "mmol/l", + "normLower": 3.2, + "normUpper": 7.4, + "normStatus": 0, + "responseTime": "2024-02-29T10:19:11+00:00", + "responseValue": 6.4, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "22664-7" + } + }, + { + "analysisIdOriginal": "50561-0", + "isWaitingForResults": false, + "analysisName": "Valk", + "results": { + "nestedElements": [], + "unit": null, + "normLower": null, + "normUpper": 0.25, + "normStatus": 0, + "responseTime": "2024-02-29T10:13:01+00:00", + "responseValue": null, + "responseValueIsNegative": true, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "50561-0" + } + }, + { + "analysisIdOriginal": "60493-4", + "isWaitingForResults": false, + "analysisName": "Vitamiin D (25-OH)", + "results": { + "nestedElements": [], + "labComment": "Väärtus >75 nmol/l on D-vitamiini tervislik tase", + "unit": "nmol/l", + "normLower": 75, + "normUpper": null, + "normStatus": 0, + "responseTime": "2024-02-29T10:49:22+00:00", + "responseValue": 105.5, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "60493-4" + } + }, + { + "analysisIdOriginal": "60025-4", + "isWaitingForResults": false, + "analysisName": "Urobilinogeen", + "results": { + "nestedElements": [], + "unit": null, + "normLower": null, + "normUpper": 17, + "normStatus": 0, + "responseTime": "2024-02-29T10:13:01+00:00", + "responseValue": null, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": true, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "60025-4" + }, + } + ], +}; + +const big2: AnalysisTestResponse = { + "id": 3, + "orderedAnalysisElements": [ + { + "analysisIdOriginal": "1988-5", + "isWaitingForResults": false, + "analysisName": "CRP", + "results": { + "nestedElements": [], + "unit": "mg/L", + "normLower": null, + "normUpper": 5, + "normStatus": 0, + "responseTime": "2025-09-12T14:02:04+00:00", + "responseValue": 1, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "1988-5" + } + }, + { + "analysisIdOriginal": "57021-8", + "isWaitingForResults": false, + "analysisName": "Hemogramm", + "results": { + "nestedElements": [ + { + "status": 4, + "unit": "g/L", + "normLower": 134, + "normUpper": 170, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 150, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "718-7" + }, + { + "status": 4, + "unit": "%", + "normLower": 40, + "normUpper": 49, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 45, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "4544-3" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 4.1, + "normUpper": 9.7, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "6690-2" + }, + { + "status": 4, + "unit": "E12/L", + "normLower": 4.5, + "normUpper": 5.7, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "789-8" + }, + { + "status": 4, + "unit": "fL", + "normLower": 82, + "normUpper": 95, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 85, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "787-2" + }, + { + "status": 4, + "unit": "pg", + "normLower": 28, + "normUpper": 33, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 30, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "785-6" + }, + { + "status": 4, + "unit": "g/L", + "normLower": 322, + "normUpper": 356, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 355, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "786-4" + }, + { + "status": 4, + "unit": "%", + "normLower": 12, + "normUpper": 15, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 15, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "788-0" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 157, + "normUpper": 372, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 255, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "777-3" + }, + { + "status": 4, + "unit": "%", + "normLower": 0.18, + "normUpper": 0.38, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0.2, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "51637-7" + }, + { + "status": 4, + "unit": "fL", + "normLower": 9.2, + "normUpper": 12.3, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 10, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "32623-1" + }, + { + "status": 4, + "unit": "fL", + "normLower": 10.1, + "normUpper": 16.2, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 15, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "32207-3" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 0.01, + "normUpper": 0.08, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0.05, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "704-7" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 0.02, + "normUpper": 0.4, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0.05, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "711-2" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 1.9, + "normUpper": 6.7, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "751-8" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 0.24, + "normUpper": 0.8, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0.5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "742-7" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 1.3, + "normUpper": 3.1, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 1.5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "731-0" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 0, + "normUpper": 0.03, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 0, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "51584-1" + }, + { + "status": 4, + "unit": "%", + "normLower": 0, + "normUpper": 0.5, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "38518-7" + }, + { + "status": 4, + "unit": "E9/L", + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "771-6" + }, + { + "status": 4, + "unit": "/100WBC", + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "58413-6" + } + ], + "unit": null, + "normLower": null, + "normUpper": null, + "normStatus": null, + "responseTime": null, + "responseValue": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "responseValueIsNegative": false, + "responseValueIsWithinNorm": false, + "status": "4", + "analysisElementOriginalId": "57021-8" + } + }, + { + "analysisIdOriginal": "43583-4", + "isWaitingForResults": false, + "analysisName": "Lipoproteiin a", + "results": { + "nestedElements": [], + "labComment": "Kliendi soovil analüüs tühistatud.", + "unit": null, + "normLower": null, + "normUpper": null, + "normStatus": null, + "responseTime": null, + "responseValue": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "responseValueIsNegative": false, + "responseValueIsWithinNorm": false, + "status": "5", + "analysisElementOriginalId": "43583-4" + } + }, + { + "analysisIdOriginal": "60493-4", + "isWaitingForResults": false, + "analysisName": "Vitamiin D (25-OH)", + "results": { + "nestedElements": [], + "labComment": "Väärtus vahemikus 30-49.9 nmol/L on D-vitamiini ebapiisav tase.", + "unit": "nmol/L", + "normLower": 75, + "normUpper": null, + "normStatus": 1, + "responseTime": "2025-09-12T14:02:04+00:00", + "responseValue": 30, + "normLowerIncluded": false, + "normUpperIncluded": false, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": null, + "status": "4", + "analysisElementOriginalId": "60493-4" + } + } + ], +}; + +const big2: AnalysisTestResponse = { + "id": 3, + "orderedAnalysisElements": [ + { + "analysisIdOriginal": "57021-8", + "isWaitingForResults": false, + "analysisName": "Hemogramm", + "results": { + "nestedElements": [ + { + "status": 4, + "unit": "g/L", + "normLower": 134, + "normUpper": 170, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 150, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "718-7" + }, + { + "status": 4, + "unit": "%", + "normLower": 40, + "normUpper": 49, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 45, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "4544-3" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 4.1, + "normUpper": 9.7, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "6690-2" + }, + { + "status": 4, + "unit": "E12/L", + "normLower": 4.5, + "normUpper": 5.7, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "789-8" + }, + { + "status": 4, + "unit": "fL", + "normLower": 82, + "normUpper": 95, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 85, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "787-2" + }, + { + "status": 4, + "unit": "pg", + "normLower": 28, + "normUpper": 33, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 30, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "785-6" + }, + { + "status": 4, + "unit": "g/L", + "normLower": 322, + "normUpper": 356, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 355, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "786-4" + }, + { + "status": 4, + "unit": "%", + "normLower": 12, + "normUpper": 15, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 15, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "788-0" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 157, + "normUpper": 372, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 255, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "777-3" + }, + { + "status": 4, + "unit": "%", + "normLower": 0.18, + "normUpper": 0.38, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0.2, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "51637-7" + }, + { + "status": 4, + "unit": "fL", + "normLower": 9.2, + "normUpper": 12.3, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 10, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "32623-1" + }, + { + "status": 4, + "unit": "fL", + "normLower": 10.1, + "normUpper": 16.2, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 15, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "32207-3" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 0.01, + "normUpper": 0.08, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0.05, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "704-7" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 0.02, + "normUpper": 0.4, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0.05, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "711-2" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 1.9, + "normUpper": 6.7, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "751-8" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 0.24, + "normUpper": 0.8, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0.5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "742-7" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 1.3, + "normUpper": 3.1, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 1.5, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "731-0" + }, + { + "status": 4, + "unit": "E9/L", + "normLower": 0, + "normUpper": 0.03, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:03", + "responseValue": 0, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "51584-1" + }, + { + "status": 4, + "unit": "%", + "normLower": 0, + "normUpper": 0.5, + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "38518-7" + }, + { + "status": 4, + "unit": "E9/L", + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "771-6" + }, + { + "status": 4, + "unit": "/100WBC", + "normStatus": 0, + "responseTime": "2025-09-12 14:02:04", + "responseValue": 0, + "normLowerIncluded": false, + "normUpperIncluded": false, + "analysisElementOriginalId": "58413-6" + } + ], + "unit": null, + "normLower": null, + "normUpper": null, + "normStatus": null, + "responseTime": null, + "responseValue": null, + "normLowerIncluded": false, + "normUpperIncluded": false, + "responseValueIsNegative": false, + "responseValueIsWithinNorm": false, + "status": "4", + "analysisElementOriginalId": "57021-8", + "summary": 'test' + }, + }, + ], +}; + +export const analysisResponses: AnalysisTestResponse[] = [ + empty1, + big1, + big2, +]; From 4302ddb90ec73e691781de9fbad25efcd702b807 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:17:50 +0300 Subject: [PATCH 23/27] feat(MED-161): add jest for service tests --- __mocks__/isikukood.ts | 44 + __mocks__/server-only.ts | 3 + .../analysis-results/test/test-responses.ts | 276 ---- jest.config.js | 66 + jest.setup.js | 59 + .../medipostPrivateMessage.service.test.ts | 112 ++ package.json | 9 + pnpm-lock.yaml | 1278 ++++++++++++++++- 8 files changed, 1503 insertions(+), 344 deletions(-) create mode 100644 __mocks__/isikukood.ts create mode 100644 __mocks__/server-only.ts create mode 100644 jest.config.js create mode 100644 jest.setup.js create mode 100644 lib/services/medipost/medipostPrivateMessage.service.test.ts diff --git a/__mocks__/isikukood.ts b/__mocks__/isikukood.ts new file mode 100644 index 0000000..1705754 --- /dev/null +++ b/__mocks__/isikukood.ts @@ -0,0 +1,44 @@ +// Mock for isikukood library to avoid ES module issues in tests + +export enum Gender { + MALE = 'male', + FEMALE = 'female', +} + +export default class Isikukood { + private code: string; + + constructor(code: string) { + this.code = code; + } + + static validate(code: string): boolean { + return true; // Mock always returns true for tests + } + + static generate(options?: { gender?: Gender; century?: number }): string { + return '39001010002'; // Mock Estonian ID code + } + + isValid(): boolean { + return true; + } + + getGender(): Gender { + return Gender.MALE; + } + + getBirthDate(): Date { + return new Date('1990-01-01'); + } + + getAge(): number { + return 30; + } + + getCentury(): number { + return 3; + } +} + +export { Isikukood }; diff --git a/__mocks__/server-only.ts b/__mocks__/server-only.ts new file mode 100644 index 0000000..780c05b --- /dev/null +++ b/__mocks__/server-only.ts @@ -0,0 +1,3 @@ +// Mock for server-only to avoid Next.js server component issues in tests +// This module does nothing in tests - it's just a marker for Next.js +export {}; diff --git a/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts index 0801825..e4da467 100644 --- a/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts +++ b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts @@ -833,282 +833,6 @@ const big2: AnalysisTestResponse = { ], }; -const big2: AnalysisTestResponse = { - "id": 3, - "orderedAnalysisElements": [ - { - "analysisIdOriginal": "57021-8", - "isWaitingForResults": false, - "analysisName": "Hemogramm", - "results": { - "nestedElements": [ - { - "status": 4, - "unit": "g/L", - "normLower": 134, - "normUpper": 170, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 150, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "718-7" - }, - { - "status": 4, - "unit": "%", - "normLower": 40, - "normUpper": 49, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 45, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "4544-3" - }, - { - "status": 4, - "unit": "E9/L", - "normLower": 4.1, - "normUpper": 9.7, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "6690-2" - }, - { - "status": 4, - "unit": "E12/L", - "normLower": 4.5, - "normUpper": 5.7, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "789-8" - }, - { - "status": 4, - "unit": "fL", - "normLower": 82, - "normUpper": 95, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 85, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "787-2" - }, - { - "status": 4, - "unit": "pg", - "normLower": 28, - "normUpper": 33, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 30, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "785-6" - }, - { - "status": 4, - "unit": "g/L", - "normLower": 322, - "normUpper": 356, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 355, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "786-4" - }, - { - "status": 4, - "unit": "%", - "normLower": 12, - "normUpper": 15, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 15, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "788-0" - }, - { - "status": 4, - "unit": "E9/L", - "normLower": 157, - "normUpper": 372, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 255, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "777-3" - }, - { - "status": 4, - "unit": "%", - "normLower": 0.18, - "normUpper": 0.38, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0.2, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "51637-7" - }, - { - "status": 4, - "unit": "fL", - "normLower": 9.2, - "normUpper": 12.3, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 10, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "32623-1" - }, - { - "status": 4, - "unit": "fL", - "normLower": 10.1, - "normUpper": 16.2, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 15, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "32207-3" - }, - { - "status": 4, - "unit": "E9/L", - "normLower": 0.01, - "normUpper": 0.08, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0.05, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "704-7" - }, - { - "status": 4, - "unit": "E9/L", - "normLower": 0.02, - "normUpper": 0.4, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0.05, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "711-2" - }, - { - "status": 4, - "unit": "E9/L", - "normLower": 1.9, - "normUpper": 6.7, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "751-8" - }, - { - "status": 4, - "unit": "E9/L", - "normLower": 0.24, - "normUpper": 0.8, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0.5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "742-7" - }, - { - "status": 4, - "unit": "E9/L", - "normLower": 1.3, - "normUpper": 3.1, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 1.5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "731-0" - }, - { - "status": 4, - "unit": "E9/L", - "normLower": 0, - "normUpper": 0.03, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 0, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "51584-1" - }, - { - "status": 4, - "unit": "%", - "normLower": 0, - "normUpper": 0.5, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "38518-7" - }, - { - "status": 4, - "unit": "E9/L", - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "771-6" - }, - { - "status": 4, - "unit": "/100WBC", - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "58413-6" - } - ], - "unit": null, - "normLower": null, - "normUpper": null, - "normStatus": null, - "responseTime": null, - "responseValue": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "responseValueIsNegative": false, - "responseValueIsWithinNorm": false, - "status": "4", - "analysisElementOriginalId": "57021-8", - "summary": 'test' - }, - }, - ], -}; - export const analysisResponses: AnalysisTestResponse[] = [ empty1, big1, diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..eac3110 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,66 @@ +import { pathsToModuleNameMapper } from 'ts-jest'; +import { readFileSync } from 'fs'; + +const tsconfig = JSON.parse(readFileSync('./tsconfig.json', 'utf8')); + +/** @type {import('jest').Config} */ +export default { + preset: 'ts-jest', + testEnvironment: 'node', + + // Handle module resolution for TypeScript paths + moduleNameMapper: { + ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { + prefix: '/', + }), + // Mock problematic libraries + '^isikukood$': '/__mocks__/isikukood.ts', + '^server-only$': '/__mocks__/server-only.ts', + }, + + // Test file patterns + testMatch: [ + '**/__tests__/**/*.(ts|tsx|js)', + '**/*.(test|spec).(ts|tsx|js)' + ], + + // Setup files + setupFilesAfterEnv: ['/jest.setup.js'], + + // Coverage configuration + collectCoverageFrom: [ + 'lib/**/*.{ts,tsx}', + 'app/**/*.{ts,tsx}', + 'components/**/*.{ts,tsx}', + '!**/*.d.ts', + '!**/node_modules/**', + '!**/.next/**' + ], + + // Transform configuration + transform: { + '^.+\\.(ts|tsx)$': ['ts-jest', { + useESM: true, + tsconfig: { + jsx: 'react-jsx', + }, + }], + }, + + // Module file extensions + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + + // Ignore patterns + testPathIgnorePatterns: [ + '/.next/', + '/node_modules/', + ], + + // Transform ignore patterns for node_modules + transformIgnorePatterns: [ + 'node_modules/(?!(.*\\.mjs$))', + ], + + // ESM support + extensionsToTreatAsEsm: ['.ts'], +}; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..540bbba --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,59 @@ +// Jest setup file for global test configuration + +// Mock Next.js router +jest.mock('next/router', () => ({ + useRouter() { + return { + route: '/', + pathname: '/', + query: {}, + asPath: '/', + push: jest.fn(), + pop: jest.fn(), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn().mockResolvedValue(undefined), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + isFallback: false, + }; + }, +})); + +// Mock Next.js navigation +jest.mock('next/navigation', () => ({ + useRouter() { + return { + push: jest.fn(), + replace: jest.fn(), + prefetch: jest.fn(), + back: jest.fn(), + forward: jest.fn(), + refresh: jest.fn(), + }; + }, + useSearchParams() { + return new URLSearchParams(); + }, + usePathname() { + return '/'; + }, +})); + +// Global test utilities +global.console = { + ...console, + // Suppress console.log in tests unless needed + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +}; + +// Set up environment variables for tests +process.env.NODE_ENV = 'test'; diff --git a/lib/services/medipost/medipostPrivateMessage.service.test.ts b/lib/services/medipost/medipostPrivateMessage.service.test.ts new file mode 100644 index 0000000..aeb16f1 --- /dev/null +++ b/lib/services/medipost/medipostPrivateMessage.service.test.ts @@ -0,0 +1,112 @@ +import { AnalysisResponseElement } from "~/lib/types/analysis-response-element"; +import { canCreateAnalysisResponseElement, getAnalysisResponseElementsForGroup } from "./medipostPrivateMessage.service"; +import { ResponseUuring } from "@/packages/shared/src/types/medipost-analysis"; + +type TestExistingElement = Pick; + +describe('medipostPrivateMessage.service', () => { + describe('canCreateAnalysisResponseElement', () => { + it('should return true if the analysis response element does not exist', async () => { + const existingElements = [] as TestExistingElement[]; + const groupUuring = { + UuringuElement: { + UuringOlek: 1, + UuringId: '1', + }, + } as const; + const responseValue = 1; + const log = jest.fn(); + expect(await canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })).toBe(true); + }); + + it('should return false if the analysis response element exists and the status is higher', async () => { + const existingElements = [{ analysis_element_original_id: '1', status: '2', response_value: 1 }] as TestExistingElement[]; + const groupUuring = { + UuringuElement: { + UuringOlek: 1, + UuringId: '1', + }, + } as const; + const responseValue = 1; + const log = jest.fn(); + expect(await canCreateAnalysisResponseElement({ existingElements, groupUuring, responseValue, log })).toBe(false); + }); + }); + + describe('getAnalysisResponseElementsForGroup', () => { + it('should return single new element', async () => { + const analysisGroup = { + UuringuGruppNimi: '1', + Uuring: [ + { + UuringuElement: { + UuringOlek: 1, + UuringId: '1', + UuringuVastus: [{ VastuseVaartus: '1' }], + }, + }, + ] as unknown as ResponseUuring[], + } as const; + const existingElements = [] as TestExistingElement[]; + const log = jest.fn(); + expect(await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log })) + .toEqual([{ + analysis_element_original_id: '1', + analysis_name: undefined, + comment: null, + norm_lower: null, + norm_lower_included: false, + norm_status: undefined, + norm_upper: null, + norm_upper_included: false, + response_time: null, + response_value: 1, + unit: null, + original_response_element: { + UuringOlek: 1, + UuringId: '1', + UuringuVastus: [{ VastuseVaartus: '1' }], + }, + status: '1', + }]); + }); + + it('should return no new element if element already exists in higher status', async () => { + const analysisGroup = { + UuringuGruppNimi: '1', + Uuring: [ + { + UuringuElement: { + UuringOlek: 1, + UuringId: '1', + UuringuVastus: [{ VastuseVaartus: '1' }], + }, + }, + ] as unknown as ResponseUuring[], + } as const; + const existingElements = [{ analysis_element_original_id: '1', status: '2', response_value: 1 }] as TestExistingElement[]; + const log = jest.fn(); + expect(await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log })) + .toEqual([]); + }); + + it('should return no new element if element already exists with response value', async () => { + const analysisGroup = { + UuringuGruppNimi: '1', + Uuring: [ + { + UuringuElement: { + UuringOlek: 1, + UuringId: '1', + UuringuVastus: [{ VastuseVaartus: '' }], + }, + }, + ] as unknown as ResponseUuring[], + } as const; + const existingElements = [{ analysis_element_original_id: '1', status: '1', response_value: 1 }] as TestExistingElement[]; + const log = jest.fn(); + expect(await getAnalysisResponseElementsForGroup({ analysisGroup, existingElements, log })) + .toEqual([]); + }); + }); +}); diff --git a/package.json b/package.json index 823ccc7..c038bb1 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,10 @@ "start": "next start", "start:test": "NODE_ENV=test next start", "typecheck": "tsc --noEmit", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "test:ci": "jest --ci --coverage --watchAll=false", "supabase": "supabase", "supabase:start": "supabase status || supabase start", "supabase:stop": "supabase stop", @@ -88,6 +92,7 @@ }, "devDependencies": { "@hookform/resolvers": "^5.0.1", + "@jest/globals": "^30.1.2", "@kit/eslint-config": "workspace:*", "@kit/prettier-config": "workspace:*", "@kit/tsconfig": "workspace:*", @@ -95,6 +100,7 @@ "@medusajs/ui-preset": "latest", "@next/bundle-analyzer": "15.3.2", "@tailwindcss/postcss": "^4.1.10", + "@types/jest": "^30.0.0", "@types/jsonwebtoken": "9.0.10", "@types/lodash": "^4.17.17", "@types/node": "^22.15.32", @@ -103,11 +109,14 @@ "babel-plugin-react-compiler": "19.1.0-rc.2", "cssnano": "^7.0.7", "dotenv": "^16.5.0", + "jest": "^30.1.3", + "jest-environment-node": "^30.1.2", "pino-pretty": "13.0.0", "prettier": "^3.5.3", "supabase": "^2.30.4", "tailwindcss": "4.1.7", "tailwindcss-animate": "^1.0.7", + "ts-jest": "^29.4.2", "typescript": "^5.8.3", "yup": "^1.6.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 041d098..08b330e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,10 +88,10 @@ importers: version: 2.10.1(react@19.1.0) '@medusajs/js-sdk': specifier: latest - version: 2.10.1(awilix@8.0.1) + version: 2.10.2(awilix@8.0.1) '@medusajs/ui': specifier: latest - version: 4.0.21(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2) + version: 4.0.22(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2) '@nosecone/next': specifier: 1.0.0-beta.7 version: 1.0.0-beta.7(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) @@ -177,6 +177,9 @@ importers: specifier: ^4.1.5 version: 4.1.5 devDependencies: + '@jest/globals': + specifier: ^30.1.2 + version: 30.1.2 '@kit/eslint-config': specifier: workspace:* version: link:tooling/eslint @@ -188,16 +191,19 @@ importers: version: link:tooling/typescript '@medusajs/types': specifier: latest - version: 2.10.1(awilix@8.0.1) + version: 2.10.2(awilix@8.0.1) '@medusajs/ui-preset': specifier: latest - version: 2.10.1(tailwindcss@4.1.7) + version: 2.10.2(tailwindcss@4.1.7) '@next/bundle-analyzer': specifier: 15.3.2 version: 15.3.2 '@tailwindcss/postcss': specifier: ^4.1.10 version: 4.1.12 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 '@types/jsonwebtoken': specifier: 9.0.10 version: 9.0.10 @@ -222,6 +228,12 @@ importers: dotenv: specifier: ^16.5.0 version: 16.6.1 + jest: + specifier: ^30.1.3 + version: 30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) + jest-environment-node: + specifier: ^30.1.2 + version: 30.1.2 pino-pretty: specifier: 13.0.0 version: 13.0.0 @@ -237,6 +249,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@4.1.7) + ts-jest: + specifier: ^29.4.2 + version: 29.4.2(@babel/core@7.28.3)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.3))(jest-util@30.0.5)(jest@30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)))(typescript@5.9.2) typescript: specifier: ^5.8.3 version: 5.9.2 @@ -856,10 +871,10 @@ importers: version: 2.2.7(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@medusajs/js-sdk': specifier: latest - version: 2.10.1(awilix@8.0.1) + version: 2.10.2(awilix@8.0.1) '@medusajs/ui': specifier: latest - version: 4.0.21(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.9.2) + version: 4.0.22(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.9.2) '@radix-ui/react-accordion': specifier: ^1.2.1 version: 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) @@ -905,10 +920,10 @@ importers: version: 7.28.3 '@medusajs/types': specifier: latest - version: 2.10.1(awilix@8.0.1) + version: 2.10.2(awilix@8.0.1) '@medusajs/ui-preset': specifier: latest - version: 2.10.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2))) + version: 2.10.2(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2))) '@types/lodash': specifier: ^4.14.195 version: 4.17.20 @@ -1933,6 +1948,28 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@7.0.2': + resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} + peerDependencies: + '@dnd-kit/core': ^6.0.7 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@edge-csrf/nextjs@2.5.3-cloudflare-rc1': resolution: {integrity: sha512-sH8HKl2s/zFkIgXcVzODljHsBhKW7LN2gXeUNEQTSP31Chy40ryjR7iTf0MA/DtPhOpXUkd6IBUMMF820sK+rA==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -2408,6 +2445,10 @@ packages: resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/console@30.1.2': + resolution: {integrity: sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/core@29.7.0': resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2417,26 +2458,67 @@ packages: node-notifier: optional: true + '@jest/core@30.1.3': + resolution: {integrity: sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment@29.7.0': resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/environment@30.1.2': + resolution: {integrity: sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect-utils@29.7.0': resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect-utils@30.1.2': + resolution: {integrity: sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect@29.7.0': resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect@30.1.2': + resolution: {integrity: sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/fake-timers@29.7.0': resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/fake-timers@30.1.2': + resolution: {integrity: sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/globals@29.7.0': resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/globals@30.1.2': + resolution: {integrity: sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/reporters@29.7.0': resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2446,30 +2528,67 @@ packages: node-notifier: optional: true + '@jest/reporters@30.1.3': + resolution: {integrity: sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.1.2': + resolution: {integrity: sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/source-map@29.6.3': resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-result@29.7.0': resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-result@30.1.3': + resolution: {integrity: sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-sequencer@29.7.0': resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-sequencer@30.1.3': + resolution: {integrity: sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/transform@29.7.0': resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/transform@30.1.2': + resolution: {integrity: sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@29.6.3': resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/types@30.0.5': + resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -2589,12 +2708,17 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - '@medusajs/js-sdk@2.10.1': - resolution: {integrity: sha512-KOXSU56cFu58w85PxgnqJsxaYNmGVCh4a70pROc4709u/Z6eGx5RdbdOdksk5vwEDWqr4OlngFB8aYVzLpXX4w==} + '@medusajs/icons@2.10.2': + resolution: {integrity: sha512-ZZFEWTGdQGvsRPs5ANV9GlFUFyba852cqWtRu7aO3QlPhk+ECbWqJYkhL3h/HZ1twN5nEG51QAncUkjm5TVBRw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + '@medusajs/js-sdk@2.10.2': + resolution: {integrity: sha512-P6A9E5LQkG/g9YiHlvUbH/k777XTfnY47neI1EmA56B66XBVIwzTqUglluREVZ+akjlI0eOxpwk2PDFPVaK1ZQ==} engines: {node: '>=20'} - '@medusajs/types@2.10.1': - resolution: {integrity: sha512-V4pRtwdCZQRnaXTgtTOD2EFOWaIz4Z59EMueGxHyepV/lST16hMNxzve3pf0KvqkWHSg8yGLMuXGwQDbfSYsIw==} + '@medusajs/types@2.10.2': + resolution: {integrity: sha512-yonCLLnO2FFDB+HCo6nAGz+I7u+7ux+HDJCT8KUPlZJ5/6AYZcgdsuNWMwaa441yjEXDDdZmGZtbSM37lBFoxA==} engines: {node: '>=20'} peerDependencies: awilix: ^8.0.1 @@ -2606,13 +2730,13 @@ packages: vite: optional: true - '@medusajs/ui-preset@2.10.1': - resolution: {integrity: sha512-vm6Zz5qLf63Y18yQE9M+v+uVT2rSExGU8EeaN9wOh4Wbc3nMJ0aJBLag4utIlYBY5MIfMU5EHPNqwfTGvNi6SA==} + '@medusajs/ui-preset@2.10.2': + resolution: {integrity: sha512-t3VmloSbeZOjfyYL+iQTU94wutX0/B+uv9wz8L1MK6CErHK4T4ox246QDsNAR2QrNE0EUEEy+SX0qiW+xHFsMQ==} peerDependencies: tailwindcss: '>=3.0.0' - '@medusajs/ui@4.0.21': - resolution: {integrity: sha512-C2XnIoksSDOUZKaTeRn8jMzTo0kUVZLyggIL5tSLL/oqZOkOYmnnio4wQoZ1FXKf0h8OE6awWLpVncFsQojnXw==} + '@medusajs/ui@4.0.22': + resolution: {integrity: sha512-p3Nl6OTyxe583VVmvGAYbizFFPzr9GsZzbO54Ku9jZqf+MYNqdDjKFP28K5QHkpLpbvkGU05BQZ5d5H49vGxiQ==} peerDependencies: react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc @@ -3101,6 +3225,10 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -3476,6 +3604,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dialog@1.1.4': + resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dialog@1.1.5': resolution: {integrity: sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==} peerDependencies: @@ -3520,6 +3661,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dismissable-layer@1.1.4': resolution: {integrity: sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==} peerDependencies: @@ -5614,6 +5768,9 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sinclair/typebox@0.34.41': + resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} + '@sindresorhus/slugify@1.1.2': resolution: {integrity: sha512-V9nR/W0Xd9TSGXpZ4iFUcFGhuOJtZX82Fzxj1YISlbSgKvIiNa7eLEZrT0vAraPOt++KHauIVNYgGRgjc13dXA==} engines: {node: '>=10'} @@ -5628,6 +5785,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@sinonjs/fake-timers@13.0.5': + resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -5947,6 +6107,9 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -6162,6 +6325,9 @@ packages: resolution: {integrity: sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@unrs/resolver-binding-darwin-arm64@1.7.11': resolution: {integrity: sha512-i3/wlWjQJXMh1uiGtiv7k1EYvrrS3L1hdwmWJJiz1D8jWy726YFYPIxQWbEIVPVAgrfRR0XNlLrTQwq17cuCGw==} cpu: [arm64] @@ -6526,6 +6692,12 @@ packages: peerDependencies: '@babel/core': ^7.8.0 + babel-jest@30.1.2: + resolution: {integrity: sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + babel-loader@8.4.1: resolution: {integrity: sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==} engines: {node: '>= 8.9'} @@ -6537,10 +6709,18 @@ packages: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} + babel-plugin-jest-hoist@29.6.3: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-plugin-jest-hoist@30.0.1: + resolution: {integrity: sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -6559,6 +6739,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + babel-preset-jest@30.0.1: + resolution: {integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -6599,6 +6785,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -6695,9 +6885,16 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + ci-info@4.3.0: + resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} + engines: {node: '>=8'} + cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.1.0: + resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -7372,6 +7569,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -7380,6 +7581,10 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + expect@30.1.2: + resolution: {integrity: sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + facepaint@1.2.1: resolution: {integrity: sha512-oNvBekbhsm/0PNSOWca5raHNAi6dG960Bx6LJgxDPNF59WpuspgQ17bN5MKwOr7JcFdQYc7StW3VZ28DBZLavQ==} @@ -7638,6 +7843,11 @@ packages: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -7955,6 +8165,10 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + istanbul-reports@3.1.7: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} @@ -7973,10 +8187,18 @@ packages: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-changed-files@30.0.5: + resolution: {integrity: sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-circus@29.7.0: resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-circus@30.1.3: + resolution: {integrity: sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-cli@29.7.0: resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7987,6 +8209,16 @@ packages: node-notifier: optional: true + jest-cli@30.1.3: + resolution: {integrity: sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jest-config@29.7.0: resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7999,22 +8231,53 @@ packages: ts-node: optional: true + jest-config@30.1.3: + resolution: {integrity: sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-diff@30.1.2: + resolution: {integrity: sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-docblock@29.7.0: resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-docblock@30.0.1: + resolution: {integrity: sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-each@29.7.0: resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-each@30.1.0: + resolution: {integrity: sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-environment-node@29.7.0: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-environment-node@30.1.2: + resolution: {integrity: sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-get-type@29.6.3: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8023,22 +8286,42 @@ packages: resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-haste-map@30.1.0: + resolution: {integrity: sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-leak-detector@29.7.0: resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-leak-detector@30.1.0: + resolution: {integrity: sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-matcher-utils@29.7.0: resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-matcher-utils@30.1.2: + resolution: {integrity: sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@29.7.0: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-message-util@30.1.0: + resolution: {integrity: sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@29.7.0: resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-mock@30.0.5: + resolution: {integrity: sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-pnp-resolver@1.2.3: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -8052,38 +8335,74 @@ packages: resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve-dependencies@29.7.0: resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve-dependencies@30.1.3: + resolution: {integrity: sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve@29.7.0: resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve@30.1.3: + resolution: {integrity: sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runner@29.7.0: resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runner@30.1.3: + resolution: {integrity: sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runtime@29.7.0: resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runtime@30.1.3: + resolution: {integrity: sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-snapshot@29.7.0: resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-snapshot@30.1.2: + resolution: {integrity: sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-util@29.7.0: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-util@30.0.5: + resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@29.7.0: resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-validate@30.1.0: + resolution: {integrity: sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-watcher@29.7.0: resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-watcher@30.1.3: + resolution: {integrity: sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} @@ -8092,6 +8411,10 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-worker@30.1.0: + resolution: {integrity: sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest@29.7.0: resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8102,6 +8425,16 @@ packages: node-notifier: optional: true + jest@30.1.3: + resolution: {integrity: sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -9247,6 +9580,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@30.0.5: + resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + prism-react-renderer@2.4.1: resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} peerDependencies: @@ -9318,6 +9655,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -9910,6 +10250,10 @@ packages: engines: {node: '>=16'} hasBin: true + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -10036,6 +10380,33 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.4.2: + resolution: {integrity: sha512-pBNOkn4HtuLpNrXTMVRC9b642CBaDnKqWXny4OzuoULT9S7Kf8MMlaRe2veKax12rjf5WcpMBhVPbQurlWGNxA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -10114,6 +10485,10 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -10142,6 +10517,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -10317,6 +10697,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wp-types@4.68.1: resolution: {integrity: sha512-PpyF5va7pxdBSV8cE3oao/Wfsrbx1gZKmBaijOd6/Q6RmuzCfUSPHtj8RzAtwDYD8dv7cga5lX63TvYTHdOIZA==} @@ -10335,6 +10718,10 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + write-file-atomic@6.0.0: resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -10785,6 +11172,56 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} + '@dnd-kit/accessibility@3.1.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + + '@dnd-kit/accessibility@3.1.1(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.0.0-rc-66855b96-20241106) + '@dnd-kit/utilities': 3.2.2(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + + '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + + '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + '@edge-csrf/nextjs@2.5.3-cloudflare-rc1(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': dependencies: next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -11281,6 +11718,15 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 + '@jest/console@30.1.2': + dependencies: + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + chalk: 4.1.2 + jest-message-util: 30.1.0 + jest-util: 30.0.5 + slash: 3.0.0 + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2))': dependencies: '@jest/console': 29.7.0 @@ -11316,6 +11762,44 @@ snapshots: - supports-color - ts-node + '@jest/core@30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2))': + dependencies: + '@jest/console': 30.1.2 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.1.3 + '@jest/test-result': 30.1.3 + '@jest/transform': 30.1.2 + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.0 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.0.5 + jest-config: 30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) + jest-haste-map: 30.1.0 + jest-message-util: 30.1.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.1.3 + jest-resolve-dependencies: 30.1.3 + jest-runner: 30.1.3 + jest-runtime: 30.1.3 + jest-snapshot: 30.1.2 + jest-util: 30.0.5 + jest-validate: 30.1.0 + jest-watcher: 30.1.3 + micromatch: 4.0.8 + pretty-format: 30.0.5 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.0.1': {} + '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 @@ -11323,10 +11807,21 @@ snapshots: '@types/node': 22.18.0 jest-mock: 29.7.0 + '@jest/environment@30.1.2': + dependencies: + '@jest/fake-timers': 30.1.2 + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + jest-mock: 30.0.5 + '@jest/expect-utils@29.7.0': dependencies: jest-get-type: 29.6.3 + '@jest/expect-utils@30.1.2': + dependencies: + '@jest/get-type': 30.1.0 + '@jest/expect@29.7.0': dependencies: expect: 29.7.0 @@ -11334,6 +11829,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/expect@30.1.2': + dependencies: + expect: 30.1.2 + jest-snapshot: 30.1.2 + transitivePeerDependencies: + - supports-color + '@jest/fake-timers@29.7.0': dependencies: '@jest/types': 29.6.3 @@ -11343,6 +11845,17 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 + '@jest/fake-timers@30.1.2': + dependencies: + '@jest/types': 30.0.5 + '@sinonjs/fake-timers': 13.0.5 + '@types/node': 22.18.0 + jest-message-util: 30.1.0 + jest-mock: 30.0.5 + jest-util: 30.0.5 + + '@jest/get-type@30.1.0': {} + '@jest/globals@29.7.0': dependencies: '@jest/environment': 29.7.0 @@ -11352,6 +11865,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/globals@30.1.2': + dependencies: + '@jest/environment': 30.1.2 + '@jest/expect': 30.1.2 + '@jest/types': 30.0.5 + jest-mock: 30.0.5 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 22.18.0 + jest-regex-util: 30.0.1 + '@jest/reporters@29.7.0': dependencies: '@bcoe/v8-coverage': 0.2.3 @@ -11381,16 +11908,61 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/reporters@30.1.3': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.1.2 + '@jest/test-result': 30.1.3 + '@jest/transform': 30.1.2 + '@jest/types': 30.0.5 + '@jridgewell/trace-mapping': 0.3.30 + '@types/node': 22.18.0 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit-x: 0.2.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + jest-message-util: 30.1.0 + jest-util: 30.0.5 + jest-worker: 30.1.0 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.41 + + '@jest/snapshot-utils@30.1.2': + dependencies: + '@jest/types': 30.0.5 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + '@jest/source-map@29.6.3': dependencies: '@jridgewell/trace-mapping': 0.3.30 callsites: 3.1.0 graceful-fs: 4.2.11 + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.30 + callsites: 3.1.0 + graceful-fs: 4.2.11 + '@jest/test-result@29.7.0': dependencies: '@jest/console': 29.7.0 @@ -11398,6 +11970,13 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.2 + '@jest/test-result@30.1.3': + dependencies: + '@jest/console': 30.1.2 + '@jest/types': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + '@jest/test-sequencer@29.7.0': dependencies: '@jest/test-result': 29.7.0 @@ -11405,6 +11984,13 @@ snapshots: jest-haste-map: 29.7.0 slash: 3.0.0 + '@jest/test-sequencer@30.1.3': + dependencies: + '@jest/test-result': 30.1.3 + graceful-fs: 4.2.11 + jest-haste-map: 30.1.0 + slash: 3.0.0 + '@jest/transform@29.7.0': dependencies: '@babel/core': 7.28.3 @@ -11425,6 +12011,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/transform@30.1.2': + dependencies: + '@babel/core': 7.28.3 + '@jest/types': 30.0.5 + '@jridgewell/trace-mapping': 0.3.30 + babel-plugin-istanbul: 7.0.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.1.0 + jest-regex-util: 30.0.1 + jest-util: 30.0.5 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + '@jest/types@29.6.3': dependencies: '@jest/schemas': 29.6.3 @@ -11434,6 +12040,16 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jest/types@30.0.5': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.18.0 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -11704,17 +12320,21 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@medusajs/icons@2.10.1(react@19.0.0-rc-66855b96-20241106)': - dependencies: - react: 19.0.0-rc-66855b96-20241106 - '@medusajs/icons@2.10.1(react@19.1.0)': dependencies: react: 19.1.0 - '@medusajs/js-sdk@2.10.1(awilix@8.0.1)': + '@medusajs/icons@2.10.2(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@medusajs/types': 2.10.1(awilix@8.0.1) + react: 19.0.0-rc-66855b96-20241106 + + '@medusajs/icons@2.10.2(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@medusajs/js-sdk@2.10.2(awilix@8.0.1)': + dependencies: + '@medusajs/types': 2.10.2(awilix@8.0.1) fetch-event-stream: 0.1.5 qs: 6.14.0 transitivePeerDependencies: @@ -11722,26 +12342,31 @@ snapshots: - ioredis - vite - '@medusajs/types@2.10.1(awilix@8.0.1)': + '@medusajs/types@2.10.2(awilix@8.0.1)': dependencies: awilix: 8.0.1 bignumber.js: 9.3.0 - '@medusajs/ui-preset@2.10.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2)))': + '@medusajs/ui-preset@2.10.2(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2)))': dependencies: '@tailwindcss/forms': 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2))) tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2)) tailwindcss-animate: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2))) - '@medusajs/ui-preset@2.10.1(tailwindcss@4.1.7)': + '@medusajs/ui-preset@2.10.2(tailwindcss@4.1.7)': dependencies: '@tailwindcss/forms': 0.5.10(tailwindcss@4.1.7) tailwindcss: 4.1.7 tailwindcss-animate: 1.0.7(tailwindcss@4.1.7) - '@medusajs/ui@4.0.21(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.9.2)': + '@medusajs/ui@4.0.22(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.9.2)': dependencies: - '@medusajs/icons': 2.10.1(react@19.0.0-rc-66855b96-20241106) + '@dnd-kit/core': 6.3.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.0.0-rc-66855b96-20241106) + '@dnd-kit/utilities': 3.2.2(react@19.0.0-rc-66855b96-20241106) + '@medusajs/icons': 2.10.2(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-dismissable-layer': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@tanstack/react-table': 8.20.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) clsx: 1.2.1 copy-to-clipboard: 3.3.3 @@ -11761,9 +12386,14 @@ snapshots: - '@types/react-dom' - typescript - '@medusajs/ui@4.0.21(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2)': + '@medusajs/ui@4.0.22(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2)': dependencies: - '@medusajs/icons': 2.10.1(react@19.1.0) + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + '@medusajs/icons': 2.10.2(react@19.1.0) + '@radix-ui/react-dialog': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-table': 8.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) clsx: 1.2.1 copy-to-clipboard: 3.3.3 @@ -12320,6 +12950,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@pkgr/core@0.2.9': {} + '@polka/url@1.0.0-next.29': {} '@prisma/instrumentation@6.11.1(@opentelemetry/api@1.9.0)': @@ -12851,6 +13483,50 @@ snapshots: '@types/react': 19.1.4 '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-dialog@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + aria-hidden: 1.2.6 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-dialog@1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.4)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.1(@types/react@19.1.4)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-dialog@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -12932,6 +13608,32 @@ snapshots: '@types/react': 19.1.4 '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.1.4)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-dismissable-layer@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -17376,6 +18078,8 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@sinclair/typebox@0.34.41': {} + '@sindresorhus/slugify@1.1.2': dependencies: '@sindresorhus/transliterate': 0.1.2 @@ -17394,6 +18098,10 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@13.0.5': + dependencies: + '@sinonjs/commons': 3.0.1 + '@standard-schema/utils@0.3.0': {} '@stripe/react-stripe-js@1.16.5(@stripe/stripe-js@1.54.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': @@ -17657,7 +18365,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 24.3.0 + '@types/node': 22.18.0 '@types/d3-array@3.2.1': {} @@ -17725,6 +18433,11 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jest@30.0.0': + dependencies: + expect: 30.1.2 + pretty-format: 30.0.5 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -17756,7 +18469,7 @@ snapshots: '@types/mysql@2.15.26': dependencies: - '@types/node': 24.3.0 + '@types/node': 22.18.0 '@types/node@17.0.21': {} @@ -17786,7 +18499,7 @@ snapshots: '@types/pg@8.6.1': dependencies: - '@types/node': 24.3.0 + '@types/node': 22.18.0 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -17830,7 +18543,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 24.3.0 + '@types/node': 22.18.0 '@types/unist@2.0.11': {} @@ -18066,6 +18779,8 @@ snapshots: '@typescript-eslint/types': 8.33.1 eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} + '@unrs/resolver-binding-darwin-arm64@1.7.11': optional: true @@ -18468,6 +19183,19 @@ snapshots: transitivePeerDependencies: - supports-color + babel-jest@30.1.2(@babel/core@7.28.3): + dependencies: + '@babel/core': 7.28.3 + '@jest/transform': 30.1.2 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.0.1(@babel/core@7.28.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + babel-loader@8.4.1(@babel/core@7.28.3)(webpack@5.101.3): dependencies: '@babel/core': 7.28.3 @@ -18487,6 +19215,16 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-istanbul@7.0.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + babel-plugin-jest-hoist@29.6.3: dependencies: '@babel/template': 7.27.2 @@ -18494,6 +19232,12 @@ snapshots: '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.7 + babel-plugin-jest-hoist@30.0.1: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + '@types/babel__core': 7.20.5 + babel-plugin-macros@3.1.0: dependencies: '@babel/runtime': 7.27.6 @@ -18529,6 +19273,12 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.3) + babel-preset-jest@30.0.1(@babel/core@7.28.3): + dependencies: + '@babel/core': 7.28.3 + babel-plugin-jest-hoist: 30.0.1 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.3) + balanced-match@1.0.2: {} big.js@5.2.2: {} @@ -18574,6 +19324,10 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.4) + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -18667,8 +19421,12 @@ snapshots: ci-info@3.9.0: {} + ci-info@4.3.0: {} + cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.1.0: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -19198,8 +19956,8 @@ snapshots: '@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.9.2) eslint: 8.10.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.10.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.10.0) eslint-plugin-react: 7.37.5(eslint@8.10.0) eslint-plugin-react-hooks: 5.2.0(eslint@8.10.0) @@ -19218,8 +19976,8 @@ snapshots: '@typescript-eslint/parser': 8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.34.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.34.0(jiti@2.5.1)) @@ -19244,22 +20002,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1 - eslint: 9.34.0(jiti@2.5.1) - get-tsconfig: 4.10.1 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.14 - unrs-resolver: 1.7.11 - optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@8.10.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -19270,33 +20013,48 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.7.11 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.34.0(jiti@2.5.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.1 + eslint: 9.34.0(jiti@2.5.1) + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.14 + unrs-resolver: 1.7.11 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.10.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.9.2) eslint: 8.10.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.10.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.34.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.34.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -19305,9 +20063,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.10.0 + eslint: 9.34.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -19319,13 +20077,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.9.2) + '@typescript-eslint/parser': 8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint@8.10.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -19334,9 +20092,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.34.0(jiti@2.5.1) + eslint: 8.10.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.10.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -19348,7 +20106,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.33.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -19613,6 +20371,8 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + exit-x@0.2.2: {} + exit@0.1.2: {} expect@29.7.0: @@ -19623,6 +20383,15 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + expect@30.1.2: + dependencies: + '@jest/expect-utils': 30.1.2 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.1.2 + jest-message-util: 30.1.0 + jest-mock: 30.0.5 + jest-util: 30.0.5 + facepaint@1.2.1: {} fast-copy@3.0.2: {} @@ -19877,6 +20646,15 @@ snapshots: dependencies: duplexer: 0.1.2 + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -20227,6 +21005,14 @@ snapshots: transitivePeerDependencies: - supports-color + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.30 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + istanbul-reports@3.1.7: dependencies: html-escaper: 2.0.2 @@ -20255,6 +21041,12 @@ snapshots: jest-util: 29.7.0 p-limit: 3.1.0 + jest-changed-files@30.0.5: + dependencies: + execa: 5.1.1 + jest-util: 30.0.5 + p-limit: 3.1.0 + jest-circus@29.7.0(babel-plugin-macros@3.1.0): dependencies: '@jest/environment': 29.7.0 @@ -20281,6 +21073,32 @@ snapshots: - babel-plugin-macros - supports-color + jest-circus@30.1.3(babel-plugin-macros@3.1.0): + dependencies: + '@jest/environment': 30.1.2 + '@jest/expect': 30.1.2 + '@jest/test-result': 30.1.3 + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.6.0(babel-plugin-macros@3.1.0) + is-generator-fn: 2.1.0 + jest-each: 30.1.0 + jest-matcher-utils: 30.1.2 + jest-message-util: 30.1.0 + jest-runtime: 30.1.3 + jest-snapshot: 30.1.2 + jest-util: 30.0.5 + p-limit: 3.1.0 + pretty-format: 30.0.5 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-cli@29.7.0(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) @@ -20300,6 +21118,25 @@ snapshots: - supports-color - ts-node + jest-cli@30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)): + dependencies: + '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) + '@jest/test-result': 30.1.3 + '@jest/types': 30.0.5 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) + jest-util: 30.0.5 + jest-validate: 30.1.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jest-config@29.7.0(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)): dependencies: '@babel/core': 7.28.3 @@ -20331,6 +21168,39 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)): + dependencies: + '@babel/core': 7.28.3 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.1.3 + '@jest/types': 30.0.5 + babel-jest: 30.1.2(@babel/core@7.28.3) + chalk: 4.1.2 + ci-info: 4.3.0 + deepmerge: 4.3.1 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-circus: 30.1.3(babel-plugin-macros@3.1.0) + jest-docblock: 30.0.1 + jest-environment-node: 30.1.2 + jest-regex-util: 30.0.1 + jest-resolve: 30.1.3 + jest-runner: 30.1.3 + jest-util: 30.0.5 + jest-validate: 30.1.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.0.5 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.18.0 + ts-node: 10.9.2(@types/node@22.18.0)(typescript@5.9.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -20338,10 +21208,21 @@ snapshots: jest-get-type: 29.6.3 pretty-format: 29.7.0 + jest-diff@30.1.2: + dependencies: + '@jest/diff-sequences': 30.0.1 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.0.5 + jest-docblock@29.7.0: dependencies: detect-newline: 3.1.0 + jest-docblock@30.0.1: + dependencies: + detect-newline: 3.1.0 + jest-each@29.7.0: dependencies: '@jest/types': 29.6.3 @@ -20350,6 +21231,14 @@ snapshots: jest-util: 29.7.0 pretty-format: 29.7.0 + jest-each@30.1.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.0.5 + chalk: 4.1.2 + jest-util: 30.0.5 + pretty-format: 30.0.5 + jest-environment-node@29.7.0: dependencies: '@jest/environment': 29.7.0 @@ -20359,6 +21248,16 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 + jest-environment-node@30.1.2: + dependencies: + '@jest/environment': 30.1.2 + '@jest/fake-timers': 30.1.2 + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + jest-mock: 30.0.5 + jest-util: 30.0.5 + jest-validate: 30.1.0 + jest-get-type@29.6.3: {} jest-haste-map@29.7.0: @@ -20377,11 +21276,31 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + jest-haste-map@30.1.0: + dependencies: + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.0.5 + jest-worker: 30.1.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + jest-leak-detector@29.7.0: dependencies: jest-get-type: 29.6.3 pretty-format: 29.7.0 + jest-leak-detector@30.1.0: + dependencies: + '@jest/get-type': 30.1.0 + pretty-format: 30.0.5 + jest-matcher-utils@29.7.0: dependencies: chalk: 4.1.2 @@ -20389,6 +21308,13 @@ snapshots: jest-get-type: 29.6.3 pretty-format: 29.7.0 + jest-matcher-utils@30.1.2: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.1.2 + pretty-format: 30.0.5 + jest-message-util@29.7.0: dependencies: '@babel/code-frame': 7.27.1 @@ -20401,18 +21327,42 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 + jest-message-util@30.1.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 30.0.5 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 30.0.5 + slash: 3.0.0 + stack-utils: 2.0.6 + jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 '@types/node': 22.18.0 jest-util: 29.7.0 + jest-mock@30.0.5: + dependencies: + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + jest-util: 30.0.5 + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): optionalDependencies: jest-resolve: 29.7.0 + jest-pnp-resolver@1.2.3(jest-resolve@30.1.3): + optionalDependencies: + jest-resolve: 30.1.3 + jest-regex-util@29.6.3: {} + jest-regex-util@30.0.1: {} + jest-resolve-dependencies@29.7.0: dependencies: jest-regex-util: 29.6.3 @@ -20420,6 +21370,13 @@ snapshots: transitivePeerDependencies: - supports-color + jest-resolve-dependencies@30.1.3: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.1.2 + transitivePeerDependencies: + - supports-color + jest-resolve@29.7.0: dependencies: chalk: 4.1.2 @@ -20432,6 +21389,17 @@ snapshots: resolve.exports: 2.0.3 slash: 3.0.0 + jest-resolve@30.1.3: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.1.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.1.3) + jest-util: 30.0.5 + jest-validate: 30.1.0 + slash: 3.0.0 + unrs-resolver: 1.7.11 + jest-runner@29.7.0: dependencies: '@jest/console': 29.7.0 @@ -20458,6 +21426,33 @@ snapshots: transitivePeerDependencies: - supports-color + jest-runner@30.1.3: + dependencies: + '@jest/console': 30.1.2 + '@jest/environment': 30.1.2 + '@jest/test-result': 30.1.3 + '@jest/transform': 30.1.2 + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.0.1 + jest-environment-node: 30.1.2 + jest-haste-map: 30.1.0 + jest-leak-detector: 30.1.0 + jest-message-util: 30.1.0 + jest-resolve: 30.1.3 + jest-runtime: 30.1.3 + jest-util: 30.0.5 + jest-watcher: 30.1.3 + jest-worker: 30.1.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + jest-runtime@29.7.0: dependencies: '@jest/environment': 29.7.0 @@ -20485,6 +21480,33 @@ snapshots: transitivePeerDependencies: - supports-color + jest-runtime@30.1.3: + dependencies: + '@jest/environment': 30.1.2 + '@jest/fake-timers': 30.1.2 + '@jest/globals': 30.1.2 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.1.3 + '@jest/transform': 30.1.2 + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + chalk: 4.1.2 + cjs-module-lexer: 2.1.0 + collect-v8-coverage: 1.0.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-haste-map: 30.1.0 + jest-message-util: 30.1.0 + jest-mock: 30.0.5 + jest-regex-util: 30.0.1 + jest-resolve: 30.1.3 + jest-snapshot: 30.1.2 + jest-util: 30.0.5 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + jest-snapshot@29.7.0: dependencies: '@babel/core': 7.28.3 @@ -20510,6 +21532,32 @@ snapshots: transitivePeerDependencies: - supports-color + jest-snapshot@30.1.2: + dependencies: + '@babel/core': 7.28.3 + '@babel/generator': 7.28.3 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) + '@babel/types': 7.28.2 + '@jest/expect-utils': 30.1.2 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.1.2 + '@jest/transform': 30.1.2 + '@jest/types': 30.0.5 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.3) + chalk: 4.1.2 + expect: 30.1.2 + graceful-fs: 4.2.11 + jest-diff: 30.1.2 + jest-matcher-utils: 30.1.2 + jest-message-util: 30.1.0 + jest-util: 30.0.5 + pretty-format: 30.0.5 + semver: 7.7.2 + synckit: 0.11.11 + transitivePeerDependencies: + - supports-color + jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 @@ -20519,6 +21567,15 @@ snapshots: graceful-fs: 4.2.11 picomatch: 2.3.1 + jest-util@30.0.5: + dependencies: + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + chalk: 4.1.2 + ci-info: 4.3.0 + graceful-fs: 4.2.11 + picomatch: 4.0.2 + jest-validate@29.7.0: dependencies: '@jest/types': 29.6.3 @@ -20528,6 +21585,15 @@ snapshots: leven: 3.1.0 pretty-format: 29.7.0 + jest-validate@30.1.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.0.5 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.0.5 + jest-watcher@29.7.0: dependencies: '@jest/test-result': 29.7.0 @@ -20539,9 +21605,20 @@ snapshots: jest-util: 29.7.0 string-length: 4.0.2 + jest-watcher@30.1.3: + dependencies: + '@jest/test-result': 30.1.3 + '@jest/types': 30.0.5 + '@types/node': 22.18.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.0.5 + string-length: 4.0.2 + jest-worker@27.5.1: dependencies: - '@types/node': 17.0.21 + '@types/node': 22.18.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -20552,6 +21629,14 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest-worker@30.1.0: + dependencies: + '@types/node': 22.18.0 + '@ungap/structured-clone': 1.3.0 + jest-util: 30.0.5 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jest@29.7.0(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) @@ -20564,6 +21649,19 @@ snapshots: - supports-color - ts-node + jest@30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)): + dependencies: + '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) + '@jest/types': 30.0.5 + import-local: 3.2.0 + jest-cli: 30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jiti@1.21.7: {} jiti@2.5.1: {} @@ -21793,6 +22891,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-format@30.0.5: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + prism-react-renderer@2.4.1(react@19.0.0-rc-66855b96-20241106): dependencies: '@types/prismjs': 1.26.5 @@ -21884,7 +22988,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.3.0 + '@types/node': 22.18.0 long: 5.3.2 proxy-from-env@1.1.0: {} @@ -21898,6 +23002,8 @@ snapshots: pure-rand@6.1.0: {} + pure-rand@7.0.1: {} + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -22890,6 +23996,10 @@ snapshots: picocolors: 1.1.1 sax: 1.4.1 + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + tabbable@6.2.0: {} tailwind-merge@2.6.0: {} @@ -23021,6 +24131,26 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-jest@29.4.2(@babel/core@7.28.3)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.3))(jest-util@30.0.5)(jest@30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)))(typescript@5.9.2): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 30.1.3(@types/node@22.18.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.9.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.3 + '@jest/transform': 30.1.2 + '@jest/types': 30.0.5 + babel-jest: 30.1.2(@babel/core@7.28.3) + jest-util: 30.0.5 + ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -23108,6 +24238,8 @@ snapshots: type-fest@2.19.0: {} + type-fest@4.41.0: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -23153,6 +24285,9 @@ snapshots: typescript@5.9.2: {} + uglify-js@3.19.3: + optional: true + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -23442,6 +24577,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wp-types@4.68.1: dependencies: typescript: 5.9.2 @@ -23465,6 +24602,11 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + write-file-atomic@6.0.0: dependencies: imurmurhash: 0.1.4 From 5c4831c5aff087e170ef48e0f85f060ae7d05dcf Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:12:20 +0300 Subject: [PATCH 24/27] feat(MED-161): add extra log --- lib/services/medipost/medipostXML.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/medipost/medipostXML.service.ts b/lib/services/medipost/medipostXML.service.ts index 335a011..231a92e 100644 --- a/lib/services/medipost/medipostXML.service.ts +++ b/lib/services/medipost/medipostXML.service.ts @@ -145,7 +145,6 @@ export async function composeOrderXML({ ); } - // Find the specimen order numbers for analysis elements const uuringElementInputs: { analysisElement: Tables<{ schema: 'medreport' }, 'analysis_elements'>, specimenOrderNr: number, @@ -156,6 +155,7 @@ export async function composeOrderXML({ for (const material of materials) { const uniqueMaterial = uniqueMaterials.get(material.MaterjaliTyyp); if (!uniqueMaterial) { + console.info(`Unique material not found for material: ${material.MaterjaliTyyp}, analysis element: ${analysisElement.id} ${analysisElement.analysis_id_original} ${analysisElement.analysis_name_lab}`); continue; } uuringElementInputs.push({ From 8a7d88d645ba482b32b356e2cd423012839ad33e Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:28:32 +0300 Subject: [PATCH 25/27] feat(MED-161): no need for default value as it's only used in one place --- lib/services/medipost/medipostXML.service.ts | 2 +- lib/templates/medipost-order.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/services/medipost/medipostXML.service.ts b/lib/services/medipost/medipostXML.service.ts index 231a92e..064aec5 100644 --- a/lib/services/medipost/medipostXML.service.ts +++ b/lib/services/medipost/medipostXML.service.ts @@ -177,7 +177,7 @@ export async function composeOrderXML({ return ` - ${getPais(USER, RECIPIENT, orderId)} + ${getPais(USER, RECIPIENT, orderId, "OL")} ${orderId} ${getClientInstitution()} diff --git a/lib/templates/medipost-order.ts b/lib/templates/medipost-order.ts index 9ca1f55..46a9e8a 100644 --- a/lib/templates/medipost-order.ts +++ b/lib/templates/medipost-order.ts @@ -9,11 +9,8 @@ export const getPais = ( sender: string, recipient: string, orderId: number, - packageName = "OL", + packageName: string, ) => { - if (isProd) { - // return correct data - } return ` ${packageName} ${sender} From 924e89c714b1615b85b2f2eaa1974e5c46093d76 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:47:51 +0300 Subject: [PATCH 26/27] remove console log --- packages/features/user-analyses/src/server/api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/features/user-analyses/src/server/api.ts b/packages/features/user-analyses/src/server/api.ts index 6b74201..a7cc4cd 100644 --- a/packages/features/user-analyses/src/server/api.ts +++ b/packages/features/user-analyses/src/server/api.ts @@ -110,7 +110,6 @@ class UserAnalysesApi { .from('analysis_elements') .select('*') .in('id', nestedAnalysisElementIds); - console.info('analysisResponse nestedAnalysisElementIds', nestedAnalysisElementIds) if (!nestedAnalysisElementsError && nestedAnalysisElements) { for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) { const { results } = mappedOrderedAnalysisElement; From a243bd47694568fa3998055394dee397dd119228 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 17 Sep 2025 11:48:49 +0300 Subject: [PATCH 27/27] improve nested elements null filtering --- packages/features/user-analyses/src/server/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/user-analyses/src/server/api.ts b/packages/features/user-analyses/src/server/api.ts index a7cc4cd..e44faec 100644 --- a/packages/features/user-analyses/src/server/api.ts +++ b/packages/features/user-analyses/src/server/api.ts @@ -103,7 +103,7 @@ class UserAnalysesApi { analysisResponseElements, }); }).sort((a, b) => a.analysisName.localeCompare(b.analysisName)); - const nestedAnalysisElementIds = mappedOrderedAnalysisElements.map(({ results }) => results?.nestedElements.map(({ analysisElementOriginalId }) => analysisElementOriginalId)).flat(); + const nestedAnalysisElementIds = mappedOrderedAnalysisElements.map(({ results }) => results?.nestedElements.map(({ analysisElementOriginalId }) => analysisElementOriginalId)).flat().filter(Boolean); if (nestedAnalysisElementIds.length > 0) { const { data: nestedAnalysisElements, error: nestedAnalysisElementsError } = await this.client .schema('medreport')