feat(gating): reserve transaction splits for the Base edition #452
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#452
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-369-split-base-edition"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #369
Réserve la Répartition (bouton de la page Transactions) à l'édition Base, comme l'écran de déverrouillage le promettait déjà (
upsell.features.adjustments). La page Transactions reste Free ; seul ce bouton est gaté, paradjustments.Comportement
UpsellDialog), jamais la fenêtre de répartition, pour toute ligneL'icône orange d'une ligne répartie reste dans tous les cas. Les répartitions existantes restent en base, comptées par les rapports et le tableau de bord (ADR 0017 §5).
Code
src/shared/splitGate.ts:splitAccessFor(gate)→open,lockedoupending. Plus strict queprofileGate:allowedn'y court-circuite pasready(documenté et testé), puisquereadyest faux pendant le chargement comme sur erreur.src/components/transactions/SplitButton.tsx: bouton sans état, qui décide de ce qu'ouvre un clic.src/components/shared/UpsellDialog.tsx:UpsellGatedans une fenêtre (role="dialog",aria-modal, nom accessibleupsell.title), fermée par son X et par « J'ai déjà une clé ».TransactionTable: propsplitAccessobligatoire, puisqu'une valeur par défaut « open » laisserait un nouvel appelant hors du gate sans rien dire. La fenêtre de répartition n'est pas rendue enlocked, mais reste montée pendant un rafraîchissement de licence :refresh()repasse parloading, et une garde=== "open"perdrait la saisie en cours. L'incitation n'est pas rendue enopen.TransactionsPage:useEntitlement("adjustments").Docs
ADR 0017 (amendement daté, matrice, §4, §5, Liens), guide (Transactions, Éditions), docs intégrées FR et EN (
docs.transactions,docs.editions),architecture.md(comptes de composants, table du gating, phrase « Modules Free »), commentaire d'en-tête d'entitlements.ts, CHANGELOG (Changed / Modifié).Laissés tels quels, volontairement :
docs.gettingStarted.features[2]et la liste de la section 1 du guide. Cette liste ne marque l'édition d'aucune fonction (ni le Budget, ni les profils multiples), alors que l'analyse de l'issue la citait parmi les textes sans mention d'édition.Tests
splitGate.test.ts: les trois éditions, etpendingavantready, queallowedsoit vrai ou faux.SplitButton.test.tsx: cadenas, orange,disabledet titre selon l'état ; le clic, par les props du bouton (verrouillé → incitation, sur une ligne répartie comme sur une ligne simple ; ouvert → fenêtre de répartition ; en attente → rien) ; la clétransactions.splitLockeddans les deux langues.UpsellDialog.render.test.tsx: rendu nommé, fermeture par le X et paronNavigate, tier requis par la fonction.TransactionTable.wiring.test.ts, avec le harnaishookHarness(#409) : ce qu'ouvre chaque clic, sur les deux lignes ; la fenêtre de répartition survit à un rafraîchissement et se ferme en Gratuite ; l'incitation survit à un rafraîchissement et disparaît en Base.TransactionTable.render.test.tsxetTransactionsPage.render.test.tsx: un cadenas par ligne en Gratuite, aucun en Base, bouton inactif tant que la licence n'est pas prête, et la page demande bienadjustments. Les vérifications de la page ne regardent que le bouton Répartition, la pagination ayant toujours un bouton désactivé.Résultats :
npm run buildvert ;npm test2248 tests sur 134 fichiers (2222 avant).Passe de mutations
Une mutation à la fois sur
cb7f847, restaurée par git, arbre propre à la fin : 21 mutations, 21 rouges, aucune survivante.openpendingdonnelockedpendingavecalloweddonneopenlockeddisabledretiré enpendingbudget=== "open"access="open"en duruseEntitlement("budget")ready ? "locked" : "pending")onNavigatenon câblébaseen dur dans la fenêtresplitLockedabsent d'en.jsonAprès le merge
STATE.md: la notification de la-compagnie-maximus#180 rejoint « Vérifications dues à la prochaine release », avec la vérification dans l'app (Gratuite : cadenas et écran de déverrouillage ; Base : fenêtre de répartition). Un commentaire sur la-compagnie-maximus#180 dira « mergé, publié avec la prochaine release ».ProfileSwitchersurUpsellDialog, fermeture sur Échap).🤖 Generated with Claude Code
The Répartition (split) button of the Transactions page now belongs to the `adjustments` feature, as the unlock screen already promised (upsell.features.adjustments). On the Free edition the button keeps its place and its icon, carries a lock and opens the locked screen in a dialog instead of the split window, for every row, split or not. While the license is not ready it shows no lock and does nothing. Existing splits stay in the data, counted by the reports and flagged by the orange icon. - splitGate.ts: pure splitAccessFor(gate) -> open | locked | pending, stricter than profileGate: `allowed` does not short-circuit `ready`. - SplitButton: stateless button that decides what a click opens. - UpsellDialog: UpsellGate in a named modal dialog, closed by its X and by "I already have a key". - TransactionTable: required splitAccess prop. The split window is not rendered once the edition resolves to Free, and stays mounted through a license refresh; the upsell is not rendered once it resolves to Base. - TransactionsPage reads useEntitlement("adjustments"). - ADR 0017 amendment and matrix, user guide, in-app docs (FR, EN), architecture, entitlements header comment, both changelogs. Tests: the predicate, the button's markup and click, the dialog, the table's wiring through the shared hook harness (#409), the page under the three gate states, and the new key in both locales. Fixes #369 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>CI lue dans le log :
PR Check — Frontend, run 557, surcb7f847(pull/452/head), succès en 2 min 16.✓ built in 13.19s(tsc puis vite, l. 518)Test Files 134 passed (134)etTests 2248 passed (2248)(l. 703 et 704)FAILni×check-rustn'a pas tourné, ce qui est attendu : rien soussrc-tauri/ni.cargo/.Revue adversariale — PR #452 (
cb7f847)Verdict : APPROVE
Résumé. Le gate de la Répartition est complet et correct : le bouton était le seul point d'entrée non gaté de la fenêtre de répartition,
splitAccessForcouvre les trois états de la licence, et les gardes deTransactionTablesuivent les vraies transitions deLicenseContext. Les tests rougissent sur 14 des 15 mutations du relecteur ; la survivante est une épingle manquante sur un code correct.Ce qui a été exécuté
cb7f847(git archive),npm ci, puisnpm run buildvert etvitest run: 134 fichiers, 2248 tests verts, les chiffres du corps de la PR et du run 557 (statut agrégésuccesssur la tête).pendingqui ouvre la fenêtre de répartitionallowed ? "open" : "locked")=== "locked")locked(dans un DOM, le clic n'atteindrait jamais l'incitation)multi-profile(mêmes éditions, mauvaise clé)pending, jamais d'incitationsplitLockedFR avec l'édition en duraria-labelVérifié à la lecture
TransactionTable(gatée ici) et parAdjustmentsPage, déjà sousRequireFeature feature="adjustments"(App.tsx:87-88). Aucun autre appelant desaveSplitAdjustmentni dedeleteSplitAdjustment; le menu contextuel de la page n'offre que « Ajouter comme mot-clé », etTransactionDetailModal(exploration depuis le tableau de bord) est en lecture seule.LOAD_STARTpasse enloadingen gardant l'édition,LOAD_DONEpasse enreadyavec la nouvelle,VALIDATE_STARTne touche passtatus: aucun étatready+freetransitoire pendant un rafraîchissement, donc la garde!== "locked"ne démonte la fenêtre de répartition qu'à une vraie résolution en Gratuite. Le seul passage Gratuite → Base sans démontage de la table est le contrôle de révocation du démarrage qui revient « valide » ; la garde symétrique de l'incitation le couvre.parent_transaction_idn'apparaît que danstransactionService.ts(la liste écarte les enfants, l. 231, et la lecture des répartitions) et dans l'export : le tableau de bord additionne toutes les lignes, enfants compris (dashboardService.ts:87-99). Le CHANGELOG, le guide et l'ADR disent vrai.common.closeprésent dans les deux. La section 1 du guide etdocs.gettingStarted.features[2]laissées telles quelles : cohérent, cette liste ne marque l'édition ni du Budget ni des profils multiples.architecture.mdjustes : 13 composants dansshared/surmain(le « 12 » était périmé), 14 avecUpsellDialog; 5 → 6 danstransactions/. Amendement de l'ADR 0017 à sa place chronologique ; entrées du CHANGELOG sous Changed / Modifié, dans les deux fichiers.pull_refde #452 encloses, et le commit porteFixes #369. L'avis à la-compagnie-maximus#180 après la release passe par l'étape 0 du skill de release (PR #450) et par la puce STATE.md annoncée..onlyni.skip, aucune migration, aucun secret.Suggestions (non bloquantes)
TransactionTable.wiring.test.ts:128-132, le passage en Base vérifie que l'incitation disparaît, pas qu'aucune fenêtre de répartition n'apparaît à sa place. Garder le rendu"open"dans une variable et y ajouterexpect(splitWindows(upgraded)).toHaveLength(0): vert sur le code actuel, rouge sous R14 (rejoué). Même forme à la ligne 125 pour le cas symétrique,upsells(...)vide sur le rendu"locked"(non rejoué)."adjustments"est écrit trois fois pour la Répartition (TransactionsPage.tsx:47,SplitButton.tsx:43,TransactionTable.tsx:354). Une constante exportée desplitGate.ts(SPLIT_FEATURE: FeatureKey = "adjustments") ramènerait un futur déplacement (clé propre, Premium) à une ligne. Aujourd'hui, R10 et la mutation « fenêtre d'incitation pourbudget» de l'auteur gardent les trois littéraux alignés.ProfileSwitcher;PinDialogetImportConfirmModalposent unautoFocus.Hors périmètre, préexistant
Reclasser une ligne déjà répartie laisse la ligne de compensation dans l'ancienne catégorie :
updateTransactionCategory(transactionService.ts:301-315) ne met à jour que le parent, alors que la compensation a reçuparent.category_idà l'enregistrement (saveSplitAdjustment, l. 447-461). Rejoué en SQLite surconsolidated_schema.sql: parent −120 en A, répartition −50 en B, d'où A −70 et B −50 ; parent reclassé en C, d'où A +50, B −50 et C −120, compté tel quel par le tableau de bord. À la réouverture,SplitAdjustmentModal(l. 49-52) ne reconnaît la compensation que par « même catégorie que le parent, signe opposé » et l'affiche donc comme une entrée ordinaire (A, 50). En Base, retirer cette entrée puis enregistrer répare ; en Gratuite, cette PR ferme ce chemin. Rare et sans lien avec le diff : mérite une issue (propager la catégorie à la compensation, ou la reconnaître autrement que par sa catégorie).Copie hors dépôt et script de mutations supprimés en fin de revue ; dépôt local inchangé.
Suites de la revue (commentaire 24129) appliquées en tête, à la demande de Max, en deux commits non relus : la nouvelle tête est
a8c3fd6, l'APPROVE portait surcb7f847.d5d0e89test(gating): pin that one click never arms the other window(suggestion 1) : les deux changements d'édition vérifient maintenant que l'autre fenêtre reste fermée, pendant le rafraîchissement de licence comme après.a8c3fd6refactor(gating): one constant for the split's feature key(suggestion 2) :SPLIT_FEATUREdanssplitGate.ts, lu par la page, le bouton et la table. Aucun changement de comportement.Preuve du rouge, sur l'arbre corrigé, chaque fichier restauré depuis une copie et l'arbre vérifié identique après coup :
SPLIT_FEATUREpassé àbudgetLes tests concernés passent avant et après les mutations (42 sur 42). Tip
a8c3fd6validé en worktree propre :npm ci, sonde rouge (rc=1),npm run build,npm testà 2248 tests sur 134 fichiers.Le point 3 (focus de la fenêtre) est versé dans #451 (commentaire 24139) ; le défaut préexistant de la compensation devient #454.
Classement selon
/merge-stack: un test et un refactor sans changement de règle, donc merge sans nouvelle relecture, en nommant ici les deux commits non relus.