update payment

This commit is contained in:
2025-12-02 11:11:36 +03:30
parent 1165167619
commit 2e9a1dc7dc
12 changed files with 250 additions and 61 deletions
+12 -9
View File
@@ -12,15 +12,17 @@ import { OrderStatus } from './interface/order-status';
import { PaymentStatus } from '../payments/interface/payment-status'; import { PaymentStatus } from '../payments/interface/payment-status';
import { Cart } from '../cart/interfaces/cart.interface'; import { Cart } from '../cart/interfaces/cart.interface';
import { RestaurantPaymentMethodRepository } from '../payments/repositories/restaurant-payment-method.repository'; import { RestaurantPaymentMethodRepository } from '../payments/repositories/restaurant-payment-method.repository';
import { PaymentGatewayService } from '../payments/services/payment-gateway.service'; // import { PaymentGatewayService } from '../payments/services/payment-gateway.service.tss';
import { PaymentsService } from '../payments/services/payments.service';
@Injectable() @Injectable()
export class OrdersService { export class OrdersService {
constructor( constructor(
private readonly em: EntityManager, private readonly em: EntityManager,
private readonly cartService: CartService, private readonly cartService: CartService,
private readonly paymentsService: PaymentsService,
private readonly RestaurantPaymentMethodRepository: RestaurantPaymentMethodRepository, private readonly RestaurantPaymentMethodRepository: RestaurantPaymentMethodRepository,
private readonly paymentGatewayService: PaymentGatewayService, // private readonly paymentGatewayService: PaymentGatewayService,
) {} ) {}
async create(userId: string, restaurantId: string) { async create(userId: string, restaurantId: string) {
@@ -76,24 +78,25 @@ export class OrdersService {
// Flush all changes // Flush all changes
await em.flush(); await em.flush();
let paymentUrl: string | null = null;
// Check if payment method is online and redirect to payment gateway // Check if payment method is online and redirect to payment gateway
if (validationResult.paymentMethod.paymentMethod.isOnline) { if (validationResult.paymentMethod.paymentMethod.isOnline) {
const paymentRedirect = this.paymentGatewayService.generateRedirectUrl(order, validationResult.paymentMethod); const result = await this.paymentsService.initializePayment(
validationResult.paymentMethod.id,
order.total,
order.id,
);
// await this.cartService.clearCart(userId, restaurantId); // await this.cartService.clearCart(userId, restaurantId);
// Return order with redirect URL for online payment // Return order with redirect URL for online payment
return { paymentUrl = result.paymentUrl;
order,
redirectUrl: paymentRedirect.redirectUrl,
};
} else { } else {
// await this.cartService.clearCart(userId, restaurantId); // await this.cartService.clearCart(userId, restaurantId);
// For offline payment methods (cash on delivery, etc.), order is created and pending // For offline payment methods (cash on delivery, etc.), order is created and pending
// Payment status will be updated when payment is confirmed // Payment status will be updated when payment is confirmed
// TODO: Implement offline payment confirmation flow if needed // TODO: Implement offline payment confirmation flow if needed
} }
return { order }; return { order, paymentUrl };
}); });
} }
@@ -38,15 +38,15 @@ export class PaymentsController {
return this.restaurantPaymentMethodService.findByRestaurant(restId); return this.restaurantPaymentMethodService.findByRestaurant(restId);
} }
@UseGuards(AdminAuthGuard) // @UseGuards(AdminAuthGuard)
@ApiBearerAuth() // @ApiBearerAuth()
@Post('admin/payments') // @Post('admin/payments')
@ApiOperation({ summary: 'Create a new payment' }) // @ApiOperation({ summary: 'Create a new payment' })
@ApiBody({ type: CreatePaymentDto }) // @ApiBody({ type: CreatePaymentDto })
@ApiCreatedResponse({ description: 'Payment created successfully' }) // @ApiCreatedResponse({ description: 'Payment created successfully' })
createPayment(@Body() createPaymentDto: CreatePaymentDto) { // createPayment(@Body() createPaymentDto: CreatePaymentDto) {
return this.paymentsService.create(createPaymentDto); // return this.paymentsService.create(createPaymentDto);
} // }
@UseGuards(AdminAuthGuard) @UseGuards(AdminAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@@ -56,16 +56,16 @@ export class PaymentsController {
findAllPayments() { findAllPayments() {
return this.paymentsService.findAll(); return this.paymentsService.findAll();
} }
@UseGuards(AdminAuthGuard) // @UseGuards(AdminAuthGuard)
@ApiBearerAuth() // @ApiBearerAuth()
@Get('admin/payments/:id') // @Get('admin/payments/:id')
@ApiOperation({ summary: 'Get a payment by ID' }) // @ApiOperation({ summary: 'Get a payment by ID' })
@ApiParam({ name: 'id', type: 'number', description: 'Payment ID' }) // @ApiParam({ name: 'id', type: 'number', description: 'Payment ID' })
@ApiOkResponse({ description: 'Payment found' }) // @ApiOkResponse({ description: 'Payment found' })
@ApiNotFoundResponse({ description: 'Payment not found' }) // @ApiNotFoundResponse({ description: 'Payment not found' })
findOnePayment(@Param('id') id: string) { // findOnePayment(@Param('id') id: string) {
return this.paymentsService.findOne(+id); // return this.paymentsService.findById(+id);
} // }
@UseGuards(AdminAuthGuard) @UseGuards(AdminAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@Patch('admin/payments/:id') @Patch('admin/payments/:id')
@@ -38,5 +38,5 @@ export class CreatePaymentMethodDto {
@ApiProperty({ description: 'Callback URL for payment method', required: false }) @ApiProperty({ description: 'Callback URL for payment method', required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
callbackUrl?: string; paymentUrl?: string;
} }
+11 -4
View File
@@ -1,9 +1,16 @@
import { IsNumber } from 'class-validator'; import { IsNumber, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class CreatePaymentDto { export class CreatePaymentDto {
@ApiProperty({ description: 'Example field (placeholder)' }) @ApiProperty({ description: 'Payment amount' })
@IsNumber() @IsNumber()
exampleField: number; amount: number;
}
@ApiProperty({ description: 'Payment authority' })
@IsString()
restaurantPaymentId: string;
@ApiProperty({ description: 'Payment gateway' })
@IsString()
gateway: string;
}
@@ -24,6 +24,7 @@ export class PaymentMethod extends BaseEntity {
@Property({ type: 'int', nullable: true }) @Property({ type: 'int', nullable: true })
order?: number; order?: number;
@Property({ nullable: true }) @Property({ nullable: true })
callbackUrl?: string; paymentUrl?: string;
} }
@@ -1,3 +1,37 @@
export class Payment { import { Entity, ManyToOne, Property, Enum } from '@mikro-orm/core';
exampleField: number; import { BaseEntity } from '../../../common/entities/base.entity';
import { Order } from '../../orders/entities/order.entity';
import { PaymentStatus } from '../interface/payment-status';
@Entity({ tableName: 'payments' })
export class Payment extends BaseEntity {
@ManyToOne(() => Order)
order!: Order;
@Property({ type: 'decimal', precision: 10, scale: 0 })
amount!: number;
@Property({ unique: true })
authority!: string;
@Property()
gateway!: string;
@Property({ nullable: true })
refId?: string;
@Enum(() => PaymentStatus)
status!: PaymentStatus;
@Property({ nullable: true })
cardPan?: string;
@Property({ type: 'json' })
verifyResponse?: Record<string, any>;
@Property({ nullable: true })
paidAt: Date;
@Property({ nullable: true })
failedAt: Date;
} }
@@ -16,6 +16,9 @@ export class RestaurantPaymentMethod extends BaseEntity {
@Property({ nullable: true }) @Property({ nullable: true })
merchantId?: string; merchantId?: string;
@Property({ nullable: true })
callbackUrl?: string;
@Property({ type: 'boolean', default: true }) @Property({ type: 'boolean', default: true })
isActive: boolean = true; isActive: boolean = true;
} }
@@ -4,3 +4,17 @@ export enum PaymentStatus {
Failed = 'failed', Failed = 'failed',
} }
export interface IPaymentRequest {
amount: number;
callbackUrl: string;
merchantId: string;
description: string;
}
export interface IPaymentResponse {
code: number;
message: string;
authority: string;
fee_type: string;
fee: number;
}
+3 -3
View File
@@ -7,7 +7,7 @@ import { PaymentMethodService } from './services/payment-method.service';
import { RestaurantPaymentMethod } from './entities/restaurant-payment-method.entity'; import { RestaurantPaymentMethod } from './entities/restaurant-payment-method.entity';
import { RestaurantPaymentMethodRepository } from './repositories/restaurant-payment-method.repository'; import { RestaurantPaymentMethodRepository } from './repositories/restaurant-payment-method.repository';
import { RestaurantPaymentMethodService } from './services/restaurant-payment-method.service'; import { RestaurantPaymentMethodService } from './services/restaurant-payment-method.service';
import { PaymentGatewayService } from './services/payment-gateway.service'; // import { PaymentGatewayService } from './services/payment-gateway.service.tss';
import { Restaurant } from '../restaurants/entities/restaurant.entity'; import { Restaurant } from '../restaurants/entities/restaurant.entity';
import { PaymentsController } from './controllers/payments.controller'; import { PaymentsController } from './controllers/payments.controller';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
@@ -22,7 +22,7 @@ import { JwtModule } from '@nestjs/jwt';
PaymentMethodRepository, PaymentMethodRepository,
RestaurantPaymentMethodService, RestaurantPaymentMethodService,
RestaurantPaymentMethodRepository, RestaurantPaymentMethodRepository,
PaymentGatewayService, // PaymentGatewayService,
], ],
exports: [ exports: [
PaymentMethodRepository, PaymentMethodRepository,
@@ -30,7 +30,7 @@ import { JwtModule } from '@nestjs/jwt';
RestaurantPaymentMethodRepository, RestaurantPaymentMethodRepository,
RestaurantPaymentMethodService, RestaurantPaymentMethodService,
PaymentsService, PaymentsService,
PaymentGatewayService, // PaymentGatewayService,
], ],
}) })
export class PaymentsModule {} export class PaymentsModule {}
+102 -14
View File
@@ -1,23 +1,111 @@
import { Injectable } from '@nestjs/common'; import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { CreatePaymentDto } from '../dto/create-payment.dto'; import { RestaurantPaymentMethod } from '../entities/restaurant-payment-method.entity';
import { UpdatePaymentDto } from '../dto/update-payment.dto'; import axios from 'axios';
import { IPaymentRequest, IPaymentResponse, PaymentStatus } from '../interface/payment-status';
import { Payment } from '../entities/payment.entity';
import { EntityManager, RequiredEntityData } from '@mikro-orm/core';
import { Order } from '../../orders/entities/order.entity';
@Injectable() @Injectable()
export class PaymentsService { export class PaymentsService {
create(createPaymentDto: CreatePaymentDto) { constructor(private readonly em: EntityManager) {}
return 'This action adds a new payment';
async initializePayment(
restaurantPaymentMethodId: string,
amount: number,
orderId: string,
): Promise<{ paymentUrl: string }> {
// Validate amount
if (amount <= 0) {
throw new BadRequestException('Amount must be greater than zero');
}
// Validate order exists
const order = await this.em.findOne(Order, { id: orderId });
if (!order) {
throw new NotFoundException('Order not found');
}
// Load restaurant payment method with payment method relationship
const restaurantPaymentMethod = await this.em.findOne(
RestaurantPaymentMethod,
{ id: restaurantPaymentMethodId },
{ populate: ['paymentMethod'] },
);
if (!restaurantPaymentMethod) {
throw new NotFoundException('Restaurant payment method not found');
}
if (!restaurantPaymentMethod.paymentMethod?.isOnline) {
throw new BadRequestException('Payment method is not online');
}
if (!restaurantPaymentMethod.callbackUrl) {
throw new BadRequestException('Callback URL is not configured');
}
if (!restaurantPaymentMethod.merchantId) {
throw new BadRequestException('Merchant ID is not provided');
}
if (!restaurantPaymentMethod.paymentMethod?.paymentUrl) {
throw new BadRequestException('Payment URL is not configured');
}
// Request to payment gateway with error handling
let gatewayResponse: IPaymentResponse;
try {
gatewayResponse = await this.requestToPaymentGateway(restaurantPaymentMethod.paymentMethod.paymentUrl, {
amount,
callbackUrl: restaurantPaymentMethod.callbackUrl,
merchantId: restaurantPaymentMethod.merchantId,
description: `Payment for order #${orderId}`,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to connect to payment gateway: ${errorMessage}`);
}
// Check gateway response code (typically 100 means success for Iranian gateways)
if (gatewayResponse.code !== 100 && gatewayResponse.code !== 0) {
throw new BadRequestException(`Payment gateway error: ${gatewayResponse.message || 'Unknown error'}`);
}
if (!gatewayResponse.authority) {
throw new BadRequestException('Payment gateway did not return an authority token');
}
// Create payment record and save authority
const payment = this.em.create(Payment, {
amount,
authority: gatewayResponse.authority,
order: this.em.getReference(Order, orderId),
gateway: restaurantPaymentMethod.paymentMethod.name,
status: PaymentStatus.Pending,
} as RequiredEntityData<Payment>);
await this.em.persistAndFlush(payment);
// Return payment URL
const paymentUrl = `${restaurantPaymentMethod.paymentMethod.paymentUrl}/${gatewayResponse.authority}`;
return { paymentUrl };
}
private async requestToPaymentGateway(
gatewayPaymentUrl: string,
requestPayment: IPaymentRequest,
): Promise<IPaymentResponse> {
const response = await axios.post<IPaymentResponse>(gatewayPaymentUrl, requestPayment);
return response.data;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
update(_id: number, _updatePaymentDto: unknown) {
return `This action updates a #${_id} payment`;
} }
findAll() { findAll() {
return `This action returns all payments`; return this.em.find(Payment, {});
}
findOne(id: number) {
return `This action returns a #${id} payment`;
}
update(id: number, updatePaymentDto: UpdatePaymentDto) {
return `This action updates a #${id} payment`;
} }
remove(id: number) { remove(id: number) {
+46 -7
View File
@@ -94,18 +94,57 @@ export class DatabaseSeeder extends Seeder {
const superAdmin = await em.findOne(Role, { name: 'superAdmin' }); const superAdmin = await em.findOne(Role, { name: 'superAdmin' });
// 3. Create Payment Methods // 3. Create Payment Methods
const zarinpalPaymentMethod = await em.findOne(PaymentMethod, { keyName: 'zarinpal' }); const paymentMethodsData = [
if (!zarinpalPaymentMethod) { {
const paymentMethod = em.create(PaymentMethod, { name: 'پرداخت در محل',
keyName: 'payment-on-delivery',
description: 'پرداخت در محل تحویل',
isActive: true,
order: 1,
isOnline: false,
},
{
name: 'نقدی',
keyName: 'cash',
description: 'پرداخت نقدی',
isActive: true,
order: 2,
isOnline: false,
},
{
name: 'زرین‌پال', name: 'زرین‌پال',
keyName: 'zarinpal', keyName: 'zarinpal',
description: 'درگاه پرداخت زرین‌پال', description: 'درگاه پرداخت زرین‌پال',
isActive: true, isActive: true,
order: 1, order: 3,
isOnline: true, isOnline: true,
callbackUrl: 'https://www.zarinpal.com/pg/StartPay', paymentUrl: 'https://www.zarinpal.com/pg/StartPay',
}); },
];
for (const methodData of paymentMethodsData) {
let paymentMethod = await em.findOne(PaymentMethod, { keyName: methodData.keyName });
if (!paymentMethod) {
paymentMethod = em.create(PaymentMethod, methodData);
em.persist(paymentMethod); em.persist(paymentMethod);
} else {
// Update existing payment method if needed
if (
paymentMethod.name !== methodData.name ||
paymentMethod.description !== methodData.description ||
paymentMethod.isOnline !== methodData.isOnline ||
paymentMethod.order !== methodData.order
) {
paymentMethod.name = methodData.name;
paymentMethod.description = methodData.description;
paymentMethod.isOnline = methodData.isOnline;
paymentMethod.order = methodData.order;
if (methodData.paymentUrl) {
paymentMethod.paymentUrl = methodData.paymentUrl;
}
em.persist(paymentMethod);
}
}
} }
await em.flush(); await em.flush();
@@ -211,7 +250,7 @@ export class DatabaseSeeder extends Seeder {
const boote = await em.findOne(Restaurant, { slug: 'boote' }); const boote = await em.findOne(Restaurant, { slug: 'boote' });
// 5.5. Create Restaurant Payment Methods // 5.5. Create Restaurant Payment Methods
const allRestaurants = [zhivan, boote].filter(Boolean) as Restaurant[]; const allRestaurants = await em.find(Restaurant, {});
const allPaymentMethods = await em.find(PaymentMethod, { isActive: true }); const allPaymentMethods = await em.find(PaymentMethod, { isActive: true });
for (const restaurant of allRestaurants) { for (const restaurant of allRestaurants) {