diff --git a/src/modules/payments/controllers/payments.controller.ts b/src/modules/payments/controllers/payments.controller.ts index 3e9c9ad..0a293db 100644 --- a/src/modules/payments/controllers/payments.controller.ts +++ b/src/modules/payments/controllers/payments.controller.ts @@ -23,7 +23,7 @@ export class PaymentsController { constructor( private readonly paymentsService: PaymentsService, private readonly paymentMethodService: PaymentMethodService, - ) {} + ) { } @UseGuards(AuthGuard) @ApiBearerAuth() @@ -68,7 +68,7 @@ export class PaymentsController { required: true, }) payAnOrder(@Param('orderId') orderId: string, @RestId() restId: string) { - return this.paymentsService.startPayment(orderId, restId); + return this.paymentsService.payOrder(orderId, restId); } // @UseGuards(AdminAuthGuard) // @ApiBearerAuth() diff --git a/src/modules/payments/dto/create-payment.dto.ts b/src/modules/payments/dto/create-payment.dto.ts deleted file mode 100644 index 85a63a3..0000000 --- a/src/modules/payments/dto/create-payment.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { PaymentGatewayEnum, PaymentMethodEnum } from '../interface/payment'; - -export class CreatePaymentDto { - @ApiProperty({ description: 'Order ID' }) - @IsString() - orderId: string; - - @ApiProperty({ description: 'Payment amount' }) - @IsNumber() - amount: number; - - @ApiProperty({ description: 'Payment method' }) - @IsEnum(PaymentMethodEnum) - method: PaymentMethodEnum; - - @ApiProperty({ description: 'Payment authority' }) - @IsOptional() - @IsString() - merchantId?: string | null; - - @ApiProperty({ description: 'Payment gateway' }) - @IsOptional() - @IsEnum(PaymentGatewayEnum) - gateway?: PaymentGatewayEnum | null; -} diff --git a/src/modules/payments/dto/update-payment.dto.ts b/src/modules/payments/dto/update-payment.dto.ts index 13f3f5e..a3d0fd2 100644 --- a/src/modules/payments/dto/update-payment.dto.ts +++ b/src/modules/payments/dto/update-payment.dto.ts @@ -1,9 +1,7 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { CreatePaymentDto } from './create-payment.dto'; import { IsNumber } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -export class UpdatePaymentDto extends PartialType(CreatePaymentDto) { +export class UpdatePaymentDto { @ApiProperty({ description: 'Payment ID' }) @IsNumber() id: number; diff --git a/src/modules/payments/entities/payment.entity.ts b/src/modules/payments/entities/payment.entity.ts index b40808d..974c8df 100644 --- a/src/modules/payments/entities/payment.entity.ts +++ b/src/modules/payments/entities/payment.entity.ts @@ -12,13 +12,13 @@ export class Payment extends BaseEntity { amount!: number; @Property({ unique: true, nullable: true }) - authority?: string | null; + referenceId?: string | null; @Enum(() => PaymentGatewayEnum) gateway?: PaymentGatewayEnum | null = null; @Property({ nullable: true }) - refId?: string | null = null; + transactionId?: string | null = null; @Enum(() => PaymentStatusEnum) status!: PaymentStatusEnum; diff --git a/src/modules/payments/gateways/zarinpal.gateway.ts b/src/modules/payments/gateways/zarinpal.gateway.ts index a254f92..6ffd28e 100755 --- a/src/modules/payments/gateways/zarinpal.gateway.ts +++ b/src/modules/payments/gateways/zarinpal.gateway.ts @@ -3,63 +3,58 @@ import axios from 'axios'; import { IPaymentGateway, IPaymentVerifyParams, - IProcessPaymentData, - IProcessPaymentParams, - IVerifyPayment, + IRequestPaymentData, + IRequestPaymentParams, + IPaymentVerifyData, + IZarinpalPaymentResponse, + IZarinpalRequestPayment, + IZarinpalVerifyRequest, + IZarinpalVerifyResponse, } from '../interface/gateway'; import { ConfigService } from '@nestjs/config'; -import { PaymentGatewayEnum } from '../interface/payment'; @Injectable() export class ZarinpalGateway implements IPaymentGateway { private readonly logger = new Logger(ZarinpalGateway.name); private readonly zarinpalRequestUrl: string; - private readonly zarinpalPaymentBaseUrl: string; private readonly zarinpalVerifyUrl: string; + private readonly zarinpalPaymentBaseUrl: string; + private readonly axiosConfig = { + timeout: 10_000, + headers: { + 'Content-Type': 'application/json', + }, + }; constructor(private readonly configService: ConfigService) { const zarinpalBaseUrl = this.configService.get('ZARINPAL_BASE_URL') || 'https://sandbox.zarinpal.com'; + this.zarinpalPaymentBaseUrl = zarinpalBaseUrl; this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`; - this.zarinpalPaymentBaseUrl = `${zarinpalBaseUrl}/pg/StartPay`; this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`; } - async processPayment(params: IProcessPaymentParams): Promise { + async requestPayment({ amount, merchantId, orderId, domain }: IRequestPaymentParams): 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, + const callbackUrl = `${domain}/verify/${orderId}`; + const zarinpalRequest: IZarinpalRequestPayment = { + amount, + merchant_id: merchantId, + description: `Payment for order #${orderId}`, + callback_url: callbackUrl, metadata: { - order_id: params.metadata.orderId, + order_id: orderId, }, }; try { - // 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); - + const res = await axios.post(this.zarinpalRequestUrl, zarinpalRequest, this.axiosConfig); + const { code, message, authority, errors } = res.data; // 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}`); + if ((code !== 100 && code !== 0) || !authority) { + throw new BadRequestException(message ?? 'Zarinpal payment request error'); } - // Return the nested data object - if (!response.data.data) { - throw new BadRequestException('Payment gateway returned invalid response structure'); - } - - return response.data.data; + return { transactionId: authority }; } catch (error) { // Log the actual API error response for debugging if (axios.isAxiosError(error) && error.response) { @@ -73,185 +68,47 @@ export class ZarinpalGateway implements IPaymentGateway { } } - zarinpalPaymentUrl(gateway: PaymentGatewayEnum | null, authority: string | null) { - if (gateway === PaymentGatewayEnum.ZarinPal && authority) { - return `${this.zarinpalPaymentBaseUrl}/${authority}`; - } - return null; - } - - async verifyPayment(verifyRequest: IPaymentVerifyParams): Promise { + async verifyPayment({ merchantId, amount, transactionId }: IPaymentVerifyParams): Promise { try { // Transform camelCase to snake_case for Zarinpal API v4 - const zarinpalVerifyRequest = { - merchant_id: verifyRequest.merchantId, - amount: verifyRequest.amount, - authority: verifyRequest.authority, + const zarinpalVerifyRequest: IZarinpalVerifyRequest = { + merchant_id: merchantId, + amount, + authority: transactionId, }; - // Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] } - interface ZarinpalError { - message?: string; - code?: number; + const res = await axios.post(this.zarinpalVerifyUrl, zarinpalVerifyRequest, this.axiosConfig); + + // Check if response data exists + if (!res.data || !res.data.data) { + throw new BadRequestException('Invalid response from Zarinpal API'); } - interface ZarinpalVerifyResponse { - data: IVerifyPayment; - errors: ZarinpalError[]; - } - const response = await axios.post(this.zarinpalVerifyUrl, zarinpalVerifyRequest); + + const { code, message, ref_id, card_pan } = res.data.data; // 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}`); + if (code !== 100 && code !== 0) { + throw new BadRequestException(message ?? 'Zarinpal error'); } - // Return the nested data object - if (!response.data.data) { - throw new BadRequestException('Payment gateway returned invalid response structure'); - } - - return response.data.data; + return { + success: code === 100, + referenceId: ref_id ? ref_id.toString() : undefined, + cardPan: card_pan, + raw: res.data, + }; } catch (error) { - if (error instanceof BadRequestException) { - throw error; + if (axios.isAxiosError(error) && error.response) { + this.logger.error('Zarinpal verify failed', error.response.data); + throw new BadRequestException(error.response.data); } 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, - // }); + getPaymentUrl(authority: string): string { + return `${this.zarinpalPaymentBaseUrl}/pg/StartPay/${authority}`; + } - // 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 index 4762111..1e20b19 100644 --- a/src/modules/payments/interface/gateway.ts +++ b/src/modules/payments/interface/gateway.ts @@ -1,38 +1,67 @@ export interface IPaymentGateway { - processPayment(processPaymentParam: IProcessPaymentParams): Promise; - verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise; + requestPayment(requestPaymentParams: IRequestPaymentParams): Promise; + verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise; + getPaymentUrl(authority: string): string; } -export interface IProcessPaymentParams { +export interface IRequestPaymentParams { amount: number; - callbackUrl: string; merchantId: string; - description: string; - metadata: { - orderId: string; - }; + domain: string; + orderId: string; } -export interface IProcessPaymentData { - code: number; - message: string; - authority: string; - fee_type: string; - fee: number; +export interface IRequestPaymentData { + transactionId: string; } export interface IPaymentVerifyParams { + amount: number; merchantId: string; + transactionId: string; +} + +export interface IPaymentVerifyData { + success: boolean; + referenceId?: string; + cardPan?: string; + raw: Record; +} +////////////////////////// Zarinpal API v4 ////////////////////////// +export interface IZarinpalRequestPayment { + amount: number; + merchant_id: string; + description: string; + callback_url: string; + metadata?: { + order_id?: string; + mobile?: string; + email?: string; + }; + referrer_id?: string; +} +export interface IZarinpalPaymentResponse { + code: number; + message: string; + authority: string; + fee_type: string; + fee: number; + errors: []; +} +export interface IZarinpalVerifyRequest { + merchant_id: string; amount: number; authority: string; } - -export interface IVerifyPayment { - code: number; - message: string; - refId: number; - cardPan: string; - cardHash: string; - fee_type: string; - fee: number; +export interface IZarinpalVerifyResponse { + data: { + code: number; + message: string; + card_hash: string; + card_pan: string; + ref_id: number; + fee_type: string; + fee: number; + }; + errors: []; } diff --git a/src/modules/payments/interface/payment.ts b/src/modules/payments/interface/payment.ts index c8d87d8..1ee1872 100644 --- a/src/modules/payments/interface/payment.ts +++ b/src/modules/payments/interface/payment.ts @@ -11,3 +11,11 @@ export enum PaymentStatusEnum { export enum PaymentGatewayEnum { ZarinPal = 'zarinpal', } + +export interface ICreatePayment { + orderId: string; + amount: number; + method: PaymentMethodEnum; + transactionId: string; + gateway?: PaymentGatewayEnum | null; +} \ No newline at end of file diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index fe6388b..1a9cc91 100644 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -10,11 +10,12 @@ import { AuthModule } from '../auth/auth.module'; import { JwtModule } from '@nestjs/jwt'; import { Payment } from './entities/payment.entity'; import { ZarinpalGateway } from './gateways/zarinpal.gateway'; +import { GatewayManager } from './services/gateway.manager'; @Module({ imports: [MikroOrmModule.forFeature([PaymentMethod, Payment, Restaurant]), AuthModule, JwtModule], controllers: [PaymentsController], - providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, ZarinpalGateway], + providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, ZarinpalGateway, GatewayManager], exports: [ PaymentMethodRepository, PaymentMethodService, @@ -22,4 +23,4 @@ import { ZarinpalGateway } from './gateways/zarinpal.gateway'; // PaymentGatewayService, ], }) -export class PaymentsModule {} +export class PaymentsModule { } diff --git a/src/modules/payments/services/gateway.manager.ts b/src/modules/payments/services/gateway.manager.ts new file mode 100644 index 0000000..7292888 --- /dev/null +++ b/src/modules/payments/services/gateway.manager.ts @@ -0,0 +1,20 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { PaymentGatewayEnum } from '../interface/payment'; +import { ZarinpalGateway } from '../gateways/zarinpal.gateway'; +import { IPaymentGateway } from '../interface/gateway'; + +@Injectable() +export class GatewayManager { + constructor( + private readonly zarinpal: ZarinpalGateway, + ) { } + + get(gateway: PaymentGatewayEnum): IPaymentGateway { + switch (gateway) { + case PaymentGatewayEnum.ZarinPal: + return this.zarinpal; + default: + throw new BadRequestException(`Unsupported gateway: ${gateway}`); + } + } +} diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index 8ada90d..0eabe8e 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -4,10 +4,9 @@ import { Payment } from '../entities/payment.entity'; import { EntityManager, RequiredEntityData } from '@mikro-orm/core'; 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'; + import { Logger } from '@nestjs/common'; +import { GatewayManager } from './gateway.manager'; +import { UserWallet } from 'src/modules/users/entities/user-wallet.entity'; @Injectable() export class PaymentsService { @@ -15,36 +14,62 @@ export class PaymentsService { constructor( private readonly em: EntityManager, - private readonly zarinpalGateway: ZarinpalGateway, - ) {} + private readonly gatewayManager: GatewayManager, + ) { } - async startPayment(orderId: string, restId: string): Promise<{ paymentUrl: string | null }> { - const { amount, method, restaurantDomain, gateway, merchantId, user } = await this.validateOrder(orderId, restId); + async payOrder(orderId: string, restId: string): Promise<{ paymentUrl: string | null }> { + const { amount, method, restaurantDomain, gateway, merchantId, user } = + await this.validateOrder(orderId, restId); // Handle Wallet payment immediately - // if (method === PaymentMethodEnum.Wallet) { - // // Check wallet balance - // if (user.wallet < amount) { - // throw new BadRequestException('Insufficient wallet balance'); - // } + if (method === PaymentMethodEnum.Cash) { + const payment = this.em.create(Payment, { + amount, + transactionId: null, + order: this.em.getReference(Order, orderId), + gateway: null, + method, + status: PaymentStatusEnum.Pending, + } as RequiredEntityData); - // // Deduct from wallet and create payment record - // const payment = await this.processWalletPayment(orderId, amount, user); + await this.em.persistAndFlush(payment); - // return { paymentUrl: null }; // No redirect needed for wallet - // } + } + if (method === PaymentMethodEnum.Wallet) { + const userWallet = await this.em.findOne(UserWallet, { user: { id: user.id }, restaurant: { id: restId } }); + // Check wallet balance + if (!userWallet) { + throw new NotFoundException('User wallet not found'); + } + if (userWallet.wallet < amount) { + throw new BadRequestException('Insufficient wallet balance'); + } - const { authority } = await this.createPayment(restaurantDomain, { + // Deduct from wallet and create payment record + await this.processWalletPayment(orderId, amount, user.id, restId); + + return { paymentUrl: null }; // No redirect needed for wallet + } + const gatewayInstance = this.gatewayManager.get(gateway as PaymentGatewayEnum); + const { transactionId } = await gatewayInstance.requestPayment({ amount, orderId, - method, - merchantId, - gateway, + merchantId: merchantId as string, + domain: restaurantDomain, }); - const paymentUrl = this.zarinpalGateway.zarinpalPaymentUrl(gateway, authority); + const payment = this.em.create(Payment, { + amount, + transactionId, + order: this.em.getReference(Order, orderId), + gateway, + method, + status: PaymentStatusEnum.Pending, + } as RequiredEntityData); - return { paymentUrl }; + await this.em.persistAndFlush(payment); + + return { paymentUrl: gatewayInstance.getPaymentUrl(transactionId) }; } private async validateOrder(orderId: string, restId: string) { @@ -81,39 +106,23 @@ export class PaymentsService { return { amount, method, restaurantDomain, gateway, merchantId, user }; } - async createPayment(domain: string, dto: CreatePaymentDto) { - const { amount, orderId, merchantId, gateway, method } = dto; - const { authority } = await this.requestToGateway(method, amount, orderId, merchantId, domain, gateway); - - const payment = this.em.create(Payment, { - amount, - authority: authority, - order: this.em.getReference(Order, orderId), - gateway, - status: PaymentStatusEnum.Pending, - } as RequiredEntityData); - - await this.em.persistAndFlush(payment); - return { authority, payment }; - } - - private async processWalletPayment(orderId: string, amount: number, user: User): Promise { + private async processWalletPayment(orderId: string, amount: number, userId: string, restId: string): Promise { return this.em.transactional(async em => { // Reload user to get latest wallet balance - const freshUser = await em.findOne(User, { id: user.id }); - if (!freshUser) { - throw new NotFoundException('User not found'); + const userWallet = await em.findOne(UserWallet, { user: { id: userId }, restaurant: { id: restId } }); + if (!userWallet) { + throw new NotFoundException('User wallet not found'); } // Double-check balance - // if (freshUser.wallet < amount) { - // throw new BadRequestException('Insufficient wallet balance'); - // } + if (userWallet.wallet < amount) { + throw new BadRequestException('Insufficient wallet balance'); + } // Deduct from wallet - // freshUser.wallet -= amount; - // em.persist(freshUser); + userWallet.wallet -= amount; + em.persist(userWallet); // Create payment record const payment = em.create(Payment, { @@ -139,114 +148,80 @@ export class PaymentsService { }); } - async verifyPayment(authority: string, orderId: string): Promise { - // Find payment by authority and orderId - const payment = await this.em.findOne( - Payment, - { authority, order: { id: orderId } }, - { populate: ['order', 'order.paymentMethod'] }, - ); - if (!payment) { - throw new NotFoundException('Payment not found'); - } + async verifyPayment(transactionId: string, orderId: string): Promise { + return this.em.transactional(async em => { - // If payment is already verified, return it - if (payment.status === PaymentStatusEnum.Paid) { - return payment; - } + // Find payment by authority and orderId + const payment = await em.findOne( + Payment, + { transactionId, order: { id: orderId } }, + { populate: ['order', 'order.paymentMethod'] }, + ); + if (!payment) { + throw new NotFoundException('Payment not found'); + } - // Get payment method from order (already populated) - if (!payment.order.paymentMethod) { - throw new NotFoundException('Payment method not found for this order'); - } - - const paymentMethod = await this.em.findOne(PaymentMethod, { id: payment.order.paymentMethod.id }); - if (!paymentMethod) { - throw new NotFoundException('Payment method not found'); - } - - // For Wallet payments, they're already paid during startPayment - // if (paymentMethod.method === PaymentMethodEnum.Wallet) { - // if (payment.status === PaymentStatusEnum.Paid ) { - // return payment; - // } - // throw new BadRequestException('Wallet payment was not processed correctly'); - // } - - // For non-online payments, mark as paid directly - // if (paymentMethod.method !== PaymentMethodEnum.Online) { - // payment.status = PaymentStatusEnum.Paid; - // payment.paidAt = new Date(); - // await this.em.persistAndFlush(payment); - // return payment; - // } - - // Online payments require gateway verification - if (!paymentMethod.merchantId) { - throw new BadRequestException('Merchant ID is required for online payment verification'); - } - - if (!payment.gateway) { - throw new BadRequestException('Payment gateway is required for online payment verification'); - } - - // Use payment amount - const verifyAmount = payment.amount; - - // Handle ZarinPal gateway verification - if (payment.gateway === PaymentGatewayEnum.ZarinPal) { - try { - const verifyResponse = await this.zarinpalGateway.verifyPayment({ - merchantId: paymentMethod.merchantId, - amount: verifyAmount, - authority, - }); - - // Type guard to ensure we have a valid response - if (!verifyResponse || typeof verifyResponse !== 'object') { - throw new BadRequestException('Invalid verification response from payment gateway'); - } - - const response = verifyResponse; - - if (response.code === 100) { - // Payment successful - payment.status = PaymentStatusEnum.Paid; - payment.refId = String(response.refId); - - payment.cardPan = response.cardPan; - payment.verifyResponse = response as unknown as Record; - payment.paidAt = new Date(); - payment.order.paymentStatus = PaymentStatusEnum.Paid; - } else { - // Payment failed - payment.status = PaymentStatusEnum.Failed; - payment.verifyResponse = response as unknown as Record; - payment.failedAt = new Date(); - payment.order.paymentStatus = PaymentStatusEnum.Failed; - } - - await this.em.persistAndFlush(payment); + // If payment is already verified, return it + if (payment.status === PaymentStatusEnum.Paid) { return payment; - } catch (error: unknown) { - // Mark payment as failed on error + } + + // Get payment method from order (already populated) + if (!payment.order.paymentMethod) { + throw new NotFoundException('Payment method not found for this order'); + } + + const paymentMethod = await em.findOne(PaymentMethod, { id: payment.order.paymentMethod.id }); + if (!paymentMethod) { + throw new NotFoundException('Payment method not found'); + } + + // Online payments require gateway verification + if (!paymentMethod.merchantId) { + throw new BadRequestException('Merchant ID is required for online payment verification'); + } + + if (!payment.gateway) { + throw new BadRequestException('Payment gateway is required for online payment verification'); + } + + // Use payment amount + const verifyAmount = payment.amount; + + const gatewayInstance = this.gatewayManager.get(payment.gateway as PaymentGatewayEnum); + + + const verifyResponse = await gatewayInstance.verifyPayment({ + merchantId: paymentMethod.merchantId, + amount: verifyAmount, + transactionId: payment.transactionId as string, + }); + + const { success, referenceId, cardPan, raw } = verifyResponse; + + if (!success) { + // Payment failed payment.status = PaymentStatusEnum.Failed; + payment.verifyResponse = raw; payment.failedAt = new Date(); payment.order.paymentStatus = PaymentStatusEnum.Failed; - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - payment.verifyResponse = { - error: errorMessage, - }; - await this.em.persistAndFlush(payment); - - throw new BadRequestException(`Payment verification failed: ${errorMessage}`); } - } + // Payment successful + payment.status = PaymentStatusEnum.Paid; + payment.referenceId = referenceId - // If gateway is not supported, throw an error - throw new BadRequestException(`Unsupported payment gateway: ${String(payment.gateway)}`); + payment.cardPan = cardPan; + payment.verifyResponse = raw; + payment.paidAt = new Date(); + payment.order.paymentStatus = PaymentStatusEnum.Paid; + + + await em.persistAndFlush(payment); + return payment; + }) } + findAllByRestaurantId(restId: string, userId: string) { return this.em.find( Payment, @@ -255,65 +230,4 @@ export class PaymentsService { ); } - 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)}`); - } }