Modifier une dépense partagée doit redéfinir le partage
Les valeurs par défaut d'une modification sont des bugs d'argent, donc Dimesum rend participants et split_type obligatoires sur chaque PATCH de dépense, aux côtés de base_version qui rejette une modification obsolète au lieu de la fusionner.
Corriger une faute de frappe ne devrait pas changer qui doit de l'argent. Dans Dimesum, modifier une dépense partagée redéfinit tout le partage. participants et split_type sont obligatoires sur chaque PATCH, et une modification qui omet l'un ou l'autre revient en 400 au lieu de remplir une valeur par défaut. Les valeurs par défaut de la création sont tous les membres actuels et un partage égal, qui sur une modification sont des bugs d'argent.
Deux échecs rendent le cas concret. Un colocataire arrivé en août est entraîné dans le dîner de juillet, parce que « tous les membres actuels » est évalué au moment où la modification arrive, non au moment où la dépense a été écrite. Un partage de loyer délibéré de 70/30 s'aplatit à 50/50, parce qu'un split_type absent signifie EQUAL. Aucun de ces échecs ne déclenche d'erreur, et tous deux déplacent de l'argent réel.
Les valeurs par défaut d'une modification sont des bugs d'argent. Une valeur par défaut au moment de la création devine le groupe que l'auteur a sous les yeux à cet instant. Une modification arrive plus tard, contre un groupe qui a bougé, et la même supposition réécrit discrètement ce que les gens doivent.
Les valeurs par défaut de la création décrivent une nouvelle dépense, pas une ancienne
Au moment de la création, les valeurs par défaut sont honnêtes. L'auteur regarde le groupe tel qu'il est, et un partage égal entre tout le monde est le cas courant, donc Dimesum les remplit tous les deux. La dépense enregistre ses données saisies plutôt que ses seuls résultats : splits.percent_bp, splits.weight et splits.exact_minor conservent ce que l'utilisateur a tapé, de sorte qu'une modification ultérieure peut la rouvrir.
Une modification est un autre acte. La même dépense peut être corrigée des semaines plus tard, une fois qu'un nouveau colocataire est arrivé ou qu'un membre fantôme a été revendiqué. L'appartenance au groupe est une cible mouvante et le partage ne l'est pas. Réutiliser les valeurs par défaut de la création demande au groupe d'aujourd'hui de répondre à une question à laquelle l'ancienne dépense a déjà répondu.
Le refus de deviner n'est pas nouveau ici. Une dépense à plusieurs payeurs doit elle aussi redéfinir ses payeurs. soleStoredPayer réutilise le payeur enregistré seulement quand la dépense en a exactement un, si bien qu'une modification qui reste muette sur deux payeurs est refusée plutôt que réattribuée. Une modification qui ne dit rien des payeurs conserve le payeur propre à la dépense, jamais la personne qui effectue la modification.
L'API refuse une modification qui ne redéfinit pas son partage
La vérification de la passerelle s'exécute avant tout calcul d'argent. Quand participants est vide ou split_type est laissé blanc, la requête revient en 400 avec le code invalid_expense et le message « an edit must restate the split: participants and split_type are required ». Le service de dépenses répète la règle dans validateAmend, si bien qu'un appelant qui atteint le service par un autre chemin rencontre le même refus.
| Champ | À la création | Sur une modification | Ce que la valeur par défaut coûterait |
|---|---|---|---|
participants | Facultatif. Par défaut, tous les membres actuels | Obligatoire | Un membre arrivé après rejoint une ancienne dépense |
split_type | Facultatif. Par défaut, EQUAL | Obligatoire | Un partage 70/30 s'aplatit à 50/50 |
payers | Facultatif. Par défaut, l'auteur | Omis, conserve l'unique payeur de la dépense ; deux payeurs doivent être redéfinis | Celui qui modifie devient le payeur, inversant qui doit à qui |
base_version | Non envoyé | Obligatoire, et doit être égal à la version actuelle | Une modification obsolète écrase un changement que son auteur n'a jamais lu |
revision_id | UUIDv7 client, la clé d'idempotence | Idem, un par version | Une modification réessayée facture le groupe deux fois |
currency | Indiquée par dépense | Doit correspondre ; un changement est refusé | Une ligne de soldes contient une seule devise par membre |
La devise appartient à la même famille de refus. Une modification ne peut pas redénominer une dépense, parce qu'une ligne de soldes contient une seule devise par membre. Le côté écriture rejette le changement, et le grand livre en ajout seul met de côté un tel amendement si jamais il l'atteint. Refuser à la porte empêche les deux moitiés d'être en désaccord.
Une modification obsolète est rejetée pour son auteur, jamais fusionnée
base_version est l'autre moitié du contrat. Chaque PATCH porte la version que son auteur a lue, et checkTransition la compare à la ligne que la transaction vient de verrouiller. Égales, et la modification s'applique à la version plus un. Différentes, et l'appelant reçoit un HTTP 409 avec le code stale_version.
La fusion est l'alternative tentante, et elle est fausse. Deux modifications d'une même dépense sont deux énoncés complets de ce que la facture signifie. Les fusionner produit un troisième énoncé que personne n'a écrit, avec des parts qu'aucun des deux auteurs ne reconnaîtrait. Le rejet rend le conflit à la seule personne qui peut le résoudre.
L'idempotence et la concurrence sont tenues à l'écart l'une de l'autre à dessein. L'insertion de révision s'exécute avant la vérification de version, parce qu'une modification réessayée porte la version de base qu'elle a lue à l'origine, désormais obsolète. Une réexécution doit se lire comme une réexécution plutôt que comme un conflit, donc revision_id répond en premier et renvoie le résultat enregistré.
La réponse porte déjà ce dont la prochaine modification a besoin
Exiger davantage de champs sur un PATCH n'est juste que si un client peut les obtenir à peu de frais. Chaque réponse de dépense porte version, de sorte qu'un client qui vient d'écrire une dépense peut la modifier sans seconde lecture. La réponse de création, la réponse de modification et chaque ligne de liste portent le même champ.
Pour le client qui n'a pas écrit la dépense à l'instant, GET /v1/groups/{id}/expenses/{id} renvoie la version, les parts calculées et les données qui les sous-tendent. Ces données reviennent sous les mêmes noms qu'un PATCH accepte : participants, split_type, percents, weights, shares, items, pools. Un client lit une seule forme et la renvoie avec ses modifications, au lieu de traduire entre deux vocabulaires pour une même facture.
La symétrie compte le plus pour les partages qui ne peuvent pas être reconstruits. Un partage PERCENT ou une facture détaillée ne peut pas être redéfini à partir de ses seules parts résolues, parce que l'arrondi a déjà été appliqué et que les points de base et les lignes de détail ont disparu. Les instantanés de révision portent aussi les données saisies, de sorte que la feuille d'historique peut montrer ce qui était détaillé sur n'importe quelle version passée.
Obligatoire maintenant, parce qu'une exigence ne peut pas être ajoutée plus tard
Exiger un champ dès le premier jour est une décision sur l'avenir plutôt que sur aujourd'hui. Assouplir un champ obligatoire plus tard reste rétrocompatible : les clients l'envoient déjà, et le serveur commence à accepter les requêtes sans lui. Ajouter une exigence plus tard casse tous les clients qui comptaient sur l'ancienne valeur par défaut.
La direction se choisit donc une fois, tôt. Dimesum exige participants, split_type et base_version sur une modification tant que le nombre de clients est encore assez faible pour changer. Si une valeur par défaut sûre pour les modifications est un jour trouvée, les champs deviennent facultatifs et rien de ce qui est déjà livré ne cesse de fonctionner.
Faites qu'une modification redéfinisse ce qu'elle signifie
Les valeurs par défaut appartiennent à la création, où l'auteur peut voir le groupe auquel il consent. Sur une modification, les mêmes valeurs par défaut sont une supposition sur un groupe qui a depuis bougé. Votre prochaine étape : ouvrez votre propre point de terminaison de lecture et vérifiez qu'il rend les données de partage sous les noms de champ exacts que votre point de terminaison d'écriture accepte. Un client qui doit traduire entre les deux finira par en traduire un de travers.
Questions fréquentes
Pourquoi faut-il indiquer participants et split_type pour modifier une dépense partagée ?
Dimesum exige les deux champs parce que les valeurs par défaut de la création sont fausses pour une modification. À la création, Dimesum retient par défaut tous les membres actuels, avec un partage égal, ce qui correspond au groupe que l'auteur a sous les yeux. Une modification peut arriver des semaines plus tard, après l'arrivée de quelqu'un. Réutiliser ces valeurs par défaut ferait entrer un nouveau colocataire dans un ancien dîner et ramènerait un partage de loyer délibéré de 70/30 à 50/50, sans qu'aucune erreur ne s'affiche.
Que se passe-t-il quand deux personnes modifient la même dépense en même temps ?
La seconde modification est refusée avec un HTTP 409 et le code d'erreur stale_version. Chaque PATCH porte base_version, la version que son auteur a lue, et le chemin de modification la compare à la ligne verrouillée. Un écart signifie que la dépense a bougé, donc la modification revient à son auteur pour être réappliquée sur la version qu'il peut désormais voir. Rien n'est fusionné.
Modifier une dépense met-il à jour les écritures du grand livre directement ?
Non, modifier une dépense ne met jamais à jour une écriture du grand livre dans Dimesum. Une modification enregistre deux écritures dans une seule transaction : un EXPENSE_REVERSAL qui annule l'ancienne version poste par poste, puis une écriture EXPENSE pour la nouvelle version. UPDATE et DELETE sont retirés du rôle de base de données propre au grand livre, si bien qu'une mutation est impossible même pour du code bogué. Vous voyez un badge de modification et une feuille d'historique.
Faut-il un second appel API avant de modifier une dépense partagée ?
Non, un client qui vient d'écrire la dépense détient déjà la version dont une modification a besoin. Chaque réponse de dépense porte version, qui est ce que le prochain PATCH envoie comme base_version. Un client qui n'a pas écrit la dépense appelle GET /v1/groups/{id}/expenses/{id}, qui renvoie la version ainsi que les données de partage sous les mêmes noms de champ qu'un PATCH accepte.
Pourquoi exiger participants et split_type dès maintenant plutôt que les ajouter plus tard ?
Exiger un champ dès le premier jour est réversible, l'ajouter plus tard ne l'est pas. Assouplir un champ obligatoire plus tard reste rétrocompatible : les clients l'envoient déjà, et le serveur commence à accepter les requêtes sans lui. Ajouter une exigence plus tard casse tous les clients qui comptaient sur l'ancienne valeur par défaut, et dans une API d'argent, la casse est silencieuse jusqu'à ce que le solde de quelqu'un soit faux.
Articles populaires
- Le grand livre append-only qui garde les soldes exacts9 min de lecture
- Six bugs de devises dans le partage de dépenses10 min de lecture
- Solder les comptes d'un groupe en moins de virements5 min de lecture
- Partager une addition quand un plat n'a pas été partagé10 min de lecture
- Partager le loyer équitablement entre colocataires6 min de lecture