From 38d73e27ad59e64c75117f3097746b357b1c8fea Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:50:03 +0300 Subject: [PATCH 01/17] feat(MED-121): use age+sex specific analysis package variants --- .../_components/compare-packages-modal.tsx | 56 +++--- .../_components/order-analyses-cards.tsx | 27 +-- app/home/(user)/_lib/server/load-analyses.ts | 14 +- .../_lib/server/load-analysis-packages.ts | 161 +++++++++++++++--- lib/services/medusaCart.service.ts | 2 +- .../components/select-analysis-package.tsx | 62 +++---- .../components/select-analysis-packages.tsx | 15 +- utils/medusa-product.ts | 6 +- 8 files changed, 237 insertions(+), 106 deletions(-) diff --git a/app/home/(user)/_components/compare-packages-modal.tsx b/app/home/(user)/_components/compare-packages-modal.tsx index 0fbc24d..7c163bd 100644 --- a/app/home/(user)/_components/compare-packages-modal.tsx +++ b/app/home/(user)/_components/compare-packages-modal.tsx @@ -22,8 +22,14 @@ import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { PackageHeader } from '@kit/shared/components/package-header'; import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip'; import { StoreProduct } from '@medusajs/types'; -import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product'; -import { withI18n } from '@/lib/i18n/with-i18n'; +import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package'; +import { withI18n } from '~/lib/i18n/with-i18n'; + +export type AnalysisPackageElement = Pick & { + isIncludedInStandard: boolean; + isIncludedInStandardPlus: boolean; + isIncludedInPremium: boolean; +}; const CheckWithBackground = () => { return ( @@ -33,15 +39,15 @@ const CheckWithBackground = () => { ); }; -const PackageTableHead = async ({ product, nrOfAnalyses }: { product: StoreProduct, nrOfAnalyses: number }) => { +const PackageTableHead = async ({ product }: { product: AnalysisPackageWithVariant }) => { const { t, language } = await createI18nServerInstance(); - const variant = product.variants?.[0]; - const titleKey = product.title; - const price = variant?.calculated_price?.calculated_amount ?? 0; + + const { title, price, nrOfAnalyses } = product; + return ( { 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')!; + const standardPackage = analysisPackages.find(({ isStandard }) => isStandard); + const standardPlusPackage = analysisPackages.find(({ isStandardPlus }) => isStandardPlus); + const premiumPackage = analysisPackages.find(({ isPremium }) => isPremium); if (!standardPackage || !standardPlusPackage || !premiumPackage) { return null; } - const standardPackageAnalyses = getAnalysisElementMedusaProductIds([standardPackage]); - const standardPlusPackageAnalyses = getAnalysisElementMedusaProductIds([standardPlusPackage]); - const premiumPackageAnalyses = getAnalysisElementMedusaProductIds([premiumPackage]); - return ( {triggerElement} @@ -103,9 +105,9 @@ const ComparePackagesModal = async ({ - - - + + + @@ -115,29 +117,29 @@ const ComparePackagesModal = async ({ title, id, description, + isIncludedInStandard, + isIncludedInStandardPlus, + isIncludedInPremium, }, - index, ) => { if (!title) { return null; } - const includedInStandard = standardPackageAnalyses.includes(id); - const includedInStandardPlus = standardPlusPackageAnalyses.includes(id); - const includedInPremium = premiumPackageAnalyses.includes(id); + return ( - + {title}{' '} {description && (} />)} - {includedInStandard && } + {isIncludedInStandard && } - {(includedInStandard || includedInStandardPlus) && } + {(isIncludedInStandard || isIncludedInStandardPlus) && } - {(includedInStandard || includedInStandardPlus || includedInPremium) && } + {(isIncludedInStandard || isIncludedInStandardPlus || isIncludedInPremium) && } ); diff --git a/app/home/(user)/_components/order-analyses-cards.tsx b/app/home/(user)/_components/order-analyses-cards.tsx index 7cf627f..b6a2d75 100644 --- a/app/home/(user)/_components/order-analyses-cards.tsx +++ b/app/home/(user)/_components/order-analyses-cards.tsx @@ -9,30 +9,39 @@ import { CardFooter, CardDescription, } from '@kit/ui/card'; -import { StoreProduct, StoreProductVariant } from '@medusajs/types'; +import { StoreProduct } from '@medusajs/types'; import { useState } from 'react'; import { handleAddToCart } from '~/lib/services/medusaCart.service'; import { useRouter } from 'next/navigation'; import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip'; import { Trans } from '@kit/ui/trans'; +export type OrderAnalysisCard = Pick< + StoreProduct, 'title' | 'description' | 'subtitle' +> & { + isAvailable: boolean; + variant: { id: string }; +}; + export default function OrderAnalysesCards({ analyses, countryCode, }: { - analyses: StoreProduct[]; + analyses: OrderAnalysisCard[]; countryCode: string; }) { const router = useRouter(); const [isAddingToCart, setIsAddingToCart] = useState(false); - const handleSelect = async (selectedVariant: StoreProductVariant) => { - if (!selectedVariant?.id || isAddingToCart) return null + const handleSelect = async (variantId: string) => { + if (isAddingToCart) { + return null; + } setIsAddingToCart(true); try { await handleAddToCart({ - selectedVariant, + selectedVariant: { id: variantId }, countryCode, }); setIsAddingToCart(false); @@ -47,13 +56,11 @@ export default function OrderAnalysesCards({
{analyses.map(({ title, - variants, + variant, description, subtitle, - status, - metadata, + isAvailable, }) => { - const isAvailable = status === 'published' && !!metadata?.analysisIdOriginal; return ( handleSelect(variants![0]!)} + onClick={() => handleSelect(variant.id)} > {isAddingToCart ? : } diff --git a/app/home/(user)/_lib/server/load-analyses.ts b/app/home/(user)/_lib/server/load-analyses.ts index d6bc2e2..424ff25 100644 --- a/app/home/(user)/_lib/server/load-analyses.ts +++ b/app/home/(user)/_lib/server/load-analyses.ts @@ -3,6 +3,7 @@ import { cache } from 'react'; import { listProductTypes } from "@lib/data/products"; import { listRegions } from '@lib/data/regions'; import { getProductCategories } from '@lib/data/categories'; +import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; async function countryCodesLoader() { const countryCodes = await listRegions().then((regions) => @@ -34,7 +35,18 @@ async function analysesLoader() { const category = productCategories.find(({ metadata }) => metadata?.page === 'order-analysis'); return { - analyses: category?.products ?? [], + analyses: category?.products?.map(({ title, description, subtitle, variants, status, metadata }) => { + const variant = variants![0]!; + return { + title, + description, + subtitle, + variant: { + id: variant.id, + }, + isAvailable: status === 'published' && !!metadata?.analysisIdOriginal, + }; + }) ?? [], countryCode, } } diff --git a/app/home/(user)/_lib/server/load-analysis-packages.ts b/app/home/(user)/_lib/server/load-analysis-packages.ts index b7caa78..ddbf14c 100644 --- a/app/home/(user)/_lib/server/load-analysis-packages.ts +++ b/app/home/(user)/_lib/server/load-analysis-packages.ts @@ -1,9 +1,13 @@ import { cache } from 'react'; +import Isikukood, { Gender } from 'isikukood'; 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'; +import { loadCurrentUserAccount } from './load-user-account'; +import { AnalysisPackageWithVariant } from '~/components/select-analysis-package'; +import { AccountWithParams } from '@/packages/features/accounts/src/server/api'; async function countryCodesLoader() { const countryCodes = await listRegions().then((regions) => @@ -19,36 +23,153 @@ async function productTypesLoader() { } export const loadProductTypes = cache(productTypesLoader); -async function analysisPackagesLoader() { - const [countryCodes, productTypes] = await Promise.all([loadCountryCodes(), loadProductTypes()]); - const countryCode = countryCodes[0]!; +function userSpecificVariantLoader({ + account, +}: { + account: AccountWithParams; +}) { + const { personal_code: personalCode } = account; + if (!personalCode) { + throw new Error('Personal code not found'); + } + const parsed = new Isikukood(personalCode); + const ageRange = (() => { + const age = parsed.getAge(); + if (age >= 18 && age <= 29) { + return '18-29'; + } + if (age >= 30 && age <= 49) { + return '30-49'; + } + if (age >= 50 && age <= 59) { + return '50-59'; + } + if (age >= 60) { + return '60'; + } + throw new Error('Age range not supported'); + })(); + const gender = parsed.getGender() === Gender.MALE ? 'M' : 'F'; - let analysisPackages: StoreProduct[] = []; - let analysisPackageElements: StoreProduct[] = []; + return ({ + product, + }: { + product: StoreProduct; + }) => { + const variants = product.variants; + if (!variants) { + return null; + } - const productType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages'); - if (!productType) { - return { analysisPackageElements, analysisPackages, countryCode }; + const variant = variants.find((v) => v.options?.every((o) => [ageRange, gender].includes(o.value))); + if (!variant) { + return null; + } + return variant; + } +} + +async function analysisPackageElementsLoader({ + analysisPackagesWithVariant, + countryCode, +}: { + analysisPackagesWithVariant: AnalysisPackageWithVariant[]; + countryCode: string; +}) { + const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds(analysisPackagesWithVariant); + if (analysisElementMedusaProductIds.length === 0) { + return []; } + const { response: { products } } = await listProducts({ + countryCode, + queryParams: { + id: analysisElementMedusaProductIds, + limit: 100, + }, + }); + + const standardPackage = analysisPackagesWithVariant.find(({ isStandard }) => isStandard); + const standardPlusPackage = analysisPackagesWithVariant.find(({ isStandardPlus }) => isStandardPlus); + const premiumPackage = analysisPackagesWithVariant.find(({ isPremium }) => isPremium); + if (!standardPackage || !standardPlusPackage || !premiumPackage) { + return []; + } + + const standardPackageAnalyses = getAnalysisElementMedusaProductIds([standardPackage]); + const standardPlusPackageAnalyses = getAnalysisElementMedusaProductIds([standardPlusPackage]); + const premiumPackageAnalyses = getAnalysisElementMedusaProductIds([premiumPackage]); + + return products.map(({ id, title, description }) => ({ + id, + title, + description, + isIncludedInStandard: standardPackageAnalyses.includes(id), + isIncludedInStandardPlus: standardPlusPackageAnalyses.includes(id), + isIncludedInPremium: premiumPackageAnalyses.includes(id), + })); +} + +async function analysisPackagesWithVariantLoader({ + account, + countryCode, +}: { + account: AccountWithParams; + countryCode: string; +}) { + const productTypes = await loadProductTypes(); + const productType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages'); + if (!productType) { + return null; + } const analysisPackagesResponse = await listProducts({ countryCode, queryParams: { limit: 100, "type_id[0]": productType.id }, }); - 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; + const getVariant = userSpecificVariantLoader({ account }); + const analysisPackagesWithVariant = analysisPackagesResponse.response.products + .reduce((acc, product) => { + const variant = getVariant({ product }); + if (!variant) { + return acc; + } + return [ + ...acc, + { + variantId: variant.id, + nrOfAnalyses: getAnalysisElementMedusaProductIds([product]).length, + price: variant.calculated_price?.calculated_amount ?? 0, + title: product.title, + subtitle: product.subtitle, + description: product.description, + metadata: product.metadata, + isStandard: product.metadata?.analysisPackageTier === 'standard', + isStandardPlus: product.metadata?.analysisPackageTier === 'standard-plus', + isPremium: product.metadata?.analysisPackageTier === 'premium', + }, + ]; + }, [] as AnalysisPackageWithVariant[]); + + return analysisPackagesWithVariant; +} + +async function analysisPackagesLoader() { + const account = await loadCurrentUserAccount(); + if (!account) { + throw new Error('Account not found'); } - return { analysisPackageElements, analysisPackages, countryCode }; + const countryCodes = await loadCountryCodes(); + const countryCode = countryCodes[0]!; + + const analysisPackagesWithVariant = await analysisPackagesWithVariantLoader({ account, countryCode }); + if (!analysisPackagesWithVariant) { + return { analysisPackageElements: [], analysisPackages: [], countryCode }; + } + + const analysisPackageElements = await analysisPackageElementsLoader({ analysisPackagesWithVariant, countryCode }); + + return { analysisPackageElements, analysisPackages: analysisPackagesWithVariant, countryCode }; } export const loadAnalysisPackages = cache(analysisPackagesLoader); diff --git a/lib/services/medusaCart.service.ts b/lib/services/medusaCart.service.ts index a84c596..e838c79 100644 --- a/lib/services/medusaCart.service.ts +++ b/lib/services/medusaCart.service.ts @@ -31,7 +31,7 @@ export async function handleAddToCart({ selectedVariant, countryCode, }: { - selectedVariant: StoreProductVariant + selectedVariant: Pick countryCode: string }) { const supabase = getSupabaseServerClient(); diff --git a/packages/shared/src/components/select-analysis-package.tsx b/packages/shared/src/components/select-analysis-package.tsx index 69fa24c..1709138 100644 --- a/packages/shared/src/components/select-analysis-package.tsx +++ b/packages/shared/src/components/select-analysis-package.tsx @@ -5,9 +5,10 @@ import { useState } from 'react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import { StoreProduct, StoreProductVariant } from '@medusajs/types'; +import { StoreProduct } from '@medusajs/types'; import { Button } from '@medusajs/ui'; import { useTranslation } from 'react-i18next'; +import { handleAddToCart } from '../../../../lib/services/medusaCart.service'; import { Card, @@ -17,25 +18,24 @@ import { CardHeader, } from '@kit/ui/card'; import { Trans } from '@kit/ui/trans'; - -import { handleAddToCart } from '../../../../lib/services/medusaCart.service'; -import { getAnalysisElementMedusaProductIds } from '../../../../utils/medusa-product'; -import { PackageHeader } from './package-header'; import { ButtonTooltip } from './ui/button-tooltip'; +import { PackageHeader } from './package-header'; -export interface IAnalysisPackage { - titleKey: string; +export type AnalysisPackageWithVariant = Pick & { + variantId: string; + nrOfAnalyses: number; price: number; - tagColor: string; - descriptionKey: string; -} + isStandard: boolean; + isStandardPlus: boolean; + isPremium: boolean; +}; export default function SelectAnalysisPackage({ analysisPackage, countryCode, }: { - analysisPackage: StoreProduct; - countryCode: string; + analysisPackage: AnalysisPackageWithVariant; + countryCode: string, }) { const router = useRouter(); const { @@ -44,35 +44,21 @@ export default function SelectAnalysisPackage({ } = useTranslation(); const [isAddingToCart, setIsAddingToCart] = useState(false); - const handleSelect = async (selectedVariant: StoreProductVariant) => { - if (!selectedVariant?.id) return null; + + const { nrOfAnalyses, variantId, title, subtitle = '', description = '', price } = analysisPackage; + const handleSelect = async () => { setIsAddingToCart(true); await handleAddToCart({ - selectedVariant, + selectedVariant: { id: variantId }, countryCode, }); setIsAddingToCart(false); router.push('/home/cart'); }; - const titleKey = analysisPackage.title; - const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds([ - analysisPackage, - ]); - const nrOfAnalyses = analysisElementMedusaProductIds.length; - const description = analysisPackage.description ?? ''; - const subtitle = analysisPackage.subtitle ?? ''; - const variant = analysisPackage.variants?.[0]; - - if (!variant) { - return null; - } - - const price = variant.calculated_price?.calculated_amount ?? 0; - return ( - + {description && ( {subtitle} - diff --git a/packages/shared/src/components/select-analysis-packages.tsx b/packages/shared/src/components/select-analysis-packages.tsx index 921c696..2206676 100644 --- a/packages/shared/src/components/select-analysis-packages.tsx +++ b/packages/shared/src/components/select-analysis-packages.tsx @@ -1,14 +1,19 @@ import { Trans } from '@kit/ui/trans'; -import { StoreProduct } from '@medusajs/types'; -import SelectAnalysisPackage from './select-analysis-package'; +import SelectAnalysisPackage, { AnalysisPackageWithVariant } from './select-analysis-package'; -export default function SelectAnalysisPackages({ analysisPackages, countryCode }: { analysisPackages: StoreProduct[], countryCode: string }) { +export default function SelectAnalysisPackages({ + analysisPackages, + countryCode, +}: { + analysisPackages: AnalysisPackageWithVariant[]; + countryCode: string; +}) { return (
{analysisPackages.length > 0 ? analysisPackages.map( - (product) => ( - + (analysisPackage) => ( + )) : (

diff --git a/utils/medusa-product.ts b/utils/medusa-product.ts index ef4384f..6cbbb3b 100644 --- a/utils/medusa-product.ts +++ b/utils/medusa-product.ts @@ -1,4 +1,8 @@ -export const getAnalysisElementMedusaProductIds = (products: ({ metadata?: { analysisElementMedusaProductIds?: string } | null } | null)[]) => { +export const getAnalysisElementMedusaProductIds = (products: ({ + metadata?: { + analysisElementMedusaProductIds?: string; + } | null; +} | null)[]) => { if (!products) { return []; } From b6a0940506597a8d5fbf7f72686a59e68aa739d5 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:50:22 +0300 Subject: [PATCH 02/17] feat(MED-87): don't show 'waiting for results' text if order is cancelled --- .../analysis-results/_components/analysis.tsx | 14 +++++++++----- .../(user)/(dashboard)/analysis-results/page.tsx | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx index 0535ffe..67579b4 100644 --- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx @@ -37,9 +37,11 @@ export enum AnalysisStatus { const Analysis = ({ analysisElement, results, + isCancelled, }: { - analysisElement: Pick; - results?: AnalysisResultForDisplay; + analysisElement: AnalysisElement; + results?: UserAnalysisElement; + isCancelled?: boolean; }) => { const name = analysisElement.analysis_name_lab || ''; const status = results?.norm_status || AnalysisStatus.NORMAL; @@ -121,9 +123,11 @@ const Analysis = ({ ) : ( <>
-
- -
+ {!isCancelled && ( +
+ +
+ )}
diff --git a/app/home/(user)/(dashboard)/analysis-results/page.tsx b/app/home/(user)/(dashboard)/analysis-results/page.tsx index 1ac3d5e..608d8ef 100644 --- a/app/home/(user)/(dashboard)/analysis-results/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/page.tsx @@ -104,7 +104,7 @@ async function AnalysisResultsPage() { && analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original); if (!results) { return ( - + ); } return ( From e8e762e7ee9a2d9f2ee42cfe8353d1ef5e280103 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:50:32 +0300 Subject: [PATCH 03/17] feat(MED-100): update analysis level bars --- .../_components/analysis-level-bar.tsx | 20 +++++++++++++------ .../analysis-results/_components/analysis.tsx | 14 ++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx index b808403..71a7036 100644 --- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis-level-bar.tsx @@ -85,35 +85,43 @@ const AnalysisLevelBar = ({ return calculated; }, [value, upper, lower]); + const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(() => [ + level === AnalysisResultLevel.VERY_LOW, + level === AnalysisResultLevel.LOW, + level === AnalysisResultLevel.HIGH, + level === AnalysisResultLevel.VERY_HIGH, + ], [level, value, upper, lower]); + + const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh; + return (
{normLowerIncluded && ( <> - + )} {normUpperIncluded && ( <> diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx index 67579b4..5ad0f8e 100644 --- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx @@ -108,7 +108,7 @@ const Analysis = ({
{unit}
- {normLower} - {normUpper} + {normLower} - {normUpper} %
@@ -120,19 +120,17 @@ const Analysis = ({ level={analysisResultLevel!} /> - ) : ( + ) : (isCancelled ? null : ( <>
- {!isCancelled && ( -
- -
- )} +
+ +
- )} + ))}
); }; From 1d1b10d094bc417701c1a684868fd9164655a0bf Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:50:47 +0300 Subject: [PATCH 04/17] feat(MED-100): show analysis packages and analyses in separate block from tto services --- app/home/(user)/(dashboard)/cart/page.tsx | 21 ++++++++++++++------- app/home/(user)/_components/cart/index.tsx | 16 ++++++++-------- public/locales/en/cart.json | 6 +++--- public/locales/et/cart.json | 6 +++--- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/app/home/(user)/(dashboard)/cart/page.tsx b/app/home/(user)/(dashboard)/cart/page.tsx index 8eed4f6..250412b 100644 --- a/app/home/(user)/(dashboard)/cart/page.tsx +++ b/app/home/(user)/(dashboard)/cart/page.tsx @@ -25,21 +25,28 @@ export default async function CartPage() { const { productTypes } = await listProductTypes(); const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages'); - const analysisPackages = analysisPackagesType && cart?.items - ? cart.items.filter((item) => item.product?.type_id === analysisPackagesType.id) + const synlabAnalysisType = productTypes.find(({ metadata }) => metadata?.handle === 'synlab-analysis'); + const synlabAnalyses = analysisPackagesType && synlabAnalysisType && cart?.items + ? cart.items.filter((item) => { + const productTypeId = item.product?.type_id; + if (!productTypeId) { + return false; + } + return [analysisPackagesType.id, synlabAnalysisType.id].includes(productTypeId); + }) : []; - const otherItems = cart?.items?.filter((item) => item.product?.type_id !== analysisPackagesType?.id) ?? []; + const ttoServiceItems = cart?.items?.filter((item) => !synlabAnalyses.some((analysis) => analysis.id === item.id)) ?? []; - const otherItemsSorted = otherItems.sort((a, b) => (a.updated_at ?? "") > (b.updated_at ?? "") ? -1 : 1); + const otherItemsSorted = ttoServiceItems.sort((a, b) => (a.updated_at ?? "") > (b.updated_at ?? "") ? -1 : 1); const item = otherItemsSorted[0]; - const hasItemsWithTimer = false as boolean; + const isTimerShown = ttoServiceItems.length > 0 && !!item && !!item.updated_at; return ( }> - {hasItemsWithTimer && item && item.updated_at && } + {isTimerShown && } - + ); } diff --git a/app/home/(user)/_components/cart/index.tsx b/app/home/(user)/_components/cart/index.tsx index 4e0086b..9f2b0fd 100644 --- a/app/home/(user)/_components/cart/index.tsx +++ b/app/home/(user)/_components/cart/index.tsx @@ -22,12 +22,12 @@ const IS_DISCOUNT_SHOWN = false as boolean; export default function Cart({ cart, - analysisPackages, - otherItems, + synlabAnalyses, + ttoServiceItems, }: { cart: StoreCart | null - analysisPackages: StoreCartLineItem[]; - otherItems: StoreCartLineItem[]; + synlabAnalyses: StoreCartLineItem[]; + ttoServiceItems: StoreCartLineItem[]; }) { const { i18n: { language } } = useTranslation(); @@ -68,13 +68,13 @@ export default function Cart({ } const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0; - const isLocationsShown = analysisPackages.length > 0; + const isLocationsShown = synlabAnalyses.length > 0; return (
- - + +
{hasCartItems && (
@@ -121,7 +121,7 @@ export default function Cart({

- + )} diff --git a/public/locales/en/cart.json b/public/locales/en/cart.json index 3913a78..1580536 100644 --- a/public/locales/en/cart.json +++ b/public/locales/en/cart.json @@ -30,10 +30,10 @@ "placeholder": "Enter promotion code" }, "items": { - "analysisPackages": { - "productColumnLabel": "Package name" + "synlabAnalyses": { + "productColumnLabel": "Analysis name" }, - "services": { + "ttoServices": { "productColumnLabel": "Service name" }, "delete": { diff --git a/public/locales/et/cart.json b/public/locales/et/cart.json index 06ef7b5..efb1423 100644 --- a/public/locales/et/cart.json +++ b/public/locales/et/cart.json @@ -31,10 +31,10 @@ "placeholder": "Sisesta promo kood" }, "items": { - "analysisPackages": { - "productColumnLabel": "Paketi nimi" + "synlabAnalyses": { + "productColumnLabel": "Analüüsi nimi" }, - "services": { + "ttoServices": { "productColumnLabel": "Teenuse nimi" }, "delete": { From 4e6f12a9a050f22a0804efe93fbd551afc51e6b1 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:50:55 +0300 Subject: [PATCH 05/17] feat(MED-100): add partner locations list for cart --- .../_components/cart/analysis-location.tsx | 49 ++++--- .../_components/cart/partner-locations.json | 122 ++++++++++++++++++ 2 files changed, 155 insertions(+), 16 deletions(-) create mode 100644 app/home/(user)/_components/cart/partner-locations.json diff --git a/app/home/(user)/_components/cart/analysis-location.tsx b/app/home/(user)/_components/cart/analysis-location.tsx index ff1ff0e..9e1ab16 100644 --- a/app/home/(user)/_components/cart/analysis-location.tsx +++ b/app/home/(user)/_components/cart/analysis-location.tsx @@ -19,17 +19,13 @@ import { } from '@kit/ui/select'; import { updateCartPartnerLocation } from '../../_lib/server/update-cart-partner-location'; +import partnerLocations from './partner-locations.json'; + const AnalysisLocationSchema = z.object({ locationId: z.string().min(1), }); -const MOCK_LOCATIONS: { id: string, name: string }[] = [ - { id: "synlab-tallinn-1", name: "SYNLAB - Tallinn" }, - { id: "synlab-tartu-1", name: "SYNLAB - Tartu" }, - { id: "synlab-parnu-1", name: "SYNLAB - Pärnu" }, -] - -export default function AnalysisLocation({ cart, analysisPackages }: { cart: StoreCart, analysisPackages: StoreCartLineItem[] }) { +export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: StoreCart, synlabAnalyses: StoreCartLineItem[] }) { const { t } = useTranslation('cart'); const form = useForm>({ @@ -39,12 +35,16 @@ export default function AnalysisLocation({ cart, analysisPackages }: { cart: Sto resolver: zodResolver(AnalysisLocationSchema), }); + const getLocation = (locationId: string) => partnerLocations.find(({ name }) => name === locationId); + + const selectedLocation = getLocation(form.watch('locationId')); + const onSubmit = async ({ locationId }: z.infer) => { const promise = updateCartPartnerLocation({ cartId: cart.id, - lineIds: analysisPackages.map(({ id }) => id), + lineIds: synlabAnalyses.map(({ id }) => id), partnerLocationId: locationId, - partnerLocationName: MOCK_LOCATIONS.find((location) => location.id === locationId)?.name ?? '', + partnerLocationName: getLocation(locationId)?.name ?? '', }); toast.promise(promise, { @@ -55,7 +55,7 @@ export default function AnalysisLocation({ cart, analysisPackages }: { cart: Sto } return ( -
+
onSubmit(data))} @@ -78,18 +78,35 @@ export default function AnalysisLocation({ cart, analysisPackages }: { cart: Sto - - {t('cart:locations.locationSelect')} - - {MOCK_LOCATIONS.map((location) => ( - {location.name} + {Object.entries(partnerLocations + .reduce((acc, curr) => ({ + ...acc, + [curr.city]: [...((acc[curr.city] as typeof partnerLocations) ?? []), curr], + }), {} as Record)) + .map(([city, locations]) => ( + + {city} + {locations.map((location) => ( + {location.name} + ))} + ))} -
+ {selectedLocation && ( +
+

+ {selectedLocation.address} +

+

+ {selectedLocation.hours} +

+
+ )} +

diff --git a/app/home/(user)/_components/cart/partner-locations.json b/app/home/(user)/_components/cart/partner-locations.json new file mode 100644 index 0000000..1266f0c --- /dev/null +++ b/app/home/(user)/_components/cart/partner-locations.json @@ -0,0 +1,122 @@ +[ + { + "name": "SYNLAB Eesti Veerenni verevõtupunkt", + "address": "Veerenni 53a, VI korrus Tel: 17123", + "hours": "Verevõtt E-R 7.30-15.30 ja L 8.00-16.00 (lõunapaus 13.00-13.30)", + "city": "Tallinn" + }, + { + "name": "SYNLAB Eesti kesklinna verevõtupunkt", + "address": "Pärnu mnt 15 (Kawe Plaza), I korrus Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-16.00 (lõunapaus 12.00-12.30)", + "city": "Tallinn" + }, + { + "name": "SYNLAB Eesti Lasnamäe verevõtupunkt", + "address": "Linnamäe tee 3 (Lasnamäe Tervisemaja), II korrus Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-16.00 (lõunapaus 12.00-12.30)", + "city": "Tallinn" + }, + { + "name": "SYNLAB Eesti Ülemiste verevõtupunkt", + "address": "Valukoja 7 (Ülemiste Tervisemaja), II korrus Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-16.00 (lõunapaus 12.00-12.30)", + "city": "Tallinn" + }, + { + "name": "SYNLAB Eesti Sepapaja verevõtupunkt", + "address": "Sepapaja 12/1 (Ülemiste Tervisemaja 2, Karl Ernst von Baeri maja), III korrus Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-16.00 (lõunapaus 12.30-13.00)", + "city": "Tallinn" + }, + { + "name": "SYNLAB Eesti Viimsi verevõtupunkt", + "address": "Ravi tee 4 (Viimsi Fertilitas) Tel: 17123", + "hours": "Verevõtt tööpäeviti 7.30-15.30 (lõunapaus 12.30-13.00)", + "city": "Viimsi" + }, + { + "name": "SYNLAB Eesti Maardu verevõtupunkt", + "address": "Kallasmaa 4 (Maardu Tervisekeskus) Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-15.00 (lõunapaus 12.00-12.30)", + "city": "Maardu" + }, + { + "name": "SYNLAB Eesti Tartu kliiniline labor", + "address": "Raatuse 21, II korrus Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-16.30", + "city": "Tartu" + }, + { + "name": "SYNLAB Eesti Tasku verevõtupunkt", + "address": "Turu 2, IV korrus (Tasku Meditsiinikeskus) Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-15.30", + "city": "Tartu" + }, + { + "name": "SYNLAB Eesti Tartu Tervisekeskuse verevõtupunkt", + "address": "Mõisavahe 34b, I korrus Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-15.00 (lõunapaus 12:30-13:00)", + "city": "Tartu" + }, + { + "name": "SYNLAB Eesti Pärnu Tervis SPA verevõtupunkt", + "address": "Seedri 6 (Tervis SPA), kabinet 202 Tel: 17123", + "hours": "Verevõtt tööpäeviti E-N 8.00-15.00 R 8.00-13.00", + "city": "Pärnu" + }, + { + "name": "SYNLAB Eesti Suur-Sepa verevõtupunkt", + "address": "Suur-Sepa 14, kabinet 102 Tel: 17123", + "hours": "Verevõtt tööpäeviti E-N 8.00-16.00 (lõunapaus 12.30-13.00) ja R 8.00-14.00 (lõunapaus 12.30-13.00)", + "city": "Pärnu" + }, + { + "name": "SYNLAB Eesti Narva verevõtupunkt", + "address": "Fama 10/2, kabinet 14 Tel: 17123", + "hours": "Verevõtt tööpäeviti 7.30-12.00", + "city": "Narva" + }, + { + "name": "SYNLAB Eesti Sillamäe verevõtupunkt", + "address": "Kajaka 9, IV korrus, kabinet 404 Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-11.30", + "city": "Sillamäe" + }, + { + "name": "SYNLAB Eesti Jõhvi verevõtupunkt", + "address": "Jaama 34, I korrus, kabinet 15 Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-13.00", + "city": "Jõhvi" + }, + { + "name": "SYNLAB Eesti Viljandi verevõtupunkt", + "address": "Tallinna 19, II korrus, kabinet 210 Tel: 17123", + "hours": "Verevõtt tööpäeviti E-N 8.00-15.00 ja R 8.00-12.00", + "city": "Viljandi" + }, + { + "name": "SYNLAB Eesti Võru labor", + "address": "Tartu tn 9 Tel: 17123", + "hours": "Verevõtt tööpäeviti E-N 8.00-15.30 R 8.00-13.00", + "city": "Võru" + }, + { + "name": "SYNLAB Eesti Elva labor", + "address": "Supelranna 21, kabinet 133 Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-14.00", + "city": "Elva" + }, + { + "name": "SYNLAB Eesti Põltsamaa labor", + "address": "Lossi 49 Tel: 17123", + "hours": "Verevõtt tööpäeviti E-N 8.00-15.00 (lõunapaus 12:00-12:30) ja R 8.00-12.00", + "city": "Põltsamaa" + }, + { + "name": "SYNLAB Eesti Otepää labor", + "address": "Tartu mnt 2 Tel: 17123", + "hours": "Verevõtt tööpäeviti 8.00-12.00", + "city": "Otepää" + } +] \ No newline at end of file From 5108087cc54ded3ab84bf7cc71cd8f314d301d56 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:51:04 +0300 Subject: [PATCH 06/17] feat(MED-100): improve translation for status --- public/locales/et/orders.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/et/orders.json b/public/locales/et/orders.json index 04ba985..7243f21 100644 --- a/public/locales/et/orders.json +++ b/public/locales/et/orders.json @@ -14,7 +14,7 @@ "PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused", "FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes", "WAITING_FOR_DOCTOR_RESPONSE": "Ootab arsti kokkuvõtet", - "COMPLETED": "Lõplikud tulemused", + "COMPLETED": "Kinnitatud", "REJECTED": "Tagastatud", "CANCELLED": "Tühistatud" } From 89d6035151f7e05076e93b36e58dff6d84471fe5 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:51:12 +0300 Subject: [PATCH 07/17] feat(MED-85): create customer group for company account in Medusa --- .../src/lib/server/admin-server-actions.ts | 54 ++++++++++++++++++- .../server/services/admin-accounts.service.ts | 11 ++++ .../admin/src/lib/server/utils/medusa-sdk.ts | 16 ++++++ .../src/lib/data/customer.ts | 12 +++++ packages/supabase/src/database.types.ts | 3 ++ .../hooks/use-sign-in-with-email-password.ts | 9 +++- .../hooks/use-sign-up-with-email-password.ts | 7 ++- .../20250825065821_medusa_account_id.sql | 1 + 8 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 packages/features/admin/src/lib/server/utils/medusa-sdk.ts create mode 100644 supabase/migrations/20250825065821_medusa_account_id.sql diff --git a/packages/features/admin/src/lib/server/admin-server-actions.ts b/packages/features/admin/src/lib/server/admin-server-actions.ts index 80229fb..1393f09 100644 --- a/packages/features/admin/src/lib/server/admin-server-actions.ts +++ b/packages/features/admin/src/lib/server/admin-server-actions.ts @@ -23,6 +23,7 @@ import { createAdminAccountsService } from './services/admin-accounts.service'; import { createAdminAuthUserService } from './services/admin-auth-user.service'; import { createCreateCompanyAccountService } from './services/admin-create-company-account.service'; import { adminAction } from './utils/admin-action'; +import { getAdminSdk } from './utils/medusa-sdk'; /** * @name banUserAction @@ -138,7 +139,24 @@ export const deleteAccountAction = adminAction( logger.info({ accountId }, `Super Admin is deleting account...`); - await service.deleteAccount(accountId); + const { name: customerGroupName } = await service.getAccount(accountId); + try { + await service.deleteAccount(accountId); + } catch (e) { + logger.error({ accountId }, `Error deleting company account`); + throw e; + } + const medusa = getAdminSdk(); + const { customer_groups } = await medusa.admin.customerGroup.list(); + const customerGroup = customer_groups.find(({ name }) => name === customerGroupName); + if (customerGroup) { + try { + await medusa.admin.customerGroup.delete(customerGroup.id); + } catch (e) { + logger.error({ accountId }, `Error deleting Medusa customer group for company ${customerGroupName}`); + throw e; + } + } logger.info( { accountId }, @@ -267,6 +285,40 @@ export const createCompanyAccountAction = enhanceAction( } logger.info(ctx, `Company account created`); + + logger.info(ctx, `Creating Medusa customer group`); + const medusa = getAdminSdk(); + const { customer_groups: existingCustomerGroups } = await medusa.admin.customerGroup.list(); + const isExisting = existingCustomerGroups.find((group) => group.name === name); + if (isExisting) { + logger.info(ctx, `Customer group already exists`); + } else { + logger.info(ctx, `Creating Medusa customer group`); + const { data: account } = await client + .schema('medreport').from('accounts') + .select('medusa_account_id') + .eq('personal_code', ownerPersonalCode) + .single().throwOnError(); + const medusaAccountId = account.medusa_account_id; + if (!medusaAccountId) { + logger.error(ctx, `User has no Medusa account ID`); + } else { + const { customer_group: { id: customerGroupId } } = await medusa.admin.customerGroup.create({ name }); + const { customers } = await medusa.admin.customer.list({ + id: medusaAccountId, + }); + if (customers.length !== 1) { + logger.error(ctx, `Customer not found`); + } else { + const customerId = customers[0]!.id; + await medusa.admin.customer.batchCustomerGroups(customerId, { + add: [customerGroupId], + }); + } + } + + } + redirect(`/admin/accounts/${data.id}`); }, { diff --git a/packages/features/admin/src/lib/server/services/admin-accounts.service.ts b/packages/features/admin/src/lib/server/services/admin-accounts.service.ts index e06bb55..87e72e9 100644 --- a/packages/features/admin/src/lib/server/services/admin-accounts.service.ts +++ b/packages/features/admin/src/lib/server/services/admin-accounts.service.ts @@ -37,4 +37,15 @@ class AdminAccountsService { throw error; } } + + async getAccount(accountId: string) { + const { data } = await this.adminClient + .schema('medreport') + .from('accounts') + .select('*') + .eq('id', accountId) + .single().throwOnError(); + + return data; + } } diff --git a/packages/features/admin/src/lib/server/utils/medusa-sdk.ts b/packages/features/admin/src/lib/server/utils/medusa-sdk.ts new file mode 100644 index 0000000..997ab55 --- /dev/null +++ b/packages/features/admin/src/lib/server/utils/medusa-sdk.ts @@ -0,0 +1,16 @@ +import Medusa from "@medusajs/js-sdk" + +export 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, + }); +} diff --git a/packages/features/medusa-storefront/src/lib/data/customer.ts b/packages/features/medusa-storefront/src/lib/data/customer.ts index 9b22479..42e0468 100644 --- a/packages/features/medusa-storefront/src/lib/data/customer.ts +++ b/packages/features/medusa-storefront/src/lib/data/customer.ts @@ -276,6 +276,12 @@ export async function medusaLoginOrRegister(credentials: { const customerCacheTag = await getCacheTag("customers"); revalidateTag(customerCacheTag); + + const customer = await retrieveCustomer(); + if (!customer) { + throw new Error("Customer not found"); + } + return customer.id; } catch (error) { console.error("Failed to login customer, attempting to register", error); try { @@ -302,6 +308,12 @@ export async function medusaLoginOrRegister(credentials: { const customerCacheTag = await getCacheTag("customers"); revalidateTag(customerCacheTag); await transferCart(); + + const customer = await retrieveCustomer(); + if (!customer) { + throw new Error("Customer not found"); + } + return customer.id; } catch (registerError) { throw medusaError(registerError); } diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 0ff6ae6..edc9a99 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -281,6 +281,7 @@ export type Database = { primary_owner_user_id: string public_data: Json slug: string | null + medusa_account_id: string | null updated_at: string | null updated_by: string | null } @@ -302,6 +303,7 @@ export type Database = { primary_owner_user_id?: string public_data?: Json slug?: string | null + medusa_account_id?: string | null updated_at?: string | null updated_by?: string | null } @@ -323,6 +325,7 @@ export type Database = { primary_owner_user_id?: string public_data?: Json slug?: string | null + medusa_account_id?: string | null updated_at?: string | null updated_by?: string | null } diff --git a/packages/supabase/src/hooks/use-sign-in-with-email-password.ts b/packages/supabase/src/hooks/use-sign-in-with-email-password.ts index 2e11a95..6ed91c9 100644 --- a/packages/supabase/src/hooks/use-sign-in-with-email-password.ts +++ b/packages/supabase/src/hooks/use-sign-in-with-email-password.ts @@ -20,15 +20,20 @@ export function useSignInWithEmailPassword() { const identities = user?.identities ?? []; if (identities.length === 0) { - throw new Error('User already registered'); + throw new Error('Invalid user'); } if ('email' in credentials) { try { - await medusaLoginOrRegister({ + const medusaAccountId = await medusaLoginOrRegister({ email: credentials.email, password: credentials.password, }); + await client + .schema('medreport').from('accounts') + .update({ medusa_account_id: medusaAccountId }) + .eq('primary_owner_user_id', user.id) + .eq('is_personal_account', true); } catch (error) { await client.auth.signOut(); throw error; diff --git a/packages/supabase/src/hooks/use-sign-up-with-email-password.ts b/packages/supabase/src/hooks/use-sign-up-with-email-password.ts index 2d73b0b..2a3df83 100644 --- a/packages/supabase/src/hooks/use-sign-up-with-email-password.ts +++ b/packages/supabase/src/hooks/use-sign-up-with-email-password.ts @@ -39,10 +39,15 @@ export function useSignUpWithEmailAndPassword() { if ('email' in credentials) { try { - await medusaLoginOrRegister({ + const medusaAccountId = await medusaLoginOrRegister({ email: credentials.email, password: credentials.password, }); + await client + .schema('medreport').from('accounts') + .update({ medusa_account_id: medusaAccountId }) + .eq('primary_owner_user_id', user!.id) + .eq('is_personal_account', true); } catch (error) { await client.auth.signOut(); throw error; diff --git a/supabase/migrations/20250825065821_medusa_account_id.sql b/supabase/migrations/20250825065821_medusa_account_id.sql new file mode 100644 index 0000000..b5f06ff --- /dev/null +++ b/supabase/migrations/20250825065821_medusa_account_id.sql @@ -0,0 +1 @@ +ALTER TABLE medreport.accounts ADD COLUMN medusa_account_id TEXT; From 811e8609654e9481177328d49060af7c07653a8a Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:51:27 +0300 Subject: [PATCH 08/17] feat(MED-85): fix delete company account error --- .../20250825072326_account_membership_id_for_audit.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 supabase/migrations/20250825072326_account_membership_id_for_audit.sql diff --git a/supabase/migrations/20250825072326_account_membership_id_for_audit.sql b/supabase/migrations/20250825072326_account_membership_id_for_audit.sql new file mode 100644 index 0000000..46d11b6 --- /dev/null +++ b/supabase/migrations/20250825072326_account_membership_id_for_audit.sql @@ -0,0 +1,5 @@ +-- user_id+account_id primary key -> separate id column for audit +ALTER TABLE medreport.accounts_memberships DROP CONSTRAINT accounts_memberships_pkey; +ALTER TABLE medreport.accounts_memberships ADD COLUMN id UUID DEFAULT gen_random_uuid(); +ALTER TABLE medreport.accounts_memberships ADD CONSTRAINT accounts_memberships_pkey PRIMARY KEY (id); +ALTER TABLE medreport.accounts_memberships ADD CONSTRAINT unique_user_account UNIQUE (user_id, account_id); From eb1eeb690bce49b71b3e117b59ec6008db43d9a0 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:51:35 +0300 Subject: [PATCH 09/17] feat(MED-85): create audit logs for medusa admin actions --- packages/supabase/src/database.types.ts | 22 +++++++++++++++++++ .../20250825081751_medusa_audit_logs.sql | 19 ++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 supabase/migrations/20250825081751_medusa_audit_logs.sql diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index edc9a99..f6a8221 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -174,6 +174,28 @@ export type Database = { } Relationships: [] } + medusa_action: { + Row: { + id: number + medusa_user_id: string + user_email: string + action: string + page: string + created_at: string + } + Insert: { + medusa_user_id: string + user_email: string + action: string + page: string + } + Update: { + medusa_user_id?: string + user_email?: string + action?: string + page?: string + } + } } Views: { [_ in never]: never diff --git a/supabase/migrations/20250825081751_medusa_audit_logs.sql b/supabase/migrations/20250825081751_medusa_audit_logs.sql new file mode 100644 index 0000000..df6768d --- /dev/null +++ b/supabase/migrations/20250825081751_medusa_audit_logs.sql @@ -0,0 +1,19 @@ +create table "audit"."medusa_action" ( + "id" bigint generated by default as identity not null, + "medusa_user_id" text not null, + "user_email" text not null, + "action" text not null, + "page" text, + "created_at" timestamp with time zone not null default now() +); + +grant usage on schema audit to authenticated; +grant select, insert, update, delete on table audit.medusa_action to authenticated; + +alter table "audit"."medusa_action" enable row level security; + +create policy "service_role_select" on "audit"."medusa_action" for select to service_role using (true); +create policy "service_role_insert" on "audit"."medusa_action" for insert to service_role with check (true); +create policy "service_role_update" on "audit"."medusa_action" for update to service_role using (true); +create policy "service_role_delete" on "audit"."medusa_action" for delete to service_role using (true); +grant select, insert, update, delete on table audit.medusa_action to service_role; From 828f32ee8182a84d1ef1b8b05bcd84662f7388a6 Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:51:43 +0300 Subject: [PATCH 10/17] feat(MED-85): create audit log on orders view + export csv --- .../20250825094541_medipost_dispatch_audit.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 supabase/migrations/20250825094541_medipost_dispatch_audit.sql diff --git a/supabase/migrations/20250825094541_medipost_dispatch_audit.sql b/supabase/migrations/20250825094541_medipost_dispatch_audit.sql new file mode 100644 index 0000000..97e65fd --- /dev/null +++ b/supabase/migrations/20250825094541_medipost_dispatch_audit.sql @@ -0,0 +1,12 @@ +CREATE TABLE "audit"."medipost_dispatch" ( + "id" bigint generated by default as identity not null, + "medusa_order_id" text not null, + "is_medipost_error" boolean not null, + "is_success" boolean not null, + "error_message" text, + "created_at" timestamp with time zone not null default now(), + "changed_by" uuid not null +); + +grant usage on schema audit to service_role; +grant select, insert, update, delete on table audit.medipost_dispatch to service_role; From da8b5aa59f1b5a80e5d0175876b4e4da42a67e3e Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 12:22:29 +0300 Subject: [PATCH 11/17] feat(MED-85): create logs of sending order to medipost success/error --- lib/services/audit.service.ts | 43 ++++++++++++------- lib/services/medipost.service.ts | 23 ++++++++-- .../medipost/MedipostValidationError.ts | 9 ++++ 3 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 lib/services/medipost/MedipostValidationError.ts diff --git a/lib/services/audit.service.ts b/lib/services/audit.service.ts index 5d15548..5c102bf 100644 --- a/lib/services/audit.service.ts +++ b/lib/services/audit.service.ts @@ -1,11 +1,10 @@ 'use server' -import { createClient } from '@supabase/supabase-js'; - import { RequestStatus } from '@/lib/types/audit'; import { ConnectedOnlineMethodName } from '@/lib/types/connected-online'; import { ExternalApi } from '@/lib/types/external'; import { MedipostAction } from '@/lib/types/medipost'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; export default async function logRequestResult( /* personalCode: string, */ requestApi: keyof typeof ExternalApi, @@ -16,19 +15,7 @@ export default async function logRequestResult( serviceId?: number, serviceProviderId?: number, ) { - const supabaseServiceUser = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY!, - { - auth: { - persistSession: false, - autoRefreshToken: false, - detectSessionInUrl: false, - }, - }, - ); - - const { error } = await supabaseServiceUser + const { error } = await getSupabaseServerClient() .schema('audit') .from('request_entries') .insert({ @@ -46,3 +33,29 @@ export default async function logRequestResult( throw new Error('Failed to insert log entry, error: ' + error.message); } } + +export async function logMedipostDispatch({ + medusaOrderId, + isSuccess, + isMedipostError, + errorMessage, +}: { + medusaOrderId: string; + isSuccess: boolean; + isMedipostError: boolean; + errorMessage?: string; +}) { + const { error } = await getSupabaseServerClient() + .schema('audit') + .from('medipost_dispatch') + .insert({ + medusa_order_id: medusaOrderId, + is_success: isSuccess, + is_medipost_error: isMedipostError, + error_message: errorMessage, + }); + + if (error) { + throw new Error('Failed to insert log entry, error: ' + error.message); + } +} diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index 8508663..e809cdf 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -44,6 +44,8 @@ import { StoreOrder } from '@medusajs/types'; import { listProducts } from '@lib/data/products'; import { listRegions } from '@lib/data/regions'; import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product'; +import { MedipostValidationError } from './medipost/MedipostValidationError'; +import { logMedipostDispatch } from './audit.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -63,14 +65,14 @@ export async function validateMedipostResponse(response: string, { canHaveEmptyC if (canHaveEmptyCode) { if (code && code !== 0) { console.error("Bad response", response); - throw new Error(`Medipost response is invalid`); + throw new MedipostValidationError(response); } return; } if (typeof code !== 'number' || (code !== 0 && !canHaveEmptyCode)) { console.error("Bad response", response); - throw new Error(`Medipost response is invalid`); + throw new MedipostValidationError(response); } } @@ -704,7 +706,22 @@ export async function sendOrderToMedipost({ comment: '', }); - await sendPrivateMessage(orderXml); + try { + await sendPrivateMessage(orderXml); + } catch (e) { + await logMedipostDispatch({ + medusaOrderId, + isSuccess: false, + isMedipostError: e instanceof MedipostValidationError, + errorMessage: e instanceof MedipostValidationError ? e.response : undefined, + }); + throw e; + } + await logMedipostDispatch({ + medusaOrderId, + isSuccess: true, + isMedipostError: false, + }); await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); } diff --git a/lib/services/medipost/MedipostValidationError.ts b/lib/services/medipost/MedipostValidationError.ts new file mode 100644 index 0000000..6b9645a --- /dev/null +++ b/lib/services/medipost/MedipostValidationError.ts @@ -0,0 +1,9 @@ +export class MedipostValidationError extends Error { + response: string; + + constructor(response: string) { + super('Invalid Medipost response'); + this.name = 'MedipostValidationError'; + this.response = response; + } +} From 2b2a0b8bc4db0104989933b24d7396852fd33e7a Mon Sep 17 00:00:00 2001 From: k4rli Date: Wed, 27 Aug 2025 08:04:37 +0300 Subject: [PATCH 12/17] feat(MED-85): update dispatch order to medipost retry --- app/api/job/medipost-retry-dispatch/route.ts | 40 ++++++++++++++++++ lib/services/audit.service.ts | 12 +++++- lib/services/medipost.service.ts | 5 ++- lib/services/order.service.ts | 5 ++- packages/supabase/src/database.types.ts | 15 +++++++ ...20250825094541_medipost_dispatch_audit.sql | 28 ++++++++++++- ...20250825120858_medipost_retry_dispatch.sql | 42 +++++++++++++++++++ 7 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 app/api/job/medipost-retry-dispatch/route.ts create mode 100644 supabase/migrations/20250825120858_medipost_retry_dispatch.sql diff --git a/app/api/job/medipost-retry-dispatch/route.ts b/app/api/job/medipost-retry-dispatch/route.ts new file mode 100644 index 0000000..2258e2b --- /dev/null +++ b/app/api/job/medipost-retry-dispatch/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import loadEnv from "../handler/load-env"; +import validateApiKey from "../handler/validate-api-key"; +import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from "~/lib/services/medipost.service"; +import { retrieveOrder } from "@lib/data/orders"; +import { getMedipostDispatchTries } from "~/lib/services/audit.service"; + +export const POST = async (request: NextRequest) => { + loadEnv(); + + const { medusaOrderId } = await request.json(); + + try { + validateApiKey(request); + } catch (e) { + return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' }); + } + + const tries = await getMedipostDispatchTries(medusaOrderId); + if (tries >= 3) { + return NextResponse.json({ + message: 'Order has been retried too many times', + }, { status: 400 }); + } + + try { + const medusaOrder = await retrieveOrder(medusaOrderId); + const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder }); + await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); + console.info("Successfully sent order to medipost"); + return NextResponse.json({ + message: 'Successfully sent order to medipost', + }, { status: 200 }); + } catch (e) { + console.error("Error sending order to medipost", e); + return NextResponse.json({ + message: 'Failed to send order to medipost', + }, { status: 500 }); + } +}; diff --git a/lib/services/audit.service.ts b/lib/services/audit.service.ts index 5c102bf..880c037 100644 --- a/lib/services/audit.service.ts +++ b/lib/services/audit.service.ts @@ -4,6 +4,7 @@ import { RequestStatus } from '@/lib/types/audit'; import { ConnectedOnlineMethodName } from '@/lib/types/connected-online'; import { ExternalApi } from '@/lib/types/external'; import { MedipostAction } from '@/lib/types/medipost'; +import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; export default async function logRequestResult( @@ -45,7 +46,7 @@ export async function logMedipostDispatch({ isMedipostError: boolean; errorMessage?: string; }) { - const { error } = await getSupabaseServerClient() + const { error } = await getSupabaseServerAdminClient() .schema('audit') .from('medipost_dispatch') .insert({ @@ -59,3 +60,12 @@ export async function logMedipostDispatch({ throw new Error('Failed to insert log entry, error: ' + error.message); } } + +export async function getMedipostDispatchTries(medusaOrderId: string) { + const { data } = await getSupabaseServerAdminClient() + .schema('medreport') + .rpc('get_medipost_dispatch_tries', { p_medusa_order_id: medusaOrderId }) + .throwOnError(); + + return data; +} diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index e809cdf..4b299d2 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -709,11 +709,12 @@ export async function sendOrderToMedipost({ try { await sendPrivateMessage(orderXml); } catch (e) { + const isMedipostError = e instanceof MedipostValidationError; await logMedipostDispatch({ medusaOrderId, isSuccess: false, - isMedipostError: e instanceof MedipostValidationError, - errorMessage: e instanceof MedipostValidationError ? e.response : undefined, + isMedipostError, + errorMessage: isMedipostError ? e.response : undefined, }); throw e; } diff --git a/lib/services/order.service.ts b/lib/services/order.service.ts index 998be03..8afb4c4 100644 --- a/lib/services/order.service.ts +++ b/lib/services/order.service.ts @@ -99,7 +99,10 @@ export async function getOrder({ throw new Error('Either medusaOrderId or orderId must be provided'); } - const { data: order } = await query.single().throwOnError(); + const { data: order, error } = await query.single(); + if (error) { + throw new Error(`Failed to get order by medusaOrderId=${medusaOrderId} or orderId=${orderId}, message=${error.message}, data=${JSON.stringify(order)}`); + } return order; } diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index f6a8221..af95d00 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -2003,6 +2003,21 @@ export type Database = { } Returns: Json } + medipost_retry_dispatch: { + Args: { + order_id: string + } + Returns: { + success: boolean + error: string | null + } + } + get_medipost_dispatch_tries: { + Args: { + p_medusa_order_id: string + } + Returns: number + } } Enums: { analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED" diff --git a/supabase/migrations/20250825094541_medipost_dispatch_audit.sql b/supabase/migrations/20250825094541_medipost_dispatch_audit.sql index 97e65fd..c024626 100644 --- a/supabase/migrations/20250825094541_medipost_dispatch_audit.sql +++ b/supabase/migrations/20250825094541_medipost_dispatch_audit.sql @@ -5,8 +5,32 @@ CREATE TABLE "audit"."medipost_dispatch" ( "is_success" boolean not null, "error_message" text, "created_at" timestamp with time zone not null default now(), - "changed_by" uuid not null + "changed_by" uuid default auth.uid() ); -grant usage on schema audit to service_role; +grant usage on schema audit to authenticated; +grant select, insert, update, delete on table audit.medipost_dispatch to authenticated; +grant usage on schema medreport to service_role; + +alter table "audit"."medipost_dispatch" enable row level security; + +create policy "service_role_select" on "audit"."medipost_dispatch" for select to service_role using (true); +create policy "service_role_insert" on "audit"."medipost_dispatch" for insert to service_role with check (true); +create policy "service_role_update" on "audit"."medipost_dispatch" for update to service_role using (true); +create policy "service_role_delete" on "audit"."medipost_dispatch" for delete to service_role using (true); + +CREATE OR REPLACE FUNCTION medreport.get_medipost_dispatch_tries(p_medusa_order_id text) +returns integer +language plpgsql +security definer +as $function$ +declare + tries integer; +begin + select count(*) from audit.medipost_dispatch m where m.medusa_order_id = p_medusa_order_id and m.created_at > now() - interval '1 day' and m.is_success = false into tries; + return tries; +end; +$function$; + +grant execute on function medreport.get_medipost_dispatch_tries(text) to service_role; grant select, insert, update, delete on table audit.medipost_dispatch to service_role; diff --git a/supabase/migrations/20250825120858_medipost_retry_dispatch.sql b/supabase/migrations/20250825120858_medipost_retry_dispatch.sql new file mode 100644 index 0000000..73e6e27 --- /dev/null +++ b/supabase/migrations/20250825120858_medipost_retry_dispatch.sql @@ -0,0 +1,42 @@ +create extension if not exists pg_net; + +create or replace function medreport.medipost_retry_dispatch( + order_id text +) +returns jsonb +language plpgsql +as $function$ +declare + response_result record; +begin + select into response_result + net.http_post( + url := 'https://test.medreport.ee/api/job/medipost-retry-dispatch', + headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84' + ), + body := jsonb_build_object( + 'medusaOrderId', order_id + )::text + ) as request_id; + + return jsonb_build_object( + 'success', true + ); + +exception + when others then + return jsonb_build_object( + 'success', false + ); +end; +$function$; + +grant execute on function medreport.medipost_retry_dispatch(text) to service_role; + +comment on function medreport.medipost_retry_dispatch(text) is +'Manually trigger a medipost retry dispatch for a specific order ID. +Parameters: +- order_id: The medusa order ID to retry dispatch for +Returns: JSONB with success status and request details'; From 7087c9a6da84cd6f482fe00974738e3093a29891 Mon Sep 17 00:00:00 2001 From: k4rli Date: Wed, 27 Aug 2025 08:06:00 +0300 Subject: [PATCH 13/17] feat(MED-85): fix missing async --- app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index 5ee25aa..970a01d 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -143,7 +143,7 @@ export async function processMontonioCallback(orderToken: string) { console.error("Missing email or analysisPackageName", orderResult); } - sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); + await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); return { success: true, orderId }; } catch (error) { From 68e5101885e6eaa3a98042879bcb97254ce7a433 Mon Sep 17 00:00:00 2001 From: k4rli Date: Wed, 27 Aug 2025 08:06:30 +0300 Subject: [PATCH 14/17] feat(MED-85): update order status translations --- public/locales/en/orders.json | 3 +-- public/locales/et/orders.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/public/locales/en/orders.json b/public/locales/en/orders.json index cd36098..7c16fc9 100644 --- a/public/locales/en/orders.json +++ b/public/locales/en/orders.json @@ -12,8 +12,7 @@ "ON_HOLD": "Waiting for analysis results", "PROCESSING": "In progress", "PARTIAL_ANALYSIS_RESPONSE": "Partial analysis response", - "FULL_ANALYSIS_RESPONSE": "All analysis responses", - "WAITING_FOR_DOCTOR_RESPONSE": "Waiting for doctor response", + "FULL_ANALYSIS_RESPONSE": "All analysis responses received, waiting for doctor response", "COMPLETED": "Completed", "REJECTED": "Rejected", "CANCELLED": "Cancelled" diff --git a/public/locales/et/orders.json b/public/locales/et/orders.json index 7243f21..ef0a203 100644 --- a/public/locales/et/orders.json +++ b/public/locales/et/orders.json @@ -12,8 +12,7 @@ "ON_HOLD": "Makstud", "PROCESSING": "Synlabile edastatud", "PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused", - "FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes", - "WAITING_FOR_DOCTOR_RESPONSE": "Ootab arsti kokkuvõtet", + "FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes, ootab arsti kokkuvõtet", "COMPLETED": "Kinnitatud", "REJECTED": "Tagastatud", "CANCELLED": "Tühistatud" From 380363922c2479cb890a5a94776c0983442d1f51 Mon Sep 17 00:00:00 2001 From: k4rli Date: Wed, 27 Aug 2025 08:07:18 +0300 Subject: [PATCH 15/17] feat(MED-85): improve results sync logs --- app/api/job/handler/sync-analysis-results.ts | 55 ++++++++++++++---- lib/services/medipost.service.ts | 60 +++++++++++++++----- 2 files changed, 91 insertions(+), 24 deletions(-) diff --git a/app/api/job/handler/sync-analysis-results.ts b/app/api/job/handler/sync-analysis-results.ts index ddf07de..c7fd529 100644 --- a/app/api/job/handler/sync-analysis-results.ts +++ b/app/api/job/handler/sync-analysis-results.ts @@ -1,29 +1,62 @@ import { readPrivateMessageResponse } from "~/lib/services/medipost.service"; +type ProcessedMessage = { + messageId: string; + hasAnalysisResponse: boolean; + hasPartialAnalysisResponse: boolean; + hasFullAnalysisResponse: boolean; + medusaOrderId: string | undefined; +}; + +type GroupedResults = { + processed: Pick[]; + waitingForResults: Pick[]; +}; + export default async function syncAnalysisResults() { console.info("Syncing analysis results"); - let processedMessageIds: string[] = []; + let processedMessages: ProcessedMessage[] = []; const excludedMessageIds: string[] = []; while (true) { - console.info("Fetching private messages"); - const { messageIdErrored, messageIdProcessed } = await readPrivateMessageResponse({ excludedMessageIds }); - if (messageIdProcessed) { - processedMessageIds.push(messageIdProcessed); + const result = await readPrivateMessageResponse({ excludedMessageIds }); + if (result.messageId) { + processedMessages.push(result as ProcessedMessage); } - if (!messageIdErrored) { + if (!result.messageId) { console.info("No more messages to process"); break; } - if (excludedMessageIds.includes(messageIdErrored)) { - console.info(`Message id=${messageIdErrored} has already been processed, stopping`); + if (!excludedMessageIds.includes(result.messageId)) { + excludedMessageIds.push(result.messageId); + } else { break; } - - excludedMessageIds.push(messageIdErrored); } - console.info(`Processed ${processedMessageIds.length} messages, ids: ${processedMessageIds.join(', ')}`); + const groupedResults = processedMessages.reduce((acc, result) => { + if (result.medusaOrderId) { + if (result.hasAnalysisResponse) { + if (!acc.processed) { + acc.processed = []; + } + acc.processed.push({ + messageId: result.messageId, + medusaOrderId: result.medusaOrderId, + }); + } else { + if (!acc.waitingForResults) { + acc.waitingForResults = []; + } + acc.waitingForResults.push({ + messageId: result.messageId, + medusaOrderId: result.medusaOrderId, + }); + } + } + return acc; + }, {} as GroupedResults); + console.info(`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults, undefined, 2)}`); } diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index 4b299d2..39763cf 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -202,28 +202,60 @@ export async function readPrivateMessageResponse({ excludedMessageIds, }: { excludedMessageIds: string[]; -}) { - let messageIdErrored: string | null = null; - let messageIdProcessed: string | null = null; +}): Promise<{ messageId: string | null; hasAnalysisResponse: boolean; hasPartialAnalysisResponse: boolean; hasFullAnalysisResponse: boolean; medusaOrderId: string | undefined }> { + let messageId: string | null = null; + let hasAnalysisResponse = false; + let hasPartialAnalysisResponse = false; + let hasFullAnalysisResponse = false; + let medusaOrderId: string | undefined = undefined; + try { const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds }); if (!privateMessage) { - throw new Error(`No private message found`); + return { + messageId: null, + hasAnalysisResponse: false, + hasPartialAnalysisResponse: false, + hasFullAnalysisResponse: false, + medusaOrderId: undefined, + }; } - messageIdErrored = privateMessage.messageId; - if (!messageIdErrored) { - throw new Error(`No message id found`); + messageId = privateMessage.messageId; + if (!messageId) { + return { + messageId: null, + hasAnalysisResponse: false, + hasPartialAnalysisResponse: false, + hasFullAnalysisResponse: false, + medusaOrderId: undefined, + }; } const privateMessageContent = await getPrivateMessage( privateMessage.messageId, ); const messageResponse = privateMessageContent?.Saadetis?.Vastus; - const medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId; + medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId; + + if (!medusaOrderId || !medusaOrderId.toString().startsWith('order_')) { + return { + messageId, + hasAnalysisResponse: false, + hasPartialAnalysisResponse: false, + hasFullAnalysisResponse: false, + medusaOrderId: undefined, + }; + } if (!messageResponse) { - throw new Error(`Private message response has no results yet for order=${medusaOrderId}`); + return { + messageId, + hasAnalysisResponse: false, + hasPartialAnalysisResponse: false, + hasFullAnalysisResponse: false, + medusaOrderId, + }; } let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; @@ -238,17 +270,19 @@ export async function readPrivateMessageResponse({ if (status.isPartial) { await updateOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' }); - messageIdProcessed = privateMessage.messageId; + hasAnalysisResponse = true; + hasPartialAnalysisResponse = true; } else if (status.isCompleted) { await updateOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' }); await deletePrivateMessage(privateMessage.messageId); - messageIdProcessed = privateMessage.messageId; + hasAnalysisResponse = true; + hasFullAnalysisResponse = true; } } catch (e) { - console.warn(`Failed to process private message id=${messageIdErrored}, message=${(e as Error).message}`); + console.warn(`Failed to process private message id=${messageId}, message=${(e as Error).message}`); } - return { messageIdErrored, messageIdProcessed }; + return { messageId, hasAnalysisResponse, hasPartialAnalysisResponse, hasFullAnalysisResponse, medusaOrderId }; } async function saveAnalysisGroup( From a03db16092d5d1e8ebfc6c3d9f42bb48cb560e1e Mon Sep 17 00:00:00 2001 From: k4rli Date: Wed, 27 Aug 2025 08:27:44 +0300 Subject: [PATCH 16/17] feat(MED-85): add `get_order_possible_actions` function for backoffice --- .../20250825134421_order_possible_actions.sql | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 supabase/migrations/20250825134421_order_possible_actions.sql diff --git a/supabase/migrations/20250825134421_order_possible_actions.sql b/supabase/migrations/20250825134421_order_possible_actions.sql new file mode 100644 index 0000000..86961b9 --- /dev/null +++ b/supabase/migrations/20250825134421_order_possible_actions.sql @@ -0,0 +1,25 @@ +CREATE OR REPLACE FUNCTION medreport.get_order_possible_actions(p_medusa_order_id text) +RETURNS jsonb +LANGUAGE plpgsql +AS $$ +DECLARE + order_status text; + is_queued boolean; +BEGIN + -- Get the analysis order status + SELECT status INTO order_status + FROM medreport.analysis_orders + WHERE medusa_order_id = p_medusa_order_id; + + -- Check if status is QUEUED + is_queued := (order_status = 'QUEUED'); + + -- Return JSON object with actions and their allowed status + RETURN jsonb_build_object( + 'retry_dispatch', is_queued, + 'mark_as_not_received_by_synlab', is_queued + ); +END; +$$; + +grant execute on function medreport.get_order_possible_actions(text) to service_role; From 94439432fdbcbb6d2f925ca68379730bfebae0cf Mon Sep 17 00:00:00 2001 From: k4rli Date: Wed, 27 Aug 2025 08:29:36 +0300 Subject: [PATCH 17/17] feat(MED-85): improve naming --- app/doctor/analysis/[id]/page.tsx | 4 ++-- .../doctor/src/lib/server/services/doctor-analysis.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/doctor/analysis/[id]/page.tsx b/app/doctor/analysis/[id]/page.tsx index 3b74f13..c0fa462 100644 --- a/app/doctor/analysis/[id]/page.tsx +++ b/app/doctor/analysis/[id]/page.tsx @@ -12,8 +12,8 @@ async function AnalysisPage({ id: string; }>; }) { - const { id } = await params; - const analysisResultDetails = await loadResult(Number(id)); + const { id: analysisResponseId } = await params; + const analysisResultDetails = await loadResult(Number(analysisResponseId)); if (!analysisResultDetails) { return null; diff --git a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts index fae6290..97756e4 100644 --- a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts +++ b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts @@ -356,7 +356,7 @@ export async function getOtherResponses({ } export async function getAnalysisResultsForDoctor( - id: number, + analysisResponseId: number, ): Promise { const supabase = getSupabaseServerClient(); @@ -367,7 +367,7 @@ export async function getAnalysisResultsForDoctor( `*, analysis_responses(user_id, analysis_order_id(id,medusa_order_id, analysis_element_ids))`, ) - .eq('analysis_response_id', id); + .eq('analysis_response_id', analysisResponseId); if (error) { throw new Error('Something went wrong.');