Merge pull request #40 from MR-medreport/MED-131-v2
feat(MED-131): update analysis/package -> cart -> medipost flow, many fixes/improvements
This commit is contained in:
3
.env
3
.env
@@ -73,3 +73,6 @@ MONTONIO_API_URL=https://sandbox-stargate.montonio.com
|
||||
# MEDUSA
|
||||
MEDUSA_BACKEND_URL=http://localhost:9000
|
||||
MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000
|
||||
|
||||
# JOBS
|
||||
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197
|
||||
|
||||
@@ -23,3 +23,5 @@ NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo
|
||||
MONTONIO_API_URL=https://sandbox-stargate.montonio.com
|
||||
|
||||
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197
|
||||
|
||||
38
README.md
38
README.md
@@ -100,3 +100,41 @@ To access admin pages follow these steps:
|
||||
|
||||
- [View emails](http://localhost:1080/#/)
|
||||
- Mail server is running on `localhost:1025` without password
|
||||
|
||||
## Medipost flow
|
||||
|
||||
1. Customer adds analysis to cart in **B2B** storefront
|
||||
2. Customer checks out from cart and is redirected to **Montonio**
|
||||
3. Customer pays and is redirected back to **B2B** `GET B2B/home/cart/montonio-callback?order-token=$JWT`
|
||||
- **Medusa** order is created and cart is emptied
|
||||
- email is sent to customer
|
||||
- B2B sends order XML as private message to Medipost.
|
||||
When **Montonio** has confirmed payment, it will call **Medusa** webhook endpoint and **Medusa** will mark order payment as captured.
|
||||
|
||||
In background a job will call `POST B2B/api/job/sync-analysis-results` every n minutes and sync private messages with responses from **Medipost**.
|
||||
|
||||
In local dev environment, you can create a private message with analysis responses in **Medipost** system for a submitted order:
|
||||
`POST B2B/api/order/medipost-test-response body={medusaOrderId:'input here'}`
|
||||
After that run `POST /api/job/sync-analysis-results` and analysis results should be synced.
|
||||
|
||||
In local dev environment, you can import products from B2B to Medusa with this API:
|
||||
|
||||
- `POST /api/job/sync-analysis-groups-store`
|
||||
- Syncs required data of `analyses`, `analysis_elements` data from **B2B** to **Medusa** and creates relevant products and categories.
|
||||
If product or category already exists, then it is not recreated. Old entries are not deleted either currently.
|
||||
|
||||
## Jobs
|
||||
|
||||
Required headers:
|
||||
|
||||
- `x-jobs-api-key` in UUID format
|
||||
|
||||
Endpoints:
|
||||
|
||||
- `POST /api/job/sync-analysis-groups`
|
||||
- Queries **Medipost** for public messages list and takes analysis info from the latest event.
|
||||
Updates `analyses` and `analysis_elements` lists in **B2B**.
|
||||
- `POST /api/job/sync-analysis-results`
|
||||
- Queries **Medipost** for latest private message that has a response and updates order analysis results from lab results data.
|
||||
- `POST /api/job/sync-connected-online`
|
||||
- TODO
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cache } from 'react';
|
||||
|
||||
import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { getAccount } from '~/lib/services/account.service';
|
||||
|
||||
interface Params {
|
||||
params: Promise<{
|
||||
@@ -28,21 +28,4 @@ async function AccountPage(props: Params) {
|
||||
|
||||
export default AdminGuard(AccountPage);
|
||||
|
||||
const loadAccount = cache(accountLoader);
|
||||
|
||||
async function accountLoader(id: string) {
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data, error } = await client
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('*, memberships: accounts_memberships (*)')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
const loadAccount = cache(getAccount);
|
||||
|
||||
8
app/api/job/handler/load-env.ts
Normal file
8
app/api/job/handler/load-env.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { config } from 'dotenv';
|
||||
|
||||
export default function loadEnv() {
|
||||
config({ path: `.env` });
|
||||
if (['local', 'test', 'development', 'production'].includes(process.env.NODE_ENV!)) {
|
||||
config({ path: `.env.${process.env.NODE_ENV}` });
|
||||
}
|
||||
}
|
||||
241
app/api/job/handler/sync-analysis-groups-store.ts
Normal file
241
app/api/job/handler/sync-analysis-groups-store.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import Medusa from "@medusajs/js-sdk"
|
||||
import type { AdminProductCategory } from "@medusajs/types";
|
||||
import { listProductTypes } from "@lib/data/products";
|
||||
import { getAnalysisElements } from "~/lib/services/analysis-element.service";
|
||||
import { getAnalysisGroups } from "~/lib/services/analysis-group.service";
|
||||
import { createMedusaSyncFailEntry, createMedusaSyncSuccessEntry } from "~/lib/services/analyses.service";
|
||||
|
||||
const SYNLAB_SERVICES_CATEGORY_HANDLE = 'synlab-services';
|
||||
const SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE = 'synlab-analysis';
|
||||
|
||||
const BASE_ANALYSIS_PRODUCT_HANDLE = 'analysis-base';
|
||||
|
||||
const getAdminSdk = () => {
|
||||
const medusaBackendUrl = process.env.MEDUSA_BACKEND_PUBLIC_URL!;
|
||||
const medusaPublishableApiKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!;
|
||||
const key = process.env.MEDUSA_SECRET_API_KEY!;
|
||||
|
||||
if (!medusaBackendUrl || !medusaPublishableApiKey) {
|
||||
throw new Error('Medusa environment variables not set');
|
||||
}
|
||||
return new Medusa({
|
||||
baseUrl: medusaBackendUrl,
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
apiKey: key,
|
||||
});
|
||||
}
|
||||
|
||||
async function createProductCategories({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const { product_categories: existingProductCategories } = await medusa.admin.productCategory.list();
|
||||
const parentCategory = existingProductCategories.find(({ handle }) => handle === SYNLAB_SERVICES_CATEGORY_HANDLE);
|
||||
|
||||
if (!parentCategory) {
|
||||
throw new Error('Parent category not found');
|
||||
}
|
||||
|
||||
const analysisGroups = await getAnalysisGroups();
|
||||
if (!analysisGroups) {
|
||||
throw new Error('Analysis groups not found');
|
||||
}
|
||||
|
||||
const createdCategories: AdminProductCategory[] = [];
|
||||
for (const analysisGroup of analysisGroups) {
|
||||
console.info(`Processing analysis group '${analysisGroup.name}'`);
|
||||
|
||||
const isExisting = existingProductCategories.find(({ name }) => name === analysisGroup.name);
|
||||
const isNewlyCreated = createdCategories.find(({ name }) => name === analysisGroup.name);
|
||||
if (isExisting || isNewlyCreated) {
|
||||
console.info(`Analysis group '${analysisGroup.name}' already exists`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const createResponse = await medusa.admin.productCategory.create({
|
||||
name: analysisGroup.name,
|
||||
handle: analysisGroup.name,
|
||||
parent_category_id: parentCategory.id,
|
||||
is_active: true,
|
||||
metadata: {
|
||||
analysisGroupOriginalId: analysisGroup.original_id,
|
||||
analysisGroupId: analysisGroup.id,
|
||||
},
|
||||
});
|
||||
console.info(`Successfully created category, id=${createResponse.product_category.id}`);
|
||||
createdCategories.push(createResponse.product_category);
|
||||
}
|
||||
}
|
||||
|
||||
async function getChildProductCategories({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const { product_categories: allCategories } = await medusa.admin.productCategory.list();
|
||||
const childCategories = allCategories.filter(({ parent_category_id }) => parent_category_id !== null);
|
||||
return childCategories;
|
||||
}
|
||||
|
||||
async function deleteProductCategories({
|
||||
medusa,
|
||||
categories,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
categories: AdminProductCategory[];
|
||||
}) {
|
||||
for (const category of categories) {
|
||||
await medusa.admin.productCategory.delete(category.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In case a reset is needed
|
||||
*/
|
||||
async function deleteProducts({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const { products: existingProducts } = await medusa.admin.product.list({
|
||||
fields: 'id,collection_id',
|
||||
limit: 1000,
|
||||
});
|
||||
|
||||
await Promise.all(existingProducts.filter((a) => !a.collection_id).map(({ id }) => medusa.admin.product.delete(id)));
|
||||
}
|
||||
|
||||
async function getAnalysisPackagesType() {
|
||||
const { productTypes } = await listProductTypes();
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === SYNLAB_ANALYSIS_PRODUCT_TYPE_HANDLE);
|
||||
if (!analysisPackagesType) {
|
||||
throw new Error('Synlab analysis packages type not found');
|
||||
}
|
||||
return analysisPackagesType;
|
||||
}
|
||||
|
||||
async function getProductDefaultFields({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const baseProductsResponse = await medusa.admin.product.list({ handle: BASE_ANALYSIS_PRODUCT_HANDLE })
|
||||
const baseProduct = baseProductsResponse.products[0];
|
||||
if (!baseProduct) {
|
||||
throw new Error('Base product not found');
|
||||
}
|
||||
const defaultSalesChannels = baseProduct.sales_channels;
|
||||
if (!Array.isArray(defaultSalesChannels)) {
|
||||
throw new Error('Base analysis product has no required sales channels');
|
||||
}
|
||||
const defaultProductOption = baseProduct.options;
|
||||
if (!Array.isArray(defaultProductOption)) {
|
||||
throw new Error('Base analysis product has no required options');
|
||||
}
|
||||
const defaultProductVariant = baseProduct.variants?.[0];
|
||||
if (!defaultProductVariant) {
|
||||
throw new Error('Base analysis product has no required variant');
|
||||
}
|
||||
|
||||
return {
|
||||
defaultSalesChannels,
|
||||
defaultProductOption,
|
||||
defaultProductVariant,
|
||||
}
|
||||
}
|
||||
|
||||
async function createProducts({
|
||||
medusa,
|
||||
}: {
|
||||
medusa: Medusa;
|
||||
}) {
|
||||
const { product_categories: allCategories } = await medusa.admin.productCategory.list();
|
||||
|
||||
const [
|
||||
{ products: existingProducts },
|
||||
analysisElements,
|
||||
analysisPackagesType,
|
||||
{
|
||||
defaultSalesChannels,
|
||||
defaultProductOption,
|
||||
defaultProductVariant,
|
||||
}
|
||||
] = await Promise.all([
|
||||
medusa.admin.product.list({
|
||||
category_id: allCategories.map(({ id }) => id),
|
||||
}),
|
||||
getAnalysisElements({}),
|
||||
getAnalysisPackagesType(),
|
||||
getProductDefaultFields({ medusa }),
|
||||
])
|
||||
|
||||
for (const analysisElement of analysisElements) {
|
||||
const { analysis_id_original: originalId } = analysisElement;
|
||||
const isExisting = existingProducts.find(({ metadata }) => metadata?.analysisIdOriginal === originalId);
|
||||
if (isExisting) {
|
||||
console.info(`Analysis element '${analysisElement.analysis_name_lab}' already exists`);
|
||||
continue;
|
||||
}
|
||||
const { analysis_name_lab: name } = analysisElement;
|
||||
if (!name) {
|
||||
console.error(`Analysis element '${originalId}' has no name`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = allCategories.find(({ metadata }) => metadata?.analysisGroupId === analysisElement.parent_analysis_group_id);
|
||||
if (!category) {
|
||||
console.error(`Category not found for analysis element '${name}'`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await medusa.admin.product.create({
|
||||
title: name,
|
||||
handle: `analysis-element-${analysisElement.id}`,
|
||||
categories: [{ id: category.id }],
|
||||
options: defaultProductOption.map(({ id, title, values }) => ({
|
||||
id,
|
||||
title,
|
||||
values: values?.map(({ value }) => value) ?? [],
|
||||
})),
|
||||
metadata: {
|
||||
analysisIdOriginal: originalId,
|
||||
},
|
||||
is_giftcard: false,
|
||||
discountable: false,
|
||||
status: 'published',
|
||||
sales_channels: defaultSalesChannels.map(({ id }) => ({ id })),
|
||||
variants: [
|
||||
{
|
||||
title: defaultProductVariant.title!,
|
||||
prices: defaultProductVariant.prices!,
|
||||
manage_inventory: false,
|
||||
},
|
||||
],
|
||||
type_id: analysisPackagesType.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default async function syncAnalysisGroupsStore() {
|
||||
const medusa = getAdminSdk();
|
||||
|
||||
try {
|
||||
await createProductCategories({ medusa });
|
||||
|
||||
// const categories = await getChildProductCategories({ medusa });
|
||||
// await deleteProductCategories({ medusa, categories });
|
||||
// await deleteProducts({ medusa });
|
||||
// return;
|
||||
|
||||
await createProducts({ medusa });
|
||||
|
||||
await createMedusaSyncSuccessEntry();
|
||||
} catch (e) {
|
||||
await createMedusaSyncFailEntry(JSON.stringify(e));
|
||||
console.error(e);
|
||||
throw new Error(
|
||||
`Failed to sync analyses to Medusa, error: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
173
app/api/job/handler/sync-analysis-groups.ts
Normal file
173
app/api/job/handler/sync-analysis-groups.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import axios from 'axios';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import fs from 'fs';
|
||||
import { createAnalysisGroup, getAnalysisGroups } from '~/lib/services/analysis-group.service';
|
||||
import { IMedipostPublicMessageDataParsed } from '~/lib/services/medipost.types';
|
||||
import { createAnalysis, createNoDataReceivedEntry, createNoNewDataReceivedEntry, createSyncFailEntry, createSyncSuccessEntry } from '~/lib/services/analyses.service';
|
||||
import { getLastCheckedDate } from '~/lib/services/sync-entries.service';
|
||||
import { createAnalysisElement } from '~/lib/services/analysis-element.service';
|
||||
import { createCodes } from '~/lib/services/codes.service';
|
||||
import { getLatestPublicMessageListItem } from '~/lib/services/medipost.service';
|
||||
import type { ICode } from '~/lib/types/code';
|
||||
|
||||
function toArray<T>(input?: T | T[] | null): T[] {
|
||||
if (!input) return [];
|
||||
return Array.isArray(input) ? input : [input];
|
||||
}
|
||||
|
||||
const WRITE_XML_TO_FILE = false as boolean;
|
||||
|
||||
export default async function syncAnalysisGroups() {
|
||||
const baseUrl = process.env.MEDIPOST_URL;
|
||||
const user = process.env.MEDIPOST_USER;
|
||||
const password = process.env.MEDIPOST_PASSWORD;
|
||||
const sender = process.env.MEDIPOST_MESSAGE_SENDER;
|
||||
|
||||
if (!baseUrl || !user || !password || !sender) {
|
||||
throw new Error('Could not access all necessary environment variables');
|
||||
}
|
||||
|
||||
try {
|
||||
console.info('Getting latest public message id');
|
||||
const lastCheckedDate = await getLastCheckedDate();
|
||||
|
||||
const latestMessage = await getLatestPublicMessageListItem();
|
||||
if (!latestMessage) {
|
||||
console.info('No new data received');
|
||||
await createNoNewDataReceivedEntry();
|
||||
return;
|
||||
}
|
||||
console.info('Getting public message with id: ', latestMessage.messageId);
|
||||
|
||||
const { data: publicMessageData } = await axios.get(baseUrl, {
|
||||
params: {
|
||||
Action: 'GetPublicMessage',
|
||||
User: user,
|
||||
Password: password,
|
||||
MessageId: latestMessage.messageId,
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/xml',
|
||||
},
|
||||
});
|
||||
|
||||
if (WRITE_XML_TO_FILE) {
|
||||
fs.writeFileSync('public-messages-list-response.xml', publicMessageData);
|
||||
}
|
||||
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
const parsed: IMedipostPublicMessageDataParsed = parser.parse(publicMessageData);
|
||||
|
||||
if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get public message (id: ${latestMessage.messageId})`,
|
||||
);
|
||||
}
|
||||
|
||||
const existingAnalysisGroups = await getAnalysisGroups();
|
||||
|
||||
// SAVE PUBLIC MESSAGE DATA
|
||||
|
||||
const providers = toArray(parsed?.Saadetis?.Teenused.Teostaja);
|
||||
const analysisGroups = providers.flatMap((provider) =>
|
||||
toArray(provider.UuringuGrupp),
|
||||
);
|
||||
|
||||
if (!parsed || !analysisGroups.length) {
|
||||
console.info('No analysis groups data received');
|
||||
await createNoDataReceivedEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
const codes: ICode[] = [];
|
||||
for (const analysisGroup of analysisGroups) {
|
||||
const existingAnalysisGroup = existingAnalysisGroups?.find(({ original_id }) => original_id === analysisGroup.UuringuGruppId);
|
||||
if (existingAnalysisGroup) {
|
||||
console.info(`Analysis group '${analysisGroup.UuringuGruppNimi}' already exists`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// SAVE ANALYSIS GROUP
|
||||
const analysisGroupId = await createAnalysisGroup({
|
||||
id: analysisGroup.UuringuGruppId,
|
||||
name: analysisGroup.UuringuGruppNimi,
|
||||
order: analysisGroup.UuringuGruppJarjekord,
|
||||
});
|
||||
|
||||
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
||||
codes.push(
|
||||
...analysisGroupCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: analysisGroupId,
|
||||
analysis_element_id: null,
|
||||
analysis_id: null,
|
||||
})),
|
||||
);
|
||||
|
||||
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
||||
|
||||
for (const item of analysisGroupItems) {
|
||||
const analysisElement = item.UuringuElement;
|
||||
|
||||
const insertedAnalysisElementId = await createAnalysisElement({
|
||||
analysisElement,
|
||||
analysisGroupId,
|
||||
materialGroups: toArray(item.MaterjalideGrupp),
|
||||
});
|
||||
|
||||
if (analysisElement.Kood) {
|
||||
const analysisElementCodes = toArray(analysisElement.Kood);
|
||||
codes.push(
|
||||
...analysisElementCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: null,
|
||||
analysis_element_id: insertedAnalysisElementId,
|
||||
analysis_id: null,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const analyses = analysisElement.UuringuElement;
|
||||
if (analyses?.length) {
|
||||
for (const analysis of analyses) {
|
||||
const insertedAnalysisId = await createAnalysis(analysis, analysisGroupId);
|
||||
|
||||
if (analysis.Kood) {
|
||||
const analysisCodes = toArray(analysis.Kood);
|
||||
|
||||
codes.push(
|
||||
...analysisCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: null,
|
||||
analysis_element_id: null,
|
||||
analysis_id: insertedAnalysisId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Inserting codes');
|
||||
await createCodes(codes);
|
||||
|
||||
console.info('Inserting sync entry');
|
||||
await createSyncSuccessEntry();
|
||||
} catch (e) {
|
||||
await createSyncFailEntry(JSON.stringify(e));
|
||||
console.error(e);
|
||||
throw new Error(
|
||||
`Failed to sync public message data, error: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
29
app/api/job/handler/sync-analysis-results.ts
Normal file
29
app/api/job/handler/sync-analysis-results.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { readPrivateMessageResponse } from "~/lib/services/medipost.service";
|
||||
|
||||
export default async function syncAnalysisResults() {
|
||||
console.info("Syncing analysis results");
|
||||
|
||||
let processedMessageIds: string[] = [];
|
||||
const excludedMessageIds: string[] = [];
|
||||
while (true) {
|
||||
console.info("Fetching private messages");
|
||||
const { messageIdErrored, messageIdProcessed } = await readPrivateMessageResponse({ excludedMessageIds });
|
||||
if (messageIdProcessed) {
|
||||
processedMessageIds.push(messageIdProcessed);
|
||||
}
|
||||
|
||||
if (!messageIdErrored) {
|
||||
console.info("No more messages to process");
|
||||
break;
|
||||
}
|
||||
|
||||
if (excludedMessageIds.includes(messageIdErrored)) {
|
||||
console.info(`Message id=${messageIdErrored} has already been processed, stopping`);
|
||||
break;
|
||||
}
|
||||
|
||||
excludedMessageIds.push(messageIdErrored);
|
||||
}
|
||||
|
||||
console.info(`Processed ${processedMessageIds.length} messages, ids: ${processedMessageIds.join(', ')}`);
|
||||
}
|
||||
@@ -1,45 +1,29 @@
|
||||
import { createClient as createCustomClient } from '@supabase/supabase-js';
|
||||
|
||||
import axios from 'axios';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
async function syncData() {
|
||||
if (process.env.NODE_ENV === 'local') {
|
||||
config({ path: `.env.${process.env.NODE_ENV}` });
|
||||
}
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
import type { ISearchLoadResponse } from '~/lib/types/connected-online';
|
||||
|
||||
export default async function syncConnectedOnline() {
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const baseUrl = process.env.CONNECTED_ONLINE_URL;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
if (!baseUrl || !supabaseUrl || !supabaseServiceRoleKey) {
|
||||
if (!baseUrl) {
|
||||
throw new Error('Could not access all necessary environment variables');
|
||||
}
|
||||
|
||||
const supabase = createCustomClient(supabaseUrl, supabaseServiceRoleKey, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
const supabase = getSupabaseServerAdminClient();
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${baseUrl}/Search_Load`, {
|
||||
const response = await axios.post<{ d: string }>(`${baseUrl}/Search_Load`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
param: "{'Value':'|et|-1'}", // get all available services in Estonian
|
||||
});
|
||||
|
||||
const responseData: {
|
||||
Value: any;
|
||||
Data: any;
|
||||
ErrorCode: number;
|
||||
ErrorMessage: string;
|
||||
} = JSON.parse(response.data.d);
|
||||
const responseData: ISearchLoadResponse = JSON.parse(response.data.d);
|
||||
|
||||
if (responseData?.ErrorCode !== 0) {
|
||||
throw new Error('Failed to get Connected Online data');
|
||||
@@ -147,5 +131,3 @@ async function syncData() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
syncData();
|
||||
9
app/api/job/handler/validate-api-key.ts
Normal file
9
app/api/job/handler/validate-api-key.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export default function validateApiKey(request: NextRequest) {
|
||||
const envApiKey = process.env.JOBS_API_TOKEN;
|
||||
const requestApiKey = request.headers.get('x-jobs-api-key');
|
||||
if (requestApiKey !== envApiKey) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
}
|
||||
27
app/api/job/sync-analysis-groups-store/route.ts
Normal file
27
app/api/job/sync-analysis-groups-store/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
import syncAnalysisGroupsStore from "../handler/sync-analysis-groups-store";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
loadEnv();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await syncAnalysisGroupsStore();
|
||||
console.info("Successfully synced analysis groups store");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully synced analysis groups store',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error("Error syncing analysis groups store", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to sync analysis groups store',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
27
app/api/job/sync-analysis-groups/route.ts
Normal file
27
app/api/job/sync-analysis-groups/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import syncAnalysisGroups from "../handler/sync-analysis-groups";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
loadEnv();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await syncAnalysisGroups();
|
||||
console.info("Successfully synced analysis groups");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully synced analysis groups',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error("Error syncing analysis groups", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to sync analysis groups',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
27
app/api/job/sync-analysis-results/route.ts
Normal file
27
app/api/job/sync-analysis-results/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
import syncAnalysisResults from "../handler/sync-analysis-results";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
loadEnv();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await syncAnalysisResults();
|
||||
console.info("Successfully synced analysis results");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully synced analysis results',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error("Error syncing analysis results", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to sync analysis results',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
27
app/api/job/sync-connected-online/route.ts
Normal file
27
app/api/job/sync-connected-online/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
import syncConnectedOnline from "../handler/sync-connected-online";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
loadEnv();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await syncConnectedOnline();
|
||||
console.info("Successfully synced connected-online");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully synced connected-online',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error("Error syncing connected-online", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to sync connected-online',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
8
app/api/order/medipost-create/route.ts
Normal file
8
app/api/order/medipost-create/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { sendOrderToMedipost } from "~/lib/services/medipost.service";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const { medusaOrderId } = (await request.json()) as { medusaOrderId: string };
|
||||
await sendOrderToMedipost({ medusaOrderId });
|
||||
return NextResponse.json({ success: true });
|
||||
};
|
||||
43
app/api/order/medipost-test-response/route.ts
Normal file
43
app/api/order/medipost-test-response/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getOrder } from "~/lib/services/order.service";
|
||||
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
|
||||
import { retrieveOrder } from "@lib/data";
|
||||
import { getAccountAdmin } from "~/lib/services/account.service";
|
||||
import { getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
if (!isDev) {
|
||||
return NextResponse.json({ error: 'This endpoint is only available in development mode' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { medusaOrderId } = await request.json();
|
||||
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId)
|
||||
const medreportOrder = await getOrder({ medusaOrderId });
|
||||
|
||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||
|
||||
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
|
||||
const messageXml = await composeOrderTestResponseXML({
|
||||
person: {
|
||||
idCode: account.personal_code!,
|
||||
firstName: account.name ?? '',
|
||||
lastName: account.last_name ?? '',
|
||||
phone: account.phone ?? '',
|
||||
},
|
||||
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId),
|
||||
orderedAnalysesIds: [],
|
||||
orderId: medusaOrderId,
|
||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||
});
|
||||
|
||||
try {
|
||||
await sendPrivateMessageTestResponse({ messageXml });
|
||||
} catch (error) {
|
||||
console.error("Error sending private message test response: ", error);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -40,7 +40,7 @@ async function VerifyPage(props: Props) {
|
||||
}
|
||||
|
||||
const nextPath = (await props.searchParams).next;
|
||||
const redirectPath = nextPath ?? pathsConfig.app.home;
|
||||
const redirectPath = !!nextPath && nextPath.length > 0 ? nextPath : pathsConfig.app.home;
|
||||
|
||||
return (
|
||||
<MultiFactorChallengeContainer
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Fragment } from 'react';
|
||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||
import { withI18n } from '@/lib/i18n/with-i18n';
|
||||
|
||||
@@ -28,7 +29,11 @@ async function AnalysisResultsPage() {
|
||||
<Trans i18nKey="account:analysisResults.pageTitle" />
|
||||
</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey="account:analysisResults.description" />
|
||||
{analysisList && analysisList.length > 0 ? (
|
||||
<Trans i18nKey="account:analysisResults.description" />
|
||||
) : (
|
||||
<Trans i18nKey="account:analysisResults.descriptionEmpty" />
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
@@ -36,20 +41,24 @@ async function AnalysisResultsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{analysisList?.map((analysis, index) => (
|
||||
<Analysis
|
||||
key={index}
|
||||
analysis={{
|
||||
name: analysis.element.analysis_name || '',
|
||||
status: analysis.element.norm_status as AnalysisStatus,
|
||||
unit: analysis.element.unit || '',
|
||||
value: analysis.element.response_value,
|
||||
normLowerIncluded: !!analysis.element.norm_lower_included,
|
||||
normUpperIncluded: !!analysis.element.norm_upper_included,
|
||||
normLower: analysis.element.norm_lower || 0,
|
||||
normUpper: analysis.element.norm_upper || 0,
|
||||
}}
|
||||
/>
|
||||
{analysisList?.map((analysis) => (
|
||||
<Fragment key={analysis.id}>
|
||||
{analysis.elements.map((element) => (
|
||||
<Analysis
|
||||
key={element.id}
|
||||
analysis={{
|
||||
name: element.analysis_name || '',
|
||||
status: element.norm_status as AnalysisStatus,
|
||||
unit: element.unit || '',
|
||||
value: element.response_value,
|
||||
normLowerIncluded: !!element.norm_lower_included,
|
||||
normUpperIncluded: !!element.norm_upper_included,
|
||||
normLower: element.norm_lower || 0,
|
||||
normUpper: element.norm_upper || 0,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</PageBody>
|
||||
|
||||
@@ -3,12 +3,22 @@ import { z } from "zod";
|
||||
import { MontonioOrderToken } from "@/app/home/(user)/_components/cart/types";
|
||||
import { loadCurrentUserAccount } from "@/app/home/(user)/_lib/server/load-user-account";
|
||||
import { listProductTypes } from "@lib/data/products";
|
||||
import { placeOrder } from "@lib/data/cart";
|
||||
import { placeOrder, retrieveCart } from "@lib/data/cart";
|
||||
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
||||
import { createOrder } from '~/lib/services/order.service';
|
||||
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
|
||||
|
||||
const emailSender = process.env.EMAIL_SENDER;
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL!;
|
||||
|
||||
const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
|
||||
const MONTONIO_PAID_STATUS = 'PAID';
|
||||
|
||||
/**
|
||||
* This is needed locally, because Montonio doesn't accept "localhost" redirect/notification URLs
|
||||
*/
|
||||
const LOCAL_MONTONIO_REDIRECT_FAKE_ORIGIN = 'webhook.site';
|
||||
|
||||
const env = z
|
||||
.object({
|
||||
emailSender: z
|
||||
@@ -62,38 +72,50 @@ const handleOrderToken = async (orderToken: string) => {
|
||||
const decoded = jwt.verify(orderToken, secretKey, {
|
||||
algorithms: ['HS256'],
|
||||
}) as MontonioOrderToken;
|
||||
if (decoded.paymentStatus !== 'PAID') {
|
||||
if (decoded.paymentStatus !== MONTONIO_PAID_STATUS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const [, , cartId] = decoded.merchantReferenceDisplay.split(':');
|
||||
const [,, cartId] = decoded.merchantReferenceDisplay.split(':');
|
||||
if (!cartId) {
|
||||
throw new Error("Cart ID not found");
|
||||
}
|
||||
|
||||
const cart = await retrieveCart(cartId);
|
||||
if (!cart) {
|
||||
throw new Error("Cart not found");
|
||||
}
|
||||
|
||||
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: true });
|
||||
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||
await createOrder({ medusaOrder, orderedAnalysisElements });
|
||||
|
||||
const { productTypes } = await listProductTypes();
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
||||
const order = await placeOrder(cartId, { revalidateCacheTags: true });
|
||||
const analysisPackageOrderItem = order.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id);
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE);
|
||||
const analysisPackageOrderItem = medusaOrder.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id);
|
||||
return {
|
||||
email: order.email,
|
||||
medusaOrderId: medusaOrder.id,
|
||||
email: medusaOrder.email,
|
||||
partnerLocationName: analysisPackageOrderItem?.metadata?.partner_location_name as string ?? '',
|
||||
analysisPackageName: analysisPackageOrderItem?.title ?? '',
|
||||
orderedAnalysisElements,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to place order", error);
|
||||
throw new Error(`Failed to place order, message=${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { language } = await createI18nServerInstance();
|
||||
const baseUrl = new URL(env.siteUrl.replace("localhost", "webhook.site"));
|
||||
const baseUrl = new URL(env.siteUrl.replace("localhost", LOCAL_MONTONIO_REDIRECT_FAKE_ORIGIN));
|
||||
try {
|
||||
const orderToken = new URL(request.url).searchParams.get('order-token');
|
||||
if (!orderToken) {
|
||||
throw new Error("Order token is missing");
|
||||
}
|
||||
|
||||
|
||||
const account = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error("Account not found in context");
|
||||
@@ -104,13 +126,19 @@ export async function GET(request: Request) {
|
||||
throw new Error("Order result is missing");
|
||||
}
|
||||
|
||||
const { email, partnerLocationName, analysisPackageName } = orderResult;
|
||||
const { medusaOrderId, email, partnerLocationName, analysisPackageName, orderedAnalysisElements } = orderResult;
|
||||
const personName = account.name;
|
||||
if (email && analysisPackageName) {
|
||||
await sendEmail({ email, analysisPackageName, personName, partnerLocationName, language });
|
||||
try {
|
||||
await sendEmail({ email, analysisPackageName, personName, partnerLocationName, language });
|
||||
} catch (error) {
|
||||
console.error("Failed to send email", error);
|
||||
}
|
||||
} else {
|
||||
// @TODO send email for separate analyses
|
||||
console.error("Missing email or analysisPackageName", orderResult);
|
||||
}
|
||||
sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements })
|
||||
return Response.redirect(new URL('/home/order', baseUrl))
|
||||
} catch (error) {
|
||||
console.error("Failed to place order", error);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { GET } from "./[montonioId]/route";
|
||||
@@ -5,7 +5,7 @@ import { notFound } from 'next/navigation';
|
||||
|
||||
import { retrieveCart } from '@lib/data/cart';
|
||||
import Cart from '../../_components/cart';
|
||||
import { listProductTypes } from '@lib/data';
|
||||
import { listProductTypes } from '@lib/data/products';
|
||||
import CartTimer from '../../_components/cart/cart-timer';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
async function OrderAnalysisPackagePage() {
|
||||
const { analysisPackages, countryCode } = await loadAnalysisPackages();
|
||||
const { analysisPackageElements, analysisPackages, countryCode } = await loadAnalysisPackages();
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
@@ -30,6 +30,7 @@ async function OrderAnalysisPackagePage() {
|
||||
</h3>
|
||||
<ComparePackagesModal
|
||||
analysisPackages={analysisPackages}
|
||||
analysisPackageElements={analysisPackageElements}
|
||||
triggerElement={
|
||||
<Button variant="secondary" className="gap-2">
|
||||
<Trans i18nKey={'marketing:comparePackages'} />
|
||||
|
||||
@@ -3,6 +3,8 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
||||
import OrderAnalysesCards from '../../_components/order-analyses-cards';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const { t } = await createI18nServerInstance();
|
||||
@@ -13,6 +15,8 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
async function OrderAnalysisPage() {
|
||||
const { analyses, countryCode } = await loadAnalyses();
|
||||
|
||||
return (
|
||||
<>
|
||||
<HomeLayoutPageHeader
|
||||
@@ -20,6 +24,7 @@ async function OrderAnalysisPage() {
|
||||
description={<Trans i18nKey={'order-analysis:description'} />}
|
||||
/>
|
||||
<PageBody>
|
||||
<OrderAnalysesCards analyses={analyses} countryCode={countryCode} />
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,8 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { listOrders } from '~/medusa/lib/data/orders';
|
||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||
import { listProductTypes, retrieveCustomer } from '@lib/data';
|
||||
import { listProductTypes } from '@lib/data/products';
|
||||
import { retrieveCustomer } from '@lib/data/customer';
|
||||
import { PageBody } from '@kit/ui/makerkit/page';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { toast } from 'sonner';
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { updateLineItem } from "@lib/data/cart"
|
||||
import { StoreCart, StoreCartLineItem } from "@medusajs/types"
|
||||
import { Form } from "@kit/ui/form";
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@kit/ui/select';
|
||||
import { updateCartPartnerLocation } from '../../_lib/server/update-cart-partner-location';
|
||||
|
||||
const AnalysisLocationSchema = z.object({
|
||||
locationId: z.string().min(1),
|
||||
@@ -40,16 +40,12 @@ export default function AnalysisLocation({ cart, analysisPackages }: { cart: Sto
|
||||
});
|
||||
|
||||
const onSubmit = async ({ locationId }: z.infer<typeof AnalysisLocationSchema>) => {
|
||||
const promise = Promise.all(analysisPackages.map(async ({ id, quantity }) => {
|
||||
await updateLineItem({
|
||||
lineId: id,
|
||||
quantity,
|
||||
metadata: {
|
||||
partner_location_name: MOCK_LOCATIONS.find((location) => location.id === locationId)?.name ?? '',
|
||||
partner_location_id: locationId,
|
||||
},
|
||||
});
|
||||
}));
|
||||
const promise = updateCartPartnerLocation({
|
||||
cartId: cart.id,
|
||||
lineIds: analysisPackages.map(({ id }) => id),
|
||||
partnerLocationId: locationId,
|
||||
partnerLocationName: MOCK_LOCATIONS.find((location) => location.id === locationId)?.name ?? '',
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
success: t(`cart:items.analysisLocation.success`),
|
||||
|
||||
@@ -23,79 +23,7 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import { PackageHeader } from '@/components/package-header';
|
||||
import { InfoTooltip } from '@/components/ui/info-tooltip';
|
||||
import { StoreProduct } from '@medusajs/types';
|
||||
|
||||
const dummyCards = [
|
||||
{
|
||||
titleKey: 'product:standard.label',
|
||||
price: 40,
|
||||
nrOfAnalyses: 4,
|
||||
tagColor: 'bg-cyan',
|
||||
},
|
||||
{
|
||||
titleKey: 'product:standardPlus.label',
|
||||
price: 85,
|
||||
nrOfAnalyses: 10,
|
||||
tagColor: 'bg-warning',
|
||||
},
|
||||
{
|
||||
titleKey: 'product:premium.label',
|
||||
price: 140,
|
||||
nrOfAnalyses: '12+',
|
||||
tagColor: 'bg-purple',
|
||||
},
|
||||
];
|
||||
|
||||
const dummyRows = [
|
||||
{
|
||||
analysisNameKey: 'product:clinicalBloodDraw.label',
|
||||
tooltipContentKey: 'product:clinicalBloodDraw.description',
|
||||
includedInStandard: 1,
|
||||
includedInStandardPlus: 1,
|
||||
includedInPremium: 1,
|
||||
},
|
||||
{
|
||||
analysisNameKey: 'product:crp.label',
|
||||
tooltipContentKey: 'product:crp.description',
|
||||
includedInStandard: 1,
|
||||
includedInStandardPlus: 1,
|
||||
includedInPremium: 1,
|
||||
},
|
||||
{
|
||||
analysisNameKey: 'product:ferritin.label',
|
||||
tooltipContentKey: 'product:ferritin.description',
|
||||
includedInStandard: 0,
|
||||
includedInStandardPlus: 1,
|
||||
includedInPremium: 1,
|
||||
},
|
||||
{
|
||||
analysisNameKey: 'product:vitaminD.label',
|
||||
tooltipContentKey: 'product:vitaminD.description',
|
||||
includedInStandard: 0,
|
||||
includedInStandardPlus: 1,
|
||||
includedInPremium: 1,
|
||||
},
|
||||
{
|
||||
analysisNameKey: 'product:glucose.label',
|
||||
tooltipContentKey: 'product:glucose.description',
|
||||
includedInStandard: 1,
|
||||
includedInStandardPlus: 1,
|
||||
includedInPremium: 1,
|
||||
},
|
||||
{
|
||||
analysisNameKey: 'product:alat.label',
|
||||
tooltipContentKey: 'product:alat.description',
|
||||
includedInStandard: 1,
|
||||
includedInStandardPlus: 1,
|
||||
includedInPremium: 1,
|
||||
},
|
||||
{
|
||||
analysisNameKey: 'product:ast.label',
|
||||
tooltipContentKey: 'product:ast.description',
|
||||
includedInStandard: 1,
|
||||
includedInStandardPlus: 1,
|
||||
includedInPremium: 1,
|
||||
},
|
||||
];
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
|
||||
const CheckWithBackground = () => {
|
||||
return (
|
||||
@@ -105,14 +33,46 @@ const CheckWithBackground = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const PackageTableHead = async ({ product, nrOfAnalyses }: { product: StoreProduct, nrOfAnalyses: number }) => {
|
||||
const { t, language } = await createI18nServerInstance();
|
||||
const variant = product.variants?.[0];
|
||||
const titleKey = product.title;
|
||||
const price = variant?.calculated_price?.calculated_amount ?? 0;
|
||||
return (
|
||||
<TableHead className="py-2">
|
||||
<PackageHeader
|
||||
title={t(titleKey)}
|
||||
tagColor='bg-cyan'
|
||||
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
|
||||
language={language}
|
||||
price={price}
|
||||
/>
|
||||
</TableHead>
|
||||
)
|
||||
}
|
||||
|
||||
const ComparePackagesModal = async ({
|
||||
analysisPackages,
|
||||
analysisPackageElements,
|
||||
triggerElement,
|
||||
}: {
|
||||
analysisPackages: StoreProduct[];
|
||||
analysisPackageElements: StoreProduct[];
|
||||
triggerElement: JSX.Element;
|
||||
}) => {
|
||||
const { t, language } = await createI18nServerInstance();
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
const standardPackage = analysisPackages.find(({ metadata }) => metadata?.analysisPackageTier === 'standard')!;
|
||||
const standardPlusPackage = analysisPackages.find(({ metadata }) => metadata?.analysisPackageTier === 'standard-plus')!;
|
||||
const premiumPackage = analysisPackages.find(({ metadata }) => metadata?.analysisPackageTier === 'premium')!;
|
||||
|
||||
if (!standardPackage || !standardPlusPackage || !premiumPackage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const standardPackageAnalyses = getAnalysisElementMedusaProductIds([standardPackage]);
|
||||
const standardPlusPackageAnalyses = getAnalysisElementMedusaProductIds([standardPlusPackage]);
|
||||
const premiumPackageAnalyses = getAnalysisElementMedusaProductIds([premiumPackage]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
@@ -138,64 +98,50 @@ const ComparePackagesModal = async ({
|
||||
<p className="text-muted-foreground mx-auto w-3/5 text-sm">
|
||||
{t('product:healthPackageComparison.description')}
|
||||
</p>
|
||||
<div className="rounded-md border">
|
||||
<div className="rounded-md border max-h-[80vh] overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead></TableHead>
|
||||
{analysisPackages.map(
|
||||
(product) => {
|
||||
const variant = product.variants?.[0];
|
||||
const titleKey = product.title;
|
||||
const price = variant?.calculated_price?.calculated_amount ?? 0;
|
||||
return (
|
||||
<TableHead key={titleKey} className="py-2">
|
||||
<PackageHeader
|
||||
title={t(titleKey)}
|
||||
tagColor='bg-cyan'
|
||||
analysesNr={t('product:nrOfAnalyses', {
|
||||
nr: product?.metadata?.nrOfAnalyses ?? 0,
|
||||
})}
|
||||
language={language}
|
||||
price={price}
|
||||
/>
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
<PackageTableHead product={standardPackage} nrOfAnalyses={standardPackageAnalyses.length} />
|
||||
<PackageTableHead product={standardPlusPackage} nrOfAnalyses={standardPlusPackageAnalyses.length} />
|
||||
<PackageTableHead product={premiumPackage} nrOfAnalyses={premiumPackageAnalyses.length} />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{dummyRows.map(
|
||||
{analysisPackageElements.map(
|
||||
(
|
||||
{
|
||||
analysisNameKey,
|
||||
tooltipContentKey,
|
||||
includedInStandard,
|
||||
includedInStandardPlus,
|
||||
includedInPremium,
|
||||
title,
|
||||
id,
|
||||
description,
|
||||
},
|
||||
index,
|
||||
) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="py-6">
|
||||
{t(analysisNameKey)}{' '}
|
||||
<InfoTooltip
|
||||
content={t(tooltipContentKey)}
|
||||
icon={<QuestionMarkCircledIcon />}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{!!includedInStandard && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{!!includedInStandardPlus && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{!!includedInPremium && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
) => {
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
const includedInStandard = standardPackageAnalyses.includes(id);
|
||||
const includedInStandardPlus = standardPlusPackageAnalyses.includes(id);
|
||||
const includedInPremium = premiumPackageAnalyses.includes(id);
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="py-6">
|
||||
{title}{' '}
|
||||
{description && (<InfoTooltip content={description} icon={<QuestionMarkCircledIcon />} />)}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{includedInStandard && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{(includedInStandard || includedInStandardPlus) && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{(includedInStandard || includedInStandardPlus || includedInPremium) && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
80
app/home/(user)/_components/order-analyses-cards.tsx
Normal file
80
app/home/(user)/_components/order-analyses-cards.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { HeartPulse, Loader2, ShoppingCart } from 'lucide-react';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
} from '@kit/ui/card';
|
||||
import { StoreProduct, StoreProductVariant } from '@medusajs/types';
|
||||
import { useState } from 'react';
|
||||
import { handleAddToCart } from '~/lib/services/medusaCart.service';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function OrderAnalysesCards({
|
||||
analyses,
|
||||
countryCode,
|
||||
}: {
|
||||
analyses: StoreProduct[];
|
||||
countryCode: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
||||
const handleSelect = async (selectedVariant: StoreProductVariant) => {
|
||||
if (!selectedVariant?.id || isAddingToCart) return null
|
||||
|
||||
setIsAddingToCart(true);
|
||||
try {
|
||||
await handleAddToCart({
|
||||
selectedVariant,
|
||||
countryCode,
|
||||
});
|
||||
setIsAddingToCart(false);
|
||||
router.push('/home/cart');
|
||||
} catch (e) {
|
||||
setIsAddingToCart(false);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-6 mt-4">
|
||||
{analyses.map(({
|
||||
title,
|
||||
variants
|
||||
}) => (
|
||||
<Card
|
||||
key={title}
|
||||
variant="gradient-success"
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<CardHeader className="items-end-safe">
|
||||
<div className='flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="px-2 text-black"
|
||||
onClick={() => handleSelect(variants![0]!)}
|
||||
>
|
||||
{isAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex flex-col items-start gap-2">
|
||||
<div
|
||||
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
|
||||
>
|
||||
<HeartPulse className="size-4 fill-green-500" />
|
||||
</div>
|
||||
<h5>
|
||||
{title}
|
||||
</h5>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
app/home/(user)/_lib/server/load-analyses.ts
Normal file
41
app/home/(user)/_lib/server/load-analyses.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { listProductTypes } from "@lib/data/products";
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getProductCategories } from '@lib/data/categories';
|
||||
|
||||
async function countryCodesLoader() {
|
||||
const countryCodes = await listRegions().then((regions) =>
|
||||
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
|
||||
);
|
||||
return countryCodes ?? [];
|
||||
}
|
||||
export const loadCountryCodes = cache(countryCodesLoader);
|
||||
|
||||
async function productCategoriesLoader() {
|
||||
const productCategories = await getProductCategories({ fields: "*products, *products.variants" });
|
||||
return productCategories.product_categories ?? [];
|
||||
}
|
||||
export const loadProductCategories = cache(productCategoriesLoader);
|
||||
|
||||
async function productTypesLoader() {
|
||||
const { productTypes } = await listProductTypes();
|
||||
return productTypes ?? [];
|
||||
}
|
||||
export const loadProductTypes = cache(productTypesLoader);
|
||||
|
||||
async function analysesLoader() {
|
||||
const [countryCodes, productCategories] = await Promise.all([
|
||||
loadCountryCodes(),
|
||||
loadProductCategories(),
|
||||
]);
|
||||
const countryCode = countryCodes[0]!;
|
||||
|
||||
const category = productCategories.find(({ metadata }) => metadata?.page === 'order-analysis');
|
||||
|
||||
return {
|
||||
analyses: category?.products ?? [],
|
||||
countryCode,
|
||||
}
|
||||
}
|
||||
export const loadAnalyses = cache(analysesLoader);
|
||||
@@ -1,6 +1,9 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { listProductTypes, listProducts, listRegions } from "@lib/data";
|
||||
import { listProductTypes, listProducts } from "@lib/data/products";
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
import type { StoreProduct } from '@medusajs/types';
|
||||
|
||||
async function countryCodesLoader() {
|
||||
const countryCodes = await listRegions().then((regions) =>
|
||||
@@ -20,15 +23,32 @@ async function analysisPackagesLoader() {
|
||||
const [countryCodes, productTypes] = await Promise.all([loadCountryCodes(), loadProductTypes()]);
|
||||
const countryCode = countryCodes[0]!;
|
||||
|
||||
let analysisPackages: StoreProduct[] = [];
|
||||
let analysisPackageElements: StoreProduct[] = [];
|
||||
|
||||
const productType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
||||
if (!productType) {
|
||||
return { analysisPackages: [], countryCode };
|
||||
return { analysisPackageElements, analysisPackages, countryCode };
|
||||
}
|
||||
|
||||
const { response } = await listProducts({
|
||||
const analysisPackagesResponse = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, "type_id[0]": productType.id },
|
||||
});
|
||||
return { analysisPackages: response.products, countryCode };
|
||||
analysisPackages = analysisPackagesResponse.response.products;
|
||||
|
||||
const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds(analysisPackages);
|
||||
if (analysisElementMedusaProductIds.length > 0) {
|
||||
const { response: { products } } = await listProducts({
|
||||
countryCode,
|
||||
queryParams: {
|
||||
id: analysisElementMedusaProductIds,
|
||||
limit: 100,
|
||||
},
|
||||
});
|
||||
analysisPackageElements = products;
|
||||
}
|
||||
|
||||
return { analysisPackageElements, analysisPackages, countryCode };
|
||||
}
|
||||
export const loadAnalysisPackages = cache(analysisPackagesLoader);
|
||||
|
||||
38
app/home/(user)/_lib/server/update-cart-partner-location.ts
Normal file
38
app/home/(user)/_lib/server/update-cart-partner-location.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
"use server";
|
||||
|
||||
import { retrieveCart, updateCart, updateLineItem } from "@lib/data/cart";
|
||||
|
||||
export const updateCartPartnerLocation = async ({
|
||||
cartId,
|
||||
lineIds,
|
||||
partnerLocationId,
|
||||
partnerLocationName,
|
||||
}: {
|
||||
cartId: string;
|
||||
lineIds: string[];
|
||||
partnerLocationId: string;
|
||||
partnerLocationName: string;
|
||||
}) => {
|
||||
const cart = await retrieveCart(cartId);
|
||||
if (!cart) {
|
||||
throw new Error("Cart not found");
|
||||
}
|
||||
|
||||
for (const lineItemId of lineIds) {
|
||||
await updateLineItem({
|
||||
lineId: lineItemId,
|
||||
quantity: 1,
|
||||
metadata: {
|
||||
partner_location_name: partnerLocationName,
|
||||
partner_location_id: partnerLocationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
await updateCart({
|
||||
id: cartId,
|
||||
metadata: {
|
||||
partner_location_name: partnerLocationName,
|
||||
partner_location_id: partnerLocationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
async function SelectPackagePage() {
|
||||
const { analysisPackages, countryCode } = await loadAnalysisPackages();
|
||||
const { analysisPackageElements, analysisPackages, countryCode } = await loadAnalysisPackages();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto my-24 flex flex-col items-center space-y-12">
|
||||
@@ -35,6 +35,7 @@ async function SelectPackagePage() {
|
||||
</h3>
|
||||
<ComparePackagesModal
|
||||
analysisPackages={analysisPackages}
|
||||
analysisPackageElements={analysisPackageElements}
|
||||
triggerElement={
|
||||
<Button variant="secondary" className="gap-2">
|
||||
<Trans i18nKey={'marketing:comparePackages'} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { use, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -19,11 +19,11 @@ import { handleAddToCart } from '@/lib/services/medusaCart.service';
|
||||
|
||||
import { PackageHeader } from './package-header';
|
||||
import { ButtonTooltip } from './ui/button-tooltip';
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
|
||||
export interface IAnalysisPackage {
|
||||
titleKey: string;
|
||||
price: number;
|
||||
nrOfAnalyses: number | string;
|
||||
tagColor: string;
|
||||
descriptionKey: string;
|
||||
}
|
||||
@@ -52,7 +52,8 @@ export default function SelectAnalysisPackage({
|
||||
}
|
||||
|
||||
const titleKey = analysisPackage.title;
|
||||
const nrOfAnalyses = analysisPackage?.metadata?.nrOfAnalyses ?? 0;
|
||||
const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds([analysisPackage]);
|
||||
const nrOfAnalyses = analysisElementMedusaProductIds.length;
|
||||
const description = analysisPackage.description ?? '';
|
||||
const subtitle = analysisPackage.subtitle ?? '';
|
||||
const variant = analysisPackage.variants?.[0];
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
import { createClient as createCustomClient } from '@supabase/supabase-js';
|
||||
|
||||
import axios from 'axios';
|
||||
import { format } from 'date-fns';
|
||||
import { config } from 'dotenv';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
|
||||
function getLatestMessage(messages) {
|
||||
if (!messages?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return messages.reduce((prev, current) =>
|
||||
Number(prev.messageId) > Number(current.messageId) ? prev : current,
|
||||
);
|
||||
}
|
||||
|
||||
export function toArray<T>(input?: T | T[] | null): T[] {
|
||||
if (!input) return [];
|
||||
return Array.isArray(input) ? input : [input];
|
||||
}
|
||||
|
||||
async function syncData() {
|
||||
if (process.env.NODE_ENV === 'local') {
|
||||
config({ path: `.env.${process.env.NODE_ENV}` });
|
||||
}
|
||||
|
||||
const baseUrl = process.env.MEDIPOST_URL;
|
||||
const user = process.env.MEDIPOST_USER;
|
||||
const password = process.env.MEDIPOST_PASSWORD;
|
||||
const sender = process.env.MEDIPOST_MESSAGE_SENDER;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
if (
|
||||
!baseUrl ||
|
||||
!supabaseUrl ||
|
||||
!supabaseServiceRoleKey ||
|
||||
!user ||
|
||||
!password ||
|
||||
!sender
|
||||
) {
|
||||
throw new Error('Could not access all necessary environment variables');
|
||||
}
|
||||
|
||||
const supabase = createCustomClient(supabaseUrl, supabaseServiceRoleKey, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// GET LATEST PUBLIC MESSAGE ID
|
||||
const { data: lastChecked } = await supabase
|
||||
.schema('audit')
|
||||
.from('sync_entries')
|
||||
.select('created_at')
|
||||
.eq('status', 'SUCCESS')
|
||||
.order('created_at')
|
||||
.limit(1);
|
||||
|
||||
const lastCheckedDate = lastChecked?.length
|
||||
? {
|
||||
LastChecked: format(lastChecked[0].created_at, 'yyyy-MM-dd HH:mm:ss'),
|
||||
}
|
||||
: {};
|
||||
|
||||
const { data, status } = await axios.get(baseUrl, {
|
||||
params: {
|
||||
Action: 'GetPublicMessageList',
|
||||
User: user,
|
||||
Password: password,
|
||||
Sender: sender,
|
||||
...lastCheckedDate,
|
||||
MessageType: 'Teenus',
|
||||
},
|
||||
});
|
||||
|
||||
if (!data || status !== 200) {
|
||||
console.error("Failed to get public message list, status: ", status, data);
|
||||
throw new Error('Failed to get public message list');
|
||||
}
|
||||
|
||||
if (data.code && data.code !== 0) {
|
||||
throw new Error('Failed to get public message list');
|
||||
}
|
||||
|
||||
if (!data.messages?.length) {
|
||||
return supabase.schema('audit').from('sync_entries').insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
comment: 'No new data received',
|
||||
status: 'SUCCESS',
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
}
|
||||
|
||||
const latestMessage = getLatestMessage(data?.messages);
|
||||
|
||||
// GET PUBLIC MESSAGE WITH GIVEN ID
|
||||
|
||||
const { data: publicMessageData } = await axios.get(baseUrl, {
|
||||
params: {
|
||||
Action: 'GetPublicMessage',
|
||||
User: user,
|
||||
Password: password,
|
||||
MessageId: latestMessage.messageId,
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/xml',
|
||||
},
|
||||
});
|
||||
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
const parsed = parser.parse(publicMessageData);
|
||||
|
||||
if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get public message (id: ${latestMessage.messageId})`,
|
||||
);
|
||||
}
|
||||
|
||||
// SAVE PUBLIC MESSAGE DATA
|
||||
|
||||
const providers = toArray(parsed?.Saadetis?.Teenused.Teostaja);
|
||||
const analysisGroups = providers.flatMap((provider) =>
|
||||
toArray(provider.UuringuGrupp),
|
||||
);
|
||||
|
||||
if (!parsed || !analysisGroups.length) {
|
||||
return supabase.schema('audit').from('sync_entries').insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
comment: 'No data received',
|
||||
status: 'FAIL',
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
}
|
||||
|
||||
const codes: any[] = [];
|
||||
for (const analysisGroup of analysisGroups) {
|
||||
// SAVE ANALYSIS GROUP
|
||||
const { data: insertedAnalysisGroup, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_groups')
|
||||
.upsert(
|
||||
{
|
||||
original_id: analysisGroup.UuringuGruppId,
|
||||
name: analysisGroup.UuringuGruppNimi,
|
||||
order: analysisGroup.UuringuGruppJarjekord,
|
||||
},
|
||||
{ onConflict: 'original_id', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
if (error || !insertedAnalysisGroup[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis group (id: ${analysisGroup.UuringuGruppId}), error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
const analysisGroupId = insertedAnalysisGroup[0].id;
|
||||
|
||||
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
||||
codes.push(
|
||||
...analysisGroupCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: analysisGroupId,
|
||||
analysis_element_id: null,
|
||||
analysis_id: null,
|
||||
})),
|
||||
);
|
||||
|
||||
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
||||
|
||||
for (const item of analysisGroupItems) {
|
||||
const analysisElement = item.UuringuElement;
|
||||
|
||||
const { data: insertedAnalysisElement, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.upsert(
|
||||
{
|
||||
analysis_id_oid: analysisElement.UuringIdOID,
|
||||
analysis_id_original: analysisElement.UuringId,
|
||||
tehik_short_loinc: analysisElement.TLyhend,
|
||||
tehik_loinc_name: analysisElement.KNimetus,
|
||||
analysis_name_lab: analysisElement.UuringNimi,
|
||||
order: analysisElement.Jarjekord,
|
||||
parent_analysis_group_id: analysisGroupId,
|
||||
material_groups: toArray(item.MaterjalideGrupp),
|
||||
},
|
||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
if (error || !insertedAnalysisElement[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis element (id: ${analysisElement.UuringId}), error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const insertedAnalysisElementId = insertedAnalysisElement[0].id;
|
||||
|
||||
if (analysisElement.Kood) {
|
||||
const analysisElementCodes = toArray(analysisElement.Kood);
|
||||
codes.push(
|
||||
...analysisElementCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: null,
|
||||
analysis_element_id: insertedAnalysisElementId,
|
||||
analysis_id: null,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const analyses = analysisElement.UuringuElement;
|
||||
if (analyses?.length) {
|
||||
for (const analysis of analyses) {
|
||||
const { data: insertedAnalysis, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analyses')
|
||||
.upsert(
|
||||
{
|
||||
analysis_id_oid: analysis.UuringIdOID,
|
||||
analysis_id_original: analysis.UuringId,
|
||||
tehik_short_loinc: analysis.TLyhend,
|
||||
tehik_loinc_name: analysis.KNimetus,
|
||||
analysis_name_lab: analysis.UuringNimi,
|
||||
order: analysis.Jarjekord,
|
||||
parent_analysis_element_id: insertedAnalysisElementId,
|
||||
},
|
||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
if (error || !insertedAnalysis[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis (id: ${analysis.UuringId}) error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const insertedAnalysisId = insertedAnalysis[0].id;
|
||||
if (analysis.Kood) {
|
||||
const analysisCodes = toArray(analysis.Kood);
|
||||
|
||||
codes.push(
|
||||
...analysisCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: null,
|
||||
analysis_element_id: null,
|
||||
analysis_id: insertedAnalysisId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await supabase.schema('medreport').from('codes').upsert(codes);
|
||||
|
||||
await supabase.schema('audit').from('sync_entries').insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: 'SUCCESS',
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
} catch (e) {
|
||||
await supabase
|
||||
.schema('audit')
|
||||
.from('sync_entries')
|
||||
.insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: 'FAIL',
|
||||
comment: JSON.stringify(e),
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
throw new Error(
|
||||
`Failed to sync public message data, error: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
syncData();
|
||||
@@ -36,6 +36,7 @@ export const defaultI18nNamespaces = [
|
||||
'product',
|
||||
'booking',
|
||||
'order-analysis-package',
|
||||
'order-analysis',
|
||||
'cart',
|
||||
'orders',
|
||||
];
|
||||
|
||||
41
lib/services/account.service.ts
Normal file
41
lib/services/account.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getSupabaseServerClient } from "@kit/supabase/server-client";
|
||||
import { getSupabaseServerAdminClient } from "@kit/supabase/server-admin-client";
|
||||
import type { Tables } from "@/packages/supabase/src/database.types";
|
||||
|
||||
type Account = Tables<{ schema: 'medreport' }, 'accounts'>;
|
||||
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'>;
|
||||
|
||||
export type AccountWithMemberships = Account & { memberships: Membership[] }
|
||||
|
||||
export async function getAccount(id: string): Promise<AccountWithMemberships> {
|
||||
const { data } = await getSupabaseServerClient()
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('*, memberships: accounts_memberships (*)')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
.throwOnError();
|
||||
|
||||
return data as unknown as AccountWithMemberships;
|
||||
}
|
||||
|
||||
export async function getAccountAdmin({
|
||||
primaryOwnerUserId,
|
||||
}: {
|
||||
primaryOwnerUserId: string;
|
||||
}): Promise<AccountWithMemberships> {
|
||||
const query = getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('*, memberships: accounts_memberships (*)')
|
||||
|
||||
if (primaryOwnerUserId) {
|
||||
query.eq('primary_owner_user_id', primaryOwnerUserId);
|
||||
} else {
|
||||
throw new Error('primaryOwnerUserId is required');
|
||||
}
|
||||
|
||||
const { data } = await query.single().throwOnError();
|
||||
|
||||
return data as unknown as AccountWithMemberships;
|
||||
}
|
||||
116
lib/services/analyses.service.ts
Normal file
116
lib/services/analyses.service.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { Tables } from '@/packages/supabase/src/database.types';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import type { IUuringElement } from "./medipost.types";
|
||||
|
||||
type AnalysesWithGroupsAndElements = ({
|
||||
analysis_elements: Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
||||
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
||||
};
|
||||
} & Tables<{ schema: 'medreport' }, 'analyses'>)[];
|
||||
|
||||
export const createAnalysis = async (
|
||||
analysis: IUuringElement,
|
||||
insertedAnalysisElementId: number,
|
||||
) => {
|
||||
const { data: insertedAnalysis, error } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analyses')
|
||||
.upsert(
|
||||
{
|
||||
analysis_id_oid: analysis.UuringIdOID,
|
||||
analysis_id_original: analysis.UuringId,
|
||||
tehik_short_loinc: analysis.TLyhend,
|
||||
tehik_loinc_name: analysis.KNimetus,
|
||||
analysis_name_lab: analysis.UuringNimi,
|
||||
order: analysis.Jarjekord,
|
||||
parent_analysis_element_id: insertedAnalysisElementId,
|
||||
},
|
||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
const insertedAnalysisId = insertedAnalysis?.[0]?.id as number;
|
||||
|
||||
if (error || !insertedAnalysisId) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis (id: ${analysis.UuringId}) error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return insertedAnalysisId;
|
||||
}
|
||||
|
||||
const createSyncEntry = async ({
|
||||
operation,
|
||||
status,
|
||||
comment,
|
||||
}: {
|
||||
operation: 'ANALYSES_SYNC' | 'ANALYSIS_GROUPS_SYNC' | 'ANALYSES_MEDUSA_SYNC';
|
||||
status: 'SUCCESS' | 'FAIL';
|
||||
comment?: string;
|
||||
}) => {
|
||||
await getSupabaseServerAdminClient()
|
||||
.schema('audit').from('sync_entries')
|
||||
.insert({
|
||||
operation,
|
||||
status,
|
||||
changed_by_role: 'service_role',
|
||||
comment,
|
||||
});
|
||||
}
|
||||
|
||||
export const createNoNewDataReceivedEntry = async () => {
|
||||
await createSyncEntry({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: 'SUCCESS',
|
||||
comment: 'No new data received',
|
||||
});
|
||||
}
|
||||
|
||||
export const createNoDataReceivedEntry = async () => {
|
||||
await createSyncEntry({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: 'SUCCESS',
|
||||
comment: 'No data received',
|
||||
});
|
||||
}
|
||||
|
||||
export const createSyncFailEntry = async (error: string) => {
|
||||
await createSyncEntry({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: 'FAIL',
|
||||
comment: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const createSyncSuccessEntry = async () => {
|
||||
await createSyncEntry({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: 'SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export const createMedusaSyncFailEntry = async (error: string) => {
|
||||
await createSyncEntry({
|
||||
operation: 'ANALYSES_MEDUSA_SYNC',
|
||||
status: 'FAIL',
|
||||
comment: error,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const createMedusaSyncSuccessEntry = async () => {
|
||||
await createSyncEntry({
|
||||
operation: 'ANALYSES_MEDUSA_SYNC',
|
||||
status: 'SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAnalyses({ ids }: { ids: number[] }): Promise<AnalysesWithGroupsAndElements> {
|
||||
const { data } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analyses')
|
||||
.select(`*, analysis_elements(*, analysis_groups(*))`)
|
||||
.in('id', ids);
|
||||
|
||||
return data as unknown as AnalysesWithGroupsAndElements;
|
||||
}
|
||||
93
lib/services/analysis-element.service.ts
Normal file
93
lib/services/analysis-element.service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Json, Tables } from '@kit/supabase/database';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import type { IMaterialGroup, IUuringElement } from './medipost.types';
|
||||
|
||||
export type AnalysisElement = Tables<{ schema: 'medreport' }, 'analysis_elements'> & {
|
||||
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
||||
};
|
||||
|
||||
export async function getAnalysisElements({
|
||||
originalIds,
|
||||
}: {
|
||||
originalIds?: string[];
|
||||
}): Promise<AnalysisElement[]> {
|
||||
const query = getSupabaseServerClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.select(`*, analysis_groups(*)`)
|
||||
.order('order', { ascending: true });
|
||||
|
||||
if (Array.isArray(originalIds)) {
|
||||
query.in('analysis_id_original', [...new Set(originalIds)]);
|
||||
}
|
||||
|
||||
const { data: analysisElements, error } = await query;
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to get analysis elements: ${error.message}`);
|
||||
}
|
||||
|
||||
return analysisElements ?? [];
|
||||
}
|
||||
|
||||
export async function getAnalysisElementsAdmin({
|
||||
ids,
|
||||
}: {
|
||||
ids?: number[];
|
||||
} = {}): Promise<AnalysisElement[]> {
|
||||
const query = getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.select(`*, analysis_groups(*)`)
|
||||
.order('order', { ascending: true });
|
||||
|
||||
if (Array.isArray(ids)) {
|
||||
query.in('id', ids);
|
||||
}
|
||||
|
||||
const { data: analysisElements, error } = await query;
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to get analysis elements: ${error.message}`);
|
||||
}
|
||||
|
||||
return analysisElements ?? [];
|
||||
}
|
||||
|
||||
export async function createAnalysisElement({
|
||||
analysisElement,
|
||||
analysisGroupId,
|
||||
materialGroups,
|
||||
}: {
|
||||
analysisElement: IUuringElement;
|
||||
analysisGroupId: number;
|
||||
materialGroups: IMaterialGroup[];
|
||||
}) {
|
||||
const { data: insertedAnalysisElement, error } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.upsert(
|
||||
{
|
||||
analysis_id_oid: analysisElement.UuringIdOID,
|
||||
analysis_id_original: analysisElement.UuringId,
|
||||
tehik_short_loinc: analysisElement.TLyhend,
|
||||
tehik_loinc_name: analysisElement.KNimetus,
|
||||
analysis_name_lab: analysisElement.UuringNimi,
|
||||
order: analysisElement.Jarjekord,
|
||||
parent_analysis_group_id: analysisGroupId,
|
||||
material_groups: materialGroups as unknown as Json[],
|
||||
},
|
||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
const id = insertedAnalysisElement?.[0]?.id;
|
||||
if (error || !id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis element (id: ${analysisElement.UuringId}), error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
40
lib/services/analysis-group.service.ts
Normal file
40
lib/services/analysis-group.service.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getSupabaseServerAdminClient } from "@kit/supabase/server-admin-client";
|
||||
|
||||
export const createAnalysisGroup = async (
|
||||
analysisGroup: {
|
||||
id: string;
|
||||
name: string;
|
||||
order: number;
|
||||
}
|
||||
) => {
|
||||
const { data: insertedAnalysisGroup, error } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_groups')
|
||||
.upsert(
|
||||
{
|
||||
original_id: analysisGroup.id,
|
||||
name: analysisGroup.name,
|
||||
order: analysisGroup.order,
|
||||
},
|
||||
{ onConflict: 'original_id', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
const analysisGroupId = insertedAnalysisGroup?.[0]?.id as number;
|
||||
|
||||
if (error || !analysisGroupId) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis group (id: ${analysisGroup.id}), error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return analysisGroupId;
|
||||
}
|
||||
|
||||
export const getAnalysisGroups = async () => {
|
||||
const { data: analysisGroups } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_groups')
|
||||
.select('*');
|
||||
|
||||
return analysisGroups;
|
||||
}
|
||||
8
lib/services/codes.service.ts
Normal file
8
lib/services/codes.service.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { getSupabaseServerAdminClient } from "@kit/supabase/server-admin-client";
|
||||
import type { ICode } from "~/lib/types/code";
|
||||
|
||||
export const createCodes = async (codes: ICode[]) => {
|
||||
await getSupabaseServerAdminClient()
|
||||
.schema('medreport').from('codes')
|
||||
.upsert(codes);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
getClientInstitution,
|
||||
getClientPerson,
|
||||
getConfidentiality,
|
||||
getOrderEnteredByPerson,
|
||||
getPais,
|
||||
getPatient,
|
||||
getProviderInstitution,
|
||||
@@ -20,6 +19,7 @@ import { SyncStatus } from '@/lib/types/audit';
|
||||
import {
|
||||
AnalysisOrderStatus,
|
||||
GetMessageListResponse,
|
||||
IMedipostResponseXMLBase,
|
||||
MaterjalideGrupp,
|
||||
MedipostAction,
|
||||
MedipostOrderResponse,
|
||||
@@ -34,10 +34,46 @@ import { XMLParser } from 'fast-xml-parser';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { createAnalysisGroup } from './analysis-group.service';
|
||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||
import { getOrder } from './order.service';
|
||||
import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service';
|
||||
import { getAnalyses } from './analyses.service';
|
||||
import { getAccountAdmin } from './account.service';
|
||||
import { StoreOrder } from '@medusajs/types';
|
||||
import { listProducts } from '@lib/data/products';
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
|
||||
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 ANALYSIS_ELEMENT_HANDLE_PREFIX = 'analysis-element-';
|
||||
const ANALYSIS_PACKAGE_HANDLE_PREFIX = 'analysis-package-';
|
||||
|
||||
function parseXML(xml: string) {
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
return parser.parse(xml);
|
||||
}
|
||||
|
||||
export async function validateMedipostResponse(response: string, { canHaveEmptyCode = false }: { canHaveEmptyCode?: boolean } = {}) {
|
||||
const parsed: IMedipostResponseXMLBase = parseXML(response);
|
||||
const code = parsed.ANSWER?.CODE;
|
||||
if (canHaveEmptyCode) {
|
||||
if (code && code !== 0) {
|
||||
console.error("Bad response", response);
|
||||
throw new Error(`Medipost response is invalid`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof code !== 'number' || (code !== 0 && !canHaveEmptyCode)) {
|
||||
console.error("Bad response", response);
|
||||
throw new Error(`Medipost response is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMessages() {
|
||||
try {
|
||||
@@ -70,7 +106,7 @@ export async function getLatestPublicMessageListItem() {
|
||||
throw new Error('Failed to get public message list');
|
||||
}
|
||||
|
||||
return getLatestMessage(data?.messages);
|
||||
return getLatestMessage({ messages: data?.messages });
|
||||
}
|
||||
|
||||
export async function getPublicMessage(messageId: string) {
|
||||
@@ -85,22 +121,16 @@ export async function getPublicMessage(messageId: string) {
|
||||
Accept: 'application/xml',
|
||||
},
|
||||
});
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
const parsed: MedipostPublicMessageResponse = parser.parse(data);
|
||||
|
||||
if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
|
||||
throw new Error(`Failed to get public message (id: ${messageId})`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
await validateMedipostResponse(data);
|
||||
return parseXML(data) as MedipostPublicMessageResponse;
|
||||
}
|
||||
|
||||
export async function sendPrivateMessage(messageXml: string, receiver: string) {
|
||||
export async function sendPrivateMessage(messageXml: string) {
|
||||
const body = new FormData();
|
||||
body.append('Action', MedipostAction.SendPrivateMessage);
|
||||
body.append('User', USER);
|
||||
body.append('Password', PASSWORD);
|
||||
body.append('Receiver', receiver);
|
||||
body.append('Receiver', RECIPIENT);
|
||||
body.append('MessageType', 'Tellimus');
|
||||
body.append(
|
||||
'Message',
|
||||
@@ -111,12 +141,14 @@ export async function sendPrivateMessage(messageXml: string, receiver: string) {
|
||||
|
||||
const { data } = await axios.post(BASE_URL, body);
|
||||
|
||||
if (data.code && data.code !== 0) {
|
||||
throw new Error(`Failed to send private message`);
|
||||
}
|
||||
await validateMedipostResponse(data);
|
||||
}
|
||||
|
||||
export async function getLatestPrivateMessageListItem() {
|
||||
export async function getLatestPrivateMessageListItem({
|
||||
excludedMessageIds,
|
||||
}: {
|
||||
excludedMessageIds: string[];
|
||||
}) {
|
||||
const { data } = await axios.get<GetMessageListResponse>(BASE_URL, {
|
||||
params: {
|
||||
Action: MedipostAction.GetPrivateMessageList,
|
||||
@@ -129,7 +161,7 @@ export async function getLatestPrivateMessageListItem() {
|
||||
throw new Error('Failed to get private message list');
|
||||
}
|
||||
|
||||
return getLatestMessage(data?.messages);
|
||||
return getLatestMessage({ messages: data?.messages, excludedMessageIds });
|
||||
}
|
||||
|
||||
export async function getPrivateMessage(messageId: string) {
|
||||
@@ -145,14 +177,9 @@ export async function getPrivateMessage(messageId: string) {
|
||||
},
|
||||
});
|
||||
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
const parsed = parser.parse(data);
|
||||
await validateMedipostResponse(data, { canHaveEmptyCode: true });
|
||||
|
||||
if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
|
||||
throw new Error(`Failed to get private message (id: ${messageId})`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
return parseXML(data) as MedipostOrderResponse;
|
||||
}
|
||||
|
||||
export async function deletePrivateMessage(messageId: string) {
|
||||
@@ -170,51 +197,64 @@ export async function deletePrivateMessage(messageId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function readPrivateMessageResponse() {
|
||||
export async function readPrivateMessageResponse({
|
||||
excludedMessageIds,
|
||||
}: {
|
||||
excludedMessageIds: string[];
|
||||
}) {
|
||||
let messageIdErrored: string | null = null;
|
||||
let messageIdProcessed: string | null = null;
|
||||
try {
|
||||
const privateMessage = await getLatestPrivateMessageListItem();
|
||||
|
||||
const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds });
|
||||
if (!privateMessage) {
|
||||
return null;
|
||||
throw new Error(`No private message found`);
|
||||
}
|
||||
|
||||
messageIdErrored = privateMessage.messageId;
|
||||
if (!messageIdErrored) {
|
||||
throw new Error(`No message id found`);
|
||||
}
|
||||
|
||||
const privateMessageContent = await getPrivateMessage(
|
||||
privateMessage.messageId,
|
||||
);
|
||||
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
|
||||
|
||||
const status = await syncPrivateMessage(privateMessageContent);
|
||||
if (!messageResponse) {
|
||||
throw new Error(`Private message response has no results yet`);
|
||||
}
|
||||
console.info(`Private message content: ${JSON.stringify(privateMessageContent)}`);
|
||||
|
||||
let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||
try {
|
||||
order = await getOrder({ medusaOrderId: messageResponse.ValisTellimuseId });
|
||||
} catch (e) {
|
||||
await deletePrivateMessage(privateMessage.messageId);
|
||||
throw new Error(`Order not found by Medipost message ValisTellimuseId=${messageResponse.ValisTellimuseId}`);
|
||||
}
|
||||
|
||||
const status = await syncPrivateMessage({ messageResponse, order });
|
||||
|
||||
if (status === 'COMPLETED') {
|
||||
await deletePrivateMessage(privateMessage.messageId);
|
||||
messageIdProcessed = privateMessage.messageId;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.warn(`Failed to process private message id=${messageIdErrored}, message=${(e as Error).message}`);
|
||||
}
|
||||
|
||||
return { messageIdErrored, messageIdProcessed };
|
||||
}
|
||||
|
||||
async function saveAnalysisGroup(
|
||||
analysisGroup: UuringuGrupp,
|
||||
supabase: SupabaseClient,
|
||||
) {
|
||||
const { data: insertedAnalysisGroup, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_groups')
|
||||
.upsert(
|
||||
{
|
||||
original_id: analysisGroup.UuringuGruppId,
|
||||
name: analysisGroup.UuringuGruppNimi,
|
||||
order: analysisGroup.UuringuGruppJarjekord,
|
||||
},
|
||||
{ onConflict: 'original_id', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
if (error || !insertedAnalysisGroup[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis group (id: ${analysisGroup.UuringuGruppId}), error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
const analysisGroupId = insertedAnalysisGroup[0].id;
|
||||
const analysisGroupId = await createAnalysisGroup({
|
||||
id: analysisGroup.UuringuGruppId,
|
||||
name: analysisGroup.UuringuGruppNimi,
|
||||
order: analysisGroup.UuringuGruppJarjekord,
|
||||
});
|
||||
|
||||
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
||||
const codes: Partial<Tables<{ schema: 'medreport' }, 'codes'>>[] =
|
||||
@@ -378,60 +418,35 @@ export async function syncPublicMessage(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO use actual parameters
|
||||
export async function composeOrderXML(
|
||||
/* chosenAnalysisElements?: number[],
|
||||
chosenAnalyses?: number[], */
|
||||
comment?: string,
|
||||
) {
|
||||
const supabase = createCustomClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// TODO remove dummy when actual implemetation is present
|
||||
const orderedElements = [1, 75];
|
||||
const orderedAnalyses = [10, 11, 100];
|
||||
|
||||
const createdAnalysisOrder = {
|
||||
id: 4,
|
||||
user_id: 'currentUser.user?.id',
|
||||
analysis_element_ids: orderedElements,
|
||||
analysis_ids: orderedAnalyses,
|
||||
status: AnalysisOrderStatus[1],
|
||||
created_at: new Date(),
|
||||
export async function composeOrderXML({
|
||||
person,
|
||||
orderedAnalysisElementsIds,
|
||||
orderedAnalysesIds,
|
||||
orderId,
|
||||
orderCreatedAt,
|
||||
comment,
|
||||
}: {
|
||||
person: {
|
||||
idCode: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phone: string;
|
||||
};
|
||||
orderedAnalysisElementsIds: number[];
|
||||
orderedAnalysesIds: number[];
|
||||
orderId: string;
|
||||
orderCreatedAt: Date;
|
||||
comment?: string;
|
||||
}) {
|
||||
const analysisElements = await getAnalysisElementsAdmin({ ids: orderedAnalysisElementsIds });
|
||||
if (analysisElements.length !== orderedAnalysisElementsIds.length) {
|
||||
throw new Error(`Got ${analysisElements.length} analysis elements, expected ${orderedAnalysisElementsIds.length}`);
|
||||
}
|
||||
|
||||
const { data: analysisElements } = (await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.select(`*, analysis_groups(*)`)
|
||||
.in('id', orderedElements)) as {
|
||||
data: ({
|
||||
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
||||
} & Tables<{ schema: 'medreport' }, 'analysis_elements'>)[];
|
||||
};
|
||||
const { data: analyses } = (await supabase
|
||||
.schema('medreport')
|
||||
.from('analyses')
|
||||
.select(`*, analysis_elements(*, analysis_groups(*))`)
|
||||
.in('id', orderedAnalyses)) as {
|
||||
data: ({
|
||||
analysis_elements: Tables<
|
||||
{ schema: 'medreport' },
|
||||
'analysis_elements'
|
||||
> & {
|
||||
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
||||
};
|
||||
} & Tables<{ schema: 'medreport' }, 'analyses'>)[];
|
||||
};
|
||||
const analyses = await getAnalyses({ ids: orderedAnalysesIds });
|
||||
if (analyses.length !== orderedAnalysesIds.length) {
|
||||
throw new Error(`Got ${analyses.length} analyses, expected ${orderedAnalysesIds.length}`);
|
||||
}
|
||||
|
||||
const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] =
|
||||
uniqBy(
|
||||
@@ -501,22 +516,19 @@ export async function composeOrderXML(
|
||||
analysisSection.push(groupXml);
|
||||
}
|
||||
|
||||
// TODO get actual data when order creation is implemented
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
|
||||
${getPais(process.env.MEDIPOST_USER!, process.env.MEDIPOST_RECIPIENT!, createdAnalysisOrder.created_at, createdAnalysisOrder.id)}
|
||||
${getPais(USER, RECIPIENT, orderCreatedAt, orderId)}
|
||||
<Tellimus cito="EI">
|
||||
<ValisTellimuseId>${createdAnalysisOrder.id}</ValisTellimuseId>
|
||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||
<!--<TellijaAsutus>-->
|
||||
${getClientInstitution()}
|
||||
<!--<TeostajaAsutus>-->
|
||||
${getProviderInstitution()}
|
||||
<!--<TellijaIsik>-->
|
||||
${getClientPerson()}
|
||||
<!--<SisestajaIsik>-->
|
||||
${getOrderEnteredByPerson()}
|
||||
${getClientPerson(person)}
|
||||
<TellijaMarkused>${comment ?? ''}</TellijaMarkused>
|
||||
${getPatient(49610230861, 'Surname', 'First Name', '1996-10-23', 'N')}
|
||||
${getPatient(person)}
|
||||
${getConfidentiality()}
|
||||
${specimenSection.join('')}
|
||||
${analysisSection?.join('')}
|
||||
@@ -524,48 +536,47 @@ export async function composeOrderXML(
|
||||
</Saadetis>`;
|
||||
}
|
||||
|
||||
function getLatestMessage(messages?: Message[]) {
|
||||
function getLatestMessage({
|
||||
messages,
|
||||
excludedMessageIds,
|
||||
}: {
|
||||
messages?: Message[];
|
||||
excludedMessageIds?: string[];
|
||||
}) {
|
||||
if (!messages?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return messages.reduce((prev, current) =>
|
||||
const filtered = messages.filter(({ messageId }) => !excludedMessageIds?.includes(messageId));
|
||||
|
||||
if (!filtered.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filtered.reduce((prev, current) =>
|
||||
Number(prev.messageId) > Number(current.messageId) ? prev : current,
|
||||
{ messageId: '' } as Message,
|
||||
);
|
||||
}
|
||||
|
||||
export async function syncPrivateMessage(
|
||||
parsedMessage?: MedipostOrderResponse,
|
||||
) {
|
||||
const supabase = createCustomClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const response = parsedMessage?.Saadetis?.Vastus;
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`Invalid data in private message response`);
|
||||
}
|
||||
|
||||
const status = response.TellimuseOlek;
|
||||
export async function syncPrivateMessage({
|
||||
messageResponse,
|
||||
order,
|
||||
}: {
|
||||
messageResponse: MedipostOrderResponse['Saadetis']['Vastus'];
|
||||
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||
}) {
|
||||
const supabase = getSupabaseServerAdminClient()
|
||||
|
||||
const { data: analysisOrder, error: analysisOrderError } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_orders')
|
||||
.select('user_id')
|
||||
.eq('id', response.ValisTellimuseId);
|
||||
.eq('id', order.id);
|
||||
|
||||
if (analysisOrderError || !analysisOrder?.[0]?.user_id) {
|
||||
throw new Error(
|
||||
`Could not find analysis order with id ${response.ValisTellimuseId}`,
|
||||
`Could not find analysis order with id ${messageResponse.ValisTellimuseId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -574,9 +585,9 @@ export async function syncPrivateMessage(
|
||||
.from('analysis_responses')
|
||||
.upsert(
|
||||
{
|
||||
analysis_order_id: response.ValisTellimuseId,
|
||||
order_number: response.TellimuseNumber,
|
||||
order_status: AnalysisOrderStatus[status],
|
||||
analysis_order_id: order.id,
|
||||
order_number: messageResponse.TellimuseNumber,
|
||||
order_status: AnalysisOrderStatus[messageResponse.TellimuseOlek],
|
||||
user_id: analysisOrder[0].user_id,
|
||||
},
|
||||
{ onConflict: 'order_number', ignoreDuplicates: false },
|
||||
@@ -585,10 +596,11 @@ export async function syncPrivateMessage(
|
||||
|
||||
if (error || !analysisResponse?.[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert or update analysis order response (external id: ${response?.TellimuseNumber})`,
|
||||
`Failed to insert or update analysis order response (external id: ${messageResponse?.TellimuseNumber})`,
|
||||
);
|
||||
}
|
||||
const analysisGroups = toArray(response.UuringuGrupp);
|
||||
const analysisGroups = toArray(messageResponse.UuringuGrupp);
|
||||
console.info(`Order has results for ${analysisGroups.length} analysis groups`);
|
||||
|
||||
const responses: Omit<
|
||||
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>,
|
||||
@@ -598,6 +610,7 @@ export async function syncPrivateMessage(
|
||||
const groupItems = toArray(
|
||||
analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
|
||||
);
|
||||
console.info(`Order has results in group ${analysisGroup.UuringuGruppNimi} for ${groupItems.length} analysis elements`);
|
||||
for (const item of groupItems) {
|
||||
const element = item.UuringuElement;
|
||||
const elementAnalysisResponses = toArray(element.UuringuVastus);
|
||||
@@ -646,5 +659,96 @@ export async function syncPrivateMessage(
|
||||
);
|
||||
}
|
||||
|
||||
return AnalysisOrderStatus[status];
|
||||
return AnalysisOrderStatus[messageResponse.TellimuseOlek];
|
||||
}
|
||||
|
||||
export async function sendOrderToMedipost({
|
||||
medusaOrderId,
|
||||
orderedAnalysisElements,
|
||||
}: {
|
||||
medusaOrderId: string;
|
||||
orderedAnalysisElements: { analysisElementId: number }[];
|
||||
}) {
|
||||
const medreportOrder = await getOrder({ medusaOrderId });
|
||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||
|
||||
const orderXml = await composeOrderXML({
|
||||
person: {
|
||||
idCode: account.personal_code!,
|
||||
firstName: account.name ?? '',
|
||||
lastName: account.last_name ?? '',
|
||||
phone: account.phone ?? '',
|
||||
},
|
||||
orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId),
|
||||
orderedAnalysesIds: [],
|
||||
orderId: medusaOrderId,
|
||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||
comment: '',
|
||||
});
|
||||
|
||||
await sendPrivateMessage(orderXml);
|
||||
}
|
||||
|
||||
export async function getOrderedAnalysisElementsIds({
|
||||
medusaOrder,
|
||||
}: {
|
||||
medusaOrder: StoreOrder;
|
||||
}): Promise<{
|
||||
analysisElementId: number;
|
||||
}[]> {
|
||||
const countryCodes = await listRegions();
|
||||
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
|
||||
|
||||
function getOrderedAnalysisElements(medusaOrder: StoreOrder) {
|
||||
return (medusaOrder?.items ?? [])
|
||||
.filter(({ product }) => product?.handle.startsWith(ANALYSIS_ELEMENT_HANDLE_PREFIX))
|
||||
.map((line) => {
|
||||
const analysisElementId = Number(line.product?.handle?.replace(ANALYSIS_ELEMENT_HANDLE_PREFIX, ''));
|
||||
if (Number.isNaN(analysisElementId)) {
|
||||
return null;
|
||||
}
|
||||
return { analysisElementId };
|
||||
}) as { analysisElementId: number }[];
|
||||
}
|
||||
|
||||
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
|
||||
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
|
||||
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
|
||||
if (orderedPackageIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
console.info(`Order has ${orderedPackageIds.length} packages`);
|
||||
const { response: { products: orderedPackagesProducts } } = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, id: orderedPackageIds },
|
||||
});
|
||||
console.info(`Order has ${orderedPackagesProducts.length} packages`);
|
||||
if (orderedPackagesProducts.length !== orderedPackageIds.length) {
|
||||
throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`);
|
||||
}
|
||||
|
||||
const ids = getAnalysisElementMedusaProductIds(orderedPackagesProducts);
|
||||
const { response: { products: analysisPackagesProducts } } = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, id: ids },
|
||||
});
|
||||
if (analysisPackagesProducts.length !== ids.length) {
|
||||
throw new Error(`Got ${analysisPackagesProducts.length} analysis packages products, expected ${ids.length}`);
|
||||
}
|
||||
|
||||
const originalIds = analysisPackagesProducts
|
||||
.map(({ metadata }) => metadata?.analysisIdOriginal)
|
||||
.filter((id) => typeof id === 'string');
|
||||
if (originalIds.length !== ids.length) {
|
||||
throw new Error(`Got ${originalIds.length} analysis packages products with analysisIdOriginal, expected ${ids.length}`);
|
||||
}
|
||||
const analysisElements = await getAnalysisElements({ originalIds });
|
||||
|
||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||
}
|
||||
|
||||
const analysisPackageElements = await getOrderedAnalysisPackages(medusaOrder);
|
||||
const orderedAnalysisElements = getOrderedAnalysisElements(medusaOrder);
|
||||
|
||||
return [...analysisPackageElements, ...orderedAnalysisElements];
|
||||
}
|
||||
|
||||
85
lib/services/medipost.types.ts
Normal file
85
lib/services/medipost.types.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export interface IUuringElement {
|
||||
UuringIdOID: string;
|
||||
UuringId: string;
|
||||
TLyhend: string;
|
||||
KNimetus: string;
|
||||
UuringNimi: string;
|
||||
Jarjekord: number;
|
||||
Kood: {
|
||||
HkKood: string;
|
||||
HkKoodiKordaja: number;
|
||||
Koefitsient: number;
|
||||
Hind: number;
|
||||
}[];
|
||||
UuringuElement: {
|
||||
UuringIdOID: string;
|
||||
UuringId: string;
|
||||
TLyhend: string;
|
||||
KNimetus: string;
|
||||
UuringNimi: string;
|
||||
Jarjekord: number;
|
||||
Kood: {
|
||||
HkKood: string;
|
||||
HkKoodiKordaja: number;
|
||||
Koefitsient: number;
|
||||
Hind: number;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IMaterialGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface IMedipostPublicMessageDataParsed {
|
||||
ANSWER: {
|
||||
CODE: number;
|
||||
MESSAGE: string;
|
||||
};
|
||||
Saadetis: {
|
||||
Teenused: {
|
||||
Teostaja: {
|
||||
UuringuGrupp: {
|
||||
UuringuGruppId: string;
|
||||
UuringuGruppNimi: string;
|
||||
UuringuGruppJarjekord: number;
|
||||
Kood: {
|
||||
HkKood: string;
|
||||
HkKoodiKordaja: number;
|
||||
Koefitsient: number;
|
||||
Hind: number;
|
||||
}[];
|
||||
Uuring: {
|
||||
UuringId: string;
|
||||
UuringNimi: string;
|
||||
UuringJarjekord: number;
|
||||
UuringuElement: {
|
||||
UuringIdOID: string;
|
||||
UuringId: string;
|
||||
TLyhend: string;
|
||||
KNimetus: string;
|
||||
UuringNimi: string;
|
||||
Jarjekord: number;
|
||||
Kood: {
|
||||
HkKood: string;
|
||||
HkKoodiKordaja: number;
|
||||
Koefitsient: number;
|
||||
Hind: number;
|
||||
}[];
|
||||
UuringuElement: IUuringElement;
|
||||
}[];
|
||||
MaterjalideGrupp: IMaterialGroup[];
|
||||
Kood: {
|
||||
HkKood: string;
|
||||
HkKoodiKordaja: number;
|
||||
Koefitsient: number;
|
||||
Hind: number;
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}
|
||||
189
lib/services/medipostTest.service.ts
Normal file
189
lib/services/medipostTest.service.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
getClientInstitution,
|
||||
getClientPerson,
|
||||
getPais,
|
||||
getPatient,
|
||||
getProviderInstitution,
|
||||
} from '@/lib/templates/medipost-order';
|
||||
import {
|
||||
MedipostAction,
|
||||
} from '@/lib/types/medipost';
|
||||
import axios from 'axios';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { formatDate } from 'date-fns';
|
||||
import { getAnalyses } from './analyses.service';
|
||||
import { getAnalysisElementsAdmin } from './analysis-element.service';
|
||||
import { validateMedipostResponse } from './medipost.service';
|
||||
|
||||
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!;
|
||||
|
||||
export async function sendPrivateMessageTestResponse({
|
||||
messageXml,
|
||||
}: {
|
||||
messageXml: string;
|
||||
}) {
|
||||
const body = new FormData();
|
||||
body.append('Action', MedipostAction.SendPrivateMessage);
|
||||
body.append('User', USER);
|
||||
body.append('Password', PASSWORD);
|
||||
body.append('Receiver', RECIPIENT);
|
||||
body.append('MessageType', 'Vastus');
|
||||
body.append(
|
||||
'Message',
|
||||
new Blob([messageXml], {
|
||||
type: 'text/xml; charset=UTF-8',
|
||||
}),
|
||||
);
|
||||
|
||||
const { data } = await axios.post(BASE_URL, body);
|
||||
await validateMedipostResponse(data);
|
||||
}
|
||||
|
||||
function getRandomInt(min: number, max: number) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
export async function composeOrderTestResponseXML({
|
||||
person,
|
||||
orderedAnalysisElementsIds,
|
||||
orderedAnalysesIds,
|
||||
orderId,
|
||||
orderCreatedAt,
|
||||
}: {
|
||||
person: {
|
||||
idCode: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phone: string;
|
||||
};
|
||||
orderedAnalysisElementsIds: number[];
|
||||
orderedAnalysesIds: number[];
|
||||
orderId: string;
|
||||
orderCreatedAt: Date;
|
||||
}) {
|
||||
const analysisElements = await getAnalysisElementsAdmin({ ids: orderedAnalysisElementsIds });
|
||||
const analyses = await getAnalyses({ ids: orderedAnalysesIds });
|
||||
|
||||
const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] =
|
||||
uniqBy(
|
||||
(
|
||||
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ??
|
||||
[]
|
||||
).concat(
|
||||
analyses?.flatMap(
|
||||
({ analysis_elements }) => analysis_elements.analysis_groups,
|
||||
) ?? [],
|
||||
),
|
||||
'id',
|
||||
);
|
||||
|
||||
// Tellimuse olek:
|
||||
// 1 – Järjekorras, 2 – Ootel, 3 - Töös, 4 – Lõpetatud,
|
||||
// 5 – Tagasi lükatud, 6 – Tühistatud.
|
||||
const orderStatus = 4;
|
||||
const orderNumber = 'TSU000001200';
|
||||
|
||||
const allAnalysisElementsForGroups = analysisElements?.filter((element) => {
|
||||
return analysisGroups.some((group) => group.id === element.analysis_groups.id);
|
||||
});
|
||||
const addedIds = new Set<number>();
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
|
||||
${getPais(USER, RECIPIENT, orderCreatedAt, orderId, "AL")}
|
||||
<Vastus>
|
||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||
${getClientInstitution({ index: 1 })}
|
||||
${getProviderInstitution({ index: 1 })}
|
||||
${getClientPerson(person)}
|
||||
<TellijaMarkused>Siia tuleb tellija poolne märkus</TellijaMarkused>
|
||||
|
||||
${getPatient(person)}
|
||||
|
||||
<Proov>
|
||||
<ProovinouIdOID>1.3.6.1.4.1.28284.1.625.2.17</ProovinouIdOID>
|
||||
<ProovinouId>16522314</ProovinouId>
|
||||
<MaterjaliTyypOID>1.3.6.1.4.1.28284.6.2.1.244.8</MaterjaliTyypOID>
|
||||
<MaterjaliTyyp>119297000</MaterjaliTyyp>
|
||||
<MaterjaliNimi>Veri</MaterjaliNimi>
|
||||
<Ribakood>16522314</Ribakood>
|
||||
<Jarjenumber>1</Jarjenumber>
|
||||
<VotmisAeg>2022-08-19 08:53:00</VotmisAeg>
|
||||
<SaabumisAeg>2022-08-23 15:10:00</SaabumisAeg>
|
||||
</Proov>
|
||||
|
||||
<TellimuseNumber>${orderNumber}</TellimuseNumber>
|
||||
|
||||
<TellimuseOlek>${orderStatus}</TellimuseOlek>
|
||||
${allAnalysisElementsForGroups.map((analysisElement) => {
|
||||
const group = analysisGroups.find((group) => group.id === analysisElement.analysis_groups.id);
|
||||
if (!group) {
|
||||
throw new Error(`Failed to find group for analysis element ${analysisElement.id}`);
|
||||
}
|
||||
|
||||
let relatedAnalysisElement = analysisElements?.find(
|
||||
(element) => element.analysis_groups.id === group.id && !addedIds.has(element.id),
|
||||
);
|
||||
const relatedAnalyses = analyses?.filter((analysis) => {
|
||||
return analysis.analysis_elements.analysis_groups.id === group.id && !addedIds.has(analysis.analysis_elements.id);
|
||||
});
|
||||
|
||||
if (!relatedAnalysisElement) {
|
||||
relatedAnalysisElement = relatedAnalyses?.find(
|
||||
(relatedAnalysis) =>
|
||||
relatedAnalysis.analysis_elements.analysis_groups.id ===
|
||||
group.id,
|
||||
)?.analysis_elements;
|
||||
}
|
||||
|
||||
if (!relatedAnalysisElement || !relatedAnalysisElement.material_groups) {
|
||||
throw new Error(
|
||||
`Failed to find related analysis element for group ${group.name} (id: ${group.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
const lower = getRandomInt(0, 100);
|
||||
const upper = getRandomInt(lower + 1, 500);
|
||||
const result = getRandomInt(lower, upper);
|
||||
addedIds.add(relatedAnalysisElement.id);
|
||||
return (`
|
||||
<UuringuGrupp>
|
||||
<UuringuGruppId>${group.original_id}</UuringuGruppId>
|
||||
<UuringuGruppNimi>${group.name}</UuringuGruppNimi>
|
||||
<Uuring>
|
||||
<UuringuElement>
|
||||
<UuringIdOID>${relatedAnalysisElement.analysis_id_oid}</UuringIdOID>
|
||||
<UuringId>${relatedAnalysisElement.analysis_id_original}</UuringId>
|
||||
<TLyhend>${relatedAnalysisElement.tehik_short_loinc}</TLyhend>
|
||||
<KNimetus>${relatedAnalysisElement.tehik_loinc_name}</KNimetus>
|
||||
<UuringNimi>${relatedAnalysisElement.analysis_name_lab ?? relatedAnalysisElement.tehik_loinc_name}</UuringNimi>
|
||||
<TellijaUuringId>${relatedAnalysisElement.id}</TellijaUuringId>
|
||||
<TeostajaUuringId>${relatedAnalysisElement.id}</TeostajaUuringId>
|
||||
<UuringOlek>4</UuringOlek>
|
||||
<Mootyhik>%</Mootyhik>
|
||||
<UuringuVastus>
|
||||
<VastuseVaartus>${result}</VastuseVaartus>
|
||||
<VastuseAeg>${formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss')}</VastuseAeg>
|
||||
<NormYlem kaasaarvatud=\"EI\">${upper}</NormYlem>
|
||||
<NormAlum kaasaarvatud=\"EI\">${lower}</NormAlum>
|
||||
<NormiStaatus>0</NormiStaatus>
|
||||
<ProoviJarjenumber>1</ProoviJarjenumber>
|
||||
</UuringuVastus>
|
||||
</UuringuElement>
|
||||
<UuringuTaitjaAsutuseJnr>2</UuringuTaitjaAsutuseJnr>
|
||||
</Uuring>
|
||||
</UuringuGrupp>
|
||||
`);
|
||||
}).join('')}
|
||||
</Vastus>
|
||||
</Saadetis>`;
|
||||
}
|
||||
86
lib/services/order.service.ts
Normal file
86
lib/services/order.service.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import type { Tables } from '@kit/supabase/database';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import type { StoreOrder } from '@medusajs/types';
|
||||
|
||||
export async function createOrder({
|
||||
medusaOrder,
|
||||
orderedAnalysisElements,
|
||||
}: {
|
||||
medusaOrder: StoreOrder;
|
||||
orderedAnalysisElements: { analysisElementId: number }[];
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const orderResult = await supabase.schema('medreport')
|
||||
.from('analysis_orders')
|
||||
.insert({
|
||||
analysis_element_ids: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId),
|
||||
analysis_ids: [],
|
||||
status: 'QUEUED',
|
||||
user_id: user.id,
|
||||
medusa_order_id: medusaOrder.id,
|
||||
})
|
||||
.select('id')
|
||||
.single()
|
||||
.throwOnError();
|
||||
|
||||
if (orderResult.error || !orderResult.data?.id) {
|
||||
throw new Error(`Failed to create order, message=${orderResult.error}, data=${JSON.stringify(orderResult)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateOrder({
|
||||
orderId,
|
||||
orderStatus,
|
||||
}: {
|
||||
orderId: number;
|
||||
orderStatus: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
||||
}) {
|
||||
const { error } = await getSupabaseServerClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_orders')
|
||||
.update({
|
||||
status: orderStatus,
|
||||
})
|
||||
.eq('id', orderId)
|
||||
.throwOnError();
|
||||
if (error) {
|
||||
throw new Error(`Failed to update order, message=${error}, data=${JSON.stringify(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrder({
|
||||
medusaOrderId,
|
||||
}: {
|
||||
medusaOrderId: string;
|
||||
}) {
|
||||
const query = getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_orders')
|
||||
.select('*')
|
||||
.eq('medusa_order_id', medusaOrderId)
|
||||
|
||||
const { data: order } = await query.single().throwOnError();
|
||||
return order;
|
||||
}
|
||||
|
||||
export async function getOrders({
|
||||
orderStatus,
|
||||
}: {
|
||||
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
|
||||
} = {}) {
|
||||
const query = getSupabaseServerClient()
|
||||
.schema('medreport')
|
||||
.from('analysis_orders')
|
||||
.select('*')
|
||||
if (orderStatus) {
|
||||
query.eq('status', orderStatus);
|
||||
}
|
||||
const orders = await query.throwOnError();
|
||||
return orders.data;
|
||||
}
|
||||
18
lib/services/sync-entries.service.ts
Normal file
18
lib/services/sync-entries.service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { format } from 'date-fns';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
export const getLastCheckedDate = async () => {
|
||||
const { data: lastChecked } = await getSupabaseServerAdminClient()
|
||||
.schema('audit')
|
||||
.from('sync_entries')
|
||||
.select('created_at')
|
||||
.eq('status', 'SUCCESS')
|
||||
.order('created_at')
|
||||
.limit(1);
|
||||
const lastEntry = lastChecked?.[0];
|
||||
const lastCheckedDate = lastEntry
|
||||
? format(lastEntry.created_at, 'yyyy-MM-dd HH:mm:ss')
|
||||
: null;
|
||||
|
||||
return lastCheckedDate;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DATE_TIME_FORMAT } from '@/lib/constants';
|
||||
import { Tables } from '@/packages/supabase/src/database.types';
|
||||
import { format } from 'date-fns';
|
||||
import Isikukood, { Gender } from 'isikukood';
|
||||
import { Tables } from '@/packages/supabase/src/database.types';
|
||||
import { DATE_FORMAT, DATE_TIME_FORMAT } from '@/lib/constants';
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
@@ -8,26 +9,27 @@ export const getPais = (
|
||||
sender: string,
|
||||
recipient: string,
|
||||
createdAt: Date,
|
||||
messageId: number,
|
||||
orderId: string,
|
||||
packageName = "OL",
|
||||
) => {
|
||||
if (isProd) {
|
||||
// return correct data
|
||||
}
|
||||
return `<Pais>
|
||||
<Pakett versioon="20">OL</Pakett>
|
||||
<Pakett versioon="20">${packageName}</Pakett>
|
||||
<Saatja>${sender}</Saatja>
|
||||
<Saaja>${recipient}</Saaja>
|
||||
<Aeg>${format(createdAt, DATE_TIME_FORMAT)}</Aeg>
|
||||
<SaadetisId>${messageId}</SaadetisId>
|
||||
<SaadetisId>${orderId}</SaadetisId>
|
||||
<Email>argo@medreport.ee</Email>
|
||||
</Pais>`;
|
||||
};
|
||||
|
||||
export const getClientInstitution = () => {
|
||||
export const getClientInstitution = ({ index }: { index?: number } = {}) => {
|
||||
if (isProd) {
|
||||
// return correct data
|
||||
}
|
||||
return `<Asutus tyyp="TELLIJA">
|
||||
return `<Asutus tyyp="TELLIJA" ${index ? ` jarjenumber="${index}"` : ''}>
|
||||
<AsutuseId>16381793</AsutuseId>
|
||||
<AsutuseNimi>MedReport OÜ</AsutuseNimi>
|
||||
<AsutuseKood>TSU</AsutuseKood>
|
||||
@@ -35,11 +37,11 @@ export const getClientInstitution = () => {
|
||||
</Asutus>`;
|
||||
};
|
||||
|
||||
export const getProviderInstitution = () => {
|
||||
export const getProviderInstitution = ({ index }: { index?: number } = {}) => {
|
||||
if (isProd) {
|
||||
// return correct data
|
||||
}
|
||||
return `<Asutus tyyp="TEOSTAJA">
|
||||
return `<Asutus tyyp="TEOSTAJA" ${index ? ` jarjenumber="${index}"` : ''}>
|
||||
<AsutuseId>11107913</AsutuseId>
|
||||
<AsutuseNimi>Synlab HTI Tallinn</AsutuseNimi>
|
||||
<AsutuseKood>SLA</AsutuseKood>
|
||||
@@ -48,47 +50,60 @@ export const getProviderInstitution = () => {
|
||||
</Asutus>`;
|
||||
};
|
||||
|
||||
export const getClientPerson = () => {
|
||||
export const getClientPerson = ({
|
||||
idCode,
|
||||
firstName,
|
||||
lastName,
|
||||
phone,
|
||||
}: {
|
||||
idCode: string,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
phone: string,
|
||||
}) => {
|
||||
if (isProd) {
|
||||
// return correct data
|
||||
}
|
||||
return `<Personal tyyp="TELLIJA" jarjenumber="1">
|
||||
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||
<PersonalKood>D07907</PersonalKood>
|
||||
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||
<Telefon>+37258131202</Telefon>
|
||||
<PersonalKood>${idCode}</PersonalKood>
|
||||
<PersonalPerekonnaNimi>${lastName}</PersonalPerekonnaNimi>
|
||||
<PersonalEesNimi>${firstName}</PersonalEesNimi>
|
||||
${phone ? `<Telefon>${phone.startsWith('+372') ? phone : `+372${phone}`}</Telefon>` : ''}
|
||||
</Personal>`;
|
||||
};
|
||||
|
||||
export const getOrderEnteredPerson = () => {
|
||||
if (isProd) {
|
||||
// return correct data
|
||||
}
|
||||
return `<Personal tyyp="SISESTAJA" jarjenumber="1">
|
||||
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||
<PersonalKood>D07907</PersonalKood>
|
||||
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||
<Telefon>+37258131202</Telefon>
|
||||
</Personal>`;
|
||||
};
|
||||
// export const getOrderEnteredPerson = () => {
|
||||
// if (isProd) {
|
||||
// // return correct data
|
||||
// }
|
||||
// return `<Personal tyyp="SISESTAJA" jarjenumber="1">
|
||||
// <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||
// <PersonalKood>D07907</PersonalKood>
|
||||
// <PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||
// <PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||
// <Telefon>+37258131202</Telefon>
|
||||
// </Personal>`;
|
||||
// };
|
||||
|
||||
export const getPatient = (
|
||||
idCode: number,
|
||||
surname: string,
|
||||
export const getPatient = ({
|
||||
idCode,
|
||||
lastName,
|
||||
firstName,
|
||||
}: {
|
||||
idCode: string,
|
||||
lastName: string,
|
||||
firstName: string,
|
||||
birthDate: string,
|
||||
genderLetter: string,
|
||||
) => {
|
||||
}) => {
|
||||
const isikukood = new Isikukood(idCode);
|
||||
return `<Patsient>
|
||||
<IsikukoodiOID>1.3.6.1.4.1.28284.6.2.2.1</IsikukoodiOID>
|
||||
<Isikukood>${idCode}</Isikukood>
|
||||
<PerekonnaNimi>${surname}</PerekonnaNimi>
|
||||
<PerekonnaNimi>${lastName}</PerekonnaNimi>
|
||||
<EesNimi>${firstName}</EesNimi>
|
||||
<SynniAeg>${birthDate}</SynniAeg>
|
||||
<SynniAeg>${format(isikukood.getBirthday(), DATE_FORMAT)}</SynniAeg>
|
||||
<SuguOID>1.3.6.1.4.1.28284.6.2.3.16.2</SuguOID>
|
||||
<Sugu>${genderLetter}</Sugu>
|
||||
<Sugu>${isikukood.getGender() === Gender.MALE ? 'M' : 'N'}</Sugu>
|
||||
</Patsient>`;
|
||||
};
|
||||
|
||||
@@ -106,19 +121,19 @@ export const getConfidentiality = () => {
|
||||
</Konfidentsiaalsus>`;
|
||||
};
|
||||
|
||||
export const getOrderEnteredByPerson = () => {
|
||||
if (isProd) {
|
||||
// return correct data
|
||||
}
|
||||
return `
|
||||
<Personal tyyp="SISESTAJA" jarjenumber="1">
|
||||
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||
<PersonalKood>D07907</PersonalKood>
|
||||
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||
<Telefon>+37258131202</Telefon>
|
||||
</Personal>`;
|
||||
};
|
||||
// export const getOrderEnteredByPerson = () => {
|
||||
// if (isProd) {
|
||||
// // return correct data
|
||||
// }
|
||||
// return `
|
||||
// <Personal tyyp="SISESTAJA" jarjenumber="1">
|
||||
// <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||
// <PersonalKood>D07907</PersonalKood>
|
||||
// <PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||
// <PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||
// <Telefon>+37258131202</Telefon>
|
||||
// </Personal>`;
|
||||
// };
|
||||
|
||||
export const getSpecimen = (
|
||||
materialTypeOid: string,
|
||||
|
||||
9
lib/types/code.ts
Normal file
9
lib/types/code.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ICode {
|
||||
hk_code: string;
|
||||
hk_code_multiplier: number;
|
||||
coefficient: number;
|
||||
price: number;
|
||||
analysis_group_id: number | null;
|
||||
analysis_element_id: number | null;
|
||||
analysis_id: number | null;
|
||||
}
|
||||
@@ -224,3 +224,36 @@ export const ConfirmedLoadResponseSchema = z.object({
|
||||
ErrorMessage: z.union([z.string(), z.null()]),
|
||||
});
|
||||
export type ConfirmedLoadResponse = z.infer<typeof ConfirmedLoadResponseSchema>;
|
||||
|
||||
export interface ISearchLoadResponse {
|
||||
Value: string;
|
||||
Data: {
|
||||
T_Lic: {
|
||||
ID: number;
|
||||
Name: string;
|
||||
OnlineCanSelectWorker: boolean;
|
||||
Email: string | null;
|
||||
PersonalCodeRequired: boolean;
|
||||
Phone: string | null;
|
||||
}[];
|
||||
T_Service: {
|
||||
ID: number;
|
||||
ClinicID: number;
|
||||
Code: string;
|
||||
Description: string | null;
|
||||
Display: string;
|
||||
Duration: number;
|
||||
HasFreeCodes: boolean;
|
||||
Name: string;
|
||||
NetoDuration: number;
|
||||
OnlineHideDuration: number;
|
||||
OnlineHidePrice: number;
|
||||
Price: number;
|
||||
PricePeriods: string | null;
|
||||
RequiresPayment: boolean;
|
||||
SyncID: string;
|
||||
}[];
|
||||
};
|
||||
ErrorCode: number;
|
||||
ErrorMessage: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
export interface IMedipostResponseXMLBase {
|
||||
'?xml': {
|
||||
'@_version': string;
|
||||
'@_encoding': string;
|
||||
'@_standalone': 'yes' | 'no';
|
||||
};
|
||||
ANSWER?: { CODE: number };
|
||||
}
|
||||
|
||||
export type Message = {
|
||||
messageId: string;
|
||||
messageType: string;
|
||||
@@ -120,13 +129,7 @@ export type Teostaja = {
|
||||
Sisendparameeter?: Sisendparameeter | Sisendparameeter[]; //0...n
|
||||
};
|
||||
|
||||
export type MedipostPublicMessageResponse = {
|
||||
'?xml': {
|
||||
'@_version': string;
|
||||
'@_encoding': string;
|
||||
'@_standalone'?: 'yes' | 'no';
|
||||
};
|
||||
ANSWER?: { CODE: number };
|
||||
export type MedipostPublicMessageResponse = IMedipostResponseXMLBase & {
|
||||
Saadetis?: {
|
||||
Pais: {
|
||||
Pakett: { '#text': 'SL' | 'OL' | 'AL' | 'ME' }; // SL - Teenused, OL - Tellimus (meie poolt saadetav saatekiri), AL - Vastus (saatekirja vastus), ME - Teade
|
||||
@@ -186,13 +189,7 @@ export type ResponseUuringuGrupp = {
|
||||
};
|
||||
|
||||
// type for UuringuGrupp is correct, but some of this is generated by an LLM and should be checked if data in use
|
||||
export type MedipostOrderResponse = {
|
||||
'?xml': {
|
||||
'@_version': string;
|
||||
'@_encoding': string;
|
||||
'@_standalone': 'yes' | 'no';
|
||||
};
|
||||
ANSWER?: { CODE: number };
|
||||
export type MedipostOrderResponse = IMedipostResponseXMLBase & {
|
||||
Saadetis: {
|
||||
Pais: {
|
||||
Pakett: {
|
||||
@@ -206,7 +203,7 @@ export type MedipostOrderResponse = {
|
||||
Email: string;
|
||||
};
|
||||
Vastus: {
|
||||
ValisTellimuseId: number;
|
||||
ValisTellimuseId: string;
|
||||
Asutus: {
|
||||
'@_tyyp': string; // TEOSTAJA
|
||||
'@_jarjenumber': string;
|
||||
@@ -252,16 +249,16 @@ export type MedipostOrderResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export const AnalysisOrderStatus: Record<number, string> = {
|
||||
export const AnalysisOrderStatus = {
|
||||
1: 'QUEUED',
|
||||
2: 'ON_HOLD',
|
||||
3: 'PROCESSING',
|
||||
4: 'COMPLETED',
|
||||
5: 'REJECTED',
|
||||
6: 'CANCELLED',
|
||||
};
|
||||
} as const;
|
||||
export const NormStatus: Record<number, string> = {
|
||||
1: 'NORMAL',
|
||||
2: 'WARNING',
|
||||
3: 'REQUIRES_ATTENTION',
|
||||
};
|
||||
} as const;
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
"supabase:db:diff": "supabase db diff --schema auth --schema audit --schema medreport",
|
||||
"supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push",
|
||||
"supabase:typegen": "supabase gen types typescript --local > ./packages/supabase/src/database.types.ts",
|
||||
"supabase:db:dump:local": "supabase db dump --local --data-only",
|
||||
"sync-analysis-groups:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts",
|
||||
"sync-connected-online:dev": "NODE_ENV=local ts-node jobs/sync-connected-online.ts"
|
||||
"supabase:db:dump:local": "supabase db dump --local --data-only"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edge-csrf/nextjs": "2.5.3-cloudflare-rc1",
|
||||
|
||||
@@ -209,15 +209,10 @@ class AccountsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elementMap = new Map(
|
||||
analysisResponseElements.map((e) => [e.analysis_response_id, e]),
|
||||
);
|
||||
|
||||
return analysisResponses
|
||||
.filter((r) => elementMap.has(r.id))
|
||||
.map((r) => ({
|
||||
...r,
|
||||
element: elementMap.get(r.id)!,
|
||||
elements: analysisResponseElements.filter((e) => e.analysis_response_id === r.id),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ import { Database } from '@kit/supabase/database';
|
||||
|
||||
export type UserAnalysis =
|
||||
(Database['medreport']['Tables']['analysis_responses']['Row'] & {
|
||||
element: Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
||||
elements: Database['medreport']['Tables']['analysis_response_elements']['Row'][];
|
||||
})[];
|
||||
|
||||
@@ -87,8 +87,8 @@ export async function getOrSetCart(countryCode: string) {
|
||||
return cart;
|
||||
}
|
||||
|
||||
export async function updateCart(data: HttpTypes.StoreUpdateCart) {
|
||||
const cartId = await getCartId();
|
||||
export async function updateCart({ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string }) {
|
||||
const cartId = id || (await getCartId());
|
||||
|
||||
if (!cartId) {
|
||||
throw new Error(
|
||||
|
||||
@@ -27,8 +27,22 @@ export const listCategories = async (query?: Record<string, any>) => {
|
||||
}
|
||||
|
||||
export const getCategoryByHandle = async (categoryHandle: string[]) => {
|
||||
const handle = `${categoryHandle.join("/")}`
|
||||
const { product_categories } = await getProductCategories({
|
||||
handle: `${categoryHandle.join("/")}`,
|
||||
limit: 1,
|
||||
});
|
||||
return product_categories[0];
|
||||
}
|
||||
|
||||
export const getProductCategories = async ({
|
||||
handle,
|
||||
limit,
|
||||
fields = "*category_children, *products",
|
||||
}: {
|
||||
handle?: string;
|
||||
limit?: number;
|
||||
fields?: string;
|
||||
} = {}) => {
|
||||
const next = {
|
||||
...(await getCacheOptions("categories")),
|
||||
}
|
||||
@@ -38,12 +52,12 @@ export const getCategoryByHandle = async (categoryHandle: string[]) => {
|
||||
`/store/product-categories`,
|
||||
{
|
||||
query: {
|
||||
fields: "*category_children, *products",
|
||||
fields,
|
||||
handle,
|
||||
limit,
|
||||
},
|
||||
next,
|
||||
cache: "force-cache",
|
||||
//cache: "force-cache",
|
||||
}
|
||||
)
|
||||
.then(({ product_categories }) => product_categories[0])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const listProducts = async ({
|
||||
regionId,
|
||||
}: {
|
||||
pageParam?: number
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { "type_id[0]"?: string }
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { "type_id[0]"?: string; id?: string[] }
|
||||
countryCode?: string
|
||||
regionId?: string
|
||||
}): Promise<{
|
||||
@@ -145,7 +145,7 @@ export const listProductTypes = async (): Promise<{ productTypes: HttpTypes.Stor
|
||||
"/store/product-types",
|
||||
{
|
||||
next,
|
||||
cache: "force-cache",
|
||||
//cache: "force-cache",
|
||||
query: {
|
||||
fields: "id,value,metadata",
|
||||
},
|
||||
|
||||
@@ -490,6 +490,7 @@ export type Database = {
|
||||
analysis_ids: number[] | null
|
||||
created_at: string
|
||||
id: number
|
||||
medusa_order_id: string
|
||||
status: Database["medreport"]["Enums"]["analysis_order_status"]
|
||||
user_id: string
|
||||
}
|
||||
@@ -498,6 +499,7 @@ export type Database = {
|
||||
analysis_ids?: number[] | null
|
||||
created_at?: string
|
||||
id?: number
|
||||
medusa_order_id: string
|
||||
status: Database["medreport"]["Enums"]["analysis_order_status"]
|
||||
user_id: string
|
||||
}
|
||||
@@ -506,6 +508,7 @@ export type Database = {
|
||||
analysis_ids?: number[] | null
|
||||
created_at?: string
|
||||
id?: number
|
||||
medusa_order_id?: string
|
||||
status?: Database["medreport"]["Enums"]["analysis_order_status"]
|
||||
user_id?: string
|
||||
}
|
||||
|
||||
@@ -125,7 +125,8 @@
|
||||
},
|
||||
"analysisResults": {
|
||||
"pageTitle": "My analysis results",
|
||||
"description": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad:",
|
||||
"orderNewAnalysis": "Telli uued analüüsid"
|
||||
"description": "Super, you've already done your analysis. Here are your important results:",
|
||||
"descriptionEmpty": "If you've already done your analysis, your results will appear here soon.",
|
||||
"orderNewAnalysis": "Order new analyses"
|
||||
}
|
||||
}
|
||||
|
||||
4
public/locales/en/order-analysis.json
Normal file
4
public/locales/en/order-analysis.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Select analysis",
|
||||
"description": "Select the analysis that suits your needs"
|
||||
}
|
||||
@@ -149,6 +149,7 @@
|
||||
"analysisResults": {
|
||||
"pageTitle": "Minu analüüside vastused",
|
||||
"description": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad:",
|
||||
"descriptionEmpty": "Kui oled juba käinud analüüse andmas, siis varsti jõuavad siia sinu analüüside vastused.",
|
||||
"orderNewAnalysis": "Telli uued analüüsid"
|
||||
}
|
||||
}
|
||||
|
||||
4
public/locales/et/order-analysis.json
Normal file
4
public/locales/et/order-analysis.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Vali analüüs",
|
||||
"description": "Vali enda vajadustele sobiv analüüs"
|
||||
}
|
||||
2
supabase/migrations/20250804041940_medusa_order_id.sql
Normal file
2
supabase/migrations/20250804041940_medusa_order_id.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE medreport.analysis_orders
|
||||
ADD COLUMN medusa_order_id TEXT NOT NULL;
|
||||
19
utils/medusa-product.ts
Normal file
19
utils/medusa-product.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const getAnalysisElementMedusaProductIds = (products: ({ metadata?: { analysisElementMedusaProductIds?: string } | null } | null)[]) => {
|
||||
if (!products) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const mapped = products
|
||||
.flatMap((product) => {
|
||||
const value = product?.metadata?.analysisElementMedusaProductIds;
|
||||
try {
|
||||
return JSON.parse(value as string);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse analysisElementMedusaProductIds from analysis package, possibly invalid format", e);
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
return [...new Set(mapped)];
|
||||
}
|
||||
Reference in New Issue
Block a user