payment refactor

This commit is contained in:
2025-12-15 23:42:28 +03:30
parent 3e4a4eebe4
commit 5a00874a4e
6 changed files with 344 additions and 432 deletions
+235 -145
View File
@@ -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<string, string> = { "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<string>('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<ZarinPalPGNewRequestData>(`${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<IProcessPaymentData> {
// 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<ZarinPalPGVerifyData>(`${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<ZarinpalResponse>(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<IVerifyPayment> {
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<ZarinpalVerifyResponse>(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<ZarinPalPGNewRequestData>(`${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<ZarinPalPGVerifyData>(`${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);
// }
// }
}
+38
View File
@@ -0,0 +1,38 @@
export interface IPaymentGateway {
processPayment(processPaymentParam: IProcessPaymentParams): Promise<IProcessPaymentData>;
verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise<IVerifyPayment>;
}
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;
}
-34
View File
@@ -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;
}
+2 -2
View File
@@ -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,
@@ -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<string>('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<IPaymentResponse> {
// 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<ZarinpalResponse>(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<IPaymentVerifyResponse> {
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<ZarinpalVerifyResponse>(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}`);
}
}
}
@@ -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)}`);
}
}