feat(MED-97): update cart flow for using benefits

This commit is contained in:
2025-09-26 13:24:09 +03:00
parent 56f84a003c
commit db38e602aa
15 changed files with 419 additions and 81 deletions

View File

@@ -1,5 +1,3 @@
import { notFound } from 'next/navigation';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
import { retrieveCart } from '@lib/data/cart';
@@ -11,6 +9,8 @@ import { withI18n } from '~/lib/i18n/with-i18n';
import Cart from '../../_components/cart';
import CartTimer from '../../_components/cart/cart-timer';
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
import { AccountBalanceService } from '~/lib/services/accountBalance.service';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
@@ -21,12 +21,22 @@ export async function generateMetadata() {
}
async function CartPage() {
const cart = await retrieveCart().catch((error) => {
console.error('Failed to retrieve cart', error);
return notFound();
});
const [
cart,
{ productTypes },
{ account },
] = await Promise.all([
retrieveCart(),
listProductTypes(),
loadCurrentUserAccount(),
]);
if (!account) {
return null;
}
const balanceSummary = await new AccountBalanceService().getBalanceSummary(account.id);
const { productTypes } = await listProductTypes();
const analysisPackagesType = productTypes.find(
({ metadata }) => metadata?.handle === 'analysis-packages',
);
@@ -63,9 +73,11 @@ async function CartPage() {
{isTimerShown && <CartTimer cartItem={item} />}
</PageHeader>
<Cart
accountId={account.id}
cart={cart}
synlabAnalyses={synlabAnalyses}
ttoServiceItems={ttoServiceItems}
balanceSummary={balanceSummary}
/>
</PageBody>
);

View File

@@ -2,9 +2,7 @@
import { useState } from 'react';
import { handleNavigateToPayment } from '@/lib/services/medusaCart.service';
import { formatCurrency } from '@/packages/shared/src/utils';
import { initiatePaymentSession } from '@lib/data/cart';
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
import { Loader2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -16,27 +14,35 @@ import { Trans } from '@kit/ui/trans';
import AnalysisLocation from './analysis-location';
import CartItems from './cart-items';
import DiscountCode from './discount-code';
import { initiatePayment } from '../../_lib/server/cart-actions';
import { useRouter } from 'next/navigation';
import { AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
const IS_DISCOUNT_SHOWN = true as boolean;
export default function Cart({
accountId,
cart,
synlabAnalyses,
ttoServiceItems,
balanceSummary,
}: {
accountId: string;
cart: StoreCart | null;
synlabAnalyses: StoreCartLineItem[];
ttoServiceItems: StoreCartLineItem[];
balanceSummary: AccountBalanceSummary | null;
}) {
const {
i18n: { language },
} = useTranslation();
const [isInitiatingSession, setIsInitiatingSession] = useState(false);
const router = useRouter();
const items = cart?.items ?? [];
const hasCartItems = cart && Array.isArray(items) && items.length > 0;
if (!cart || items.length === 0) {
if (!hasCartItems) {
return (
<div className="content-container py-5 lg:px-4">
<div>
@@ -56,24 +62,35 @@ export default function Cart({
);
}
async function initiatePayment() {
async function initiateSession() {
setIsInitiatingSession(true);
const response = await initiatePaymentSession(cart!, {
provider_id: 'pp_montonio_montonio',
});
if (response.payment_collection) {
const { payment_sessions } = response.payment_collection;
const paymentSessionId = payment_sessions![0]!.id;
const url = await handleNavigateToPayment({ language, paymentSessionId });
window.location.href = url;
} else {
try {
const { url, isFullyPaidByBenefits, orderId } = await initiatePayment({
accountId,
balanceSummary: balanceSummary!,
cart: cart!,
language,
});
if (url) {
window.location.href = url;
} else if (isFullyPaidByBenefits) {
if (typeof orderId !== 'number') {
throw new Error('Order ID is missing');
}
router.push(`/home/order/${orderId}/confirmed`);
}
} catch (error) {
console.error('Failed to initiate payment', error);
setIsInitiatingSession(false);
}
}
const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0;
const isLocationsShown = synlabAnalyses.length > 0;
const companyBenefitsTotal = balanceSummary?.totalBalance ?? 0;
const montonioTotal = cart && companyBenefitsTotal > 0 ? cart.total - companyBenefitsTotal : cart.total;
return (
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40 lg:px-4">
<div className="flex flex-col gap-y-6 bg-white">
@@ -106,7 +123,7 @@ export default function Cart({
</p>
</div>
</div>
<div className="flex gap-x-4 px-4 py-2 sm:justify-end sm:px-6 sm:py-4">
<div className="flex gap-x-4 px-4 pt-2 sm:justify-end sm:px-6 sm:pt-4">
<div className="w-full sm:mr-[42px] sm:w-auto">
<p className="text-muted-foreground ml-0 text-sm font-bold">
<Trans i18nKey="cart:order.promotionsTotal" />
@@ -122,6 +139,24 @@ export default function Cart({
</p>
</div>
</div>
{companyBenefitsTotal > 0 && (
<div className="flex gap-x-4 px-4 py-2 sm:justify-end sm:px-6 sm:py-4">
<div className="w-full sm:mr-[42px] sm:w-auto">
<p className="text-muted-foreground ml-0 text-sm font-bold">
<Trans i18nKey="cart:order.companyBenefitsTotal" />
</p>
</div>
<div className={`sm:mr-[112px] sm:w-[50px]`}>
<p className="text-right text-sm">
{formatCurrency({
value: (companyBenefitsTotal > cart.total ? cart.total : companyBenefitsTotal),
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
</div>
)}
<div className="flex gap-x-4 px-4 sm:justify-end sm:px-6">
<div className="w-full sm:mr-[42px] sm:w-auto">
<p className="ml-0 text-sm font-bold">
@@ -131,7 +166,7 @@ export default function Cart({
<div className={`sm:mr-[112px] sm:w-[50px]`}>
<p className="text-right text-sm">
{formatCurrency({
value: cart.total,
value: montonioTotal < 0 ? 0 : montonioTotal,
currencyCode: cart.currency_code,
locale: language,
})}
@@ -175,7 +210,7 @@ export default function Cart({
<div>
<Button
className="h-10"
onClick={initiatePayment}
onClick={initiateSession}
disabled={isInitiatingSession}
>
{isInitiatingSession && (

View File

@@ -8,6 +8,11 @@ import { useTranslation } from 'react-i18next';
import { Trans } from '@kit/ui/trans';
const PaymentProviderIds = {
COMPANY_BENEFITS: "pp_company-benefits_company-benefits",
MONTONIO: "pp_montonio_montonio",
};
export default function CartTotals({
medusaOrder,
}: {
@@ -20,11 +25,16 @@ export default function CartTotals({
currency_code,
total,
subtotal,
tax_total,
discount_total,
gift_card_total,
payment_collections,
} = medusaOrder;
const montonioPayment = payment_collections?.[0]?.payments
?.find(({ provider_id }) => provider_id === PaymentProviderIds.MONTONIO);
const companyBenefitsPayment = payment_collections?.[0]?.payments
?.find(({ provider_id }) => provider_id === PaymentProviderIds.COMPANY_BENEFITS);
return (
<div>
<div className="txt-medium text-ui-fg-subtle flex flex-col gap-y-2">
@@ -86,8 +96,11 @@ export default function CartTotals({
</span>
</div>
)}
</div>
<div className="my-4 h-px w-full border-b border-gray-200" />
<div className="text-ui-fg-base txt-medium mb-2 flex items-center justify-between">
<span className="font-bold">
<Trans i18nKey="cart:order.total" />
@@ -104,7 +117,42 @@ export default function CartTotals({
})}
</span>
</div>
<div className="mt-4 h-px w-full border-b border-gray-200" />
<div className="my-4 h-px w-full border-b border-gray-200" />
<div className="txt-medium text-ui-fg-subtle flex flex-col gap-y-2">
{companyBenefitsPayment && (
<div className="flex items-center justify-between">
<span className="flex items-center gap-x-1">
<Trans i18nKey="cart:order.benefitsTotal" />
</span>
<span data-testid="cart-subtotal" data-value={companyBenefitsPayment.amount || 0}>
-{' '}
{formatCurrency({
value: companyBenefitsPayment.amount ?? 0,
currencyCode: currency_code,
locale: language,
})}
</span>
</div>
)}
{montonioPayment && (
<div className="flex items-center justify-between">
<span className="flex items-center gap-x-1">
<Trans i18nKey="cart:order.montonioTotal" />
</span>
<span data-testid="cart-subtotal" data-value={montonioPayment.amount || 0}>
-{' '}
{formatCurrency({
value: montonioPayment.amount ?? 0,
currencyCode: currency_code,
locale: language,
})}
</span>
</div>
)}
</div>
</div>
);
}

View File

@@ -11,7 +11,7 @@ export default function OrderDetails({ order }: { order: AnalysisOrder }) {
<span className="font-bold">
<Trans i18nKey="cart:orderConfirmed.orderNumber" />:{' '}
</span>
<span>{order.medusa_order_id}</span>
<span className="break-all">{order.medusa_order_id}</span>
</div>
<div>

View File

@@ -0,0 +1,13 @@
'use server';
import { AccountBalanceService, AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
export async function getAccountBalanceSummary(accountId: string): Promise<AccountBalanceSummary | null> {
try {
const service = new AccountBalanceService();
return await service.getBalanceSummary(accountId);
} catch (error) {
console.error('Error getting account balance summary:', error);
return null;
}
}

View File

@@ -6,7 +6,7 @@ import jwt from 'jsonwebtoken';
import type { StoreCart, StoreOrder } from "@medusajs/types";
import { initiateMultiPaymentSession, placeOrder } from "@lib/data/cart";
import type { AccountBalanceSummary } from "~/lib/services/accountBalance.service";
import type { AccountBalanceSummary } from "@kit/accounts/services/account-balance.service";
import { handleNavigateToPayment } from "~/lib/services/medusaCart.service";
import { loadCurrentUserAccount } from "./load-user-account";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";