diff --git a/src/modules/orders/orders.service.ts b/src/modules/orders/orders.service.ts index 5933664..5e14b5e 100644 --- a/src/modules/orders/orders.service.ts +++ b/src/modules/orders/orders.service.ts @@ -12,15 +12,17 @@ import { OrderStatus } from './interface/order-status'; import { PaymentStatus } from '../payments/interface/payment-status'; import { Cart } from '../cart/interfaces/cart.interface'; 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() export class OrdersService { constructor( private readonly em: EntityManager, private readonly cartService: CartService, + private readonly paymentsService: PaymentsService, private readonly RestaurantPaymentMethodRepository: RestaurantPaymentMethodRepository, - private readonly paymentGatewayService: PaymentGatewayService, + // private readonly paymentGatewayService: PaymentGatewayService, ) {} async create(userId: string, restaurantId: string) { @@ -76,24 +78,25 @@ export class OrdersService { // Flush all changes await em.flush(); - + let paymentUrl: string | null = null; // Check if payment method is online and redirect to payment gateway 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); // Return order with redirect URL for online payment - return { - order, - redirectUrl: paymentRedirect.redirectUrl, - }; + paymentUrl = result.paymentUrl; } else { // await this.cartService.clearCart(userId, restaurantId); // For offline payment methods (cash on delivery, etc.), order is created and pending // Payment status will be updated when payment is confirmed // TODO: Implement offline payment confirmation flow if needed } - return { order }; + return { order, paymentUrl }; }); } diff --git a/src/modules/payments/controllers/payments.controller.ts b/src/modules/payments/controllers/payments.controller.ts index 45d0852..6a528b3 100644 --- a/src/modules/payments/controllers/payments.controller.ts +++ b/src/modules/payments/controllers/payments.controller.ts @@ -38,15 +38,15 @@ export class PaymentsController { return this.restaurantPaymentMethodService.findByRestaurant(restId); } - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Post('admin/payments') - @ApiOperation({ summary: 'Create a new payment' }) - @ApiBody({ type: CreatePaymentDto }) - @ApiCreatedResponse({ description: 'Payment created successfully' }) - createPayment(@Body() createPaymentDto: CreatePaymentDto) { - return this.paymentsService.create(createPaymentDto); - } + // @UseGuards(AdminAuthGuard) + // @ApiBearerAuth() + // @Post('admin/payments') + // @ApiOperation({ summary: 'Create a new payment' }) + // @ApiBody({ type: CreatePaymentDto }) + // @ApiCreatedResponse({ description: 'Payment created successfully' }) + // createPayment(@Body() createPaymentDto: CreatePaymentDto) { + // return this.paymentsService.create(createPaymentDto); + // } @UseGuards(AdminAuthGuard) @ApiBearerAuth() @@ -56,16 +56,16 @@ export class PaymentsController { findAllPayments() { return this.paymentsService.findAll(); } - @UseGuards(AdminAuthGuard) - @ApiBearerAuth() - @Get('admin/payments/:id') - @ApiOperation({ summary: 'Get a payment by ID' }) - @ApiParam({ name: 'id', type: 'number', description: 'Payment ID' }) - @ApiOkResponse({ description: 'Payment found' }) - @ApiNotFoundResponse({ description: 'Payment not found' }) - findOnePayment(@Param('id') id: string) { - return this.paymentsService.findOne(+id); - } + // @UseGuards(AdminAuthGuard) + // @ApiBearerAuth() + // @Get('admin/payments/:id') + // @ApiOperation({ summary: 'Get a payment by ID' }) + // @ApiParam({ name: 'id', type: 'number', description: 'Payment ID' }) + // @ApiOkResponse({ description: 'Payment found' }) + // @ApiNotFoundResponse({ description: 'Payment not found' }) + // findOnePayment(@Param('id') id: string) { + // return this.paymentsService.findById(+id); + // } @UseGuards(AdminAuthGuard) @ApiBearerAuth() @Patch('admin/payments/:id') diff --git a/src/modules/payments/dto/create-payment-method.dto.ts b/src/modules/payments/dto/create-payment-method.dto.ts index 6463cce..c6c7fc2 100644 --- a/src/modules/payments/dto/create-payment-method.dto.ts +++ b/src/modules/payments/dto/create-payment-method.dto.ts @@ -38,5 +38,5 @@ export class CreatePaymentMethodDto { @ApiProperty({ description: 'Callback URL for payment method', required: false }) @IsOptional() @IsString() - callbackUrl?: string; + paymentUrl?: string; } diff --git a/src/modules/payments/dto/create-payment.dto.ts b/src/modules/payments/dto/create-payment.dto.ts index 0d38018..d52f73c 100644 --- a/src/modules/payments/dto/create-payment.dto.ts +++ b/src/modules/payments/dto/create-payment.dto.ts @@ -1,9 +1,16 @@ -import { IsNumber } from 'class-validator'; +import { IsNumber, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class CreatePaymentDto { - @ApiProperty({ description: 'Example field (placeholder)' }) + @ApiProperty({ description: 'Payment amount' }) @IsNumber() - exampleField: number; -} + amount: number; + @ApiProperty({ description: 'Payment authority' }) + @IsString() + restaurantPaymentId: string; + + @ApiProperty({ description: 'Payment gateway' }) + @IsString() + gateway: string; +} diff --git a/src/modules/payments/entities/payment-method.entity.ts b/src/modules/payments/entities/payment-method.entity.ts index 40cde74..bab1daf 100644 --- a/src/modules/payments/entities/payment-method.entity.ts +++ b/src/modules/payments/entities/payment-method.entity.ts @@ -24,6 +24,7 @@ export class PaymentMethod extends BaseEntity { @Property({ type: 'int', nullable: true }) order?: number; + @Property({ nullable: true }) - callbackUrl?: string; + paymentUrl?: string; } diff --git a/src/modules/payments/entities/payment.entity.ts b/src/modules/payments/entities/payment.entity.ts index 4b89f06..7a579ee 100644 --- a/src/modules/payments/entities/payment.entity.ts +++ b/src/modules/payments/entities/payment.entity.ts @@ -1,3 +1,37 @@ -export class Payment { - exampleField: number; +import { Entity, ManyToOne, Property, Enum } from '@mikro-orm/core'; +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; + + @Property({ nullable: true }) + paidAt: Date; + + @Property({ nullable: true }) + failedAt: Date; } diff --git a/src/modules/payments/entities/restaurant-payment-method.entity.ts b/src/modules/payments/entities/restaurant-payment-method.entity.ts index e226fd5..aeed169 100644 --- a/src/modules/payments/entities/restaurant-payment-method.entity.ts +++ b/src/modules/payments/entities/restaurant-payment-method.entity.ts @@ -16,6 +16,9 @@ export class RestaurantPaymentMethod extends BaseEntity { @Property({ nullable: true }) merchantId?: string; + @Property({ nullable: true }) + callbackUrl?: string; + @Property({ type: 'boolean', default: true }) isActive: boolean = true; } diff --git a/src/modules/payments/interface/payment-status.ts b/src/modules/payments/interface/payment-status.ts index 37e47b7..3841bd5 100644 --- a/src/modules/payments/interface/payment-status.ts +++ b/src/modules/payments/interface/payment-status.ts @@ -4,3 +4,17 @@ export enum PaymentStatus { 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; +} diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index 3433051..b082a3f 100644 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -7,7 +7,7 @@ import { PaymentMethodService } from './services/payment-method.service'; import { RestaurantPaymentMethod } from './entities/restaurant-payment-method.entity'; import { RestaurantPaymentMethodRepository } from './repositories/restaurant-payment-method.repository'; 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 { PaymentsController } from './controllers/payments.controller'; import { AuthModule } from '../auth/auth.module'; @@ -22,7 +22,7 @@ import { JwtModule } from '@nestjs/jwt'; PaymentMethodRepository, RestaurantPaymentMethodService, RestaurantPaymentMethodRepository, - PaymentGatewayService, + // PaymentGatewayService, ], exports: [ PaymentMethodRepository, @@ -30,7 +30,7 @@ import { JwtModule } from '@nestjs/jwt'; RestaurantPaymentMethodRepository, RestaurantPaymentMethodService, PaymentsService, - PaymentGatewayService, + // PaymentGatewayService, ], }) export class PaymentsModule {} diff --git a/src/modules/payments/services/payment-gateway.service.ts b/src/modules/payments/services/payment-gateway.service.tss similarity index 100% rename from src/modules/payments/services/payment-gateway.service.ts rename to src/modules/payments/services/payment-gateway.service.tss diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index 44054fb..c3f8491 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -1,23 +1,111 @@ -import { Injectable } from '@nestjs/common'; -import { CreatePaymentDto } from '../dto/create-payment.dto'; -import { UpdatePaymentDto } from '../dto/update-payment.dto'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { RestaurantPaymentMethod } from '../entities/restaurant-payment-method.entity'; +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() export class PaymentsService { - create(createPaymentDto: CreatePaymentDto) { - return 'This action adds a new payment'; + constructor(private readonly em: EntityManager) {} + + 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); + + 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 { + const response = await axios.post(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() { - return `This action returns all payments`; - } - - findOne(id: number) { - return `This action returns a #${id} payment`; - } - - update(id: number, updatePaymentDto: UpdatePaymentDto) { - return `This action updates a #${id} payment`; + return this.em.find(Payment, {}); } remove(id: number) { diff --git a/src/seeders/DatabaseSeeder.ts b/src/seeders/DatabaseSeeder.ts index 70be999..e23e051 100644 --- a/src/seeders/DatabaseSeeder.ts +++ b/src/seeders/DatabaseSeeder.ts @@ -94,18 +94,57 @@ export class DatabaseSeeder extends Seeder { const superAdmin = await em.findOne(Role, { name: 'superAdmin' }); // 3. Create Payment Methods - const zarinpalPaymentMethod = await em.findOne(PaymentMethod, { keyName: 'zarinpal' }); - if (!zarinpalPaymentMethod) { - const paymentMethod = em.create(PaymentMethod, { + const paymentMethodsData = [ + { + name: 'پرداخت در محل', + keyName: 'payment-on-delivery', + description: 'پرداخت در محل تحویل', + isActive: true, + order: 1, + isOnline: false, + }, + { + name: 'نقدی', + keyName: 'cash', + description: 'پرداخت نقدی', + isActive: true, + order: 2, + isOnline: false, + }, + { name: 'زرین‌پال', keyName: 'zarinpal', description: 'درگاه پرداخت زرین‌پال', isActive: true, - order: 1, + order: 3, isOnline: true, - callbackUrl: 'https://www.zarinpal.com/pg/StartPay', - }); - em.persist(paymentMethod); + 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); + } 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(); @@ -211,7 +250,7 @@ export class DatabaseSeeder extends Seeder { const boote = await em.findOne(Restaurant, { slug: 'boote' }); // 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 }); for (const restaurant of allRestaurants) {