diff --git a/app/home/(user)/(dashboard)/cart/page.tsx b/app/home/(user)/(dashboard)/cart/page.tsx
index 41dca03..a514cad 100644
--- a/app/home/(user)/(dashboard)/cart/page.tsx
+++ b/app/home/(user)/(dashboard)/cart/page.tsx
@@ -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 && }
);
diff --git a/app/home/(user)/_components/cart/index.tsx b/app/home/(user)/_components/cart/index.tsx
index 7887040..328dbdf 100644
--- a/app/home/(user)/_components/cart/index.tsx
+++ b/app/home/(user)/_components/cart/index.tsx
@@ -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 (
@@ -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 (
@@ -106,7 +123,7 @@ export default function Cart({
-
+
@@ -122,6 +139,24 @@ export default function Cart({
+ {companyBenefitsTotal > 0 && (
+
+
+
+
+ {formatCurrency({
+ value: (companyBenefitsTotal > cart.total ? cart.total : companyBenefitsTotal),
+ currencyCode: cart.currency_code,
+ locale: language,
+ })}
+
+
+
+ )}
@@ -131,7 +166,7 @@ export default function Cart({
{formatCurrency({
- value: cart.total,
+ value: montonioTotal < 0 ? 0 : montonioTotal,
currencyCode: cart.currency_code,
locale: language,
})}
@@ -175,7 +210,7 @@ export default function Cart({
);
}
diff --git a/app/home/(user)/_components/order/order-details.tsx b/app/home/(user)/_components/order/order-details.tsx
index 1cda501..0dbe973 100644
--- a/app/home/(user)/_components/order/order-details.tsx
+++ b/app/home/(user)/_components/order/order-details.tsx
@@ -11,7 +11,7 @@ export default function OrderDetails({ order }: { order: AnalysisOrder }) {
:{' '}
-
{order.medusa_order_id}
+
{order.medusa_order_id}
diff --git a/app/home/(user)/_lib/server/balance-actions.ts b/app/home/(user)/_lib/server/balance-actions.ts
new file mode 100644
index 0000000..425af58
--- /dev/null
+++ b/app/home/(user)/_lib/server/balance-actions.ts
@@ -0,0 +1,13 @@
+'use server';
+
+import { AccountBalanceService, AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
+
+export async function getAccountBalanceSummary(accountId: string): Promise
{
+ try {
+ const service = new AccountBalanceService();
+ return await service.getBalanceSummary(accountId);
+ } catch (error) {
+ console.error('Error getting account balance summary:', error);
+ return null;
+ }
+}
diff --git a/app/home/(user)/_lib/server/cart-actions.ts b/app/home/(user)/_lib/server/cart-actions.ts
index fb95d49..b7023f7 100644
--- a/app/home/(user)/_lib/server/cart-actions.ts
+++ b/app/home/(user)/_lib/server/cart-actions.ts
@@ -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";
diff --git a/lib/services/medusaCart.service.ts b/lib/services/medusaCart.service.ts
index 8d256b6..5d000c3 100644
--- a/lib/services/medusaCart.service.ts
+++ b/lib/services/medusaCart.service.ts
@@ -2,7 +2,7 @@
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
-import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
+import { addToCart, deleteLineItem } from '@lib/data/cart';
import { getCartId } from '@lib/data/cookies';
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
import { z } from 'zod';
@@ -87,9 +87,15 @@ export async function handleDeleteCartItem({ lineId }: { lineId: string }) {
export async function handleNavigateToPayment({
language,
paymentSessionId,
+ amount,
+ currencyCode,
+ cartId,
}: {
language: string;
paymentSessionId: string;
+ amount: number;
+ currencyCode: string;
+ cartId: string;
}) {
const supabase = getSupabaseServerClient();
const { account, user } = await loadCurrentUserAccount();
@@ -97,26 +103,21 @@ export async function handleNavigateToPayment({
throw new Error('Account not found');
}
- const cart = await retrieveCart();
- if (!cart) {
- throw new Error('No cart found');
- }
-
const paymentLink =
await new MontonioOrderHandlerService().getMontonioPaymentLink({
notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`,
returnUrl: `${env().siteUrl}/home/cart/montonio-callback`,
- amount: cart.total,
- currency: cart.currency_code.toUpperCase(),
+ amount,
+ currency: currencyCode.toUpperCase(),
description: `Order from Medreport`,
locale: language,
- merchantReference: `${account.id}:${paymentSessionId}:${cart.id}`,
+ merchantReference: `${account.id}:${paymentSessionId}:${cartId}`,
});
const { error } = await supabase.schema('audit').from('cart_entries').insert({
operation: 'NAVIGATE_TO_PAYMENT',
account_id: account.id,
- cart_id: cart.id,
+ cart_id: cartId,
changed_by: user.id,
});
if (error) {
diff --git a/lib/services/order.service.ts b/lib/services/order.service.ts
index 7edb7f7..b703cd8 100644
--- a/lib/services/order.service.ts
+++ b/lib/services/order.service.ts
@@ -51,48 +51,6 @@ export async function createAnalysisOrder({
return orderResult.data.id;
}
-export async function updateAnalysisOrder({
- orderId,
- orderStatus,
-}: {
- orderId: number;
- orderStatus: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
-}) {
- console.info(`Updating order id=${orderId} status=${orderStatus}`);
- await getSupabaseServerAdminClient()
- .schema('medreport')
- .from('analysis_orders')
- .update({
- status: orderStatus,
- })
- .eq('id', orderId)
- .throwOnError();
-}
-
-export async function updateAnalysisOrderStatus({
- orderId,
- medusaOrderId,
- orderStatus,
-}: {
- orderId?: number;
- medusaOrderId?: string;
- orderStatus: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
-}) {
- const orderIdParam = orderId;
- const medusaOrderIdParam = medusaOrderId;
- if (!orderIdParam && !medusaOrderIdParam) {
- throw new Error('Either orderId or medusaOrderId must be provided');
- }
- await getSupabaseServerAdminClient()
- .schema('medreport')
- .rpc('update_analysis_order_status', {
- order_id: orderIdParam ?? -1,
- status_param: orderStatus,
- medusa_order_id_param: medusaOrderIdParam ?? '',
- })
- .throwOnError();
-}
-
export async function getAnalysisOrder({
medusaOrderId,
analysisOrderId,
diff --git a/lib/types/account-balance-entry.ts b/lib/types/account-balance-entry.ts
new file mode 100644
index 0000000..434e5e6
--- /dev/null
+++ b/lib/types/account-balance-entry.ts
@@ -0,0 +1,3 @@
+import type { Database } from "@/packages/supabase/src/database.types";
+
+export type AccountBalanceEntry = Database['medreport']['Tables']['account_balance_entries']['Row'];
diff --git a/packages/features/accounts/package.json b/packages/features/accounts/package.json
index e75f6b5..9385881 100644
--- a/packages/features/accounts/package.json
+++ b/packages/features/accounts/package.json
@@ -12,6 +12,7 @@
"./personal-account-settings": "./src/components/personal-account-settings/index.ts",
"./components": "./src/components/index.ts",
"./hooks/*": "./src/hooks/*.ts",
+ "./services/*": "./src/server/services/*.ts",
"./api": "./src/server/api.ts",
"./types/*": "./src/types/*.ts"
},
diff --git a/packages/features/accounts/src/server/services/account-balance.service.ts b/packages/features/accounts/src/server/services/account-balance.service.ts
new file mode 100644
index 0000000..76f1b2f
--- /dev/null
+++ b/packages/features/accounts/src/server/services/account-balance.service.ts
@@ -0,0 +1,125 @@
+import { getSupabaseServerClient } from '@kit/supabase/server-client';
+
+import type { AccountBalanceEntry } from '../../types/account-balance-entry';
+
+export type AccountBalanceSummary = {
+ totalBalance: number;
+ expiringSoon: number;
+ recentEntries: AccountBalanceEntry[];
+};
+
+export class AccountBalanceService {
+ private supabase: ReturnType;
+
+ constructor() {
+ this.supabase = getSupabaseServerClient();
+ }
+
+ /**
+ * Get the current balance for a specific account
+ */
+ async getAccountBalance(accountId: string): Promise {
+ const { data, error } = await this.supabase
+ .schema('medreport')
+ .rpc('get_account_balance', {
+ p_account_id: accountId,
+ });
+
+ if (error) {
+ console.error('Error getting account balance:', error);
+ throw new Error('Failed to get account balance');
+ }
+
+ return data || 0;
+ }
+
+ /**
+ * Get balance entries for an account with pagination
+ */
+ async getAccountBalanceEntries(
+ accountId: string,
+ options: {
+ limit?: number;
+ offset?: number;
+ entryType?: string;
+ includeInactive?: boolean;
+ } = {}
+ ): Promise<{
+ entries: AccountBalanceEntry[];
+ total: number;
+ }> {
+ const { limit = 50, offset = 0, entryType, includeInactive = false } = options;
+
+ let query = this.supabase
+ .schema('medreport')
+ .from('account_balance_entries')
+ .select('*', { count: 'exact' })
+ .eq('account_id', accountId)
+ .order('created_at', { ascending: false })
+ .range(offset, offset + limit - 1);
+
+ if (!includeInactive) {
+ query = query.eq('is_active', true);
+ }
+
+ if (entryType) {
+ query = query.eq('entry_type', entryType);
+ }
+
+ const { data, error, count } = await query;
+
+ if (error) {
+ console.error('Error getting account balance entries:', error);
+ throw new Error('Failed to get account balance entries');
+ }
+
+ return {
+ entries: data || [],
+ total: count || 0,
+ };
+ }
+
+ /**
+ * Get balance summary for dashboard display
+ */
+ async getBalanceSummary(accountId: string): Promise {
+ const [balance, entries] = await Promise.all([
+ this.getAccountBalance(accountId),
+ this.getAccountBalanceEntries(accountId, { limit: 5 }),
+ ]);
+
+ // Calculate expiring balance (next 30 days)
+ const thirtyDaysFromNow = new Date();
+ thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
+
+ const { data: expiringData, error: expiringError } = await this.supabase
+ .schema('medreport')
+ .from('account_balance_entries')
+ .select('amount')
+ .eq('account_id', accountId)
+ .eq('is_active', true)
+ .not('expires_at', 'is', null)
+ .lte('expires_at', thirtyDaysFromNow.toISOString());
+
+ if (expiringError) {
+ console.error('Error getting expiring balance:', expiringError);
+ }
+
+ const expiringSoon = expiringData?.reduce((sum, entry) => sum + (entry.amount || 0), 0) || 0;
+
+ return {
+ totalBalance: balance,
+ expiringSoon,
+ recentEntries: entries.entries,
+ };
+ }
+
+ async processPeriodicBenefitDistributions(): Promise {
+ const { error } = await this.supabase.schema('medreport').rpc('process_periodic_benefit_distributions')
+ if (error) {
+ console.error('Error processing periodic benefit distributions:', error);
+ throw new Error('Failed to process periodic benefit distributions');
+ }
+ }
+
+}
diff --git a/packages/features/accounts/src/types/account-balance-entry.ts b/packages/features/accounts/src/types/account-balance-entry.ts
new file mode 100644
index 0000000..e76cca1
--- /dev/null
+++ b/packages/features/accounts/src/types/account-balance-entry.ts
@@ -0,0 +1,3 @@
+import type { Database } from '@kit/supabase/database';
+
+export type AccountBalanceEntry = Database['medreport']['Tables']['account_balance_entries']['Row'];
diff --git a/packages/features/medusa-storefront/src/lib/data/cart.ts b/packages/features/medusa-storefront/src/lib/data/cart.ts
index 411edc5..659495f 100644
--- a/packages/features/medusa-storefront/src/lib/data/cart.ts
+++ b/packages/features/medusa-storefront/src/lib/data/cart.ts
@@ -253,6 +253,37 @@ export async function setShippingMethod({
.catch(medusaError);
}
+export async function initiateMultiPaymentSession(
+ cart: HttpTypes.StoreCart,
+ benefitsAmount: number,
+) {
+ const headers = {
+ ...(await getAuthHeaders()),
+ };
+
+ return sdk.client.fetch(`/store/multi-payment`, {
+ method: 'POST',
+ body: { cartId: cart.id, benefitsAmount },
+ headers,
+ })
+ .then(async (response) => {
+ console.info('Payment session initiated:', response);
+ const cartCacheTag = await getCacheTag('carts');
+ revalidateTag(cartCacheTag);
+ return response as {
+ montonioPaymentSessionId: string | null;
+ companyBenefitsPaymentSessionId: string | null;
+ totalByBenefits: number;
+ totalByMontonio: number;
+ isFullyPaidByBenefits: boolean;
+ };
+ })
+ .catch((e) => {
+ console.error('Error initiating payment session:', e, JSON.stringify(Object.keys(e)));
+ return medusaError(e);
+ });
+}
+
export async function initiatePaymentSession(
cart: HttpTypes.StoreCart,
data: HttpTypes.StoreInitializePaymentSession,
diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts
index 30db3f3..58ffac2 100644
--- a/packages/supabase/src/database.types.ts
+++ b/packages/supabase/src/database.types.ts
@@ -337,6 +337,51 @@ export type Database = {
}
medreport: {
Tables: {
+ account_balance_entries: {
+ Row: {
+ id: string
+ account_id: string
+ amount: number
+ entry_type: string
+ description: string
+ source_company_id: string
+ reference_id: string
+ created_at: string
+ created_by: string
+ expires_at: string
+ is_active: boolean
+ is_analysis_order: boolean
+ is_analysis_package_order: boolean
+ }
+ Insert: {
+ account_id: string
+ amount: number
+ entry_type: string
+ description: string
+ source_company_id: string
+ reference_id: string
+ created_at: string
+ created_by: string
+ expires_at: string
+ is_active: boolean
+ is_analysis_order?: boolean
+ is_analysis_package_order?: boolean
+ }
+ Update: {
+ account_id?: string
+ amount?: number
+ entry_type?: string
+ description?: string
+ source_company_id?: string
+ reference_id?: string
+ created_at?: string
+ created_by?: string
+ expires_at?: string
+ is_active?: boolean
+ is_analysis_order?: boolean
+ is_analysis_package_order?: boolean
+ }
+ }
account_params: {
Row: {
account_id: string
@@ -2148,6 +2193,10 @@ export type Database = {
Args: { medusa_order_id: string }
Returns: boolean
}
+ process_periodic_benefit_distributions: {
+ Args: {}
+ Returns: void
+ }
revoke_nonce: {
Args: { p_id: string; p_reason?: string }
Returns: boolean
diff --git a/supabase/migrations/20250926040043_update_consume_balance.sql b/supabase/migrations/20250926040043_update_consume_balance.sql
new file mode 100644
index 0000000..5f50c31
--- /dev/null
+++ b/supabase/migrations/20250926040043_update_consume_balance.sql
@@ -0,0 +1,59 @@
+alter table medreport.account_balance_entries add column "is_analysis_order" boolean;
+alter table medreport.account_balance_entries add column "is_analysis_package_order" boolean;
+
+drop function if exists medreport.consume_account_balance(uuid, numeric, text, text);
+
+-- Create function to consume balance (for purchases)
+create or replace function medreport.consume_account_balance(
+ p_account_id uuid,
+ p_amount numeric,
+ p_description text,
+ p_reference_id text default null,
+ p_is_analysis_order boolean default false,
+ p_is_analysis_package_order boolean default false
+)
+returns boolean
+language plpgsql
+security definer
+as $$
+declare
+ current_balance numeric;
+ remaining_amount numeric := p_amount;
+ entry_record record;
+ consumed_amount numeric;
+begin
+ -- Get current balance
+ current_balance := medreport.get_account_balance(p_account_id);
+
+ -- Check if sufficient balance
+ if current_balance < p_amount then
+ return false;
+ end if;
+
+ -- Record the consumption
+ insert into medreport.account_balance_entries (
+ account_id,
+ amount,
+ entry_type,
+ description,
+ reference_id,
+ created_by,
+ is_analysis_order,
+ is_analysis_package_order
+ ) values (
+ p_account_id,
+ -p_amount,
+ 'purchase',
+ p_description,
+ p_reference_id,
+ auth.uid(),
+ p_is_analysis_order,
+ p_is_analysis_package_order
+ );
+
+ return true;
+end;
+$$;
+
+-- Grant execute permission
+grant execute on function medreport.consume_account_balance(uuid, numeric, text, text, boolean, boolean) to authenticated, service_role;