From 0af15e26fc62ceb21451c14f4a5460c4710eda4e Mon Sep 17 00:00:00 2001 From: "Mamadou Khoussa [028918 DSI/DAC/DIF/DS]" Date: Fri, 14 Nov 2025 16:50:42 +0000 Subject: [PATCH] fix subscription from orange --- .../adapters/operator.adapter.interface.ts | 29 +++-- .../operators/adapters/orange.adapter.ts | 115 ++++++++++++++++++ .../transformers/orange.transformer.ts | 18 ++- src/modules/payments/payments.service.ts | 14 ++- .../subscriptions/subscriptions.controller.ts | 4 +- .../subscriptions/subscriptions.module.ts | 2 + .../subscriptions/subscriptions.service.ts | 53 ++++++-- 7 files changed, 213 insertions(+), 22 deletions(-) diff --git a/src/modules/operators/adapters/operator.adapter.interface.ts b/src/modules/operators/adapters/operator.adapter.interface.ts index 76cf70b..765afa7 100644 --- a/src/modules/operators/adapters/operator.adapter.interface.ts +++ b/src/modules/operators/adapters/operator.adapter.interface.ts @@ -24,19 +24,13 @@ export interface RefundResponse{ } -export interface SubscriptionParams{ - -} -export interface SubscriptionResponse{ - -} export interface IOperatorAdapter { initializeAuth(params: AuthInitParams): Promise; validateAuth(params: AuthValidateParams): Promise; charge(params: ChargeParams): Promise; refund(params: RefundParams): Promise; sendSms(params: SmsParams): Promise; - createSubscription?( + createSubscription( params: SubscriptionParams, ): Promise; cancelSubscription?(subscriptionId: string): Promise; @@ -73,3 +67,24 @@ export interface ChargeResponse { resourceURL: string; currency: string; } + + +export interface SubscriptionParams { + merchantId: any; + periodicity: any; + userToken: string; + userAlias: string; + amount: number; + currency: string; + description: string; + productId: string; +} + +export interface SubscriptionResponse { + subscriptionId: string; + 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 eaecfaa..a9dbcaf 100644 --- a/src/modules/operators/adapters/orange.adapter.ts +++ b/src/modules/operators/adapters/orange.adapter.ts @@ -9,6 +9,8 @@ import { AuthInitResponse, ChargeParams, ChargeResponse, + SubscriptionParams, + SubscriptionResponse, } from './operator.adapter.interface'; import { OrangeTransformer } from '../transformers/orange.transformer'; import { @@ -161,6 +163,117 @@ export class OrangeAdapter implements IOperatorAdapter { }; } + async createSubscription( + params: SubscriptionParams, +): Promise { + this.logger.debug( + `[orange adapter createSubscription]: ${JSON.stringify(params, null, 2)}`, + ); + const hubRequest = { + note: { + "text": "partner data" + }, + relatedPublicKey: { + "id": "PDKSUB-200-KzIxNnh4eHh4eC1TRU4tMTc1ODY1MjI5MjMwMg==", + "name": "ISE2" + }, + relatedParty: [ + { + "id": "{{serviceId)}}", + "name": " DIGITALAFRIQUETELECOM ", + "role": "partner" + }, + { + "id": `${params.merchantId}`, + "name": "{{onBehalfOf)}}", + "role": "retailer" + } + ], + orderItem: { + "action": "add", + "state": "Completed", + "product": { + "id": `${params.productId}}`, + "href": "antifraudId", + "productCharacteristic": [ + { + "name": "taxAmount", + "value": "0" + }, + { + "name": "amount", + "value": `${params.amount}` + }, + { + "name": "currency",//ISO 4217 see Annexes + "value": `${params.currency}` + }, + { + "name": "periodicity",//86400 (daily), 604800 (weekly), 0 (monthly) only those values will be accepted + "value": `${params.periodicity}` + }, + { + "name": "startDate",//YYYY-MM-DD + "value": "2021-08-16" + }, + { + "name": "country", + "value": "COD" + }, + { + "name": "language",//ISO 639-1 see Annexes + "value": "fr" + }, + { + "name": "mode", + "value": "hybrid" + } + ] + } + }, + + }; + + const token = await this.getAccessToken(); + + this.logger.debug( + `[requesting subscription to]: ${this.config.baseUrl}/payment/mea/v1/digipay_sub/productOrder`, + ); + + this.logger.debug(`[requesting token]: ${token}`); + + const response = await firstValueFrom( + this.httpService.post( + `${this.config.baseUrl}/payment/mea/v1/digipay_sub/productOrder`, + hubRequest, + { + headers: { + Authorization: `Bearer ${token}`, + 'X-Orange-ISE2': params.userToken, + 'X-Orange-MCO': 'orange', + 'Content-Type': 'application/json', + }, + }, + ), + ); + + this.logger.debug( + `[response from orange subscription]: ${JSON.stringify(response.data, null, 2)}`, + ); + + return this.transformer.transformSubscriptionResponse(response.data); +} + +async cancelSubscription(subscriptionId: string): Promise { + this.logger.debug( + `[orange adapter cancelSubscription]: ${subscriptionId}`, + ); + + // Implémentation de l'annulation d'abonnement + // Cela dépend de l'API Orange - à adapter selon la documentation + throw new Error('Cancel subscription not implemented for Orange'); +} + async charge(params: ChargeParams): Promise { this.logger.debug( `[orange adapter charge ]: ${JSON.stringify(params, null, 2)}`, @@ -221,6 +334,8 @@ export class OrangeAdapter implements IOperatorAdapter { throw new Error('Refund not implemented for Orange'); } + + async sendSms(params: any): Promise { const smsRequest = { outboundSMSMessageRequest: { diff --git a/src/modules/operators/transformers/orange.transformer.ts b/src/modules/operators/transformers/orange.transformer.ts index e04c821..538d61c 100644 --- a/src/modules/operators/transformers/orange.transformer.ts +++ b/src/modules/operators/transformers/orange.transformer.ts @@ -20,8 +20,24 @@ export class OrangeTransformer { }; } - private mapStatus(orangeStatus: string): string { + + transformSubscriptionResponse(orangeResponse: any): any { + return { + subscriptionId: orangeResponse.id, + status: this.mapStatus( + orangeResponse.state, + ), + operatorReference: orangeResponse.amountTransaction?.serverReferenceCode, + amount: parseFloat( + orangeResponse.amountTransaction?.paymentAmount?.totalAmountCharged, + ), + createdAt: new Date(), + }; + } + + private mapStatus(orangeStatus: string): string {//todo make exaustifs const statusMap = { + Completed: 'SUCCESS', Charged: 'SUCCESS', Failed: 'FAILED', Pending: 'PENDING', diff --git a/src/modules/payments/payments.service.ts b/src/modules/payments/payments.service.ts index 0480199..ae64a29 100644 --- a/src/modules/payments/payments.service.ts +++ b/src/modules/payments/payments.service.ts @@ -9,6 +9,14 @@ 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; @@ -76,11 +84,7 @@ export class PaymentsService { processPayment(paymentId: any): any { throw new Error('Method not implemented.'); } - constructor( - private readonly operatorsService: OperatorsService, - private readonly prisma: PrismaService, - private readonly eventEmitter: EventEmitter2, - ) {} + async createCharge(chargeDto: ChargeDto) { /* Récupérer les informations de l'utilisateur diff --git a/src/modules/subscriptions/subscriptions.controller.ts b/src/modules/subscriptions/subscriptions.controller.ts index 0148808..8b90a5b 100644 --- a/src/modules/subscriptions/subscriptions.controller.ts +++ b/src/modules/subscriptions/subscriptions.controller.ts @@ -30,12 +30,14 @@ export class SubscriptionsController { @ApiOperation({ summary: 'Create a new subscription' }) async create( @Headers('X-Merchant-ID') merchantId: string, + @Headers('X-COUNTRY') country: string, + @Headers('X-OPERATOR') operator: string, @Request() req, @Body() dto: CreateSubscriptionDto) { this.logger.log('Merchant ID from header:'+ merchantId); this.logger.debug( `[request to hub ]: ${JSON.stringify(dto, null, 2)}`, ) - return this.subscriptionsService.create(merchantId, dto); + return this.subscriptionsService.create(merchantId, dto,country,operator); } @Get('/') diff --git a/src/modules/subscriptions/subscriptions.module.ts b/src/modules/subscriptions/subscriptions.module.ts index c59387d..b2f7a71 100644 --- a/src/modules/subscriptions/subscriptions.module.ts +++ b/src/modules/subscriptions/subscriptions.module.ts @@ -8,6 +8,7 @@ import { SubscriptionProcessor } from './processors/subscription.processor'; import { PrismaService } from '../../shared/services/prisma.service'; import { PaymentsModule } from '../payments/payments.module'; import { HttpModule } from '@nestjs/axios'; +import { OperatorsModule } from '../operators/operators.module'; @Module({ imports: [ @@ -18,6 +19,7 @@ import { PaymentsModule } from '../payments/payments.module'; BullModule.registerQueue({ name: 'billing', }), + OperatorsModule // PaymentsModule, ], controllers: [SubscriptionsController], diff --git a/src/modules/subscriptions/subscriptions.service.ts b/src/modules/subscriptions/subscriptions.service.ts index 7d35a44..dcc11b3 100644 --- a/src/modules/subscriptions/subscriptions.service.ts +++ b/src/modules/subscriptions/subscriptions.service.ts @@ -1,15 +1,26 @@ -import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; +import { Injectable, BadRequestException, NotFoundException, Logger } from '@nestjs/common'; import { InjectQueue } from '@nestjs/bull'; import bull from 'bull'; import { PrismaService } from '../../shared/services/prisma.service'; import { PaymentsService } from '../payments/payments.service'; import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto'; import { Subscription } from 'generated/prisma'; +import { OperatorsService } from '../operators/operators.service'; //import { SubscriptionStatus } from '@prisma/client'; //import { SubscriptionStatus, Prisma } from '@prisma/client'; @Injectable() export class SubscriptionsService { + private readonly logger = new Logger(SubscriptionsService.name); + + + constructor( + private readonly prisma: PrismaService, + private readonly operatorsService: OperatorsService, + @InjectQueue('subscriptions') private subscriptionQueue: bull.Queue, + @InjectQueue('billing') private billingQueue: bull.Queue, + ) {} + async get(id: number):Promise { const service = await this.prisma.subscription.findUnique({ where: { id }, @@ -50,13 +61,9 @@ export class SubscriptionsService { getInvoices(id: string, partnerId: any) { throw new Error('Method not implemented.'); } - constructor( - private readonly prisma: PrismaService, - @InjectQueue('subscriptions') private subscriptionQueue: bull.Queue, - @InjectQueue('billing') private billingQueue: bull.Queue, - ) {} + - async create(partnerId: string, dto: CreateSubscriptionDto) { + async create(partnerId: string, dto: CreateSubscriptionDto, country:string,operator:string) { /* todo Vérifier l'utilisateur const user = await this.prisma.user.findFirst({ @@ -82,6 +89,8 @@ export class SubscriptionsService { } */ + + // Vérifier s'il n'y a pas déjà une subscription active const existingSubscription = await this.prisma.subscription.findFirst({ where: { @@ -96,12 +105,36 @@ export class SubscriptionsService { throw new BadRequestException('User already has an active subscription for this plan'); } + const adapter = this.operatorsService.getAdapter( + operator, + country, + + ); + + const subscriptionParams = { + userToken: dto.userToken, + userAlias: dto.userToken, //todo make alias in contrat + amount: 200,//plan.amount,todo + currency: 'XOF',//plan.currency,todo + description: 'dto.description',//plan.description,todo + productId: dto.planId +'', + merchantId: partnerId, + periodicity: '86400', // todo 86400 (daily), 604800 (weekly), 0 (monthly) only those values will be accepted + }; + + const result = await adapter.createSubscription(subscriptionParams); + + this.logger.debug( + `result from adapter ${JSON.stringify(result, null, 2)} for subscription creation`, + ); + // Créer la subscription const subscription = await this.prisma.subscription.create({ data: { customerId: 1, //user.id, todo + externalReference: result.subscriptionId, merchantPartnerId: 4,// todo , parseInt(partnerId), token: dto.userToken, planId: dto.planId, @@ -109,7 +142,11 @@ export class SubscriptionsService { periodicity: "Daily", amount: 20, currency: "XOF", - status: 'ACTIVE', + status: 'ACTIVE',//todo mapping result.status 'SUCCESS' ? 'ACTIVE' : 'PENDING', + //currentPeriodStart: new Date(), + //currentPeriodEnd: new Date(), // todo À ajuster selon la périodicité + // nextBillingDate: new Date(), // todo À ajuster selon la périodicité + //renewalCount: 0, startDate: new Date(), failureCount: 0, nextPaymentDate: new Date(), // todo À ajuster selon la périodicité