This commit is contained in:
@@ -1076,7 +1076,8 @@
|
|||||||
"enumItems": [
|
"enumItems": [
|
||||||
"Online",
|
"Online",
|
||||||
"Cash",
|
"Cash",
|
||||||
"Wallet"
|
"Wallet",
|
||||||
|
"CreditCard"
|
||||||
],
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
@@ -1102,6 +1103,16 @@
|
|||||||
"length": 255,
|
"length": 255,
|
||||||
"mappedType": "string"
|
"mappedType": "string"
|
||||||
},
|
},
|
||||||
|
"card_number": {
|
||||||
|
"name": "card_number",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"length": 255,
|
||||||
|
"mappedType": "string"
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "enabled",
|
"name": "enabled",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@@ -4523,7 +4534,8 @@
|
|||||||
"enumItems": [
|
"enumItems": [
|
||||||
"Online",
|
"Online",
|
||||||
"Cash",
|
"Cash",
|
||||||
"Wallet"
|
"Wallet",
|
||||||
|
"CreditCard"
|
||||||
],
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
@@ -4612,6 +4624,15 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"length": 255,
|
"length": 255,
|
||||||
"mappedType": "string"
|
"mappedType": "string"
|
||||||
|
},
|
||||||
|
"attachments": {
|
||||||
|
"name": "attachments",
|
||||||
|
"type": "jsonb",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"mappedType": "json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "payments",
|
"name": "payments",
|
||||||
|
|||||||
@@ -1077,7 +1077,7 @@
|
|||||||
"Online",
|
"Online",
|
||||||
"Cash",
|
"Cash",
|
||||||
"Wallet",
|
"Wallet",
|
||||||
"Cart"
|
"CreditCard"
|
||||||
],
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
@@ -4535,7 +4535,7 @@
|
|||||||
"Online",
|
"Online",
|
||||||
"Cash",
|
"Cash",
|
||||||
"Wallet",
|
"Wallet",
|
||||||
"Cart"
|
"CreditCard"
|
||||||
],
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
@@ -4624,6 +4624,15 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"length": 255,
|
"length": 255,
|
||||||
"mappedType": "string"
|
"mappedType": "string"
|
||||||
|
},
|
||||||
|
"attachments": {
|
||||||
|
"name": "attachments",
|
||||||
|
"type": "jsonb",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"mappedType": "json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "payments",
|
"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 = 'پرداخت یافت نشد',
|
PAYMENT_NOT_FOUND = 'پرداخت یافت نشد',
|
||||||
INVALID_PAYMENT_CONFIGURATION = 'پیکربندی پرداخت نامعتبر است',
|
INVALID_PAYMENT_CONFIGURATION = 'پیکربندی پرداخت نامعتبر است',
|
||||||
PAYMENT_METHOD_NOT_CASH = 'روش پرداخت نقدی نیست',
|
PAYMENT_METHOD_NOT_CASH = 'روش پرداخت نقدی نیست',
|
||||||
|
PAYMENT_METHOD_NOT_CREDIT_CARD = 'روش پرداخت کارت به کارت نیست',
|
||||||
|
CARD_NUMBER_REQUIRED = 'شماره کارت برای پرداخت کارت به کارت الزامی است',
|
||||||
PAYMENT_ALREADY_PAID = 'پرداخت قبلا انجام شده است',
|
PAYMENT_ALREADY_PAID = 'پرداخت قبلا انجام شده است',
|
||||||
EXISTING_PENDING_PAYMENT_MISMATCH = 'پرداخت در انتظار موجود با روش پرداخت سفارش مطابقت ندارد',
|
EXISTING_PENDING_PAYMENT_MISMATCH = 'پرداخت در انتظار موجود با روش پرداخت سفارش مطابقت ندارد',
|
||||||
ZARINPAL_INVALID_RESPONSE = 'پاسخ نامعتبر از API زرینپال',
|
ZARINPAL_INVALID_RESPONSE = 'پاسخ نامعتبر از API زرینپال',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
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';
|
import { SetCarDeliveryDto } from './set-car-delivery.dto';
|
||||||
|
|
||||||
|
|
||||||
@@ -37,4 +37,24 @@ export class SetAllCartParmsDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
carAddress?: SetCarDeliveryDto;
|
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;
|
deliveryMethodId?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tableNumber?: string;
|
tableNumber?: string;
|
||||||
|
attachments?: string[];
|
||||||
|
paymentDesc?: string;
|
||||||
|
|
||||||
deliveryFee: number;
|
deliveryFee: number;
|
||||||
subTotal: number;
|
subTotal: number;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class CartService {
|
|||||||
* Set all cart parameters at once
|
* Set all cart parameters at once
|
||||||
*/
|
*/
|
||||||
async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise<Cart> {
|
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)
|
// get existing cart (or throw)
|
||||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||||
@@ -120,6 +120,14 @@ export class CartService {
|
|||||||
cart.description = description;
|
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
|
// Final validation: if effective delivery method is courier, ensure all foods have pickupServe
|
||||||
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method);
|
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method);
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ export class OrdersService {
|
|||||||
status: PaymentStatusEnum.Pending,
|
status: PaymentStatusEnum.Pending,
|
||||||
method: order.paymentMethod.method,
|
method: order.paymentMethod.method,
|
||||||
gateway: order.paymentMethod.gateway ?? null,
|
gateway: order.paymentMethod.gateway ?? null,
|
||||||
|
...(cart.attachments?.length ? { attachments: cart.attachments } : {}),
|
||||||
|
...(cart.paymentDesc ? { description: cart.paymentDesc } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
em.persist(payment);
|
em.persist(payment);
|
||||||
|
|||||||
@@ -131,6 +131,16 @@ export class PaymentsController {
|
|||||||
return this.paymentsService.verifyCashPayment(paymentId);
|
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)
|
@UseGuards(AdminAuthGuard)
|
||||||
@Permissions(Permission.VIEW_REPORTS)
|
@Permissions(Permission.VIEW_REPORTS)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
|
|||||||
@@ -42,4 +42,8 @@ export class Payment extends BaseEntity {
|
|||||||
|
|
||||||
@Property({ nullable: true })
|
@Property({ nullable: true })
|
||||||
description?: string | null = null;
|
description?: string | null = null;
|
||||||
|
|
||||||
|
// payment receipt attachments
|
||||||
|
@Property({ type: 'json', nullable: true })
|
||||||
|
attachments?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export enum PaymentMethodEnum {
|
|||||||
Online = 'Online',
|
Online = 'Online',
|
||||||
Cash = 'Cash',
|
Cash = 'Cash',
|
||||||
Wallet = 'Wallet',
|
Wallet = 'Wallet',
|
||||||
Cart = 'Cart',
|
CreditCard = 'CreditCard',
|
||||||
}
|
}
|
||||||
export enum PaymentStatusEnum {
|
export enum PaymentStatusEnum {
|
||||||
Pending = 'pending',
|
Pending = 'pending',
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export class PaymentListeners {
|
|||||||
Cash: 'نقدی',
|
Cash: 'نقدی',
|
||||||
Wallet: 'کیف پول',
|
Wallet: 'کیف پول',
|
||||||
Online: 'آنلاین',
|
Online: 'آنلاین',
|
||||||
|
CreditCard: 'کارت به کارت',
|
||||||
};
|
};
|
||||||
return methodMap[method] || method;
|
return methodMap[method] || method;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export class PaymentsService {
|
|||||||
case PaymentMethodEnum.Online:
|
case PaymentMethodEnum.Online:
|
||||||
return this.handleOnlinePayment(ctx);
|
return this.handleOnlinePayment(ctx);
|
||||||
|
|
||||||
|
case PaymentMethodEnum.CreditCard:
|
||||||
|
await this.handleCreditCardPayment(ctx);
|
||||||
|
return { paymentUrl: null };
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD);
|
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.restaurant?.domain) throw new BadRequestException(PaymentMessage.RESTAURANT_DOMAIN_REQUIRED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pm.method === PaymentMethodEnum.CreditCard && !pm.cardNumber) {
|
||||||
|
throw new BadRequestException(PaymentMessage.CARD_NUMBER_REQUIRED);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
order,
|
order,
|
||||||
amount: order.total,
|
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
|
// TODO clean this code and refactor it
|
||||||
private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> {
|
private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||||
await this.em.transactional(async em => {
|
await this.em.transactional(async em => {
|
||||||
@@ -242,13 +258,25 @@ export class PaymentsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verifyCashPayment(id: string): Promise<Payment> {
|
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 this.em.transactional(async em => {
|
||||||
const payment = await em.findOne(Payment, id, { populate: ['order'] });
|
const payment = await em.findOne(Payment, id, { populate: ['order'] });
|
||||||
if (!payment) {
|
if (!payment) {
|
||||||
throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (payment.method !== PaymentMethodEnum.Cash) {
|
if (payment.method !== expectedMethod) {
|
||||||
throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH);
|
throw new BadRequestException(wrongMethodMessage);
|
||||||
}
|
}
|
||||||
if (payment.status === PaymentStatusEnum.Paid) {
|
if (payment.status === PaymentStatusEnum.Paid) {
|
||||||
throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID);
|
throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID);
|
||||||
|
|||||||
@@ -33,4 +33,12 @@ export const paymentMethodsData: PaymentMethodData[] = [
|
|||||||
order: 3,
|
order: 3,
|
||||||
gateway: null,
|
gateway: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: PaymentMethodEnum.CreditCard,
|
||||||
|
description: 'پرداخت کارت به کارت',
|
||||||
|
enabled: true,
|
||||||
|
order: 4,
|
||||||
|
gateway: null,
|
||||||
|
cardNumber: '6037991234567890',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user