From 377ec7fb3449758481a0e95a860ac2f3bcb5454f Mon Sep 17 00:00:00 2001
From: parsa aghaei
Date: Sun, 26 Jul 2026 20:40:01 +0330
Subject: [PATCH] fix(orders,wallet): implement full-stack wallet deduction in
PostgreSQL and sync charity donation total in user profile
---
.ai_agency/memory/backlog.json | 29 ++++++++++++++++---
.ai_agency/memory/state.json | 6 ++--
backend/src/orders/dto/create-order.dto.ts | 5 ++++
backend/src/orders/orders.service.ts | 29 ++++++++++++++++++-
.../application/components/CheckoutPage.tsx | 14 ++++-----
.../application/lib/services/orderService.ts | 1 +
frontend/application/lib/store/cartStore.ts | 10 ++++++-
7 files changed, 76 insertions(+), 18 deletions(-)
diff --git a/.ai_agency/memory/backlog.json b/.ai_agency/memory/backlog.json
index c16a052..16ef298 100644
--- a/.ai_agency/memory/backlog.json
+++ b/.ai_agency/memory/backlog.json
@@ -378,14 +378,35 @@
{ "index": 1, "name": "add_pet_store_reset", "description": "Add reset method in usePetStore and call on logout", "status": "done" },
{ "index": 2, "name": "enforce_auth_guard_on_pet_store", "description": "Enforce isLoggedIn check on getActivePet()", "status": "done" }
]
+ },
+ {
+ "id": "EPIC-10-TASK-01",
+ "title": "Full-Stack Wallet Payment & Balance Deduction Persistence",
+ "description": "Add paymentMethod to CreateOrderDto and OrdersService.create in NestJS backend to deduct order amount from user walletBalance in PostgreSQL and create withdrawal WalletTransaction record upon checkout.",
+ "architectural_layer": "business_logic",
+ "assigned_role": "05_dev_backend",
+ "priority": "HIGH",
+ "status": "completed",
+ "max_files_allowed": 5,
+ "estimated_minutes": 25,
+ "dependency_task_ids": ["EPIC-09-TASK-03"],
+ "acceptance_criteria": [
+ "کسر واقعی و دقیق مبلغ سفارش از کیف پول کاربر در دیتابیس PostgreSQL هنگام پرداخت با کیف پول",
+ "ثبت تراکنش کسر از کیف پول (withdrawal) در دیتابیس و بروزرسانی لحظهای پروفایل کاربر"
+ ],
+ "sub_steps": [
+ { "index": 1, "name": "add_payment_method_dto", "description": "Add paymentMethod field to CreateOrderDto", "status": "done" },
+ { "index": 2, "name": "implement_wallet_deduction_in_orders_service", "description": "Deduct user walletBalance and create WalletTransaction in OrdersService.create", "status": "done" },
+ { "index": 3, "name": "wire_checkout_page_wallet_sync", "description": "Send paymentMethod in API call and trigger fetchProfile() in CheckoutPage.tsx", "status": "done" }
+ ]
}
],
"metadata": {
- "total": 19,
- "completed": 19,
+ "total": 20,
+ "completed": 20,
"in_progress": 0,
"pending": 0,
- "generated_at": "2026-07-26T20:35:00Z",
- "decomposition_pass": 7
+ "generated_at": "2026-07-26T20:40:00Z",
+ "decomposition_pass": 8
}
}
\ No newline at end of file
diff --git a/.ai_agency/memory/state.json b/.ai_agency/memory/state.json
index e81ac6f..12d2c33 100644
--- a/.ai_agency/memory/state.json
+++ b/.ai_agency/memory/state.json
@@ -7,12 +7,12 @@
"status": "COMPLETE",
"checkpoint": {
"active_agent": "01_auditor",
- "current_ticket_id": "EPIC-09-TASK-03",
+ "current_ticket_id": "EPIC-10-TASK-01",
"sub_step": {
"index": 3,
"total": 3,
- "name": "enforce_auth_guard_on_pet_store",
- "description": "Root cause pet profile mismatch resolved; Prisma Client regenerated, configurable charity round step added in admin panel, all builds 100% verified"
+ "name": "wire_checkout_page_wallet_sync",
+ "description": "Full-stack wallet balance deduction and charity donation total persistence in PostgreSQL complete and 100% verified"
}
},
"review_phase": {
diff --git a/backend/src/orders/dto/create-order.dto.ts b/backend/src/orders/dto/create-order.dto.ts
index becfa34..55e8837 100644
--- a/backend/src/orders/dto/create-order.dto.ts
+++ b/backend/src/orders/dto/create-order.dto.ts
@@ -39,6 +39,11 @@ export class CreateOrderDto {
@IsOptional()
isRefill?: boolean;
+ @ApiPropertyOptional({ description: 'روش پرداخت (online, wallet)' })
+ @IsOptional()
+ @IsString()
+ paymentMethod?: string;
+
@ApiPropertyOptional({ description: 'دوره زمانی تمدید خودکار (روز)' })
@IsOptional()
@IsNumber()
diff --git a/backend/src/orders/orders.service.ts b/backend/src/orders/orders.service.ts
index 30775cf..4e3cf01 100644
--- a/backend/src/orders/orders.service.ts
+++ b/backend/src/orders/orders.service.ts
@@ -116,8 +116,35 @@ export class OrdersService {
const finalAmount = Math.max(totalAmount - discountAmount - refillDiscount + charityAmount, 0);
const trackingNumber = this.generateTrackingNumber();
+ // Deduct user wallet balance if payment method is wallet
+ if (createOrderDto.paymentMethod === 'wallet' && userId) {
+ const user = await this.prisma.user.findUnique({ where: { id: userId } });
+ if (!user) {
+ throw new NotFoundException('کاربر یافت نشد');
+ }
+ const userBalance = Number(user.walletBalance || 0);
+ if (userBalance < finalAmount) {
+ throw new BadRequestException('موجودی کیف پول برای پرداخت این سفارش کافی نیست');
+ }
+ await this.prisma.user.update({
+ where: { id: userId },
+ data: {
+ walletBalance: { decrement: finalAmount }
+ }
+ });
+ await this.prisma.walletTransaction.create({
+ data: {
+ userId,
+ amount: finalAmount,
+ type: 'withdrawal',
+ status: 'completed',
+ description: `پرداخت سفارش ${trackingNumber}`
+ }
+ });
+ }
+
// Increment user charity total if charity donation was added
- if (charityAmount > 0) {
+ if (charityAmount > 0 && userId) {
try {
await this.prisma.user.update({
where: { id: userId },
diff --git a/frontend/application/components/CheckoutPage.tsx b/frontend/application/components/CheckoutPage.tsx
index a1cc928..6a2935a 100644
--- a/frontend/application/components/CheckoutPage.tsx
+++ b/frontend/application/components/CheckoutPage.tsx
@@ -114,19 +114,15 @@ export default function CheckoutPage() {
items: [...items],
total: totalAmount,
charityDonation: charityDonation,
+ paymentMethod: paymentMethod,
+ isRefill: isSubscribed,
petId: activePet?.id,
shippingAddress: shippingAddressStr
});
- // 3. Update user charity total
- if (charityDonation > 0) {
- useUserStore.setState((state) => ({
- profile: {
- ...state.profile,
- charityDonationTotal: (state.profile.charityDonationTotal || 0) + charityDonation
- }
- }));
- }
+ // Re-fetch user profile from backend to sync PostgreSQL wallet balance and charity donation total
+ const { fetchProfile } = useUserStore.getState();
+ await fetchProfile();
// 4. Update pet consumptions and charity contributions for active pet
if (activePet) {
diff --git a/frontend/application/lib/services/orderService.ts b/frontend/application/lib/services/orderService.ts
index 816c3a9..047a90d 100644
--- a/frontend/application/lib/services/orderService.ts
+++ b/frontend/application/lib/services/orderService.ts
@@ -41,6 +41,7 @@ export class OrderService {
couponCode?: string;
prescriptionUrl?: string;
charityDonation?: number;
+ paymentMethod?: string;
isRefill?: boolean;
refillIntervalDays?: number;
shippingAddress?: string;
diff --git a/frontend/application/lib/store/cartStore.ts b/frontend/application/lib/store/cartStore.ts
index d7b9874..f6c3786 100644
--- a/frontend/application/lib/store/cartStore.ts
+++ b/frontend/application/lib/store/cartStore.ts
@@ -21,6 +21,8 @@ interface Order {
items: CartItem[];
total: number;
charityDonation?: number;
+ paymentMethod?: string;
+ isRefill?: boolean;
status: 'processing' | 'shipped' | 'delivered';
petId?: string;
shippingAddress?: string;
@@ -39,7 +41,7 @@ interface CartStore {
toggleSubscription: () => void;
setCharityDonation: (amount: number) => void;
applyCoupon: (code: string) => Promise;
- addOrder: (order: Omit) => Promise;
+ addOrder: (orderData: Omit) => Promise;
clearCart: () => void;
removeCoupon: () => void;
setOrders: (orders: any[]) => void;
@@ -104,6 +106,12 @@ export const useCartStore = create()(
addOrder: async (orderData) => {
const payload = {
petId: orderData.petId,
+ couponCode: get().coupon?.code,
+ charityDonation: orderData.charityDonation,
+ paymentMethod: orderData.paymentMethod,
+ isRefill: orderData.isRefill || get().isSubscribed,
+ refillIntervalDays: 60,
+ shippingAddress: orderData.shippingAddress,
items: orderData.items.map(i => ({
productId: i.product.id,
quantity: i.quantity