Financement et réconciliation
Comment l'argent arrive sur le compte d'un enfant (argent de poche, demande acceptée, mission validée) quand la banque branche son core banking, et comment magot reste aligné sur la banque, qui fait foi.
Principe
magot ne détient pas de fonds. Quand un parent recharge, magot demande un virement du compte des parents vers celui de l'enfant ; le solde affiché dans l'app n'augmente qu'au règlement de ce virement par la banque.
Sans port de financement branché (démo, sandbox), le solde magot reste un registre interne crédité immédiatement : c'est le comportement actuel.
Port FundingAdapter
transfer({ transferId, householdRef, childRef, toAccountRef, amountCents, label })
→ { ref, transferId, status: "pending" | "settled" | "rejected", amountCents, rejectReason }
getTransfer(ref) → même forme, ou null
transferIdest un UUID fixé par magot et dérivé de l'opération (tenant + endpoint +Idempotency-Keyde la requête app). La banque doit être idempotente sur cet identifiant : un second appel rend le même virement et ne débite jamais deux fois. magot s'en sert pour rejouer sans risque après une panne ou un timeout.householdRef/childRef: identifiants magot du foyer et de l'enfant, que la banque associe à ses comptes ;toAccountRef: le compte de l'enfant chez l'émetteur quand il est connu.- Suite de tests de contrat :
test/funding-contract.test.ts, à passer telle quelle par toute implémentation.
Déroulé d'une recharge
| Réponse de la banque | Effet chez magot | Réponse à l'app |
|---|---|---|
settled | solde crédité, TopUpReceived | funding_status: "settled" + nouveau solde |
pending | rien au solde, TopUpPending | funding_status: "pending" + transfer_id |
rejected | rien n'est enregistré | 402 avec la raison |
| injoignable / timeout (5 s) | virement « à redemander », TopUpPending | funding_status: "pending" |
Demande acceptée et mission validée suivent le même chemin. Un refus immédiat annule toute l'opération : la demande reste en attente, la mission reste à valider, le parent peut réessayer.
Règlement asynchrone : webhook de la banque
POST https://<hôte-autorisation>/webhooks/funding/transfer
{ "transfer_id": "<uuid magot>", "status": "settled" | "rejected",
"provider_ref": "<réf. banque>", "reject_reason": "..." }
Même authentification que le webhook d'autorisation (issuer-webhook.md) : signature HMAC, horodatage, mTLS, établissement déduit du credential. Réponses :
200 { applied: true }: règlement appliqué (solde crédité, enfant notifié) ou rejet enregistré (TopUpRejected) ;200 { applied: false, status }: déjà résolu — la banque peut rejouer sa notification sans effet ; un règlement arrivant après un rejet ne crédite pas ;404: virement inconnu de cet établissement.
Réconciliation
Un passage toutes les 15 minutes (RECONCILE_INTERVAL_MINUTES), par
établissement ayant branché un port, un seul passage à la fois :
- Virements en attente : redemandés à la banque avec le même identifiant si elle n'avait jamais répondu, relus sinon ; réglés ou rejetés chez magot selon sa réponse.
- Gels de carte non transmis à l'émetteur (panne lors du gel) : rejoués.
- Soldes : solde du compte chez l'émetteur contre portefeuille + pots
magot (les pots sont des enveloppes internes du même compte). Un écart
produit
BalanceMismatchà son apparition ou quand il varie, puisBalanceReconciledquand il se résorbe. Il n'est pas corrigé automatiquement : corriger un solde est une décision humaine.
Chaque passage est tracé dans reconciliation_run (compteurs, écarts,
erreurs par élément ; une erreur n'arrête pas le passage).
Limite actuelle
Les paiements carte réels ne sont pas encore importés depuis l'émetteur
(listTransactions) : tant que ce n'est pas fait, chaque achat carte
apparaîtra comme un écart de solde. C'est la prochaine étape du port
émetteur.