From 278f7accb6cb39728a340af50270680d3742574d Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 15 Jun 2026 11:59:52 +0330 Subject: [PATCH] credircard payment --- ...0000_remove_payment_details_from_orders.ts | 15 ++++++++ src/common/enums/message.enum.ts | 2 ++ .../cart/dto/set-all-cart-params.dto.ts | 4 +-- .../cart/providers/cart-validation.service.ts | 18 ++++++++++ src/modules/cart/providers/cart.service.ts | 14 ++++---- .../orders/dto/admin-create-order.dto.ts | 18 ++++++++++ .../orders/providers/orders.service.ts | 36 +++++++++++++++++-- 7 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 database/migrations/Migration20260615130000_remove_payment_details_from_orders.ts diff --git a/database/migrations/Migration20260615130000_remove_payment_details_from_orders.ts b/database/migrations/Migration20260615130000_remove_payment_details_from_orders.ts new file mode 100644 index 0000000..0c58b3d --- /dev/null +++ b/database/migrations/Migration20260615130000_remove_payment_details_from_orders.ts @@ -0,0 +1,15 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260615130000_remove_payment_details_from_orders extends Migration { + + override async up(): Promise { + this.addSql(`alter table "orders" drop column if exists "payment_description";`); + this.addSql(`alter table "orders" drop column if exists "payment_attachments";`); + } + + override async down(): Promise { + this.addSql(`alter table "orders" add column "payment_description" text null;`); + this.addSql(`alter table "orders" add column "payment_attachments" jsonb null;`); + } + +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 0e6a91e..e7e5d25 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -694,6 +694,7 @@ export const enum OrderMessage { PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست', FOOD_NOT_FOUND = 'غذا یافت نشد', FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد', + CREDIT_CARD_PAYMENT_DETAILS_REQUIRED = 'حداقل یکی از توضیحات پرداخت یا پیوست رسید کارت به کارت الزامی است', } export const enum CartMessage { @@ -726,6 +727,7 @@ export const enum CartMessage { FOOD_ONLY_AVAILABLE_DURING_MEAL_TIMES = 'غذا [foodTitle] فقط در ساعات وعده‌های غذایی (صبحانه، ناهار، عصرانه یا شام) در دسترس است. زمان فعلی خارج از ساعات وعده‌های غذایی است', FOOD_ONLY_AVAILABLE_FOR_MEAL_TYPES = 'غذا [foodTitle] فقط برای [allowedMealTypes] در دسترس است. زمان فعلی [mealType] است که با این غذا سازگار نیست', FOOD_ONLY_AVAILABLE_ON_DAYS = 'غذا [foodTitle] فقط در روزهای [allowedDays] در دسترس است. امروز [currentDay] است که این غذا در دسترس نیست', + CREDIT_CARD_PAYMENT_DETAILS_REQUIRED = 'حداقل یکی از توضیحات پرداخت یا پیوست رسید کارت به کارت الزامی است', } export const enum PaymentMessage { diff --git a/src/modules/cart/dto/set-all-cart-params.dto.ts b/src/modules/cart/dto/set-all-cart-params.dto.ts index 3151a67..9710211 100644 --- a/src/modules/cart/dto/set-all-cart-params.dto.ts +++ b/src/modules/cart/dto/set-all-cart-params.dto.ts @@ -39,7 +39,7 @@ export class SetAllCartParmsDto { carAddress?: SetCarDeliveryDto; @ApiProperty({ - description: 'Payment receipt attachments (URLs)', + description: 'Payment receipt attachments (URLs). At least one of paymentDesc or attachments is required for credit card.', required: false, type: [String], example: ['https://example.com/receipt.jpg'], @@ -50,7 +50,7 @@ export class SetAllCartParmsDto { attachments?: string[]; @ApiProperty({ - description: 'Payment description or note', + description: 'Payment description or note. At least one of paymentDesc or attachments is required for credit card.', required: false, example: 'Transfer from account ending 1234', }) diff --git a/src/modules/cart/providers/cart-validation.service.ts b/src/modules/cart/providers/cart-validation.service.ts index 6af1065..5d149a2 100644 --- a/src/modules/cart/providers/cart-validation.service.ts +++ b/src/modules/cart/providers/cart-validation.service.ts @@ -5,6 +5,7 @@ import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { UserAddress } from '../../users/entities/user-address.entity'; import { User } from '../../users/entities/user.entity'; import { PaymentMethod } from '../../payments/entities/payment-method.entity'; +import { PaymentMethodEnum } from '../../payments/interface/payment'; import { Delivery } from '../../delivery/entities/delivery.entity'; import { DeliveryMethodEnum } from '../../delivery/interface/delivery'; import { PointTransaction } from '../../users/entities/point-transaction.entity'; @@ -216,6 +217,23 @@ export class CartValidationService { return paymentMethod; } + assertCreditCardPaymentDetails( + paymentMethod: PaymentMethod, + paymentDesc?: string | null, + attachments?: string[] | null, + ): void { + if (paymentMethod.method !== PaymentMethodEnum.CreditCard) { + return; + } + + const hasDescription = Boolean(paymentDesc?.trim()); + const hasAttachments = Boolean(attachments?.length); + + if (!hasDescription && !hasAttachments) { + throw new BadRequestException(CartMessage.CREDIT_CARD_PAYMENT_DETAILS_REQUIRED); + } + } + /** * Assert wallet has enough balance */ diff --git a/src/modules/cart/providers/cart.service.ts b/src/modules/cart/providers/cart.service.ts index f67366a..692d7f6 100644 --- a/src/modules/cart/providers/cart.service.ts +++ b/src/modules/cart/providers/cart.service.ts @@ -115,17 +115,17 @@ export class CartService { cart.paymentMethodId = paymentMethodId; - // Set description (if provided) if (description !== undefined) { cart.description = description; } - if (attachments !== undefined) { - cart.attachments = attachments; - } - - if (paymentDesc !== undefined) { - cart.paymentDesc = paymentDesc; + if (paymentMethod.method === PaymentMethodEnum.CreditCard) { + this.validationService.assertCreditCardPaymentDetails(paymentMethod, paymentDesc, attachments); + cart.paymentDesc = paymentDesc?.trim() || undefined; + cart.attachments = attachments?.length ? attachments : undefined; + } else { + delete cart.paymentDesc; + delete cart.attachments; } // Final validation: if effective delivery method is courier, ensure all foods have pickupServe diff --git a/src/modules/orders/dto/admin-create-order.dto.ts b/src/modules/orders/dto/admin-create-order.dto.ts index d147083..65077d8 100644 --- a/src/modules/orders/dto/admin-create-order.dto.ts +++ b/src/modules/orders/dto/admin-create-order.dto.ts @@ -94,4 +94,22 @@ export class AdminCreateOrderDto { }) @IsOptional() carAddress?: OrderCarAddress; + + @ApiPropertyOptional({ + description: 'Payment description. At least one of paymentDesc or paymentAttachments is required for credit card.', + example: 'Transfer from account ending 1234', + }) + @IsOptional() + @IsString() + paymentDesc?: string; + + @ApiPropertyOptional({ + description: 'Payment receipt attachments (URLs). At least one of paymentDesc or paymentAttachments is required for credit card.', + type: [String], + example: ['https://example.com/receipt.jpg'], + }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + paymentAttachments?: string[]; } diff --git a/src/modules/orders/providers/orders.service.ts b/src/modules/orders/providers/orders.service.ts index b347b6b..f3254f6 100644 --- a/src/modules/orders/providers/orders.service.ts +++ b/src/modules/orders/providers/orders.service.ts @@ -64,6 +64,7 @@ export class OrdersService { async checkout(userId: string, restaurantId: string) { const cart = await this.cartService.findOneOrFail(userId, restaurantId); const validated = await this.validateCartForOrder(userId, restaurantId, cart); +console.log(cart); const order = await this.em.transactional(async em => { const order = em.create(Order, { @@ -115,8 +116,7 @@ export class OrdersService { status: PaymentStatusEnum.Pending, method: order.paymentMethod.method, gateway: order.paymentMethod.gateway ?? null, - ...(cart.attachments?.length ? { attachments: cart.attachments } : {}), - ...(cart.paymentDesc ? { description: cart.paymentDesc } : {}), + ...this.buildCreditCardPaymentFields(validated.paymentMethod, cart.paymentDesc, cart.attachments), }); em.persist(payment); @@ -164,6 +164,7 @@ export class OrdersService { const paymentMethod = await this.getPaymentMethodOrFail(dto.paymentMethodId, restaurantId); this.assertPaymentMethodEnabled(paymentMethod); + this.assertCreditCardPaymentDetails(paymentMethod, dto.paymentDesc, dto.paymentAttachments); this.assertAdminDeliveryRequirements(dto, delivery); @@ -215,6 +216,7 @@ export class OrdersService { status: PaymentStatusEnum.Pending, method: order.paymentMethod.method, gateway: order.paymentMethod.gateway ?? null, + ...this.buildCreditCardPaymentFields(paymentMethod, dto.paymentDesc, dto.paymentAttachments), }); em.persist(payment); @@ -255,6 +257,7 @@ export class OrdersService { const paymentMethod = await this.getPaymentMethodOrFail(cart.paymentMethodId!, restaurantId); this.assertPaymentMethodEnabled(paymentMethod); + this.assertCreditCardPaymentDetails(paymentMethod, cart.paymentDesc, cart.attachments); const orderItemsData = await this.buildOrderItemsData(cart, restaurantId); @@ -479,6 +482,35 @@ export class OrdersService { } } + private buildCreditCardPaymentFields( + paymentMethod: PaymentMethod, + paymentDesc?: string | null, + attachments?: string[] | null, + ): Pick { + + return { + ...(attachments?.length ? { attachments } : {}), + ...(paymentDesc?.trim() ? { description: paymentDesc.trim() } : {}), + }; + } + + private assertCreditCardPaymentDetails( + paymentMethod: PaymentMethod, + paymentDesc?: string | null, + attachments?: string[] | null, + ): void { + if (paymentMethod.method !== PaymentMethodEnum.CreditCard) { + return; + } + + const hasDescription = Boolean(paymentDesc?.trim()); + const hasAttachments = Boolean(attachments?.length); + + if (!hasDescription && !hasAttachments) { + throw new BadRequestException(OrderMessage.CREDIT_CARD_PAYMENT_DETAILS_REQUIRED); + } + } + private assertDeliveryMethodRequirements(cart: Cart, delivery: Delivery) { if (delivery.method === DeliveryMethodEnum.DineIn) { if (!cart.tableNumber || cart.tableNumber.trim() === '') {