first commit

This commit is contained in:
Mamadou Khoussa [028918 DSI/DAC/DIF/DS] 2025-10-22 00:58:47 +00:00
parent bde4f90235
commit b409b81b18
27 changed files with 278 additions and 94 deletions

23
package-lock.json generated
View File

@ -26,6 +26,8 @@
"cache-manager-redis-store": "^3.0.1", "cache-manager-redis-store": "^3.0.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"passport-headerapikey": "^1.2.2",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },
@ -9201,11 +9203,30 @@
"url": "https://github.com/sponsors/jaredhanson" "url": "https://github.com/sponsors/jaredhanson"
} }
}, },
"node_modules/passport-headerapikey": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/passport-headerapikey/-/passport-headerapikey-1.2.2.tgz",
"integrity": "sha512-4BvVJRrWsNJPrd3UoZfcnnl4zvUWYKEtfYkoDsaOKBsrWHYmzTApCjs7qUbncOLexE9ul0IRiYBFfBG0y9IVQA==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.15",
"passport-strategy": "^1.0.0"
}
},
"node_modules/passport-jwt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
"passport-strategy": "^1.0.0"
}
},
"node_modules/passport-strategy": { "node_modules/passport-strategy": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"peer": true,
"engines": { "engines": {
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }

View File

@ -37,6 +37,8 @@
"cache-manager-redis-store": "^3.0.1", "cache-manager-redis-store": "^3.0.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"passport-headerapikey": "^1.2.2",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },

View File

@ -1,6 +1,6 @@
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common'; import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { Response } from 'express';
import { Response } from 'express'; import { Prisma } from 'generated/prisma';
@Catch(Prisma.PrismaClientKnownRequestError) @Catch(Prisma.PrismaClientKnownRequestError)
export class PrismaExceptionFilter implements ExceptionFilter { export class PrismaExceptionFilter implements ExceptionFilter {

View File

@ -2,7 +2,7 @@ import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable() @Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') { export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) { constructor(private reflector: Reflector) {

View File

@ -1,7 +1,7 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator'; import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable() @Injectable()
export class RolesGuard implements CanActivate { export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {} constructor(private reflector: Reflector) {}

View File

@ -1,7 +1,7 @@
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
export default registerAs('app', () => ({ export default registerAs('app', () => ({
port: parseInt(process.env.PORT, 10) || 3000, port: parseInt(process.env.PORT ?? "3000", 10) || 3000,
env: process.env.NODE_ENV || 'development', env: process.env.NODE_ENV || 'development',
apiPrefix: process.env.API_PREFIX || 'v2', apiPrefix: process.env.API_PREFIX || 'v2',
jwtSecret: process.env.JWT_SECRET || 'your-secret-key', jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
@ -11,6 +11,6 @@ export default registerAs('app', () => ({
}, },
redis: { redis: {
host: process.env.REDIS_HOST || 'localhost', host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379, port: parseInt(process.env.REDIS_PORT?? "6379", 10) || 6379,
}, },
})); }));

View File

@ -0,0 +1,3 @@
export class AuthController{
}

View File

@ -14,13 +14,23 @@ import { OperatorsModule } from '../operators/operators.module';
PassportModule.register({ defaultStrategy: 'jwt' }), PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({ JwtModule.registerAsync({
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('app.jwtSecret'), secret: configService.get<string>('JWT_SECRET'),
signOptions: { signOptions: {
expiresIn: configService.get<string>('app.jwtExpiresIn'), expiresIn: configService.get('JWT_EXPIRATION') || '1h',
}, },
}), }),
inject: [ConfigService], /*todo
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string | number>('JWT_EXPIRATION') || '1h'
},
};
},*/
}), }),
OperatorsModule, OperatorsModule,
], ],

View File

@ -8,10 +8,7 @@ export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, 'api-
constructor(private readonly prisma: PrismaService) { constructor(private readonly prisma: PrismaService) {
super( super(
{ header: 'X-API-Key', prefix: '' }, { header: 'X-API-Key', prefix: '' },
true, true
async (apiKey, done) => {
return this.validate(apiKey, done);
},
); );
} }

View File

@ -2,8 +2,8 @@ import { Controller, Post, Get, Body, Param, Query, UseGuards, Request } from '@
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { NotificationsService } from './services/notifications.service'; import { NotificationsService } from './services/notifications.service';
import { SendNotificationDto, BulkNotificationDto } from './dto/notification.dto'; import { SendNotificationDto, BulkNotificationDto } from './dto/notification.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
@ApiTags('notifications') @ApiTags('notifications')
@Controller('notifications') @Controller('notifications')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)

View File

@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { NotificationsController } from './notifications.controller'; import { NotificationsController } from './notifications.controller';
import { NotificationsService } from './notifications.service'; import { NotificationsService } from './services/notifications.service';
import { SmsService } from './services/sms.service'; import { SmsService } from './services/sms.service';
import { EmailService } from './services/email.service'; import { EmailService } from './services/email.service';
import { WebhookService } from './services/webhook.service'; import { WebhookService } from './services/webhook.service';

View File

@ -1,6 +1,6 @@
import { Process, Processor } from '@nestjs/bull'; import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull'; import bull from 'bull';
import { NotificationsService } from '../notifications.service'; import { NotificationsService } from '../services/notifications.service';
import { WebhookService } from '../services/webhook.service'; import { WebhookService } from '../services/webhook.service';
@Processor('notifications') @Processor('notifications')
@ -10,7 +10,7 @@ export class NotificationProcessor {
) {} ) {}
@Process('send-notification') @Process('send-notification')
async handleSendNotification(job: Job) { async handleSendNotification(job: bull.Job) {
const { notificationId } = job.data; const { notificationId } = job.data;
try { try {
@ -23,9 +23,9 @@ export class NotificationProcessor {
} }
@Process('bulk-send') @Process('bulk-send')
async handleBulkSend(job: Job) { async handleBulkSend(job: bull.Job) {
const { notifications } = job.data; const { notifications } = job.data;
const results = []; const results:any = [];
for (const notificationId of notifications) { for (const notificationId of notifications) {
try { try {
@ -47,7 +47,7 @@ export class WebhookProcessor {
) {} ) {}
@Process('send-webhook') @Process('send-webhook')
async handleSendWebhook(job: Job) { async handleSendWebhook(job: bull.Job) {
const { webhookId, attempt } = job.data; const { webhookId, attempt } = job.data;
return await this.webhookService.processWebhook(webhookId, attempt); return await this.webhookService.processWebhook(webhookId, attempt);

View File

@ -0,0 +1,98 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OperatorsService } from '../../operators/operators.service';
import { PrismaService } from '../../../shared/services/prisma.service';
//todo rewrite
@Injectable()
export class EmailService {
constructor(
private readonly operatorsService: OperatorsService,
private readonly prisma: PrismaService,
private readonly configService: ConfigService,
) {}
async send(params: {
to: string;
message: string;
subject?:string;
content?:string;
template?:string
userToken?: string;
userAlias?: string;
from?: string;
}) {
// Si on a un userToken, utiliser l'opérateur de l'utilisateur
if (params.userToken) {
const user = await this.prisma.user.findUnique({
where: { userToken: params.userToken },
include: { operator: true },
});
if (user) {
const adapter = this.operatorsService.getAdapter(
user.operator.code,
user.country,
);
return await adapter.sendSms({
to: params.to,
message: params.message,
userToken: params.userToken,
userAlias: params.userAlias,
from: params.from,
country: user.country,
});
}
}
// Sinon, détecter l'opérateur par le numéro
const operator = this.detectOperatorByNumber(params.to);
const adapter = this.operatorsService.getAdapter(operator.code, operator.country);
return await adapter.sendSms({
to: params.to,
message: params.message,
from: params.from,
country: operator.country,
});
}
async sendOtp(msisdn: string, code: string, template?: string) {
const message = template
? template.replace('{code}', code)
: `Your verification code is: ${code}`;
return this.send({
to: msisdn,
message: message,
});
}
async sendTransactional(params: {
to: string;
template: string;
variables: Record<string, any>;
userToken?: string;
}) {
// Remplacer les variables dans le template
let message = params.template;
for (const [key, value] of Object.entries(params.variables)) {
message = message.replace(new RegExp(`{${key}}`, 'g'), value);
}
return this.send({
to: params.to,
message: message,
userToken: params.userToken,
});
}
private detectOperatorByNumber(msisdn: string) {
// Logique de détection basée sur le préfixe
// Pour simplifier, on retourne Orange CI par défaut
return {
code: 'ORANGE',
country: 'CI',
};
}
}

View File

@ -1,12 +1,12 @@
import { Injectable, BadRequestException } from '@nestjs/common'; import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull'; import bull from 'bull';
import { PrismaService } from '../../shared/services/prisma.service'; import { SmsService } from './sms.service';
import { SmsService } from './services/sms.service'; import { EmailService } from './email.service';
import { EmailService } from './services/email.service'; import { WebhookService } from './webhook.service';
import { WebhookService } from './services/webhook.service'; import { NotificationTemplateService } from './template.service';
import { NotificationTemplateService } from './services/template.service'; import { SendNotificationDto, BulkNotificationDto } from '../dto/notification.dto';
import { SendNotificationDto, BulkNotificationDto } from './dto/notification.dto'; import { PrismaService } from 'src/shared/services/prisma.service';
@Injectable() @Injectable()
export class NotificationsService { export class NotificationsService {
@ -16,12 +16,12 @@ export class NotificationsService {
private readonly emailService: EmailService, private readonly emailService: EmailService,
private readonly webhookService: WebhookService, private readonly webhookService: WebhookService,
private readonly templateService: NotificationTemplateService, private readonly templateService: NotificationTemplateService,
@InjectQueue('notifications') private notificationQueue: Queue, @InjectQueue('notifications') private notificationQueue: bull.Queue,
) {} ) {}
async send(partnerId: string, dto: SendNotificationDto) { async send(partnerId: string, dto: SendNotificationDto) {
// Valider l'utilisateur si fourni // Valider l'utilisateur si fourni
let user = null; let user:any = null;
if (dto.userToken) { if (dto.userToken) {
user = await this.prisma.user.findFirst({ user = await this.prisma.user.findFirst({
where: { where: {
@ -70,13 +70,13 @@ export class NotificationsService {
} }
async sendBulk(partnerId: string, dto: BulkNotificationDto) { async sendBulk(partnerId: string, dto: BulkNotificationDto) {
const notifications = []; const notifications :any= [];
// Récupérer les destinataires selon les critères // Récupérer les destinataires selon les critères
const recipients = await this.getRecipients(partnerId, dto); const recipients:any = await this.getRecipients(partnerId, dto);
for (const recipient of recipients) { for (const recipient of recipients) {
const notification = await this.prisma.notification.create({ const notification:any = await this.prisma.notification.create({
data: { data: {
partnerId: partnerId, partnerId: partnerId,
userId: recipient.userId, userId: recipient.userId,
@ -148,6 +148,7 @@ export class NotificationsService {
subject: notification.subject, subject: notification.subject,
content: notification.content, content: notification.content,
template: notification.templateId, template: notification.templateId,
message: ''
}); });
break; break;
@ -230,7 +231,7 @@ export class NotificationsService {
} }
private async getRecipients(partnerId: string, dto: BulkNotificationDto) { private async getRecipients(partnerId: string, dto: BulkNotificationDto) {
const recipients = []; const recipients:any = [];
if (dto.userIds && dto.userIds.length > 0) { if (dto.userIds && dto.userIds.length > 0) {
const users = await this.prisma.user.findMany({ const users = await this.prisma.user.findMany({
@ -259,7 +260,7 @@ export class NotificationsService {
} }
private async getSegmentUsers(partnerId: string, segments: string[]) { private async getSegmentUsers(partnerId: string, segments: string[]) {
const users = []; const users:any = [];
for (const segment of segments) { for (const segment of segments) {
switch (segment) { switch (segment) {

View File

@ -0,0 +1,6 @@
export class NotificationTemplateService{
render(templateId: string | undefined, arg1: any) {
throw new Error('Method not implemented.');
}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios'; import { HttpService } from '@nestjs/axios';
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull'; import bull from 'bull';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { PrismaService } from '../../../shared/services/prisma.service'; import { PrismaService } from '../../../shared/services/prisma.service';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
@ -11,7 +11,7 @@ export class WebhookService {
constructor( constructor(
private readonly httpService: HttpService, private readonly httpService: HttpService,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
@InjectQueue('webhooks') private webhookQueue: Queue, @InjectQueue('webhooks') private webhookQueue: bull.Queue,
) {} ) {}
async send(params: { async send(params: {

View File

@ -1,3 +1,35 @@
export interface AuthValidateParams{
}
export interface AuthValidateResponse{
}
export interface RefundParams{
}
export interface SmsParams{
}
export interface SmsResponse{
}
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>;

View File

@ -10,6 +10,7 @@ import {
ChargeResponse, ChargeResponse,
} from './operator.adapter.interface'; } from './operator.adapter.interface';
import { OrangeTransformer } from '../transformers/orange.transformer'; import { OrangeTransformer } from '../transformers/orange.transformer';
@Injectable() @Injectable()
export class OrangeAdapter implements IOperatorAdapter { export class OrangeAdapter implements IOperatorAdapter {
@ -21,8 +22,8 @@ export class OrangeAdapter implements IOperatorAdapter {
private readonly httpService: HttpService, private readonly httpService: HttpService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
) { ) {
this.baseUrl = this.configService.get('ORANGE_API_URL'); this.baseUrl = this.configService.get('ORANGE_API_URL') as string;
this.accessToken = this.configService.get('ORANGE_ACCESS_TOKEN'); this.accessToken = this.configService.get('ORANGE_ACCESS_TOKEN') as string;
this.transformer = new OrangeTransformer(); this.transformer = new OrangeTransformer();
} }

View File

@ -1,30 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class OrangeTransformer {
transformChargeResponse(bizaoResponse: any): any {
return {
paymentId: bizaoResponse.amountTransaction?.serverReferenceCode,
status: this.mapStatus(
bizaoResponse.amountTransaction?.transactionOperationStatus,
),
operatorReference: bizaoResponse.amountTransaction?.serverReferenceCode,
amount: parseFloat(
bizaoResponse.amountTransaction?.paymentAmount?.totalAmountCharged,
),
currency:
bizaoResponse.amountTransaction?.paymentAmount?.chargingInformation
?.currency,
createdAt: new Date(),
};
}
private mapStatus(bizaoStatus: string): string {
const statusMap = {
Charged: 'SUCCESS',
Failed: 'FAILED',
Pending: 'PENDING',
};
return statusMap[bizaoStatus] || 'PENDING';
}
}

View File

@ -1,3 +1,30 @@
export class OrangeTransformer{ import { Injectable } from '@nestjs/common';
} @Injectable()
export class OrangeTransformer {
transformChargeResponse(bizaoResponse: any): any {
return {
paymentId: bizaoResponse.amountTransaction?.serverReferenceCode,
status: this.mapStatus(
bizaoResponse.amountTransaction?.transactionOperationStatus,
),
operatorReference: bizaoResponse.amountTransaction?.serverReferenceCode,
amount: parseFloat(
bizaoResponse.amountTransaction?.paymentAmount?.totalAmountCharged,
),
currency:
bizaoResponse.amountTransaction?.paymentAmount?.chargingInformation
?.currency,
createdAt: new Date(),
};
}
private mapStatus(bizaoStatus: string): string {
const statusMap = {
Charged: 'SUCCESS',
Failed: 'FAILED',
Pending: 'PENDING',
};
return statusMap[bizaoStatus] || 'PENDING';
}
}

View File

@ -0,0 +1,4 @@
//todoe
export class PartnersController{
}

View File

@ -1,5 +1,5 @@
import { Process, Processor } from '@nestjs/bull'; import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull'; import bull from 'bull';
import { SubscriptionsService } from '../subscriptions.service'; import { SubscriptionsService } from '../subscriptions.service';
import { HttpService } from '@nestjs/axios'; import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
@ -12,7 +12,7 @@ export class SubscriptionProcessor {
) {} ) {}
@Process('webhook-notification') @Process('webhook-notification')
async handleWebhookNotification(job: Job) { async handleWebhookNotification(job: bull.Job) {
const { url, event, subscription, payment } = job.data; const { url, event, subscription, payment } = job.data;
try { try {
@ -50,13 +50,13 @@ export class BillingProcessor {
) {} ) {}
@Process('process-renewal') @Process('process-renewal')
async handleRenewal(job: Job) { async handleRenewal(job: bull.Job) {
const { subscriptionId } = job.data; const { subscriptionId } = job.data;
await this.subscriptionsService.processRenewal(subscriptionId); await this.subscriptionsService.processRenewal(subscriptionId);
} }
@Process('trial-end') @Process('trial-end')
async handleTrialEnd(job: Job) { async handleTrialEnd(job: bull.Job) {
const { subscriptionId } = job.data; const { subscriptionId } = job.data;
// Convertir de TRIAL à ACTIVE et traiter le premier paiement // Convertir de TRIAL à ACTIVE et traiter le premier paiement
@ -64,7 +64,7 @@ export class BillingProcessor {
} }
@Process('retry-renewal') @Process('retry-renewal')
async handleRetryRenewal(job: Job) { async handleRetryRenewal(job: bull.Job) {
const { subscriptionId, attempt } = job.data; const { subscriptionId, attempt } = job.data;
console.log(`Retrying renewal for subscription ${subscriptionId}, attempt ${attempt}`); console.log(`Retrying renewal for subscription ${subscriptionId}, attempt ${attempt}`);

View File

@ -1,14 +1,14 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule'; import { Cron, CronExpression } from '@nestjs/schedule';
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull'; import bull from 'bull';
import { PrismaService } from '../../../shared/services/prisma.service'; import { PrismaService } from '../../../shared/services/prisma.service';
@Injectable() @Injectable()
export class SubscriptionScheduler { export class SubscriptionScheduler {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
@InjectQueue('billing') private billingQueue: Queue, @InjectQueue('billing') private billingQueue: bull.Queue,
) {} ) {}
@Cron(CronExpression.EVERY_HOUR) @Cron(CronExpression.EVERY_HOUR)

View File

@ -45,6 +45,7 @@ export class BillingService {
end: subscription.currentPeriodEnd, end: subscription.currentPeriodEnd,
}, },
}, },
partnerId: ''
}); });
// Mettre à jour la facture // Mettre à jour la facture

View File

@ -1,7 +1,8 @@
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../../shared/services/prisma.service'; import { PrismaService } from '../../../shared/services/prisma.service';
import { CreatePlanDto, UpdatePlanDto } from '../dto/plan.dto'; import { CreatePlanDto, UpdatePlanDto } from '../dto/plan.dto';
import { Prisma } from '@prisma/client'; import { Prisma } from 'generated/prisma';
@Injectable() @Injectable()
export class PlanService { export class PlanService {

View File

@ -9,6 +9,15 @@ import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription
@Injectable() @Injectable()
export class SubscriptionsService { export class SubscriptionsService {
get(id: string, partnerId: any) {
throw new Error('Method not implemented.');
}
list(arg0: { partnerId: any; status: string | undefined; userId: string | undefined; page: number; limit: number; }) {
throw new Error('Method not implemented.');
}
getInvoices(id: string, partnerId: any) {
throw new Error('Method not implemented.');
}
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly paymentsService: PaymentsService, private readonly paymentsService: PaymentsService,
@ -141,7 +150,7 @@ export class SubscriptionsService {
if (dto.status === 'PAUSED' && subscription.status === 'ACTIVE') { if (dto.status === 'PAUSED' && subscription.status === 'ACTIVE') {
updateData.status = 'PAUSED'; updateData.status = 'PAUSED';
updateData.pausedAt = new Date(); updateData.pausedAt = new Date();
} else if (dto.status === 'ACTIVE' && subscription.status === 'PAUSED') { } else if (dto.status === 'ACTIVE' && subscription.status === 'SUSPENDED') {
updateData.status = 'ACTIVE'; updateData.status = 'ACTIVE';
updateData.pausedAt = null; updateData.pausedAt = null;
// Recalculer la prochaine date de facturation // Recalculer la prochaine date de facturation
@ -166,7 +175,7 @@ export class SubscriptionsService {
} }
if (dto.metadata) { if (dto.metadata) {
updateData.metadata = { ...subscription.metadata, ...dto.metadata }; updateData.metadata = { metadata:subscription.metadata, ...dto.metadata };
} }
const updatedSubscription = await this.prisma.subscription.update({ const updatedSubscription = await this.prisma.subscription.update({
@ -201,10 +210,8 @@ export class SubscriptionsService {
where: { id: subscriptionId }, where: { id: subscriptionId },
data: { data: {
status: 'CANCELLED', status: 'CANCELLED',
cancelledAt: new Date(), cancelledAt: new Date(),
cancellationReason: reason, metadata: {
metadata: {
...subscription.metadata,
cancellationDetails: { cancellationDetails: {
reason, reason,
cancelledBy: 'partner', cancelledBy: 'partner',
@ -227,7 +234,7 @@ export class SubscriptionsService {
where: { id: partnerId }, where: { id: partnerId },
}); });
if (partner?.callbacks?.subscription?.onCancel) { if (partner?.callbacks?subscription?.onCancel) {
await this.subscriptionQueue.add('webhook-notification', { await this.subscriptionQueue.add('webhook-notification', {
url: partner.callbacks.subscription.onCancel, url: partner.callbacks.subscription.onCancel,
event: 'SUBSCRIPTION_CANCELLED', event: 'SUBSCRIPTION_CANCELLED',
@ -259,10 +266,11 @@ export class SubscriptionsService {
try { try {
// Créer le paiement de renouvellement // Créer le paiement de renouvellement
//todo
const payment = await this.paymentsService.createCharge({ const payment = await this.paymentsService.createCharge({
userToken: subscription.user.userToken, userToken: subscription.user.userToken,
amount: subscription.amount, amount: subscription.plan.amount,
currency: subscription.currency, currency: subscription.plan.currency,
description: `Renewal: ${subscription.plan.name}`, description: `Renewal: ${subscription.plan.name}`,
reference: `REN-${subscription.id}-${Date.now()}`, reference: `REN-${subscription.id}-${Date.now()}`,
metadata: { metadata: {
@ -273,6 +281,7 @@ export class SubscriptionsService {
end: this.calculatePeriodEnd(subscription.plan, subscription.currentPeriodEnd), end: this.calculatePeriodEnd(subscription.plan, subscription.currentPeriodEnd),
}, },
}, },
partnerId: ''
}); });
if (payment.status === 'SUCCESS') { if (payment.status === 'SUCCESS') {
@ -315,7 +324,7 @@ export class SubscriptionsService {
await this.handleRenewalFailure(subscription); await this.handleRenewalFailure(subscription);
} }
} }
//todo
private async processInitialPayment(subscription: any, callbackUrl?: string) { private async processInitialPayment(subscription: any, callbackUrl?: string) {
try { try {
const payment = await this.paymentsService.createCharge({ const payment = await this.paymentsService.createCharge({
@ -329,6 +338,7 @@ export class SubscriptionsService {
subscriptionId: subscription.id, subscriptionId: subscription.id,
type: 'initial', type: 'initial',
}, },
partnerId:""
}); });
if (payment.status === 'SUCCESS') { if (payment.status === 'SUCCESS') {

View File

@ -1,6 +1,6 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from 'generated/prisma';
@Injectable() @Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() { constructor() {