From 1ddc8e9ee4e42998d3c81c7c255f7564b9b64b87 Mon Sep 17 00:00:00 2001 From: "Mamadou Khoussa [028918 DSI/DAC/DIF/DS]" Date: Sat, 25 Oct 2025 00:28:27 +0000 Subject: [PATCH] fix error --- src/app.module.ts | 12 +- src/main.ts | 3 +- src/modules/auth/auth.controller.ts | 10 - src/modules/auth/auth.module.ts | 41 --- src/modules/auth/auth.service.ts | 245 ------------- src/modules/auth/dto/auth.dto.ts | 50 --- .../auth/strategies/api-key.strategy.ts | 26 -- src/modules/auth/strategies/jwt.strategy.ts | 23 -- .../challenge/otp.challenge.controller.ts | 10 +- src/modules/challenge/otp.challenge.module.ts | 2 +- .../challenge/otp.challenge.service.ts | 10 + .../notifications/dto/notification.dto.ts | 91 ----- .../notifications/notifications.controller.ts | 37 -- .../notifications/notifications.module.ts | 40 -- .../processors/notification.processor.ts | 55 --- .../notifications/services/email.service.ts | 98 ----- .../services/notifications.service.ts | 292 --------------- .../notifications/services/sms.service.ts | 95 ----- .../services/template.service.ts | 6 - .../notifications/services/webhook.service.ts | 126 ------- src/modules/operators/operators.service.ts | 4 +- src/modules/partners/dto/partner.dto.ts | 178 --------- src/modules/partners/partners.controller.ts | 342 ------------------ src/modules/partners/partners.module.ts | 13 - src/modules/partners/partners.service.ts | 229 ------------ src/modules/payments/payments.controller.ts | 44 +-- .../subscriptions/subscriptions.controller.ts | 18 +- .../subscriptions/subscriptions.module.ts | 6 +- 28 files changed, 25 insertions(+), 2081 deletions(-) delete mode 100644 src/modules/auth/auth.controller.ts delete mode 100644 src/modules/auth/auth.module.ts delete mode 100644 src/modules/auth/auth.service.ts delete mode 100644 src/modules/auth/dto/auth.dto.ts delete mode 100644 src/modules/auth/strategies/api-key.strategy.ts delete mode 100644 src/modules/auth/strategies/jwt.strategy.ts delete mode 100644 src/modules/notifications/dto/notification.dto.ts delete mode 100644 src/modules/notifications/notifications.controller.ts delete mode 100644 src/modules/notifications/notifications.module.ts delete mode 100644 src/modules/notifications/processors/notification.processor.ts delete mode 100644 src/modules/notifications/services/email.service.ts delete mode 100644 src/modules/notifications/services/notifications.service.ts delete mode 100644 src/modules/notifications/services/sms.service.ts delete mode 100644 src/modules/notifications/services/template.service.ts delete mode 100644 src/modules/notifications/services/webhook.service.ts delete mode 100644 src/modules/partners/dto/partner.dto.ts delete mode 100644 src/modules/partners/partners.controller.ts delete mode 100644 src/modules/partners/partners.module.ts delete mode 100644 src/modules/partners/partners.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 526ef7f..aa872bb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,13 +13,10 @@ import databaseConfig from './config/database.config'; // Import des modules import { PrismaService } from './shared/services/prisma.service'; -import { AuthModule } from './modules/auth/auth.module'; -import { PartnersModule } from './modules/partners/partners.module'; -import { OperatorsModule } from './modules/operators/operators.module'; + import { OperatorsModule } from './modules/operators/operators.module'; import { PaymentsModule } from './modules/payments/payments.module'; import { SubscriptionsModule } from './modules/subscriptions/subscriptions.module'; -import { NotificationsModule } from './modules/notifications/notifications.module'; -import { OtpChallengeModule } from './modules/challenge/otp.challenge.module'; + import { OtpChallengeModule } from './modules/challenge/otp.challenge.module'; @Module({ imports: [ @@ -51,12 +48,9 @@ import { OtpChallengeModule } from './modules/challenge/otp.challenge.module'; }), ScheduleModule.forRoot(), EventEmitterModule.forRoot(), - AuthModule, - PartnersModule, OperatorsModule, PaymentsModule, - SubscriptionsModule, - NotificationsModule, + SubscriptionsModule, OtpChallengeModule ], providers: [PrismaService], diff --git a/src/main.ts b/src/main.ts index c33fe8e..2ff953d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,8 +29,7 @@ async function bootstrap() { .setTitle('Payment Hub API') .setDescription('Unified DCB Payment Aggregation Platform') .setVersion('1.0.0') - .addBearerAuth() - .addTag('auth') + .addBearerAuth() .addTag('payments') .addTag('subscriptions') .build(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts deleted file mode 100644 index d0d50c0..0000000 --- a/src/modules/auth/auth.controller.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Controller, Get } from "@nestjs/common"; - -//todo -@Controller() -export class AuthController{ - @Get() - getHello(): string { - return 'Hello World!'; - } -} \ No newline at end of file diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts deleted file mode 100644 index 50e178f..0000000 --- a/src/modules/auth/auth.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Module } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { AuthService } from './auth.service'; -import { AuthController } from './auth.controller'; -import { JwtStrategy } from './strategies/jwt.strategy'; -import { ApiKeyStrategy } from './strategies/api-key.strategy'; -import { PrismaService } from '../../shared/services/prisma.service'; -import { OperatorsModule } from '../operators/operators.module'; - -@Module({ - imports: [ - PassportModule.register({ defaultStrategy: 'jwt' }), - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: async (configService: ConfigService) => ({ - secret: configService.get('JWT_SECRET'), - signOptions: { - expiresIn: configService.get('JWT_EXPIRATION') || '1h', - }, - }), - /*todo - useFactory: async (configService: ConfigService) => { - return { - secret: configService.get('JWT_SECRET'), - signOptions: { - expiresIn: configService.get('JWT_EXPIRATION') || '1h' - }, - }; - },*/ - - }), - OperatorsModule, - ], - controllers: [AuthController], - providers: [AuthService, JwtStrategy, ApiKeyStrategy, PrismaService], - exports: [AuthService, JwtModule], -}) -export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts deleted file mode 100644 index bb34f99..0000000 --- a/src/modules/auth/auth.service.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { - Injectable, - UnauthorizedException, - BadRequestException, -} from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import { PrismaService } from '../../shared/services/prisma.service'; -import { OperatorsService } from '../operators/operators.service'; -import * as bcrypt from 'bcrypt'; -import { AuthInitDto, AuthValidateDto, LoginDto } from './dto/auth.dto'; - -@Injectable() -export class AuthService { - constructor( - private readonly prisma: PrismaService, - private readonly jwtService: JwtService, - private readonly operatorsService: OperatorsService, - ) {} - - async initializeUserAuth(partnerId: string, dto: AuthInitDto) { - // Vérifier le partenaire - const partner = await this.prisma.partner.findUnique({ - where: { id: partnerId }, - }); - - if (!partner || partner.status !== 'ACTIVE') { - throw new UnauthorizedException('Invalid partner'); - } - - // Déterminer l'opérateur basé sur le numéro - const operator = this.detectOperator(dto.msisdn, dto.country); - - // Obtenir l'adaptateur approprié - const adapter = this.operatorsService.getAdapter(operator, dto.country); - - // Initialiser l'authentification avec l'opérateur - const authResponse = await adapter.initializeAuth({ - msisdn: dto.msisdn, - country: dto.country, - metadata: dto.metadata, - }); - - // Créer une session temporaire - const session = await this.prisma.authSession.create({ - data: { - sessionId: authResponse.sessionId, - partnerId: partnerId, - msisdn: dto.msisdn, - operator: operator, - country: dto.country, - authMethod: dto.authMethod, - challengeId: authResponse.challengeId, - status: 'PENDING', - expiresAt: authResponse.expiresAt, - }, - }); - - return { - sessionId: session.sessionId, - authMethod: dto.authMethod, - status: 'PENDING', - redirectUrl: authResponse.redirectUrl, - challengeId: authResponse.challengeId, - expiresAt: authResponse.expiresAt, - }; - } - - async validateUserAuth(dto: AuthValidateDto) { - // Récupérer la session - const session = await this.prisma.authSession.findUnique({ - where: { sessionId: dto.sessionId }, - }); - - if (!session) { - throw new BadRequestException('Invalid session'); - } - - if (session.status !== 'PENDING') { - throw new BadRequestException('Session already processed'); - } - - if (new Date() > session.expiresAt) { - throw new BadRequestException('Session expired'); - } - - // Obtenir l'adaptateur - const adapter = this.operatorsService.getAdapter( - session.operator, - session.country, - ); - - // Valider avec l'opérateur - const validationResponse = await adapter.validateAuth({ - challengeId: session.challengeId, - otpCode: dto.otpCode, - msisdn: session.msisdn, - country: session.country, - }); - - if (!validationResponse.success) { - await this.prisma.authSession.update({ - where: { id: session.id }, - data: { status: 'FAILED' }, - }); - throw new UnauthorizedException('Authentication failed'); - } - - // Créer ou mettre à jour l'utilisateur - const user = await this.prisma.user.upsert({ - where: { msisdn: session.msisdn }, - update: { - userToken: validationResponse.userToken, - userAlias: validationResponse.userAlias, - updatedAt: new Date(), - }, - create: { - msisdn: session.msisdn, - userToken: validationResponse.userToken, - userAlias: validationResponse.userAlias, - operatorId: await this.getOperatorId(session.operator, session.country), - partnerId: session.partnerId, - country: session.country, - }, - }); - - // Mettre à jour la session - await this.prisma.authSession.update({ - where: { id: session.id }, - data: { - status: 'SUCCESS', - userId: user.id, - }, - }); - - // Créer un JWT pour le partenaire - const payload = { - userId: user.id, - partnerId: session.partnerId, - msisdn: user.msisdn, - operator: session.operator, - }; - - return { - success: true, - accessToken: this.jwtService.sign(payload), - userToken: validationResponse.userToken, - userAlias: validationResponse.userAlias, - msisdn: session.msisdn, - operator: session.operator, - country: session.country, - expiresAt: validationResponse.expiresAt, - }; - } - - async loginPartner(dto: LoginDto) { - const partner = await this.prisma.partner.findUnique({ - where: { email: dto.email }, - }); - - if (!partner) { - throw new UnauthorizedException('Invalid credentials'); - } - - const isPasswordValid = await bcrypt.compare( - dto.password, - partner.passwordHash, - ); - - if (!isPasswordValid) { - throw new UnauthorizedException('Invalid credentials'); - } - - const payload = { - partnerId: partner.id, - email: partner.email, - type: 'partner', - }; - - return { - accessToken: this.jwtService.sign(payload), - partner: { - id: partner.id, - name: partner.name, - email: partner.email, - status: partner.status, - }, - }; - } - - private detectOperator(msisdn: string, country: string): string { - // Logique pour détecter l'opérateur basé sur le préfixe - const prefixMap = { - CI: { - '07': 'ORANGE', - '08': 'ORANGE', - '09': 'ORANGE', - '04': 'MTN', - '05': 'MTN', - '06': 'MTN', - '01': 'MOOV', - }, - SN: { - '77': 'ORANGE', - '78': 'ORANGE', - '76': 'FREE', - '70': 'EXPRESSO', - }, - // Ajouter d'autres pays - }; - - const countryPrefixes = prefixMap[country]; - if (!countryPrefixes) { - throw new BadRequestException(`Country ${country} not supported`); - } - - const prefix = msisdn.substring(0, 2); - const operator = countryPrefixes[prefix]; - - if (!operator) { - throw new BadRequestException(`Cannot detect operator for ${msisdn}`); - } - - return operator; - } - - private async getOperatorId( - operatorCode: string, - country: string, - ): Promise { - const operator = await this.prisma.operator.findFirst({ - where: { - code: operatorCode as any, - country: country, - }, - }); - - if (!operator) { - throw new BadRequestException( - `Operator ${operatorCode} not found in ${country}`, - ); - } - - return operator.id; - } -} diff --git a/src/modules/auth/dto/auth.dto.ts b/src/modules/auth/dto/auth.dto.ts deleted file mode 100644 index 4eb39a5..0000000 --- a/src/modules/auth/dto/auth.dto.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IsString, IsEnum, IsOptional, IsMobilePhone } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class AuthInitDto { - @ApiProperty() - @IsMobilePhone() - msisdn: string; - - @ApiProperty() - @IsString() - country: string; - - @ApiProperty({ enum: ['OTP_SMS', 'REDIRECT_3G', 'SMS_MO', 'USSD'] }) - @IsEnum(['OTP_SMS', 'REDIRECT_3G', 'SMS_MO', 'USSD']) - authMethod: string; - - @ApiProperty({ required: false }) - @IsOptional() - redirectUrl?: string; - - @ApiProperty({ required: false }) - @IsOptional() - metadata?: Record; -} - -export class AuthValidateDto { - @ApiProperty() - @IsString() - sessionId: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - otpCode?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - challengeResponse?: string; -} - -export class LoginDto { - @ApiProperty() - @IsString() - email: string; - - @ApiProperty() - @IsString() - password: string; -} diff --git a/src/modules/auth/strategies/api-key.strategy.ts b/src/modules/auth/strategies/api-key.strategy.ts deleted file mode 100644 index 93d048a..0000000 --- a/src/modules/auth/strategies/api-key.strategy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { HeaderAPIKeyStrategy } from 'passport-headerapikey'; -import { PrismaService } from '../../../shared/services/prisma.service'; - -@Injectable() -export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, 'api-key') { - constructor(private readonly prisma: PrismaService) { - super( - { header: 'X-API-Key', prefix: '' }, - true - ); - } - - async validate(apiKey: string, done: any) { - const partner = await this.prisma.partner.findUnique({ - where: { apiKey }, - }); - - if (!partner || partner.status !== 'ACTIVE') { - return done(new UnauthorizedException(), false); - } - - return done(null, partner); - } -} \ No newline at end of file diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts deleted file mode 100644 index 718b472..0000000 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ExtractJwt, Strategy } from 'passport-jwt'; -import { PassportStrategy } from '@nestjs/passport'; -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(private configService: ConfigService) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - secretOrKey: configService.get('app.jwtSecret'), - }); - } - - async validate(payload: any) { - return { - userId: payload.userId, - partnerId: payload.partnerId, - email: payload.email, - }; - } -} diff --git a/src/modules/challenge/otp.challenge.controller.ts b/src/modules/challenge/otp.challenge.controller.ts index 74d0652..56804a2 100644 --- a/src/modules/challenge/otp.challenge.controller.ts +++ b/src/modules/challenge/otp.challenge.controller.ts @@ -175,8 +175,8 @@ export class OtpChallengeController { async verifyOtp( @Param('challengeId') challengeId: string, @Body('otpCode') otpCode: string, - @Headers('x-merchant-id') merchantId: string, - @Headers('x-api-key') apiKey: string, + @Headers('X-Merchant-ID') merchantId: string, + @Headers('x-API-KEY') apiKey: string, ): Promise { this.logger.log( `[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`, @@ -186,11 +186,7 @@ export class OtpChallengeController { // Valider les headers this.validateMerchantHeaders(merchantId, apiKey); - // Valider le code OTP - if (!otpCode || otpCode.trim().length === 0) { - throw new HttpException('OTP code is required', HttpStatus.BAD_REQUEST); - } - + // Valider le code OTP // Appeler le service const response = await this.otpChallengeService.verifyOtp( challengeId, diff --git a/src/modules/challenge/otp.challenge.module.ts b/src/modules/challenge/otp.challenge.module.ts index b15ea6f..a8705e3 100644 --- a/src/modules/challenge/otp.challenge.module.ts +++ b/src/modules/challenge/otp.challenge.module.ts @@ -15,7 +15,7 @@ import { OtpChallengeService } from './otp.challenge.service'; { provide: 'ORANGE_CONFIG', useFactory: (configService: ConfigService): OrangeConfig => ({ - baseUrl: configService.get('ORANGE_BASE_URL', 'https://api-gateway.app.cameleonapp.com/api-orange-dcb/1.0.0'), + baseUrl: 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'), diff --git a/src/modules/challenge/otp.challenge.service.ts b/src/modules/challenge/otp.challenge.service.ts index 4361e63..c6dac3b 100644 --- a/src/modules/challenge/otp.challenge.service.ts +++ b/src/modules/challenge/otp.challenge.service.ts @@ -28,6 +28,16 @@ export class OtpChallengeService implements IOtpChallengeService { // Appeler l'adaptateur Orange const response = await this.orangeAdapter.initiateChallenge(request); + if (response.challengeId || true) { + this.challengeCache.set(response.challengeId, { request, response }); + + // Nettoyer le cache après expiration (par défaut 5 minutes) + const expirationTime = (response.expiresIn || 300) * 1000; + setTimeout(() => { + this.challengeCache.delete(response.challengeId); + }, expirationTime); + } + return response; } catch (error) { return this.createErrorResponse(request, 'INITIATE_FAILED', error.message); diff --git a/src/modules/notifications/dto/notification.dto.ts b/src/modules/notifications/dto/notification.dto.ts deleted file mode 100644 index b04e534..0000000 --- a/src/modules/notifications/dto/notification.dto.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { IsString, IsEnum, IsOptional, IsArray, IsDateString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class SendNotificationDto { - @ApiProperty({ enum: ['PAYMENT', 'SUBSCRIPTION', 'ALERT', 'MARKETING'] }) - @IsEnum(['PAYMENT', 'SUBSCRIPTION', 'ALERT', 'MARKETING']) - type: string; - - @ApiProperty({ enum: ['SMS', 'EMAIL', 'WEBHOOK'] }) - @IsEnum(['SMS', 'EMAIL', 'WEBHOOK']) - channel: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - userToken?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - recipient?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - subject?: string; - - @ApiProperty() - @IsString() - content: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - templateId?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsDateString() - scheduledFor?: string; - - @ApiProperty({ required: false }) - @IsOptional() - metadata?: Record; -} - -export class BulkNotificationDto { - @ApiProperty({ enum: ['PAYMENT', 'SUBSCRIPTION', 'ALERT', 'MARKETING'] }) - @IsEnum(['PAYMENT', 'SUBSCRIPTION', 'ALERT', 'MARKETING']) - type: string; - - @ApiProperty({ enum: ['SMS', 'EMAIL'] }) - @IsEnum(['SMS', 'EMAIL']) - channel: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsArray() - userIds?: string[]; - - @ApiProperty({ required: false }) - @IsOptional() - @IsArray() - segments?: string[]; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - subject?: string; - - @ApiProperty() - @IsString() - content: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - templateId?: string; - - @ApiProperty({ required: false }) - @IsOptional() - variables?: Record; - - @ApiProperty() - @IsString() - batchId: string; - - @ApiProperty({ required: false }) - @IsOptional() - metadata?: Record; -} \ No newline at end of file diff --git a/src/modules/notifications/notifications.controller.ts b/src/modules/notifications/notifications.controller.ts deleted file mode 100644 index e795fe6..0000000 --- a/src/modules/notifications/notifications.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Controller, Post, Get, Body, Param, Query, UseGuards, Request } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; -import { NotificationsService } from './services/notifications.service'; -import { SendNotificationDto, BulkNotificationDto } from './dto/notification.dto'; -import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard'; - -@ApiTags('notifications') -@Controller('notifications') -@UseGuards(JwtAuthGuard) -@ApiBearerAuth() -export class NotificationsController { - constructor(private readonly notificationsService: NotificationsService) {} - - @Post('send') - @ApiOperation({ summary: 'Send a notification' }) - async send(@Request() req, @Body() dto: SendNotificationDto) { - return this.notificationsService.send(req.user.partnerId, dto); - } - - @Post('bulk') - @ApiOperation({ summary: 'Send bulk notifications' }) - async sendBulk(@Request() req, @Body() dto: BulkNotificationDto) { - return this.notificationsService.sendBulk(req.user.partnerId, dto); - } - - @Get(':id/status') - @ApiOperation({ summary: 'Get notification status' }) - async getStatus(@Request() req, @Param('id') id: string) { - return this.notificationsService.getStatus(id, req.user.partnerId); - } - - @Get('batch/:batchId/status') - @ApiOperation({ summary: 'Get batch notification status' }) - async getBatchStatus(@Request() req, @Param('batchId') batchId: string) { - return this.notificationsService.getBatchStatus(batchId, req.user.partnerId); - } -} diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts deleted file mode 100644 index 629bab5..0000000 --- a/src/modules/notifications/notifications.module.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Module } from '@nestjs/common'; -import { BullModule } from '@nestjs/bull'; -import { HttpModule } from '@nestjs/axios'; -import { NotificationsController } from './notifications.controller'; -import { NotificationsService } from './services/notifications.service'; -import { SmsService } from './services/sms.service'; -import { EmailService } from './services/email.service'; -import { WebhookService } from './services/webhook.service'; -import { NotificationProcessor } from './processors/notification.processor'; -import { NotificationTemplateService } from './services/template.service'; -import { PrismaService } from '../../shared/services/prisma.service'; -import { OperatorsModule } from '../operators/operators.module'; - -@Module({ - imports: [ - BullModule.registerQueue({ - name: 'notifications', - }), - BullModule.registerQueue({ - name: 'webhooks', - }), - HttpModule.register({ - timeout: 10000, - maxRedirects: 2, - }), - OperatorsModule, - ], - controllers: [NotificationsController], - providers: [ - NotificationsService, - SmsService, - EmailService, - WebhookService, - NotificationProcessor, - NotificationTemplateService, - PrismaService, - ], - exports: [NotificationsService, SmsService, WebhookService], -}) -export class NotificationsModule {} \ No newline at end of file diff --git a/src/modules/notifications/processors/notification.processor.ts b/src/modules/notifications/processors/notification.processor.ts deleted file mode 100644 index d2d2570..0000000 --- a/src/modules/notifications/processors/notification.processor.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Process, Processor } from '@nestjs/bull'; -import bull from 'bull'; -import { NotificationsService } from '../services/notifications.service'; -import { WebhookService } from '../services/webhook.service'; - -@Processor('notifications') -export class NotificationProcessor { - constructor( - private readonly notificationsService: NotificationsService, - ) {} - - @Process('send-notification') - async handleSendNotification(job: bull.Job) { - const { notificationId } = job.data; - - try { - await this.notificationsService.processNotification(notificationId); - return { success: true, notificationId }; - } catch (error) { - console.error(`Failed to send notification ${notificationId}:`, error); - throw error; - } - } - - @Process('bulk-send') - async handleBulkSend(job: bull.Job) { - const { notifications } = job.data; - const results:any = []; - - for (const notificationId of notifications) { - try { - await this.notificationsService.processNotification(notificationId); - results.push({ notificationId, success: true }); - } catch (error) { - results.push({ notificationId, success: false, error: error.message }); - } - } - - return results; - } -} - -@Processor('webhooks') -export class WebhookProcessor { - constructor( - private readonly webhookService: WebhookService, - ) {} - - @Process('send-webhook') - async handleSendWebhook(job: bull.Job) { - const { webhookId, attempt } = job.data; - - return await this.webhookService.processWebhook(webhookId, attempt); - } -} \ No newline at end of file diff --git a/src/modules/notifications/services/email.service.ts b/src/modules/notifications/services/email.service.ts deleted file mode 100644 index 439c01a..0000000 --- a/src/modules/notifications/services/email.service.ts +++ /dev/null @@ -1,98 +0,0 @@ -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; - 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', - }; - } -} \ No newline at end of file diff --git a/src/modules/notifications/services/notifications.service.ts b/src/modules/notifications/services/notifications.service.ts deleted file mode 100644 index 4043202..0000000 --- a/src/modules/notifications/services/notifications.service.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; -import { InjectQueue } from '@nestjs/bull'; -import bull from 'bull'; - import { SmsService } from './sms.service'; -import { EmailService } from './email.service'; -import { WebhookService } from './webhook.service'; -import { NotificationTemplateService } from './template.service'; -import { SendNotificationDto, BulkNotificationDto } from '../dto/notification.dto'; -import { PrismaService } from 'src/shared/services/prisma.service'; - -@Injectable() -export class NotificationsService { - constructor( - private readonly prisma: PrismaService, - private readonly smsService: SmsService, - private readonly emailService: EmailService, - private readonly webhookService: WebhookService, - private readonly templateService: NotificationTemplateService, - @InjectQueue('notifications') private notificationQueue: bull.Queue, - ) {} - - async send(partnerId: string, dto: SendNotificationDto) { - // Valider l'utilisateur si fourni - let user:any = null; - if (dto.userToken) { - user = await this.prisma.user.findFirst({ - where: { - userToken: dto.userToken, - partnerId: partnerId, - }, - include: { operator: true }, - }); - - if (!user) { - throw new BadRequestException('Invalid user token'); - } - } - - // Créer l'enregistrement de notification - const notification = await this.prisma.notification.create({ - data: { - partnerId: partnerId, - userId: user?.id, - type: dto.type, - channel: dto.channel, - recipient: dto.recipient || user?.msisdn, - subject: dto.subject, - content: dto.content, - templateId: dto.templateId, - status: 'PENDING', - metadata: dto.metadata, - scheduledFor: dto.scheduledFor, - }, - }); - - // Si programmée pour plus tard, ajouter à la queue avec délai - if (dto.scheduledFor && new Date(dto.scheduledFor) > new Date()) { - const delay = new Date(dto.scheduledFor).getTime() - Date.now(); - await this.notificationQueue.add( - 'send-notification', - { notificationId: notification.id }, - { delay }, - ); - } else { - // Envoyer immédiatement - await this.processNotification(notification.id); - } - - return notification; - } - - async sendBulk(partnerId: string, dto: BulkNotificationDto) { - const notifications :any= []; - - // Récupérer les destinataires selon les critères - const recipients:any = await this.getRecipients(partnerId, dto); - - for (const recipient of recipients) { - const notification:any = await this.prisma.notification.create({ - data: { - partnerId: partnerId, - userId: recipient.userId, - type: dto.type, - channel: dto.channel, - recipient: recipient.contact, - subject: dto.subject, - content:'{}', - //content: await this.templateService.render(dto.templateId, { - // ...recipient.data, - // ...dto.variables, - //}), - templateId: dto.templateId, - status: 'PENDING', - batchId: dto.batchId, - metadata: dto.metadata, - }, - }); - - notifications.push(notification); - - // Ajouter à la queue avec rate limiting - await this.notificationQueue.add( - 'send-notification', - { notificationId: notification.id }, - { - delay: notifications.length * 100, // 100ms entre chaque envoi - attempts: 3, - }, - ); - } - - return { - batchId: dto.batchId, - totalRecipients: notifications.length, - status: 'PROCESSING', - }; - } - - async processNotification(notificationId: string) { - const notification = await this.prisma.notification.findUnique({ - where: { id: notificationId }, - include: { - user: true, - partner: true, - }, - }); - - if (!notification) { - throw new BadRequestException('Notification not found'); - } - - try { - let result; - - switch (notification.channel) { - case 'SMS': - result = await this.smsService.send({ - to: notification.recipient, - message: notification.content, - userToken: notification.user?.userToken, - userAlias: notification.user?.userAlias, - //from: notification.metadata?.from, - }); - break; - - case 'EMAIL': - result = await this.emailService.send({ - to: notification.recipient, - // subject: notification.subject, - content: notification.content, - //template: notification.templateId, - message: '' - }); - break; - - case 'WEBHOOK': - result = await this.webhookService.send({ - url: notification.recipient, - event: notification.type, - payload: { - subject: notification.subject, - content: notification.content, - metadata: notification.metadata, - }, - }); - break; - - default: - throw new BadRequestException(`Unsupported channel: ${notification.channel}`); - } - - // Mettre à jour le statut - await this.prisma.notification.update({ - where: { id: notificationId }, - data: { - status: 'SENT', - sentAt: new Date(), - response: result, - }, - }); - - return result; - } catch (error) { - await this.prisma.notification.update({ - where: { id: notificationId }, - data: { - status: 'FAILED', - failureReason: error.message, - failedAt: new Date(), - }, - }); - throw error; - } - } - - async getStatus(notificationId: string, partnerId: string) { - const notification = await this.prisma.notification.findFirst({ - where: { - id: notificationId, - partnerId: partnerId, - }, - }); - - if (!notification) { - throw new BadRequestException('Notification not found'); - } - - return notification; - } - - async getBatchStatus(batchId: string, partnerId: string) { - const notifications = await this.prisma.notification.findMany({ - where: { - batchId: batchId, - partnerId: partnerId, - }, - select: { - status: true, - }, - }); - - const statusCount = notifications.reduce((acc, n) => { - acc[n.status] = (acc[n.status] || 0) + 1; - return acc; - }, {}); - - return { - batchId, - total: notifications.length, - statusBreakdown: statusCount, - }; - } - - private async getRecipients(partnerId: string, dto: BulkNotificationDto) { - const recipients:any = []; - - if (dto.userIds && dto.userIds.length > 0) { - const users = await this.prisma.user.findMany({ - where: { - id: { in: dto.userIds }, - partnerId: partnerId, - }, - }); - - for (const user of users) { - recipients.push({ - userId: user.id, - contact: dto.channel === 'SMS' ? user.msisdn : user.msisdn, - data: { name: user.msisdn, msisdn: user.msisdn }, - }); - } - } - - if (dto.segments) { - // Logique pour récupérer les utilisateurs par segments - const segmentUsers = await this.getSegmentUsers(partnerId, dto.segments); - recipients.push(...segmentUsers); - } - - return recipients; - } - - private async getSegmentUsers(partnerId: string, segments: string[]) { - const users:any = []; - - for (const segment of segments) { - switch (segment) { - case 'ACTIVE_SUBSCRIBERS': - const activeUsers = await this.prisma.user.findMany({ - where: { - partnerId: partnerId, - subscriptions: { - some: { - status: 'ACTIVE', - }, - }, - }, - }); - users.push(...activeUsers.map(u => ({ - userId: u.id, - contact: u.msisdn, - data: { name: u.msisdn, msisdn: u.msisdn }, - }))); - break; - - // Ajouter d'autres segments - } - } - - return users; - } -} \ No newline at end of file diff --git a/src/modules/notifications/services/sms.service.ts b/src/modules/notifications/services/sms.service.ts deleted file mode 100644 index ebe4c19..0000000 --- a/src/modules/notifications/services/sms.service.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { OperatorsService } from '../../operators/operators.service'; -import { PrismaService } from '../../../shared/services/prisma.service'; - -@Injectable() -export class SmsService { - constructor( - private readonly operatorsService: OperatorsService, - private readonly prisma: PrismaService, - private readonly configService: ConfigService, - ) {} - - async send(params: { - to: string; - message: 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; - 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', - }; - } -} \ No newline at end of file diff --git a/src/modules/notifications/services/template.service.ts b/src/modules/notifications/services/template.service.ts deleted file mode 100644 index 5a35d24..0000000 --- a/src/modules/notifications/services/template.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class NotificationTemplateService{ - render(templateId: string | undefined, arg1: any) { - throw new Error('Method not implemented.'); - } - -} \ No newline at end of file diff --git a/src/modules/notifications/services/webhook.service.ts b/src/modules/notifications/services/webhook.service.ts deleted file mode 100644 index b6046d8..0000000 --- a/src/modules/notifications/services/webhook.service.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { InjectQueue } from '@nestjs/bull'; -import bull from 'bull'; -import { firstValueFrom } from 'rxjs'; -import { PrismaService } from '../../../shared/services/prisma.service'; -import * as crypto from 'crypto'; - -@Injectable() -export class WebhookService { - constructor( - private readonly httpService: HttpService, - private readonly prisma: PrismaService, - @InjectQueue('webhooks') private webhookQueue: bull.Queue, - ) {} - - async send(params: { - url: string; - event: string; - payload: any; - partnerId?: string; - retries?: number; - }) { - const webhook = await this.prisma.webhook.create({ - data: { - url: params.url, - event: params.event, - payload: params.payload, - status: 'PENDING', - partnerId: params.partnerId, - }, - }); - - // Ajouter à la queue - await this.webhookQueue.add( - 'send-webhook', - { - webhookId: webhook.id, - attempt: 1, - }, - { - attempts: params.retries || 3, - backoff: { - type: 'exponential', - delay: 2000, - }, - }, - ); - - return webhook; - } - - async processWebhook(webhookId: string, attempt: number) { - const webhook = await this.prisma.webhook.findUnique({ - where: { id: webhookId }, - include: { partner: true }, - }); - - if (!webhook) { - throw new Error('Webhook not found'); - } - - try { - const signature = this.generateSignature( - webhook.payload, - webhook.partner?.secretKey as string, - ); - - const response = await firstValueFrom( - this.httpService.post(webhook.url, webhook.payload, { - headers: { - 'Content-Type': 'application/json', - 'X-Webhook-Event': webhook.event, - 'X-Webhook-Signature': signature, - 'X-Webhook-Timestamp': new Date().toISOString(), - 'X-Webhook-Attempt': attempt.toString(), - }, - timeout: 10000, - }), - ); - - await this.prisma.webhook.update({ - where: { id: webhookId }, - data: { - status: 'SUCCESS', - response: response.data, - //responseCode: response.status, - //deliveredAt: new Date(), - attempts: attempt, - }, - }); - - return response.data; - } catch (error) { - await this.prisma.webhook.update({ - where: { id: webhookId }, - data: { - status: attempt >= 3 ? 'FAILED' : 'RETRYING', - //lastError: error.message, - attempts: attempt, - lastAttempt: new Date(), - }, - }); - - if (attempt >= 3) { - // Notifier l'échec définitif - await this.notifyWebhookFailure(webhook); - } - - throw error; - } - } - - private generateSignature(payload: any, secret: string): string { - if (!secret) return ''; - - const hmac = crypto.createHmac('sha256', secret); - hmac.update(JSON.stringify(payload)); - return hmac.digest('hex'); - } - - private async notifyWebhookFailure(webhook: any) { - // Envoyer un email ou une notification au partenaire - console.error(`Webhook failed after max retries: ${webhook.id}`); - } -} \ No newline at end of file diff --git a/src/modules/operators/operators.service.ts b/src/modules/operators/operators.service.ts index 8710deb..dde224c 100644 --- a/src/modules/operators/operators.service.ts +++ b/src/modules/operators/operators.service.ts @@ -149,7 +149,7 @@ export class OperatorsService{ message: 'Connection successful', details: { sessionId: result.sessionId, - authMethod: result.authMethod, + // authMethod: result.authMethod, }, }; } catch (error) { @@ -385,7 +385,7 @@ export class OperatorsService{ return country; } } - return null; + return ""; } private detectOperatorFromPrefix(msisdn: string, countryCode: string): string { diff --git a/src/modules/partners/dto/partner.dto.ts b/src/modules/partners/dto/partner.dto.ts deleted file mode 100644 index 8bd74a7..0000000 --- a/src/modules/partners/dto/partner.dto.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { - IsString, - IsEmail, - IsOptional, - IsObject, - MinLength, - IsUrl, -} from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class CreatePartnerDto { - @ApiProperty() - @IsString() - name: string; - - @ApiProperty() - @IsEmail() - email: string; - - @ApiProperty() - @IsString() - @MinLength(8) - password: string; - - @ApiProperty() - @IsString() - country: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsObject() - companyInfo?: { - legalName: string; - taxId: string; - address: string; - phone?: string; - website?: string; - }; - - @ApiProperty({ required: false }) - @IsOptional() - metadata?: Record; -} - -export class UpdateCallbacksDto { - @ApiProperty({ required: false }) - @IsOptional() - headerEnrichment?: { - url: string; - method: string; - headers?: Record; - }; - - @ApiProperty({ required: false }) - @IsOptional() - subscription?: { - onCreate?: string; - onRenew?: string; - onCancel?: string; - onExpire?: string; - }; - - @ApiProperty({ required: false }) - @IsOptional() - payment?: { - onSuccess?: string; - onFailure?: string; - onRefund?: string; - }; - - @ApiProperty({ required: false }) - @IsOptional() - authentication?: { - onSuccess?: string; - onFailure?: string; - }; - - @IsOptional() - @IsUrl() - success?: string; - - @IsOptional() - @IsUrl() - cancel?: string; - - @IsOptional() - @IsUrl() - webhook?: string; -} - -export class LoginPartnerDto { - @ApiProperty() - @IsEmail() - email: string; - - @ApiProperty() - @IsString() - password: string; -} - -export class ChangePasswordDto { - @ApiProperty() - @IsString() - currentPassword: string; - - @ApiProperty() - @IsString() - @MinLength(8) - newPassword: string; -} - -export class UpdatePartnerDto { - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - name?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsObject() - companyInfo?: { - legalName?: string; - taxId?: string; - address?: string; - phone?: string; - website?: string; - }; - - @ApiProperty({ required: false }) - @IsOptional() - metadata?: Record; -} - -export class PartnerResponseDto { - @ApiProperty() - id: string; - - @ApiProperty() - name: string; - - @ApiProperty() - email: string; - - @ApiProperty() - status: string; - - @ApiProperty() - apiKey: string; - - @ApiProperty() - country: string; - - @ApiProperty({ required: false }) - companyInfo?: any; - - @ApiProperty() - createdAt: Date; -} - -export class PartnerStatsResponseDto { - @ApiProperty() - totalUsers: number; - - @ApiProperty() - activeSubscriptions: number; - - @ApiProperty() - todayPayments: number; - - @ApiProperty() - monthRevenue: number; - - @ApiProperty() - successRate: number; - - @ApiProperty() - avgPaymentAmount: number; -} diff --git a/src/modules/partners/partners.controller.ts b/src/modules/partners/partners.controller.ts deleted file mode 100644 index fd17797..0000000 --- a/src/modules/partners/partners.controller.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { - Controller, - Post, - Get, - Put, - Delete, - Body, - Param, - Query, - UseGuards, - Request, - HttpCode, - HttpStatus, - Patch, -} from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiBearerAuth, - ApiResponse, - ApiQuery, -} from '@nestjs/swagger'; -import { PartnersService } from './partners.service'; -import { - CreatePartnerDto, - UpdatePartnerDto, - UpdateCallbacksDto, - LoginPartnerDto, - ChangePasswordDto, - PartnerResponseDto, - PartnerStatsResponseDto, -} from './dto/partner.dto'; -import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; -import { Public } from '../../common/decorators/public.decorator'; -import { AuthService } from '../auth/auth.service'; - -@ApiTags('partners') -@Controller('partners') -export class PartnersController { - constructor( - private readonly partnersService: PartnersService, - private readonly authService: AuthService, - ) {} - - @Post('register') - @Public() - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: 'Register a new partner' }) - @ApiResponse({ - status: 201, - description: 'Partner registered successfully', - type: PartnerResponseDto, - }) - @ApiResponse({ status: 409, description: 'Email already exists' }) - async register(@Body() createPartnerDto: CreatePartnerDto) { - return this.partnersService.register(createPartnerDto); - } - - @Post('login') - @Public() - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Partner login' }) - @ApiResponse({ - status: 200, - description: 'Login successful', - }) - @ApiResponse({ status: 401, description: 'Invalid credentials' }) - async login(@Body() loginDto: LoginPartnerDto) { - return this.authService.loginPartner(loginDto); - } - - @Get('profile') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get partner profile' }) - @ApiResponse({ - status: 200, - description: 'Partner profile retrieved', - type: PartnerResponseDto, - }) - async getProfile(@Request() req) { - return this.partnersService.getPartner(req.user.partnerId); - } - - @Put('profile') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Update partner profile' }) - @ApiResponse({ - status: 200, - description: 'Profile updated successfully', - type: PartnerResponseDto, - }) - async updateProfile( - @Request() req, - @Body() updatePartnerDto: UpdatePartnerDto, - ) { - return this.partnersService.updatePartner( - req.user.partnerId, - updatePartnerDto, - ); - } - - @Put('callbacks') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Update callback URLs' }) - @ApiResponse({ - status: 200, - description: 'Callbacks updated successfully', - }) - async updateCallbacks( - @Request() req, - @Body() updateCallbacksDto: UpdateCallbacksDto, - ) { - return this.partnersService.updateCallbacks( - req.user.partnerId, - updateCallbacksDto, - ); - } - - @Get('callbacks') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get callback configuration' }) - @ApiResponse({ - status: 200, - description: 'Callback configuration retrieved', - }) - async getCallbacks(@Request() req) { - return this.partnersService.getCallbacks(req.user.partnerId); - } - - @Post('keys/regenerate') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Regenerate API keys' }) - @ApiResponse({ - status: 200, - description: 'Keys regenerated successfully', - }) - async regenerateKeys(@Request() req) { - return this.partnersService.regenerateKeys(req.user.partnerId); - } - - @Get('statistics') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get partner statistics' }) - @ApiResponse({ - status: 200, - description: 'Statistics retrieved', - type: PartnerStatsResponseDto, - }) - async getStatistics(@Request() req) { - return this.partnersService.getPartnerStats(req.user.partnerId); - } - - @Get('statistics/dashboard') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get dashboard statistics' }) - @ApiQuery({ name: 'period', required: false, enum: ['daily', 'weekly', 'monthly', 'yearly'] }) - async getDashboardStats( - @Request() req, - @Query('period') period: string = 'monthly', - ) { - return this.partnersService.getDashboardStats(req.user.partnerId, period); - } - - @Post('password/change') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Change password' }) - @ApiResponse({ - status: 200, - description: 'Password changed successfully', - }) - async changePassword( - @Request() req, - @Body() changePasswordDto: ChangePasswordDto, - ) { - return this.partnersService.changePassword( - req.user.partnerId, - changePasswordDto, - ); - } - - @Get('users') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'List partner users' }) - @ApiQuery({ name: 'page', required: false, type: Number }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'status', required: false }) - async listUsers( - @Request() req, - @Query('page') page: number = 1, - @Query('limit') limit: number = 20, - @Query('status') status?: string, - ) { - return this.partnersService.listPartnerUsers(req.user.partnerId, { - page, - limit, - status, - }); - } - - @Get('users/:userId') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get user details' }) - async getUserDetails(@Request() req, @Param('userId') userId: string) { - return this.partnersService.getUserDetails(req.user.partnerId, userId); - } - - @Get('transactions') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get transaction history' }) - @ApiQuery({ name: 'startDate', required: false, type: Date }) - @ApiQuery({ name: 'endDate', required: false, type: Date }) - @ApiQuery({ name: 'page', required: false, type: Number }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - async getTransactions( - @Request() req, - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - @Query('page') page: number = 1, - @Query('limit') limit: number = 20, - ) { - return this.partnersService.getTransactionHistory(req.user.partnerId, { - startDate: startDate ? new Date(startDate) : undefined, - endDate: endDate ? new Date(endDate) : undefined, - page, - limit, - }); - } - - @Get('revenue') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get revenue analytics' }) - @ApiQuery({ name: 'period', required: false, enum: ['daily', 'weekly', 'monthly', 'yearly'] }) - @ApiQuery({ name: 'groupBy', required: false, enum: ['day', 'week', 'month', 'operator', 'country'] }) - async getRevenue( - @Request() req, - @Query('period') period: string = 'monthly', - @Query('groupBy') groupBy: string = 'day', - ) { - return this.partnersService.getRevenueAnalytics(req.user.partnerId, { - period, - groupBy, - }); - } - - @Post('webhook/test') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Test webhook configuration' }) - async testWebhook( - @Request() req, - @Body() body: { url: string; event: string }, - ) { - return this.partnersService.testWebhook( - req.user.partnerId, - body.url, - body.event, - ); - } - - @Get('settings') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get partner settings' }) - async getSettings(@Request() req) { - return this.partnersService.getSettings(req.user.partnerId); - } - - @Put('settings') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Update partner settings' }) - async updateSettings(@Request() req, @Body() settings: any) { - return this.partnersService.updateSettings(req.user.partnerId, settings); - } - - @Get('api-usage') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get API usage statistics' }) - @ApiQuery({ name: 'period', required: false, enum: ['hourly', 'daily', 'weekly', 'monthly'] }) - async getApiUsage( - @Request() req, - @Query('period') period: string = 'daily', - ) { - return this.partnersService.getApiUsageStats(req.user.partnerId, period); - } - - @Get('logs') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get activity logs' }) - @ApiQuery({ name: 'type', required: false }) - @ApiQuery({ name: 'page', required: false, type: Number }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - async getActivityLogs( - @Request() req, - @Query('type') type?: string, - @Query('page') page: number = 1, - @Query('limit') limit: number = 50, - ) { - return this.partnersService.getActivityLogs(req.user.partnerId, { - type, - page, - limit, - }); - } - - @Delete('account') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Delete partner account' }) - @ApiResponse({ - status: 200, - description: 'Account deletion initiated', - }) - async deleteAccount( - @Request() req, - @Body() body: { password: string; reason?: string }, - ) { - return this.partnersService.deleteAccount( - req.user.partnerId, - body.password, - body.reason, - ); - } -} \ No newline at end of file diff --git a/src/modules/partners/partners.module.ts b/src/modules/partners/partners.module.ts deleted file mode 100644 index bd1f5f6..0000000 --- a/src/modules/partners/partners.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { PartnersController } from './partners.controller'; -import { PartnersService } from './partners.service'; -import { PrismaService } from '../../shared/services/prisma.service'; -import { AuthModule } from '../auth/auth.module'; - -@Module({ - imports:[AuthModule], - controllers: [PartnersController], - providers: [PartnersService, PrismaService], - exports: [PartnersService], -}) -export class PartnersModule {} diff --git a/src/modules/partners/partners.service.ts b/src/modules/partners/partners.service.ts deleted file mode 100644 index e6f1bf1..0000000 --- a/src/modules/partners/partners.service.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { - Injectable, - ConflictException, - NotFoundException, -} from '@nestjs/common'; -import { PrismaService } from '../../shared/services/prisma.service'; -import * as bcrypt from 'bcrypt'; -import * as crypto from 'crypto'; -import { ChangePasswordDto, CreatePartnerDto, UpdateCallbacksDto, UpdatePartnerDto } from './dto/partner.dto'; -import { Prisma } from 'generated/prisma'; - -@Injectable() -export class PartnersService { - getActivityLogs(partnerId: any, arg1: { type: string | undefined; page: number; limit: number; }) { - throw new Error('Method not implemented.'); - } - getApiUsageStats(partnerId: any, period: string) { - throw new Error('Method not implemented.'); - } - getSettings(partnerId: any) { - throw new Error('Method not implemented.'); - } - updateSettings(partnerId: any, settings: any) { - throw new Error('Method not implemented.'); - } - getTransactionHistory(partnerId: any, arg1: { startDate: Date | undefined; endDate: Date | undefined; page: number; limit: number; }) { - throw new Error('Method not implemented.'); - } - listPartnerUsers(partnerId: any, arg1: { page: number; limit: number; status: string | undefined; }) { - throw new Error('Method not implemented.'); - } - testWebhook(partnerId: any, url: string, event: string) { - throw new Error('Method not implemented.'); - } - getRevenueAnalytics(partnerId: any, arg1: { period: string; groupBy: string; }) { - throw new Error('Method not implemented.'); - } - getCallbacks(partnerId: any) { - throw new Error('Method not implemented.'); - } - getUserDetails(partnerId: any, userId: string) { - throw new Error('Method not implemented.'); - } - changePassword(partnerId: any, changePasswordDto: ChangePasswordDto) { - throw new Error('Method not implemented.'); - } - getDashboardStats(partnerId: any, period: string) { - throw new Error('Method not implemented.'); - } - updatePartner(partnerId: any, updatePartnerDto: UpdatePartnerDto) { - throw new Error('Method not implemented.'); - } - deleteAccount(partnerId: any, password: string, reason: string | undefined) { - throw new Error('Method not implemented.'); - } - constructor(private readonly prisma: PrismaService) {} - - async register(dto: CreatePartnerDto) { - // Vérifier si l'email existe déjà - const existingPartner = await this.prisma.partner.findUnique({ - where: { email: dto.email }, - }); - - if (existingPartner) { - throw new ConflictException('Email already registered'); - } - - // Générer les clés API - const apiKey = this.generateApiKey(); - const secretKey = this.generateSecretKey(); - - // Hasher le mot de passe - const passwordHash = await bcrypt.hash(dto.password, 10); - - // Créer le partenaire - const partner = await this.prisma.partner.create({ - data: { - name: dto.name, - email: dto.email, - passwordHash: passwordHash, - apiKey: apiKey, - secretKey: secretKey, - status: 'PENDING', - companyInfo: dto.companyInfo, - country: dto.country, - metadata: dto.metadata, - }, - }); - - return { - partnerId: partner.id, - apiKey: partner.apiKey, - secretKey: partner.secretKey, - status: partner.status, - message: 'Partner registered successfully. Awaiting approval.', - }; - } - - async updateCallbacks(partnerId: string, dto: UpdateCallbacksDto) { - const partner = await this.prisma.partner.findUnique({ - where: { id: partnerId }, - }); - - if (!partner) { - throw new NotFoundException('Partner not found'); - } - - const updatedPartner = await this.prisma.partner.update({ - where: { id: partnerId }, - data: { - //callbacks: dto as unknown as Prisma.JsonValue, - // ou - callbacks: JSON.parse(JSON.stringify(dto)), - }, - }); - - return { - partnerId: updatedPartner.id, - callbacks: updatedPartner.callbacks, - }; - } - - async getPartner(partnerId: string) { - const partner = await this.prisma.partner.findUnique({ - where: { id: partnerId }, - select: { - id: true, - name: true, - email: true, - status: true, - callbacks: true, - companyInfo: true, - createdAt: true, - _count: { - select: { - users: true, - subscriptions: true, - payments: true, - }, - }, - }, - }); - - if (!partner) { - throw new NotFoundException('Partner not found'); - } - - return partner; - } - - async getPartnerStats(partnerId: string) { - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const [totalUsers, activeSubscriptions, todayPayments, monthRevenue] = - await Promise.all([ - this.prisma.user.count({ - where: { partnerId }, - }), - this.prisma.subscription.count({ - where: { - partnerId, - status: 'ACTIVE', - }, - }), - this.prisma.payment.count({ - where: { - partnerId, - createdAt: { gte: today }, - }, - }), - this.prisma.payment.aggregate({ - where: { - partnerId, - status: 'SUCCESS', - createdAt: { - gte: new Date(today.getFullYear(), today.getMonth(), 1), - }, - }, - _sum: { - amount: true, - }, - }), - ]); - - return { - totalUsers, - activeSubscriptions, - todayPayments, - monthRevenue: monthRevenue._sum.amount || 0, - }; - } - - async regenerateKeys(partnerId: string) { - const partner = await this.prisma.partner.findUnique({ - where: { id: partnerId }, - }); - - if (!partner) { - throw new NotFoundException('Partner not found'); - } - - const newApiKey = this.generateApiKey(); - const newSecretKey = this.generateSecretKey(); - - const updatedPartner = await this.prisma.partner.update({ - where: { id: partnerId }, - data: { - apiKey: newApiKey, - secretKey: newSecretKey, - keysRotatedAt: new Date(), - }, - }); - - return { - apiKey: updatedPartner.apiKey, - secretKey: updatedPartner.secretKey, - message: 'Keys regenerated successfully', - }; - } - - private generateApiKey(): string { - return `pk_${crypto.randomBytes(32).toString('hex')}`; - } - - private generateSecretKey(): string { - return `sk_${crypto.randomBytes(32).toString('hex')}`; - } -} diff --git a/src/modules/payments/payments.controller.ts b/src/modules/payments/payments.controller.ts index 93e0b09..549684c 100644 --- a/src/modules/payments/payments.controller.ts +++ b/src/modules/payments/payments.controller.ts @@ -89,28 +89,7 @@ export class PaymentsController { return this.paymentsService.getPayment(paymentId, req.user.partnerId); } - @Get() - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'List payments' }) - @ApiQuery({ name: 'status', required: false, enum: ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED'] }) - @ApiQuery({ name: 'userId', required: false }) - @ApiQuery({ name: 'subscriptionId', required: false }) - @ApiQuery({ name: 'startDate', required: false, type: Date }) - @ApiQuery({ name: 'endDate', required: false, type: Date }) - @ApiQuery({ name: 'page', required: false, type: Number, default: 1 }) - @ApiQuery({ name: 'limit', required: false, type: Number, default: 20 }) - @ApiResponse({ - status: 200, - description: 'List of payments', - type: PaymentListResponseDto, - }) - async listPayments(@Request() req, @Query() query: PaymentQueryDto) { - return this.paymentsService.listPayments({ - partnerId: req.user.partnerId, - ...query, - }); - } + @Get('reference/:reference') @UseGuards(JwtAuthGuard) @@ -148,26 +127,7 @@ export class PaymentsController { return this.paymentsService.retryPayment(paymentId, req.user.partnerId); } - @Get('statistics/summary') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get payment statistics' }) - @ApiQuery({ name: 'period', required: false, enum: ['daily', 'weekly', 'monthly', 'yearly'] }) - @ApiQuery({ name: 'startDate', required: false, type: Date }) - @ApiQuery({ name: 'endDate', required: false, type: Date }) - async getStatistics( - @Request() req, - @Query('period') period?: string, - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - ) { - return this.paymentsService.getStatistics({ - partnerId: req.user.partnerId, - period: period || 'monthly', - startDate: startDate ? new Date(startDate) : undefined, - endDate: endDate ? new Date(endDate) : undefined, - }); - } + @Post('validate') @UseGuards(JwtAuthGuard) diff --git a/src/modules/subscriptions/subscriptions.controller.ts b/src/modules/subscriptions/subscriptions.controller.ts index 741583b..2ddb2ec 100644 --- a/src/modules/subscriptions/subscriptions.controller.ts +++ b/src/modules/subscriptions/subscriptions.controller.ts @@ -28,23 +28,7 @@ export class SubscriptionsController { return this.subscriptionsService.create(req.user.partnerId, dto); } - @Get() - @ApiOperation({ summary: 'List subscriptions' }) - async list( - @Request() req, - @Query('status') status?: string, - @Query('userId') userId?: string, - @Query('page') page = 1, - @Query('limit') limit = 20, - ) { - return this.subscriptionsService.list({ - partnerId: req.user.partnerId, - status, - userId, - page, - limit, - }); - } + @Get(':id') @ApiOperation({ summary: 'Get subscription details' }) diff --git a/src/modules/subscriptions/subscriptions.module.ts b/src/modules/subscriptions/subscriptions.module.ts index 24261b6..9b69b67 100644 --- a/src/modules/subscriptions/subscriptions.module.ts +++ b/src/modules/subscriptions/subscriptions.module.ts @@ -8,8 +8,7 @@ import { PlanService } from './services/plan.service'; import { BillingService } from './services/billing.service'; import { PrismaService } from '../../shared/services/prisma.service'; import { PaymentsModule } from '../payments/payments.module'; -import { NotificationsModule } from '../notifications/notifications.module'; -import { HttpModule } from '@nestjs/axios'; + import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ @@ -20,8 +19,7 @@ import { HttpModule } from '@nestjs/axios'; BullModule.registerQueue({ name: 'billing', }), - PaymentsModule, - NotificationsModule, + PaymentsModule, ], controllers: [SubscriptionsController], providers: [