From 38d73e27ad59e64c75117f3097746b357b1c8fea Mon Sep 17 00:00:00 2001 From: k4rli Date: Mon, 25 Aug 2025 11:50:03 +0300 Subject: [PATCH] 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 []; }