Module FieldAgent — Opérations cash en tournée
Module dédié aux opérations cash collectées par un agent terrain
(FIELD_AGENT) hors de l'agence physique, sans session de caisse.
Pourquoi un module séparé du TellerModule ?
TellerService impose l'ouverture / clôture d'une session caisse
(comptage cash, écarts, etc.). En tournée village, ce modèle n'est
pas applicable. FieldAgentService :
- Travaille sans session (
TellerSession) - Crée des transactions standards (
DEPOSIT,LOAN_REPAYMENT) tagguées viaperformedById= userId agent pour audit + suivi - Délègue à
SavingsService.performDeposit()pour la mécanique comptable + génération de référence transaction
Le rapprochement final (l'agent rend le cash au caissier en rentrant) sera tracé par un workflow back-office dédié à venir.
Endpoints
POST /v1/field-agent/cash-deposit
Rôles : FIELD_AGENT, DIRECTOR
// Body
{ "accountId": "uuid", "amount": 5000, "notes": "Tournée village Tové" }
// Réponse
{
"transaction": { "id", "reference", "amount", "balanceAfter", "performedAt" },
"account": { "id", "accountNumber" }
}
Effets :
- Transaction
DEPOSITcréée sur le compte - Écritures comptables OHADA automatiques (via
SavingsService) - Audit log
field-agent.cash-deposit - SMS au membre via
MemberNotifier(templateteller.deposit) - Push notification au membre via
MemberPushHelper.notifyDeposit(channelFIELD_AGENT)
POST /v1/field-agent/loan-repayment
Rôles : FIELD_AGENT, DIRECTOR
// Body
{ "loanId": "uuid", "amount": 10000, "notes": "..." }
// Réponse
{ "ok": true, "paidSchedules": 1, "loanClosed": false }
Effets :
- Impute le montant sur les échéances
PENDING/OVERDUE/PARTIALdans l'ordre chronologique - Marque les échéances
PAIDquandpaidAmount >= totalDue - Si toutes les échéances sont soldées →
LoanApplication.status = CLOSED - Trace une transaction
LOAN_REPAYMENT(informative) sur le 1er compte actif du membre - Audit, SMS (
loan.repaid), push (MemberPushHelper.notifyLoanRepayment)
⚠️ Différence avec TellerService.loanRepayment : pas de débit
d'un compte épargne du membre (le cash est collecté directement).
POST /v1/field-agent/loan-disbursement
Rôles : FIELD_AGENT, DIRECTOR
// Body — le montant est fixé par le prêt approuvé, pas par l'agent
{ "loanId": "uuid", "notes": "Remis au domicile, présence de Mme Y" }
// Réponse
{
"ok": true,
"reference": "PRT-2026-001",
"amount": 100000,
"loan": { /* loan complet avec schedules */ }
}
Logique :
- Vérifie que le prêt existe, est
APPROVED, et aapprovedAmount > 0 - Délègue tout à
LoansService.disburse(tenant, loanId, { description }, agentId)avectoAccountId: undefined(= cash direct, pas de virement compte épargne) LoansService.disbursegère :- Génération de l'échéancier (cf.
buildSchedule()) - Passage à
ACTIVE+disbursedAt = now - Écritures comptables OHADA via
AccountingService - SMS
loan.disbursedau membre - Push notification
MemberPushHelper.notifyLoanDisbursement - Webhook
LOAN_DISBURSED
- Génération de l'échéancier (cf.
- Le wrapper field-agent ajoute uniquement :
- Audit log
field-agent.loan-disbursement(canalFIELD_AGENT, notes terrain)
- Audit log
⚠️ Séparation des pouvoirs : un agent ne peut pas approuver et
décaisser le même dossier. L'APPROVED doit venir d'un DIRECTOR ou
CREDIT_MANAGER en amont (via le back-office web).
GET /v1/field-agent/my-day
Rôles : FIELD_AGENT, DIRECTOR
Résumé des opérations de l'agent aujourd'hui
(performedAt >= startOfDay).
{
"date": "2026-06-09T00:00:00.000Z",
"operationsCount": 7,
"totalDeposit": 35000, // collecté (dépôts)
"totalRepayment": 12000, // collecté (remboursements crédit)
"totalDisbursement": 50000, // sorti vers les membres (décaissements)
"totalCollected": 47000, // = totalDeposit + totalRepayment
"recentOperations": [
{ "id", "type", "amount", "reference", "performedAt" },
// ... jusqu'à 20
]
}
totalCollected exclut volontairement totalDisbursement — le
décaissement n'est pas de l'argent encaissé, c'est de l'argent
distribué. Pour le rapprochement caisse de fin de tournée, l'agent
doit avoir : (cash de départ) + totalCollected − totalDisbursement = (cash en poche actuel).
Utilisé par l'écran « Ma journée » du profil agent mobile.
Dépendances
SavingsService(mécanique dépôt + génération référence)MemberNotifier(SMS via NotificationsModule global)MemberPushHelper(push via PushModule global)AuditService
Audit actions
field-agent.cash-deposit— newValue :{ accountId, memberId, amount, channel: 'FIELD_AGENT', notes }field-agent.loan-repayment— newValue :{ amount, channel: 'FIELD_AGENT', schedules: [scheduleIds] }field-agent.loan-disbursement— newValue :{ amount, memberId, channel: 'FIELD_AGENT', notes }
UI mobile (Flutter)
Côté fiche membre agent (lib/screens/agent/member_detail_screen.dart) :
- Bouton « Encaisser dépôt » → ouvre
CashOperationSheet(kind=deposit) avec liste des comptes ACTIVE - Bouton « Remboursement crédit » → ouvre
CashOperationSheet(kind=loanRepayment) avec liste des crédits ACTIVE - Le sheet ferme après succès et invalide le provider de détail membre (refresh comptes + crédits)
Profil agent (lib/screens/agent/agent_profile_screen.dart) :
- Carte « Ma journée » consomme
/field-agent/my-dayvia_myDayProviderRiverpod
À venir
- Workflow rapprochement caisse : l'agent rend le cash au TELLER
qui valide le différentiel
totalCollected − totalDisbursement - Mode hors-ligne avec queue de sync (utile en zone sans réseau)
- Génération de reçu PDF imprimable côté mobile
- Photo de la pièce d'identité à la création membre (KYC accéléré)