Aller au contenu

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_number opaque unique, owner_type (character/joint/group), owner_group_id (ON DELETE SET NULL) et owner_group_label_snapshot — dénormalisé par services/groups.py:delete_group avant la suppression d'un groupe titulaire, pour garder une trace lisible côté admin une fois le groupe disparu. owner_character_label_snapshot joue le même rôle pour un compte personnel/joint dont le dernier titulaire est supprimé : services/admin_characters.py:delete_character retire explicitement la ligne bank_account_members du personnage supprimé (sans dépendre du ON DELETE CASCADE dé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. balance reste toujours ≥ 0.
  • bank_account_members : role (primary/co_owner) et status (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 si actor_character_id est exposé aux joueurs — visible aux co-titulaires pour teller, jamais pour card ni admin.
  • 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 (nativeAtmModels dans medusa-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.