feat(MED-105): update sending test analysis results
This commit is contained in:
54
app/api/job/test-medipost-responses/route.ts
Normal file
54
app/api/job/test-medipost-responses/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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!}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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('*')
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
19
schedule-setup/setup_send_analysis_test_results_cron.sql
Normal file
19
schedule-setup/setup_send_analysis_test_results_cron.sql
Normal 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;
|
||||||
|
$$
|
||||||
|
);
|
||||||
19
schedule-setup/setup_sync_analysis_results_cron.sql
Normal file
19
schedule-setup/setup_sync_analysis_results_cron.sql
Normal 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;
|
||||||
|
$$
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user