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 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
📍 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", // 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éficiaire | Pourcentage | Montant Calculé |
|---|---|---|
| Nex (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 (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_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
É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
-- 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:
- 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: 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:
// 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 (Nex):
// Wallet système Nex
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
Détail des étapes
| # | Étape | Statut | Détail |
|---|---|---|---|
| 1 | Transaction cash-in | implémenté | Client : 10 000 XAF → marchand. Frais : 200 XAF prélevés. |
| 2 | Collecte des frais | ✅ implémenté | client.balance -= 200 XAF · FEE_COLLECTION.balance += 200 XAF · Ledger : DEBIT client, CREDIT FEE_COLLECTION. |
| 3 | Récupération des règles | ✅ implémenté | Configuration Service — Nex 30 % → 60 XAF · Corporate 20 % → 40 XAF · Merchant 50 % → 100 XAF. |
| 4 | Résolution des wallets | ⚠️ prévu | transaction_entities → merchant_id, corporate_id · walletsService.findOneByEntity() → Wallet · Wallet.accounts → Account XAF. |
| 5 | Distribution | ⚠️ prévu | Pour chaque bénéficiaire : FEE_COLLECTION.balance -= amount · BeneficiaryAccount.balance += amount · Ledger : DEBIT FEE_COLLECTION, CREDIT Beneficiary · commission_distributions: status='completed'. |
| 6 | Résultat final | implémenté | FEE_COLLECTION : 0 XAF · Nex : +60 XAF · Corporate : +40 XAF · Merchant : +100 XAF · Total : 200 XAF ✅. |