This commit is contained in:
2025-12-02 22:57:30 +03:30
parent 40442686be
commit f193266235
19 changed files with 219 additions and 404 deletions
@@ -1,10 +1,11 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { PaymentMethod } from '../entities/payment-method.entity';
import { PaymentMethodRepository } from '../repositories/payment-method.repository';
import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto';
import { RequiredEntityData } from '@mikro-orm/core';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
@Injectable()
export class PaymentMethodService {
@@ -13,26 +14,40 @@ export class PaymentMethodService {
private readonly em: EntityManager,
) {}
async create(createPaymentMethodDto: CreatePaymentMethodDto): Promise<PaymentMethod> {
const paymentMethod = this.paymentMethodRepository.create(
createPaymentMethodDto as unknown as RequiredEntityData<PaymentMethod>,
);
async create(restId: string, createPaymentMethodDto: CreatePaymentMethodDto): Promise<PaymentMethod> {
// Check if restaurant exists
const restaurant = await this.em.findOne(Restaurant, { id: restId });
if (!restaurant) {
throw new NotFoundException(`Restaurant with ID ${restId} not found`);
}
const paymentMethod = this.paymentMethodRepository.create({
restaurant,
...createPaymentMethodDto,
} as unknown as RequiredEntityData<PaymentMethod>);
await this.em.persistAndFlush(paymentMethod);
return paymentMethod;
}
async findAll(): Promise<PaymentMethod[]> {
return this.paymentMethodRepository.findAll();
return this.paymentMethodRepository.findAll({ populate: ['restaurant'] });
}
async findOne(id: string): Promise<PaymentMethod> {
const paymentMethod = await this.paymentMethodRepository.findOne({ id });
const paymentMethod = await this.paymentMethodRepository.findOne({ id }, { populate: ['restaurant'] });
if (!paymentMethod) {
throw new NotFoundException(`PaymentMethod with ID ${id} not found`);
}
return paymentMethod;
}
async findByRestaurant(restaurantId: string): Promise<PaymentMethod[]> {
return this.paymentMethodRepository.find(
{ restaurant: { id: restaurantId } },
{ populate: ['restaurant'] },
);
}
async update(id: string, updatePaymentMethodDto: UpdatePaymentMethodDto): Promise<PaymentMethod> {
const paymentMethod = await this.findOne(id);
this.em.assign(paymentMethod, updatePaymentMethodDto);
@@ -1,17 +1,24 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { RestaurantPaymentMethod } from '../entities/restaurant-payment-method.entity';
import axios from 'axios';
import { IPaymentRequest, IPaymentResponse, PaymentStatus } from '../interface/payment-status';
import {
IPaymentRequest,
IPaymentResponse,
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';
import { PaymentMethod } from '../entities/payment-method.entity';
import { CreatePaymentDto } from '../dto/create-payment.dto';
@Injectable()
export class PaymentsService {
constructor(private readonly em: EntityManager) {}
async initializePayment(
restaurantPaymentMethodId: string,
paymentMethodId: string,
amount: number,
orderId: string,
): Promise<{ paymentUrl: string | null }> {
@@ -27,78 +34,97 @@ export class PaymentsService {
}
// Load restaurant payment method with payment method relationship
const restaurantPaymentMethod = await this.em.findOne(
RestaurantPaymentMethod,
{ id: restaurantPaymentMethodId },
{ populate: ['paymentMethod'] },
);
if (!restaurantPaymentMethod) {
throw new NotFoundException('Restaurant payment method not found');
const paymentMethod = await this.em.findOne(PaymentMethod, { id: paymentMethodId }, { populate: ['restaurant'] });
if (!paymentMethod) {
throw new NotFoundException('Payment method not found');
}
if (!restaurantPaymentMethod.paymentMethod?.isOnline) {
return { paymentUrl: null };
}
if (!restaurantPaymentMethod.callbackUrl) {
throw new BadRequestException('Callback URL is not configured');
}
if (!restaurantPaymentMethod.merchantId) {
if (paymentMethod.method !== PaymentMethodEnum.Online && !paymentMethod.merchantId) {
throw new BadRequestException('Merchant ID is not provided');
}
if (!restaurantPaymentMethod.paymentMethod?.paymentUrl) {
throw new BadRequestException('Payment URL is not configured');
}
// Request to payment gateway with error handling
let gatewayResponse: IPaymentResponse;
try {
gatewayResponse = await this.requestToPaymentGateway(restaurantPaymentMethod.paymentMethod.paymentUrl, {
amount,
callbackUrl: restaurantPaymentMethod.callbackUrl,
merchantId: restaurantPaymentMethod.merchantId,
description: `Payment for order #${orderId}`,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to connect to payment gateway: ${errorMessage}`);
}
// 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');
}
// Create payment record and save authority
const payment = this.em.create(Payment, {
const restaurantDomain = paymentMethod.restaurant.domain;
const { authority } = await this.createPayment(restaurantDomain, {
amount,
authority: gatewayResponse.authority,
order: this.em.getReference(Order, orderId),
gateway: restaurantPaymentMethod.paymentMethod.name,
status: PaymentStatus.Pending,
} as RequiredEntityData<Payment>);
orderId,
paymentMethod: paymentMethod.method,
merchantId: paymentMethod.merchantId ?? null,
gateway: paymentMethod.gateway as PaymentGatewayEnum | null,
});
await this.em.persistAndFlush(payment);
const paymentUrl = this.zarinpalPaymentUrl(paymentMethod.gateway, authority);
// Return payment URL
const paymentUrl = `${restaurantPaymentMethod.paymentMethod.paymentUrl}/${gatewayResponse.authority}`;
return { paymentUrl };
}
private async requestToPaymentGateway(
gatewayPaymentUrl: string,
requestPayment: IPaymentRequest,
): Promise<IPaymentResponse> {
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 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 };
}
async requestToGateway(
paymentMethod: PaymentMethodEnum,
amount: number,
orderId: string,
merchantId: string | null | undefined,
domain: string,
gateway: PaymentGatewayEnum | null | undefined,
): Promise<{ authority: string | null }> {
if (paymentMethod === PaymentMethodEnum.Online && merchantId) {
const callbackUrl = `${domain}/payments/callback`;
if (gateway === PaymentGatewayEnum.ZarinPal) {
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');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to connect to payment gateway: ${errorMessage}`);
}
}
}
return { authority: null };
}
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;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
update(_id: number, _updatePaymentDto: unknown) {
return `This action updates a #${_id} payment`;
@@ -1,135 +0,0 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { RestaurantPaymentMethod } from '../entities/restaurant-payment-method.entity';
import { RestaurantPaymentMethodRepository } from '../repositories/restaurant-payment-method.repository';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { PaymentMethod } from '../entities/payment-method.entity';
import { CreateRestaurantPaymentMethodDto } from '../dto/create-restaurant-payment-method.dto';
import { UpdateRestaurantPaymentMethodDto } from '../dto/update-restaurant-payment-method.dto';
import { RequiredEntityData } from '@mikro-orm/core';
@Injectable()
export class RestaurantPaymentMethodService {
constructor(
private readonly restaurantPaymentMethodRepository: RestaurantPaymentMethodRepository,
private readonly em: EntityManager,
) {}
async create(
restId: string,
createRestaurantPaymentMethodDto: CreateRestaurantPaymentMethodDto,
): Promise<RestaurantPaymentMethod> {
// Check if restaurant exists
const restaurant = await this.em.findOne(Restaurant, { id: restId });
if (!restaurant) {
throw new NotFoundException(`Restaurant with ID ${restId} not found`);
}
// Check if payment method exists
const paymentMethod = await this.em.findOne(PaymentMethod, {
id: createRestaurantPaymentMethodDto.paymentMethodId,
});
if (!paymentMethod) {
throw new NotFoundException(
`PaymentMethod with ID ${createRestaurantPaymentMethodDto.paymentMethodId} not found`,
);
}
// Check if the relationship already exists
const existing = await this.restaurantPaymentMethodRepository.findOne({
restaurant: { id: restId },
paymentMethod: { id: createRestaurantPaymentMethodDto.paymentMethodId },
});
if (existing) {
throw new ConflictException('This payment method is already assigned to this restaurant');
}
const restaurantPaymentMethod = this.restaurantPaymentMethodRepository.create({
restaurant,
paymentMethod,
merchantId: createRestaurantPaymentMethodDto.merchantId,
isActive: createRestaurantPaymentMethodDto.isActive ?? true,
} as unknown as RequiredEntityData<RestaurantPaymentMethod>);
await this.em.persistAndFlush(restaurantPaymentMethod);
return restaurantPaymentMethod;
}
async findAll(): Promise<RestaurantPaymentMethod[]> {
return this.restaurantPaymentMethodRepository.findAll({ populate: ['restaurant', 'paymentMethod'] });
}
async findByRestaurant(restaurantId: string): Promise<RestaurantPaymentMethod[]> {
return this.restaurantPaymentMethodRepository.find(
{ restaurant: { id: restaurantId } },
{ populate: ['restaurant', 'paymentMethod'] },
);
}
async findByPaymentMethod(restPaymentMethodId: string): Promise<RestaurantPaymentMethod | null> {
return this.restaurantPaymentMethodRepository.findOneOrFail(
{ id: restPaymentMethodId },
{ populate: ['restaurant', 'paymentMethod'] },
);
}
async findOne(id: string): Promise<RestaurantPaymentMethod> {
const restaurantPaymentMethod = await this.restaurantPaymentMethodRepository.findOne(
{ id },
{ populate: ['restaurant', 'paymentMethod'] },
);
if (!restaurantPaymentMethod) {
throw new NotFoundException(`RestaurantPaymentMethod with ID ${id} not found`);
}
return restaurantPaymentMethod;
}
async update(
id: string,
updateRestaurantPaymentMethodDto: UpdateRestaurantPaymentMethodDto,
): Promise<RestaurantPaymentMethod> {
const restaurantPaymentMethod = await this.findOne(id);
if (updateRestaurantPaymentMethodDto.paymentMethodId) {
const paymentMethod = await this.em.findOne(PaymentMethod, {
id: updateRestaurantPaymentMethodDto.paymentMethodId,
});
if (!paymentMethod) {
throw new NotFoundException(
`PaymentMethod with ID ${updateRestaurantPaymentMethodDto.paymentMethodId} not found`,
);
}
// Check if the new relationship already exists
const existing = await this.restaurantPaymentMethodRepository.findOne({
restaurant: { id: restaurantPaymentMethod.restaurant.id },
paymentMethod: { id: updateRestaurantPaymentMethodDto.paymentMethodId },
});
if (existing && existing.id !== id) {
throw new ConflictException('This payment method is already assigned to this restaurant');
}
restaurantPaymentMethod.paymentMethod = paymentMethod;
}
// Update merchantId and isActive if provided
if (updateRestaurantPaymentMethodDto.merchantId !== undefined) {
restaurantPaymentMethod.merchantId = updateRestaurantPaymentMethodDto.merchantId;
}
if (updateRestaurantPaymentMethodDto.isActive !== undefined) {
restaurantPaymentMethod.isActive = updateRestaurantPaymentMethodDto.isActive;
}
await this.em.flush();
return restaurantPaymentMethod;
}
async remove(id: string): Promise<RestaurantPaymentMethod> {
const restaurantPaymentMethod = await this.findOne(id);
await this.em.removeAndFlush(restaurantPaymentMethod);
return restaurantPaymentMethod;
}
}