This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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'));`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -33,4 +33,12 @@ export const paymentMethodsData: PaymentMethodData[] = [
|
||||
order: 3,
|
||||
gateway: null,
|
||||
},
|
||||
{
|
||||
method: PaymentMethodEnum.CreditCard,
|
||||
description: 'پرداخت کارت به کارت',
|
||||
enabled: true,
|
||||
order: 4,
|
||||
gateway: null,
|
||||
cardNumber: '6037991234567890',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user