diff --git a/app/api/order/medipost-create/route.ts b/app/api/order/medipost-create/route.ts deleted file mode 100644 index 10b9ea1..0000000 --- a/app/api/order/medipost-create/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -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 }); -}; diff --git a/app/api/order/medipost-test-response/route.ts b/app/api/order/medipost-test-response/route.ts index fb4d518..40cf4b2 100644 --- a/app/api/order/medipost-test-response/route.ts +++ b/app/api/order/medipost-test-response/route.ts @@ -11,7 +11,7 @@ export async function POST(request: Request) { // return NextResponse.json({ error: 'This endpoint is only available in development mode' }, { status: 403 }); // } - const { medusaOrderId } = await request.json(); + const { medusaOrderId, maxItems = null } = await request.json(); const medusaOrder = await retrieveOrder(medusaOrderId) const medreportOrder = await getOrder({ medusaOrderId }); @@ -19,7 +19,8 @@ export async function POST(request: Request) { 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`); + console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} (${maxItems ?? 'all'}) ordered analysis elements`); + const idsToSend = typeof maxItems === 'number' ? orderedAnalysisElementsIds.slice(0, maxItems) : orderedAnalysisElementsIds; const messageXml = await composeOrderTestResponseXML({ person: { idCode: account.personal_code!, @@ -27,7 +28,7 @@ export async function POST(request: Request) { lastName: account.last_name ?? '', phone: account.phone ?? '', }, - orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId), + orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId), orderedAnalysesIds: [], orderId: medusaOrderId, orderCreatedAt: new Date(medreportOrder.created_at), diff --git a/app/home/(user)/(dashboard)/analysis-results/page.tsx b/app/home/(user)/(dashboard)/analysis-results/page.tsx index f96f28d..d31cce5 100644 --- a/app/home/(user)/(dashboard)/analysis-results/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/page.tsx @@ -15,10 +15,11 @@ import { PAGE_VIEW_ACTION, createPageViewLog, } from '~/lib/services/audit/pageView.service'; -import { getAnalysisOrders } from '~/lib/services/order.service'; +import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service'; +import { ButtonTooltip } from '~/components/ui/button-tooltip'; -import { loadUserAnalysis } from '../../_lib/server/load-user-analysis'; import Analysis from './_components/analysis'; +import { loadUserAnalysis } from '../../_lib/server/load-user-analysis'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -51,43 +52,16 @@ async function AnalysisResultsPage() { action: PAGE_VIEW_ACTION.VIEW_ANALYSIS_RESULTS, }); - const analysisElementIds = [ - ...new Set( - analysisOrders - ?.flatMap((order) => order.analysis_element_ids) - .filter(Boolean) as number[], - ), + const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [ + ...new Set(analysisOrders?.flatMap((order) => order.analysis_element_ids).filter(Boolean) as number[]), ]; - const analysisElements = await getAnalysisElements({ - ids: analysisElementIds, - }); - const analysisElementsWithResults = - analysisResponseElements - ?.sort((a, b) => { - if (!a.response_time || !b.response_time) { - return 0; - } - return ( - new Date(b.response_time).getTime() - - new Date(a.response_time).getTime() - ); - }) - .map((results) => ({ results })) ?? []; - const analysisElementsWithoutResults = analysisElements.filter( - (element) => - !analysisElementsWithResults?.some( - ({ results }) => - results.analysis_element_original_id === element.analysis_id_original, - ), - ); - const hasNoAnalysisElements = - analysisElementsWithResults.length === 0 && - analysisElementsWithoutResults.length === 0; + const analysisElementIds = getAnalysisElementIds(analysisOrders); + const analysisElements = await getAnalysisElements({ ids: analysisElementIds }); return ( - -
+ +

@@ -106,33 +80,44 @@ async function AnalysisResultsPage() {

-
- {analysisElementsWithResults.map(({ results }) => { - const analysisElement = analysisElements.find( - (element) => - element.analysis_id_original === - results.analysis_element_original_id, - ); - if (!analysisElement) { - return null; - } +
+ {analysisOrders.length > 0 && analysisElements.length > 0 ? analysisOrders.map((analysisOrder) => { + const analysisElementIds = getAnalysisElementIds([analysisOrder]); + const analysisElementsForOrder = analysisElements.filter((element) => analysisElementIds.includes(element.id)); return ( - +
+

+ +

+
+ + +
+
+ {analysisElementsForOrder.length > 0 ? analysisElementsForOrder.map((analysisElement) => { + const results = analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original); + if (!results) { + return ( + + ); + } + return ( + + ); + }) : ( +
+ +
+ )} +
+
); - })} - {analysisElementsWithoutResults.map((element) => ( - - ))} - {hasNoAnalysisElements && ( + }) : (
- +
)}
diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index bede603..5ee25aa 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -9,6 +9,9 @@ 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'; +import { createNotificationsApi } from '@kit/notifications/api'; +import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; +import { AccountWithParams } from '@kit/accounts/api'; const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages'; const MONTONIO_PAID_STATUS = 'PAID'; @@ -31,7 +34,22 @@ const env = () => z siteUrl: process.env.NEXT_PUBLIC_SITE_URL!, }); -const sendEmail = async ({ email, analysisPackageName, personName, partnerLocationName, language }: { email: string, analysisPackageName: string, personName: string, partnerLocationName: string, language: string }) => { +const sendEmail = async ({ + account, + email, + analysisPackageName, + personName, + partnerLocationName, + language, +}: { + account: AccountWithParams, + email: string, + analysisPackageName: string, + personName: string, + partnerLocationName: string, + language: string, +}) => { + const client = getSupabaseServerAdminClient(); try { const { renderSynlabAnalysisPackageEmail } = await import('@kit/email-templates'); const { getMailer } = await import('@kit/mailers'); @@ -55,6 +73,11 @@ const sendEmail = async ({ email, analysisPackageName, personName, partnerLocati .catch((error) => { throw new Error(`Failed to send email, message=${error}`); }); + await createNotificationsApi(client) + .createNotification({ + account_id: account.id, + body: html, + }); } catch (error) { throw new Error(`Failed to send email, message=${error}`); } @@ -62,13 +85,13 @@ const sendEmail = async ({ email, analysisPackageName, personName, partnerLocati export async function processMontonioCallback(orderToken: string) { const { language } = await createI18nServerInstance(); - + const secretKey = process.env.MONTONIO_SECRET_KEY as string; const decoded = jwt.verify(orderToken, secretKey, { algorithms: ['HS256'], }) as MontonioOrderToken; - + if (decoded.paymentStatus !== MONTONIO_PAID_STATUS) { throw new Error("Payment not successful"); } @@ -79,7 +102,7 @@ export async function processMontonioCallback(orderToken: string) { } try { - const [,, cartId] = decoded.merchantReferenceDisplay.split(':'); + const [, , cartId] = decoded.merchantReferenceDisplay.split(':'); if (!cartId) { throw new Error("Cart ID not found"); } @@ -89,6 +112,7 @@ export async function processMontonioCallback(orderToken: string) { throw new Error("Cart not found"); } + const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false }); const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder }); const orderId = await createOrder({ medusaOrder, orderedAnalysisElements }); @@ -96,7 +120,7 @@ export async function processMontonioCallback(orderToken: string) { const { productTypes } = await listProductTypes(); const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE); const analysisPackageOrderItem = medusaOrder.items?.find(({ product_type_id }) => product_type_id === analysisPackagesType?.id); - + const orderResult = { medusaOrderId: medusaOrder.id, email: medusaOrder.email, @@ -107,10 +131,10 @@ export async function processMontonioCallback(orderToken: string) { const { medusaOrderId, email, partnerLocationName, analysisPackageName } = orderResult; const personName = account.name; - + if (email && analysisPackageName) { try { - await sendEmail({ email, analysisPackageName, personName, partnerLocationName, language }); + await sendEmail({ account, email, analysisPackageName, personName, partnerLocationName, language }); } catch (error) { console.error("Failed to send email", error); } @@ -118,9 +142,9 @@ export async function processMontonioCallback(orderToken: string) { // @TODO send email for separate analyses console.error("Missing email or analysisPackageName", orderResult); } - + sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); - + return { success: true, orderId }; } catch (error) { console.error("Failed to place order", error); diff --git a/app/home/(user)/(dashboard)/order/page.tsx b/app/home/(user)/(dashboard)/order/page.tsx index 4b1d747..57b11c5 100644 --- a/app/home/(user)/(dashboard)/order/page.tsx +++ b/app/home/(user)/(dashboard)/order/page.tsx @@ -7,10 +7,11 @@ import { PageBody } from '@kit/ui/makerkit/page'; import pathsConfig from '~/config/paths.config'; import { Trans } from '@kit/ui/trans'; import { HomeLayoutPageHeader } from '../../_components/home-page-header'; -import OrdersTable from '../../_components/orders/orders-table'; import { withI18n } from '~/lib/i18n/with-i18n'; -import type { IOrderLineItem } from '../../_components/orders/types'; import { getAnalysisOrders } from '~/lib/services/order.service'; +import OrderBlock from '../../_components/orders/order-block'; +import React from 'react'; +import { Divider } from '@medusajs/ui'; export async function generateMetadata() { const { t } = await createI18nServerInstance(); @@ -29,42 +30,7 @@ async function OrdersPage() { redirect(pathsConfig.auth.signIn); } - const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages'); - const analysisPackageOrders: IOrderLineItem[] = medusaOrders.flatMap(({ id, items, payment_status, fulfillment_status }) => items - ?.filter((item) => item.product_type_id === analysisPackagesType?.id) - .map((item) => { - const localOrder = analysisOrders.find((order) => order.medusa_order_id === id); - if (!localOrder) { - return null; - } - return { - item, - medusaOrderId: id, - orderId: localOrder?.id, - orderStatus: localOrder.status, - analysis_element_ids: localOrder.analysis_element_ids, - } - }) - .filter((order) => order !== null) - || []); - - const otherOrders: IOrderLineItem[] = medusaOrders - .filter(({ items }) => items?.some((item) => item.product_type_id !== analysisPackagesType?.id)) - .flatMap(({ id, items, payment_status, fulfillment_status }) => items - ?.map((item) => { - const analysisOrder = analysisOrders.find((order) => order.medusa_order_id === id); - if (!analysisOrder) { - return null; - } - return { - item, - medusaOrderId: id, - orderId: analysisOrder.id, - orderStatus: analysisOrder.status, - } - }) - .filter((order) => order !== null) - || []); + const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages')!; return ( <> @@ -73,8 +39,27 @@ async function OrdersPage() { description={} /> - - + {analysisOrders.map((analysisOrder) => { + const medusaOrder = medusaOrders.find(({ id }) => id === analysisOrder.medusa_order_id); + if (!medusaOrder) { + return null; + } + + const medusaOrderItems = medusaOrder.items || []; + const medusaOrderItemsAnalysisPackages = medusaOrderItems.filter((item) => item.product_type_id === analysisPackagesType?.id); + const medusaOrderItemsOther = medusaOrderItems.filter((item) => item.product_type_id !== analysisPackagesType?.id); + + return ( + + + + + ) + })} ); diff --git a/app/home/(user)/_components/orders/order-block.tsx b/app/home/(user)/_components/orders/order-block.tsx new file mode 100644 index 0000000..077d761 --- /dev/null +++ b/app/home/(user)/_components/orders/order-block.tsx @@ -0,0 +1,36 @@ +import { AnalysisOrder } from "~/lib/services/order.service"; +import { Trans } from '@kit/ui/makerkit/trans'; +import { StoreOrderLineItem } from "@medusajs/types"; +import OrderItemsTable from "./order-items-table"; +import Link from "next/link"; +import { Eye } from "lucide-react"; + +export default function OrderBlock({ analysisOrder, itemsAnalysisPackage, itemsOther }: { + analysisOrder: AnalysisOrder, + itemsAnalysisPackage: StoreOrderLineItem[], + itemsOther: StoreOrderLineItem[], +}) { + return ( +
+

+ +

+
+
+ +
+ + + +
+
+ + +
+
+ ) +} diff --git a/app/home/(user)/_components/orders/order-items-table.tsx b/app/home/(user)/_components/orders/order-items-table.tsx new file mode 100644 index 0000000..48d502a --- /dev/null +++ b/app/home/(user)/_components/orders/order-items-table.tsx @@ -0,0 +1,77 @@ +import { Trans } from '@kit/ui/trans'; +import { + Table, + TableBody, + TableHead, + TableRow, + TableHeader, + TableCell, +} from '@kit/ui/table'; +import { StoreOrderLineItem } from "@medusajs/types"; +import { AnalysisOrder } from '~/lib/services/order.service'; +import { formatDate } from 'date-fns'; +import Link from 'next/link'; +import { Eye } from 'lucide-react'; + +export default function OrderItemsTable({ items, title, analysisOrder }: { + items: StoreOrderLineItem[]; + title: string; + analysisOrder: AnalysisOrder; +}) { + if (!items || items.length === 0) { + return null; + } + + return ( + + + + + + + + + + + + + + + + + + {items + .sort((a, b) => (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1) + .map((orderItem) => ( + + +

+ {orderItem.product_title} +

+
+ + + {formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')} + + + + + + + + + + + + + +
+ ))} +
+
+ ) +} diff --git a/app/home/(user)/_components/orders/orders-item.tsx b/app/home/(user)/_components/orders/orders-item.tsx deleted file mode 100644 index ea8943d..0000000 --- a/app/home/(user)/_components/orders/orders-item.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - TableCell, - TableRow, -} from '@kit/ui/table'; -import { Eye } from "lucide-react"; -import Link from "next/link"; -import { formatDate } from "date-fns"; -import { IOrderLineItem } from "./types"; -import { Trans } from '@kit/ui/trans'; - -export default function OrdersItem({ orderItem }: { - orderItem: IOrderLineItem, -}) { - return ( - - -

- {orderItem.item.product_title} -

-
- - - {formatDate(orderItem.item.created_at, 'dd.MM.yyyy HH:mm')} - - - - - - - - - - - - - -
- ) -} diff --git a/app/home/(user)/_components/orders/orders-table.tsx b/app/home/(user)/_components/orders/orders-table.tsx deleted file mode 100644 index 18f1872..0000000 --- a/app/home/(user)/_components/orders/orders-table.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Trans } from '@kit/ui/trans'; -import { - Table, - TableBody, - TableHead, - TableRow, - TableHeader, -} from '@kit/ui/table'; -import OrdersItem from "./orders-item"; -import { IOrderLineItem } from "./types"; - -export default function OrdersTable({ orderItems, title }: { - orderItems: IOrderLineItem[]; - title: string; -}) { - if (!orderItems || orderItems.length === 0) { - return null; - } - - return ( - - - - - - - - - - - - - - - - - - {orderItems - .sort((a, b) => (a.item.created_at ?? "") > (b.item.created_at ?? "") ? -1 : 1) - .map((orderItem) => ())} - -
- ) -} diff --git a/app/home/(user)/_components/orders/types.ts b/app/home/(user)/_components/orders/types.ts deleted file mode 100644 index 2c7140e..0000000 --- a/app/home/(user)/_components/orders/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { StoreOrderLineItem } from "@medusajs/types"; - -export interface IOrderLineItem { - item: StoreOrderLineItem; - medusaOrderId: string; - orderId: number; - orderStatus: string; -} diff --git a/components/select-analysis-package.tsx b/components/select-analysis-package.tsx index b4863cf..d6b74d2 100644 --- a/components/select-analysis-package.tsx +++ b/components/select-analysis-package.tsx @@ -1,6 +1,6 @@ "use client"; -import { use, useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index e19e6db..4490c0b 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -36,7 +36,7 @@ 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, updateOrder } from './order.service'; +import { getOrder, updateOrderStatus } from './order.service'; import { getAnalysisElements, getAnalysisElementsAdmin } from './analysis-element.service'; import { getAnalyses } from './analyses.service'; import { getAccountAdmin } from './account.service'; @@ -218,24 +218,30 @@ export async function readPrivateMessageResponse({ privateMessage.messageId, ); const messageResponse = privateMessageContent?.Saadetis?.Vastus; + const medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId; if (!messageResponse) { - throw new Error(`Private message response has no results yet`); + if (medusaOrderId === 'order_01K2JSJXR5XVNRWEAGB199RCKP') { + console.info("messageResponse", JSON.stringify(privateMessageContent, null, 2)); + } + throw new Error(`Private message response has no results yet for order=${medusaOrderId}`); } - console.info(`Private message content: ${JSON.stringify(privateMessageContent)}`); let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; try { - order = await getOrder({ medusaOrderId: messageResponse.ValisTellimuseId }); + order = await getOrder({ medusaOrderId }); } catch (e) { await deletePrivateMessage(privateMessage.messageId); - throw new Error(`Order not found by Medipost message ValisTellimuseId=${messageResponse.ValisTellimuseId}`); + throw new Error(`Order not found by Medipost message ValisTellimuseId=${medusaOrderId}`); } const status = await syncPrivateMessage({ messageResponse, order }); - if (status === 'COMPLETED') { - await updateOrder({ orderId: order.id, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); + if (status.isPartial) { + await updateOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); + messageIdProcessed = privateMessage.messageId; + } else if (status.isCompleted) { + await updateOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); await deletePrivateMessage(privateMessage.messageId); messageIdProcessed = privateMessage.messageId; } @@ -559,11 +565,11 @@ function getLatestMessage({ ); } -export async function syncPrivateMessage({ +async function syncPrivateMessage({ messageResponse, order, }: { - messageResponse: MedipostOrderResponse['Saadetis']['Vastus']; + messageResponse: NonNullable; order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; }) { const supabase = getSupabaseServerAdminClient() @@ -606,6 +612,9 @@ export async function syncPrivateMessage({ Tables<{ schema: 'medreport' }, 'analysis_response_elements'>, 'id' | 'created_at' | 'updated_at' >[] = []; + + const analysisResponseId = analysisResponse[0]!.id; + for (const analysisGroup of analysisGroups) { const groupItems = toArray( analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'], @@ -618,7 +627,7 @@ export async function syncPrivateMessage({ responses.push( ...elementAnalysisResponses.map((response) => ({ analysis_element_original_id: element.UuringId, - analysis_response_id: analysisResponse[0]!.id, + analysis_response_id: analysisResponseId, norm_lower: response.NormAlum?.['#text'] ?? null, norm_lower_included: response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah', @@ -640,11 +649,11 @@ export async function syncPrivateMessage({ .schema('medreport') .from('analysis_response_elements') .delete() - .eq('analysis_response_id', analysisResponse[0].id); + .eq('analysis_response_id', analysisResponseId); if (deleteError) { throw new Error( - `Failed to clean up response elements for response id ${analysisResponse[0].id}`, + `Failed to clean up response elements for response id ${analysisResponseId}`, ); } @@ -655,12 +664,23 @@ export async function syncPrivateMessage({ if (elementInsertError) { throw new Error( - `Failed to insert order response elements for response id ${analysisResponse[0].id}`, + `Failed to insert order response elements for response id ${analysisResponseId}`, ); } - console.info("status", AnalysisOrderStatus[messageResponse.TellimuseOlek], messageResponse.TellimuseOlek); - return AnalysisOrderStatus[messageResponse.TellimuseOlek]; + const { data: allOrderResponseElements} = await supabase + .schema('medreport') + .from('analysis_response_elements') + .select('*') + .eq('analysis_response_id', analysisResponseId) + .throwOnError(); + const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0; + if (allOrderResponseElements.length !== expectedOrderResponseElements) { + return { isPartial: true }; + } + + const statusFromResponse = AnalysisOrderStatus[messageResponse.TellimuseOlek]; + return { isCompleted: statusFromResponse === 'COMPLETED' }; } export async function sendOrderToMedipost({ @@ -688,7 +708,7 @@ export async function sendOrderToMedipost({ }); await sendPrivateMessage(orderXml); - await updateOrder({ orderId: medreportOrder.id, orderStatus: 'PROCESSING' }); + await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); } export async function getOrderedAnalysisElementsIds({ @@ -720,7 +740,7 @@ export async function getOrderedAnalysisElementsIds({ countryCode, queryParams: { limit: 100, id: orderedPackageIds }, }); - console.info(`Order has ${orderedPackagesProducts.length} packages`); + console.info(`Order has ${orderedPackagesProducts.length} packages = ${JSON.stringify(orderedPackageIds, null, 2)}`); if (orderedPackagesProducts.length !== orderedPackageIds.length) { throw new Error(`Got ${orderedPackagesProducts.length} ordered packages products, expected ${orderedPackageIds.length}`); } diff --git a/lib/services/order.service.ts b/lib/services/order.service.ts index 998d564..3725e5c 100644 --- a/lib/services/order.service.ts +++ b/lib/services/order.service.ts @@ -56,6 +56,30 @@ export async function updateOrder({ .throwOnError(); } +export async function updateOrderStatus({ + orderId, + medusaOrderId, + orderStatus, +}: { + orderId?: number; + medusaOrderId?: string; + orderStatus: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status']; +}) { + const orderIdParam = orderId; + const medusaOrderIdParam = medusaOrderId; + if (!orderIdParam && !medusaOrderIdParam) { + throw new Error('Either orderId or medusaOrderId must be provided'); + } + await getSupabaseServerAdminClient() + .schema('medreport') + .rpc('update_analysis_order_status', { + order_id: orderIdParam ?? -1, + status_param: orderStatus, + medusa_order_id_param: medusaOrderIdParam ?? '', + }) + .throwOnError(); +} + export async function getOrder({ medusaOrderId, orderId, @@ -91,6 +115,6 @@ export async function getAnalysisOrders({ if (orderStatus) { query.eq('status', orderStatus); } - const orders = await query.throwOnError(); + const orders = await query.order('created_at', { ascending: false }).throwOnError(); return orders.data; } diff --git a/lib/types/medipost.ts b/lib/types/medipost.ts index 0794fa0..b47d985 100644 --- a/lib/types/medipost.ts +++ b/lib/types/medipost.ts @@ -202,7 +202,7 @@ export type MedipostOrderResponse = IMedipostResponseXMLBase & { SaadetisId: string; Email: string; }; - Vastus: { + Vastus?: { ValisTellimuseId: string; Asutus: { '@_tyyp': string; // TEOSTAJA @@ -246,6 +246,9 @@ export type MedipostOrderResponse = IMedipostResponseXMLBase & { TellimuseOlek: keyof typeof AnalysisOrderStatus; UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[]; }; + Tellimus?: { + ValisTellimuseId: string; + } }; }; diff --git a/lib/utils.ts b/lib/utils.ts index 1eb8901..a9c34c4 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -18,3 +18,12 @@ export function toTitleCase(str?: string) { text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(), ); } + +export function sortByDate(a: T[] | undefined, key: keyof T): T[] | undefined { + return a?.sort((a, b) => { + if (!a[key] || !b[key]) { + return 0; + } + return new Date(b[key] as string).getTime() - new Date(a[key] as string).getTime(); + }); +} diff --git a/packages/email-templates/src/locales/en/synlab-email.json b/packages/email-templates/src/locales/en/synlab-email.json index 3270ecc..ae86859 100644 --- a/packages/email-templates/src/locales/en/synlab-email.json +++ b/packages/email-templates/src/locales/en/synlab-email.json @@ -1,9 +1,9 @@ { - "subject": "Your Synlab order has been placed - {{analysisPackageName}}", - "previewText": "Your Synlab order has been placed - {{analysisPackageName}}", - "heading": "Your Synlab order has been placed - {{analysisPackageName}}", + "subject": "Your Medreport order has been placed - {{analysisPackageName}}", + "previewText": "Your Medreport order has been placed - {{analysisPackageName}}", + "heading": "Your Medreport order has been placed - {{analysisPackageName}}", "hello": "Hello {{personName}},", - "lines1": "The order for {{analysisPackageName}} analysis package has been sent to the lab. Please go to the lab to collect the sample: SYNLAB - {{partnerLocationName}}", + "lines1": "The order for {{analysisPackageName}} analysis package has been sent to the lab. Please go to the lab to collect the sample: Medreport - {{partnerLocationName}}", "lines2": "If you are unable to go to the lab to collect the sample, you can go to any other suitable collection point - view locations and opening hours.", "lines3": "It is recommended to collect the sample in the morning (before 12:00) and not to eat or drink (water can be drunk).", "lines4": "At the collection point, select the order from the queue: the order from the doctor.", diff --git a/packages/email-templates/src/locales/et/synlab-email.json b/packages/email-templates/src/locales/et/synlab-email.json index fd11035..843aa87 100644 --- a/packages/email-templates/src/locales/et/synlab-email.json +++ b/packages/email-templates/src/locales/et/synlab-email.json @@ -1,9 +1,9 @@ { - "subject": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}", - "previewText": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}", - "heading": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}", + "subject": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}", + "previewText": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}", + "heading": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}", "hello": "Tere {{personName}},", - "lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: SYNLAB - {{partnerLocationName}}", + "lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: Medreport - {{partnerLocationName}}", "lines2": "Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - vaata asukohti ja lahtiolekuaegasid.", "lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).", "lines4": "Proovivõtupunktis valige järjekorrasüsteemis: saatekirjad alt eriarsti saatekiri.", diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 2c8c64b..9c0ec2f 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -1828,6 +1828,22 @@ export type Database = { } Returns: undefined } + update_analysis_order_status: { + Args: { + order_id: number + medusa_order_id_param: string + status_param: Database["medreport"]["Enums"]["analysis_order_status"] + } + Returns: { + analysis_element_ids: number[] | null + analysis_ids: number[] | null + created_at: string + id: number + medusa_order_id: string + status: Database["medreport"]["Enums"]["analysis_order_status"] + user_id: string + } + } upsert_order: { Args: { target_account_id: string diff --git a/public/locales/en/analysis-results.json b/public/locales/en/analysis-results.json index ada8eac..9f6a491 100644 --- a/public/locales/en/analysis-results.json +++ b/public/locales/en/analysis-results.json @@ -5,10 +5,12 @@ "orderNewAnalysis": "Order new analyses", "waitingForResults": "Waiting for results", "noAnalysisElements": "No analysis orders found", + "noAnalysisOrders": "No analysis orders found", "analysisDate": "Analysis result date", "results": { "range": { "normal": "Normal range" } - } + }, + "orderTitle": "Order number {{orderNumber}}" } \ No newline at end of file diff --git a/public/locales/et/analysis-results.json b/public/locales/et/analysis-results.json index 0acc140..6334d60 100644 --- a/public/locales/et/analysis-results.json +++ b/public/locales/et/analysis-results.json @@ -5,10 +5,12 @@ "orderNewAnalysis": "Telli uued analüüsid", "waitingForResults": "Tulemuse ootel", "noAnalysisElements": "Veel ei ole tellitud analüüse", + "noAnalysisOrders": "Veel ei ole analüüside tellimusi", "analysisDate": "Analüüsi vastuse kuupäev", "results": { "range": { "normal": "Normaalne vahemik" } - } + }, + "orderTitle": "Tellimus {{orderNumber}}" } \ No newline at end of file diff --git a/run-test-sync-local.sh b/run-test-sync-local.sh index 16d9645..509be1c 100644 --- a/run-test-sync-local.sh +++ b/run-test-sync-local.sh @@ -12,7 +12,7 @@ function send_medipost_test_response() { curl -X POST "$HOSTNAME/api/order/medipost-test-response" \ --header "x-jobs-api-key: $JOBS_API_TOKEN" \ --header 'Content-Type: application/json' \ - --data '{ "medusaOrderId": "'$MEDUSA_ORDER_ID'" }' + --data '{ "medusaOrderId": "'$MEDUSA_ORDER_ID'", "maxItems": 2 }' } function sync_analysis_results() { diff --git a/supabase/migrations/20250813204850_update_analysis_order_status.sql b/supabase/migrations/20250813204850_update_analysis_order_status.sql new file mode 100644 index 0000000..c02419f --- /dev/null +++ b/supabase/migrations/20250813204850_update_analysis_order_status.sql @@ -0,0 +1,33 @@ +-- Function "medreport.update_analysis_order_status" +-- Update an analysis order status +create + or replace function medreport.update_analysis_order_status ( + order_id bigint, + medusa_order_id_param text, + status_param medreport.analysis_order_status +) returns medreport.analysis_orders + set + search_path = '' as $$ +declare + updated_order medreport.analysis_orders; +begin + update medreport.analysis_orders + set status = status_param + where (id = order_id OR medusa_order_id = medusa_order_id_param) + returning * into updated_order; + + return updated_order; + +end; + +$$ language plpgsql; + +grant + execute on function medreport.update_analysis_order_status ( + bigint, + text, + medreport.analysis_order_status + ) to service_role; + +-- example: +-- select medreport.update_analysis_order_status(-1, 'order_01K1TQQHZGPXKDHAH81TDSNGXR', 'CANCELLED')