From 6ea3ece796733985f7686356655d10696a945489 Mon Sep 17 00:00:00 2001 From: "Mamadou Khoussa [028918 DSI/DAC/DIF/DS]" Date: Fri, 14 Nov 2025 13:27:42 +0000 Subject: [PATCH] infis on payment and subs --- .../20251114124248_init/migration.sql | 2 + .../20251114125457_init/migration.sql | 2 + .../20251114130651_init/migration.sql | 2 + prisma/schema.prisma | 3 + src/config/operators.config.ts | 8 +- .../adapters/operator.adapter.interface.ts | 1 + .../operators/adapters/orange.adapter.ts | 23 +- .../transformers/orange.transformer.ts | 17 +- src/modules/payments/dto/payment.dto.ts | 9 +- src/modules/payments/payments.controller.ts | 32 +- src/modules/payments/payments.service.ts | 380 ++++++++++-------- 11 files changed, 287 insertions(+), 192 deletions(-) create mode 100644 prisma/migrations/20251114124248_init/migration.sql create mode 100644 prisma/migrations/20251114125457_init/migration.sql create mode 100644 prisma/migrations/20251114130651_init/migration.sql diff --git a/prisma/migrations/20251114124248_init/migration.sql b/prisma/migrations/20251114124248_init/migration.sql new file mode 100644 index 0000000..194b899 --- /dev/null +++ b/prisma/migrations/20251114124248_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "payments" ADD COLUMN "link" TEXT; diff --git a/prisma/migrations/20251114125457_init/migration.sql b/prisma/migrations/20251114125457_init/migration.sql new file mode 100644 index 0000000..eb782a4 --- /dev/null +++ b/prisma/migrations/20251114125457_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "payments" ADD COLUMN "subscriptionId" INTEGER; diff --git a/prisma/migrations/20251114130651_init/migration.sql b/prisma/migrations/20251114130651_init/migration.sql new file mode 100644 index 0000000..b8fe00d --- /dev/null +++ b/prisma/migrations/20251114130651_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "payments" ADD COLUMN "reference" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2e94103..c78d3c8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -93,6 +93,7 @@ model ReversementRequest { model Payment { id Int @id @default(autoincrement()) externalReference String? + reference String? type PaymentType status TransactionStatus merchantPartnerId Int @@ -103,7 +104,9 @@ model Payment { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt customerId Int + subscriptionId Int? metadata Json? + link String? reversementRequests ReversementRequest[] diff --git a/src/config/operators.config.ts b/src/config/operators.config.ts index 628e358..3caa98b 100644 --- a/src/config/operators.config.ts +++ b/src/config/operators.config.ts @@ -3,7 +3,7 @@ import { registerAs } from '@nestjs/config'; export default registerAs('operators', () => ({ ORANGE_CIV: { name: 'Orange Côte d Ivoire', - baseUrl: process.env.ORANGE_CIV_BASE_URL || 'https://api.bizao.com', + baseUrl: process.env.ORANGE_CIV_BASE_URL || 'https://api.DCB-HUB.com', authType: 'OTP', endpoints: { auth: { @@ -20,7 +20,7 @@ export default registerAs('operators', () => ({ }, }, headers: { - 'X-OAPI-Application-Id': 'BIZAO', + 'X-OAPI-Application-Id': 'DCB-HUB', 'X-Orange-MCO': 'OCI', }, transformers: { @@ -30,7 +30,7 @@ export default registerAs('operators', () => ({ }, ORANGE_SEN: { name: 'Orange Sénégal', - baseUrl: process.env.ORANGE_SEN_BASE_URL || 'https://api.bizao.com', + baseUrl: process.env.ORANGE_SEN_BASE_URL || 'https://api.DCB-HUB.com', authType: 'OTP', endpoints: { auth: { @@ -47,7 +47,7 @@ export default registerAs('operators', () => ({ }, }, headers: { - 'X-OAPI-Application-Id': 'BIZAO', + 'X-OAPI-Application-Id': 'DCB-HUB', 'X-Orange-MCO': 'OSN', }, transformers: { diff --git a/src/modules/operators/adapters/operator.adapter.interface.ts b/src/modules/operators/adapters/operator.adapter.interface.ts index c43bddb..76cf70b 100644 --- a/src/modules/operators/adapters/operator.adapter.interface.ts +++ b/src/modules/operators/adapters/operator.adapter.interface.ts @@ -70,5 +70,6 @@ export interface ChargeResponse { status: 'SUCCESS' | 'FAILED' | 'PENDING'; operatorReference: string; amount: number; + resourceURL: string; currency: string; } diff --git a/src/modules/operators/adapters/orange.adapter.ts b/src/modules/operators/adapters/orange.adapter.ts index 956e542..eaecfaa 100644 --- a/src/modules/operators/adapters/orange.adapter.ts +++ b/src/modules/operators/adapters/orange.adapter.ts @@ -57,7 +57,7 @@ export class OrangeAdapter implements IOperatorAdapter { challenge: { method: 'OTP-SMS-AUTH', country: countryCode, - service: 'BIZAO', + service: 'DCB_HUB', partnerId: 'PDKSUB', inputs: [ { @@ -114,7 +114,7 @@ export class OrangeAdapter implements IOperatorAdapter { challenge: { method: 'OTP-SMS-AUTH', country: params.country, - service: 'BIZAO', + service: 'DCB_HUB', partnerId: 'PDKSUB', inputs: [ { @@ -178,7 +178,7 @@ export class OrangeAdapter implements IOperatorAdapter { chargingMetaData: { onBehalfOf: 'PaymentHub', //from config todo purchaseCategoryCode: 'Service', //todo from config - serviceId: 'BIZAO', + serviceId: 'DCB_HUB', }, }, transactionOperationStatus: 'Charged', @@ -188,12 +188,17 @@ export class OrangeAdapter implements IOperatorAdapter { }; const token = await this.getAccessToken(); this.logger.debug( - `[requesting to ]: ${this.config.baseUrl}/payment/v1/acr%3AOrangeAPIToken/transactions/amount`, + `[requesting to ]: ${this.config.baseUrl}/payment/mea/v1/acr%3AX-Orange-ISE2/transactions/amount`, + + ); + this.logger.debug( + `[requesting token ]: ${token} `, + ); const response = await firstValueFrom( this.httpService.post( - `${this.config.baseUrl}}/payment/v1/acr%3AOrangeAPIToken/transactions/amount`, + `${this.config.baseUrl}/payment/mea/v1/acr%3AX-Orange-ISE2/transactions/amount`, hubRequest, { headers: { @@ -238,11 +243,11 @@ export class OrangeAdapter implements IOperatorAdapter { { headers: { Authorization: `Bearer ${this.accessToken}`, - 'X-OAPI-Application-Id': 'BIZAO', - 'X-OAPI-Contact-Id': 'b2b-bizao-97b5878', + 'X-OAPI-Application-Id': 'DCB_HUB', + 'X-OAPI-Contact-Id': 'b2b-DCB_HUB-97b5878', 'X-OAPI-Resource-Type': 'SMS_OSM', - 'bizao-alias': params.userAlias, - 'bizao-token': params.userToken, + 'DCB_HUB-alias': params.userAlias, + 'DCB_HUB-token': params.userToken, 'X-Orange-MCO': this.getMCO(params.country), 'Content-Type': 'application/json', }, diff --git a/src/modules/operators/transformers/orange.transformer.ts b/src/modules/operators/transformers/orange.transformer.ts index 79e90d0..e04c821 100644 --- a/src/modules/operators/transformers/orange.transformer.ts +++ b/src/modules/operators/transformers/orange.transformer.ts @@ -2,29 +2,30 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class OrangeTransformer { - transformChargeResponse(bizaoResponse: any): any { + transformChargeResponse(orangeResponse: any): any { return { - paymentId: bizaoResponse.amountTransaction?.serverReferenceCode, + paymentId: orangeResponse.amountTransaction?.serverReferenceCode, status: this.mapStatus( - bizaoResponse.amountTransaction?.transactionOperationStatus, + orangeResponse.amountTransaction?.transactionOperationStatus, ), - operatorReference: bizaoResponse.amountTransaction?.serverReferenceCode, + operatorReference: orangeResponse.amountTransaction?.serverReferenceCode, amount: parseFloat( - bizaoResponse.amountTransaction?.paymentAmount?.totalAmountCharged, + orangeResponse.amountTransaction?.paymentAmount?.totalAmountCharged, ), + resourceURL: orangeResponse.amountTransaction?.resourceURL, currency: - bizaoResponse.amountTransaction?.paymentAmount?.chargingInformation + orangeResponse.amountTransaction?.paymentAmount?.chargingInformation ?.currency, createdAt: new Date(), }; } - private mapStatus(bizaoStatus: string): string { + private mapStatus(orangeStatus: string): string { const statusMap = { Charged: 'SUCCESS', Failed: 'FAILED', Pending: 'PENDING', }; - return statusMap[bizaoStatus] || 'PENDING'; + return statusMap[orangeStatus] || 'PENDING'; } } diff --git a/src/modules/payments/dto/payment.dto.ts b/src/modules/payments/dto/payment.dto.ts index 03ba750..a8eafe1 100644 --- a/src/modules/payments/dto/payment.dto.ts +++ b/src/modules/payments/dto/payment.dto.ts @@ -6,6 +6,7 @@ import { Min, IsEnum, IsDateString, + isNumber, } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; @@ -35,7 +36,7 @@ export class ChargeDto { @ApiProperty({ required: false, description: 'Subscription ID if recurring' }) @IsOptional() - @IsString() + @IsNumber() subscriptionId?: number; @ApiProperty({ required: false, description: 'Callback URL for notifications' }) @@ -64,7 +65,7 @@ export class RefundDto { @ApiProperty({ required: false, description: 'Amount to refund (partial refund)' }) @IsOptional() @IsNumber() - @Min(0) + @Min(1) amount?: number; @ApiProperty({ description: 'Reason for refund' }) @@ -89,8 +90,8 @@ export class PaymentQueryDto { @ApiProperty({ required: false }) @IsOptional() - @IsString() - subscriptionId?: string; + @IsNumber() + subscriptionId?: number; @ApiProperty({ required: false }) @IsOptional() diff --git a/src/modules/payments/payments.controller.ts b/src/modules/payments/payments.controller.ts index 25ad47a..6844ee6 100644 --- a/src/modules/payments/payments.controller.ts +++ b/src/modules/payments/payments.controller.ts @@ -89,7 +89,7 @@ export class PaymentsController { } @Get(':paymentId') - @UseGuards(JwtAuthGuard) + //@UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get payment details' }) @ApiResponse({ @@ -98,8 +98,9 @@ export class PaymentsController { type: PaymentResponseDto, }) @ApiResponse({ status: 404, description: 'Payment not found' }) - async getPayment(@Request() req, @Param('paymentId') paymentId: string) { - return this.paymentsService.getPayment(paymentId, req.user.partnerId); + async getPayment(@Request() req, @Param('paymentId') paymentId: number) { + console.log('Fetching payment with ID:', paymentId); + return this.paymentsService.getPayment(paymentId); } @@ -118,11 +119,32 @@ export class PaymentsController { @Param('reference') reference: string, ) { return this.paymentsService.getPaymentByReference( - reference, - req.user.partnerId, + reference ); } + + @Get('/') + @ApiOperation({ summary: 'Get payments list' }) + async getAll(@Request() req) { + return this.paymentsService.findAll(); + } + + @Get('merchant/:merchantId') + @ApiOperation({ summary: 'Get payments list by merchant' }) + async getAllByPaymentByMerchant(@Request() req, @Param('merchantId', ParseIntPipe) merchantId: number) { + return this.paymentsService.findAllByMerchant(merchantId); + } + + @Get('merchant/:merchantId/subscription/:subscriptionId') + @ApiOperation({ summary: 'Get payments list by merchant' }) + async getAllBySubscription(@Request() req, + @Param('merchantId', ParseIntPipe) merchantId: number, + @Param('subscriptionId', ParseIntPipe) subscriptionId: number) { + return this.paymentsService.findAllByMerchantSubscription(merchantId,subscriptionId); + } + + @Post(':paymentId/retry') // @UseGuards(JwtAuthGuard) @ApiBearerAuth() diff --git a/src/modules/payments/payments.service.ts b/src/modules/payments/payments.service.ts index bb66f05..0480199 100644 --- a/src/modules/payments/payments.service.ts +++ b/src/modules/payments/payments.service.ts @@ -2,29 +2,78 @@ 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 { 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); - handleWebhook(arg0: { partnerId: any; event: any; payload: any; signature: any; }) { - throw new Error('Method not implemented.'); + 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', + }, + }); } - getPaymentByReference(reference: string, partnerId: any) { - throw new Error('Method not implemented.'); + + async findAllByMerchantSubscription(merchantId: number, subscriptionId: number): Promise { + // Check if merchant exists + + return this.prisma.payment.findMany({ + where: { merchantPartnerId: merchantId, subscriptionId: subscriptionId }, + + orderBy: { + createdAt: 'desc', + }, + }); } - getPayment(paymentId: string, partnerId: any) { - throw new Error('Method not implemented.'); - } - refundPayment(paymentId: string, partnerId: any, refundDto: RefundDto) { - throw new Error('Method not implemented.'); + + 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{ + processPayment(paymentId: any): any { throw new Error('Method not implemented.'); } constructor( @@ -47,13 +96,14 @@ export class PaymentsService { // Créer la transaction dans la base const payment = await this.prisma.payment.create({ - data: { - merchantPartnerId:chargeDto.partnerId , // À remplacer par le bon partnerId + 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, @@ -64,28 +114,31 @@ export class PaymentsService { try { // Router vers le bon opérateur this.logger.debug( - `[getting adaptator for ]: ${chargeDto.operator}_${chargeDto.country} `) + `[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}`); + 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 + 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 + 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}`); - - + this.logger.debug( + `result frm adaptaor ${result} for payment ${payment.id}`, + ); // Mettre à jour le paiement const updatedPayment = await this.prisma.payment.update({ @@ -95,7 +148,8 @@ export class PaymentsService { result.status === 'SUCCESS' ? TransactionStatus.SUCCESS : TransactionStatus.FAILED, - //operatorReference: result.operatorReference, + externalReference: result.operatorReference, + link: result.resourceURL, completedAt: new Date(), }, }); @@ -113,8 +167,12 @@ export class PaymentsService { 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({ + const resultFinal = await this.prisma.payment.update({ where: { id: payment.id }, data: { status: TransactionStatus.FAILED, @@ -137,150 +195,148 @@ export class PaymentsService { // 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', + async listPayments(filters: any) { + const where: any = { + partnerId: filters.partnerId, }; - } - // 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 (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), }, - }, - }); - - if (todayPayments >= 10) { - return { - valid: false, - error: 'Daily payment limit reached', }; } - return { - valid: true, - user: { - id: user.id, - msisdn: user.msisdn, - country: user.country, - }, - }; -} + 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, + }, + }; + } }