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:
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user