Skip to content

💰 Flux Complet: Système de Commissions Cash-In

Date: 2026-01-29
Version: 1.0.0
Status: ⚠️ Configuration 100% | Distribution 0% (Prévu mais non implémenté)


📋 Vue d'Ensemble

Ce document explique comment fonctionne le système de commissions pour les Cash-In, de la collecte des frais jusqu'au versement aux bénéficiaires.

État Actuel:

  • Configuration: 100% implémentée
  • Collecte des frais: 100% implémentée
  • ⚠️ Distribution des commissions: 0% (logique prévue, code manquant)

🔄 Flux Complet (Théorique vs Réel)

ÉTAPE 1: Transaction Cash-In avec Frais ✅ IMPLÉMENTÉ

1.1. Client effectue un Cash-In

Client → Marchand → Cash-In de 10,000 XAF
Frais collectés: 200 XAF

1.2. Création de la Transaction

Fichier: services/ledger-wallets/src/transactions/orchestrators/transaction-creation.orchestrator.ts

typescript
// Transaction créée avec:
{
  amount: 10000,           // Montant principal
  feeAmount: 200,          // Frais collectés
  sourceAccountId: "...",  // Compte client
  destinationAccountId: "...", // Compte marchand
  transactionTypeId: "uuid-deposit-cashin"
}

1.3. Débit des Frais du Compte Client ✅ IMPLÉMENTÉ

Code:

typescript
// Ligne 100-106: TransactionCreationOrchestrator
if (feeAmount > 0 && sourceAccount) {
  // Débiter les frais du compte client
  await this.balanceManagement.debitAccount(
    queryRunner.manager,
    sourceAccount,
    feeAmount,  // 200 XAF
  );
}

Résultat:

  • Client: balance -= 200 XAF (débité)
  • Compte FEE_COLLECTION: balance += 200 XAF (crédité)

ÉTAPE 2: Collecte des Frais dans le Compte Système ✅ IMPLÉMENTÉ

2.1. Création de l'Entrée Ledger pour les Frais

Fichier: services/ledger-wallets/src/transactions/services/ledger-creation.service.ts (lignes 154-244)

typescript
async createFeeLedgerEntry(...) {
  // Récupérer le compte FEE_COLLECTION (système)
  const feeCollectionAccount = await queryRunner.findOne(Account, {
    where: { id: SYSTEM_ACCOUNT_IDS.FEE_COLLECTION_XAF },
  });

  // Créditer le compte FEE_COLLECTION
  feeCollectionAccount.balance += feeAmount;  // +200 XAF
  
  // Créer double entrée comptable:
  // DEBIT: compte client (200 XAF)
  // CREDIT: compte FEE_COLLECTION (200 XAF)
}

2.2. État Après Collecte

┌─────────────────────────────────────────┐
│ COMPTE FEE_COLLECTION (Système)        │
├─────────────────────────────────────────┤
│ Balance: +200 XAF                       │
│ (Frais collectés sur Cash-In)          │
└─────────────────────────────────────────┘

📍 Où va l'argent?

  • Actuellement: Dans le compte système FEE_COLLECTION_XAF
  • ⚠️ Prévu: Répartir selon les règles de commission

ÉTAPE 3: Récupération des Règles de Commission ✅ IMPLÉMENTÉ

3.1. Configuration Active

Exemple de Configuration Cash-In:

json
{
  "transactionTypeId": "uuid-deposit-cashin",
  "rules": [
    {
      "beneficiaryType": "platform",    // NxPay
      "percentage": 30.00               // 30%
    },
    {
      "beneficiaryType": "corporate",   // Corporate
      "percentage": 20.00,              // 20%
      "fallbackBeneficiaryType": "platform"  // Si absent → NxPay
    },
    {
      "beneficiaryType": "merchant",    // Marchand
      "percentage": 50.00               // 50%
    }
  ]
}

Total: 100% ✅

3.2. Calcul de la Répartition

Pour 200 XAF de frais collectés:

BénéficiairePourcentageMontant Calculé
NxPay (platform)30%60 XAF
Corporate20%40 XAF
Marchand50%100 XAF
TOTAL100%200 XAF

Fichier: services/configuration/src/commission-rules/commission-rules.service.ts

typescript
// Méthode simulate() (lignes 264-339)
async simulate(dto: SimulateCommissionDto) {
  // 1. Récupérer règles actives
  const rules = await this.getApplicableRules(...);
  
  // 2. Calculer montants
  const distributions = rules.map(rule => ({
    beneficiaryType: rule.beneficiaryType,
    percentage: rule.percentage,
    amount: (dto.feeAmount * rule.percentage) / 100
  }));
  
  return { distributions, totalAmount: dto.feeAmount };
}

ÉTAPE 4: Résolution des Entités et Wallets ⚠️ PRÉVU (Non Implémenté)

4.1. Contexte de la Transaction

Table: ledger_wallet.transaction_entities

sql
SELECT * FROM ledger_wallet.transaction_entities 
WHERE transaction_id = 'uuid-transaction';

-- Résultat:
-- merchant_id: uuid-merchant-123
-- corporate_id: uuid-corporate-456 (si affilié)
-- client_user_id: uuid-client-789

4.2. Résolution des Wallets par Bénéficiaire

Logique Prévue:

typescript
// 1. PLATFORM (NxPay)
// → Wallet système NxPay (entity_type='platform', entity_id='NXPAY_SYSTEM')

// 2. CORPORATE
// → Chercher wallet corporate via:
//    - entity_type='corporate'
//    - entity_id=transaction_entities.corporate_id
// → Si absent → Fallback vers platform

// 3. MERCHANT
// → Chercher wallet merchant via:
//    - entity_type='merchant'
//    - entity_id=transaction_entities.merchant_id

Fichier Prévu: services/orchestrator/src/application/services/commission-distribution.service.ts

typescript
// Logique prévue (non implémentée)
async resolveBeneficiaryAccount(
  beneficiaryType: string,
  transactionId: string
): Promise<Account | null> {
  // 1. Récupérer contexte transaction
  const txEntities = await this.getTransactionEntities(transactionId);
  
  // 2. Résoudre selon type
  switch (beneficiaryType) {
    case 'platform':
      return await this.findPlatformAccount();
      
    case 'corporate':
      if (!txEntities.corporate_id) {
        return null; // Fallback géré après
      }
      return await this.findCorporateWallet(txEntities.corporate_id);
      
    case 'merchant':
      return await this.findMerchantWallet(txEntities.merchant_id);
  }
}

4.3. Recherche des Comptes Commission

Structure Wallet:

Wallet (entity_id, entity_type)
  └── Account (currency_code='XAF')
      └── balance (où verser la commission)

Fichier: services/ledger-wallets/src/wallets/wallets.service.ts

typescript
// Méthode existante (lignes 175-225)
async findOneByEntity(
  entityId: string,
  entityType: string
): Promise<Wallet> {
  // Trouve wallet par entity_id + entity_type
  const wallet = await this.walletsRepository.findOne({
    where: { entityId, entityType, status: 'active' }
  });
  
  // Retourne wallet avec compte XAF
  return wallet;
}

Exemple de Résolution:

typescript
// Pour Corporate
const corporateWallet = await walletsService.findOneByEntity(
  corporateId,      // uuid-corporate-456
  'corporate'      // entity_type
);

const commissionAccount = corporateWallet.accounts.find(
  acc => acc.currencyCode === 'XAF'
);
// → Account.id = uuid-account-corporate-commission

ÉTAPE 5: Distribution des Commissions ⚠️ PRÉVU (Non Implémenté)

5.1. Service de Distribution

Fichier Prévu: services/orchestrator/src/application/services/commission-distribution.service.ts

typescript
// Logique prévue (non implémentée)
async distribute(input: {
  transactionId: string;
  transactionTypeId: string;
  feeAmount: number;
  currencyCode: string;
}): Promise<void> {
  // 1. Récupérer règles actives
  const rules = await this.configService.getCommissionRules(
    input.transactionTypeId,
    'fee_split'
  );
  
  // 2. Pour chaque règle, calculer et distribuer
  for (const rule of rules) {
    const amount = (input.feeAmount * rule.percentage) / 100;
    
    // 3. Résoudre compte bénéficiaire
    let beneficiaryAccount = await this.resolveBeneficiaryAccount(
      rule.beneficiaryType,
      input.transactionId
    );
    
    // 4. Gérer fallback si compte absent
    if (!beneficiaryAccount && rule.fallbackBeneficiaryType) {
      beneficiaryAccount = await this.resolveBeneficiaryAccount(
        rule.fallbackBeneficiaryType,
        input.transactionId
      );
    }
    
    // 5. Créer transfert depuis FEE_COLLECTION vers bénéficiaire
    if (beneficiaryAccount) {
      await this.createCommissionTransfer({
        fromAccountId: SYSTEM_ACCOUNT_IDS.FEE_COLLECTION_XAF,
        toAccountId: beneficiaryAccount.id,
        amount: amount,
        transactionId: input.transactionId,
        commissionRuleId: rule.id
      });
    }
    
    // 6. Sauvegarder dans commission_distributions
    await this.saveCommissionDistribution({
      transactionId: input.transactionId,
      commissionRuleId: rule.id,
      beneficiaryType: rule.beneficiaryType,
      amount: amount,
      status: beneficiaryAccount ? 'completed' : 'pending'
    });
  }
}

5.2. Création des Transferts

Logique Prévue:

typescript
async createCommissionTransfer(input: {
  fromAccountId: string;  // FEE_COLLECTION_XAF
  toAccountId: string;     // Compte bénéficiaire
  amount: number;
  transactionId: string;
}): Promise<void> {
  // 1. Débiter FEE_COLLECTION
  await this.balanceManagement.debitAccount(
    FEE_COLLECTION_ACCOUNT,
    input.amount
  );
  
  // 2. Créditer compte bénéficiaire
  await this.balanceManagement.creditAccount(
    BENEFICIARY_ACCOUNT,
    input.amount
  );
  
  // 3. Créer entrées ledger (double entrée)
  await this.ledgerService.createDoubleEntry(
    fromAccountId: input.fromAccountId,
    toAccountId: input.toAccountId,
    amount: input.amount,
    operationType: 'commission_distribution',
    transactionId: input.transactionId
  );
}

5.3. État Final Après Distribution

┌─────────────────────────────────────────┐
│ AVANT DISTRIBUTION                      │
├─────────────────────────────────────────┤
│ FEE_COLLECTION: 200 XAF                 │
└─────────────────────────────────────────┘


        ┌───────────────────┐
        │ DISTRIBUTION      │
        └───────────────────┘

        ┌───────────┼───────────┐
        ▼           ▼           ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ NxPay Wallet │ │Corporate     │ │Merchant     │
│ +60 XAF      │ │Wallet        │ │Wallet       │
│              │ │+40 XAF       │ │+100 XAF     │
└─────────────┘ └─────────────┘ └─────────────┘

ÉTAPE 6: Sauvegarde des Distributions ⚠️ PRÉVU (Non Implémenté)

6.1. Table commission_distributions

Fichier: tools/database/migrations/ledger_wallet/006-create-commission-distributions-table.sql

sql
CREATE TABLE ledger_wallet.commission_distributions (
    id UUID PRIMARY KEY,
    transaction_id UUID,              -- Transaction source
    commission_rule_id UUID,          -- Règle appliquée
    beneficiary_type VARCHAR(50),      -- platform/corporate/merchant
    beneficiary_account_id UUID,       -- Compte crédité
    amount DECIMAL(20,8),              -- Montant distribué
    percentage DECIMAL(5,2),           -- Pourcentage appliqué
    status VARCHAR(20),                -- pending/completed/failed
    distributed_at TIMESTAMP,         -- Date versement
    ...
);

6.2. Enregistrement

Exemple d'Enregistrement:

sql
INSERT INTO ledger_wallet.commission_distributions VALUES
  -- NxPay
  (uuid1, transaction_id, rule_id_platform, 'platform', 
   account_nxpay_id, 60.00, 30.00, 'completed', now()),
  
  -- Corporate
  (uuid2, transaction_id, rule_id_corporate, 'corporate',
   account_corporate_id, 40.00, 20.00, 'completed', now()),
  
  -- Merchant
  (uuid3, transaction_id, rule_id_merchant, 'merchant',
   account_merchant_id, 100.00, 50.00, 'completed', now());

🔍 Détails Techniques

1. D'où Vient l'Argent?

Réponse: Des frais prélevés sur le client lors du Cash-In

Client Cash-In 10,000 XAF
  ├── Montant principal: 10,000 XAF → Compte Marchand
  └── Frais: 200 XAF → Compte FEE_COLLECTION (système)

Code:

  • TransactionCreationOrchestrator.execute() (ligne 100-106)
  • LedgerCreationService.createFeeLedgerEntry() (ligne 154-244)

2. Comment il Calcule?

Réponse: Via les règles de commission configurées

Processus:

  1. Récupérer règles actives pour transactionTypeId = DEPOSIT
  2. Filtrer par rule_type = 'fee_split'
  3. Calculer: montant = (feeAmount * percentage) / 100

Code:

  • CommissionRulesService.simulate() (ligne 264-339)
  • CommissionRulesService.getApplicableRules() (ligne 223-263)

Exemple:

typescript
// Règles: NxPay 30%, Corporate 20%, Merchant 50%
// Frais: 200 XAF

distributions = [
  { beneficiaryType: 'platform', amount: 200 * 0.30 = 60 },
  { beneficiaryType: 'corporate', amount: 200 * 0.20 = 40 },
  { beneficiaryType: 'merchant', amount: 200 * 0.50 = 100 }
]

3. Comment il Dispers/Verse?

Réponse: ⚠️ Non Implémenté (Logique prévue)

Logique Prévue:

typescript
// Pour chaque bénéficiaire:
1. Résoudre wallet/compte bénéficiaire
2. Créer transfert depuis FEE_COLLECTION
3. Créditer compte bénéficiaire
4. Créer entrées ledger
5. Sauvegarder dans commission_distributions

Fichiers à Créer:

  • services/orchestrator/src/application/services/commission-distribution.service.ts
  • services/ledger-wallets/src/commission-distributions/commission-distributions.service.ts

4. Comment il Trouve les Wallets?

Réponse: Via entity_id + entity_type

4.1. Structure Wallet

typescript
Wallet {
  entityId: string,      // UUID de l'entité (merchant_id, corporate_id, etc.)
  entityType: string,    // 'merchant', 'corporate', 'platform', 'user'
  accounts: Account[]    // Comptes (un par devise)
}

4.2. Résolution par Type

PLATFORM (NxPay):

typescript
// Wallet système NxPay
const platformWallet = await walletsService.findOneByEntity(
  'NXPAY_SYSTEM',  // entity_id fixe
  'platform'       // entity_type
);

CORPORATE:

typescript
// 1. Récupérer corporate_id depuis transaction_entities
const txEntities = await getTransactionEntities(transactionId);
const corporateId = txEntities.corporate_id;

// 2. Trouver wallet corporate
const corporateWallet = await walletsService.findOneByEntity(
  corporateId,
  'corporate'
);

// 3. Récupérer compte XAF
const commissionAccount = corporateWallet.accounts.find(
  acc => acc.currencyCode === 'XAF'
);

MERCHANT:

typescript
// 1. Récupérer merchant_id depuis transaction_entities
const merchantId = txEntities.merchant_id;

// 2. Trouver wallet merchant
const merchantWallet = await walletsService.findOneByEntity(
  merchantId,
  'merchant'
);

// 3. Récupérer compte XAF
const commissionAccount = merchantWallet.accounts.find(
  acc => acc.currencyCode === 'XAF'
);

Code Existant:

  • WalletsService.findOneByEntity() (ligne 175-225)
  • AccountsService.findByWalletAndCurrency() (via repository)

📊 Schéma de Flux Complet

┌─────────────────────────────────────────────────────────────┐
│ ÉTAPE 1: Transaction Cash-In                                │
├─────────────────────────────────────────────────────────────┤
│ Client: 10,000 XAF → Marchand                                │
│ Frais: 200 XAF prélevés                                     │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ ÉTAPE 2: Collecte Frais (✅ IMPLÉMENTÉ)                    │
├─────────────────────────────────────────────────────────────┤
│ Client.balance -= 200 XAF                                   │
│ FEE_COLLECTION.balance += 200 XAF                           │
│ Ledger: DEBIT client, CREDIT FEE_COLLECTION                 │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ ÉTAPE 3: Récupération Règles (✅ IMPLÉMENTÉ)                │
├─────────────────────────────────────────────────────────────┤
│ Configuration Service                                        │
│ ├── NxPay: 30% → 60 XAF                                     │
│ ├── Corporate: 20% → 40 XAF                                 │
│ └── Merchant: 50% → 100 XAF                                 │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ ÉTAPE 4: Résolution Wallets (⚠️ PRÉVU)                     │
├─────────────────────────────────────────────────────────────┤
│ transaction_entities → merchant_id, corporate_id            │
│ walletsService.findOneByEntity() → Wallet                    │
│ Wallet.accounts → Account XAF                                │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ ÉTAPE 5: Distribution (⚠️ PRÉVU - NON IMPLÉMENTÉ)             │
├─────────────────────────────────────────────────────────────┤
│ Pour chaque bénéficiaire:                                    │
│ ├── FEE_COLLECTION.balance -= amount                        │
│ ├── BeneficiaryAccount.balance += amount                     │
│ ├── Ledger: DEBIT FEE_COLLECTION, CREDIT Beneficiary        │
│ └── commission_distributions: status='completed'            │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ ÉTAPE 6: Résultat Final                                      │
├─────────────────────────────────────────────────────────────┤
│ FEE_COLLECTION: 0 XAF (vide)                                │
│ NxPay Wallet: +60 XAF                                        │
│ Corporate Wallet: +40 XAF                                    │
│ Merchant Wallet: +100 XAF                                    │
│ Total: 200 XAF ✅                                            │
└─────────────────────────────────────────────────────────────┘

⚠️ État Actuel vs Prévu

✅ Ce Qui Est Implémenté

  1. Configuration des Règles

    • CRUD configurations
    • Validation total = 100%
    • Simulateur de distribution
    • Historique
  2. Collecte des Frais

    • Débit client
    • Crédit FEE_COLLECTION
    • Ledger entries
  3. Calcul des Montants

    • Service de simulation
    • Calcul par pourcentage

⚠️ Ce Qui Est Prévu Mais Non Implémenté

  1. Distribution Automatique

    • Service CommissionDistributionService manquant
    • Pas d'intégration dans TransactionCreationOrchestrator
  2. Résolution Wallets

    • Logique prévue mais non codée
    • Pas de service TransactionEntitiesService
  3. Sauvegarde Distributions

    • Table commission_distributions créée
    • Pas de service pour l'utiliser

🔧 Ce Qui Doit Être Fait

Phase 1: Service Transaction Entities (1 jour)

Créer:

services/ledger-wallets/src/transaction-entities/
├── transaction-entities.module.ts
├── transaction-entities.service.ts
└── entities/transaction-entity.entity.ts

Fonctionnalité:

  • Sauvegarder contexte transaction (merchant_id, corporate_id, etc.)
  • Récupérer entités pour résolution wallets

Phase 2: Service Commission Distributions (1 jour)

Créer:

services/ledger-wallets/src/commission-distributions/
├── commission-distributions.module.ts
├── commission-distributions.service.ts
└── entities/commission-distribution.entity.ts

Fonctionnalité:

  • Sauvegarder distributions
  • Gérer status (pending/completed/failed)
  • Retry pour comptes manquants

Phase 3: Service Distribution Orchestrator (2 jours)

Créer:

services/orchestrator/src/application/services/
└── commission-distribution.service.ts

Fonctionnalité:

  1. Récupérer règles depuis Configuration Service
  2. Résoudre entités depuis transaction_entities
  3. Résoudre wallets/comptes bénéficiaires
  4. Créer transferts depuis FEE_COLLECTION
  5. Sauvegarder dans commission_distributions

Phase 4: Intégration (1 jour)

Modifier:

services/ledger-wallets/src/transactions/orchestrators/
└── transaction-creation.orchestrator.ts

Ajouter après ligne 116:

typescript
// ÉTAPE 5: Distribuer commissions (si frais)
if (feeAmount > 0 && savedTransaction.status === TransactionStatus.COMPLETED) {
  await this.commissionDistributionService.distribute({
    transactionId: savedTransaction.id,
    transactionTypeId: dto.transactionTypeId,
    feeAmount: feeAmount,
    currencyCode: dto.currencyCode || 'XAF',
  });
}

📝 Exemple Complet de Flux

Scénario: Cash-In 10,000 XAF avec frais 200 XAF

1. Transaction Créée

json
{
  "id": "tx-123",
  "amount": 10000,
  "feeAmount": 200,
  "transactionTypeId": "deposit-cashin-uuid"
}

2. Frais Collectés

FEE_COLLECTION.balance: 0 → 200 XAF

3. Règles Appliquées

json
{
  "platform": { "percentage": 30, "amount": 60 },
  "corporate": { "percentage": 20, "amount": 40 },
  "merchant": { "percentage": 50, "amount": 100 }
}

4. Distribution (Prévue)

FEE_COLLECTION: 200 → 0 XAF
NxPay Wallet: 0 → 60 XAF
Corporate Wallet: 0 → 40 XAF
Merchant Wallet: 0 → 100 XAF

5. Enregistrements

sql
-- commission_distributions
INSERT INTO commission_distributions VALUES
  ('dist-1', 'tx-123', 'rule-platform', 'platform', 'account-nxpay', 60, 30, 'completed'),
  ('dist-2', 'tx-123', 'rule-corporate', 'corporate', 'account-corp', 40, 20, 'completed'),
  ('dist-3', 'tx-123', 'rule-merchant', 'merchant', 'account-merchant', 100, 50, 'completed');

🎯 Conclusion

État Actuel:

  • ✅ Configuration: 100%
  • ✅ Collecte: 100%
  • ⚠️ Distribution: 0%

L'argent est collecté mais pas encore distribué automatiquement.

Pour activer la distribution:

  1. Implémenter les services manquants (4-5 jours)
  2. Intégrer dans le flux de transaction
  3. Tester avec transactions réelles

Document créé le: 2026-01-29
Version: 1.0.0

NxPay — Plateforme fintech CEMAC