main
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f3fa8e7456 |
Confirm a plan switch in the shared dialog
#291. A plan switch (Upgrade, Downgrade, or Pay for this plan on a granted tier's card) was confirmed in a panel at the top of the Plans section, above the cards and away from the button that opened it. It now confirms in the same dialog as Cancel plan and Resume plan. The button opens the dialog at once, titled with the panel's question ("Switch to Pro?", or "Pay for Pro?" on the granted tier's own card), and the dialog's body loads from the switch's preview route, because the charge it states comes from Stripe at that moment. Keep current plan closes the dialog and sends nothing. An answer that belongs in the dialog stays there: a declined card, a payment awaiting the bank's approval, stale figures; focus moves to the new form. An answer that lands outside closes it: the switch made or refused, or a plan withdrawn after the page loaded. A load that fails closes the dialog with the error toast; a commit that fails (403 or 5xx, which the pages' htmx config does not swap) leaves it open with the toast. Closing the dialog while its body loads aborts the load, and the aborted load shows no toast. The shared dialog gains a region the loaded form lives in; the forms library renders a preview that leaves by the dialog without a box of its own; the lint accepts `data-action-load` as a trigger's route. No string is added or reworded. docs/design-system.md, docs/operator-ux-conventions.md and docs/stripe.md say the same. Tests: TestSwitchAnswersNameTheirRegion, TestPlanMoveTitle, TestSwitchControlOpensTheSharedDialog, TestConfirmModalCarriesTheLoadedRegion, TestPreviewInTheDialogRendersNoBox, and the walkthrough TestSwitchDialogWalkthrough (seven steps in a browser, among them a load answered offline, a load closed mid-flight, and a commit answered 503 by the browser so nothing reaches Stripe). The walkthrough resets the application database, so it runs only with SWITCH_DIALOG_WALK=1, as the merchant-disclosure sheet runs only when asked; make test skips it. 35 goldens change: every member plans golden loses one blank line, the switch controls of 12 lose their hx-* attributes for the dialog's, and the plan move form's golden loses its box. Full suite on reset databases: 5249 passed, 16 skipped, none failed. Sandbox walk on this commit, all twelve steps passed: the dialog opens at once and loads Stripe's figure ($20.00 equal to the preview's amount_due); Keep current plan sends nothing; Cancel plan shows its own form; offline the dialog closes with the toast; stale figures, a declined card and a payment awaiting approval stay in the dialog with focus on the new form; the made switch closes it and Max is current; a plan withdrawn in another session refuses in the Plans region; Pay for this plan reads "Pay for Max?". make screens: no page this commit touches changed. Three operator captures differ for other reasons: person detail and invoice detail show new ids, because the demo snapshot was rebuilt when the previous landing added a FedWiki migration, and the domains list's age column counts one more day (#120). Not walked by the walkthrough, with the reason: a request in flight at a chosen moment (the dialog closed while the commit runs, another switch opened) and a page restored from the browser's cache; a load answered 400 and a commit the switch refuses answer through the same close rule as the withdrawn plan and the made switch that the walkthrough and the hand walk cover. Breaks, all CAUGHT (19 test records): - own card titled as a switch (title test) - own card titled as a switch (control test) - setMove leaves the title unset - control loads without the price (control test) - control loads without the price (render test) - control loads the switch's route - heading left in the message - current tier's name unescaped - refused preview left in the dialog - confirmation answered into the Plans region (a declined switch) - confirmation answered into the Plans region (a stale switch) - confirmation answered into the Plans region (a switch awaiting approval) - decline answered 200 - way out a link again - commit's description dropped - region renamed in the partial - box rule removed - box rule widened to every kind - box rule widened to every way out Breaks of the walkthrough, all CAUGHT (8 records, each building and booting the app from the edited tree): - the close not deferred - the abort on close dropped - the cancelled mark ignored by the toast - the load not sent - the loaded region left showing under a static trigger - the close on an answer outside the dialog removed - the failed commit closing the dialog - the way out a link again Seen by no test: renderConfirmation without its describeCommit call (the database tests' handlers carry no Stripe key fingerprint, so the line is never shown there; describeCommit itself is pinned); data-action-load dropped from the lint's condition (no template interpolates a route into it); the spinner and aria-busy not set, and the focus after a stay-open answer not moved, which only the hand walk reaches, because the walkthrough stops before any Stripe write. |
||
|
|
fc68a6b78e |
Stop offering "None" for the entitlement set of a product that is a tier
Fourth section of #153's change. The product edit reads the ladders the product is a tier of. For a tier product the Entitlement set select no longer offers "None"; it opens with the disabled placeholder "Choose an entitlement set", and the field's one line reads "This product is a tier of <ladders>, and a tier needs an entitlement set." in place of the off-ladder sentence, which moves from the field's declared hint to the same line slot (the create page keeps it). A submitted "None" is refused with that sentence; so is a clear refused by section 2's trigger when the product became a tier after the page was read, after the ladders are read again. A failed read of the ladders is logged and saves nothing, instead of reading as "on no ladder". The edit's write now runs in its own READ COMMITTED transaction (db.InTx with db.ReadCommitted, item 4.6), so a tier add that commits first is seen by the clear whatever the server's default isolation; at REPEATABLE READ the edit's snapshot predated the tier and the clear committed. UpdateProduct is split into helpers, each answer and log line kept. Tests: TestTheEntitlementSetFieldFollowsTheProductsTiers, TestTheCreatePageCarriesTheOffLadderLine, TestTierNeedsSetSentence, TestProductEditOfATierProductOffersNoNone, TestProductEditRefusesNoneForAProductThatBecameATier, TestProductEditWithUnreadLaddersSavesNothing, TestProductEditClearingASetIsRefusedWhenATierAddCommitsFirstAtAnyDefaultIsolation (on a connection whose default is REPEATABLE READ). The forms golden operator.product.html moves the hint to the line. TestUpdateProductEntitlementSetSentinel builds its product on committed rows, since the edit's own transaction cannot see an uncommitted fixture; its assertions are unchanged. productTierOf takes no context: it logs through the handler's logger and unparam refuses the unused parameter. Breaks: 20 of 20 caught. The parse's error left in place; None offered whatever the tiers (T6, T6c, T6d); the placeholder left out (T6d, T6); the ladders out of name order; the line kept beside the error; the off-ladder line blanked beside an error; the create page's line lost; the failed read treated as no tier; the save going ahead when the read failed; the parse's field error left when the ladders are unread; L3 not logged; the second-read refusal not logged; the database's refusal unmapped; the refusal named over a failed second read; the join reduced to the first name; the write at the server's default isolation; the empty-value condition dropped. Review: round 1, no blocker or major; four minor and three nits, applied. #153's sequence (item 5.4), app rebuilt from this tree, from the demo snapshot, the whole walkthrough package three times without a reset: ok each run. The plan-ladders walkthrough's ladder now has tiers with sets, the org-types walkthrough committed a default change to it on runs 2 and 3 and put the default back, and the grant revoke passed on all three runs; the app log has no "has no entitlement set". Item 5.4 is reworded to what ran: the three-walkthrough filter it named finds no revoke subject on the demo snapshot, which another walkthrough's grant supplies. Full suite on reset databases: 4731 pass, 14 skip. make screens: no change beyond section 1's; the captured product is not a tier. |
||
|
|
b0df28647f |
Move organizations to a new default as obligations an act owns
A default change saved the new default outside any lock, then moved every organization of the type inside the request, from a list read after the save. At size the request ran out of time. A failure part way left the rest unmoved, with "Details are in the server logs." as the only answer and committing again as the only re-run (#187). Migration 00028 gives the default change work it can own and resume: - core.plan_act_reached_org_types records which types an act reaches. core.plan_act_owns_org_type states once that the newest act to reach a type owns it, so a newer act takes over what an older one has not moved. - core.plan_act_obligations holds one row per organization the act must move. core.plan_act_orgs_without_pool records an organization with no default pool, which is logged and counted, not owed. - core.plan_act_listing_frontier states once which organizations the listing has not reached: the writer, the listing-done read, the poller's read and the card's count all call it, and PostgreSQL inlines it into each. - Four enclosed writers list, settle, record a failure (failed after five attempts) and requeue. core.record_plan_act now writes the reached types and refuses a reorder detail that disagrees with the ladder. core.grants.act_id names the act a grant came from. The commit's first transaction runs on the runner under the exclusive rendezvous, inside a 5 s deadline. It locks the type and refuses a reserved type, a candidate that is already the default ("Already the default.") and a ladder with no rank 0. It then saves the default and records the act. With no disposition given, it rolls back when an organization holds the outgoing default, and the 422 preview is rebuilt after the rollback through the capped read. A listing then writes the obligations in batches of 500, in org type and organization id order. Each move runs in its own transaction under the shared rendezvous. It locks the pool, locks the obligation together with its type's owner, and reads the current default and its rank 0. It then migrates or grandfathers the organization, settles it as already at the default, or, under an act with no disposition, leaves it untouched with a Warn line. A failure is recorded on the obligation alone with its step, in a plain transaction that takes no pool row. The request tries up to 50 obligations within 2 s. Its lock and transaction timeouts are 500 ms and 2 s, and a move they cut records no attempt. It then starts PlanActDrainWorkflow for the rest; PollPlanActs starts a drain for any act still owing work. The toast counts what is not yet reached and what has no default pool, and leaves out any count it could not read. The org type's card shows each open act's lines: organizations not yet reached, and those not moved, each with its reason, with Retry. They are read in two statements in one read-only repeatable-read transaction, and "Pending plan changes could not be read. Reference: {request id}." replaces them when that read fails. Retry is an act of its own: it requeues the failed obligations on the types the act still owns, and a retried organization is recorded under the operator who pressed Retry. It answers a lock wait with the collision sentence and any other failure with "Failed to retry. Reference: {request id}.". Along the way: - The default change's sentences ending "Details are in the server logs.", in six places, now end "Reference: {request id}.", logged through the request's logger. - The select's label becomes "New default", so the form library's refusal reads "Choose a new default from the list." and its required message "Choose a new default.". The change's specs now carry that label for org-types and form-library. The design states the frontier function and that the request's cap counts obligations tried, not settled. docs/database-locks.md, docs/plan-management.md and the two docs quoting the label follow the code. Goldens: - Behaviour changes in contention, default-already-at, default-migrate, default-none-migrate, default-needs-disposition, default-begin-fails, default-initiate-fails, default-migrate-restore-fails, default-grandfather, default-change-twice, default-current-refused, default-save-fails, the four removal-against-default files and the four tier-change-during-conferral files. - The three default form goldens move with the label. routes.golden gains the Retry route's probes. - default-list-fails is retired: no listing runs outside the act any more. New: default-listing-fails, default-superseded, default-request-cap, default-untouched-outgoing, default-retry, default-retry-nothing, default-preview-capped and default-gate-true-preview-empty. - The other 40 plan-act goldens gain only the reached-type, without-pool and obligation sections and the grants' act column. The full suite passes. |
||
|
|
ab1c0dbb69 |
Write the list search's hidden sibling inputs in sorted order
An embedded list's search form re-submits its sibling lists' state as hidden inputs. listSearchForm added them by ranging over a map, and a GET form submits its inputs in document order, so with two or more siblings the parameters of the URL the search pushes came out in a different order from one render to the next. They are now added in the sorted order of their keys, so the order is the same on every render. With one sibling, or none, the HTML is unchanged. TestListSearchFormCarriesSiblingInputsInSortedOrder renders the form 20 times with three siblings and checks each render for key order. It failed in 3 of 3 runs at the parent and passed in 3 of 3 at this commit. The new file internal/server/testdata/forms/per-request/list-search-three-siblings.html pins the three-sibling HTML as the other per-request files do, and the comment in forms_golden_test.go that explained why the list search was rendered with one sibling is removed. No existing golden file changes. No assertion of TestFormsHTML changed; it gains one case. How it was shown to change no other output: the names and results of ./internal/server (the whole package, on the test databases) are equal at the parent and at this commit apart from the two added lines, the new test and the new TestFormsHTML/per-request/list-search-three-siblings case, none failing. One break (the sorted keys walked backward) is caught by the new test. The demo-state walkthroughs give 16 pass and 6 skip at the parent and at this commit. Not probed: that a browser submits the inputs in document order. The HTML specification says it does, and no test here drives a browser's form submission. |
||
|
|
bc4a933a3d |
Pin the HTML of every registered form
TestFormsHTML renders each declaration in every binding the library renders it in and compares the text, through golden.Compare, with a file under internal/server/testdata/forms. A form that is not a rows form is rendered unbound, on the edit side and on the create side, unbound with values, and as a record (every field bound to a value its rules accept, three cells per field, an instance and a message); a search is then rendered boosted and with a target, a select and an out-of-band select, and any other form is rendered refused (an empty POST parsed through the declaration) with the refusals of a 600-character value in every field. A rows form is rendered unbound, then with an open row, a staged delta and the tray (or the row's own commit), each row field and one row action. Runtime options are a fixed "Choose one", Alpha and Beta. The subtests are grouped in three. registered/<name> holds the 19 forms forms.All returns in the internal/server test binary; the files number 26 in all. per-request/<case> holds the declarations built per request: the settings form, the org type card's two copies, the rules form, and the list search embedded (one sibling parameter) and standalone. library-defaults holds a table's "Field" and "Value" headings, a textarea's three rows and the htmx verb of GET, PUT, PATCH, DELETE and POST on a form that is not a search. TestFormsGoldenFilesAreAllRendered fails for any file under testdata/forms that TestFormsHTML does not write, so a form that leaves the registry leaves no stale file. TestPerRequestFormsSatisfyTheInvariants runs CheckInvariants over the org type card's two copies and the rules form's copy; the settings form has its own test, and the list search builds its declaration inside listSearchForm and never returns it, so no test can pass it to CheckInvariants and it stays unchecked. Shown to change no output: only a test file and its goldens are added. The names and results of ./internal/forms ./internal/server ./internal/integrations/... (2113 at the parent, 2144 here, none failing, one skip on each side), taken with the test database reset and the app down at the parent and at this commit, are equal apart from the new tests and their 28 subtests. The registered forms' files in name order followed by the defaults' file are byte-equal to the output of a standalone program that renders the same bindings, run at 0abbfac7; no file under internal/forms, internal/server or internal/embeds that is not a test or a golden has changed since. A separate render of the per-request declarations, unbound only, equals the unbound section of the org type card's files and of the rules form's file apart from the runtime options it did not bind, and equals the list search's two files exactly. The other bindings and the settings file were read against their declarations, not compared with another render. Three runs in a row produce the same files, with no -update. 22 breaks in the form library's render.go and the three server files of the per-request cases, each edited once and each caught by the new tests. The old side has no counterpart: every break is a new-side record, since no old test names these. None dropped. Pinned defects, so a later fix flips them by name: the list search's hidden sibling inputs come out in map order with two or more siblings (rendered here with one, so the file does not vary from run to run); the library defaults no registered form reaches, which every earlier test missed. Not probed: the templates' own markup, because the files hold it for a later template change to show while the breaks probe the view the library builds; fieldAttrs, buildRows, RowField and resolveRowsFocus, whose output is in the files; the list search's per-request declaration against the invariants, for the reason above; the stale-file check, which guards the test's own files. A dense checkbox's label position is the one render break this net misses, since no registered form has a dense checkbox; TestFormPartCheckboxLabelSitsAboveTheBox catches it. |