💰 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 XAF1.2. Création de la Transaction
Fichier: services/ledger-wallets/src/transactions/orchestrators/transaction-creation.orchestrator.ts
// 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:
// 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)
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:
{
"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éficiaire | Pourcentage | Montant Calculé |
|---|---|---|
| NxPay (platform) | 30% | 60 XAF |
| Corporate | 20% | 40 XAF |
| Marchand | 50% | 100 XAF |
| TOTAL | 100% | 200 XAF |
Fichier: services/configuration/src/commission-rules/commission-rules.service.ts
// 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
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-7894.2. Résolution des Wallets par Bénéficiaire
Logique Prévue:
// 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_idFichier Prévu: services/orchestrator/src/application/services/commission-distribution.service.ts
// 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
// 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:
// 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
// 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:
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
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:
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:
- Récupérer règles actives pour
transactionTypeId = DEPOSIT - Filtrer par
rule_type = 'fee_split' - Calculer:
montant = (feeAmount * percentage) / 100
Code:
CommissionRulesService.simulate()(ligne 264-339)CommissionRulesService.getApplicableRules()(ligne 223-263)
Exemple:
// 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:
// 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_distributionsFichiers à Créer:
services/orchestrator/src/application/services/commission-distribution.service.tsservices/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
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):
// Wallet système NxPay
const platformWallet = await walletsService.findOneByEntity(
'NXPAY_SYSTEM', // entity_id fixe
'platform' // entity_type
);CORPORATE:
// 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:
// 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é
Configuration des Règles ✅
- CRUD configurations
- Validation total = 100%
- Simulateur de distribution
- Historique
Collecte des Frais ✅
- Débit client
- Crédit FEE_COLLECTION
- Ledger entries
Calcul des Montants ✅
- Service de simulation
- Calcul par pourcentage
⚠️ Ce Qui Est Prévu Mais Non Implémenté
Distribution Automatique ❌
- Service
CommissionDistributionServicemanquant - Pas d'intégration dans
TransactionCreationOrchestrator
- Service
Résolution Wallets ❌
- Logique prévue mais non codée
- Pas de service
TransactionEntitiesService
Sauvegarde Distributions ❌
- Table
commission_distributionscréée - Pas de service pour l'utiliser
- Table
🔧 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.tsFonctionnalité:
- 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.tsFonctionnalité:
- 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.tsFonctionnalité:
- Récupérer règles depuis Configuration Service
- Résoudre entités depuis
transaction_entities - Résoudre wallets/comptes bénéficiaires
- Créer transferts depuis FEE_COLLECTION
- Sauvegarder dans
commission_distributions
Phase 4: Intégration (1 jour)
Modifier:
services/ledger-wallets/src/transactions/orchestrators/
└── transaction-creation.orchestrator.tsAjouter après ligne 116:
// É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
{
"id": "tx-123",
"amount": 10000,
"feeAmount": 200,
"transactionTypeId": "deposit-cashin-uuid"
}2. Frais Collectés
FEE_COLLECTION.balance: 0 → 200 XAF3. Règles Appliquées
{
"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 XAF5. Enregistrements
-- 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:
- Implémenter les services manquants (4-5 jours)
- Intégrer dans le flux de transaction
- Tester avec transactions réelles
Document créé le: 2026-01-29
Version: 1.0.0