payment refactor

This commit is contained in:
2025-12-16 23:35:39 +03:30
parent f2ecada5a1
commit ea2b7d7968
10 changed files with 263 additions and 463 deletions
@@ -23,7 +23,7 @@ export class PaymentsController {
constructor( constructor(
private readonly paymentsService: PaymentsService, private readonly paymentsService: PaymentsService,
private readonly paymentMethodService: PaymentMethodService, private readonly paymentMethodService: PaymentMethodService,
) {} ) { }
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@@ -68,7 +68,7 @@ export class PaymentsController {
required: true, required: true,
}) })
payAnOrder(@Param('orderId') orderId: string, @RestId() restId: string) { payAnOrder(@Param('orderId') orderId: string, @RestId() restId: string) {
return this.paymentsService.startPayment(orderId, restId); return this.paymentsService.payOrder(orderId, restId);
} }
// @UseGuards(AdminAuthGuard) // @UseGuards(AdminAuthGuard)
// @ApiBearerAuth() // @ApiBearerAuth()
@@ -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;
}
@@ -1,9 +1,7 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreatePaymentDto } from './create-payment.dto';
import { IsNumber } from 'class-validator'; import { IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class UpdatePaymentDto extends PartialType(CreatePaymentDto) { export class UpdatePaymentDto {
@ApiProperty({ description: 'Payment ID' }) @ApiProperty({ description: 'Payment ID' })
@IsNumber() @IsNumber()
id: number; id: number;
@@ -12,13 +12,13 @@ export class Payment extends BaseEntity {
amount!: number; amount!: number;
@Property({ unique: true, nullable: true }) @Property({ unique: true, nullable: true })
authority?: string | null; referenceId?: string | null;
@Enum(() => PaymentGatewayEnum) @Enum(() => PaymentGatewayEnum)
gateway?: PaymentGatewayEnum | null = null; gateway?: PaymentGatewayEnum | null = null;
@Property({ nullable: true }) @Property({ nullable: true })
refId?: string | null = null; transactionId?: string | null = null;
@Enum(() => PaymentStatusEnum) @Enum(() => PaymentStatusEnum)
status!: PaymentStatusEnum; status!: PaymentStatusEnum;
+54 -197
View File
@@ -3,63 +3,58 @@ import axios from 'axios';
import { import {
IPaymentGateway, IPaymentGateway,
IPaymentVerifyParams, IPaymentVerifyParams,
IProcessPaymentData, IRequestPaymentData,
IProcessPaymentParams, IRequestPaymentParams,
IVerifyPayment, IPaymentVerifyData,
IZarinpalPaymentResponse,
IZarinpalRequestPayment,
IZarinpalVerifyRequest,
IZarinpalVerifyResponse,
} from '../interface/gateway'; } from '../interface/gateway';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { PaymentGatewayEnum } from '../interface/payment';
@Injectable() @Injectable()
export class ZarinpalGateway implements IPaymentGateway { export class ZarinpalGateway implements IPaymentGateway {
private readonly logger = new Logger(ZarinpalGateway.name); private readonly logger = new Logger(ZarinpalGateway.name);
private readonly zarinpalRequestUrl: string; private readonly zarinpalRequestUrl: string;
private readonly zarinpalPaymentBaseUrl: string;
private readonly zarinpalVerifyUrl: 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) { constructor(private readonly configService: ConfigService) {
const zarinpalBaseUrl = this.configService.get<string>('ZARINPAL_BASE_URL') || 'https://sandbox.zarinpal.com'; const zarinpalBaseUrl = this.configService.get<string>('ZARINPAL_BASE_URL') || 'https://sandbox.zarinpal.com';
this.zarinpalPaymentBaseUrl = zarinpalBaseUrl;
this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`; this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`;
this.zarinpalPaymentBaseUrl = `${zarinpalBaseUrl}/pg/StartPay`;
this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`; this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`;
} }
async processPayment(params: IProcessPaymentParams): Promise<IProcessPaymentData> { async requestPayment({ amount, merchantId, orderId, domain }: IRequestPaymentParams): Promise<IRequestPaymentData> {
// Transform camelCase to snake_case for Zarinpal API v4 // Transform camelCase to snake_case for Zarinpal API v4
const zarinpalRequest = { const callbackUrl = `${domain}/verify/${orderId}`;
amount: params.amount, const zarinpalRequest: IZarinpalRequestPayment = {
merchant_id: params.merchantId, amount,
description: params.description, merchant_id: merchantId,
callback_url: params.callbackUrl, description: `Payment for order #${orderId}`,
callback_url: callbackUrl,
metadata: { metadata: {
order_id: params.metadata.orderId, order_id: orderId,
}, },
}; };
try { try {
// Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] } const res = await axios.post<IZarinpalPaymentResponse>(this.zarinpalRequestUrl, zarinpalRequest, this.axiosConfig);
interface ZarinpalError { const { code, message, authority, errors } = res.data;
message?: string;
code?: number;
}
interface ZarinpalResponse {
data: IProcessPaymentData;
errors: ZarinpalError[];
}
const response = await axios.post<ZarinpalResponse>(this.zarinpalRequestUrl, zarinpalRequest);
// Check if there are errors in the response // Check if there are errors in the response
if (response.data.errors && response.data.errors.length > 0) { if ((code !== 100 && code !== 0) || !authority) {
const errorMessage = response.data.errors.map(err => err.message || JSON.stringify(err)).join(', '); throw new BadRequestException(message ?? 'Zarinpal payment request error');
throw new BadRequestException(`Payment gateway error: ${errorMessage}`);
} }
// Return the nested data object return { transactionId: authority };
if (!response.data.data) {
throw new BadRequestException('Payment gateway returned invalid response structure');
}
return response.data.data;
} catch (error) { } catch (error) {
// Log the actual API error response for debugging // Log the actual API error response for debugging
if (axios.isAxiosError(error) && error.response) { if (axios.isAxiosError(error) && error.response) {
@@ -73,185 +68,47 @@ export class ZarinpalGateway implements IPaymentGateway {
} }
} }
zarinpalPaymentUrl(gateway: PaymentGatewayEnum | null, authority: string | null) { async verifyPayment({ merchantId, amount, transactionId }: IPaymentVerifyParams): Promise<IPaymentVerifyData> {
if (gateway === PaymentGatewayEnum.ZarinPal && authority) {
return `${this.zarinpalPaymentBaseUrl}/${authority}`;
}
return null;
}
async verifyPayment(verifyRequest: IPaymentVerifyParams): Promise<IVerifyPayment> {
try { try {
// Transform camelCase to snake_case for Zarinpal API v4 // Transform camelCase to snake_case for Zarinpal API v4
const zarinpalVerifyRequest = { const zarinpalVerifyRequest: IZarinpalVerifyRequest = {
merchant_id: verifyRequest.merchantId, merchant_id: merchantId,
amount: verifyRequest.amount, amount,
authority: verifyRequest.authority, authority: transactionId,
}; };
// Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] } const res = await axios.post<IZarinpalVerifyResponse>(this.zarinpalVerifyUrl, zarinpalVerifyRequest, this.axiosConfig);
interface ZarinpalError {
message?: string; // Check if response data exists
code?: number; if (!res.data || !res.data.data) {
throw new BadRequestException('Invalid response from Zarinpal API');
} }
interface ZarinpalVerifyResponse {
data: IVerifyPayment; const { code, message, ref_id, card_pan } = res.data.data;
errors: ZarinpalError[];
}
const response = await axios.post<ZarinpalVerifyResponse>(this.zarinpalVerifyUrl, zarinpalVerifyRequest);
// Check if there are errors in the response // Check if there are errors in the response
if (response.data.errors && response.data.errors.length > 0) { if (code !== 100 && code !== 0) {
const errorMessage = response.data.errors.map(err => err.message || JSON.stringify(err)).join(', '); throw new BadRequestException(message ?? 'Zarinpal error');
throw new BadRequestException(`Payment gateway error: ${errorMessage}`);
} }
// Return the nested data object return {
if (!response.data.data) { success: code === 100,
throw new BadRequestException('Payment gateway returned invalid response structure'); referenceId: ref_id ? ref_id.toString() : undefined,
} cardPan: card_pan,
raw: res.data,
return response.data.data; };
} catch (error) { } catch (error) {
if (error instanceof BadRequestException) { if (axios.isAxiosError(error) && error.response) {
throw error; this.logger.error('Zarinpal verify failed', error.response.data);
throw new BadRequestException(error.response.data);
} }
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to verify payment with gateway: ${errorMessage}`); 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:`, { getPaymentUrl(authority: string): string {
// merchant_id: this.config.merchantId, return `${this.zarinpalPaymentBaseUrl}/pg/StartPay/${authority}`;
// 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);
// }
// }
} }
+52 -23
View File
@@ -1,38 +1,67 @@
export interface IPaymentGateway { export interface IPaymentGateway {
processPayment(processPaymentParam: IProcessPaymentParams): Promise<IProcessPaymentData>; requestPayment(requestPaymentParams: IRequestPaymentParams): Promise<IRequestPaymentData>;
verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise<IVerifyPayment>; verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise<IPaymentVerifyData>;
getPaymentUrl(authority: string): string;
} }
export interface IProcessPaymentParams { export interface IRequestPaymentParams {
amount: number; amount: number;
callbackUrl: string;
merchantId: string; merchantId: string;
description: string; domain: string;
metadata: { orderId: string;
orderId: string;
};
} }
export interface IProcessPaymentData { export interface IRequestPaymentData {
code: number; transactionId: string;
message: string;
authority: string;
fee_type: string;
fee: number;
} }
export interface IPaymentVerifyParams { export interface IPaymentVerifyParams {
amount: number;
merchantId: string; merchantId: string;
transactionId: string;
}
export interface IPaymentVerifyData {
success: boolean;
referenceId?: string;
cardPan?: string;
raw: Record<string, any>;
}
////////////////////////// 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; amount: number;
authority: string; authority: string;
} }
export interface IZarinpalVerifyResponse {
export interface IVerifyPayment { data: {
code: number; code: number;
message: string; message: string;
refId: number; card_hash: string;
cardPan: string; card_pan: string;
cardHash: string; ref_id: number;
fee_type: string; fee_type: string;
fee: number; fee: number;
};
errors: [];
} }
@@ -11,3 +11,11 @@ export enum PaymentStatusEnum {
export enum PaymentGatewayEnum { export enum PaymentGatewayEnum {
ZarinPal = 'zarinpal', ZarinPal = 'zarinpal',
} }
export interface ICreatePayment {
orderId: string;
amount: number;
method: PaymentMethodEnum;
transactionId: string;
gateway?: PaymentGatewayEnum | null;
}
+3 -2
View File
@@ -10,11 +10,12 @@ import { AuthModule } from '../auth/auth.module';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { Payment } from './entities/payment.entity'; import { Payment } from './entities/payment.entity';
import { ZarinpalGateway } from './gateways/zarinpal.gateway'; import { ZarinpalGateway } from './gateways/zarinpal.gateway';
import { GatewayManager } from './services/gateway.manager';
@Module({ @Module({
imports: [MikroOrmModule.forFeature([PaymentMethod, Payment, Restaurant]), AuthModule, JwtModule], imports: [MikroOrmModule.forFeature([PaymentMethod, Payment, Restaurant]), AuthModule, JwtModule],
controllers: [PaymentsController], controllers: [PaymentsController],
providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, ZarinpalGateway], providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, ZarinpalGateway, GatewayManager],
exports: [ exports: [
PaymentMethodRepository, PaymentMethodRepository,
PaymentMethodService, PaymentMethodService,
@@ -22,4 +23,4 @@ import { ZarinpalGateway } from './gateways/zarinpal.gateway';
// PaymentGatewayService, // PaymentGatewayService,
], ],
}) })
export class PaymentsModule {} export class PaymentsModule { }
@@ -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}`);
}
}
}
+121 -207
View File
@@ -4,10 +4,9 @@ import { Payment } from '../entities/payment.entity';
import { EntityManager, RequiredEntityData } from '@mikro-orm/core'; import { EntityManager, RequiredEntityData } from '@mikro-orm/core';
import { Order } from '../../orders/entities/order.entity'; import { Order } from '../../orders/entities/order.entity';
import { PaymentMethod } from '../entities/payment-method.entity'; import { PaymentMethod } from '../entities/payment-method.entity';
import { CreatePaymentDto } from '../dto/create-payment.dto'; import { Logger } from '@nestjs/common';
import { User } from '../../users/entities/user.entity'; import { GatewayManager } from './gateway.manager';
import { ZarinpalGateway } from '../gateways/zarinpal.gateway'; import { UserWallet } from 'src/modules/users/entities/user-wallet.entity';
import { Logger } from '@nestjs/common';
@Injectable() @Injectable()
export class PaymentsService { export class PaymentsService {
@@ -15,36 +14,62 @@ export class PaymentsService {
constructor( constructor(
private readonly em: EntityManager, private readonly em: EntityManager,
private readonly zarinpalGateway: ZarinpalGateway, private readonly gatewayManager: GatewayManager,
) {} ) { }
async startPayment(orderId: string, restId: string): Promise<{ paymentUrl: string | null }> { async payOrder(orderId: string, restId: string): Promise<{ paymentUrl: string | null }> {
const { amount, method, restaurantDomain, gateway, merchantId, user } = await this.validateOrder(orderId, restId); const { amount, method, restaurantDomain, gateway, merchantId, user } =
await this.validateOrder(orderId, restId);
// Handle Wallet payment immediately // Handle Wallet payment immediately
// if (method === PaymentMethodEnum.Wallet) { if (method === PaymentMethodEnum.Cash) {
// // Check wallet balance const payment = this.em.create(Payment, {
// if (user.wallet < amount) { amount,
// throw new BadRequestException('Insufficient wallet balance'); transactionId: null,
// } order: this.em.getReference(Order, orderId),
gateway: null,
method,
status: PaymentStatusEnum.Pending,
} as RequiredEntityData<Payment>);
// // Deduct from wallet and create payment record await this.em.persistAndFlush(payment);
// const payment = await this.processWalletPayment(orderId, amount, user);
// 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, amount,
orderId, orderId,
method, merchantId: merchantId as string,
merchantId, domain: restaurantDomain,
gateway,
}); });
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<Payment>);
return { paymentUrl }; await this.em.persistAndFlush(payment);
return { paymentUrl: gatewayInstance.getPaymentUrl(transactionId) };
} }
private async validateOrder(orderId: string, restId: string) { private async validateOrder(orderId: string, restId: string) {
@@ -81,39 +106,23 @@ export class PaymentsService {
return { amount, method, restaurantDomain, gateway, merchantId, user }; 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); private async processWalletPayment(orderId: string, amount: number, userId: string, restId: string): Promise<Payment> {
const payment = this.em.create(Payment, {
amount,
authority: authority,
order: this.em.getReference(Order, orderId),
gateway,
status: PaymentStatusEnum.Pending,
} as RequiredEntityData<Payment>);
await this.em.persistAndFlush(payment);
return { authority, payment };
}
private async processWalletPayment(orderId: string, amount: number, user: User): Promise<Payment> {
return this.em.transactional(async em => { return this.em.transactional(async em => {
// Reload user to get latest wallet balance // Reload user to get latest wallet balance
const freshUser = await em.findOne(User, { id: user.id }); const userWallet = await em.findOne(UserWallet, { user: { id: userId }, restaurant: { id: restId } });
if (!freshUser) { if (!userWallet) {
throw new NotFoundException('User not found'); throw new NotFoundException('User wallet not found');
} }
// Double-check balance // Double-check balance
// if (freshUser.wallet < amount) { if (userWallet.wallet < amount) {
// throw new BadRequestException('Insufficient wallet balance'); throw new BadRequestException('Insufficient wallet balance');
// } }
// Deduct from wallet // Deduct from wallet
// freshUser.wallet -= amount; userWallet.wallet -= amount;
// em.persist(freshUser); em.persist(userWallet);
// Create payment record // Create payment record
const payment = em.create(Payment, { const payment = em.create(Payment, {
@@ -139,114 +148,80 @@ export class PaymentsService {
}); });
} }
async verifyPayment(authority: string, orderId: string): Promise<Payment> { async verifyPayment(transactionId: string, orderId: string): Promise<Payment> {
// Find payment by authority and orderId return this.em.transactional(async em => {
const payment = await this.em.findOne(
Payment,
{ authority, order: { id: orderId } },
{ populate: ['order', 'order.paymentMethod'] },
);
if (!payment) {
throw new NotFoundException('Payment not found');
}
// If payment is already verified, return it // Find payment by authority and orderId
if (payment.status === PaymentStatusEnum.Paid) { const payment = await em.findOne(
return payment; 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 is already verified, return it
if (!payment.order.paymentMethod) { if (payment.status === PaymentStatusEnum.Paid) {
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<string, any>;
payment.paidAt = new Date();
payment.order.paymentStatus = PaymentStatusEnum.Paid;
} else {
// Payment failed
payment.status = PaymentStatusEnum.Failed;
payment.verifyResponse = response as unknown as Record<string, any>;
payment.failedAt = new Date();
payment.order.paymentStatus = PaymentStatusEnum.Failed;
}
await this.em.persistAndFlush(payment);
return payment; 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.status = PaymentStatusEnum.Failed;
payment.verifyResponse = raw;
payment.failedAt = new Date(); payment.failedAt = new Date();
payment.order.paymentStatus = PaymentStatusEnum.Failed; 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 payment.cardPan = cardPan;
throw new BadRequestException(`Unsupported payment gateway: ${String(payment.gateway)}`); payment.verifyResponse = raw;
payment.paidAt = new Date();
payment.order.paymentStatus = PaymentStatusEnum.Paid;
await em.persistAndFlush(payment);
return payment;
})
} }
findAllByRestaurantId(restId: string, userId: string) { findAllByRestaurantId(restId: string, userId: string) {
return this.em.find( return this.em.find(
Payment, 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)}`);
}
} }