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
+23 -2
View File
@@ -1076,7 +1076,8 @@
"enumItems": [
"Online",
"Cash",
"Wallet"
"Wallet",
"CreditCard"
],
"mappedType": "enum"
},
@@ -1102,6 +1103,16 @@
"length": 255,
"mappedType": "string"
},
"card_number": {
"name": "card_number",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 255,
"mappedType": "string"
},
"enabled": {
"name": "enabled",
"type": "boolean",
@@ -4523,7 +4534,8 @@
"enumItems": [
"Online",
"Cash",
"Wallet"
"Wallet",
"CreditCard"
],
"mappedType": "enum"
},
@@ -4612,6 +4624,15 @@
"nullable": true,
"length": 255,
"mappedType": "string"
},
"attachments": {
"name": "attachments",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
}
},
"name": "payments",
+11 -2
View File
@@ -1077,7 +1077,7 @@
"Online",
"Cash",
"Wallet",
"Cart"
"CreditCard"
],
"mappedType": "enum"
},
@@ -4535,7 +4535,7 @@
"Online",
"Cash",
"Wallet",
"Cart"
"CreditCard"
],
"mappedType": "enum"
},
@@ -4624,6 +4624,15 @@
"nullable": true,
"length": 255,
"mappedType": "string"
},
"attachments": {
"name": "attachments",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
}
},
"name": "payments",
@@ -0,0 +1,31 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260607131845 extends Migration {
override async up(): Promise<void> {
this.addSql(`update "payment_methods" set "method" = 'CreditCard' where "method" = 'Cart';`);
this.addSql(`update "payments" set "method" = 'CreditCard' where "method" = 'Cart';`);
this.addSql(`alter table "payment_methods" drop constraint if exists "payment_methods_method_check";`);
this.addSql(`alter table "payments" drop constraint if exists "payments_method_check";`);
this.addSql(`alter table "payments" add column if not exists "attachments" jsonb null;`);
this.addSql(`alter table "payment_methods" add constraint "payment_methods_method_check" check("method" in ('Online', 'Cash', 'Wallet', 'CreditCard'));`);
this.addSql(`alter table "payments" add constraint "payments_method_check" check("method" in ('Online', 'Cash', 'Wallet', 'CreditCard'));`);
}
override async down(): Promise<void> {
this.addSql(`alter table "payment_methods" drop constraint if exists "payment_methods_method_check";`);
this.addSql(`alter table "payments" drop constraint if exists "payments_method_check";`);
this.addSql(`update "payment_methods" set "method" = 'Cart' where "method" = 'CreditCard';`);
this.addSql(`update "payments" set "method" = 'Cart' where "method" = 'CreditCard';`);
this.addSql(`alter table "payments" drop column if exists "attachments";`);
this.addSql(`alter table "payment_methods" add constraint "payment_methods_method_check" check("method" in ('Online', 'Cash', 'Wallet', 'Cart'));`);
this.addSql(`alter table "payments" add constraint "payments_method_check" check("method" in ('Online', 'Cash', 'Wallet', 'Cart'));`);
}
}
+2
View File
@@ -741,6 +741,8 @@ export const enum PaymentMessage {
PAYMENT_NOT_FOUND = 'پرداخت یافت نشد',
INVALID_PAYMENT_CONFIGURATION = 'پیکربندی پرداخت نامعتبر است',
PAYMENT_METHOD_NOT_CASH = 'روش پرداخت نقدی نیست',
PAYMENT_METHOD_NOT_CREDIT_CARD = 'روش پرداخت کارت به کارت نیست',
CARD_NUMBER_REQUIRED = 'شماره کارت برای پرداخت کارت به کارت الزامی است',
PAYMENT_ALREADY_PAID = 'پرداخت قبلا انجام شده است',
EXISTING_PENDING_PAYMENT_MISMATCH = 'پرداخت در انتظار موجود با روش پرداخت سفارش مطابقت ندارد',
ZARINPAL_INVALID_RESPONSE = 'پاسخ نامعتبر از API زرین‌پال',
@@ -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);
+8
View File
@@ -33,4 +33,12 @@ export const paymentMethodsData: PaymentMethodData[] = [
order: 3,
gateway: null,
},
{
method: PaymentMethodEnum.CreditCard,
description: 'پرداخت کارت به کارت',
enabled: true,
order: 4,
gateway: null,
cardNumber: '6037991234567890',
},
];