add credit card payment method
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-06-07 16:56:55 +03:30
parent caaeb0d34a
commit 89fa1d637e
14 changed files with 155 additions and 9 deletions
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
import { IsArray, IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
import { SetCarDeliveryDto } from './set-car-delivery.dto';
@@ -37,4 +37,24 @@ export class SetAllCartParmsDto {
@IsOptional()
@IsObject()
carAddress?: SetCarDeliveryDto;
@ApiProperty({
description: 'Payment receipt attachments (URLs)',
required: false,
type: [String],
example: ['https://example.com/receipt.jpg'],
})
@IsOptional()
@IsArray()
@IsString({ each: true })
attachments?: string[];
@ApiProperty({
description: 'Payment description or note',
required: false,
example: 'Transfer from account ending 1234',
})
@IsOptional()
@IsString()
paymentDesc?: string;
}
@@ -20,6 +20,8 @@ export interface Cart {
deliveryMethodId?: string;
description?: string;
tableNumber?: string;
attachments?: string[];
paymentDesc?: string;
deliveryFee: number;
subTotal: number;
+9 -1
View File
@@ -28,7 +28,7 @@ export class CartService {
* Set all cart parameters at once
*/
async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise<Cart> {
const { deliveryMethodId, paymentMethodId, addressId, carAddress, description, tableNumber } = params;
const { deliveryMethodId, paymentMethodId, addressId, carAddress, description, tableNumber, attachments, paymentDesc } = params;
// get existing cart (or throw)
const cart = await this.findOneOrFail(userId, restaurantId);
@@ -120,6 +120,14 @@ export class CartService {
cart.description = description;
}
if (attachments !== undefined) {
cart.attachments = attachments;
}
if (paymentDesc !== undefined) {
cart.paymentDesc = paymentDesc;
}
// Final validation: if effective delivery method is courier, ensure all foods have pickupServe
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method);
@@ -115,6 +115,8 @@ 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 } : {}),
});
em.persist(payment);
@@ -131,6 +131,16 @@ export class PaymentsController {
return this.paymentsService.verifyCashPayment(paymentId);
}
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_PAYMENTS)
@ApiBearerAuth()
@Post('admin/payments/verify-credit-card-method/:paymentId')
@ApiOperation({ summary: 'Verify credit card payment' })
@ApiParam({ name: 'paymentId', type: 'string', description: 'Payment ID' })
verifyCreditCardPayment(@Param('paymentId') paymentId: string) {
return this.paymentsService.verifyCreditCardPayment(paymentId);
}
@UseGuards(AdminAuthGuard)
@Permissions(Permission.VIEW_REPORTS)
@ApiBearerAuth()
@@ -42,4 +42,8 @@ export class Payment extends BaseEntity {
@Property({ nullable: true })
description?: string | null = null;
// payment receipt attachments
@Property({ type: 'json', nullable: true })
attachments?: string[];
}
+1 -1
View File
@@ -4,7 +4,7 @@ export enum PaymentMethodEnum {
Online = 'Online',
Cash = 'Cash',
Wallet = 'Wallet',
Cart = 'Cart',
CreditCard = 'CreditCard',
}
export enum PaymentStatusEnum {
Pending = 'pending',
@@ -31,6 +31,7 @@ export class PaymentListeners {
Cash: 'نقدی',
Wallet: 'کیف پول',
Online: 'آنلاین',
CreditCard: 'کارت به کارت',
};
return methodMap[method] || method;
}
@@ -44,6 +44,10 @@ export class PaymentsService {
case PaymentMethodEnum.Online:
return this.handleOnlinePayment(ctx);
case PaymentMethodEnum.CreditCard:
await this.handleCreditCardPayment(ctx);
return { paymentUrl: null };
default:
throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD);
}
@@ -75,6 +79,10 @@ export class PaymentsService {
if (!pm.restaurant?.domain) throw new BadRequestException(PaymentMessage.RESTAURANT_DOMAIN_REQUIRED);
}
if (pm.method === PaymentMethodEnum.CreditCard && !pm.cardNumber) {
throw new BadRequestException(PaymentMessage.CARD_NUMBER_REQUIRED);
}
return {
order,
amount: order.total,
@@ -93,6 +101,14 @@ export class PaymentsService {
});
}
private async handleCreditCardPayment(ctx: OrderPaymentContext): Promise<void> {
await this.getOrCreateLatestPendingPayment(ctx.order.id, {
amount: ctx.amount,
method: PaymentMethodEnum.CreditCard,
gateway: null,
});
}
// TODO clean this code and refactor it
private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> {
await this.em.transactional(async em => {
@@ -242,13 +258,25 @@ export class PaymentsService {
}
async verifyCashPayment(id: string): Promise<Payment> {
return this.verifyManualPayment(id, PaymentMethodEnum.Cash, PaymentMessage.PAYMENT_METHOD_NOT_CASH);
}
async verifyCreditCardPayment(id: string): Promise<Payment> {
return this.verifyManualPayment(id, PaymentMethodEnum.CreditCard, PaymentMessage.PAYMENT_METHOD_NOT_CREDIT_CARD);
}
private async verifyManualPayment(
id: string,
expectedMethod: PaymentMethodEnum,
wrongMethodMessage: PaymentMessage,
): Promise<Payment> {
const payment = await this.em.transactional(async em => {
const payment = await em.findOne(Payment, id, { populate: ['order'] });
if (!payment) {
throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
}
if (payment.method !== PaymentMethodEnum.Cash) {
throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH);
if (payment.method !== expectedMethod) {
throw new BadRequestException(wrongMethodMessage);
}
if (payment.status === PaymentStatusEnum.Paid) {
throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID);