payment gateway
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<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}/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<IPaymentResponse> {
|
||||
const response = await axios.post<IPaymentResponse>(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<IPaymentVerifyResponse> {
|
||||
try {
|
||||
const response = await axios.post<IPaymentVerifyResponse>(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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string>('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<string>('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<string>('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<string>('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()}`;
|
||||
}
|
||||
}
|
||||
@@ -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<IPaymentResponse> {
|
||||
const gatewayPaymentUrl = 'https://payment.zarinpal.com/pg/v4/payment/request.json';
|
||||
const response = await axios.post<IPaymentResponse>(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<Payment> {
|
||||
// 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<IPaymentVerifyResponse> {
|
||||
const gatewayVerifyUrl = 'https://payment.zarinpal.com/pg/v4/payment/verify.json';
|
||||
try {
|
||||
const response = await axios.post<IPaymentVerifyResponse>(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`;
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user