From d8ad43a56aaf290f4a187ddb42ef741afd411bb1 Mon Sep 17 00:00:00 2001 From: "Mamadou Khoussa [028918 DSI/DAC/DIF/DS]" Date: Fri, 14 Nov 2025 12:09:56 +0000 Subject: [PATCH] payment object --- .../operators/adapters/orange.adapter.ts | 119 +++++++++++++++--- .../operators/adapters/orange.config.ts | 46 +++++++ src/modules/operators/operators.module.ts | 25 ++++ src/modules/operators/operators.service.ts | 9 +- src/modules/payments/dto/payment.dto.ts | 12 +- src/modules/payments/payments.controller.ts | 29 +++-- src/modules/payments/payments.module.ts | 4 + src/modules/payments/payments.service.ts | 30 +++-- .../subscriptions/subscriptions.controller.ts | 23 ++-- .../subscriptions/subscriptions.module.ts | 2 +- .../subscriptions/subscriptions.service.ts | 43 ++++++- 11 files changed, 283 insertions(+), 59 deletions(-) create mode 100644 src/modules/operators/adapters/orange.config.ts diff --git a/src/modules/operators/adapters/orange.adapter.ts b/src/modules/operators/adapters/orange.adapter.ts index 6fce7db..956e542 100644 --- a/src/modules/operators/adapters/orange.adapter.ts +++ b/src/modules/operators/adapters/orange.adapter.ts @@ -1,5 +1,6 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; +import axios, { AxiosInstance, AxiosError } from 'axios'; import { ConfigService } from '@nestjs/config'; import { firstValueFrom } from 'rxjs'; import { @@ -10,27 +11,49 @@ import { ChargeResponse, } from './operator.adapter.interface'; import { OrangeTransformer } from '../transformers/orange.transformer'; - +import { + DEFAULT_ORANGE_CONFIG, + COUNTRY_CODE_MAPPING, +} from './orange.config'; + +import type { OrangeConfig } from './orange.config'; @Injectable() export class OrangeAdapter implements IOperatorAdapter { + private readonly logger = new Logger(OrangeAdapter.name); + private config: OrangeConfig; private baseUrl: string; private accessToken: string; private transformer: OrangeTransformer; + private tokenExpiresAt: number = 0; + private axiosInstance: AxiosInstance; constructor( - private readonly httpService: HttpService, - private readonly configService: ConfigService, + private readonly httpService: HttpService, + @Inject('ORANGE_CONFIG')config: OrangeConfig, ) { - this.baseUrl = this.configService.get('ORANGE_API_URL') as string; - this.accessToken = this.configService.get('ORANGE_ACCESS_TOKEN') as string; + this.config = { ...DEFAULT_ORANGE_CONFIG, ...config } as OrangeConfig; + this.axiosInstance = axios.create({ + baseURL: this.config.baseUrl, + timeout: this.config.timeout, + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + }, + }); + this.axiosInstance.interceptors.response.use( + response => response, + error => this.handleError(error) + ); + this.transformer = new OrangeTransformer(); + } async initializeAuth(params: AuthInitParams): Promise { const countryCode = this.getCountryCode(params.country); - const bizaoRequest = { + const hubRequest = { challenge: { method: 'OTP-SMS-AUTH', country: countryCode, @@ -64,7 +87,7 @@ export class OrangeAdapter implements IOperatorAdapter { const response = await firstValueFrom( this.httpService.post( `${this.baseUrl}/challenge/v1/challenges`, - bizaoRequest, + hubRequest, { headers: { Authorization: `Bearer ${this.accessToken}`, @@ -87,7 +110,7 @@ export class OrangeAdapter implements IOperatorAdapter { } async validateAuth(params: any): Promise { - const bizaoRequest = { + const hubRequest = { challenge: { method: 'OTP-SMS-AUTH', country: params.country, @@ -113,7 +136,7 @@ export class OrangeAdapter implements IOperatorAdapter { const response = await firstValueFrom( this.httpService.post( `${this.baseUrl}/challenge/v1/challenges/${params.challengeId}`, - bizaoRequest, + hubRequest, { headers: { Authorization: `Bearer ${this.accessToken}`, @@ -139,7 +162,11 @@ export class OrangeAdapter implements IOperatorAdapter { } async charge(params: ChargeParams): Promise { - const bizaoRequest = { + this.logger.debug( + `[orange adapter charge ]: ${JSON.stringify(params, null, 2)}`, + ); + + const hubRequest = { amountTransaction: { endUserId: 'acr:OrangeAPIToken', paymentAmount: { @@ -149,31 +176,38 @@ export class OrangeAdapter implements IOperatorAdapter { description: params.description, }, chargingMetaData: { - onBehalfOf: 'PaymentHub', + onBehalfOf: 'PaymentHub', //from config todo + purchaseCategoryCode: 'Service', //todo from config serviceId: 'BIZAO', }, }, transactionOperationStatus: 'Charged', referenceCode: params.reference, - clientCorrelator: `${params.reference}-${Date.now()}`, + clientCorrelator: `${params.reference}-${Date.now()}`, //uniquely identifies this create charge request. }, }; + const token = await this.getAccessToken(); + this.logger.debug( + `[requesting to ]: ${this.config.baseUrl}/payment/v1/acr%3AOrangeAPIToken/transactions/amount`, + ); const response = await firstValueFrom( this.httpService.post( - `${this.baseUrl}/payment/v1/acr%3AOrangeAPIToken/transactions/amount`, - bizaoRequest, + `${this.config.baseUrl}}/payment/v1/acr%3AOrangeAPIToken/transactions/amount`, + hubRequest, { headers: { - Authorization: `Bearer ${this.accessToken}`, - 'bizao-token': params.userToken, - 'bizao-alias': params.userAlias, + Authorization: `Bearer ${token}`, + 'X-Orange-ISE2': params.userToken, + 'X-Orange-MCO': 'orange', //from country todo 'Content-Type': 'application/json', }, }, ), ); + this.logger.debug(`[response fromm orange ]: ${JSON.stringify(response.data, null, 2)}`,) + return this.transformer.transformChargeResponse(response.data); } @@ -257,4 +291,53 @@ export class OrangeAdapter implements IOperatorAdapter { }; return senderMap[country]; } + + private async getAccessToken(): Promise { + // Vérifier si le token est encore valide (avec une marge de 60 secondes) + if (this.accessToken && Date.now() < this.tokenExpiresAt - 60000) { + return this.accessToken; + } + + try { + const auth = Buffer.from( + `${this.config.clientId}:${this.config.clientSecret}`, + ).toString('base64'); + + //this.logger.debug( `request to get acces token , ${this.config.baseUrl}${this.config.tokenEndpoint}`) + + const response = await axios.post( + `${this.config.baseUrl}${this.config.tokenEndpoint}`, + 'grant_type=client_credentials', + { + headers: { + Authorization: `Basic ${auth}`, + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: '*/*', + }, + }, + ); + + this.accessToken = response.data.access_token; + const expiresIn = response.data.expires_in || 3600; + this.tokenExpiresAt = Date.now() + expiresIn * 1000; + + return this.accessToken as string; + } catch (error) { + throw new Error(`Failed to obtain Orange access token: ${error.message}`); + } + } + + + private handleError(error: AxiosError): never { + if (error.response) { + const data = error.response.data as any; + throw new Error( + `Orange API Error: ${data?.error?.message || error.message} (Code: ${data?.error?.code || error.response.status})` + ); + } else if (error.request) { + throw new Error(`No response from Orange API: ${error.message}`); + } else { + throw new Error(`Request error: ${error.message}`); + } + } } diff --git a/src/modules/operators/adapters/orange.config.ts b/src/modules/operators/adapters/orange.config.ts new file mode 100644 index 0000000..1289864 --- /dev/null +++ b/src/modules/operators/adapters/orange.config.ts @@ -0,0 +1,46 @@ +export interface OrangeConfig { + baseUrl: string; + partnerId: string; + clientId: string; + clientSecret: string; + defaultService: string; + defaultOtpLength: number; + defaultSenderName: string; + defaultOtpMessage: string; + tokenEndpoint: string; + challengeEndpoint: string; + timeout: number; +} + +export const DEFAULT_ORANGE_CONFIG: Partial = { + defaultOtpLength: 4, + defaultOtpMessage: 'To confirm your purchase please enter the code %OTP%', + tokenEndpoint: '/oauth/v3/token', + challengeEndpoint: '/challenge/v1/challenges', + timeout: 30000, // 30 secondes +}; + +/** + * Mapping des codes pays ISO vers les codes Orange + */ +export const COUNTRY_CODE_MAPPING: Record = { + 'SN': 'SEN', // Sénégal + 'CI': 'CIV', // Côte d'Ivoire + 'CM': 'CMR', // Cameroun + 'CD': 'COD', // RD Congo + 'BF': 'BFA', // Burkina Faso + 'TN': 'TUN', // Tunisie + 'ML': 'MLI', // Mali + 'GN': 'GIN', // Guinée + 'NE': 'NER', // Niger + 'MG': 'MDG', // Madagascar +}; + +/** + * Mapping des méthodes OTP génériques vers Orange + */ +export const OTP_METHOD_MAPPING: Record = { + 'SMS': 'OTP-SMS-AUTH', + 'USSD': 'OTP-USSD-AUTH', + 'IVR': 'OTP-IVR-AUTH', +}; \ No newline at end of file diff --git a/src/modules/operators/operators.module.ts b/src/modules/operators/operators.module.ts index 1731a48..1f255b9 100644 --- a/src/modules/operators/operators.module.ts +++ b/src/modules/operators/operators.module.ts @@ -8,6 +8,8 @@ import { MTNAdapter } from './adapters/mtn.adapter'; import { OrangeTransformer } from './transformers/orange.transformer'; import { MTNTransformer } from './transformers/mtn.transformer'; import { PrismaService } from '../../shared/services/prisma.service'; +import { OrangeConfig } from './adapters/orange.config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ imports: [ @@ -15,9 +17,32 @@ import { PrismaService } from '../../shared/services/prisma.service'; timeout: 30000, maxRedirects: 3, }), + ConfigModule + ], controllers: [OperatorsController], providers: [ + { + provide: 'ORANGE_CONFIG', + useFactory: (configService: ConfigService): OrangeConfig => ({ + baseUrl: configService.get('ORANGE_BASE_URL', 'https://webhook.site/69ce9344-f87b-421a-a494-c59eca7c54ce'), + //tokenUrl: configService.get('ORANGE_BASE_URL', 'https://webhook.site/69ce9344-f87b-421a-a494-c59eca7c54ce'), + partnerId: configService.get('ORANGE_PARTNER_ID', 'PDKSUB'), + clientId: configService.get('ORANGE_CLIENT_ID', 'admin'), + clientSecret: configService.get('ORANGE_CLIENT_SECRET', 'admin'), + defaultService: configService.get('ORANGE_DEFAULT_SERVICE', 'DCB_SERVICE'), + defaultOtpLength: configService.get('ORANGE_DEFAULT_OTP_LENGTH', 4), + defaultSenderName: configService.get('ORANGE_DEFAULT_SENDER_NAME', 'OTP'), + defaultOtpMessage: configService.get( + 'ORANGE_DEFAULT_OTP_MESSAGE', + 'To confirm your purchase please enter the code %OTP%' + ), + tokenEndpoint: '/oauth/v3/token', + challengeEndpoint: '/challenge/v1/challenges', + timeout: configService.get('ORANGE_TIMEOUT', 30000), + }), + inject: [ConfigService], + }, OperatorsService, OperatorAdapterFactory, OrangeAdapter, diff --git a/src/modules/operators/operators.service.ts b/src/modules/operators/operators.service.ts index c1fddd8..35e870f 100644 --- a/src/modules/operators/operators.service.ts +++ b/src/modules/operators/operators.service.ts @@ -1,10 +1,11 @@ -import { BadRequestException, NotFoundException } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { PrismaService } from "src/shared/services/prisma.service"; import { OperatorAdapterFactory } from "./adapters/operator-adapter.factory"; import { HttpService } from "@nestjs/axios"; import { firstValueFrom } from 'rxjs'; //todo tomaj +@Injectable() export class OperatorsService{ constructor( @@ -126,12 +127,6 @@ export class OperatorsService{ }; } - - - - - - private getCountryName(code: string): string { const countries = { diff --git a/src/modules/payments/dto/payment.dto.ts b/src/modules/payments/dto/payment.dto.ts index ba55cfc..03ba750 100644 --- a/src/modules/payments/dto/payment.dto.ts +++ b/src/modules/payments/dto/payment.dto.ts @@ -36,7 +36,7 @@ export class ChargeDto { @ApiProperty({ required: false, description: 'Subscription ID if recurring' }) @IsOptional() @IsString() - subscriptionId?: string; + subscriptionId?: number; @ApiProperty({ required: false, description: 'Callback URL for notifications' }) @IsOptional() @@ -49,7 +49,15 @@ export class ChargeDto { @ApiProperty({ required: false, description: 'partnerId ' }) @IsOptional() - partnerId: string; + partnerId: number; + + @ApiProperty({ required: false, description: 'country ' }) + @IsOptional() + country: string; + + @ApiProperty({ required: false, description: 'operator ' }) + @IsOptional() + operator: string; } export class RefundDto { diff --git a/src/modules/payments/payments.controller.ts b/src/modules/payments/payments.controller.ts index 549684c..25ad47a 100644 --- a/src/modules/payments/payments.controller.ts +++ b/src/modules/payments/payments.controller.ts @@ -10,6 +10,9 @@ import { HttpCode, HttpStatus, BadRequestException, + Headers, + Logger, + ParseIntPipe, } from '@nestjs/common'; import { ApiTags, @@ -32,10 +35,12 @@ import { ApiKeyGuard } from '../../common/guards/api-key.guard'; @ApiTags('payments') @Controller('payments') export class PaymentsController { + private readonly logger = new Logger(PaymentsController.name); + constructor(private readonly paymentsService: PaymentsService) {} @Post('charge') - @UseGuards(JwtAuthGuard) + //@UseGuards(JwtAuthGuard) @ApiBearerAuth() @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: 'Create a new charge' }) @@ -46,10 +51,18 @@ export class PaymentsController { }) @ApiResponse({ status: 400, description: 'Bad request' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) - async createCharge(@Request() req, @Body() chargeDto: ChargeDto) { + async createCharge( + @Headers('X-Merchant-ID') merchantId: string, + @Headers('X-COUNTRY') coutnry: string, + @Headers('X-OPERATOR') operator: string, + @Request() req, @Body() chargeDto: ChargeDto) { + this.logger.debug( + `[request charge to hub ]: ${JSON.stringify(chargeDto, null, 2)}`, + ) return this.paymentsService.createCharge({ - ...chargeDto, - partnerId: req.user.partnerId, + ...chargeDto, + country: coutnry, + operator: operator, }); } @@ -92,7 +105,7 @@ export class PaymentsController { @Get('reference/:reference') - @UseGuards(JwtAuthGuard) + //@UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get payment by reference' }) @ApiResponse({ @@ -111,7 +124,7 @@ export class PaymentsController { } @Post(':paymentId/retry') - @UseGuards(JwtAuthGuard) + // @UseGuards(JwtAuthGuard) @ApiBearerAuth() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Retry a failed payment' }) @@ -130,7 +143,7 @@ export class PaymentsController { @Post('validate') - @UseGuards(JwtAuthGuard) + //@UseGuards(JwtAuthGuard) @ApiBearerAuth() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Validate payment before processing' }) @@ -143,7 +156,7 @@ export class PaymentsController { // Webhook endpoints @Post('webhook/callback') - @UseGuards(ApiKeyGuard) + //@UseGuards(ApiKeyGuard) @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Webhook callback for payment updates' }) async handleWebhook(@Request() req, @Body() payload: any) { diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index f2c68b3..4696886 100644 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -6,6 +6,8 @@ import { PaymentProcessor } from './processors/payment.processor'; import { WebhookService } from './services/webhook.service'; import { PrismaService } from '../../shared/services/prisma.service'; import { OperatorsModule } from '../operators/operators.module'; +import { Subscription } from 'rxjs'; +import { SubscriptionsModule } from '../subscriptions/subscriptions.module'; @Module({ imports: [ @@ -16,6 +18,8 @@ import { OperatorsModule } from '../operators/operators.module'; name: 'webhooks', }), OperatorsModule, + SubscriptionsModule, + ], controllers: [PaymentsController], providers: [PaymentsService, PaymentProcessor, WebhookService, PrismaService], diff --git a/src/modules/payments/payments.service.ts b/src/modules/payments/payments.service.ts index a019f2e..bb66f05 100644 --- a/src/modules/payments/payments.service.ts +++ b/src/modules/payments/payments.service.ts @@ -1,4 +1,4 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; +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'; @@ -8,6 +8,7 @@ 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.'); } @@ -33,7 +34,7 @@ export class PaymentsService { ) {} async createCharge(chargeDto: ChargeDto) { - // Récupérer les informations de l'utilisateur + /* Récupérer les informations de l'utilisateur const user = await this.prisma.user.findUnique({ where: { userToken: chargeDto.userToken }, // include: { operator: true }, @@ -42,12 +43,13 @@ export class PaymentsService { if (!user) { throw new BadRequestException('Invalid user token'); } + */ // Créer la transaction dans la base const payment = await this.prisma.payment.create({ data: { - merchantPartnerId:1, // À remplacer par le bon partnerId + merchantPartnerId:chargeDto.partnerId , // À remplacer par le bon partnerId customerId: 1, // todo À remplacer par user.id amount: chargeDto.amount, currency: chargeDto.currency, @@ -61,21 +63,29 @@ export class PaymentsService { try { // Router vers le bon opérateur + this.logger.debug( + `[getting adaptator for ]: ${chargeDto.operator}_${chargeDto.country} `) const adapter = this.operatorsService.getAdapter( - 'user.operator.code', - user.country, + chargeDto.operator, + chargeDto.country, ); + this.logger.debug(`Processing payment ${payment.id} through operator adapter ${adapter.constructor.name}`); + const chargeParams = { - userToken: user.userToken, - userAlias: user.userAlias, + userToken: chargeDto.userToken, + userAlias: chargeDto.userToken,//todo make alias in contrat amount: chargeDto.amount, currency: chargeDto.currency, description: chargeDto.description, - reference: 'payment.reference,',//todo À remplacer par payment.reference + 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({ @@ -104,7 +114,7 @@ export class PaymentsService { return updatedPayment; } catch (error) { // En cas d'erreur, marquer comme échoué - await this.prisma.payment.update({ + const resultFinal= await this.prisma.payment.update({ where: { id: payment.id }, data: { status: TransactionStatus.FAILED, @@ -112,7 +122,7 @@ export class PaymentsService { }, }); - throw error; + return { ...resultFinal }; } } diff --git a/src/modules/subscriptions/subscriptions.controller.ts b/src/modules/subscriptions/subscriptions.controller.ts index bac9bcd..0148808 100644 --- a/src/modules/subscriptions/subscriptions.controller.ts +++ b/src/modules/subscriptions/subscriptions.controller.ts @@ -11,6 +11,7 @@ import { UseGuards, Request, Logger, + ParseIntPipe, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { SubscriptionsService } from './subscriptions.service'; @@ -37,12 +38,24 @@ export class SubscriptionsController { return this.subscriptionsService.create(merchantId, dto); } + @Get('/') + @ApiOperation({ summary: 'Get subscription list' }) + async getAll(@Request() req) { + return this.subscriptionsService.findAll(); + } + + @Get('merchant/:merchantId') + @ApiOperation({ summary: 'Get subscription list by merchant' }) + async getAllByMErchant(@Request() req, @Param('merchantId', ParseIntPipe) merchantId: number) { + return this.subscriptionsService.findAllByMerchant(merchantId); + } + @Get(':id') @ApiOperation({ summary: 'Get subscription details' }) - async get(@Request() req, @Param('id') id: string) { - return this.subscriptionsService.get(id, req.user.partnerId); + async get(@Request() req, @Param('id') id: number) { + return this.subscriptionsService.get(id); } @@ -56,9 +69,5 @@ export class SubscriptionsController { return this.subscriptionsService.cancel(id, req.user.partnerId, reason); } - @Get(':id/invoices') - @ApiOperation({ summary: 'Get subscription invoices' }) - async getInvoices(@Request() req, @Param('id') id: string) { - return this.subscriptionsService.getInvoices(id, req.user.partnerId); - } + } \ No newline at end of file diff --git a/src/modules/subscriptions/subscriptions.module.ts b/src/modules/subscriptions/subscriptions.module.ts index e19d55a..c59387d 100644 --- a/src/modules/subscriptions/subscriptions.module.ts +++ b/src/modules/subscriptions/subscriptions.module.ts @@ -18,7 +18,7 @@ import { PaymentsModule } from '../payments/payments.module'; BullModule.registerQueue({ name: 'billing', }), - PaymentsModule, + // PaymentsModule, ], controllers: [SubscriptionsController], providers: [ diff --git a/src/modules/subscriptions/subscriptions.service.ts b/src/modules/subscriptions/subscriptions.service.ts index fac7b81..7d35a44 100644 --- a/src/modules/subscriptions/subscriptions.service.ts +++ b/src/modules/subscriptions/subscriptions.service.ts @@ -4,23 +4,54 @@ 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 { SubscriptionStatus } from '@prisma/client'; //import { SubscriptionStatus, Prisma } from '@prisma/client'; @Injectable() export class SubscriptionsService { - get(id: string, partnerId: any) { - throw new Error('Method not implemented.'); + async get(id: number):Promise { + const service = await this.prisma.subscription.findUnique({ + where: { id }, + + }); + + if (!service) { + throw new NotFoundException(`Service with ID ${id} not found`); + } + + return service; } - list(arg0: { partnerId: any; status: string | undefined; userId: string | undefined; page: number; limit: number; }) { - throw new Error('Method not implemented.'); + + async findAllByMerchant(merchantId: number): Promise { + // Check if merchant exists + + return this.prisma.subscription.findMany({ + where: { merchantPartnerId: merchantId }, + + orderBy: { + createdAt: 'desc', + }, + }); } + + async findAll(): Promise { + // Check if merchant exists + + return this.prisma.subscription.findMany({ + // where: { merchantPartnerId: merchantId }, + + orderBy: { + createdAt: 'desc', + }, + }); + } + getInvoices(id: string, partnerId: any) { throw new Error('Method not implemented.'); } constructor( - private readonly prisma: PrismaService, - private readonly paymentsService: PaymentsService, + private readonly prisma: PrismaService, @InjectQueue('subscriptions') private subscriptionQueue: bull.Queue, @InjectQueue('billing') private billingQueue: bull.Queue, ) {}