credircard payment

This commit is contained in:
2026-06-15 11:59:52 +03:30
parent 4eedcf2576
commit 278f7accb6
7 changed files with 96 additions and 11 deletions
@@ -0,0 +1,15 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260615130000_remove_payment_details_from_orders extends Migration {
override async up(): Promise<void> {
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<void> {
this.addSql(`alter table "orders" add column "payment_description" text null;`);
this.addSql(`alter table "orders" add column "payment_attachments" jsonb null;`);
}
}
+2
View File
@@ -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 {
@@ -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',
})
@@ -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
*/
+7 -7
View File
@@ -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
@@ -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[];
}
+34 -2
View File
@@ -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<Payment, 'description' | 'attachments'> {
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() === '') {