Architecture NXPay : API Gateway + Orchestrator Service
Contexte
Notre plateforme est composée de plusieurs microservices. Certaines opérations métier (création de compte organization, transferts, onboarding KYC) impliquent plusieurs services et doivent être orchestrées de manière cohérente.
Problèmes actuels
- Les clients (mobile/web) peuvent potentiellement accéder directement à chaque microservice
- Les workflows multi-services ne sont pas centralisés
- Difficulté à gérer les rollbacks en cas d'échec partiel
- Pas de vision unifiée des processus métier
Architecture Cible
┌─────────────────┐
│ Mobile App │
│ Web Dashboard │
└────────┬────────┘
│
▼
┌────────────────────────────────────┐
│ API GATEWAY │
│ ┌──────────────────────────────┐ │
│ │ • Authentification (JWT) │ │
│ │ • Rate Limiting │ │
│ │ • Routing │ │
│ │ • Request/Response Logging │ │
│ └──────────────────────────────┘ │
└────────────────┬───────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Auth │ │ Orchestrator│ │Configuration│
│ Service │ │ Service │ │ Service │
│ │ │ │ │ │
│ • Login │ │ • Workflows │ │ • Params │
│ • Register │ │ • Sagas │ │ • Fees │
│ • Tokens │ │ • Process │ │ • Limits │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
│ Appels internes uniquement
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Customer │ │ Ledger │ │Notification │
│ Profiles │ │ Wallets │ │ Service │
│ KYC │ │ │ │ │
│ │ │ • Accounts │ │ • SMS │
│ • Persons │ │ • Wallets │ │ • Email │
│ • Corporate │ │ • Transfers │ │ • Push │
│ • KYC │ │ • Ledger │ │ │
└─────────────┘ └─────────────┘ └─────────────┘Principes Clés
1. Exposition Publique vs Interne
| Service | Exposition | Accès |
|---|---|---|
| API Gateway | Publique | Clients (mobile, web) |
| Auth Service | Publique (via Gateway) | Login, register, refresh token |
| Orchestrator Service | Publique (via Gateway) | Workflows métier |
| Configuration Service | Publique (via Gateway) | Lecture params, fees |
| Customer Profiles KYC | Interne uniquement | Via Orchestrator |
| Ledger Wallets | Interne uniquement | Via Orchestrator |
| Notification Service | Interne uniquement | Via Orchestrator |
2. Règle d'Or
Les services métier (Customer, Ledger, Notification) ne sont JAMAIS appelés directement par les clients.
Toute opération passe par l'Orchestrator qui garantit la cohérence du workflow.
3. Communication Inter-Services
Client → API Gateway → Orchestrator → Services Métier
↓
HTTP synchrone
Headers propagés (X-User-Id, X-Request-Id)Orchestrator Service
Responsabilités
- Orchestrer les workflows multi-services
- Gérer les transactions distribuées (Saga pattern)
- Implémenter les compensations en cas d'échec
- Centraliser la logique des processus métier
Structure du Service
services/orchestrator/src/
├── core/
│ └── constants/
│ └── injection-tokens.ts
│
├── domain/
│ └── interfaces/services/
│ ├── customer.service.interface.ts # ICustomerService
│ ├── ledger.service.interface.ts # ILedgerService
│ └── notification.service.interface.ts # INotificationService
│
├── application/
│ └── use-cases/
│ ├── organizations/
│ │ ├── create-organization.use-case.ts
│ │ ├── add-organization-member.use-case.ts
│ │ └── onboard-organization.use-case.ts
│ ├── individuals/
│ │ ├── create-individual-account.use-case.ts
│ │ └── complete-kyc.use-case.ts
│ └── transactions/
│ ├── process-transfer.use-case.ts
│ ├── process-deposit.use-case.ts
│ └── process-withdrawal.use-case.ts
│
├── infrastructure/
│ └── adapters/http/
│ ├── base-http.adapter.ts
│ ├── customer-http.adapter.ts
│ ├── ledger-http.adapter.ts
│ └── notification-http.adapter.ts
│
└── presentation/
└── controllers/
├── organizations.controller.ts
├── individuals.controller.ts
└── transactions.controller.tsExemple Concret : Création d'une Organization
Workflow
┌─────────────────────────────────────────────────────────────────┐
│ CREATE ORGANIZATION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Créer Corporate (Customer Service) │
│ └─► corporateId │
│ │
│ 2. Créer Admin Person (Customer Service) │
│ └─► personId │
│ │
│ 3. Créer Account (Ledger Service) │
│ └─► accountId │
│ │
│ 4. Créer Wallet Principal (Ledger Service) │
│ └─► walletId │
│ │
│ 5. Envoyer Email Bienvenue (Notification Service) │
│ │
│ ✓ Retourner résultat complet │
│ │
└─────────────────────────────────────────────────────────────────┘Implémentation
// application/use-cases/organizations/create-organization.use-case.ts
@Injectable()
export class CreateOrganizationUseCase {
constructor(
@Inject(CUSTOMER_SERVICE)
private readonly customerService: ICustomerService,
@Inject(LEDGER_SERVICE)
private readonly ledgerService: ILedgerService,
@Inject(NOTIFICATION_SERVICE)
private readonly notificationService: INotificationService,
) {}
async execute(input: CreateOrganizationInput): Promise<CreateOrganizationResult> {
let corporate: Corporate | null = null;
let adminPerson: Person | null = null;
let account: Account | null = null;
try {
// ═══════════════════════════════════════════════════════════
// STEP 1: Créer l'entité Corporate
// ═══════════════════════════════════════════════════════════
corporate = await this.customerService.createCorporate({
name: input.companyName,
registrationNumber: input.registrationNumber,
country: input.country,
industry: input.industry,
});
// ═══════════════════════════════════════════════════════════
// STEP 2: Créer l'Admin Person et le lier au Corporate
// ═══════════════════════════════════════════════════════════
adminPerson = await this.customerService.createPerson({
firstName: input.admin.firstName,
lastName: input.admin.lastName,
email: input.admin.email,
phone: input.admin.phone,
corporateId: corporate.id,
role: CorporateRole.ADMIN,
});
// ═══════════════════════════════════════════════════════════
// STEP 3: Créer le compte NXPay pour le Corporate
// ═══════════════════════════════════════════════════════════
account = await this.ledgerService.createAccount({
entityId: corporate.id,
entityType: EntityType.CORPORATE,
currency: input.currency || 'XAF',
name: `Compte Principal - ${input.companyName}`,
});
// ═══════════════════════════════════════════════════════════
// STEP 4: Créer le Wallet principal
// ═══════════════════════════════════════════════════════════
const wallet = await this.ledgerService.createWallet({
accountId: account.id,
type: WalletType.CORPORATE_MAIN,
name: 'Wallet Principal',
});
// ═══════════════════════════════════════════════════════════
// STEP 5: Envoyer les notifications
// ═══════════════════════════════════════════════════════════
await this.notificationService.send({
type: NotificationType.EMAIL,
template: 'CORPORATE_WELCOME',
recipient: input.admin.email,
data: {
companyName: input.companyName,
adminName: `${input.admin.firstName} ${input.admin.lastName}`,
accountNumber: account.accountNumber,
},
});
// ═══════════════════════════════════════════════════════════
// SUCCESS: Retourner le résultat
// ═══════════════════════════════════════════════════════════
return {
success: true,
data: {
corporateId: corporate.id,
adminPersonId: adminPerson.id,
accountId: account.id,
walletId: wallet.id,
accountNumber: account.accountNumber,
},
};
} catch (error) {
// ═══════════════════════════════════════════════════════════
// COMPENSATION: Rollback en cas d'échec
// ═══════════════════════════════════════════════════════════
await this.compensate({ corporate, adminPerson, account }, error);
throw error;
}
}
/**
* Compensation (rollback) en cas d'échec
* Supprime les entités créées dans l'ordre inverse
*/
private async compensate(
created: { corporate: Corporate | null; adminPerson: Person | null; account: Account | null },
originalError: Error,
): Promise<void> {
const compensationErrors: Error[] = [];
// Rollback Account (si créé)
if (created.account) {
try {
await this.ledgerService.deleteAccount(created.account.id);
} catch (e) {
compensationErrors.push(e);
}
}
// Rollback Person (si créé)
if (created.adminPerson) {
try {
await this.customerService.deletePerson(created.adminPerson.id);
} catch (e) {
compensationErrors.push(e);
}
}
// Rollback Corporate (si créé)
if (created.corporate) {
try {
await this.customerService.deleteCorporate(created.corporate.id);
} catch (e) {
compensationErrors.push(e);
}
}
// Log les erreurs de compensation pour investigation
if (compensationErrors.length > 0) {
console.error('Compensation errors:', compensationErrors);
// TODO: Alerter l'équipe ops pour intervention manuelle
}
}
}Controller
// presentation/controllers/organizations.controller.ts
@Controller('organizations')
@ApiTags('Organizations')
export class OrganizationsController {
constructor(
private readonly createOrganizationUseCase: CreateOrganizationUseCase,
private readonly addMemberUseCase: AddOrganizationMemberUseCase,
) {}
@Post()
@ApiOperation({ summary: 'Créer une nouvelle organization' })
@ApiResponse({ status: 201, type: CreateOrganizationResponseDto })
async create(
@Body() dto: CreateOrganizationDto,
@CurrentUser() user: AuthenticatedUser,
): Promise<ApiResponse<CreateOrganizationResponseDto>> {
const result = await this.createOrganizationUseCase.execute({
...dto,
createdBy: user.userId,
});
return {
success: true,
code: 'ORGANIZATION_CREATED',
data: result.data,
};
}
@Post(':corporateId/members')
@ApiOperation({ summary: 'Ajouter un membre à l\'organization' })
async addMember(
@Param('corporateId', ParseUUIDPipe) corporateId: string,
@Body() dto: AddMemberDto,
@CurrentUser() user: AuthenticatedUser,
): Promise<ApiResponse<AddMemberResponseDto>> {
const result = await this.addMemberUseCase.execute({
corporateId,
...dto,
addedBy: user.userId,
});
return {
success: true,
code: 'MEMBER_ADDED',
data: result.data,
};
}
}Interfaces des Services Externes
// domain/interfaces/services/customer.service.interface.ts
export interface ICustomerService {
// Corporate
createCorporate(input: CreateCorporateInput): Promise<Corporate>;
getCorporate(id: string): Promise<Corporate | null>;
updateCorporate(id: string, input: UpdateCorporateInput): Promise<Corporate>;
deleteCorporate(id: string): Promise<void>;
// Person
createPerson(input: CreatePersonInput): Promise<Person>;
getPerson(id: string): Promise<Person | null>;
deletePerson(id: string): Promise<void>;
// KYC
submitKycDocuments(personId: string, documents: KycDocument[]): Promise<KycSubmission>;
getKycStatus(personId: string): Promise<KycStatus>;
}
// domain/interfaces/services/ledger.service.interface.ts
export interface ILedgerService {
// Account
createAccount(input: CreateAccountInput): Promise<Account>;
getAccount(id: string): Promise<Account | null>;
getAccountsByEntity(entityId: string, entityType: EntityType): Promise<Account[]>;
deleteAccount(id: string): Promise<void>;
// Wallet
createWallet(input: CreateWalletInput): Promise<Wallet>;
getWallet(id: string): Promise<Wallet | null>;
// Transactions
transfer(input: TransferInput): Promise<Transaction>;
getTransaction(id: string): Promise<Transaction | null>;
}
// domain/interfaces/services/notification.service.interface.ts
export interface INotificationService {
send(input: SendNotificationInput): Promise<NotificationResult>;
sendBulk(inputs: SendNotificationInput[]): Promise<NotificationResult[]>;
}Configuration Réseau
Docker Compose (Services internes)
# Les services métier ne sont PAS exposés publiquement
services:
customer-profiles-kyc:
# Pas de "ports:" = non accessible de l'extérieur
networks:
- nxpay-internal
ledger-wallets:
networks:
- nxpay-internal
notifications:
networks:
- nxpay-internal
# Seuls ces services sont exposés
api-gateway:
ports:
- "3000:3000"
networks:
- nxpay-internal
- nxpay-public
orchestrator:
# Accessible uniquement via API Gateway
networks:
- nxpay-internal
networks:
nxpay-internal:
internal: true # Pas d'accès internet
nxpay-public:Variables d'Environnement (Orchestrator)
# Services internes (noms Docker)
CUSTOMER_SERVICE_URL=http://customer-profiles-kyc:3000
LEDGER_SERVICE_URL=http://ledger-wallets:3000
NOTIFICATION_SERVICE_URL=http://notifications:3000
CONFIGURATION_SERVICE_URL=http://configuration:3000Workflows à Implémenter
Phase 1 : Core
| Workflow | Use Case | Services impliqués |
|---|---|---|
| Créer Organization | CreateOrganizationUseCase | Customer, Ledger, Notification |
| Créer Compte Individuel | CreateIndividualAccountUseCase | Customer, Ledger, Notification |
| Ajouter Membre | AddOrganizationMemberUseCase | Customer, Notification |
Phase 2 : Transactions
| Workflow | Use Case | Services impliqués |
|---|---|---|
| Transfert P2P | ProcessTransferUseCase | Ledger, Notification, (Risk Engine) |
| Dépôt | ProcessDepositUseCase | Ledger, Notification |
| Retrait | ProcessWithdrawalUseCase | Ledger, Notification, (Provider) |
Phase 3 : KYC & Compliance
| Workflow | Use Case | Services impliqués |
|---|---|---|
| Soumettre KYC | SubmitKycUseCase | Customer |
| Valider KYC | ValidateKycUseCase | Customer, Ledger (upgrade limits) |
Bonnes Pratiques
1. Idempotence
Chaque opération doit pouvoir être rejouée sans effet de bord :
// Utiliser un idempotencyKey
async execute(input: CreateOrganizationInput): Promise<Result> {
// Vérifier si déjà traité
const existing = await this.findByIdempotencyKey(input.idempotencyKey);
if (existing) return existing;
// ... créer
}2. Timeouts
Configurer des timeouts sur les appels inter-services :
// infrastructure/adapters/http/base-http.adapter.ts
const response = await firstValueFrom(
this.httpService.post(url, data).pipe(
timeout(5000), // 5 secondes max
),
);3. Circuit Breaker (Future)
Implémenter un circuit breaker pour les services instables :
// Utiliser @nestjs/terminus ou opossum4. Tracing
Propager le X-Request-Id pour tracer les requêtes :
// Middleware qui propage les headers
const headers = {
'X-Request-Id': context.requestId,
'X-User-Id': context.userId,
};Migration Progressive
Étape 1 : Créer l'Orchestrator Service
- Scaffold le service
- Implémenter les adapters HTTP vers les services existants
- Implémenter
CreateOrganizationUseCase
Étape 2 : Router via API Gateway
- Configurer le routing vers l'Orchestrator
- Tester le workflow complet
Étape 3 : Fermer les accès directs
- Retirer l'exposition publique des services métier
- Configurer le réseau Docker
Étape 4 : Migrer les autres workflows
- Identifier tous les workflows multi-services
- Les migrer un par un vers l'Orchestrator
Questions Fréquentes
Pourquoi pas de message broker (Kafka/RabbitMQ) ?
Pour l'instant, la communication synchrone HTTP est suffisante et plus simple à debugger. On pourra migrer vers des events pour les workflows qui nécessitent une résilience accrue (ex: notifications asynchrones).
Comment gérer les échecs partiels ?
Le pattern Saga avec compensation : on annule les opérations réussies dans l'ordre inverse si une étape échoue.
Performance ?
L'Orchestrator ajoute un hop réseau. C'est acceptable pour les workflows de création. Pour les lectures simples (get wallet balance), on peut autoriser un accès direct via l'API Gateway.
Document à présenter à l'équipe - Version 1.0
Annexe — Audit architecture Orchestrator
Résumé Exécutif
| Critère | Status | Note |
|---|---|---|
| Interfaces définies | ✅ Bon | Interfaces pour tous les services externes |
| Injection de dépendances | ✅ Bon | Tokens utilisés correctement |
| Séparation des responsabilités | ⚠️ Partiel | Orchestrators existent mais mal positionnés |
| Clean Architecture Layers | ❌ Non conforme | Pas de séparation domain/application/infrastructure |
| Compensation (Saga) | ❌ Absent | Pas de rollback en cas d'échec |
| Naming conventions | ⚠️ Partiel | Orchestrators ≠ Use Cases |
Score global : 50% - Bonnes fondations, restructuration nécessaire.
Structure Actuelle vs Cible
Actuelle
src/
├── core/
│ ├── common/ # ✅ OK
│ ├── config/ # ✅ OK
│ ├── dto/ # ⚠️ Mal placé
│ ├── interfaces/ # ⚠️ Devrait être dans domain/
│ └── services/ # ⚠️ Devrait être dans infrastructure/
├── modules/
│ └── transactions/
│ ├── controllers/ # ✅ OK (mais devrait être dans presentation/)
│ ├── dto/ # ⚠️ Mélange request/response/internal
│ ├── orchestrators/ # ⚠️ Devrait être use-cases dans application/
│ └── services/ # ⚠️ Application services mal positionnés
└── app.module.tsCible
src/
├── core/ # Config, constants, tokens
│ ├── config/
│ └── constants/
│ └── injection-tokens.ts
├── common/ # Cross-cutting concerns
│ ├── decorators/
│ ├── filters/
│ ├── interceptors/
│ └── middleware/
├── domain/ # COUCHE DOMAIN
│ ├── interfaces/
│ │ └── services/ # ILedgerService, ICustomerService...
│ └── exceptions/ # Domain exceptions
├── application/ # COUCHE APPLICATION
│ ├── use-cases/
│ │ ├── transactions/
│ │ │ ├── process-transfer.use-case.ts
│ │ │ └── process-recharge.use-case.ts
│ │ └── organizations/
│ │ └── create-organization.use-case.ts
│ └── services/ # Application services partagés
│ ├── account-resolution.service.ts
│ ├── fees-calculation.service.ts
│ └── transaction-validation.service.ts
├── infrastructure/ # COUCHE INFRASTRUCTURE
│ └── adapters/
│ └── http/
│ ├── base-http.adapter.ts
│ ├── ledger-http.adapter.ts
│ ├── customer-http.adapter.ts
│ └── notification-http.adapter.ts
├── presentation/ # COUCHE PRESENTATION
│ ├── controllers/
│ │ └── transactions.controller.ts
│ └── dto/
│ ├── requests/
│ └── responses/
└── modules/ # Composition DI
└── transactions.module.tsAnalyse Détaillée
1. Points Positifs ✅
1.1 Interfaces bien définies
// core/interfaces/ledger-wallet.interface.ts
export interface ILedgerWalletService {
updateWallet(request: WalletUpdateRequestDto): Promise<WalletUpdateResponseDto>;
getWalletByUserId(userId: string): Promise<{ accountId: string; balance?: number }>;
createP2PTransaction(request: any): Promise<{ id: string; balance?: number }>;
}Verdict : Les interfaces sont présentes et permettent le découplage.
1.2 Injection de dépendances via tokens
// transfer.orchestrator.ts
constructor(
@Inject('LEDGER_WALLET_SERVICE')
private readonly ledgerWalletClient: ILedgerWalletService,
@Inject('RISK_ENGINE_SERVICE')
private readonly riskEngineClient: IRiskEngineService,
)Verdict : Pattern correct, permet de changer d'implémentation facilement.
1.3 Services applicatifs spécialisés
AccountResolutionService- Résolution des comptesTransactionValidationService- Validations métierFeesCalculationService- Calcul des fraisTransactionMapperService- Mapping des données
Verdict : Bonne séparation des responsabilités.
2. Points à Améliorer ⚠️
2.1 Orchestrator vs Use Case
Problème : Les "orchestrators" font le travail des "use cases" mais :
- Sont dans
modules/transactions/orchestrators/au lieu deapplication/use-cases/ - Ne suivent pas le naming convention
*.use-case.ts
Impact : Confusion sur les responsabilités, non-conformité avec le CLAUDE.md.
Solution :
# Renommer et déplacer
modules/transactions/orchestrators/transfer.orchestrator.ts
→ application/use-cases/transactions/process-transfer.use-case.ts2.2 Controller → Service → Orchestrator (couche inutile)
Problème : Le controller passe par TransactionsService qui délègue à l'orchestrator.
// Actuel : Controller → Service → Orchestrator
@Post('send')
async send(@Body() dto) {
return this.transactionsService.send(dto, ...); // Couche intermédiaire
}
// transactionsService.send() appelle juste transferOrchestrator.execute()Impact : Couche TransactionsService ajoute de la complexité sans valeur.
Solution : Controller appelle directement le Use Case.
// Cible : Controller → Use Case
@Post('send')
async send(@Body() dto) {
return this.processTransferUseCase.execute(dto, ...);
}2.3 DTOs mal organisés
Problème :
core/dto/contient des DTOs internesmodules/transactions/dto/mélange request, response, et internal
Solution :
presentation/dto/requests/send-transaction.dto.ts # Request du client
presentation/dto/responses/send-response.dto.ts # Response au client
application/dto/internal/transaction-request.dto.ts # DTOs internes3. Points Critiques ❌
3.1 Pas de couche Domain
Problème : Aucune entité de domaine, value objects, ou exceptions métier.
Impact :
- Logique métier dispersée dans les services
- Validation métier difficile à maintenir
- Pas de richesse comportementale des entités
Solution : Créer la couche domain avec :
// domain/exceptions/insufficient-balance.exception.ts
export class InsufficientBalanceException extends DomainException {
readonly code = 'INSUFFICIENT_BALANCE';
readonly statusCode = 400;
}
// domain/exceptions/transaction-blocked.exception.ts
export class TransactionBlockedException extends DomainException {
readonly code = 'TRANSACTION_BLOCKED';
readonly statusCode = 403;
}3.2 Pas de compensation (Saga Pattern)
Problème : L'orchestrator Transfer ne gère pas les échecs partiels.
// Actuel - Pas de rollback
async execute(dto, ...) {
// Si une étape échoue après la vérification PIN,
// aucune compensation n'est effectuée
const pinVerification = await this.authClient.verifyPin(...);
const [senderContext, receiverContext] = await Promise.all([...]);
// ... Si ça échoue ici, pas de rollback
const transaction = await this.ledgerWalletClient.createP2PTransaction(...);
}Impact : En cas d'échec partiel, données incohérentes possibles.
Solution : Implémenter le pattern Saga avec compensation.
async execute(input) {
let createdTransaction = null;
try {
// Steps...
createdTransaction = await this.ledgerService.createTransaction(...);
// ...
} catch (error) {
// Compensation
if (createdTransaction) {
await this.ledgerService.reverseTransaction(createdTransaction.id);
}
throw error;
}
}3.3 Adapters HTTP dans le mauvais dossier
Problème : core/services/ledger-wallet/ledger-wallet-http.service.ts
Solution : Déplacer vers infrastructure/adapters/http/ledger-http.adapter.ts
3.4 Interfaces dans core au lieu de domain
Problème : core/interfaces/ devrait être domain/interfaces/services/
Plan de Refactoring
Phase 1 : Structure des dossiers (sans casser le code)
# 1. Créer la nouvelle structure
mkdir -p src/domain/interfaces/services
mkdir -p src/domain/exceptions
mkdir -p src/application/use-cases/transactions
mkdir -p src/application/use-cases/organizations
mkdir -p src/application/services
mkdir -p src/infrastructure/adapters/http
mkdir -p src/presentation/controllers
mkdir -p src/presentation/dto/requests
mkdir -p src/presentation/dto/responses
# 2. Déplacer les interfaces (avec alias temporaires)
mv src/core/interfaces/*.interface.ts src/domain/interfaces/services/
# 3. Déplacer les adapters HTTP
mv src/core/services/*/*.service.ts src/infrastructure/adapters/http/
# Renommer en *.adapter.ts
# 4. Déplacer les orchestrators → use-cases
mv src/modules/transactions/orchestrators/*.orchestrator.ts src/application/use-cases/transactions/
# Renommer en *.use-case.tsPhase 2 : Renommage et refactoring
| Fichier Actuel | Fichier Cible |
|---|---|
transfer.orchestrator.ts | process-transfer.use-case.ts |
recharge.orchestrator.ts | process-recharge.use-case.ts |
fees.orchestrator.ts | calculate-fees.use-case.ts |
ledger-wallet-http.service.ts | ledger-http.adapter.ts |
customer-http.service.ts | customer-http.adapter.ts |
Phase 3 : Ajouter Domain Exceptions
// domain/exceptions/base.exception.ts
export abstract class DomainException extends Error {
abstract readonly code: string;
abstract readonly statusCode: number;
}
// domain/exceptions/insufficient-balance.exception.ts
// domain/exceptions/transaction-blocked.exception.ts
// domain/exceptions/wallet-not-found.exception.ts
// domain/exceptions/invalid-pin.exception.tsPhase 4 : Ajouter Compensation (Saga)
Modifier les use-cases pour gérer les rollbacks.
Checklist de Conformité
| Règle | Actuel | Action |
|---|---|---|
Interfaces dans domain/interfaces/services/ | ❌ | Déplacer |
Use Cases dans application/use-cases/ | ❌ | Déplacer + renommer |
Adapters HTTP dans infrastructure/adapters/http/ | ❌ | Déplacer + renommer |
Controllers dans presentation/controllers/ | ⚠️ | Déplacer |
| DTOs séparés (requests/responses) | ❌ | Réorganiser |
| Domain Exceptions | ❌ | Créer |
| Compensation (Saga) | ❌ | Implémenter |
| Injection tokens centralisés | ⚠️ | Consolider dans core/constants/ |
| Suffixes de fichiers corrects | ⚠️ | Renommer |
Priorités Recommandées
- P0 - Critique : Ajouter compensation dans les use-cases existants
- P1 - Important : Créer domain exceptions + les utiliser
- P2 - Moyen : Restructurer les dossiers
- P3 - Low : Renommer les fichiers
Audit réalisé le 22/01/2026