Merge pull request #50 from MR-medreport/MED-105-v3

feat(MED-105): update sending test analysis results
This commit is contained in:
2025-08-18 13:20:54 +03:00
committed by GitHub
12 changed files with 171 additions and 23 deletions

View File

@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
import { getAnalysisOrdersAdmin } 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 { getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
export async function POST(request: NextRequest) {
loadEnv();
try {
validateApiKey(request);
} catch (e) {
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
}
const analysisOrders = await getAnalysisOrdersAdmin({ orderStatus: 'QUEUED' });
console.error(`Sending test responses for ${analysisOrders.length} analysis orders`);
for (const medreportOrder of analysisOrders) {
const medusaOrderId = medreportOrder.medusa_order_id;
const medusaOrder = await retrieveOrder(medusaOrderId)
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
const idsToSend = orderedAnalysisElementsIds;
const messageXml = await composeOrderTestResponseXML({
person: {
idCode: account.personal_code!,
firstName: account.name ?? '',
lastName: account.last_name ?? '',
phone: account.phone ?? '',
},
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: [],
orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at),
});
console.info("SEND XML", messageXml);
try {
await sendPrivateMessageTestResponse({ messageXml });
} catch (error) {
console.error("Error sending private message test response: ", error);
}
}
return NextResponse.json({ success: true });
}

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React, { useMemo } from 'react';
import { ArrowDown } from 'lucide-react'; import { ArrowDown } from 'lucide-react';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
export enum AnalysisResultLevel { export enum AnalysisResultLevel {
VERY_LOW = 0, VERY_LOW = 0,
@@ -17,11 +18,13 @@ const Level = ({
color, color,
isFirst = false, isFirst = false,
isLast = false, isLast = false,
arrowLocation,
}: { }: {
isActive?: boolean; isActive?: boolean;
color: 'destructive' | 'success' | 'warning' | 'gray-200'; color: 'destructive' | 'success' | 'warning' | 'gray-200';
isFirst?: boolean; isFirst?: boolean;
isLast?: boolean; isLast?: boolean;
arrowLocation?: number;
}) => { }) => {
return ( return (
<div <div
@@ -32,7 +35,10 @@ const Level = ({
})} })}
> >
{isActive && ( {isActive && (
<div className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"> <div
className="absolute top-[-14px] left-1/2 -translate-x-1/2 rounded-[10px] bg-white p-[2px]"
style={{ left: `${arrowLocation}%` }}
>
<ArrowDown strokeWidth={2} /> <ArrowDown strokeWidth={2} />
</div> </div>
)} )}
@@ -52,11 +58,33 @@ const AnalysisLevelBar = ({
normLowerIncluded = true, normLowerIncluded = true,
normUpperIncluded = true, normUpperIncluded = true,
level, level,
results,
}: { }: {
normLowerIncluded?: boolean; normLowerIncluded?: boolean;
normUpperIncluded?: boolean; normUpperIncluded?: boolean;
level: AnalysisResultLevel; level: AnalysisResultLevel;
results: UserAnalysisElement;
}) => { }) => {
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]);
return ( return (
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0"> <div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
{normLowerIncluded && ( {normLowerIncluded && (
@@ -73,8 +101,9 @@ const AnalysisLevelBar = ({
<Level <Level
isFirst={!normLowerIncluded} isFirst={!normLowerIncluded}
isLast={!normUpperIncluded} isLast={!normUpperIncluded}
isActive={level === AnalysisResultLevel.NORMAL} color={level === AnalysisResultLevel.NORMAL ? "success" : "warning"}
color="success" isActive
arrowLocation={arrowLocation}
/> />
{normUpperIncluded && ( {normUpperIncluded && (

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts'; import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
import { format } from 'date-fns'; import { format } from 'date-fns';
@@ -39,11 +39,12 @@ const Analysis = ({
const normUpper = results?.norm_upper || 0; const normUpper = results?.norm_upper || 0;
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const isUnderNorm = value < normLower; const analysisResultLevel = useMemo(() => {
const getAnalysisResultLevel = () => {
if (!results) { if (!results) {
return null; return null;
} }
const isUnderNorm = value < normLower;
if (isUnderNorm) { if (isUnderNorm) {
switch (status) { switch (status) {
case AnalysisStatus.MEDIUM: case AnalysisStatus.MEDIUM:
@@ -60,7 +61,7 @@ const Analysis = ({
default: default:
return AnalysisResultLevel.NORMAL; return AnalysisResultLevel.NORMAL;
} }
}; }, [results, value, normLower]);
return ( return (
<div className="border-border flex flex-col items-center justify-between gap-2 rounded-lg border px-5 px-12 py-3 sm:h-[65px] sm:flex-row sm:gap-0"> <div className="border-border flex flex-col items-center justify-between gap-2 rounded-lg border px-5 px-12 py-3 sm:h-[65px] sm:flex-row sm:gap-0">
@@ -99,9 +100,10 @@ const Analysis = ({
</div> </div>
</div> </div>
<AnalysisLevelBar <AnalysisLevelBar
results={results}
normLowerIncluded={normLowerIncluded} normLowerIncluded={normLowerIncluded}
normUpperIncluded={normUpperIncluded} normUpperIncluded={normUpperIncluded}
level={getAnalysisResultLevel()!} level={analysisResultLevel!}
/> />
</> </>
) : ( ) : (

View File

@@ -82,6 +82,7 @@ async function AnalysisResultsPage() {
</div> </div>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
{analysisOrders.length > 0 && analysisElements.length > 0 ? analysisOrders.map((analysisOrder) => { {analysisOrders.length > 0 && analysisElements.length > 0 ? analysisOrders.map((analysisOrder) => {
const analysisResponse = analysisResponses?.find((response) => response.analysis_order_id === analysisOrder.id);
const analysisElementIds = getAnalysisElementIds([analysisOrder]); const analysisElementIds = getAnalysisElementIds([analysisOrder]);
const analysisElementsForOrder = analysisElements.filter((element) => analysisElementIds.includes(element.id)); const analysisElementsForOrder = analysisElements.filter((element) => analysisElementIds.includes(element.id));
return ( return (
@@ -98,7 +99,8 @@ async function AnalysisResultsPage() {
</h5> </h5>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{analysisElementsForOrder.length > 0 ? analysisElementsForOrder.map((analysisElement) => { {analysisElementsForOrder.length > 0 ? analysisElementsForOrder.map((analysisElement) => {
const results = analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original); const results = analysisResponse?.elements.some((element) => element.analysis_element_original_id === analysisElement.analysis_id_original)
&& analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original);
if (!results) { if (!results) {
return ( return (
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} /> <Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} />

View File

@@ -218,12 +218,9 @@ export async function readPrivateMessageResponse({
privateMessage.messageId, privateMessage.messageId,
); );
const messageResponse = privateMessageContent?.Saadetis?.Vastus; const messageResponse = privateMessageContent?.Saadetis?.Vastus;
const medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId; const medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId;
if (!messageResponse) { if (!messageResponse) {
if (medusaOrderId === 'order_01K2JSJXR5XVNRWEAGB199RCKP') {
console.info("messageResponse", JSON.stringify(privateMessageContent, null, 2));
}
throw new Error(`Private message response has no results yet for order=${medusaOrderId}`); throw new Error(`Private message response has no results yet for order=${medusaOrderId}`);
} }

View File

@@ -90,7 +90,7 @@ export async function composeOrderTestResponseXML({
// 1 Järjekorras, 2 Ootel, 3 - Töös, 4 Lõpetatud, // 1 Järjekorras, 2 Ootel, 3 - Töös, 4 Lõpetatud,
// 5 Tagasi lükatud, 6 Tühistatud. // 5 Tagasi lükatud, 6 Tühistatud.
const orderStatus = 4; const orderStatus = 4;
const orderNumber = 'TSU000001200'; const orderNumber = orderId;
const allAnalysisElementsForGroups = analysisElements?.filter((element) => { const allAnalysisElementsForGroups = analysisElements?.filter((element) => {
return analysisGroups.some((group) => group.id === element.analysis_groups.id); return analysisGroups.some((group) => group.id === element.analysis_groups.id);
@@ -153,7 +153,7 @@ export async function composeOrderTestResponseXML({
const lower = getRandomInt(0, 100); const lower = getRandomInt(0, 100);
const upper = getRandomInt(lower + 1, 500); const upper = getRandomInt(lower + 1, 500);
const result = getRandomInt(lower, upper); const result = getRandomInt(lower - Math.floor(lower * 0.1), upper + Math.floor(upper * 0.1));
addedIds.add(relatedAnalysisElement.id); addedIds.add(relatedAnalysisElement.id);
return (` return (`
<UuringuGrupp> <UuringuGrupp>
@@ -175,7 +175,7 @@ export async function composeOrderTestResponseXML({
<VastuseAeg>${formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss')}</VastuseAeg> <VastuseAeg>${formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss')}</VastuseAeg>
<NormYlem kaasaarvatud=\"EI\">${upper}</NormYlem> <NormYlem kaasaarvatud=\"EI\">${upper}</NormYlem>
<NormAlum kaasaarvatud=\"EI\">${lower}</NormAlum> <NormAlum kaasaarvatud=\"EI\">${lower}</NormAlum>
<NormiStaatus>0</NormiStaatus> <NormiStaatus>${result < lower ? 1 : (result > upper ? 1 : 0)}</NormiStaatus>
<ProoviJarjenumber>1</ProoviJarjenumber> <ProoviJarjenumber>1</ProoviJarjenumber>
</UuringuVastus> </UuringuVastus>
</UuringuElement> </UuringuElement>

View File

@@ -108,7 +108,33 @@ export async function getAnalysisOrders({
}: { }: {
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status']; orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
} = {}) { } = {}) {
const query = getSupabaseServerClient() const client = getSupabaseServerClient();
const {
data: { user },
} = await client.auth.getUser();
if (!user) {
throw new Error('Unauthorized');
}
const query = client
.schema('medreport')
.from('analysis_orders')
.select('*')
.eq("user_id", user.id)
if (orderStatus) {
query.eq('status', orderStatus);
}
const orders = await query.order('created_at', { ascending: false }).throwOnError();
return orders.data;
}
export async function getAnalysisOrdersAdmin({
orderStatus,
}: {
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
} = {}) {
const query = getSupabaseServerAdminClient()
.schema('medreport') .schema('medreport')
.from('analysis_orders') .from('analysis_orders')
.select('*') .select('*')

View File

@@ -261,7 +261,7 @@ export const AnalysisOrderStatus = {
6: 'CANCELLED', 6: 'CANCELLED',
} as const; } as const;
export const NormStatus: Record<number, string> = { export const NormStatus: Record<number, string> = {
1: 'NORMAL', 0: 'NORMAL',
2: 'WARNING', 1: 'WARNING',
3: 'REQUIRES_ATTENTION', 2: 'REQUIRES_ATTENTION',
} as const; } as const;

View File

@@ -3,7 +3,7 @@
"previewText": "Your Medreport order has been placed - {{analysisPackageName}}", "previewText": "Your Medreport order has been placed - {{analysisPackageName}}",
"heading": "Your Medreport order has been placed - {{analysisPackageName}}", "heading": "Your Medreport order has been placed - {{analysisPackageName}}",
"hello": "Hello {{personName}},", "hello": "Hello {{personName}},",
"lines1": "The order for {{analysisPackageName}} analysis package has been sent to the lab. Please go to the lab to collect the sample: Medreport - {{partnerLocationName}}", "lines1": "The order for {{analysisPackageName}} analysis package has been sent to the lab. Please go to the lab to collect the sample: Synlab - {{partnerLocationName}}",
"lines2": "<i>If you are unable to go to the lab to collect the sample, you can go to any other suitable collection point - <a href=\"https://medreport.ee/et/verevotupunktid\">view locations and opening hours</a>.</i>", "lines2": "<i>If you are unable to go to the lab to collect the sample, you can go to any other suitable collection point - <a href=\"https://medreport.ee/et/verevotupunktid\">view locations and opening hours</a>.</i>",
"lines3": "It is recommended to collect the sample in the morning (before 12:00) and not to eat or drink (water can be drunk).", "lines3": "It is recommended to collect the sample in the morning (before 12:00) and not to eat or drink (water can be drunk).",
"lines4": "At the collection point, select the order from the queue: the order from the doctor.", "lines4": "At the collection point, select the order from the queue: the order from the doctor.",

View File

@@ -3,7 +3,7 @@
"previewText": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}", "previewText": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
"heading": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}", "heading": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
"hello": "Tere {{personName}},", "hello": "Tere {{personName}},",
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: Medreport - {{partnerLocationName}}", "lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: Synlab - {{partnerLocationName}}",
"lines2": "<i>Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - <a href=\"https://medreport.ee/et/verevotupunktid\">vaata asukohti ja lahtiolekuaegasid</a>.</i>", "lines2": "<i>Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - <a href=\"https://medreport.ee/et/verevotupunktid\">vaata asukohti ja lahtiolekuaegasid</a>.</i>",
"lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).", "lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
"lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.", "lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.",

View File

@@ -0,0 +1,19 @@
-- Enable required extensions for cron jobs and HTTP requests
create extension if not exists pg_cron;
create extension if not exists pg_net;
-- Schedule the test-medipost-responses job to run every 15 minutes
select
cron.schedule(
'send-test-medipost-responses-every-15-minutes', -- Unique job name
'*/15 * * * *', -- Cron schedule: every 15 minutes
$$
select
net.http_post(
url := 'https://test.medreport.ee/api/job/test-medipost-responses',
headers := jsonb_build_object(
'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84'
)
) as request_id;
$$
);

View File

@@ -0,0 +1,19 @@
-- Enable required extensions for cron jobs and HTTP requests
create extension if not exists pg_cron;
create extension if not exists pg_net;
-- Schedule the sync-analysis-results job to run every 15 minutes
select
cron.schedule(
'sync-analysis-results-every-15-minutes', -- Unique job name
'*/15 * * * *', -- Cron schedule: every 15 minutes
$$
select
net.http_post(
url := 'https://test.medreport.ee/api/job/sync-analysis-results',
headers := jsonb_build_object(
'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84'
)
) as request_id;
$$
);