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
+121 -207
View File
@@ -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<Payment>);
// // 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<Payment>);
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<Payment>);
await this.em.persistAndFlush(payment);
return { authority, payment };
}
private async processWalletPayment(orderId: string, amount: number, user: User): Promise<Payment> {
private async processWalletPayment(orderId: string, amount: number, userId: string, restId: string): Promise<Payment> {
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<Payment> {
// 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<Payment> {
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<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);
// 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)}`);
}
}