From 3eae8d28057176e50fd35cbd49ae56a6c23babfe Mon Sep 17 00:00:00 2001 From: "Mamadou Khoussa [028918 DSI/DAC/DIF/DS]" Date: Fri, 24 Oct 2025 23:34:57 +0000 Subject: [PATCH] fix error --- src/app.module.ts | 2 + .../challenge/adaptor/orange.adaptor.ts | 9 +- .../challenge/dto/challenge.request.dto.ts | 6 +- .../challenge/dto/challenge.response.dto.ts | 3 +- .../challenge/otp.challenge.controller.ts | 262 ++++-------------- .../challenge/otp.challenge.interface.ts | 29 +- .../challenge/otp.challenge.service.ts | 155 +---------- 7 files changed, 73 insertions(+), 393 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 412c7ef..526ef7f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -19,6 +19,7 @@ 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'; @Module({ imports: [ @@ -56,6 +57,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul PaymentsModule, SubscriptionsModule, NotificationsModule, + OtpChallengeModule ], providers: [PrismaService], exports: [PrismaService], diff --git a/src/modules/challenge/adaptor/orange.adaptor.ts b/src/modules/challenge/adaptor/orange.adaptor.ts index e7b175c..82f981b 100644 --- a/src/modules/challenge/adaptor/orange.adaptor.ts +++ b/src/modules/challenge/adaptor/orange.adaptor.ts @@ -133,8 +133,7 @@ export class OrangeAdapter { ): OtpChallengeResponseDto { const response: OtpChallengeResponseDto = { challengeId: orangeResponse.challengeId || '', - merchantId: request.merchantId, - transactionId: request.transactionId, + merchantId: request.merchantId, status: this.mapOrangeStatus(orangeResponse), message: orangeResponse.message, expiresIn: orangeResponse.expiresIn, @@ -219,8 +218,7 @@ export class OrangeAdapter { // En cas d'erreur, retourner une réponse avec le statut FAILED return { challengeId: '', - merchantId: request.merchantId, - transactionId: request.transactionId, + merchantId: request.merchantId, status: OtpChallengeStatusEnum.FAILED, error: { code: 'ORANGE_API_ERROR', @@ -275,8 +273,7 @@ export class OrangeAdapter { } catch (error) { return { challengeId, - merchantId: originalRequest.merchantId, - transactionId: originalRequest.transactionId, + merchantId: originalRequest.merchantId, status: OtpChallengeStatusEnum.FAILED, error: { code: 'OTP_VERIFICATION_FAILED', diff --git a/src/modules/challenge/dto/challenge.request.dto.ts b/src/modules/challenge/dto/challenge.request.dto.ts index 6220386..f4d8824 100644 --- a/src/modules/challenge/dto/challenge.request.dto.ts +++ b/src/modules/challenge/dto/challenge.request.dto.ts @@ -4,7 +4,8 @@ import { Type } from 'class-transformer'; export enum OtpMethodEnum { SMS = 'SMS', USSD = 'USSD', - IVR = 'IVR' + IVR = 'IVR', + OTP_SMS_AUTH= 'OTP-SMS-AUTH' } export enum IdentifierTypeEnum { @@ -48,9 +49,6 @@ export class OtpChallengeRequestDto { @IsNotEmpty() merchantId: string; - @IsString() - @IsNotEmpty() - transactionId: string; @ValidateNested() @Type(() => IdentifierDto) diff --git a/src/modules/challenge/dto/challenge.response.dto.ts b/src/modules/challenge/dto/challenge.response.dto.ts index 2574a52..ce9c5c9 100644 --- a/src/modules/challenge/dto/challenge.response.dto.ts +++ b/src/modules/challenge/dto/challenge.response.dto.ts @@ -18,8 +18,7 @@ export class OtpChallengeResponseDto { merchantId: string; @IsString() - @IsNotEmpty() - transactionId: string; + @IsNotEmpty() @IsEnum(OtpChallengeStatusEnum) @IsNotEmpty() diff --git a/src/modules/challenge/otp.challenge.controller.ts b/src/modules/challenge/otp.challenge.controller.ts index 3d2376d..74d0652 100644 --- a/src/modules/challenge/otp.challenge.controller.ts +++ b/src/modules/challenge/otp.challenge.controller.ts @@ -9,7 +9,7 @@ import { HttpException, UseGuards, Headers, - Logger + Logger, } from '@nestjs/common'; import { ApiTags, @@ -18,11 +18,15 @@ import { ApiParam, ApiQuery, ApiHeader, - ApiBearerAuth + ApiBearerAuth, } from '@nestjs/swagger'; -import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from './dto/challenge.response.dto'; +import { + OtpChallengeResponseDto, + OtpChallengeStatusEnum, +} from './dto/challenge.response.dto'; import { OtpChallengeRequestDto } from './dto/challenge.request.dto'; - +import { OtpChallengeService } from './otp.challenge.service'; + /** * Controller pour les endpoints de challenge OTP * Architecture: Merchant -> Hub (ce controller) -> Orange (via adapter) @@ -41,37 +45,40 @@ export class OtpChallengeController { @Post('initiate') @ApiOperation({ summary: 'Initier un challenge OTP', - description: 'Envoie un code OTP au numéro de téléphone spécifié via SMS, USSD ou IVR' + description: + 'Envoie un code OTP au numéro de téléphone spécifié via SMS, USSD ou IVR', }) @ApiResponse({ status: HttpStatus.CREATED, description: 'Challenge OTP initié avec succès', - type: OtpChallengeResponseDto + type: OtpChallengeResponseDto, }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Requête invalide' + description: 'Requête invalide', }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, - description: 'Erreur serveur' + description: 'Erreur serveur', }) @ApiHeader({ name: 'X-Merchant-ID', description: 'Identifiant du merchant', - required: true + required: true, }) @ApiHeader({ name: 'X-API-Key', description: 'Clé API du merchant', - required: true + required: true, }) async initiateChallenge( @Body() request: OtpChallengeRequestDto, - @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(`[INITIATE] Merchant: ${merchantId}, Transaction: ${request.transactionId}`); + this.logger.log( + `[INITIATE] Challenge from: ${merchantId}, for customer: ${request.identifier.value}`, + ); try { // Valider les headers @@ -81,7 +88,7 @@ export class OtpChallengeController { if (request.merchantId && request.merchantId !== merchantId) { throw new HttpException( 'Merchant ID in body does not match header', - HttpStatus.BAD_REQUEST + HttpStatus.BAD_REQUEST, ); } @@ -89,11 +96,12 @@ export class OtpChallengeController { request.merchantId = merchantId; // Appeler le service - const response = await this.otpChallengeService.initiateChallenge(request); + const response = + await this.otpChallengeService.initiateChallenge(request); // Logger le résultat this.logger.log( - `[INITIATE] Result - ChallengeId: ${response.challengeId}, Status: ${response.status}` + `[INITIATE] Result - ChallengeId: ${response.challengeId}, Status: ${response.status}`, ); // Si échec, retourner une erreur HTTP appropriée @@ -102,16 +110,16 @@ export class OtpChallengeController { { statusCode: HttpStatus.BAD_REQUEST, message: response.error.message, - error: response.error + error: response.error, }, - HttpStatus.BAD_REQUEST + HttpStatus.BAD_REQUEST, ); } return response; } catch (error) { this.logger.error(`[INITIATE] Error: ${error.message}`, error.stack); - + if (error instanceof HttpException) { throw error; } @@ -120,9 +128,9 @@ export class OtpChallengeController { { statusCode: HttpStatus.INTERNAL_SERVER_ERROR, message: 'Failed to initiate OTP challenge', - error: error.message + error: error.message, }, - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -134,43 +142,45 @@ export class OtpChallengeController { @Post(':challengeId/verify') @ApiOperation({ summary: 'Vérifier un code OTP', - description: 'Vérifie le code OTP entré par l\'utilisateur' + description: "Vérifie le code OTP entré par l'utilisateur", }) @ApiParam({ name: 'challengeId', description: 'Identifiant du challenge', - type: String + type: String, }) @ApiResponse({ status: HttpStatus.OK, description: 'Code OTP vérifié avec succès', - type: OtpChallengeResponseDto + type: OtpChallengeResponseDto, }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Code OTP invalide' + description: 'Code OTP invalide', }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Challenge non trouvé' + description: 'Challenge non trouvé', }) @ApiHeader({ name: 'X-Merchant-ID', description: 'Identifiant du merchant', - required: true + required: true, }) @ApiHeader({ name: 'X-API-Key', description: 'Clé API du merchant', - required: true + required: true, }) async verifyOtp( @Param('challengeId') challengeId: string, @Body('otpCode') otpCode: string, @Headers('x-merchant-id') merchantId: string, - @Headers('x-api-key') apiKey: string + @Headers('x-api-key') apiKey: string, ): Promise { - this.logger.log(`[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`); + this.logger.log( + `[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`, + ); try { // Valider les headers @@ -185,7 +195,7 @@ export class OtpChallengeController { const response = await this.otpChallengeService.verifyOtp( challengeId, otpCode, - merchantId + merchantId, ); // Logger le résultat @@ -193,18 +203,18 @@ export class OtpChallengeController { // Si échec, retourner une erreur appropriée if (response.status === OtpChallengeStatusEnum.FAILED && response.error) { - const statusCode = - response.error.code === 'CHALLENGE_NOT_FOUND' - ? HttpStatus.NOT_FOUND + const statusCode = + response.error.code === 'CHALLENGE_NOT_FOUND' + ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST; throw new HttpException( { statusCode, message: response.error.message, - error: response.error + error: response.error, }, - statusCode + statusCode, ); } @@ -220,179 +230,9 @@ export class OtpChallengeController { { statusCode: HttpStatus.INTERNAL_SERVER_ERROR, message: 'Failed to verify OTP code', - error: error.message + error: error.message, }, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - } - - /** - * Endpoint 3: Obtenir le statut d'un challenge - * GET /api/v1/otp-challenge/:challengeId/status - */ - @Get(':challengeId/status') - @ApiOperation({ - summary: 'Obtenir le statut d\'un challenge', - description: 'Récupère le statut actuel d\'un challenge OTP' - }) - @ApiParam({ - name: 'challengeId', - description: 'Identifiant du challenge', - type: String - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Statut du challenge récupéré avec succès', - type: OtpChallengeResponseDto - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Challenge non trouvé' - }) - @ApiHeader({ - name: 'X-Merchant-ID', - description: 'Identifiant du merchant', - required: true - }) - @ApiHeader({ - name: 'X-API-Key', - description: 'Clé API du merchant', - required: true - }) - async getChallengeStatus( - @Param('challengeId') challengeId: string, - @Headers('x-merchant-id') merchantId: string, - @Headers('x-api-key') apiKey: string - ): Promise { - this.logger.log(`[STATUS] Merchant: ${merchantId}, Challenge: ${challengeId}`); - - try { - // Valider les headers - this.validateMerchantHeaders(merchantId, apiKey); - - // Appeler le service - const response = await this.otpChallengeService.getChallengeStatus( - challengeId, - merchantId - ); - - // Si non trouvé - if (response.status === OtpChallengeStatusEnum.EXPIRED || - (response.error && response.error.code === 'CHALLENGE_NOT_FOUND')) { - throw new HttpException( - { - statusCode: HttpStatus.NOT_FOUND, - message: 'Challenge not found or expired', - error: response.error - }, - HttpStatus.NOT_FOUND - ); - } - - this.logger.log(`[STATUS] Result - Status: ${response.status}`); - return response; - } catch (error) { - this.logger.error(`[STATUS] Error: ${error.message}`, error.stack); - - if (error instanceof HttpException) { - throw error; - } - - throw new HttpException( - { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, - message: 'Failed to get challenge status', - error: error.message - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - } - - /** - * Endpoint 4: Renvoyer un code OTP - * POST /api/v1/otp-challenge/:challengeId/resend - */ - @Post(':challengeId/resend') - @ApiOperation({ - summary: 'Renvoyer un code OTP', - description: 'Renvoie un nouveau code OTP pour un challenge existant' - }) - @ApiParam({ - name: 'challengeId', - description: 'Identifiant du challenge', - type: String - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Code OTP renvoyé avec succès', - type: OtpChallengeResponseDto - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Challenge non trouvé' - }) - @ApiHeader({ - name: 'X-Merchant-ID', - description: 'Identifiant du merchant', - required: true - }) - @ApiHeader({ - name: 'X-API-Key', - description: 'Clé API du merchant', - required: true - }) - async resendOtp( - @Param('challengeId') challengeId: string, - @Headers('x-merchant-id') merchantId: string, - @Headers('x-api-key') apiKey: string - ): Promise { - this.logger.log(`[RESEND] Merchant: ${merchantId}, Challenge: ${challengeId}`); - - try { - // Valider les headers - this.validateMerchantHeaders(merchantId, apiKey); - - // Appeler le service - const response = await this.otpChallengeService.resendOtp( - challengeId, - merchantId - ); - - // Si échec - if (response.status === OtpChallengeStatusEnum.FAILED && response.error) { - const statusCode = - response.error.code === 'CHALLENGE_NOT_FOUND' - ? HttpStatus.NOT_FOUND - : HttpStatus.BAD_REQUEST; - - throw new HttpException( - { - statusCode, - message: response.error.message, - error: response.error - }, - statusCode - ); - } - - this.logger.log(`[RESEND] Result - New ChallengeId: ${response.challengeId}`); - return response; - } catch (error) { - this.logger.error(`[RESEND] Error: ${error.message}`, error.stack); - - if (error instanceof HttpException) { - throw error; - } - - throw new HttpException( - { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, - message: 'Failed to resend OTP code', - error: error.message - }, - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -404,18 +244,18 @@ export class OtpChallengeController { if (!merchantId || merchantId.trim().length === 0) { throw new HttpException( 'X-Merchant-ID header is required', - HttpStatus.UNAUTHORIZED + HttpStatus.UNAUTHORIZED, ); } if (!apiKey || apiKey.trim().length === 0) { throw new HttpException( 'X-API-Key header is required', - HttpStatus.UNAUTHORIZED + HttpStatus.UNAUTHORIZED, ); } // TODO: Implémenter la validation de l'API Key // Cette logique devrait vérifier que l'API Key est valide pour le merchant } -} \ No newline at end of file +} diff --git a/src/modules/challenge/otp.challenge.interface.ts b/src/modules/challenge/otp.challenge.interface.ts index 44f48bb..5e49775 100644 --- a/src/modules/challenge/otp.challenge.interface.ts +++ b/src/modules/challenge/otp.challenge.interface.ts @@ -1,5 +1,7 @@ -import { OtpChallengeRequestDto } from '../dtos/otp-challenge-request.dto'; -import { OtpChallengeResponseDto } from '../dtos/otp-challenge-response.dto'; +import { OtpChallengeRequestDto } from "./dto/challenge.request.dto"; +import { OtpChallengeResponseDto } from "./dto/challenge.response.dto"; + + /** * Interface générique pour le service de challenge OTP @@ -26,25 +28,6 @@ export interface IOtpChallengeService { merchantId: string ): Promise; - /** - * Obtenir le statut d'un challenge - * @param challengeId - L'identifiant du challenge - * @param merchantId - L'identifiant du merchant - * @returns Le statut actuel du challenge - */ - getChallengeStatus( - challengeId: string, - merchantId: string - ): Promise; - - /** - * Renvoyer un code OTP - * @param challengeId - L'identifiant du challenge - * @param merchantId - L'identifiant du merchant - * @returns La réponse du renvoi - */ - resendOtp( - challengeId: string, - merchantId: string - ): Promise; + + } \ No newline at end of file diff --git a/src/modules/challenge/otp.challenge.service.ts b/src/modules/challenge/otp.challenge.service.ts index e70af31..4361e63 100644 --- a/src/modules/challenge/otp.challenge.service.ts +++ b/src/modules/challenge/otp.challenge.service.ts @@ -23,24 +23,11 @@ export class OtpChallengeService implements IOtpChallengeService { * Initier un challenge OTP */ async initiateChallenge(request: OtpChallengeRequestDto): Promise { - try { - // Valider la requête - this.validateRequest(request); + try { // Appeler l'adaptateur Orange const response = await this.orangeAdapter.initiateChallenge(request); - // Mettre en cache pour la vérification future - if (response.challengeId) { - 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); @@ -62,8 +49,7 @@ export class OtpChallengeService implements IOtpChallengeService { if (!cached) { return { challengeId, - merchantId, - transactionId: '', + merchantId, status: OtpChallengeStatusEnum.FAILED, error: { code: 'CHALLENGE_NOT_FOUND', @@ -77,8 +63,7 @@ export class OtpChallengeService implements IOtpChallengeService { if (cached.request.merchantId !== merchantId) { return { challengeId, - merchantId, - transactionId: cached.request.transactionId, + merchantId, status: OtpChallengeStatusEnum.FAILED, error: { code: 'MERCHANT_MISMATCH', @@ -104,8 +89,7 @@ export class OtpChallengeService implements IOtpChallengeService { } catch (error) { return { challengeId, - merchantId, - transactionId: '', + merchantId, status: OtpChallengeStatusEnum.FAILED, error: { code: 'VERIFY_FAILED', @@ -115,135 +99,13 @@ export class OtpChallengeService implements IOtpChallengeService { }; } } - - /** - * Obtenir le statut d'un challenge - */ - async getChallengeStatus( - challengeId: string, - merchantId: string - ): Promise { - const cached = this.challengeCache.get(challengeId); - - if (!cached) { - return { - challengeId, - merchantId, - transactionId: '', - status: OtpChallengeStatusEnum.EXPIRED, - error: { - code: 'CHALLENGE_NOT_FOUND', - message: 'Challenge not found or expired' - } - }; - } - - // Vérifier que le merchantId correspond - if (cached.request.merchantId !== merchantId) { - return { - challengeId, - merchantId, - transactionId: cached.request.transactionId, - status: OtpChallengeStatusEnum.FAILED, - error: { - code: 'MERCHANT_MISMATCH', - message: 'Merchant ID does not match' - } - }; - } - - return cached.response; - } - - /** - * Renvoyer un code OTP - */ - async resendOtp( - challengeId: string, - merchantId: string - ): Promise { - try { - // Récupérer le challenge depuis le cache - const cached = this.challengeCache.get(challengeId); - - if (!cached) { - return { - challengeId, - merchantId, - transactionId: '', - status: OtpChallengeStatusEnum.FAILED, - error: { - code: 'CHALLENGE_NOT_FOUND', - message: 'Challenge not found or expired' - } - }; - } - - // Vérifier que le merchantId correspond - if (cached.request.merchantId !== merchantId) { - return { - challengeId, - merchantId, - transactionId: cached.request.transactionId, - status: OtpChallengeStatusEnum.FAILED, - error: { - code: 'MERCHANT_MISMATCH', - message: 'Merchant ID does not match' - } - }; - } - - // Réinitier le challenge (nouvel envoi) - const response = await this.orangeAdapter.initiateChallenge(cached.request); - - // Mettre à jour le cache avec le nouveau challengeId - if (response.challengeId) { - // Supprimer l'ancien - this.challengeCache.delete(challengeId); - // Ajouter le nouveau - this.challengeCache.set(response.challengeId, { request: cached.request, response }); - } - - return response; - } catch (error) { - return { - challengeId, - merchantId, - transactionId: '', - status: OtpChallengeStatusEnum.FAILED, - error: { - code: 'RESEND_FAILED', - message: error.message, - description: 'Failed to resend OTP code' - } - }; - } - } + + /** * Valider la requête */ - private validateRequest(request: OtpChallengeRequestDto): void { - if (!request.merchantId) { - throw new Error('Merchant ID is required'); - } - - if (!request.transactionId) { - throw new Error('Transaction ID is required'); - } - - if (!request.identifier || !request.identifier.type || !request.identifier.value) { - throw new Error('Valid identifier is required'); - } - - if (!request.country) { - throw new Error('Country is required'); - } - - if (!request.service) { - throw new Error('Service is required'); - } - } + /** * Créer une réponse d'erreur @@ -255,8 +117,7 @@ export class OtpChallengeService implements IOtpChallengeService { ): OtpChallengeResponseDto { return { challengeId: '', - merchantId: request.merchantId, - transactionId: request.transactionId, + merchantId: request.merchantId, status: OtpChallengeStatusEnum.FAILED, error: { code,