Skip to content
StableAudienceDevQAAudit banqueOwner@product-teamDernière revue2026-05-22

Cash-in agent — Flux complet et détails techniques

Détail théorique vs réel du flux de commissions cash-in, données techniques, schéma de flux global. Pour la vue d'ensemble, voir Cash-in agent — overview.

🔄 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

📍 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",    // Nex
      "percentage": 30.00               // 30%
    },
    {
      "beneficiaryType": "corporate",   // Corporate
      "percentage": 20.00,              // 20%
      "fallbackBeneficiaryType": "platform"  // Si absent → Nex
    },
    {
      "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é
Nex (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 (Nex)
// → Wallet système Nex (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


É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
  -- Nex
  (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

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: Nex 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 (Nex):

typescript
// Wallet système Nex
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

Détail des étapes

#ÉtapeStatutDétail
1Transaction cash-inimplémentéClient : 10 000 XAF → marchand. Frais : 200 XAF prélevés.
2Collecte des frais✅ implémentéclient.balance -= 200 XAF · FEE_COLLECTION.balance += 200 XAF · Ledger : DEBIT client, CREDIT FEE_COLLECTION.
3Récupération des règles✅ implémentéConfiguration Service — Nex 30 % → 60 XAF · Corporate 20 % → 40 XAF · Merchant 50 % → 100 XAF.
4Résolution des wallets⚠️ prévutransaction_entities → merchant_id, corporate_id · walletsService.findOneByEntity() → Wallet · Wallet.accounts → Account XAF.
5Distribution⚠️ prévuPour chaque bénéficiaire : FEE_COLLECTION.balance -= amount · BeneficiaryAccount.balance += amount · Ledger : DEBIT FEE_COLLECTION, CREDIT Beneficiary · commission_distributions: status='completed'.
6Résultat finalimplémentéFEE_COLLECTION : 0 XAF · Nex : +60 XAF · Corporate : +40 XAF · Merchant : +100 XAF · Total : 200 XAF ✅.

Nex — Plateforme fintech CEMAC