fix subscription from orange

This commit is contained in:
Mamadou Khoussa [028918 DSI/DAC/DIF/DS] 2025-11-14 16:50:42 +00:00
parent 6ea3ece796
commit 0af15e26fc
7 changed files with 213 additions and 22 deletions

View File

@ -24,19 +24,13 @@ export interface RefundResponse{
} }
export interface SubscriptionParams{
}
export interface SubscriptionResponse{
}
export interface IOperatorAdapter { export interface IOperatorAdapter {
initializeAuth(params: AuthInitParams): Promise<AuthInitResponse>; initializeAuth(params: AuthInitParams): Promise<AuthInitResponse>;
validateAuth(params: AuthValidateParams): Promise<AuthValidateResponse>; validateAuth(params: AuthValidateParams): Promise<AuthValidateResponse>;
charge(params: ChargeParams): Promise<ChargeResponse>; charge(params: ChargeParams): Promise<ChargeResponse>;
refund(params: RefundParams): Promise<RefundResponse>; refund(params: RefundParams): Promise<RefundResponse>;
sendSms(params: SmsParams): Promise<SmsResponse>; sendSms(params: SmsParams): Promise<SmsResponse>;
createSubscription?( createSubscription(
params: SubscriptionParams, params: SubscriptionParams,
): Promise<SubscriptionResponse>; ): Promise<SubscriptionResponse>;
cancelSubscription?(subscriptionId: string): Promise<void>; cancelSubscription?(subscriptionId: string): Promise<void>;
@ -73,3 +67,24 @@ export interface ChargeResponse {
resourceURL: string; resourceURL: string;
currency: 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;
}

View File

@ -9,6 +9,8 @@ import {
AuthInitResponse, AuthInitResponse,
ChargeParams, ChargeParams,
ChargeResponse, ChargeResponse,
SubscriptionParams,
SubscriptionResponse,
} from './operator.adapter.interface'; } from './operator.adapter.interface';
import { OrangeTransformer } from '../transformers/orange.transformer'; import { OrangeTransformer } from '../transformers/orange.transformer';
import { import {
@ -161,6 +163,117 @@ export class OrangeAdapter implements IOperatorAdapter {
}; };
} }
async createSubscription(
params: SubscriptionParams,
): Promise<SubscriptionResponse> {
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<void> {
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<ChargeResponse> { async charge(params: ChargeParams): Promise<ChargeResponse> {
this.logger.debug( this.logger.debug(
`[orange adapter charge ]: ${JSON.stringify(params, null, 2)}`, `[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'); throw new Error('Refund not implemented for Orange');
} }
async sendSms(params: any): Promise<any> { async sendSms(params: any): Promise<any> {
const smsRequest = { const smsRequest = {
outboundSMSMessageRequest: { outboundSMSMessageRequest: {

View File

@ -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 = { const statusMap = {
Completed: 'SUCCESS',
Charged: 'SUCCESS', Charged: 'SUCCESS',
Failed: 'FAILED', Failed: 'FAILED',
Pending: 'PENDING', Pending: 'PENDING',

View File

@ -9,6 +9,14 @@ import { PaymentType, TransactionStatus } from 'generated/prisma';
@Injectable() @Injectable()
export class PaymentsService { export class PaymentsService {
private readonly logger = new Logger(PaymentsService.name); private readonly logger = new Logger(PaymentsService.name);
constructor(
private readonly operatorsService: OperatorsService,
private readonly prisma: PrismaService,
private readonly eventEmitter: EventEmitter2,
) {}
handleWebhook(arg0: { handleWebhook(arg0: {
partnerId: any; partnerId: any;
event: any; event: any;
@ -76,11 +84,7 @@ export class PaymentsService {
processPayment(paymentId: any): any { processPayment(paymentId: any): any {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
constructor(
private readonly operatorsService: OperatorsService,
private readonly prisma: PrismaService,
private readonly eventEmitter: EventEmitter2,
) {}
async createCharge(chargeDto: ChargeDto) { async createCharge(chargeDto: ChargeDto) {
/* Récupérer les informations de l'utilisateur /* Récupérer les informations de l'utilisateur

View File

@ -30,12 +30,14 @@ export class SubscriptionsController {
@ApiOperation({ summary: 'Create a new subscription' }) @ApiOperation({ summary: 'Create a new subscription' })
async create( async create(
@Headers('X-Merchant-ID') merchantId: string, @Headers('X-Merchant-ID') merchantId: string,
@Headers('X-COUNTRY') country: string,
@Headers('X-OPERATOR') operator: string,
@Request() req, @Body() dto: CreateSubscriptionDto) { @Request() req, @Body() dto: CreateSubscriptionDto) {
this.logger.log('Merchant ID from header:'+ merchantId); this.logger.log('Merchant ID from header:'+ merchantId);
this.logger.debug( this.logger.debug(
`[request to hub ]: ${JSON.stringify(dto, null, 2)}`, `[request to hub ]: ${JSON.stringify(dto, null, 2)}`,
) )
return this.subscriptionsService.create(merchantId, dto); return this.subscriptionsService.create(merchantId, dto,country,operator);
} }
@Get('/') @Get('/')

View File

@ -8,6 +8,7 @@ import { SubscriptionProcessor } from './processors/subscription.processor';
import { PrismaService } from '../../shared/services/prisma.service'; import { PrismaService } from '../../shared/services/prisma.service';
import { PaymentsModule } from '../payments/payments.module'; import { PaymentsModule } from '../payments/payments.module';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { OperatorsModule } from '../operators/operators.module';
@Module({ @Module({
imports: [ imports: [
@ -18,6 +19,7 @@ import { PaymentsModule } from '../payments/payments.module';
BullModule.registerQueue({ BullModule.registerQueue({
name: 'billing', name: 'billing',
}), }),
OperatorsModule
// PaymentsModule, // PaymentsModule,
], ],
controllers: [SubscriptionsController], controllers: [SubscriptionsController],

View File

@ -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 { InjectQueue } from '@nestjs/bull';
import bull from 'bull'; import bull from 'bull';
import { PrismaService } from '../../shared/services/prisma.service'; import { PrismaService } from '../../shared/services/prisma.service';
import { PaymentsService } from '../payments/payments.service'; import { PaymentsService } from '../payments/payments.service';
import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto'; import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto';
import { Subscription } from 'generated/prisma'; import { Subscription } from 'generated/prisma';
import { OperatorsService } from '../operators/operators.service';
//import { SubscriptionStatus } from '@prisma/client'; //import { SubscriptionStatus } from '@prisma/client';
//import { SubscriptionStatus, Prisma } from '@prisma/client'; //import { SubscriptionStatus, Prisma } from '@prisma/client';
@Injectable() @Injectable()
export class SubscriptionsService { 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<any> { async get(id: number):Promise<any> {
const service = await this.prisma.subscription.findUnique({ const service = await this.prisma.subscription.findUnique({
where: { id }, where: { id },
@ -50,13 +61,9 @@ export class SubscriptionsService {
getInvoices(id: string, partnerId: any) { getInvoices(id: string, partnerId: any) {
throw new Error('Method not implemented.'); 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 /* todo Vérifier l'utilisateur
const user = await this.prisma.user.findFirst({ 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 // Vérifier s'il n'y a pas déjà une subscription active
const existingSubscription = await this.prisma.subscription.findFirst({ const existingSubscription = await this.prisma.subscription.findFirst({
where: { where: {
@ -96,12 +105,36 @@ export class SubscriptionsService {
throw new BadRequestException('User already has an active subscription for this plan'); 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 // Créer la subscription
const subscription = await this.prisma.subscription.create({ const subscription = await this.prisma.subscription.create({
data: { data: {
customerId: 1, //user.id, todo customerId: 1, //user.id, todo
externalReference: result.subscriptionId,
merchantPartnerId: 4,// todo , parseInt(partnerId), merchantPartnerId: 4,// todo , parseInt(partnerId),
token: dto.userToken, token: dto.userToken,
planId: dto.planId, planId: dto.planId,
@ -109,7 +142,11 @@ export class SubscriptionsService {
periodicity: "Daily", periodicity: "Daily",
amount: 20, amount: 20,
currency: "XOF", 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(), startDate: new Date(),
failureCount: 0, failureCount: 0,
nextPaymentDate: new Date(), // todo À ajuster selon la périodicité nextPaymentDate: new Date(), // todo À ajuster selon la périodicité