347 lines
9.2 KiB
TypeScript
347 lines
9.2 KiB
TypeScript
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<any[]> {
|
|
// Check if merchant exists
|
|
|
|
return this.prisma.payment.findMany({
|
|
where: { merchantPartnerId: merchantId },
|
|
|
|
orderBy: {
|
|
createdAt: 'desc',
|
|
},
|
|
});
|
|
}
|
|
|
|
async findAllByMerchantSubscription(merchantId: number, subscriptionId: number): Promise<any[]> {
|
|
// Check if merchant exists
|
|
|
|
return this.prisma.payment.findMany({
|
|
where: { merchantPartnerId: merchantId, subscriptionId: subscriptionId },
|
|
|
|
orderBy: {
|
|
createdAt: 'desc',
|
|
},
|
|
});
|
|
}
|
|
|
|
async findAll(): Promise<any[]> {
|
|
// 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,
|
|
},
|
|
};
|
|
}
|
|
}
|