Merge pull request #98 from MR-medreport/MED-168

feat(MED-168): keep medipost response data unique, no need for duplicate rows
This commit is contained in:
2025-09-18 16:59:07 +03:00
committed by GitHub
7 changed files with 110 additions and 38 deletions

View File

@@ -3,7 +3,7 @@ import { getAnalysisOrder } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service"; import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service";
import { retrieveOrder } from "@lib/data"; import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service"; import { getAccountAdmin } from "~/lib/services/account.service";
import { createMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service"; import { upsertMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service"; import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
export async function POST(request: Request) { export async function POST(request: Request) {
@@ -35,7 +35,7 @@ export async function POST(request: Request) {
}); });
try { try {
await createMedipostActionLog({ await upsertMedipostActionLog({
action: 'send_fake_analysis_results_to_medipost', action: 'send_fake_analysis_results_to_medipost',
xml: messageXml, xml: messageXml,
medusaOrderId, medusaOrderId,

View File

@@ -60,19 +60,14 @@ const Analysis = ({
const unit = results?.unit || ''; const unit = results?.unit || '';
const normLower = results?.normLower; const normLower = results?.normLower;
const normUpper = results?.normUpper; const normUpper = results?.normUpper;
const normStatus = results?.normStatus ?? null;
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const analysisResultLevel = useMemo(() => { const analysisResultLevel = useMemo(() => {
if (!results) { if (normStatus === null) {
return null; return null;
} }
if (results.responseValue === null || results.responseValue === undefined) {
return null;
}
const normStatus = results.normStatus;
switch (normStatus) { switch (normStatus) {
case 1: case 1:
return AnalysisResultLevel.WARNING; return AnalysisResultLevel.WARNING;
@@ -82,12 +77,13 @@ const Analysis = ({
default: default:
return AnalysisResultLevel.NORMAL; return AnalysisResultLevel.NORMAL;
} }
}, [results]); }, [normStatus]);
const isCancelled = Number(results?.status) === 5; const isCancelled = Number(results?.status) === 5;
const hasNestedElements = results?.nestedElements.length > 0; const hasNestedElements = results?.nestedElements.length > 0;
const normRangeText = normLower !== null ? `${normLower} - ${normUpper || ''}` : null; const normRangeText = normLower !== null ? `${normLower} - ${normUpper || ''}` : null;
const hasTextualResponse = hasIsNegative || hasIsWithinNorm;
return ( return (
<div className="border-border rounded-lg border px-5"> <div className="border-border rounded-lg border px-5">
@@ -127,10 +123,18 @@ const Analysis = ({
{isCancelled || !results || hasNestedElements ? null : ( {isCancelled || !results || hasNestedElements ? null : (
<> <>
<div className="flex items-center gap-3 sm:ml-auto"> <div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">{value}</div> <div
className={cn('font-semibold', {
'text-yellow-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.WARNING,
'text-red-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.CRITICAL,
'text-green-600': hasTextualResponse && analysisResultLevel === AnalysisResultLevel.NORMAL,
})}
>
{value}
</div>
<div className="text-muted-foreground text-sm">{unit}</div> <div className="text-muted-foreground text-sm">{unit}</div>
</div> </div>
{!(hasIsNegative || hasIsWithinNorm) && ( {!hasTextualResponse && (
<> <>
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0"> <div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
{normRangeText} {normRangeText}

View File

@@ -139,7 +139,7 @@ const big1: AnalysisTestResponse = {
"unit": null, "unit": null,
"normLower": null, "normLower": null,
"normUpper": 2, "normUpper": 2,
"normStatus": 0, "normStatus": 2,
"responseTime": "2024-02-29T10:13:01+00:00", "responseTime": "2024-02-29T10:13:01+00:00",
"responseValue": null, "responseValue": null,
"responseValueIsNegative": null, "responseValueIsNegative": null,
@@ -150,6 +150,26 @@ const big1: AnalysisTestResponse = {
"analysisElementOriginalId": "59156-0" "analysisElementOriginalId": "59156-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": true,
"normLowerIncluded": false,
"normUpperIncluded": false,
"status": "4",
"analysisElementOriginalId": "59156-0"
}
},
{ {
"analysisIdOriginal": "13955-0", "analysisIdOriginal": "13955-0",
"isWaitingForResults": false, "isWaitingForResults": false,

View File

@@ -18,16 +18,32 @@ export async function getExistingAnalysisResponseElements({
return data as AnalysisResponseElement[]; return data as AnalysisResponseElement[];
} }
export async function createAnalysisResponseElement({ export async function upsertAnalysisResponseElement({
element, element,
}: { }: {
element: Omit<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id'>; element: Omit<AnalysisResponseElement, 'created_at' | 'updated_at' | 'id'>;
}) { }) {
await getSupabaseServerAdminClient() const { data } = await getSupabaseServerAdminClient()
.schema('medreport') .schema('medreport')
.from('analysis_response_elements') .from('analysis_response_elements')
.insert(element) .upsert(
element,
{
onConflict: 'analysis_response_id,analysis_element_original_id',
ignoreDuplicates: false
}
)
.select('id')
.throwOnError(); .throwOnError();
const analysisResponseElementId = data?.[0]?.id;
if (!analysisResponseElementId) {
throw new Error(
`Failed to insert or update analysis response element (response id: ${element.analysis_response_id}, element id: ${element.analysis_element_original_id})`
);
}
return { analysisResponseElementId };
} }
export async function upsertAnalysisResponse({ export async function upsertAnalysisResponse({

View File

@@ -27,7 +27,7 @@ export async function getLatestMessage({
); );
} }
export async function createMedipostActionLog({ export async function upsertMedipostActionLog({
action, action,
xml, xml,
hasAnalysisResults = false, hasAnalysisResults = false,
@@ -40,8 +40,7 @@ export async function createMedipostActionLog({
action: action:
| 'send_order_to_medipost' | 'send_order_to_medipost'
| 'sync_analysis_results_from_medipost' | 'sync_analysis_results_from_medipost'
| 'send_fake_analysis_results_to_medipost' | 'send_fake_analysis_results_to_medipost';
| 'send_analysis_results_to_medipost';
xml: string; xml: string;
hasAnalysisResults?: boolean; hasAnalysisResults?: boolean;
medusaOrderId?: string | null; medusaOrderId?: string | null;
@@ -50,19 +49,34 @@ export async function createMedipostActionLog({
medipostExternalOrderId?: string | null; medipostExternalOrderId?: string | null;
medipostPrivateMessageId?: string | null; medipostPrivateMessageId?: string | null;
}) { }) {
await getSupabaseServerAdminClient() const { data } = await getSupabaseServerAdminClient()
.schema('medreport') .schema('medreport')
.from('medipost_actions') .from('medipost_actions')
.insert({ .upsert(
action, {
xml, action,
has_analysis_results: hasAnalysisResults, xml,
medusa_order_id: medusaOrderId, has_analysis_results: hasAnalysisResults,
response_xml: responseXml, medusa_order_id: medusaOrderId,
has_error: hasError, response_xml: responseXml,
medipost_external_order_id: medipostExternalOrderId, has_error: hasError,
medipost_private_message_id: medipostPrivateMessageId, medipost_external_order_id: medipostExternalOrderId,
}) medipost_private_message_id: medipostPrivateMessageId,
},
{
onConflict: 'medipost_private_message_id',
ignoreDuplicates: false
}
)
.select('id') .select('id')
.throwOnError(); .throwOnError();
const medipostActionId = data?.[0]?.id;
if (!medipostActionId) {
throw new Error(
`Failed to insert or update medipost action (private message id: ${medipostPrivateMessageId})`
);
}
return { medipostActionId };
} }

View File

@@ -21,7 +21,7 @@ import { Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
import { getAnalysisElementsAdmin } from '../analysis-element.service'; import { getAnalysisElementsAdmin } from '../analysis-element.service';
import { getAnalyses } from '../analyses.service'; import { getAnalyses } from '../analyses.service';
import { createMedipostActionLog, getLatestMessage } from './medipostMessageBase.service'; import { upsertMedipostActionLog, getLatestMessage } from './medipostMessageBase.service';
import { validateMedipostResponse } from './medipostValidate.service'; import { validateMedipostResponse } from './medipostValidate.service';
import { getAnalysisOrder, updateAnalysisOrderStatus } from '../order.service'; import { getAnalysisOrder, updateAnalysisOrderStatus } from '../order.service';
import { parseXML } from '../util/xml.service'; import { parseXML } from '../util/xml.service';
@@ -29,7 +29,7 @@ import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service';
import { getAccountAdmin } from '../account.service'; import { getAccountAdmin } from '../account.service';
import { logMedipostDispatch } from '../audit.service'; import { logMedipostDispatch } from '../audit.service';
import { MedipostValidationError } from './MedipostValidationError'; import { MedipostValidationError } from './MedipostValidationError';
import { createAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service'; import { upsertAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service';
const BASE_URL = process.env.MEDIPOST_URL!; const BASE_URL = process.env.MEDIPOST_URL!;
const USER = process.env.MEDIPOST_USER!; const USER = process.env.MEDIPOST_USER!;
@@ -242,7 +242,7 @@ export async function syncPrivateMessage({
for (const element of newElements) { for (const element of newElements) {
try { try {
await createAnalysisResponseElement({ await upsertAnalysisResponseElement({
element: { element: {
...element, ...element,
analysis_response_id: analysisResponseId, analysis_response_id: analysisResponseId,
@@ -305,7 +305,7 @@ export async function readPrivateMessageResponse({
const hasInvalidOrderId = isNaN(analysisOrderId); const hasInvalidOrderId = isNaN(analysisOrderId);
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) { if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
await createMedipostActionLog({ await upsertMedipostActionLog({
action: 'sync_analysis_results_from_medipost', action: 'sync_analysis_results_from_medipost',
xml: privateMessageXml, xml: privateMessageXml,
hasAnalysisResults: false, hasAnalysisResults: false,
@@ -342,7 +342,7 @@ export async function readPrivateMessageResponse({
const status = await syncPrivateMessage({ messageResponse, order: analysisOrder }); const status = await syncPrivateMessage({ messageResponse, order: analysisOrder });
await createMedipostActionLog({ await upsertMedipostActionLog({
action: 'sync_analysis_results_from_medipost', action: 'sync_analysis_results_from_medipost',
xml: privateMessageXml, xml: privateMessageXml,
hasAnalysisResults: true, hasAnalysisResults: true,
@@ -475,7 +475,7 @@ export async function sendOrderToMedipost({
isMedipostError, isMedipostError,
errorMessage: e.response, errorMessage: e.response,
}); });
await createMedipostActionLog({ await upsertMedipostActionLog({
action: 'send_order_to_medipost', action: 'send_order_to_medipost',
xml: orderXml, xml: orderXml,
hasAnalysisResults: false, hasAnalysisResults: false,
@@ -489,7 +489,7 @@ export async function sendOrderToMedipost({
isSuccess: false, isSuccess: false,
isMedipostError, isMedipostError,
}); });
await createMedipostActionLog({ await upsertMedipostActionLog({
action: 'send_order_to_medipost', action: 'send_order_to_medipost',
xml: orderXml, xml: orderXml,
hasAnalysisResults: false, hasAnalysisResults: false,
@@ -505,7 +505,7 @@ export async function sendOrderToMedipost({
isSuccess: true, isSuccess: true,
isMedipostError: false, isMedipostError: false,
}); });
await createMedipostActionLog({ await upsertMedipostActionLog({
action: 'send_order_to_medipost', action: 'send_order_to_medipost',
xml: orderXml, xml: orderXml,
hasAnalysisResults: false, hasAnalysisResults: false,

View File

@@ -0,0 +1,18 @@
-- Add unique constraint on analysis_response_elements by analysis_response_id + analysis_element_original_id
CREATE UNIQUE INDEX analysis_response_elements_unique_by_response_and_element
ON "medreport"."analysis_response_elements"
USING btree (analysis_response_id, analysis_element_original_id);
ALTER TABLE "medreport"."analysis_response_elements"
ADD CONSTRAINT "analysis_response_elements_unique_by_response_and_element"
UNIQUE USING INDEX "analysis_response_elements_unique_by_response_and_element";
-- Add updated_at column to medipost_actions table
ALTER TABLE "medreport"."medipost_actions" ADD COLUMN "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now();
-- Add unique constraint on medipost_actions by medipost_private_message_id
-- Using partial index to allow multiple NULL values but enforce uniqueness for non-NULL values
CREATE UNIQUE INDEX medipost_actions_unique_by_private_message_id
ON "medreport"."medipost_actions"
USING btree (medipost_private_message_id)
WHERE medipost_private_message_id IS NOT NULL;