diff --git a/.planning/phases/18.1-payment-gateway-subscriptions/18.1-VERIFICATION.md b/.planning/phases/18.1-payment-gateway-subscriptions/18.1-VERIFICATION.md index 3339f588..b770f2c7 100644 --- a/.planning/phases/18.1-payment-gateway-subscriptions/18.1-VERIFICATION.md +++ b/.planning/phases/18.1-payment-gateway-subscriptions/18.1-VERIFICATION.md @@ -1,6 +1,6 @@ --- phase: 18.1-payment-gateway-subscriptions -verified: 2026-10-16T15:45:06Z +verified: 2026-10-16T19:06:25Z status: passed score: 5/5 roadmap success criteria verified (plan must-haves 65/65 verified, 4 of them as amended by user-accepted fixes) covered_files: @@ -42,38 +42,48 @@ covered_files: - modules/pact/capabilities.go - scripts/check-phase10.sh - scripts/check-phase18.1.sh -covered_digest: "v2:sha256:4d5014a3f2a0a899ec2e3ad3896b5355c8fd446b212d578575869422cd4560e8" +covered_digest: "v2:sha256:1d8fe46b4306d3d72b8c2adf5183b40e3489a6e6cbf179968ffdfda557a3d1dc" behavior_unverified: 0 overrides_applied: 0 re_verification: - previous_status: human_needed - previous_score: "5/5 roadmap SCs; plan must-haves 64/65 verified, 1 present-behavior-unverified (WR-07 component wiring)" - gaps_closed: - - "Previous human item 1 (disposition of re-review WR-01..WR-07): UAT row 5 passed; all seven fixed in quick 261016-myi (pgsub 2c9e5ef, pgstripe 2e44508), each with a named test that ran green here" - - "Previous human item 2 (live Stripe checkout): UAT row 6 passed (source=stripe logged, activated once, second subscribe 409)" - - "Previous human item 3 (portal cancel_at question): UAT row 7 first failed (G-18.1-7), fixed in pgstripe a5b3314, retest passed" - - "Previous human item 4 (browser pass, incl. WR-07 Back button): UAT row 4 passed after the G-18.1-4 fix (pgsub 6a3f2c4 d5579cd, framework bc5685e 013890c 17398fe)" - - "WR-07 component wiring (previously PRESENT_BEHAVIOR_UNVERIFIED): Back from Checkout observed in a real browser in UAT row 4; layer code unchanged since (v0.2.1, 22368c5)" - - "CR-01 sequence 3 coincidental reliance (Service.Cancel without the subscription lock): Cancel now runs under SubscriptionLockKey; TestCancelTakesSubscriptionLock passed here" - - "G-18.1-8 (pgsub could not boot in a summer-built app): bootService goes through lagoon.OnDatabase; TestBootBeforeDatabase passed here; UAT row 8 passed" + previous_status: passed + previous_score: "5/5 roadmap SCs; plan must-haves 65/65 verified (4 as amended, 1 by human UAT)" + stale_reason: "covered_changed: modules/pact/capabilities.go, commit 06e2e60 (quick 261016-s8x, additive pact.NavigationItem.Visible)" + gaps_closed: [] gaps_remaining: [] regressions: [] + earlier_round: # the 2026-10-16T15:45Z run, human_needed -> passed after the release go-ahead + previous_status: human_needed + gaps_closed: + - "Previous human item 1 (disposition of re-review WR-01..WR-07): UAT row 5 passed; all seven fixed in quick 261016-myi (pgsub 2c9e5ef, pgstripe 2e44508), each with a named test that ran green here" + - "Previous human item 2 (live Stripe checkout): UAT row 6 passed (source=stripe logged, activated once, second subscribe 409)" + - "Previous human item 3 (portal cancel_at question): UAT row 7 first failed (G-18.1-7), fixed in pgstripe a5b3314, retest passed" + - "Previous human item 4 (browser pass, incl. WR-07 Back button): UAT row 4 passed after the G-18.1-4 fix (pgsub 6a3f2c4 d5579cd, framework bc5685e 013890c 17398fe)" + - "WR-07 component wiring (previously PRESENT_BEHAVIOR_UNVERIFIED): Back from Checkout observed in a real browser in UAT row 4; layer code unchanged since (v0.2.1, 22368c5)" + - "CR-01 sequence 3 coincidental reliance (Service.Cancel without the subscription lock): Cancel now runs under SubscriptionLockKey; TestCancelTakesSubscriptionLock passed here" + - "G-18.1-8 (pgsub could not boot in a summer-built app): bootService goes through lagoon.OnDatabase; TestBootBeforeDatabase passed here; UAT row 8 passed" human_verified: - test: "Disposition of the re-review warnings (18.1-REVIEW.md, 2026-10-16; 18.1-REVIEW-DISPOSITION.md lists all open) before the Horoskopia cutover" uat_row: 5 - recorded_at: "2026-10-16T15:34:36.000Z" + recorded_at: "2026-10-16T18:17:28.000Z" - test: "Live Stripe test-mode checkout end to end: test-mode keys and the subscription webhook secret in pgstripe settings, `stripe listen --forward-to /webhook/stripe/subscriptions`, checkout from the layer playground (PgPlans -> PgSubscribeButton) with the user subscriber" uat_row: 6 - recorded_at: "2026-10-16T15:34:36.000Z" + recorded_at: "2026-10-16T18:17:28.000Z" - test: "Billing-portal cancellation (D-21 cancel_at question): open the portal through PgManageSubscriptionButton, cancel the subscription, inspect the delivered customer.subscription.updated and the retrieved subscription" uat_row: 7 - recorded_at: "2026-10-16T15:34:36.000Z" + recorded_at: "2026-10-16T18:17:28.000Z" - test: "Browser pass through the admin screens and the layer components" uat_row: 4 - recorded_at: "2026-10-16T15:34:36.000Z" + recorded_at: "2026-10-16T18:17:28.000Z" - test: "pgsub boots in an application built with `summer build`" uat_row: 8 - recorded_at: "2026-10-16T15:34:36.000Z" + recorded_at: "2026-10-16T18:17:28.000Z" + - test: "Live Stripe test-mode checkout end to end: test-mode keys and the subscription webhook secret in pgstripe settings, `stripe listen --forward-to /webhook/stripe/subscriptions`, run checkout from the layer playground (PgPlans -> PgSubscribeButton) with the user subscriber" + uat_row: 2 + recorded_at: "2026-10-16T18:17:28.000Z" + - test: "Billing-portal cancellation (D-21 cancel_at question): open the portal through PgManageSubscriptionButton, cancel the subscription, inspect the delivered customer.subscription.updated" + uat_row: 3 + recorded_at: "2026-10-16T18:17:28.000Z" human_verification: - test: "Release go-ahead for the post-gate fixes: framework tag, plugin require bumps and tags, pushes" expected: "The user approves (or defers with a recorded reason) the release order from the quick-task summaries: push summercms.go master and tag v0.2.2 (carries pact.ListExtendRecords, pact.FormRecordTitle and meta.title); bump sm-paymentgatewaysubscriptions-plugin's require to v0.2.2, push main, tag (v0.1.1); bump sm-pgstripe-plugin's requires to v0.2.2 and the new pgsub tag, push main, tag (v0.1.1). After that a consumer pinned to the tags gets the G-18.1-8 boot fix, WR-01..WR-07 and G-18.1-7" @@ -86,9 +96,9 @@ human_verification: **Phase goal:** The generic core of `Golem15.PaymentGatewaySubscriptions` and pgstripe's subscription gateway and webhook run on SummerCMS. Horoskopia's subscription flow (plans, checkout, Stripe webhook, `golem15.pgsub.*` events, cancel and portal) works as it does on PHP. Exceptions are the user's recorded amendments: event ordering (D-21 amendment, 2026-10-14), the portal `cancel_at` mapping (D-21 amendment, 2026-10-16), and the re-review fixes the user accepted in UAT row 5 (`canceled` fires once on a deletion, `incomplete` no longer blocks checkout). **Mode:** mvp. The ROADMAP goal is not in user-story form, so this report checks the goal text and its five success criteria, as the earlier verifications did. -**Verified:** 2026-10-16T15:45:06Z +**Verified:** 2026-10-16T19:06:25Z (earlier full re-verification 2026-10-16T15:45:06Z) **Status:** passed (the one human item, the release go-ahead, was approved and executed on 2026-10-16; see Release record) -**Re-verification:** Yes. The previous report (2026-10-16T14:45Z) had no `gaps:`. It went stale (covered_changed) after two quick tasks: 261016-myi (WR-01 to WR-07 and G-18.1-8) and 261016-nqk (G-18.1-7 and G-18.1-4). This run verifies both in full and regression-checks plans 01 to 09 through the full gate. +**Re-verification:** Yes, twice. This run (19:06Z) re-checks the report after it went stale (covered_changed) when quick 261016-s8x (commit 06e2e60) changed `modules/pact/capabilities.go`; see "Re-check after quick 261016-s8x" below. No truth regressed. The 15:45Z run: the report before it (2026-10-16T14:45Z) had no `gaps:`. It went stale (covered_changed) after two quick tasks: 261016-myi (WR-01 to WR-07 and G-18.1-8) and 261016-nqk (G-18.1-7 and G-18.1-4). This run verifies both in full and regression-checks plans 01 to 09 through the full gate. ## Goal Achievement @@ -98,12 +108,29 @@ human_verification: |---|-------|--------|----------| | 1 | SC-1: Subscription and SubscriptionPlan on a squashed, idempotent v2.4.0 baseline; subscribers registered at build time; built-in `user` subscribable | VERIFIED | TestBaselineSchema and TestUserSubscribable passed in this run's postgres stage. G-18.1-8: `boot.go` `bootService` now builds and publishes `*classes.Service` through `lagoon.OnDatabase`, so plugins can activate before a command opens the database (generated main.go order). TestBootBeforeDatabase passed here. UAT row 8 passed on a demo app built with `summer build`. The user resolver gained an optional `ResolveMany` (`contracts.SubscriberBatchResolver`). It still writes only `stripe_customer_id`. | | 2 | SC-2: Stripe implementation of the gateway incl. `syncPlanToGateway`; both webhook routes verify, enqueue, process idempotently, fire the six events | VERIFIED | `subscriptions/job.go` `applySubscription` is unchanged: marker check, then foreign-app claim, then `WithSessionLock(SubscriptionLockKey)`, then retrieve outside a transaction, then claim and Apply in one transaction. G-18.1-7: every mapping path goes through `subscriptionObject.args()`: the snapshot (`decode.go:222-226`), the fresh retrieve (`gateway.go:468-475`) and `Gateway.SyncSubscription` (`gateway.go:415-422`). `pendingCancel()` (`decode.go:162-167`) maps Stripe's flag, or `cancel_at` on an active or trialing subscription, to `cancel_at_period_end`. WR-02: `ApplyDeleted` fires `canceled` only when the row was not already canceled (`apply.go:241-243`). Passed here: TestFreshStateSmoke incl. `portal_cancel_at_only_then_uncancel` (reactivated exactly once) and `deleted_before_created` (canceled == 1); TestEventOrdering incl. `deleted_before_created` (canceled == 1, `ordering_test.go:152`); TestDecodeSubscription/a_cancel_at_date…; TestHostSubscriptionFlow; TestSubscriptionWebhookSmoke; TestSubscriptionJob. UAT rows 6 and 7 passed against real Stripe test mode. | -| 3 | SC-3: admin screens, Go service API, headless customer API, layer components | VERIFIED | Admin: `models/subscription/columns.yaml` gains a `subscriber` column, filled per page by `subscriptionsAdminController.ListExtendRecords`. Type and id stay as invisible searchable columns. The preview title comes from `FormRecordTitle`, and the label element reads its `value` property (`assets/js/fields.js`). Framework: `pact.ListExtendRecords` and `pact.FormRecordTitle` (`capabilities.go:737-763`); the SPA uses `meta.title`; dist rebuilt and matching. Service API: `Cancel` takes `SubscriptionLockKey` (`service.go:471-499`). `CheckoutGuarded` refuses on `CheckoutBlocker`, cancels incomplete subscriptions and expires open sessions inside `SubscriberLockKey` (`service.go:351-437`). `SyncPlan` with a nil tx runs under `PlanLockKey` on a re-read plan (`service.go:693-712`). Customer API: 409 on `CheckoutBlocker`; 422 on `CheckoutPriceID == ""` and on `contracts.ErrPriceUnavailable` (`api/subscription.go:93-171`). Layer: unchanged at v0.2.1; 112/112 TAP. UAT row 4 passed in a browser, covering admin and layer, including Back from Checkout and the subscriber e-mail and title. | +| 3 | SC-3: admin screens, Go service API, headless customer API, layer components | VERIFIED | Admin: `models/subscription/columns.yaml` gains a `subscriber` column, filled per page by `subscriptionsAdminController.ListExtendRecords`. Type and id stay as invisible searchable columns. The preview title comes from `FormRecordTitle`, and the label element reads its `value` property (`assets/js/fields.js`). Framework: `pact.ListExtendRecords` and `pact.FormRecordTitle` (`capabilities.go:748-774`, moved 11 lines by 06e2e60, text unchanged); the SPA uses `meta.title`; dist rebuilt and matching. Service API: `Cancel` takes `SubscriptionLockKey` (`service.go:471-499`). `CheckoutGuarded` refuses on `CheckoutBlocker`, cancels incomplete subscriptions and expires open sessions inside `SubscriberLockKey` (`service.go:351-437`). `SyncPlan` with a nil tx runs under `PlanLockKey` on a re-read plan (`service.go:693-712`). Customer API: 409 on `CheckoutBlocker`; 422 on `CheckoutPriceID == ""` and on `contracts.ErrPriceUnavailable` (`api/subscription.go:93-171`). Layer: unchanged at v0.2.1; 112/112 TAP. UAT row 4 passed in a browser, covering admin and layer, including Back from Checkout and the subscriber e-mail and title. | | 4 | SC-4: host test proves checkout, activation, invoice.paid, past_due grace, cancel, duplicate idempotency against a Stripe fake | VERIFIED | TestHostSubscriptionFlow passed in this run (postgres stage, pgstripe `./subscriptions`, 12 required tests). | | 5 | SC-5: unit tests delivered in the phase's last plan | VERIFIED | Plan 09 is the last plan, and the gate requires its names by stage and package. The quick tasks are outside the phase's plan sequence. Each carries named tests in its fix commits: pgsub TestCheckoutIncompleteAndOpenSessions, TestCancelTakesSubscriptionLock, TestSyncPlanSerialised, TestCheckoutPriceUnavailable, TestBootBeforeDatabase, TestLabelElementValueProperty; pgstripe TestReviewFixesGateway (4 subtests) and the cancel_at subtests; cabana TestListUnstoredColumn, TestListExtendRecords, TestFormRecordTitle. All passed when run by name here, with 0 skips. The gate does not pin these names (see Anti-Patterns, INFO). | **Score:** 5/5 roadmap truths verified (0 present, behavior-unverified). +### Re-check after quick 261016-s8x (2026-10-16T19:06Z) + +The only covered file changed since the 15:45Z report is `modules/pact/capabilities.go`, by commit 06e2e60 (20:34+02:00, quick 261016-s8x). No other covered file has a commit after 0e5c56b, and none has uncommitted changes. + +| Check | Result | +|-------|--------| +| Diff scope | One hunk (+12 -1) in `NavigationItem`: a longer doc comment and a new field `Visible func(ctx context.Context) bool` tagged `json:"-"`. No other declaration in the file changed | +| 18.1 declarations in the file | `BulkAction.Detached` (lines 284-306), `ListExtendRecords` (748-757) and `FormRecordTitle` (766-774) are byte-identical to 06e2e60^; only their line numbers moved (+11) | +| Nil default is the old behaviour | `cabana.Registry.Metadata` calls `Visible` only when it is non-nil; with no side item hidden, the `hidden` map is empty and `openableTarget`'s condition reduces to the previous `item.Controller == "" \|\| r.canOpen(...)`. pgsub's `admin_navigation.go` does not set `Visible` (grep), pgstripe declares no navigation, so their menus are unchanged | +| Struct comparability | A func field makes `NavigationItem` non-comparable; any `==` on it would fail to compile. `go vet ./...` passed in the framework, pgsub (8b3a5c0) and pgstripe (b870e8d) | +| Named tests | `TestListUnstoredColumn`, `TestListExtendRecords`, `TestFormRecordTitle`, `TestDetachedBulkAction` (5 subtests) and the new `TestNavigationHidesInvisibleItemsAndRepointsTarget` passed against Docker Postgres, 0 skips | +| Full gate | `PHASE181_REQUIRE_POSTGRES=1 bash scripts/check-phase18.1.sh --all` passed on 450c535, which contains 06e2e60, before v0.2.2 was tagged (Release record) | +| Debt markers | none in `capabilities.go` | +| UAT (Step 8b) | `verification.uat-evidence`: `recorded_at` 2026-10-16T18:17:28Z (commit 90b2d4e settled rows 2 and 3 as passes, duplicates of 6 and 7), `changed_since_uat: [modules/pact/capabilities.go]`. The change does not bear on any row: rows 1, 5 are decisions, 2/3/6/7 are Stripe flows, 8 is boot, and row 4's admin screens are pgsub's, whose navigation entries leave `Visible` nil. No retest | + +All five roadmap truths and the 65 plan must-haves keep their status. + ### Plan must-haves: changes since the previous report The previous report verified plans 01 to 09 (65 truths). This run re-checked all of them through the full gate (below). The rows here are the truths whose status or wording changed. @@ -183,6 +210,9 @@ No orphaned requirements. | Framework | `go vet ./...`; `go test -short ./...` | both exit 0, no failures | PASS | | Plugins vet | `go vet ./...` in pgsub and pgstripe | clean | PASS | | D-06 | `go list -deps ./...` in pgsub, count stripe-go | 0 | PASS | +| Re-check: framework and plugins vet | `go vet ./...` in summercms.go, pgsub, pgstripe (19:06Z run) | all clean | PASS | +| Re-check: pact and cabana short tests | `go test -short -count=1 ./modules/pact ./modules/cabana` | ok, ok | PASS | +| Re-check: 18.1 hooks and the new navigation test (Docker) | `go test -count=1 -v -run '^(TestListUnstoredColumn\|TestListExtendRecords\|TestFormRecordTitle\|TestNavigationHidesInvisibleItemsAndRepointsTarget\|TestDetachedBulkAction)$' ./modules/cabana` | 5/5 PASS, 0 SKIP | PASS | | Plugin build outside the workspace | `GOWORK=off GOPROXY=off go build ./...` in pgsub | builds only because pgsub's own go.mod has `replace .../summercms => ../summercms.go`; `go list -m` resolves summercms "v0.2.1" to the local checkout | INFO (see Anti-Patterns) | ### Test Quality Audit (new tests) @@ -204,8 +234,8 @@ No disabled tests, no circular expected values. The Stripe fake's responses are | File | Line | Pattern | Severity | Impact | |------|------|---------|----------|--------| -| sm-paymentgatewaysubscriptions-plugin/go.mod | 7, 149 | requires `summercms v0.2.1`, but HEAD calls `pact.ListExtendRecords` and `pact.FormRecordTitle`, which are in no framework tag; it builds only through its own `replace ../summercms.go`, which a consuming module ignores | WARNING | pgsub HEAD cannot be consumed by tag or pseudo-version until the framework is tagged and the require bumped. This is the human release item | -| sm-paymentgatewaysubscriptions-plugin, tag v0.1.0 | n/a | released plugin lacks G-18.1-8 | WARNING | an app built with `summer build` on pgsub v0.1.0 fails every command ("database is not published"). Fixed on main (ab46d6f), unreleased | +| sm-paymentgatewaysubscriptions-plugin/go.mod | 7, 149 | requires `summercms v0.2.1`, but HEAD calls `pact.ListExtendRecords` and `pact.FormRecordTitle`, which are in no framework tag; it builds only through its own `replace ../summercms.go`, which a consuming module ignores | WARNING (resolved by the release) | pgsub HEAD cannot be consumed by tag or pseudo-version until the framework is tagged and the require bumped. Resolved: pgsub 8b3a5c0 (v0.1.1) and pgstripe b870e8d (v0.1.1) require summercms v0.2.2 | +| sm-paymentgatewaysubscriptions-plugin, tag v0.1.0 | n/a | released plugin lacks G-18.1-8 | WARNING | an app built with `summer build` on pgsub v0.1.0 fails every command ("database is not published"). Fixed on main (ab46d6f) and released in pgsub v0.1.1 | | scripts/check-phase18.1.sh | required names | the quick-task tests (above) are not in the gate's required lists; 18.1-VALIDATION.md has no rows for them | INFO | the gate would not notice if they were skipped or removed. They passed here by name | | .planning/phases/18.1-…/18.1-CONTEXT.md | § Amendments | WR-02 (canceled once) and WR-07 (incomplete no longer blocks) deviations not recorded; `apply.go:205` claims they are "recorded next to the D-21 amendment" | INFO | decision trail sits in UAT row 5 and the README only | | docs/backend/forms.md | FormRecordTitle paragraph | the example title "Subscription #12 (Pro)" | INFO | generic concept, no application named; the framework rule holds | @@ -235,9 +265,11 @@ All trackable CONTEXT.md decisions are honored by shipped artifacts (24/24). UAT evidence (Step 8b): `18.1-UAT.md` is `complete`, recorded at 2026-10-16T15:34:36Z. `verification.uat-evidence` reports `changed_since_uat: []` for the covered framework files. The sibling repos' last commits are also older than the record: pgsub 17:22+02, pgstripe 17:07+02, layer 13:35+02. Their working trees are clean. Rows 4, 5, 6, 7 and 8 passed on that code and settle every human item of the previous report (see `human_verified`). Rows 2 and 3 are skipped duplicates of 6 and 7. No row needs a retest. -One new item remains: +The 19:06Z re-check: `recorded_at` is now 2026-10-16T18:17:28Z (rows 2 and 3 settled as passes), and `changed_since_uat` lists `modules/pact/capabilities.go`. That change does not bear on any row (see "Re-check after quick 261016-s8x"), so every pass stands and no row needs a retest. -1. **Release go-ahead.** Tag the framework v0.2.2, bump and tag pgsub, then pgstripe, and push all three. Until then the tagged pgsub v0.1.0 does not boot in a `summer build` app, and pgsub main cannot be required by a consumer. This needs the user's go-ahead under the project rule. If the user defers it, record the reason (for example "released together with the Horoskopia port"). +The one item the 15:45Z run raised is resolved (`result: passed`, see Release record): + +1. **Release go-ahead (resolved 2026-10-16).** Tag the framework v0.2.2, bump and tag pgsub, then pgstripe, and push all three. Until then the tagged pgsub v0.1.0 does not boot in a `summer build` app, and pgsub main cannot be required by a consumer. This needs the user's go-ahead under the project rule. If the user defers it, record the reason (for example "released together with the Horoskopia port"). ### Gaps Summary @@ -255,11 +287,11 @@ No success criterion and no plan must-have failed. The two quick tasks did what Each fix has a named test that passed in this run, and the live UAT passed on the same code. -The status is `human_needed` only because the fixes are not released. The tagged plugins still carry the boot blocker, and pgsub main depends on framework hooks that no tag has. Releasing needs the user's go-ahead. +At 15:45Z the status was `human_needed` only because the fixes were not released. The user approved the release, and framework v0.2.2, pgsub v0.1.1 and pgstripe v0.1.1 carry them (Release record). The later quick 261016-s8x change to `pact.NavigationItem` is additive and regresses no 18.1 truth. Status: passed. --- -_Verified: 2026-10-16T15:45:06Z_ +_Verified: 2026-10-16T19:06:25Z (re-check after 06e2e60; full re-verification 2026-10-16T15:45:06Z)_ _Verifier: Claude (msd-verifier)_ ## Release record (2026-10-16)