Compare commits
51 Commits
3e192c71c5
...
269b4c3e27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
269b4c3e27 | ||
|
|
0878b5d1bd | ||
|
|
6ee2e65938 | ||
|
|
633b6db1af | ||
|
|
452440e8db | ||
| 641ee91c90 | |||
|
|
2aa2ce9ce1 | ||
|
|
48381b2c27 | ||
|
|
2972988211 | ||
| ac80460d93 | |||
|
|
58958c8ace | ||
|
|
b76bdf7622 | ||
|
|
c002eeb74b | ||
|
|
5c171fb930 | ||
|
|
99f20cce39 | ||
|
|
f37d3e19fe | ||
|
|
a43624e559 | ||
|
|
3ad7afe2be | ||
|
|
4919c4fc12 | ||
|
|
b94e633742 | ||
|
|
766e44e5c3 | ||
|
|
0f962ef59a | ||
|
|
a5ddd790f6 | ||
|
|
c07b97fb3a | ||
|
|
0e14428518 | ||
|
|
839882f616 | ||
|
|
c66f71b01c | ||
|
|
6f01f31f22 | ||
|
|
975ee20254 | ||
|
|
6c9ab76439 | ||
|
|
eeea6b0d6f | ||
|
|
ca88387071 | ||
|
|
45f9283e55 | ||
|
|
bac2fba473 | ||
|
|
96d3880229 | ||
|
|
8b41653bb5 | ||
|
|
c0c4f5e3db | ||
|
|
45b77f6291 | ||
|
|
5d4dc97d19 | ||
|
|
cc8c4093ff | ||
| d37a99d7cd | |||
| d941d82373 | |||
| 857044793d | |||
| ccbaba291e | |||
|
|
03ec6eb670 | ||
| 32ce3807e8 | |||
| 90aa6f6fd6 | |||
| be1ba7ef16 | |||
| d3d937dbb2 | |||
| 7baa1d43a2 | |||
| 7a479787c8 |
@@ -59,3 +59,37 @@ MONTONIO_API_URL=https://sandbox-stargate.montonio.com
|
|||||||
|
|
||||||
# JOBS
|
# JOBS
|
||||||
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197
|
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197
|
||||||
|
|
||||||
|
|
||||||
|
MEDUSA_BACKEND_URL=http://5.181.51.38:9000
|
||||||
|
MEDUSA_BACKEND_PUBLIC_URL=http://5.181.51.38:9000
|
||||||
|
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_0ec86252438b38ce18d5601f7877e4395d7e0a6afa8687dfea8d37af33015633
|
||||||
|
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=http://5.181.51.38:54321
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
|
||||||
|
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1Njk4NDYyOCwiZXhwIjoyMDcyNTYwNjI4fQ.1UZR7AqSD9bOy1gtZRGhOCNoESsw2W-DoFDDsNNMwoE
|
||||||
|
|
||||||
|
MEDUSA_BACKEND_URL=https://backoffice-test.medreport.ee
|
||||||
|
MEDUSA_BACKEND_PUBLIC_URL=https://backoffice-test.medreport.ee
|
||||||
|
MEDUSA_SECRET_API_KEY=sk_5ac1c1c12c144cd744b6c881050d459e339ddf6a3d14eda271a0cc4f9d3812cb
|
||||||
|
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_e740b9ca22b31c4b44862044f001dbcf8f46d47d40f430733d0c75bef14d2d6a
|
||||||
|
|
||||||
|
#MEDUSA_BACKEND_URL=https://backoffice.medreport.ee
|
||||||
|
#MEDUSA_BACKEND_PUBLIC_URL=https://backoffice.medreport.ee
|
||||||
|
#MEDUSA_SECRET_API_KEY=sk_fdb1808fbabf62979cc46316aa997378ffbb87882883e8f5c3ee47cee39dcac5
|
||||||
|
#NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_068d930c33fea53608a410d84a51935f6ce2ccec5bef8e0ecf75eaee602ac486
|
||||||
|
|
||||||
|
# PROD
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0NjUyODEyMywiZXhwIjoyMDYyMTA0MTIzfQ.KVcnkZ21Pd0XkJho23dZqFHawVTLQqfvF7l2RxsELLk
|
||||||
|
MEDIPOST_URL=https://medipost2.medisoft.ee:8443/Medipost/MedipostServlet
|
||||||
|
MEDIPOST_USER=medreport
|
||||||
|
MEDIPOST_PASSWORD=85MXFFDB7
|
||||||
|
MEDIPOST_RECIPIENT=HTI
|
||||||
|
MEDIPOST_MESSAGE_SENDER=medreport
|
||||||
|
MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK=false
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||||
|
|
||||||
import { readPrivateMessageResponse } from '~/lib/services/medipost/medipostPrivateMessage.service';
|
import MedipostResultsSyncService from '~/lib/services/medipost/medipostResultsSync.service';
|
||||||
|
|
||||||
type ProcessedMessage = {
|
type ProcessedMessage = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
@@ -19,22 +19,23 @@ type GroupedResults = {
|
|||||||
|
|
||||||
export default async function syncAnalysisResults() {
|
export default async function syncAnalysisResults() {
|
||||||
console.info('Syncing analysis results');
|
console.info('Syncing analysis results');
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerAdminClient();
|
||||||
const api = createUserAnalysesApi(supabase);
|
const api = createUserAnalysesApi(supabase);
|
||||||
|
const syncService = new MedipostResultsSyncService();
|
||||||
|
|
||||||
const processedMessages: ProcessedMessage[] = [];
|
const processedMessages: ProcessedMessage[] = [];
|
||||||
const excludedMessageIds: string[] = [];
|
const excludedMessageIds: string[] = [];
|
||||||
while (true) {
|
while (true) {
|
||||||
const result = await readPrivateMessageResponse({ excludedMessageIds });
|
const result = await syncService.readPrivateMessageResponse({ excludedMessageIds });
|
||||||
if (result.messageId) {
|
if (result.messageId) {
|
||||||
processedMessages.push(result as ProcessedMessage);
|
processedMessages.push(result as ProcessedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.sendAnalysisResultsNotification({
|
// await api.sendAnalysisResultsNotification({
|
||||||
hasFullAnalysisResponse: result.hasFullAnalysisResponse,
|
// hasFullAnalysisResponse: result.hasFullAnalysisResponse,
|
||||||
hasPartialAnalysisResponse: result.hasAnalysisResponse,
|
// hasPartialAnalysisResponse: result.hasAnalysisResponse,
|
||||||
analysisOrderId: result.analysisOrderId,
|
// analysisOrderId: result.analysisOrderId,
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (!result.messageId) {
|
if (!result.messageId) {
|
||||||
console.info('No more messages to process');
|
console.info('No more messages to process');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ServiceCategory } from '../../_components/service-categories';
|
|||||||
async function ttoServicesLoader() {
|
async function ttoServicesLoader() {
|
||||||
const response = await getProductCategories({
|
const response = await getProductCategories({
|
||||||
fields: '*products, is_active, metadata',
|
fields: '*products, is_active, metadata',
|
||||||
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const heroCategories = response.product_categories?.filter(
|
const heroCategories = response.product_categories?.filter(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ async function getAssignedOrderIds() {
|
|||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('doctor_analysis_feedback')
|
.from('doctor_analysis_feedback')
|
||||||
.select('analysis_order_id')
|
.select('analysis_order_id')
|
||||||
.not('status', 'is', 'COMPLETED')
|
.not('status', 'eq', 'COMPLETED')
|
||||||
.not('doctor_user_id', 'is', null)
|
.not('doctor_user_id', 'is', null)
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import type { PostgrestError } from '@supabase/supabase-js';
|
import { MedipostAction } from '@/lib/types/medipost';
|
||||||
|
|
||||||
import { GetMessageListResponse, MedipostAction } from '@/lib/types/medipost';
|
|
||||||
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||||
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
|
|
||||||
import type {
|
import type {
|
||||||
MedipostOrderResponse,
|
|
||||||
ResponseUuringuGrupp,
|
ResponseUuringuGrupp,
|
||||||
UuringElement,
|
UuringElement,
|
||||||
} from '@/packages/shared/src/types/medipost-analysis';
|
} from '@/packages/shared/src/types/medipost-analysis';
|
||||||
@@ -14,77 +10,27 @@ import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/se
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { toArray } from '@kit/shared/utils';
|
import { toArray } from '@kit/shared/utils';
|
||||||
import { Tables } from '@kit/supabase/database';
|
|
||||||
|
|
||||||
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
||||||
import type { AnalysisOrder } from '~/lib/types/order';
|
|
||||||
|
|
||||||
import { getAccountAdmin } from '../account.service';
|
import { getAccountAdmin } from '../account.service';
|
||||||
import { getAnalyses } from '../analyses.service';
|
import { getAnalyses } from '../analyses.service';
|
||||||
import { getAnalysisElementsAdmin } from '../analysis-element.service';
|
import { getAnalysisElementsAdmin } from '../analysis-element.service';
|
||||||
import {
|
|
||||||
getExistingAnalysisResponseElements,
|
|
||||||
upsertAnalysisResponse,
|
|
||||||
upsertAnalysisResponseElement,
|
|
||||||
} from '../analysis-order.service';
|
|
||||||
import { logMedipostDispatch } from '../audit.service';
|
import { logMedipostDispatch } from '../audit.service';
|
||||||
import { getAnalysisOrder } from '../order.service';
|
import { getAnalysisOrder } from '../order.service';
|
||||||
import { parseXML } from '../util/xml.service';
|
|
||||||
import { MedipostValidationError } from './MedipostValidationError';
|
import { MedipostValidationError } from './MedipostValidationError';
|
||||||
import {
|
import {
|
||||||
getLatestMessage,
|
|
||||||
upsertMedipostActionLog,
|
upsertMedipostActionLog,
|
||||||
} from './medipostMessageBase.service';
|
} from './medipostMessageBase.service';
|
||||||
import { validateMedipostResponse } from './medipostValidate.service';
|
import { validateMedipostResponse } from './medipostValidate.service';
|
||||||
import { OrderedAnalysisElement, composeOrderXML } from './medipostXML.service';
|
import { OrderedAnalysisElement, composeOrderXML } from './medipostXML.service';
|
||||||
|
import type { Logger } from './types';
|
||||||
|
|
||||||
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!;
|
||||||
const PASSWORD = process.env.MEDIPOST_PASSWORD!;
|
const PASSWORD = process.env.MEDIPOST_PASSWORD!;
|
||||||
const RECIPIENT = process.env.MEDIPOST_RECIPIENT!;
|
const RECIPIENT = process.env.MEDIPOST_RECIPIENT!;
|
||||||
|
|
||||||
const IS_ENABLED_DELETE_PRIVATE_MESSAGE =
|
|
||||||
process.env.MEDIPOST_ENABLE_DELETE_RESPONSE_PRIVATE_MESSAGE_ON_READ ===
|
|
||||||
'true';
|
|
||||||
|
|
||||||
export async function getLatestPrivateMessageListItem({
|
|
||||||
excludedMessageIds,
|
|
||||||
}: {
|
|
||||||
excludedMessageIds: string[];
|
|
||||||
}) {
|
|
||||||
const { data } = await axios.get<GetMessageListResponse>(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 await 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function canCreateAnalysisResponseElement({
|
export async function canCreateAnalysisResponseElement({
|
||||||
existingElements,
|
existingElements,
|
||||||
groupUuring: {
|
groupUuring: {
|
||||||
@@ -101,7 +47,7 @@ export async function canCreateAnalysisResponseElement({
|
|||||||
UuringuElement: Pick<UuringElement, 'UuringOlek' | 'UuringId'>;
|
UuringuElement: Pick<UuringElement, 'UuringOlek' | 'UuringId'>;
|
||||||
};
|
};
|
||||||
responseValue: number | null;
|
responseValue: number | null;
|
||||||
log: ReturnType<typeof logger>;
|
log: Logger;
|
||||||
}) {
|
}) {
|
||||||
const existingAnalysisResponseElement = existingElements.find(
|
const existingAnalysisResponseElement = existingElements.find(
|
||||||
({ analysis_element_original_id }) =>
|
({ analysis_element_original_id }) =>
|
||||||
@@ -138,7 +84,7 @@ export async function getAnalysisResponseElementsForGroup({
|
|||||||
AnalysisResponseElement,
|
AnalysisResponseElement,
|
||||||
'analysis_element_original_id' | 'status' | 'response_value'
|
'analysis_element_original_id' | 'status' | 'response_value'
|
||||||
>[];
|
>[];
|
||||||
log: ReturnType<typeof logger>;
|
log: Logger;
|
||||||
}) {
|
}) {
|
||||||
const groupUuringItems = toArray(
|
const groupUuringItems = toArray(
|
||||||
analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
|
analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
|
||||||
@@ -211,284 +157,6 @@ export async function getAnalysisResponseElementsForGroup({
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNewAnalysisResponseElements({
|
|
||||||
analysisGroups,
|
|
||||||
existingElements,
|
|
||||||
log,
|
|
||||||
}: {
|
|
||||||
analysisGroups: ResponseUuringuGrupp[];
|
|
||||||
existingElements: AnalysisResponseElement[];
|
|
||||||
log: ReturnType<typeof logger>;
|
|
||||||
}) {
|
|
||||||
const newElements: Omit<
|
|
||||||
AnalysisResponseElement,
|
|
||||||
'created_at' | 'updated_at' | 'id' | 'analysis_response_id'
|
|
||||||
>[] = [];
|
|
||||||
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,
|
|
||||||
}: {
|
|
||||||
analysisResponseId: number;
|
|
||||||
order: Pick<AnalysisOrder, 'analysis_element_ids'>;
|
|
||||||
}) {
|
|
||||||
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<
|
|
||||||
NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>,
|
|
||||||
'ValisTellimuseId' | 'TellimuseNumber' | 'TellimuseOlek' | 'UuringuGrupp'
|
|
||||||
>;
|
|
||||||
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
|
||||||
}) {
|
|
||||||
const supabase = getSupabaseServerAdminClient();
|
|
||||||
|
|
||||||
const orderStatus = AnalysisOrderStatus[TellimuseOlek];
|
|
||||||
|
|
||||||
const log = logger(order, externalId, orderNumber);
|
|
||||||
|
|
||||||
const { data: analysisOrder } = await supabase
|
|
||||||
.schema('medreport')
|
|
||||||
.from('analysis_orders')
|
|
||||||
.select('id, user_id')
|
|
||||||
.eq('id', order.id)
|
|
||||||
.single()
|
|
||||||
.throwOnError();
|
|
||||||
|
|
||||||
const { analysisResponseId } = await upsertAnalysisResponse({
|
|
||||||
analysisOrderId: order.id,
|
|
||||||
orderNumber,
|
|
||||||
orderStatus,
|
|
||||||
userId: analysisOrder.user_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 element of newElements) {
|
|
||||||
try {
|
|
||||||
await upsertAnalysisResponseElement({
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await hasAllAnalysisResponseElements({ analysisResponseId, order }))
|
|
||||||
? { isCompleted: orderStatus === 'COMPLETED' }
|
|
||||||
: { isPartial: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
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 { messageId: privateMessageId } = privateMessage;
|
|
||||||
const { message: privateMessageContent, xml: privateMessageXml } =
|
|
||||||
await getPrivateMessage(privateMessageId);
|
|
||||||
|
|
||||||
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
|
|
||||||
const medipostExternalOrderId =
|
|
||||||
privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId ||
|
|
||||||
messageResponse?.ValisTellimuseId;
|
|
||||||
const patientPersonalCode = messageResponse?.Patsient.Isikukood?.toString();
|
|
||||||
analysisOrderId = Number(medipostExternalOrderId);
|
|
||||||
|
|
||||||
const hasInvalidOrderId = isNaN(analysisOrderId);
|
|
||||||
|
|
||||||
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
|
|
||||||
console.log({
|
|
||||||
privateMessageContent,
|
|
||||||
saadetis: privateMessageContent?.Saadetis,
|
|
||||||
messageResponse,
|
|
||||||
});
|
|
||||||
console.error(
|
|
||||||
`Invalid !order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}`,
|
|
||||||
);
|
|
||||||
await upsertMedipostActionLog({
|
|
||||||
action: 'sync_analysis_results_from_medipost',
|
|
||||||
xml: privateMessageXml,
|
|
||||||
hasAnalysisResults: false,
|
|
||||||
medipostPrivateMessageId: privateMessageId,
|
|
||||||
medusaOrderId,
|
|
||||||
medipostExternalOrderId,
|
|
||||||
hasError: true,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
messageId,
|
|
||||||
hasAnalysisResponse: false,
|
|
||||||
hasPartialAnalysisResponse: false,
|
|
||||||
hasFullAnalysisResponse: false,
|
|
||||||
medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId,
|
|
||||||
analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let analysisOrder: AnalysisOrder;
|
|
||||||
try {
|
|
||||||
analysisOrder = await getAnalysisOrder({ analysisOrderId });
|
|
||||||
medusaOrderId = analysisOrder.medusa_order_id;
|
|
||||||
} catch {
|
|
||||||
if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
|
|
||||||
await deletePrivateMessage(privateMessageId);
|
|
||||||
}
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = await syncPrivateMessage({
|
|
||||||
messageResponse,
|
|
||||||
order: analysisOrder,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
`Successfully synced analysis results from Medipost message privateMessageId=${privateMessageId}`,
|
|
||||||
);
|
|
||||||
await upsertMedipostActionLog({
|
|
||||||
action: 'sync_analysis_results_from_medipost',
|
|
||||||
xml: privateMessageXml,
|
|
||||||
hasAnalysisResults: true,
|
|
||||||
medipostPrivateMessageId: privateMessageId,
|
|
||||||
medusaOrderId,
|
|
||||||
medipostExternalOrderId,
|
|
||||||
});
|
|
||||||
if (status.isPartial) {
|
|
||||||
await createUserAnalysesApi(
|
|
||||||
getSupabaseServerAdminClient(),
|
|
||||||
).updateAnalysisOrderStatus({
|
|
||||||
medusaOrderId,
|
|
||||||
orderStatus: 'PARTIAL_ANALYSIS_RESPONSE',
|
|
||||||
});
|
|
||||||
hasAnalysisResponse = true;
|
|
||||||
hasPartialAnalysisResponse = true;
|
|
||||||
} else if (status.isCompleted) {
|
|
||||||
await createUserAnalysesApi(
|
|
||||||
getSupabaseServerAdminClient(),
|
|
||||||
).updateAnalysisOrderStatus({
|
|
||||||
medusaOrderId,
|
|
||||||
orderStatus: 'FULL_ANALYSIS_RESPONSE',
|
|
||||||
});
|
|
||||||
if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
|
|
||||||
await deletePrivateMessage(privateMessageId);
|
|
||||||
}
|
|
||||||
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) {
|
export async function sendPrivateMessage(messageXml: string) {
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
body.append('Action', MedipostAction.SendPrivateMessage);
|
body.append('Action', MedipostAction.SendPrivateMessage);
|
||||||
@@ -508,27 +176,6 @@ export async function sendPrivateMessage(messageXml: string) {
|
|||||||
await validateMedipostResponse(data);
|
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: (await parseXML(data)) as MedipostOrderResponse,
|
|
||||||
xml: data as string,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendOrderToMedipost({
|
export async function sendOrderToMedipost({
|
||||||
medusaOrderId,
|
medusaOrderId,
|
||||||
orderedAnalysisElements,
|
orderedAnalysisElements,
|
||||||
|
|||||||
389
lib/services/medipost/medipostResultsSync.service.ts
Normal file
389
lib/services/medipost/medipostResultsSync.service.ts
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import type { PostgrestError, SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
import type { Database, Tables } from '@kit/supabase/database';
|
||||||
|
import { toArray } from '@kit/shared/utils';
|
||||||
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
|
|
||||||
|
import type { GetMessageListResponse } from '~/lib/types/medipost';
|
||||||
|
import { MedipostAction } from '~/lib/types/medipost';
|
||||||
|
import type { AnalysisOrder } from "~/lib/types/order";
|
||||||
|
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
||||||
|
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
|
||||||
|
import type {
|
||||||
|
MedipostOrderResponse,
|
||||||
|
ResponseUuringuGrupp,
|
||||||
|
} from '@/packages/shared/src/types/medipost-analysis';
|
||||||
|
import { createUserAnalysesApi } from "@/packages/features/user-analyses/src/server/api";
|
||||||
|
|
||||||
|
import { getAnalysisResponseElementsForGroup } from "./medipostPrivateMessage.service";
|
||||||
|
import { getAnalysisOrder } from "../order.service";
|
||||||
|
import { getLatestMessage, upsertMedipostActionLog } from "./medipostMessageBase.service";
|
||||||
|
import { getAccountAdmin } from "../account.service";
|
||||||
|
import { getExistingAnalysisResponseElements, upsertAnalysisResponse, upsertAnalysisResponseElement } from "../analysis-order.service";
|
||||||
|
import { validateMedipostResponse } from './medipostValidate.service';
|
||||||
|
import { parseXML } from '../util/xml.service';
|
||||||
|
import type { Logger } from './types';
|
||||||
|
|
||||||
|
interface ISyncResult {
|
||||||
|
messageId: string | null;
|
||||||
|
hasAnalysisResponse: boolean;
|
||||||
|
hasPartialAnalysisResponse: boolean;
|
||||||
|
hasFullAnalysisResponse: boolean;
|
||||||
|
medusaOrderId: string | undefined;
|
||||||
|
analysisOrderId: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR_RESPONSE: ISyncResult = {
|
||||||
|
messageId: null,
|
||||||
|
hasAnalysisResponse: false,
|
||||||
|
hasPartialAnalysisResponse: false,
|
||||||
|
hasFullAnalysisResponse: false,
|
||||||
|
medusaOrderId: undefined,
|
||||||
|
analysisOrderId: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BASE_URL = process.env.MEDIPOST_URL!;
|
||||||
|
const USER = process.env.MEDIPOST_USER!;
|
||||||
|
const PASSWORD = process.env.MEDIPOST_PASSWORD!;
|
||||||
|
|
||||||
|
const IS_ENABLED_DELETE_PRIVATE_MESSAGE = false;
|
||||||
|
// process.env.MEDIPOST_ENABLE_DELETE_RESPONSE_PRIVATE_MESSAGE_ON_READ ===
|
||||||
|
// 'true';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class MedipostResultsSyncService {
|
||||||
|
private readonly client: SupabaseClient<Database>;
|
||||||
|
private readonly userAnalysesApi: ReturnType<typeof createUserAnalysesApi>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.client = getSupabaseServerAdminClient();
|
||||||
|
this.userAnalysesApi = createUserAnalysesApi(this.client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async readPrivateMessageResponse({
|
||||||
|
excludedMessageIds,
|
||||||
|
}: {
|
||||||
|
excludedMessageIds: string[];
|
||||||
|
}): Promise<ISyncResult> {
|
||||||
|
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 this.getLatestPrivateMessageListItem({
|
||||||
|
excludedMessageIds,
|
||||||
|
});
|
||||||
|
messageId = privateMessage?.messageId ?? null;
|
||||||
|
|
||||||
|
if (!privateMessage || !messageId) {
|
||||||
|
return ERROR_RESPONSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messageId: privateMessageId } = privateMessage;
|
||||||
|
const { message: privateMessageContent, xml: privateMessageXml } =
|
||||||
|
await this.getPrivateMessage(privateMessageId);
|
||||||
|
|
||||||
|
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
|
||||||
|
if (!messageResponse) {
|
||||||
|
console.info(`Skipping private message id=${privateMessageId} because it has no response`);
|
||||||
|
return ERROR_RESPONSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const medipostExternalOrderId =
|
||||||
|
privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId ||
|
||||||
|
messageResponse?.ValisTellimuseId;
|
||||||
|
|
||||||
|
console.info("PATSIENT", JSON.stringify(messageResponse?.Patsient, null, 2));
|
||||||
|
|
||||||
|
const patientPersonalCode = messageResponse?.Patsient.Isikukood?.toString();
|
||||||
|
analysisOrderId = Number(medipostExternalOrderId);
|
||||||
|
|
||||||
|
const hasInvalidOrderId = isNaN(analysisOrderId);
|
||||||
|
|
||||||
|
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
|
||||||
|
console.log({
|
||||||
|
privateMessageContent,
|
||||||
|
saadetis: privateMessageContent?.Saadetis,
|
||||||
|
messageResponse,
|
||||||
|
});
|
||||||
|
console.error(
|
||||||
|
`Invalid order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}, patientPersonalCode=${patientPersonalCode}`,
|
||||||
|
);
|
||||||
|
// await upsertMedipostActionLog({
|
||||||
|
// action: 'sync_analysis_results_from_medipost',
|
||||||
|
// xml: privateMessageXml,
|
||||||
|
// hasAnalysisResults: false,
|
||||||
|
// medipostPrivateMessageId: privateMessageId,
|
||||||
|
// medusaOrderId,
|
||||||
|
// medipostExternalOrderId,
|
||||||
|
// hasError: true,
|
||||||
|
// });
|
||||||
|
return {
|
||||||
|
...ERROR_RESPONSE,
|
||||||
|
messageId,
|
||||||
|
...(!hasInvalidOrderId && { medusaOrderId, analysisOrderId }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let analysisOrder: AnalysisOrder;
|
||||||
|
try {
|
||||||
|
analysisOrder = await getAnalysisOrder({ analysisOrderId });
|
||||||
|
medusaOrderId = analysisOrder.medusa_order_id;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Get analysis order error", e);
|
||||||
|
await this.deletePrivateMessage(privateMessageId);
|
||||||
|
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}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await this.syncPrivateMessage({
|
||||||
|
messageResponse,
|
||||||
|
order: analysisOrder,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
`Successfully synced analysis results from Medipost message privateMessageId=${privateMessageId}`,
|
||||||
|
);
|
||||||
|
await upsertMedipostActionLog({
|
||||||
|
action: 'sync_analysis_results_from_medipost',
|
||||||
|
xml: privateMessageXml,
|
||||||
|
hasAnalysisResults: true,
|
||||||
|
medipostPrivateMessageId: privateMessageId,
|
||||||
|
medusaOrderId,
|
||||||
|
medipostExternalOrderId,
|
||||||
|
});
|
||||||
|
if (status.isPartial) {
|
||||||
|
await this.userAnalysesApi.updateAnalysisOrderStatus({
|
||||||
|
medusaOrderId,
|
||||||
|
orderStatus: 'PARTIAL_ANALYSIS_RESPONSE',
|
||||||
|
});
|
||||||
|
hasAnalysisResponse = true;
|
||||||
|
hasPartialAnalysisResponse = true;
|
||||||
|
} else if (status.isCompleted) {
|
||||||
|
await this.userAnalysesApi.updateAnalysisOrderStatus({
|
||||||
|
medusaOrderId,
|
||||||
|
orderStatus: 'FULL_ANALYSIS_RESPONSE',
|
||||||
|
});
|
||||||
|
await this.deletePrivateMessage(privateMessageId);
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncPrivateMessage({
|
||||||
|
messageResponse: {
|
||||||
|
ValisTellimuseId: externalId,
|
||||||
|
TellimuseNumber: orderNumber,
|
||||||
|
TellimuseOlek,
|
||||||
|
UuringuGrupp,
|
||||||
|
},
|
||||||
|
order,
|
||||||
|
}: {
|
||||||
|
messageResponse: Pick<
|
||||||
|
NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>,
|
||||||
|
'ValisTellimuseId' | 'TellimuseNumber' | 'TellimuseOlek' | 'UuringuGrupp'
|
||||||
|
>;
|
||||||
|
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||||
|
}) {
|
||||||
|
const orderStatus = AnalysisOrderStatus[TellimuseOlek];
|
||||||
|
|
||||||
|
const log = logger(order, externalId, orderNumber);
|
||||||
|
|
||||||
|
const { data: analysisOrder } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_orders')
|
||||||
|
.select('id, user_id')
|
||||||
|
.eq('id', order.id)
|
||||||
|
.single()
|
||||||
|
.throwOnError();
|
||||||
|
|
||||||
|
console.info("ANALYSIS ORDER", JSON.stringify(analysisOrder, null, 2));
|
||||||
|
throw new Error("early return");
|
||||||
|
|
||||||
|
const { analysisResponseId } = await upsertAnalysisResponse({
|
||||||
|
analysisOrderId: order.id,
|
||||||
|
orderNumber,
|
||||||
|
orderStatus,
|
||||||
|
userId: analysisOrder.user_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingElements = await getExistingAnalysisResponseElements({
|
||||||
|
analysisResponseId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const analysisGroups = toArray(UuringuGrupp);
|
||||||
|
log(`Order has results for ${analysisGroups.length} analysis groups`);
|
||||||
|
const newElements = await this.getNewAnalysisResponseElements({
|
||||||
|
analysisGroups,
|
||||||
|
existingElements,
|
||||||
|
log,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const element of newElements) {
|
||||||
|
try {
|
||||||
|
await upsertAnalysisResponseElement({
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await this.hasAllAnalysisResponseElements({ analysisResponseId, order }))
|
||||||
|
? { isCompleted: orderStatus === 'COMPLETED' }
|
||||||
|
: { isPartial: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLatestPrivateMessageListItem({
|
||||||
|
excludedMessageIds,
|
||||||
|
}: {
|
||||||
|
excludedMessageIds: string[];
|
||||||
|
}) {
|
||||||
|
const { data } = await axios.get<GetMessageListResponse>(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 await getLatestMessage({
|
||||||
|
messages: data?.messages,
|
||||||
|
excludedMessageIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNewAnalysisResponseElements({
|
||||||
|
analysisGroups,
|
||||||
|
existingElements,
|
||||||
|
log,
|
||||||
|
}: {
|
||||||
|
analysisGroups: ResponseUuringuGrupp[];
|
||||||
|
existingElements: AnalysisResponseElement[];
|
||||||
|
log: Logger;
|
||||||
|
}) {
|
||||||
|
const newElements: Omit<
|
||||||
|
AnalysisResponseElement,
|
||||||
|
'created_at' | 'updated_at' | 'id' | 'analysis_response_id'
|
||||||
|
>[] = [];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hasAllAnalysisResponseElements({
|
||||||
|
analysisResponseId,
|
||||||
|
order,
|
||||||
|
}: {
|
||||||
|
analysisResponseId: number;
|
||||||
|
order: Pick<AnalysisOrder, 'analysis_element_ids'>;
|
||||||
|
}) {
|
||||||
|
const allOrderResponseElements = await getExistingAnalysisResponseElements({
|
||||||
|
analysisResponseId,
|
||||||
|
});
|
||||||
|
const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
|
||||||
|
return allOrderResponseElements.length >= expectedOrderResponseElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deletePrivateMessage(messageId: string) {
|
||||||
|
if (!IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
|
||||||
|
console.info(`Skipping delete private message id=${messageId} because deleting is not enabled`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async 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: (await parseXML(data)) as MedipostOrderResponse,
|
||||||
|
xml: data as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
3
lib/services/medipost/types.ts
Normal file
3
lib/services/medipost/types.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { PostgrestError } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
export type Logger = (message: string, error?: PostgrestError | null) => void;
|
||||||
@@ -72,6 +72,7 @@ export async function getAnalysisOrder({
|
|||||||
|
|
||||||
const { data: order, error } = await query.single();
|
const { data: order, error } = await query.single();
|
||||||
if (error) {
|
if (error) {
|
||||||
|
console.error("Get analysis order error", error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`,
|
`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ import {
|
|||||||
removeCartId,
|
removeCartId,
|
||||||
setAuthToken,
|
setAuthToken,
|
||||||
} from './cookies';
|
} from './cookies';
|
||||||
|
import { withRetries } from '../util/with-retries';
|
||||||
|
|
||||||
|
const MedusaApiRetriesDefaultConfig = {
|
||||||
|
attempts: 3,
|
||||||
|
baseDelayMs: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
export const retrieveCustomer =
|
export const retrieveCustomer =
|
||||||
async (): Promise<HttpTypes.StoreCustomer | null> => {
|
async (): Promise<HttpTypes.StoreCustomer | null> => {
|
||||||
@@ -268,11 +274,21 @@ export const updateCustomerAddress = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function medusaLogin(email: string, password: string) {
|
async function medusaLogin(email: string, password: string) {
|
||||||
const token = await sdk.auth.login('customer', 'emailpass', {
|
const token = await withRetries(async () => {
|
||||||
email,
|
const loginToken = await sdk.auth.login('customer', 'emailpass', {
|
||||||
password,
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
if (typeof loginToken !== 'string') {
|
||||||
|
throw new Error('Failed to login Medusa account');
|
||||||
|
}
|
||||||
|
return loginToken;
|
||||||
|
}, {
|
||||||
|
...MedusaApiRetriesDefaultConfig,
|
||||||
|
label: 'medusa.auth.login',
|
||||||
});
|
});
|
||||||
await setAuthToken(token as string);
|
|
||||||
|
await setAuthToken(token);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await transferCart();
|
await transferCart();
|
||||||
@@ -303,20 +319,38 @@ async function medusaRegister({
|
|||||||
`Creating new Medusa account for Keycloak user with email=${email}`,
|
`Creating new Medusa account for Keycloak user with email=${email}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const registerToken = await sdk.auth.register('customer', 'emailpass', {
|
const registerToken = await withRetries(
|
||||||
email,
|
async () => {
|
||||||
password,
|
const token = await sdk.auth.register('customer', 'emailpass', {
|
||||||
});
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
if (typeof token !== 'string') {
|
||||||
|
throw new Error('Failed to register Medusa account');
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...MedusaApiRetriesDefaultConfig,
|
||||||
|
label: 'medusa.auth.register',
|
||||||
|
},
|
||||||
|
);
|
||||||
await setAuthToken(registerToken);
|
await setAuthToken(registerToken);
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
`Creating new Medusa customer profile for Keycloak user with email=${email} and name=${name} and lastName=${lastName}`,
|
`Creating new Medusa customer profile for Keycloak user with email=${email} and name=${name} and lastName=${lastName}`,
|
||||||
);
|
);
|
||||||
await sdk.store.customer.create(
|
await withRetries(
|
||||||
{ email, first_name: name, last_name: lastName },
|
async () => {
|
||||||
{},
|
await sdk.store.customer.create(
|
||||||
|
{ email, first_name: name, last_name: lastName },
|
||||||
|
{},
|
||||||
|
{ ...(await getAuthHeaders()) },
|
||||||
|
);
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...(await getAuthHeaders()),
|
...MedusaApiRetriesDefaultConfig,
|
||||||
|
label: 'medusa.customer.create',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export * from './money';
|
|||||||
export * from './product';
|
export * from './product';
|
||||||
export * from './repeat';
|
export * from './repeat';
|
||||||
export * from './sort-products';
|
export * from './sort-products';
|
||||||
|
export * from './with-retries';
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
type RetryOptions = {
|
||||||
|
attempts?: number;
|
||||||
|
baseDelayMs?: number;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function sleep(delay: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withRetries<T>(
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
{ attempts = 3, baseDelayMs = 500, label }: RetryOptions = {}
|
||||||
|
): Promise<T> {
|
||||||
|
let lastError: unknown;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
|
||||||
|
if (attempt === attempts) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = baseDelayMs * 2 ** (attempt - 1);
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
console.warn(
|
||||||
|
`Retrying ${label}, attempt ${attempt + 1}/${attempts} in ${delay}ms`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
await sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError instanceof Error
|
||||||
|
? lastError
|
||||||
|
: new Error("Operation failed after retries");
|
||||||
|
}
|
||||||
@@ -507,11 +507,24 @@ class UserAnalysesApi {
|
|||||||
if (!analysisOrderId) {
|
if (!analysisOrderId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { data, error: userError } = await this.client.auth.getUser();
|
const analysisOrder = await this.getAnalysisOrder({ analysisOrderId });
|
||||||
if (userError) {
|
const userId = analysisOrder.user_id;
|
||||||
throw userError;
|
const { data: account } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
|
.from('accounts')
|
||||||
|
.select('id')
|
||||||
|
.eq('primary_owner_user_id', userId)
|
||||||
|
.maybeSingle()
|
||||||
|
.throwOnError();
|
||||||
|
|
||||||
|
const accountId = account?.id;
|
||||||
|
if (!accountId) {
|
||||||
|
console.warn(
|
||||||
|
`Order ${analysisOrderId} got new responses but no account found for user_id=${userId}. Skipping notification. AnalysisOrder=${JSON.stringify(analysisOrder)}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const { user } = data;
|
|
||||||
const notificationsApi = createNotificationsApi(this.client);
|
const notificationsApi = createNotificationsApi(this.client);
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
@@ -521,7 +534,7 @@ class UserAnalysesApi {
|
|||||||
|
|
||||||
if (hasFullAnalysisResponse || hasPartialAnalysisResponse) {
|
if (hasFullAnalysisResponse || hasPartialAnalysisResponse) {
|
||||||
await notificationsApi.createNotification({
|
await notificationsApi.createNotification({
|
||||||
account_id: user.id,
|
account_id: accountId,
|
||||||
body: t('analysis-results:notification.body'),
|
body: t('analysis-results:notification.body'),
|
||||||
link: `${pathsConfig.app.analysisResults}/${analysisOrderId}`,
|
link: `${pathsConfig.app.analysisResults}/${analysisOrderId}`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export function getServiceRoleKey() {
|
|||||||
*/
|
*/
|
||||||
export function warnServiceRoleKeyUsage() {
|
export function warnServiceRoleKeyUsage() {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
console.warn(
|
// console.warn(
|
||||||
`[Dev Only] This is a simple warning to let you know you are using the Supabase Service Role. Make sure it's the right call.`,
|
// `[Dev Only] This is a simple warning to let you know you are using the Supabase Service Role. Make sure it's the right call.`,
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
run-test-sync-local.sh
Normal file → Executable file
6
run-test-sync-local.sh
Normal file → Executable file
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
MEDUSA_ORDER_ID="order_01K1TQQHZGPXKDHAH81TDSNGXR"
|
MEDUSA_ORDER_ID="order_01K9SMB00HJ1W37S1HM0DN2SFV"
|
||||||
|
|
||||||
# HOSTNAME="https://test.medreport.ee"
|
# HOSTNAME="https://test.medreport.ee"
|
||||||
# JOBS_API_TOKEN="fd26ec26-70ed-11f0-9e95-431ac3b15a84"
|
# JOBS_API_TOKEN="fd26ec26-70ed-11f0-9e95-431ac3b15a84"
|
||||||
@@ -33,7 +33,7 @@ function sync_analysis_groups_store() {
|
|||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
# 1. Sync analysis groups from Medipost to B2B
|
# 1. Sync analysis groups from Medipost to B2B
|
||||||
sync_analysis_groups
|
#sync_analysis_groups
|
||||||
|
|
||||||
# 2. Optional - sync all Medipost analysis groups from B2B to Medusa (or add manually)
|
# 2. Optional - sync all Medipost analysis groups from B2B to Medusa (or add manually)
|
||||||
#sync_analysis_groups_store
|
#sync_analysis_groups_store
|
||||||
@@ -41,7 +41,7 @@ sync_analysis_groups
|
|||||||
# 3. Set up products configurations in Medusa so B2B "Telli analüüs" page shows the product and you can do payment flow
|
# 3. Set up products configurations in Medusa so B2B "Telli analüüs" page shows the product and you can do payment flow
|
||||||
|
|
||||||
# 4. After payment is done, run `send_medipost_test_response` to send the fake test results to Medipost
|
# 4. After payment is done, run `send_medipost_test_response` to send the fake test results to Medipost
|
||||||
# send_medipost_test_response
|
send_medipost_test_response
|
||||||
|
|
||||||
# 5. Run `sync_analysis_results` to sync the all new Medipost results to B2B
|
# 5. Run `sync_analysis_results` to sync the all new Medipost results to B2B
|
||||||
# sync_analysis_results
|
# sync_analysis_results
|
||||||
|
|||||||
Reference in New Issue
Block a user