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_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 { 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 = {
|
||||
messageId: string;
|
||||
@@ -19,22 +19,23 @@ type GroupedResults = {
|
||||
|
||||
export default async function syncAnalysisResults() {
|
||||
console.info('Syncing analysis results');
|
||||
const supabase = getSupabaseServerClient();
|
||||
const supabase = getSupabaseServerAdminClient();
|
||||
const api = createUserAnalysesApi(supabase);
|
||||
const syncService = new MedipostResultsSyncService();
|
||||
|
||||
const processedMessages: ProcessedMessage[] = [];
|
||||
const excludedMessageIds: string[] = [];
|
||||
while (true) {
|
||||
const result = await readPrivateMessageResponse({ excludedMessageIds });
|
||||
const result = await syncService.readPrivateMessageResponse({ excludedMessageIds });
|
||||
if (result.messageId) {
|
||||
processedMessages.push(result as ProcessedMessage);
|
||||
}
|
||||
|
||||
await api.sendAnalysisResultsNotification({
|
||||
hasFullAnalysisResponse: result.hasFullAnalysisResponse,
|
||||
hasPartialAnalysisResponse: result.hasAnalysisResponse,
|
||||
analysisOrderId: result.analysisOrderId,
|
||||
});
|
||||
// await api.sendAnalysisResultsNotification({
|
||||
// hasFullAnalysisResponse: result.hasFullAnalysisResponse,
|
||||
// hasPartialAnalysisResponse: result.hasAnalysisResponse,
|
||||
// analysisOrderId: result.analysisOrderId,
|
||||
// });
|
||||
|
||||
if (!result.messageId) {
|
||||
console.info('No more messages to process');
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ServiceCategory } from '../../_components/service-categories';
|
||||
async function ttoServicesLoader() {
|
||||
const response = await getProductCategories({
|
||||
fields: '*products, is_active, metadata',
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
const heroCategories = response.product_categories?.filter(
|
||||
|
||||
@@ -7,7 +7,7 @@ async function getAssignedOrderIds() {
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('analysis_order_id')
|
||||
.not('status', 'is', 'COMPLETED')
|
||||
.not('status', 'eq', 'COMPLETED')
|
||||
.not('doctor_user_id', 'is', null)
|
||||
.throwOnError();
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import type { PostgrestError } from '@supabase/supabase-js';
|
||||
|
||||
import { GetMessageListResponse, MedipostAction } from '@/lib/types/medipost';
|
||||
import { MedipostAction } from '@/lib/types/medipost';
|
||||
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
|
||||
import type {
|
||||
MedipostOrderResponse,
|
||||
ResponseUuringuGrupp,
|
||||
UuringElement,
|
||||
} from '@/packages/shared/src/types/medipost-analysis';
|
||||
@@ -14,77 +10,27 @@ import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/se
|
||||
import axios from 'axios';
|
||||
|
||||
import { toArray } from '@kit/shared/utils';
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
|
||||
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
||||
import type { AnalysisOrder } from '~/lib/types/order';
|
||||
|
||||
import { getAccountAdmin } from '../account.service';
|
||||
import { getAnalyses } from '../analyses.service';
|
||||
import { getAnalysisElementsAdmin } from '../analysis-element.service';
|
||||
import {
|
||||
getExistingAnalysisResponseElements,
|
||||
upsertAnalysisResponse,
|
||||
upsertAnalysisResponseElement,
|
||||
} from '../analysis-order.service';
|
||||
import { logMedipostDispatch } from '../audit.service';
|
||||
import { getAnalysisOrder } from '../order.service';
|
||||
import { parseXML } from '../util/xml.service';
|
||||
import { MedipostValidationError } from './MedipostValidationError';
|
||||
import {
|
||||
getLatestMessage,
|
||||
upsertMedipostActionLog,
|
||||
} from './medipostMessageBase.service';
|
||||
import { validateMedipostResponse } from './medipostValidate.service';
|
||||
import { OrderedAnalysisElement, composeOrderXML } from './medipostXML.service';
|
||||
import type { Logger } from './types';
|
||||
|
||||
const BASE_URL = process.env.MEDIPOST_URL!;
|
||||
const USER = process.env.MEDIPOST_USER!;
|
||||
const PASSWORD = process.env.MEDIPOST_PASSWORD!;
|
||||
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({
|
||||
existingElements,
|
||||
groupUuring: {
|
||||
@@ -101,7 +47,7 @@ export async function canCreateAnalysisResponseElement({
|
||||
UuringuElement: Pick<UuringElement, 'UuringOlek' | 'UuringId'>;
|
||||
};
|
||||
responseValue: number | null;
|
||||
log: ReturnType<typeof logger>;
|
||||
log: Logger;
|
||||
}) {
|
||||
const existingAnalysisResponseElement = existingElements.find(
|
||||
({ analysis_element_original_id }) =>
|
||||
@@ -138,7 +84,7 @@ export async function getAnalysisResponseElementsForGroup({
|
||||
AnalysisResponseElement,
|
||||
'analysis_element_original_id' | 'status' | 'response_value'
|
||||
>[];
|
||||
log: ReturnType<typeof logger>;
|
||||
log: Logger;
|
||||
}) {
|
||||
const groupUuringItems = toArray(
|
||||
analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
|
||||
@@ -211,284 +157,6 @@ export async function getAnalysisResponseElementsForGroup({
|
||||
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) {
|
||||
const body = new FormData();
|
||||
body.append('Action', MedipostAction.SendPrivateMessage);
|
||||
@@ -508,27 +176,6 @@ export async function sendPrivateMessage(messageXml: string) {
|
||||
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({
|
||||
medusaOrderId,
|
||||
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();
|
||||
if (error) {
|
||||
console.error("Get analysis order error", error);
|
||||
throw new Error(
|
||||
`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`,
|
||||
);
|
||||
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
removeCartId,
|
||||
setAuthToken,
|
||||
} from './cookies';
|
||||
import { withRetries } from '../util/with-retries';
|
||||
|
||||
const MedusaApiRetriesDefaultConfig = {
|
||||
attempts: 3,
|
||||
baseDelayMs: 1000,
|
||||
};
|
||||
|
||||
export const retrieveCustomer =
|
||||
async (): Promise<HttpTypes.StoreCustomer | null> => {
|
||||
@@ -268,11 +274,21 @@ export const updateCustomerAddress = async (
|
||||
};
|
||||
|
||||
async function medusaLogin(email: string, password: string) {
|
||||
const token = await sdk.auth.login('customer', 'emailpass', {
|
||||
const token = await withRetries(async () => {
|
||||
const loginToken = await sdk.auth.login('customer', 'emailpass', {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
await setAuthToken(token as string);
|
||||
if (typeof loginToken !== 'string') {
|
||||
throw new Error('Failed to login Medusa account');
|
||||
}
|
||||
return loginToken;
|
||||
}, {
|
||||
...MedusaApiRetriesDefaultConfig,
|
||||
label: 'medusa.auth.login',
|
||||
});
|
||||
|
||||
await setAuthToken(token);
|
||||
|
||||
try {
|
||||
await transferCart();
|
||||
@@ -303,20 +319,38 @@ async function medusaRegister({
|
||||
`Creating new Medusa account for Keycloak user with email=${email}`,
|
||||
);
|
||||
|
||||
const registerToken = await sdk.auth.register('customer', 'emailpass', {
|
||||
const registerToken = await withRetries(
|
||||
async () => {
|
||||
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);
|
||||
|
||||
console.info(
|
||||
`Creating new Medusa customer profile for Keycloak user with email=${email} and name=${name} and lastName=${lastName}`,
|
||||
);
|
||||
await withRetries(
|
||||
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 './repeat';
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const { data, error: userError } = await this.client.auth.getUser();
|
||||
if (userError) {
|
||||
throw userError;
|
||||
const analysisOrder = await this.getAnalysisOrder({ analysisOrderId });
|
||||
const userId = analysisOrder.user_id;
|
||||
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 { t } = await createI18nServerInstance();
|
||||
|
||||
@@ -521,7 +534,7 @@ class UserAnalysesApi {
|
||||
|
||||
if (hasFullAnalysisResponse || hasPartialAnalysisResponse) {
|
||||
await notificationsApi.createNotification({
|
||||
account_id: user.id,
|
||||
account_id: accountId,
|
||||
body: t('analysis-results:notification.body'),
|
||||
link: `${pathsConfig.app.analysisResults}/${analysisOrderId}`,
|
||||
});
|
||||
|
||||
@@ -26,8 +26,8 @@ export function getServiceRoleKey() {
|
||||
*/
|
||||
export function warnServiceRoleKeyUsage() {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
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.`,
|
||||
);
|
||||
// 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.`,
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
MEDUSA_ORDER_ID="order_01K1TQQHZGPXKDHAH81TDSNGXR"
|
||||
MEDUSA_ORDER_ID="order_01K9SMB00HJ1W37S1HM0DN2SFV"
|
||||
|
||||
# HOSTNAME="https://test.medreport.ee"
|
||||
# JOBS_API_TOKEN="fd26ec26-70ed-11f0-9e95-431ac3b15a84"
|
||||
@@ -33,7 +33,7 @@ function sync_analysis_groups_store() {
|
||||
# Requirements
|
||||
|
||||
# 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)
|
||||
#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
|
||||
|
||||
# 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
|
||||
# sync_analysis_results
|
||||
|
||||
Reference in New Issue
Block a user