fix error
This commit is contained in:
parent
3a0e3c466a
commit
3eae8d2805
@ -19,6 +19,7 @@ import { OperatorsModule } from './modules/operators/operators.module';
|
|||||||
import { PaymentsModule } from './modules/payments/payments.module';
|
import { PaymentsModule } from './modules/payments/payments.module';
|
||||||
import { SubscriptionsModule } from './modules/subscriptions/subscriptions.module';
|
import { SubscriptionsModule } from './modules/subscriptions/subscriptions.module';
|
||||||
import { NotificationsModule } from './modules/notifications/notifications.module';
|
import { NotificationsModule } from './modules/notifications/notifications.module';
|
||||||
|
import { OtpChallengeModule } from './modules/challenge/otp.challenge.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -56,6 +57,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul
|
|||||||
PaymentsModule,
|
PaymentsModule,
|
||||||
SubscriptionsModule,
|
SubscriptionsModule,
|
||||||
NotificationsModule,
|
NotificationsModule,
|
||||||
|
OtpChallengeModule
|
||||||
],
|
],
|
||||||
providers: [PrismaService],
|
providers: [PrismaService],
|
||||||
exports: [PrismaService],
|
exports: [PrismaService],
|
||||||
|
|||||||
@ -133,8 +133,7 @@ export class OrangeAdapter {
|
|||||||
): OtpChallengeResponseDto {
|
): OtpChallengeResponseDto {
|
||||||
const response: OtpChallengeResponseDto = {
|
const response: OtpChallengeResponseDto = {
|
||||||
challengeId: orangeResponse.challengeId || '',
|
challengeId: orangeResponse.challengeId || '',
|
||||||
merchantId: request.merchantId,
|
merchantId: request.merchantId,
|
||||||
transactionId: request.transactionId,
|
|
||||||
status: this.mapOrangeStatus(orangeResponse),
|
status: this.mapOrangeStatus(orangeResponse),
|
||||||
message: orangeResponse.message,
|
message: orangeResponse.message,
|
||||||
expiresIn: orangeResponse.expiresIn,
|
expiresIn: orangeResponse.expiresIn,
|
||||||
@ -219,8 +218,7 @@ export class OrangeAdapter {
|
|||||||
// En cas d'erreur, retourner une réponse avec le statut FAILED
|
// En cas d'erreur, retourner une réponse avec le statut FAILED
|
||||||
return {
|
return {
|
||||||
challengeId: '',
|
challengeId: '',
|
||||||
merchantId: request.merchantId,
|
merchantId: request.merchantId,
|
||||||
transactionId: request.transactionId,
|
|
||||||
status: OtpChallengeStatusEnum.FAILED,
|
status: OtpChallengeStatusEnum.FAILED,
|
||||||
error: {
|
error: {
|
||||||
code: 'ORANGE_API_ERROR',
|
code: 'ORANGE_API_ERROR',
|
||||||
@ -275,8 +273,7 @@ export class OrangeAdapter {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
challengeId,
|
challengeId,
|
||||||
merchantId: originalRequest.merchantId,
|
merchantId: originalRequest.merchantId,
|
||||||
transactionId: originalRequest.transactionId,
|
|
||||||
status: OtpChallengeStatusEnum.FAILED,
|
status: OtpChallengeStatusEnum.FAILED,
|
||||||
error: {
|
error: {
|
||||||
code: 'OTP_VERIFICATION_FAILED',
|
code: 'OTP_VERIFICATION_FAILED',
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import { Type } from 'class-transformer';
|
|||||||
export enum OtpMethodEnum {
|
export enum OtpMethodEnum {
|
||||||
SMS = 'SMS',
|
SMS = 'SMS',
|
||||||
USSD = 'USSD',
|
USSD = 'USSD',
|
||||||
IVR = 'IVR'
|
IVR = 'IVR',
|
||||||
|
OTP_SMS_AUTH= 'OTP-SMS-AUTH'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IdentifierTypeEnum {
|
export enum IdentifierTypeEnum {
|
||||||
@ -48,9 +49,6 @@ export class OtpChallengeRequestDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
merchantId: string;
|
merchantId: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
transactionId: string;
|
|
||||||
|
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => IdentifierDto)
|
@Type(() => IdentifierDto)
|
||||||
|
|||||||
@ -18,8 +18,7 @@ export class OtpChallengeResponseDto {
|
|||||||
merchantId: string;
|
merchantId: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
transactionId: string;
|
|
||||||
|
|
||||||
@IsEnum(OtpChallengeStatusEnum)
|
@IsEnum(OtpChallengeStatusEnum)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
HttpException,
|
HttpException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
Headers,
|
Headers,
|
||||||
Logger
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
@ -18,11 +18,15 @@ import {
|
|||||||
ApiParam,
|
ApiParam,
|
||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiHeader,
|
ApiHeader,
|
||||||
ApiBearerAuth
|
ApiBearerAuth,
|
||||||
} from '@nestjs/swagger';
|
} 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 { OtpChallengeRequestDto } from './dto/challenge.request.dto';
|
||||||
|
import { OtpChallengeService } from './otp.challenge.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller pour les endpoints de challenge OTP
|
* Controller pour les endpoints de challenge OTP
|
||||||
* Architecture: Merchant -> Hub (ce controller) -> Orange (via adapter)
|
* Architecture: Merchant -> Hub (ce controller) -> Orange (via adapter)
|
||||||
@ -41,37 +45,40 @@ export class OtpChallengeController {
|
|||||||
@Post('initiate')
|
@Post('initiate')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Initier un challenge OTP',
|
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({
|
@ApiResponse({
|
||||||
status: HttpStatus.CREATED,
|
status: HttpStatus.CREATED,
|
||||||
description: 'Challenge OTP initié avec succès',
|
description: 'Challenge OTP initié avec succès',
|
||||||
type: OtpChallengeResponseDto
|
type: OtpChallengeResponseDto,
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: HttpStatus.BAD_REQUEST,
|
status: HttpStatus.BAD_REQUEST,
|
||||||
description: 'Requête invalide'
|
description: 'Requête invalide',
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
description: 'Erreur serveur'
|
description: 'Erreur serveur',
|
||||||
})
|
})
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: 'X-Merchant-ID',
|
name: 'X-Merchant-ID',
|
||||||
description: 'Identifiant du merchant',
|
description: 'Identifiant du merchant',
|
||||||
required: true
|
required: true,
|
||||||
})
|
})
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: 'X-API-Key',
|
name: 'X-API-Key',
|
||||||
description: 'Clé API du merchant',
|
description: 'Clé API du merchant',
|
||||||
required: true
|
required: true,
|
||||||
})
|
})
|
||||||
async initiateChallenge(
|
async initiateChallenge(
|
||||||
@Body() request: OtpChallengeRequestDto,
|
@Body() request: OtpChallengeRequestDto,
|
||||||
@Headers('x-merchant-id') merchantId: string,
|
@Headers('X-Merchant-ID') merchantId: string,
|
||||||
@Headers('x-api-key') apiKey: string
|
@Headers('X-API-Key') apiKey: string,
|
||||||
): Promise<OtpChallengeResponseDto> {
|
): Promise<OtpChallengeResponseDto> {
|
||||||
this.logger.log(`[INITIATE] Merchant: ${merchantId}, Transaction: ${request.transactionId}`);
|
this.logger.log(
|
||||||
|
`[INITIATE] Challenge from: ${merchantId}, for customer: ${request.identifier.value}`,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Valider les headers
|
// Valider les headers
|
||||||
@ -81,7 +88,7 @@ export class OtpChallengeController {
|
|||||||
if (request.merchantId && request.merchantId !== merchantId) {
|
if (request.merchantId && request.merchantId !== merchantId) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
'Merchant ID in body does not match header',
|
'Merchant ID in body does not match header',
|
||||||
HttpStatus.BAD_REQUEST
|
HttpStatus.BAD_REQUEST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,11 +96,12 @@ export class OtpChallengeController {
|
|||||||
request.merchantId = merchantId;
|
request.merchantId = merchantId;
|
||||||
|
|
||||||
// Appeler le service
|
// Appeler le service
|
||||||
const response = await this.otpChallengeService.initiateChallenge(request);
|
const response =
|
||||||
|
await this.otpChallengeService.initiateChallenge(request);
|
||||||
|
|
||||||
// Logger le résultat
|
// Logger le résultat
|
||||||
this.logger.log(
|
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
|
// Si échec, retourner une erreur HTTP appropriée
|
||||||
@ -102,16 +110,16 @@ export class OtpChallengeController {
|
|||||||
{
|
{
|
||||||
statusCode: HttpStatus.BAD_REQUEST,
|
statusCode: HttpStatus.BAD_REQUEST,
|
||||||
message: response.error.message,
|
message: response.error.message,
|
||||||
error: response.error
|
error: response.error,
|
||||||
},
|
},
|
||||||
HttpStatus.BAD_REQUEST
|
HttpStatus.BAD_REQUEST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`[INITIATE] Error: ${error.message}`, error.stack);
|
this.logger.error(`[INITIATE] Error: ${error.message}`, error.stack);
|
||||||
|
|
||||||
if (error instanceof HttpException) {
|
if (error instanceof HttpException) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -120,9 +128,9 @@ export class OtpChallengeController {
|
|||||||
{
|
{
|
||||||
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
message: 'Failed to initiate OTP challenge',
|
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')
|
@Post(':challengeId/verify')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Vérifier un code OTP',
|
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({
|
@ApiParam({
|
||||||
name: 'challengeId',
|
name: 'challengeId',
|
||||||
description: 'Identifiant du challenge',
|
description: 'Identifiant du challenge',
|
||||||
type: String
|
type: String,
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: HttpStatus.OK,
|
status: HttpStatus.OK,
|
||||||
description: 'Code OTP vérifié avec succès',
|
description: 'Code OTP vérifié avec succès',
|
||||||
type: OtpChallengeResponseDto
|
type: OtpChallengeResponseDto,
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: HttpStatus.BAD_REQUEST,
|
status: HttpStatus.BAD_REQUEST,
|
||||||
description: 'Code OTP invalide'
|
description: 'Code OTP invalide',
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: HttpStatus.NOT_FOUND,
|
status: HttpStatus.NOT_FOUND,
|
||||||
description: 'Challenge non trouvé'
|
description: 'Challenge non trouvé',
|
||||||
})
|
})
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: 'X-Merchant-ID',
|
name: 'X-Merchant-ID',
|
||||||
description: 'Identifiant du merchant',
|
description: 'Identifiant du merchant',
|
||||||
required: true
|
required: true,
|
||||||
})
|
})
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: 'X-API-Key',
|
name: 'X-API-Key',
|
||||||
description: 'Clé API du merchant',
|
description: 'Clé API du merchant',
|
||||||
required: true
|
required: true,
|
||||||
})
|
})
|
||||||
async verifyOtp(
|
async verifyOtp(
|
||||||
@Param('challengeId') challengeId: string,
|
@Param('challengeId') challengeId: string,
|
||||||
@Body('otpCode') otpCode: string,
|
@Body('otpCode') otpCode: string,
|
||||||
@Headers('x-merchant-id') merchantId: string,
|
@Headers('x-merchant-id') merchantId: string,
|
||||||
@Headers('x-api-key') apiKey: string
|
@Headers('x-api-key') apiKey: string,
|
||||||
): Promise<OtpChallengeResponseDto> {
|
): Promise<OtpChallengeResponseDto> {
|
||||||
this.logger.log(`[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`);
|
this.logger.log(
|
||||||
|
`[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Valider les headers
|
// Valider les headers
|
||||||
@ -185,7 +195,7 @@ export class OtpChallengeController {
|
|||||||
const response = await this.otpChallengeService.verifyOtp(
|
const response = await this.otpChallengeService.verifyOtp(
|
||||||
challengeId,
|
challengeId,
|
||||||
otpCode,
|
otpCode,
|
||||||
merchantId
|
merchantId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Logger le résultat
|
// Logger le résultat
|
||||||
@ -193,18 +203,18 @@ export class OtpChallengeController {
|
|||||||
|
|
||||||
// Si échec, retourner une erreur appropriée
|
// Si échec, retourner une erreur appropriée
|
||||||
if (response.status === OtpChallengeStatusEnum.FAILED && response.error) {
|
if (response.status === OtpChallengeStatusEnum.FAILED && response.error) {
|
||||||
const statusCode =
|
const statusCode =
|
||||||
response.error.code === 'CHALLENGE_NOT_FOUND'
|
response.error.code === 'CHALLENGE_NOT_FOUND'
|
||||||
? HttpStatus.NOT_FOUND
|
? HttpStatus.NOT_FOUND
|
||||||
: HttpStatus.BAD_REQUEST;
|
: HttpStatus.BAD_REQUEST;
|
||||||
|
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{
|
{
|
||||||
statusCode,
|
statusCode,
|
||||||
message: response.error.message,
|
message: response.error.message,
|
||||||
error: response.error
|
error: response.error,
|
||||||
},
|
},
|
||||||
statusCode
|
statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,179 +230,9 @@ export class OtpChallengeController {
|
|||||||
{
|
{
|
||||||
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
message: 'Failed to verify OTP code',
|
message: 'Failed to verify OTP code',
|
||||||
error: error.message
|
error: error.message,
|
||||||
},
|
},
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR
|
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<OtpChallengeResponseDto> {
|
|
||||||
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<OtpChallengeResponseDto> {
|
|
||||||
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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -404,18 +244,18 @@ export class OtpChallengeController {
|
|||||||
if (!merchantId || merchantId.trim().length === 0) {
|
if (!merchantId || merchantId.trim().length === 0) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
'X-Merchant-ID header is required',
|
'X-Merchant-ID header is required',
|
||||||
HttpStatus.UNAUTHORIZED
|
HttpStatus.UNAUTHORIZED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!apiKey || apiKey.trim().length === 0) {
|
if (!apiKey || apiKey.trim().length === 0) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
'X-API-Key header is required',
|
'X-API-Key header is required',
|
||||||
HttpStatus.UNAUTHORIZED
|
HttpStatus.UNAUTHORIZED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implémenter la validation de l'API Key
|
// TODO: Implémenter la validation de l'API Key
|
||||||
// Cette logique devrait vérifier que l'API Key est valide pour le merchant
|
// Cette logique devrait vérifier que l'API Key est valide pour le merchant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { OtpChallengeRequestDto } from '../dtos/otp-challenge-request.dto';
|
import { OtpChallengeRequestDto } from "./dto/challenge.request.dto";
|
||||||
import { OtpChallengeResponseDto } from '../dtos/otp-challenge-response.dto';
|
import { OtpChallengeResponseDto } from "./dto/challenge.response.dto";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface générique pour le service de challenge OTP
|
* Interface générique pour le service de challenge OTP
|
||||||
@ -26,25 +28,6 @@ export interface IOtpChallengeService {
|
|||||||
merchantId: string
|
merchantId: string
|
||||||
): Promise<OtpChallengeResponseDto>;
|
): Promise<OtpChallengeResponseDto>;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<OtpChallengeResponseDto>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<OtpChallengeResponseDto>;
|
|
||||||
}
|
}
|
||||||
@ -23,24 +23,11 @@ export class OtpChallengeService implements IOtpChallengeService {
|
|||||||
* Initier un challenge OTP
|
* Initier un challenge OTP
|
||||||
*/
|
*/
|
||||||
async initiateChallenge(request: OtpChallengeRequestDto): Promise<OtpChallengeResponseDto> {
|
async initiateChallenge(request: OtpChallengeRequestDto): Promise<OtpChallengeResponseDto> {
|
||||||
try {
|
try {
|
||||||
// Valider la requête
|
|
||||||
this.validateRequest(request);
|
|
||||||
|
|
||||||
// Appeler l'adaptateur Orange
|
// Appeler l'adaptateur Orange
|
||||||
const response = await this.orangeAdapter.initiateChallenge(request);
|
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;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return this.createErrorResponse(request, 'INITIATE_FAILED', error.message);
|
return this.createErrorResponse(request, 'INITIATE_FAILED', error.message);
|
||||||
@ -62,8 +49,7 @@ export class OtpChallengeService implements IOtpChallengeService {
|
|||||||
if (!cached) {
|
if (!cached) {
|
||||||
return {
|
return {
|
||||||
challengeId,
|
challengeId,
|
||||||
merchantId,
|
merchantId,
|
||||||
transactionId: '',
|
|
||||||
status: OtpChallengeStatusEnum.FAILED,
|
status: OtpChallengeStatusEnum.FAILED,
|
||||||
error: {
|
error: {
|
||||||
code: 'CHALLENGE_NOT_FOUND',
|
code: 'CHALLENGE_NOT_FOUND',
|
||||||
@ -77,8 +63,7 @@ export class OtpChallengeService implements IOtpChallengeService {
|
|||||||
if (cached.request.merchantId !== merchantId) {
|
if (cached.request.merchantId !== merchantId) {
|
||||||
return {
|
return {
|
||||||
challengeId,
|
challengeId,
|
||||||
merchantId,
|
merchantId,
|
||||||
transactionId: cached.request.transactionId,
|
|
||||||
status: OtpChallengeStatusEnum.FAILED,
|
status: OtpChallengeStatusEnum.FAILED,
|
||||||
error: {
|
error: {
|
||||||
code: 'MERCHANT_MISMATCH',
|
code: 'MERCHANT_MISMATCH',
|
||||||
@ -104,8 +89,7 @@ export class OtpChallengeService implements IOtpChallengeService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
challengeId,
|
challengeId,
|
||||||
merchantId,
|
merchantId,
|
||||||
transactionId: '',
|
|
||||||
status: OtpChallengeStatusEnum.FAILED,
|
status: OtpChallengeStatusEnum.FAILED,
|
||||||
error: {
|
error: {
|
||||||
code: 'VERIFY_FAILED',
|
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<OtpChallengeResponseDto> {
|
|
||||||
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<OtpChallengeResponseDto> {
|
|
||||||
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
|
* 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
|
* Créer une réponse d'erreur
|
||||||
@ -255,8 +117,7 @@ export class OtpChallengeService implements IOtpChallengeService {
|
|||||||
): OtpChallengeResponseDto {
|
): OtpChallengeResponseDto {
|
||||||
return {
|
return {
|
||||||
challengeId: '',
|
challengeId: '',
|
||||||
merchantId: request.merchantId,
|
merchantId: request.merchantId,
|
||||||
transactionId: request.transactionId,
|
|
||||||
status: OtpChallengeStatusEnum.FAILED,
|
status: OtpChallengeStatusEnum.FAILED,
|
||||||
error: {
|
error: {
|
||||||
code,
|
code,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user