diff --git a/src/modules/payments/gateways/zarinpal.gateway.ts b/src/modules/payments/gateways/zarinpal.gateway.ts index aeb8fa1..a254f92 100755 --- a/src/modules/payments/gateways/zarinpal.gateway.ts +++ b/src/modules/payments/gateways/zarinpal.gateway.ts @@ -1,167 +1,257 @@ -import { HttpService } from "@nestjs/axios"; -import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; -import { AxiosError } from "axios"; -import { catchError, firstValueFrom, throwError } from "rxjs"; - -import { PaymentMessage } from "../../../common/enums/message.enum"; -import { IZarinpalConfig } from "../../../configs/zarinpal.config"; -import { ZARINPAL_CONFIG } from "../constants"; -import { GatewayEnum } from "../enums/gateway.enum"; +import { Injectable, Logger, BadRequestException } from '@nestjs/common'; +import axios from 'axios'; import { IPaymentGateway, IPaymentVerifyParams, + IProcessPaymentData, IProcessPaymentParams, - ZarinPalPGNewArgs, - ZarinPalPGNewRequestData, - ZarinPalPGVerifyData, -} from "../interfaces/IPayment"; + IVerifyPayment, +} from '../interface/gateway'; +import { ConfigService } from '@nestjs/config'; +import { PaymentGatewayEnum } from '../interface/payment'; @Injectable() export class ZarinpalGateway implements IPaymentGateway { - private readonly IPG_TYPE = "payment"; private readonly logger = new Logger(ZarinpalGateway.name); - private readonly gatewayApiUrl: string; - private readonly requestHeader: Record = { "Content-Type": "application/json", Accept: "application/json" }; + private readonly zarinpalRequestUrl: string; + private readonly zarinpalPaymentBaseUrl: string; + private readonly zarinpalVerifyUrl: string; - constructor( - @Inject(ZARINPAL_CONFIG) private readonly config: IZarinpalConfig, - private readonly httpService: HttpService, - ) { - this.gatewayApiUrl = `https://${this.IPG_TYPE}.zarinpal.com`; + constructor(private readonly configService: ConfigService) { + 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 processPayment(processParams: IProcessPaymentParams) { - try { - const purchaseData: ZarinPalPGNewArgs = { - merchant_id: this.config.merchantId, - amount: processParams.amount, - callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`, - description: processParams.description, - currency: "IRT", - metadata: { email: processParams.email, mobile: processParams.mobile }, - }; - - this.logger.log(`Processing payment request:`, { - merchant_id: this.config.merchantId, - amount: processParams.amount, - callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`, - description: processParams.description, - }); - - const { data } = await firstValueFrom( - this.httpService - .post(`${this.gatewayApiUrl}/pg/v4/payment/request.json`, purchaseData, { - headers: this.requestHeader, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error(`Payment request failed: ${err.message}`, err.stack); - return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT)); - }), - ), - ); - - // Check for errors in response - if (data.errors && data.errors.length > 0) { - this.logger.error(`Zarinpal payment request error:`, data.errors); - throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT); - } - - // Validate response data - if (!data.data?.authority) { - this.logger.error(`Invalid response from Zarinpal:`, data); - throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT); - } - - this.logger.log(`Payment request successful - Authority: ${data.data.authority}`); - - return { - redirectUrl: `${this.gatewayApiUrl}/pg/StartPay/${data.data.authority}`, - message: data.data.message, - reference: data.data.authority, - }; - } catch (error) { - this.logger.error(`Payment processing error:`, error); - throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT); - } - } - - async verifyPayment(verifyParams: IPaymentVerifyParams) { - const verifyData = { - merchant_id: this.config.merchantId, - amount: Number(verifyParams.amount), - authority: verifyParams.reference, + async processPayment(params: IProcessPaymentParams): Promise { + // Transform camelCase to snake_case for Zarinpal API v4 + const zarinpalRequest = { + amount: params.amount, + merchant_id: params.merchantId, + description: params.description, + callback_url: params.callbackUrl, + metadata: { + order_id: params.metadata.orderId, + }, }; - this.logger.log(`Verifying payment:`, { - merchant_id: this.config.merchantId, - amount: Number(verifyParams.amount), - authority: verifyParams.reference, - }); - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.gatewayApiUrl}/pg/v4/payment/verify.json`, verifyData, { - headers: this.requestHeader, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error(`Verification request failed: ${err.message}`, err.stack); + // Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] } + interface ZarinpalError { + message?: string; + code?: number; + } + interface ZarinpalResponse { + data: IProcessPaymentData; + errors: ZarinpalError[]; + } + const response = await axios.post(this.zarinpalRequestUrl, zarinpalRequest); - // If we have error response data from Zarinpal, extract it - if (err.response?.data) { - const errorData = err.response.data as ZarinPalPGVerifyData; - this.logger.error(`Zarinpal error response:`, errorData); - - // Return the Zarinpal error response if it has the expected structure - if (errorData.errors) { - return throwError(() => errorData); - } - } - - return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT)); - }), - ), - ); - - // Log the verification result - if (data.data) { - this.logger.log(`Verification response - Code: ${data.data.code}, RefID: ${data.data.ref_id || "N/A"}`); - } else if (data.errors && data.errors.length > 0) { - this.logger.warn(`Verification error - Code: ${data.errors[0].code}, Message: ${data.errors[0].message}`); + // Check if there are errors in the response + if (response.data.errors && response.data.errors.length > 0) { + const errorMessage = response.data.errors.map(err => err.message || JSON.stringify(err)).join(', '); + throw new BadRequestException(`Payment gateway error: ${errorMessage}`); } - // Return the complete Zarinpal response - let the service handle the business logic - return { - code: data.data?.code || data.errors?.[0]?.code || -1, - message: data.data?.message || data.errors?.[0]?.message || "Unknown error", - ref_id: data.data?.ref_id || 0, - card_hash: data.data?.card_hash || "", - card_pan: data.data?.card_pan || "", - fee_type: data.data?.fee_type || "", - fee: data.data?.fee || 0, - }; + // Return the nested data object + if (!response.data.data) { + throw new BadRequestException('Payment gateway returned invalid response structure'); + } + + return response.data.data; } catch (error) { - // If it's a Zarinpal error response, extract the error code and return it - if (error && typeof error === "object" && "errors" in error) { - const zarinpalError = error as ZarinPalPGVerifyData; - const firstError = zarinpalError.errors[0]; - this.logger.warn(`Zarinpal verification error - Code: ${firstError.code}, Message: ${firstError.message}`); - - return { - code: firstError.code, - message: firstError.message, - ref_id: 0, - card_hash: "", - card_pan: "", - fee_type: "", - fee: 0, - }; + // Log the actual API error response for debugging + if (axios.isAxiosError(error) && error.response) { + this.logger.error('Zarinpal API error response', { + status: error.response.status, + data: JSON.stringify(error.response.data), + request: zarinpalRequest, + }); } - - this.logger.error(`Payment verification error:`, error); - throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT); + throw error; } } + + zarinpalPaymentUrl(gateway: PaymentGatewayEnum | null, authority: string | null) { + if (gateway === PaymentGatewayEnum.ZarinPal && authority) { + return `${this.zarinpalPaymentBaseUrl}/${authority}`; + } + return null; + } + + async verifyPayment(verifyRequest: IPaymentVerifyParams): Promise { + try { + // Transform camelCase to snake_case for Zarinpal API v4 + const zarinpalVerifyRequest = { + merchant_id: verifyRequest.merchantId, + amount: verifyRequest.amount, + authority: verifyRequest.authority, + }; + + // Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] } + interface ZarinpalError { + message?: string; + code?: number; + } + interface ZarinpalVerifyResponse { + data: IVerifyPayment; + errors: ZarinpalError[]; + } + const response = await axios.post(this.zarinpalVerifyUrl, zarinpalVerifyRequest); + + // Check if there are errors in the response + if (response.data.errors && response.data.errors.length > 0) { + const errorMessage = response.data.errors.map(err => err.message || JSON.stringify(err)).join(', '); + throw new BadRequestException(`Payment gateway error: ${errorMessage}`); + } + + // Return the nested data object + if (!response.data.data) { + throw new BadRequestException('Payment gateway returned invalid response structure'); + } + + return response.data.data; + } catch (error) { + if (error instanceof BadRequestException) { + throw error; + } + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new BadRequestException(`Failed to verify payment with gateway: ${errorMessage}`); + } + } + // async processPayment(processParams: IProcessPaymentParams) { + // try { + // const purchaseData: ZarinPalPGNewArgs = { + // merchant_id: this.config.merchantId, + // amount: processParams.amount, + // callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`, + // description: processParams.description, + // currency: "IRT", + // metadata: { email: processParams.email, mobile: processParams.mobile }, + // }; + + // this.logger.log(`Processing payment request:`, { + // merchant_id: this.config.merchantId, + // amount: processParams.amount, + // callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`, + // description: processParams.description, + // }); + + // const { data } = await firstValueFrom( + // this.httpService + // .post(`${this.gatewayApiUrl}/pg/v4/payment/request.json`, purchaseData, { + // headers: this.requestHeader, + // }) + // .pipe( + // catchError((err: AxiosError) => { + // this.logger.error(`Payment request failed: ${err.message}`, err.stack); + // return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT)); + // }), + // ), + // ); + + // // Check for errors in response + // if (data.errors && data.errors.length > 0) { + // this.logger.error(`Zarinpal payment request error:`, data.errors); + // throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT); + // } + + // // Validate response data + // if (!data.data?.authority) { + // this.logger.error(`Invalid response from Zarinpal:`, data); + // throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT); + // } + + // this.logger.log(`Payment request successful - Authority: ${data.data.authority}`); + + // return { + // redirectUrl: `${this.gatewayApiUrl}/pg/StartPay/${data.data.authority}`, + // message: data.data.message, + // reference: data.data.authority, + // }; + // } catch (error) { + // this.logger.error(`Payment processing error:`, error); + // throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT); + // } + // } + + // async verifyPayment(verifyParams: IPaymentVerifyParams) { + // const verifyData = { + // merchant_id: this.config.merchantId, + // amount: Number(verifyParams.amount), + // authority: verifyParams.reference, + // }; + + // this.logger.log(`Verifying payment:`, { + // merchant_id: this.config.merchantId, + // amount: Number(verifyParams.amount), + // authority: verifyParams.reference, + // }); + + // try { + // const { data } = await firstValueFrom( + // this.httpService + // .post(`${this.gatewayApiUrl}/pg/v4/payment/verify.json`, verifyData, { + // headers: this.requestHeader, + // }) + // .pipe( + // catchError((err: AxiosError) => { + // this.logger.error(`Verification request failed: ${err.message}`, err.stack); + + // // If we have error response data from Zarinpal, extract it + // if (err.response?.data) { + // const errorData = err.response.data as ZarinPalPGVerifyData; + // this.logger.error(`Zarinpal error response:`, errorData); + + // // Return the Zarinpal error response if it has the expected structure + // if (errorData.errors) { + // return throwError(() => errorData); + // } + // } + + // return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT)); + // }), + // ), + // ); + + // // Log the verification result + // if (data.data) { + // this.logger.log(`Verification response - Code: ${data.data.code}, RefID: ${data.data.ref_id || "N/A"}`); + // } else if (data.errors && data.errors.length > 0) { + // this.logger.warn(`Verification error - Code: ${data.errors[0].code}, Message: ${data.errors[0].message}`); + // } + + // // Return the complete Zarinpal response - let the service handle the business logic + // return { + // code: data.data?.code || data.errors?.[0]?.code || -1, + // message: data.data?.message || data.errors?.[0]?.message || "Unknown error", + // ref_id: data.data?.ref_id || 0, + // card_hash: data.data?.card_hash || "", + // card_pan: data.data?.card_pan || "", + // fee_type: data.data?.fee_type || "", + // fee: data.data?.fee || 0, + // }; + // } catch (error) { + // // If it's a Zarinpal error response, extract the error code and return it + // if (error && typeof error === "object" && "errors" in error) { + // const zarinpalError = error as ZarinPalPGVerifyData; + // const firstError = zarinpalError.errors[0]; + // this.logger.warn(`Zarinpal verification error - Code: ${firstError.code}, Message: ${firstError.message}`); + + // return { + // code: firstError.code, + // message: firstError.message, + // ref_id: 0, + // card_hash: "", + // card_pan: "", + // fee_type: "", + // fee: 0, + // }; + // } + + // this.logger.error(`Payment verification error:`, error); + // throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT); + // } + // } } diff --git a/src/modules/payments/interface/gateway.ts b/src/modules/payments/interface/gateway.ts new file mode 100644 index 0000000..4762111 --- /dev/null +++ b/src/modules/payments/interface/gateway.ts @@ -0,0 +1,38 @@ +export interface IPaymentGateway { + processPayment(processPaymentParam: IProcessPaymentParams): Promise; + verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise; +} + +export interface IProcessPaymentParams { + amount: number; + callbackUrl: string; + merchantId: string; + description: string; + metadata: { + orderId: string; + }; +} + +export interface IProcessPaymentData { + code: number; + message: string; + authority: string; + fee_type: string; + fee: number; +} + +export interface IPaymentVerifyParams { + merchantId: string; + amount: number; + authority: string; +} + +export interface IVerifyPayment { + code: number; + message: string; + refId: number; + cardPan: string; + cardHash: string; + fee_type: string; + fee: number; +} diff --git a/src/modules/payments/interface/payment.ts b/src/modules/payments/interface/payment.ts index 007e492..c8d87d8 100644 --- a/src/modules/payments/interface/payment.ts +++ b/src/modules/payments/interface/payment.ts @@ -11,37 +11,3 @@ export enum PaymentStatusEnum { export enum PaymentGatewayEnum { ZarinPal = 'zarinpal', } - -export interface IPaymentRequest { - amount: number; - callbackUrl: string; - merchantId: string; - description: string; - metadata: { - orderId: string; - }; -} - -export interface IPaymentResponse { - code: number; - message: string; - authority: string; - fee_type: string; - fee: number; -} - -export interface IPaymentVerifyRequest { - merchantId: string; - amount: number; - authority: string; -} - -export interface IPaymentVerifyResponse { - code: number; - message: string; - refId: number; - cardPan: string; - cardHash: string; - fee_type: string; - fee: number; -} diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index 0ea6776..fe6388b 100644 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -4,17 +4,17 @@ 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'; import { Restaurant } from '../restaurants/entities/restaurant.entity'; import { PaymentsController } from './controllers/payments.controller'; import { AuthModule } from '../auth/auth.module'; import { JwtModule } from '@nestjs/jwt'; import { Payment } from './entities/payment.entity'; +import { ZarinpalGateway } from './gateways/zarinpal.gateway'; @Module({ imports: [MikroOrmModule.forFeature([PaymentMethod, Payment, Restaurant]), AuthModule, JwtModule], controllers: [PaymentsController], - providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, PaymentGatewayService], + providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, ZarinpalGateway], exports: [ PaymentMethodRepository, PaymentMethodService, diff --git a/src/modules/payments/services/payment-gateway.service.ts b/src/modules/payments/services/payment-gateway.service.ts deleted file mode 100644 index 1ed4b26..0000000 --- a/src/modules/payments/services/payment-gateway.service.ts +++ /dev/null @@ -1,190 +0,0 @@ -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}/verify/${orderId}`; - try { - const gatewayResponse = await this.requestToZarinPalGateway({ - amount, - merchantId, - description: `Payment for order #${orderId}`, - callbackUrl, - metadata: { - orderId, - }, - }); - this.logger.log('gatewayResponse', gatewayResponse); - // 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) { - this.logger.error('Error in request to gateway', 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 { - // Transform camelCase to snake_case for Zarinpal API v4 - const zarinpalRequest = { - amount: requestPayment.amount, - merchant_id: requestPayment.merchantId, - description: requestPayment.description, - callback_url: requestPayment.callbackUrl, - metadata: { - order_id: requestPayment.metadata.orderId, - }, - }; - - try { - // Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] } - interface ZarinpalError { - message?: string; - code?: number; - } - interface ZarinpalResponse { - data: IPaymentResponse; - errors: ZarinpalError[]; - } - const response = await axios.post(this.zarinpalRequestUrl, zarinpalRequest); - - // Check if there are errors in the response - if (response.data.errors && response.data.errors.length > 0) { - const errorMessage = response.data.errors.map(err => err.message || JSON.stringify(err)).join(', '); - throw new BadRequestException(`Payment gateway error: ${errorMessage}`); - } - - // Return the nested data object - if (!response.data.data) { - throw new BadRequestException('Payment gateway returned invalid response structure'); - } - - return response.data.data; - } catch (error) { - // Log the actual API error response for debugging - if (axios.isAxiosError(error) && error.response) { - this.logger.error('Zarinpal API error response', { - status: error.response.status, - data: JSON.stringify(error.response.data), - request: zarinpalRequest, - }); - } - throw error; - } - } - - zarinpalPaymentUrl(gateway: PaymentGatewayEnum | null, authority: string | null) { - if (gateway === PaymentGatewayEnum.ZarinPal && authority) { - return `${this.zarinpalPaymentBaseUrl}/${authority}`; - } - return null; - } - - async verifyZarinPalPayment(verifyRequest: IPaymentVerifyRequest): Promise { - try { - // Transform camelCase to snake_case for Zarinpal API v4 - const zarinpalVerifyRequest = { - merchant_id: verifyRequest.merchantId, - amount: verifyRequest.amount, - authority: verifyRequest.authority, - }; - - // Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] } - interface ZarinpalError { - message?: string; - code?: number; - } - interface ZarinpalVerifyResponse { - data: IPaymentVerifyResponse; - errors: ZarinpalError[]; - } - const response = await axios.post(this.zarinpalVerifyUrl, zarinpalVerifyRequest); - - // Check if there are errors in the response - if (response.data.errors && response.data.errors.length > 0) { - const errorMessage = response.data.errors.map(err => err.message || JSON.stringify(err)).join(', '); - throw new BadRequestException(`Payment gateway error: ${errorMessage}`); - } - - // Return the nested data object - if (!response.data.data) { - throw new BadRequestException('Payment gateway returned invalid response structure'); - } - - return response.data.data; - } catch (error) { - if (error instanceof BadRequestException) { - throw 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/payments.service.ts b/src/modules/payments/services/payments.service.ts index d056d9e..8ada90d 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -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'; @@ -7,56 +6,18 @@ import { Order } from '../../orders/entities/order.entity'; import { PaymentMethod } from '../entities/payment-method.entity'; import { CreatePaymentDto } from '../dto/create-payment.dto'; import { User } from '../../users/entities/user.entity'; +import { ZarinpalGateway } from '../gateways/zarinpal.gateway'; +import { Logger } from '@nestjs/common'; @Injectable() export class PaymentsService { + private readonly logger = new Logger(PaymentsService.name); + constructor( private readonly em: EntityManager, - private readonly paymentGatewayService: PaymentGatewayService, + private readonly zarinpalGateway: ZarinpalGateway, ) {} - // async initializePayment( - // paymentMethodId: string, - // amount: number, - // orderId: string, - // ): Promise<{ paymentUrl: string | null }> { - // // 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 paymentMethod = await this.em.findOne(PaymentMethod, { id: paymentMethodId }, { populate: ['restaurant'] }); - // if (!paymentMethod) { - // throw new NotFoundException('Payment method not found'); - // } - - // if (paymentMethod.method === PaymentMethodEnum.Online && !paymentMethod.merchantId) { - // throw new BadRequestException('Merchant ID is required for online payments'); - // } - - // // Create payment record and save authority - // const restaurantDomain = paymentMethod.restaurant.domain; - // const gateway = paymentMethod.gateway; - // const { authority } = await this.createPayment(restaurantDomain, { - // amount, - // orderId, - // paymentMethod: paymentMethod.method, - // merchantId: paymentMethod.merchantId ?? null, - // gateway, - // }); - - // const paymentUrl = this.paymentGatewayService.zarinpalPaymentUrl(gateway, authority); - - // return { paymentUrl }; - // } - async startPayment(orderId: string, restId: string): Promise<{ paymentUrl: string | null }> { const { amount, method, restaurantDomain, gateway, merchantId, user } = await this.validateOrder(orderId, restId); @@ -81,7 +42,7 @@ export class PaymentsService { gateway, }); - const paymentUrl = this.paymentGatewayService.zarinpalPaymentUrl(gateway, authority); + const paymentUrl = this.zarinpalGateway.zarinpalPaymentUrl(gateway, authority); return { paymentUrl }; } @@ -123,14 +84,7 @@ export class PaymentsService { async createPayment(domain: string, dto: CreatePaymentDto) { const { amount, orderId, merchantId, gateway, method } = dto; - const { authority } = await this.paymentGatewayService.requestToGateway( - method, - amount, - orderId, - merchantId, - domain, - gateway, - ); + const { authority } = await this.requestToGateway(method, amount, orderId, merchantId, domain, gateway); const payment = this.em.create(Payment, { amount, @@ -242,7 +196,7 @@ export class PaymentsService { // Handle ZarinPal gateway verification if (payment.gateway === PaymentGatewayEnum.ZarinPal) { try { - const verifyResponse = await this.paymentGatewayService.verifyZarinPalPayment({ + const verifyResponse = await this.zarinpalGateway.verifyPayment({ merchantId: paymentMethod.merchantId, amount: verifyAmount, authority, @@ -293,10 +247,6 @@ export class PaymentsService { throw new BadRequestException(`Unsupported payment gateway: ${String(payment.gateway)}`); } - // update(_id: number, _updatePaymentDto: unknown) { - // return `This action updates a #${_id} payment`; - // } - findAllByRestaurantId(restId: string, userId: string) { return this.em.find( Payment, @@ -305,7 +255,65 @@ export class PaymentsService { ); } - // remove(id: number) { - // return `This action removes a #${id} 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}/verify/${orderId}`; + try { + const gatewayResponse = await this.zarinpalGateway.processPayment({ + amount, + merchantId, + description: `Payment for order #${orderId}`, + callbackUrl, + metadata: { + orderId, + }, + }); + this.logger.log('gatewayResponse', gatewayResponse); + // 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) { + this.logger.error('Error in request to gateway', 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)}`); + } }