Banque technique¶
Le canal ledger locksmith utilise l’ordre de verrouillage inventaire → compte → véhicule/clés et un commit final partagé. Le request_id persistant empêche un double débit lors d’un retry après timeout.
La migration 0027_banking_system crée bank_accounts, bank_account_members, bank_cards,
bank_transactions et bank_terminals, ajoute groups.is_financial_authority, et supprime
character.cash/character.bank_balance (aucune donnée n'est convertie : ces champs n'étaient
mutés par aucune route). Le catalogue d'objets reçoit deux entrées : cash (empilable, 10 000) et
bank_card (non empilable).
Modèle de données¶
bank_accounts:account_numberopaque unique,owner_type(character/joint/group),owner_group_id(ON DELETE SET NULL) etowner_group_label_snapshot— dénormalisé parservices/groups.py:delete_groupavant la suppression d'un groupe titulaire, pour garder une trace lisible côté admin une fois le groupe disparu.owner_character_label_snapshotjoue le même rôle pour un compte personnel/joint dont le dernier titulaire est supprimé :services/admin_characters.py:delete_characterretire explicitement la lignebank_account_membersdu personnage supprimé (sans dépendre duON DELETE CASCADEdéclaratif de la migration, pour un comportement identique quel que soit le moteur) et, seulement si plus aucun titulaire ne reste, y écrit ce nom avant suppression.balancereste toujours ≥ 0.bank_account_members:role(primary/co_owner) etstatus(pending/accepted) — l'acceptation d'invitation à un compte joint est un simple changement de statut, sans table d'invitation dédiée contrairement aux groupes.bank_cards:pin_hash(PBKDF2-HMAC-SHA256, sel aléatoire, jamais le PIN en clair),failed_attempts,status(active/deactivated/destroyed). L'objet d'inventaire correspondant ne référence la carte que par son identifiant dans ses métadonnées ; toute donnée sensible reste côté API.bank_transactions:channel(teller/card/admin) détermine siactor_character_idest exposé aux joueurs — visible aux co-titulaires pourteller, jamais pourcardniadmin.bank_terminals:kind(npc/atm) pour les PNJ bancaires et distributeurs personnalisés placés par l'administration. Les ATM natifs du jeu n'ont aucune ligne : ils sont détectés côté client par modèle (nativeAtmModelsdansmedusa-bank/config.json).
Services (api/app/services/)¶
bank_shared.py porte les fonctions transverses : génération de numéros opaques
(régénération sur collision), hachage/vérification de PIN, et authorize_operator — le point
d'autorisation unique pour toute opération basée sur l'identité (titulaire personnel/joint accepté,
ou permission de groupe GROUP_CAN_MANAGE_BANK_ACCOUNT si owner_group_id n'est pas NULL). Un
compte de groupe orphelin (owner_group_id IS NULL) échoue toujours cette autorisation par
construction : aucune identité ne peut plus l'opérer, seules les opérations par carte restent
possibles.
bank_accounts.py gère ouverture/invitation/clôture. La clôture (close_account,
admin_close_account) n'exige plus un solde nul : un solde restant est automatiquement retiré en
argent liquide dans l'inventaire du personnage qui clôture (échec explicite et rien n'est écrit si
son inventaire ne peut pas le contenir), journalisé comme un retrait teller ; une clôture
administrative sans personnage en contexte l'annule à la place via un ajustement admin auditable.
account_by_id/account_by_number (bank_shared.py) utilisent
execution_options(populate_existing=True), comme admin_characters.character_by_id, pour qu'une
relecture du compte après une mutation dans la même session (ex. après delete_character) ne
renvoie jamais une collection members/cards mise en cache avant cette mutation. bank_ledger.py
gère dépôt, retrait,
transfert et ajustement admin : dépôt et retrait verrouillent toujours l'inventaire avant le compte
(même ordre que les opérations par carte à un distributeur, pour ne jamais s'inverser avec elles et
provoquer un interblocage), puis effectuent la mutation d'inventaire et la mutation de solde dans la
même transaction (credit_item_no_commit/debit_item_no_commit dans services/inventories.py, qui
ne committent jamais elles-mêmes, précisément pour cette atomicité inter-domaines). Un transfert
résout d'abord l'identifiant du compte destinataire sans verrou, puis verrouille les deux comptes
dans l'ordre de leurs identifiants triés, quel que soit le sens du transfert, pour qu'un aller et un
retour concurrents entre les deux mêmes comptes ne puissent jamais s'interbloquer. Le signalement
cumulé
(evaluate_flag) agrège bank_transactions.amount sur une fenêtre glissante indexée
(account_id, created_at), sans jamais bloquer la transaction qui la franchit.
bank_cards.py gère émission, désactivation/réactivation et les trois opérations ATM
(atm_deposit/atm_withdraw/atm_balance), qui résolvent la carte depuis l'objet d'inventaire du
porteur (item_instance_id), vérifient le PIN et incrémentent/réinitialisent
failed_attempts de façon atomique ; au troisième échec, la carte passe à destroyed et son objet
d'inventaire est supprimé dans la même transaction.
Routes (api/app/routers/bank.py)¶
internal_router (/internal/v1/bank/..., clé interne) sert la ressource FiveM medusa-bank :
comptes, invitations, dépôt/retrait/transfert, cartes, opérations ATM, relevé /bank. Il expose
aussi /internal/v1/bank/admin/... (comptes, registre, cartes, autorité financière) car
medusa-admin n'a pas de session OAuth navigateur : ce sont les équivalents F10 de
/admin/bank/.... Ces routes attendent discord_role_ids, le nom de champ produit par l'utilitaire
partagé actorPayload() de medusa-admin/server.js (motif déjà utilisé par buffs,
time-weather et la modération) — à ne pas confondre avec /internal/v1/bank/terminals, qui
attend actor_role_ids en suivant le motif de world_props (payload construit à la main dans
medusa-bank/server.js, sans passer par actorPayload()). Les deux conventions coexistent déjà
dans le dépôt selon l'utilitaire client utilisé ; vérifier celui réellement appelé avant d'ajouter
une route interne gardée par rôles.
admin_router (/admin/bank/..., session OAuth + CSRF) sert l'admin web. Les deux surfaces
partagent les mêmes fonctions de service ; seules l'authentification et l'autorisation diffèrent.
Ressource FiveM medusa-bank¶
PNJ bancaire et distributeurs personnalisés sont persistés dans bank_terminals et synchronisés au
client via medusa:bank:terminals:sync (diffusion périodique par empreinte, comme
medusa-clothing-shops/medusa-world-props). Le placement lui-même est auto-suffisant dans ce
resource (medusa:bank:terminals:save/remove, autorisation par rôles via medusa-moderation, audit
via /internal/v1/moderation/admin-audit), suivant exactement le motif de medusa-world-props :
medusa-admin ne fait que déclencher exports['medusa-entity-placement']:start(...) puis
transmettre le résultat à medusa-bank, sans posséder lui-même la logique de persistance.
Le modèle PNJ proposé par défaut est u_m_m_bankman. La migration
0031_fix_bank_terminal_ped_model remplace uniquement les terminaux npc persistés avec l'ancien
modèle invalide s_m_m_bankman; les ATM et modèles personnalisés restent inchangés.
Les interactions bancaires utilisent des identifiants stables et les événements client locaux
medusa:bank:openTellerInteraction et medusa:bank:openAtmInteraction. Le descripteur remis à
medusa-interactions porte lockOnAction = true : il acquiert donc le verrou à la pression sur
E, et medusa-bank ne le rend via releaseInteraction qu'à la fermeture de la NUI, en cas de
refus/erreur ou lors de l'arrêt de la ressource. Les parcours /bank et ARPhone n'acquièrent pas ce
verrou.
Les ATM natifs sont détectés par un scan périodique (GetClosestObjectOfType sur chaque modèle
connu dans un rayon configurable) ; ils sont enregistrés/désenregistrés auprès de
medusa-interactions dynamiquement selon la proximité, sans jamais être écrits en base.
Le serveur (server.js) résout systématiquement le personnage actif via
exports['medusa-player-utils'].findBySource(source, true) — jamais un character_id envoyé par le
client — sauf pour les opérations ATM, où item_instance_id est résolu et validé côté API dans
l'inventaire du personnage réellement connecté à cette source, ce qui empêche un client de désigner
un objet qu'il ne possède pas.
Admin web et F10¶
Les deux surfaces admin appellent les mêmes contrats fonctionnels (comptes/registre/cartes/autorité
financière) mais via des routes différentes (session navigateur vs actor_role_ids). Le module web
n'offre pas la création de PNJ/distributeur (pas de caméra 3D dans un navigateur) : seul le panneau
F10 place réellement une entité, le web ne fait qu'ajuster une position déjà connue, exactement comme
pour les objets persistants.
La synchronisation des terminaux est une file single-flight : un bootstrap peut utiliser le retry
transitoire partagé, les polls ordinaires restent simples, et une demande force reçue pendant une
lecture est rejouée après celle-ci. Le cache n'est remplacé qu'après une réponse valide et les logs
de panne sont dédupliqués jusqu'au rétablissement.
Intégration ARPhone¶
medusa-bank enregistre son application auprès de medusa-arphone. Le shell ne réutilise que GET /internal/v1/bank/statement/{character_id} et n'expose aucune mutation bancaire.
Règlements énergie¶
Le domaine énergie réutilise les verrous et autorisations bancaires sans changer l'unité générale en dollars. Réservation, formule à point fixe, compte système, ledger et recovery sont décrits dans Énergie des véhicules.