diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index d97fe22..d0cd741 100644 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -4,7 +4,7 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { PaymentMethod } from './entities/payment-method.entity'; import { PaymentMethodRepository } from './repositories/payment-method.repository'; import { PaymentMethodService } from './services/payment-method.service'; -// import { PaymentGatewayService } from './services/payment-gateway.service.tss'; +import { PaymentGatewayService } from './services/payment-gateway.service'; import { Restaurant } from '../restaurants/entities/restaurant.entity'; import { PaymentsController } from './controllers/payments.controller'; import { AuthModule } from '../auth/auth.module'; @@ -13,12 +13,7 @@ import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [MikroOrmModule.forFeature([PaymentMethod, Restaurant]), AuthModule, JwtModule], controllers: [PaymentsController], - providers: [ - PaymentsService, - PaymentMethodService, - PaymentMethodRepository, - // PaymentGatewayService, - ], + providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, PaymentGatewayService], exports: [ PaymentMethodRepository, PaymentMethodService, diff --git a/src/modules/payments/services/payment-gateway.service.ts b/src/modules/payments/services/payment-gateway.service.ts new file mode 100644 index 0000000..2cad649 --- /dev/null +++ b/src/modules/payments/services/payment-gateway.service.ts @@ -0,0 +1,115 @@ +import { EntityManager } from '@mikro-orm/postgresql'; +import { Injectable, BadRequestException, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + IPaymentRequest, + IPaymentResponse, + IPaymentVerifyRequest, + IPaymentVerifyResponse, + PaymentGatewayEnum, + PaymentMethodEnum, +} from '../interface/payment'; +import axios from 'axios'; + +@Injectable() +export class PaymentGatewayService { + private readonly logger = new Logger(PaymentGatewayService.name); + private readonly zarinpalRequestUrl: string; + private readonly zarinpalPaymentBaseUrl: string; + private readonly zarinpalVerifyUrl: string; + + constructor( + private readonly em: EntityManager, + private readonly configService: ConfigService, + ) { + // Get Zarinpal base URL from environment or default to sandbox for development + const zarinpalBaseUrl = this.configService.get('ZARINPAL_BASE_URL') || 'https://sandbox.zarinpal.com'; + + this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`; + this.zarinpalPaymentBaseUrl = `${zarinpalBaseUrl}/pg/StartPay`; + this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`; + } + async requestToGateway( + paymentMethod: PaymentMethodEnum, + amount: number, + orderId: string, + merchantId: string | null | undefined, + domain: string, + gateway: PaymentGatewayEnum | null | undefined, + ): Promise<{ authority: string | null }> { + // For non-online payment methods, no gateway request is needed + if (paymentMethod !== PaymentMethodEnum.Online) { + return { authority: null }; + } + + // Online payments require merchantId and gateway + if (!merchantId) { + throw new BadRequestException('Merchant ID is required for online payments'); + } + + if (!gateway) { + throw new BadRequestException('Payment gateway is required for online payments'); + } + + // Handle ZarinPal gateway + if (gateway === PaymentGatewayEnum.ZarinPal) { + const callbackUrl = `${domain}/payments/${orderId}/callback`; + try { + const gatewayResponse = await this.requestToZarinPalGateway({ + amount, + merchantId, + description: `Payment for order #${orderId}`, + callbackUrl, + metadata: { + orderId, + }, + }); + + // 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'); + } + + return { authority: gatewayResponse.authority }; + } catch (error) { + if (error instanceof BadRequestException) { + throw error; + } + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new BadRequestException(`Failed to connect to payment gateway: ${errorMessage}`); + } + } + + // If gateway is not supported, throw an error + throw new BadRequestException(`Unsupported payment gateway: ${String(gateway)}`); + } + + private async requestToZarinPalGateway(requestPayment: IPaymentRequest): Promise { + const response = await axios.post(this.zarinpalRequestUrl, requestPayment); + return response.data; + } + + zarinpalPaymentUrl(gateway: PaymentGatewayEnum | null, authority: string | null) { + if (gateway === PaymentGatewayEnum.ZarinPal) { + return `${this.zarinpalPaymentBaseUrl}/${authority}`; + } + return null; + } + + async verifyZarinPalPayment(verifyRequest: IPaymentVerifyRequest): Promise { + try { + const response = await axios.post(this.zarinpalVerifyUrl, verifyRequest); + if (!response.data) { + throw new BadRequestException('Empty response from payment gateway'); + } + return response.data; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new BadRequestException(`Failed to verify payment with gateway: ${errorMessage}`); + } + } +} diff --git a/src/modules/payments/services/payment-gateway.service.tss b/src/modules/payments/services/payment-gateway.service.tss deleted file mode 100644 index b8cf8c3..0000000 --- a/src/modules/payments/services/payment-gateway.service.tss +++ /dev/null @@ -1,161 +0,0 @@ -import { Injectable, BadRequestException, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { RestaurantPaymentMethod } from '../entities/restaurant-payment-method.entity'; -import { Order } from '../../orders/entities/order.entity'; - -export interface PaymentGatewayRedirect { - redirectUrl: string; - orderId: string; -} - -@Injectable() -export class PaymentGatewayService { - private readonly logger = new Logger(PaymentGatewayService.name); - - constructor(private readonly configService: ConfigService) {} - - /** - * Generate payment gateway redirect URL for online payment methods - */ - generateRedirectUrl(order: Order, restaurantPaymentMethod: RestaurantPaymentMethod): PaymentGatewayRedirect { - const paymentMethod = restaurantPaymentMethod.paymentMethod; - - if (!paymentMethod.isOnline) { - throw new BadRequestException('Payment method is not online'); - } - - // Get base URL for callback - const baseUrl = this.configService.get('APP_BASE_URL') || 'http://localhost:4000'; - const callbackUrl = paymentMethod.callbackUrl || `${baseUrl}/public/payments/callback`; - - // Generate payment gateway URL based on payment method keyName - const redirectUrl = this.generateGatewayUrl(paymentMethod.keyName, { - orderId: order.id, - amount: order.total, - merchantId: restaurantPaymentMethod.merchantId, - callbackUrl, - description: `Order #${order.id}`, - }); - - return { - redirectUrl, - orderId: order.id, - }; - } - - /** - * Generate gateway URL based on payment provider - * This can be extended to support multiple payment gateways (ZarinPal, Mellat, etc.) - */ - private generateGatewayUrl( - paymentMethodKey: string, - params: { - orderId: string; - amount: number; - merchantId?: string; - callbackUrl: string; - description: string; - }, - ): string { - // Convert amount to smallest currency unit (e.g., Toman to Rials for Iranian gateways) - const amountInSmallestUnit = Math.round(params.amount * 10); // Assuming Toman to Rials conversion - - switch (paymentMethodKey.toLowerCase()) { - case 'zarinpal': - return this.generateZarinPalUrl(params, amountInSmallestUnit); - - case 'mellat': - case 'bankmellat': - return this.generateMellatUrl(params, amountInSmallestUnit); - - default: - // Generic payment gateway URL structure - // You can customize this based on your payment gateway provider - this.logger.warn(`Unknown payment method: ${paymentMethodKey}, using generic URL structure`); - return this.generateGenericGatewayUrl(params, amountInSmallestUnit); - } - } - - /** - * Generate ZarinPal payment gateway URL - */ - private generateZarinPalUrl( - params: { - orderId: string; - merchantId?: string; - callbackUrl: string; - description: string; - }, - amount: number, - ): string { - if (!params.merchantId) { - throw new BadRequestException('Merchant ID is required for ZarinPal payment gateway'); - } - - // ZarinPal payment gateway URL structure - // This is a simplified version - you may need to make an API call to get the actual redirect URL - const zarinPalBaseUrl = - this.configService.get('ZARINPAL_BASE_URL') || 'https://www.zarinpal.com/pg/StartPay'; - - // For ZarinPal, you typically need to: - // 1. Call their API to create a payment request - // 2. Get an Authority token - // 3. Redirect to their payment page with the Authority - - // For now, returning a placeholder URL structure - // TODO: Implement actual ZarinPal API integration - return `${zarinPalBaseUrl}/${params.orderId}?Amount=${amount}&MerchantID=${params.merchantId}&CallbackURL=${encodeURIComponent(params.callbackUrl)}&Description=${encodeURIComponent(params.description)}`; - } - - /** - * Generate Bank Mellat payment gateway URL - */ - private generateMellatUrl( - params: { - orderId: string; - merchantId?: string; - callbackUrl: string; - description: string; - }, - amount: number, - ): string { - if (!params.merchantId) { - throw new BadRequestException('Merchant ID is required for Mellat payment gateway'); - } - - const mellatBaseUrl = - this.configService.get('MELLAT_BASE_URL') || 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat'; - - // TODO: Implement actual Mellat API integration - return `${mellatBaseUrl}?orderId=${params.orderId}&amount=${amount}&merchantId=${params.merchantId}&callbackUrl=${encodeURIComponent(params.callbackUrl)}`; - } - - /** - * Generate generic payment gateway URL - */ - private generateGenericGatewayUrl( - params: { - orderId: string; - merchantId?: string; - callbackUrl: string; - description: string; - }, - amount: number, - ): string { - const genericBaseUrl = - this.configService.get('PAYMENT_GATEWAY_BASE_URL') || 'https://payment.example.com/pay'; - - const queryParams = new URLSearchParams({ - orderId: params.orderId, - amount: amount.toString(), - callbackUrl: params.callbackUrl, - description: params.description, - }); - - if (params.merchantId) { - queryParams.append('merchantId', params.merchantId); - } - - return `${genericBaseUrl}?${queryParams.toString()}`; - } -} diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index f978a91..815ba76 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -1,14 +1,6 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import axios from 'axios'; -import { - IPaymentRequest, - IPaymentResponse, - IPaymentVerifyRequest, - IPaymentVerifyResponse, - PaymentGatewayEnum, - PaymentMethodEnum, - PaymentStatusEnum, -} from '../interface/payment'; +import { PaymentGatewayService } from './payment-gateway.service'; +import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; import { Payment } from '../entities/payment.entity'; import { EntityManager, RequiredEntityData } from '@mikro-orm/core'; import { Order } from '../../orders/entities/order.entity'; @@ -17,7 +9,10 @@ import { CreatePaymentDto } from '../dto/create-payment.dto'; @Injectable() export class PaymentsService { - constructor(private readonly em: EntityManager) {} + constructor( + private readonly em: EntityManager, + private readonly paymentGatewayService: PaymentGatewayService, + ) {} async initializePayment( paymentMethodId: string, @@ -56,7 +51,7 @@ export class PaymentsService { gateway, }); - const paymentUrl = this.zarinpalPaymentUrl(gateway, authority); + const paymentUrl = this.paymentGatewayService.zarinpalPaymentUrl(gateway, authority); return { paymentUrl }; } @@ -64,7 +59,14 @@ export class PaymentsService { async createPayment(domain: string, dto: CreatePaymentDto) { const { amount, orderId, merchantId, gateway, paymentMethod } = dto; - const { authority } = await this.requestToGateway(paymentMethod, amount, orderId, merchantId, domain, gateway); + const { authority } = await this.paymentGatewayService.requestToGateway( + paymentMethod, + amount, + orderId, + merchantId, + domain, + gateway, + ); const payment = this.em.create(Payment, { amount, @@ -78,78 +80,6 @@ export class PaymentsService { return { authority, payment }; } - async requestToGateway( - paymentMethod: PaymentMethodEnum, - amount: number, - orderId: string, - merchantId: string | null | undefined, - domain: string, - gateway: PaymentGatewayEnum | null | undefined, - ): Promise<{ authority: string | null }> { - // For non-online payment methods, no gateway request is needed - if (paymentMethod !== PaymentMethodEnum.Online) { - return { authority: null }; - } - - // Online payments require merchantId and gateway - if (!merchantId) { - throw new BadRequestException('Merchant ID is required for online payments'); - } - - if (!gateway) { - throw new BadRequestException('Payment gateway is required for online payments'); - } - - // Handle ZarinPal gateway - if (gateway === PaymentGatewayEnum.ZarinPal) { - const callbackUrl = `${domain}/payments/${PaymentGatewayEnum.ZarinPal}/${orderId}/callback`; - try { - const gatewayResponse = await this.requestToZarinPalGateway({ - amount, - merchantId, - description: `Payment for order #${orderId}`, - callbackUrl, - metadata: { - orderId, - }, - }); - - // 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'); - } - - return { authority: gatewayResponse.authority }; - } catch (error) { - if (error instanceof BadRequestException) { - throw error; - } - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new BadRequestException(`Failed to connect to payment gateway: ${errorMessage}`); - } - } - - // If gateway is not supported, throw an error - throw new BadRequestException(`Unsupported payment gateway: ${String(gateway)}`); - } - - private async requestToZarinPalGateway(requestPayment: IPaymentRequest): Promise { - const gatewayPaymentUrl = 'https://payment.zarinpal.com/pg/v4/payment/request.json'; - const response = await axios.post(gatewayPaymentUrl, requestPayment); - return response.data; - } - - zarinpalPaymentUrl(gateway: PaymentGatewayEnum | null, authority: string | null) { - if (gateway === PaymentGatewayEnum.ZarinPal) { - return `https://payment.zarinpal.com/pg/StartPay/${authority}`; - } - return null; - } - async verifyPayment(authority: string, orderId: string): Promise { // Find payment by authority and orderId const payment = await this.em.findOne( @@ -199,7 +129,7 @@ export class PaymentsService { // Handle ZarinPal gateway verification if (payment.gateway === PaymentGatewayEnum.ZarinPal) { try { - const verifyResponse = await this.verifyZarinPalPayment({ + const verifyResponse = await this.paymentGatewayService.verifyZarinPalPayment({ merchantId: paymentMethod.merchantId, amount: verifyAmount, authority, @@ -250,29 +180,15 @@ export class PaymentsService { throw new BadRequestException(`Unsupported payment gateway: ${String(payment.gateway)}`); } - private async verifyZarinPalPayment(verifyRequest: IPaymentVerifyRequest): Promise { - const gatewayVerifyUrl = 'https://payment.zarinpal.com/pg/v4/payment/verify.json'; - try { - const response = await axios.post(gatewayVerifyUrl, verifyRequest); - if (!response.data) { - throw new BadRequestException('Empty response from payment gateway'); - } - return response.data; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new BadRequestException(`Failed to verify payment with gateway: ${errorMessage}`); - } - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - update(_id: number, _updatePaymentDto: unknown) { - return `This action updates a #${_id} payment`; - } + // update(_id: number, _updatePaymentDto: unknown) { + // return `This action updates a #${_id} payment`; + // } - findAll() { - return this.em.find(Payment, {}); - } + // findAll() { + // return this.em.find(Payment, {}); + // } - remove(id: number) { - return `This action removes a #${id} payment`; - } + // remove(id: number) { + // return `This action removes a #${id} payment`; + // } }