import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { OperatorsService } from '../operators/operators.service'; import { PrismaService } from '../../shared/services/prisma.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { ChargeDto } from './dto/payment.dto'; import { RefundDto } from './dto/payment.dto'; import { PaymentType, TransactionStatus } from 'generated/prisma'; @Injectable() export class PaymentsService { private readonly logger = new Logger(PaymentsService.name); constructor( private readonly operatorsService: OperatorsService, private readonly prisma: PrismaService, private readonly eventEmitter: EventEmitter2, ) {} handleWebhook(arg0: { partnerId: any; event: any; payload: any; signature: any; }) { throw new Error('Method not implemented.'); } async getPaymentByReference(reference: string) { const plan = await this.prisma.payment.findFirst({ where: { reference: reference }, }); return plan } async getPayment(id: number) { const data = await this.prisma.payment.findUnique({ where: { id }, }); return data; } async findAllByMerchant(merchantId: number): Promise { // Check if merchant exists return this.prisma.payment.findMany({ where: { merchantPartnerId: merchantId }, orderBy: { createdAt: 'desc', }, }); } async findAllByMerchantSubscription(merchantId: number, subscriptionId: number): Promise { // Check if merchant exists return this.prisma.payment.findMany({ where: { merchantPartnerId: merchantId, subscriptionId: subscriptionId }, orderBy: { createdAt: 'desc', }, }); } async findAll(): Promise { // Check if merchant exists return this.prisma.payment.findMany({ // where: { merchantPartnerId: merchantId }, orderBy: { createdAt: 'desc', }, }); } refundPayment(paymentId: string, partnerId: any, refundDto: RefundDto) { throw new Error('Method not implemented.'); } retryPayment(paymentId: any, attempt: any) { throw new Error('Method not implemented.'); } processPayment(paymentId: any): any { throw new Error('Method not implemented.'); } async createCharge(chargeDto: ChargeDto) { /* Récupérer les informations de l'utilisateur const user = await this.prisma.user.findUnique({ where: { userToken: chargeDto.userToken }, // include: { operator: true }, }); if (!user) { throw new BadRequestException('Invalid user token'); } */ // Créer la transaction dans la base const payment = await this.prisma.payment.create({ data: { subscriptionId: chargeDto.subscriptionId, merchantPartnerId: chargeDto.partnerId, // À remplacer par le bon partnerId customerId: 1, // todo À remplacer par user.id amount: chargeDto.amount, currency: chargeDto.currency, type: PaymentType.MM, reference: chargeDto.reference || this.generateReference(), //description: chargeDto.description, //reference: chargeDto.reference || this.generateReference(), status: TransactionStatus.PENDING, metadata: chargeDto.metadata, }, }); try { // Router vers le bon opérateur this.logger.debug( `[getting adaptator for ]: ${chargeDto.operator}_${chargeDto.country} `, ); const adapter = this.operatorsService.getAdapter( chargeDto.operator, chargeDto.country, ); this.logger.debug( `Processing payment ${payment.id} through operator adapter ${adapter.constructor.name}`, ); const chargeParams = { userToken: chargeDto.userToken, userAlias: chargeDto.userToken, //todo make alias in contrat amount: chargeDto.amount, currency: chargeDto.currency, description: chargeDto.description, subscriptionId: chargeDto.subscriptionId, reference: chargeDto.reference + '', //todo make reference in contrat }; const result = await adapter.charge(chargeParams); this.logger.debug( `result frm adaptaor ${result} for payment ${payment.id}`, ); // Mettre à jour le paiement const updatedPayment = await this.prisma.payment.update({ where: { id: payment.id }, data: { status: result.status === 'SUCCESS' ? TransactionStatus.SUCCESS : TransactionStatus.FAILED, externalReference: result.operatorReference, link: result.resourceURL, completedAt: new Date(), }, }); // Émettre un événement this.eventEmitter.emit('payment.completed', { payment: updatedPayment, operator: 'user.operator.code', }); // Appeler le callback du partenaire si fourni if (chargeDto.callbackUrl) { await this.notifyPartner(chargeDto.callbackUrl, updatedPayment); } return updatedPayment; } catch (error) { this.logger.debug( `error ${error.message} processing payment ${payment.id}`, ); // En cas d'erreur, marquer comme échoué const resultFinal = await this.prisma.payment.update({ where: { id: payment.id }, data: { status: TransactionStatus.FAILED, failureReason: error.message, }, }); return { ...resultFinal }; } } private generateReference(): string { return `PAY-${Date.now()}-${Math.random().toString(36).substring(7)}`; } private async notifyPartner(callbackUrl: string, payment: any) { // Implémenter la notification webhook // Utiliser Bull Queue pour gérer les retries } // Ajouter ces méthodes dans PaymentsService async listPayments(filters: any) { const where: any = { partnerId: filters.partnerId, }; if (filters.status) { where.status = filters.status; } if (filters.userId) { where.userId = filters.userId; } if (filters.subscriptionId) { where.subscriptionId = filters.subscriptionId; } if (filters.startDate || filters.endDate) { where.createdAt = {}; if (filters.startDate) { where.createdAt.gte = new Date(filters.startDate); } if (filters.endDate) { where.createdAt.lte = new Date(filters.endDate); } } const page = filters.page || 1; const limit = filters.limit || 20; const skip = (page - 1) * limit; const [payments, total] = await Promise.all([ this.prisma.payment.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, }), this.prisma.payment.count({ where }), ]); return { data: payments, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } async getStatistics(params: { partnerId: string; period: string; startDate?: Date; endDate?: Date; }) { const { partnerId, period, startDate, endDate } = params; const where: any = { partnerId }; if (startDate || endDate) { where.createdAt = {}; if (startDate) where.createdAt.gte = startDate; if (endDate) where.createdAt.lte = endDate; } const [ totalPayments, successfulPayments, failedPayments, totalRevenue, avgPaymentAmount, ] = await Promise.all([ this.prisma.payment.count({ where }), this.prisma.payment.count({ where: { ...where, status: 'SUCCESS' } }), this.prisma.payment.count({ where: { ...where, status: 'FAILED' } }), this.prisma.payment.aggregate({ where: { ...where, status: 'SUCCESS' }, _sum: { amount: true }, }), this.prisma.payment.aggregate({ where: { ...where, status: 'SUCCESS' }, _avg: { amount: true }, }), ]); const successRate = totalPayments > 0 ? (successfulPayments / totalPayments) * 100 : 0; return { totalPayments, successfulPayments, failedPayments, successRate: Math.round(successRate * 100) / 100, totalRevenue: totalRevenue._sum.amount || 0, avgPaymentAmount: avgPaymentAmount._avg.amount || 0, period, startDate, endDate, }; } async validatePayment(params: any) { // Valider le user token const user = await this.prisma.user.findUnique({ where: { userToken: params.userToken }, }); if (!user) { return { valid: false, error: 'Invalid user token', }; } // Vérifier les limites const todayPayments = await this.prisma.payment.count({ where: { customerId: 1, // todo À remplacer par user.id status: 'SUCCESS', createdAt: { gte: new Date(new Date().setHours(0, 0, 0, 0)), }, }, }); if (todayPayments >= 10) { return { valid: false, error: 'Daily payment limit reached', }; } return { valid: true, user: { id: user.id, msisdn: user.msisdn, country: user.country, }, }; } }