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