fix(orders,wallet): implement full-stack wallet deduction in PostgreSQL and sync charity donation total in user profile

This commit is contained in:
parsa aghaei 2026-07-26 20:40:01 +03:30
parent 51b2f811fa
commit 377ec7fb34
7 changed files with 76 additions and 18 deletions

View File

@ -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
}
}

View File

@ -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": {

View File

@ -39,6 +39,11 @@ export class CreateOrderDto {
@IsOptional()
isRefill?: boolean;
@ApiPropertyOptional({ description: 'روش پرداخت (online, wallet)' })
@IsOptional()
@IsString()
paymentMethod?: string;
@ApiPropertyOptional({ description: 'دوره زمانی تمدید خودکار (روز)' })
@IsOptional()
@IsNumber()

View File

@ -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 },

View File

@ -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) {

View File

@ -41,6 +41,7 @@ export class OrderService {
couponCode?: string;
prescriptionUrl?: string;
charityDonation?: number;
paymentMethod?: string;
isRefill?: boolean;
refillIntervalDays?: number;
shippingAddress?: string;

View File

@ -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<boolean>;
addOrder: (order: Omit<Order, 'id' | 'date' | 'status' | 'trackingNumber'>) => Promise<string>;
addOrder: (orderData: Omit<Order, 'id' | 'date' | 'status' | 'trackingNumber'>) => Promise<string>;
clearCart: () => void;
removeCoupon: () => void;
setOrders: (orders: any[]) => void;
@ -104,6 +106,12 @@ export const useCartStore = create<CartStore>()(
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