Skip to content

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

ServiceExpositionAccès
API GatewayPubliqueClients (mobile, web)
Auth ServicePublique (via Gateway)Login, register, refresh token
Orchestrator ServicePublique (via Gateway)Workflows métier
Configuration ServicePublique (via Gateway)Lecture params, fees
Customer Profiles KYCInterne uniquementVia Orchestrator
Ledger WalletsInterne uniquementVia Orchestrator
Notification ServiceInterne uniquementVia 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

  1. Orchestrer les workflows multi-services
  2. Gérer les transactions distribuées (Saga pattern)
  3. Implémenter les compensations en cas d'échec
  4. 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.ts

Exemple 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

typescript
// 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

typescript
// 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

typescript
// 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)

yaml
# 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)

env
# 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:3000

Workflows à Implémenter

Phase 1 : Core

WorkflowUse CaseServices impliqués
Créer OrganizationCreateOrganizationUseCaseCustomer, Ledger, Notification
Créer Compte IndividuelCreateIndividualAccountUseCaseCustomer, Ledger, Notification
Ajouter MembreAddOrganizationMemberUseCaseCustomer, Notification

Phase 2 : Transactions

WorkflowUse CaseServices impliqués
Transfert P2PProcessTransferUseCaseLedger, Notification, (Risk Engine)
DépôtProcessDepositUseCaseLedger, Notification
RetraitProcessWithdrawalUseCaseLedger, Notification, (Provider)

Phase 3 : KYC & Compliance

WorkflowUse CaseServices impliqués
Soumettre KYCSubmitKycUseCaseCustomer
Valider KYCValidateKycUseCaseCustomer, Ledger (upgrade limits)

Bonnes Pratiques

1. Idempotence

Chaque opération doit pouvoir être rejouée sans effet de bord :

typescript
// 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 :

typescript
// 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 :

typescript
// Utiliser @nestjs/terminus ou opossum

4. Tracing

Propager le X-Request-Id pour tracer les requêtes :

typescript
// 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èreStatusNote
Interfaces définies✅ BonInterfaces pour tous les services externes
Injection de dépendances✅ BonTokens utilisés correctement
Séparation des responsabilités⚠️ PartielOrchestrators existent mais mal positionnés
Clean Architecture Layers❌ Non conformePas de séparation domain/application/infrastructure
Compensation (Saga)❌ AbsentPas de rollback en cas d'échec
Naming conventions⚠️ PartielOrchestrators ≠ 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.ts

Cible

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.ts

Analyse Détaillée

1. Points Positifs ✅

1.1 Interfaces bien définies

typescript
// 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

typescript
// 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 comptes
  • TransactionValidationService - Validations métier
  • FeesCalculationService - Calcul des frais
  • TransactionMapperService - 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 de application/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.ts

2.2 Controller → Service → Orchestrator (couche inutile)

Problème : Le controller passe par TransactionsService qui délègue à l'orchestrator.

typescript
// 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.

typescript
// 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 internes
  • modules/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 internes

3. 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 :

typescript
// 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.

typescript
// 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.

typescript
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)

bash
# 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.ts

Phase 2 : Renommage et refactoring

Fichier ActuelFichier Cible
transfer.orchestrator.tsprocess-transfer.use-case.ts
recharge.orchestrator.tsprocess-recharge.use-case.ts
fees.orchestrator.tscalculate-fees.use-case.ts
ledger-wallet-http.service.tsledger-http.adapter.ts
customer-http.service.tscustomer-http.adapter.ts

Phase 3 : Ajouter Domain Exceptions

typescript
// 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.ts

Phase 4 : Ajouter Compensation (Saga)

Modifier les use-cases pour gérer les rollbacks.


Checklist de Conformité

RègleActuelAction
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 ExceptionsCréer
Compensation (Saga)Implémenter
Injection tokens centralisés⚠️Consolider dans core/constants/
Suffixes de fichiers corrects⚠️Renommer

Priorités Recommandées

  1. P0 - Critique : Ajouter compensation dans les use-cases existants
  2. P1 - Important : Créer domain exceptions + les utiliser
  3. P2 - Moyen : Restructurer les dossiers
  4. P3 - Low : Renommer les fichiers

Audit réalisé le 22/01/2026

NxPay — Plateforme fintech CEMAC