diff --git a/.planning/phases/18.1-payment-gateway-subscriptions/18.1-04-SUMMARY.md b/.planning/phases/18.1-payment-gateway-subscriptions/18.1-04-SUMMARY.md new file mode 100644 index 0000000..3631f2a --- /dev/null +++ b/.planning/phases/18.1-payment-gateway-subscriptions/18.1-04-SUMMARY.md @@ -0,0 +1,388 @@ +--- +phase: 18.1-payment-gateway-subscriptions +plan: 04 +subsystem: payments +tags: [subscriptions, pgsub, cabana, admin, taglist, reorder, bulk-actions, customer-api, jwt, postgres, testcontainers] + +# Dependency graph +requires: + - phase: 18.1-01 + provides: cabana taglist field (mode array) and the flat reorder, compiled through replace ../summercms.go (tag v0.1.26 held) + - phase: 18.1-02 + provides: pgsub core at aad4647 (classes.Service, contracts registries, models, statuses.*/gateways.*/errors.* copy, the acme.members fixture with the fake stripe gateway) + - phase: 18-payment-gateway + provides: sm-paymentgateway-plugin api.RequireJWT, api.MoneyJSON/NewMoneyJSON, the money package, the admin controller and customer API patterns +provides: + - "pgsub admin: Subscription Plans (taglist features, flat reorder, bulk sync_to_stripe and toolbar sync_all_to_stripe through the gateway registry) and Subscriptions (filtered list, read-only preview with the registry-resolved subscriber)" + - "the three PHP permission codes verbatim, navigation subscriptions (order 510), list/preview field types subscriptionstatus and subscriptiongateway, complete EN/PL phrasebook" + - "customer API under /_paymentgatewaysubscriptions/api/v1 (bucket paymentgatewaysubscriptions-api): GET /plans, GET /subscription, POST /subscription/checkout, /portal, /cancel with the confirmed api-research contract" + - "config keys golem15.paymentgatewaysubscriptions.api.success_url, .api.cancel_url, .api.portal_return_url resolved at boot" +affects: [18.1-05, 18.1-06, vue-paymentgateway-layer, consuming applications' subscription pages] + +# Actuals (#2632) - chars/4 over the realized diff in sm-paymentgatewaysubscriptions-plugin (aad4647..b728762) +actuals: + tokens: 37255 + tasks: 5 + commits: 4 +commits: 4 +plan_head_before: aad4647dcdf4c792f9fef9d50f34db76e4c18272 +plan_head_after: b72876253a33932109003fe211c228c5c89fcb44 + +tech-stack: + added: [] + patterns: + - "Bulk action that calls a provider per record never returns an error after the first provider call: failures become `code: message` lines in the result message, so the provider ids saved in the bulk transaction survive" + - "Toolbar action over many records: one short lagoon transaction per record whose function never fails" + - "Read-only label field type with ListValue for lists and Load for the preview; a preview-only plugin field needs context [update, preview] because the record read hydrates in the update context" + - "Customer API reuses the payment gateway's exported RequireJWT and MoneyJSON; redirect targets resolved once at boot and published as *api.RedirectURLs" + - "Lang parity test: scan YAML, partials and Go sources for phrase keys and check both locales exactly (translator with fallback = locale)" + +key-files: + created: + - ../sm-paymentgatewaysubscriptions-plugin/admin.go + - ../sm-paymentgatewaysubscriptions-plugin/admin_permissions.go + - ../sm-paymentgatewaysubscriptions-plugin/admin_navigation.go + - ../sm-paymentgatewaysubscriptions-plugin/field_types.go + - ../sm-paymentgatewaysubscriptions-plugin/routes.go + - ../sm-paymentgatewaysubscriptions-plugin/assets/js/fields.js + - ../sm-paymentgatewaysubscriptions-plugin/controllers/base.go + - ../sm-paymentgatewaysubscriptions-plugin/controllers/admin_registry.go + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptionplans.go + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptionplans/config_list.yaml + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptionplans/config_form.yaml + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptionplans/config_reorder.yaml + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptions.go + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptions/config_list.yaml + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptions/config_filter.yaml + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptions/config_form.yaml + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptions/_subscription_subscriber.htm + - ../sm-paymentgatewaysubscriptions-plugin/controllers/subscriptions/_subscription_plan.htm + - ../sm-paymentgatewaysubscriptions-plugin/models/filters.go + - ../sm-paymentgatewaysubscriptions-plugin/models/subscriptionplan/fields.yaml + - ../sm-paymentgatewaysubscriptions-plugin/models/subscriptionplan/columns.yaml + - ../sm-paymentgatewaysubscriptions-plugin/models/subscription/fields.yaml + - ../sm-paymentgatewaysubscriptions-plugin/models/subscription/columns.yaml + - ../sm-paymentgatewaysubscriptions-plugin/api/errors.go + - ../sm-paymentgatewaysubscriptions-plugin/api/json.go + - ../sm-paymentgatewaysubscriptions-plugin/api/plans.go + - ../sm-paymentgatewaysubscriptions-plugin/api/subscription.go + - ../sm-paymentgatewaysubscriptions-plugin/api/urls.go + - ../sm-paymentgatewaysubscriptions-plugin/admin_harness_test.go + - ../sm-paymentgatewaysubscriptions-plugin/admin_smoke_test.go + - ../sm-paymentgatewaysubscriptions-plugin/customer_api_smoke_test.go + - .planning/phases/18.1-payment-gateway-subscriptions/deferred-items.md + modified: + - ../sm-paymentgatewaysubscriptions-plugin/config/config.yaml + - ../sm-paymentgatewaysubscriptions-plugin/lang/en/lang.yaml + - ../sm-paymentgatewaysubscriptions-plugin/lang/pl/lang.yaml + - ../sm-paymentgatewaysubscriptions-plugin/README.md + - ../sm-paymentgatewaysubscriptions-plugin/fixture_plugin_test.go + +key-decisions: + - "Customer API contract: human reply \"api-research\" at Task 4 (money objects per currency, 404 {\"error\": \"Subscription not found\"} when the user has none, latest subscription with has_access and in_grace_period)" + - "manage_placements stays registered without a screen or navigation entry, so PHP role exports import cleanly" + - "Plan sync actions never fail after the first gateway call; per-plan failures are `code: message` lines in the result message" + - "Redirect targets come only from api.success_url / api.cancel_url / api.portal_return_url, resolved at boot (absolute http(s), a path joined to app.url, or empty = app.url); a malformed value fails boot naming the key" + - "Gateway errors in the customer API are 502 (logged with user id and plan code only, never the provider text); a missing gateway is 503" + +patterns-established: + - "pgsub web host for tests: bootWeb assembles surf over the plugin set; bootWebWithoutGateway drops the acme.members fixture" + +requirements-completed: [SC-3, D-02, D-08, D-13, D-17, D-21, D-22] + +coverage: + - id: D1 + description: "Subscription Plans admin: PHP form with features as an array-mode taglist (normalised, stored as the JSON list), required code, manage_plans gate" + requirement: D-17 + verification: + - kind: integration + ref: "admin_smoke_test.go#TestAdminPlansSmoke/form_schema, create_normalises_tags, update_keeps_list, code_required, manage_plans_required" + status: pass + human_judgment: false + - id: D2 + description: "Flat reorder page writes sort_order and the list follows it" + requirement: D-17 + verification: + - kind: integration + ref: "admin_smoke_test.go#TestAdminPlansSmoke/flat_reorder" + status: pass + human_judgment: false + - id: D3 + description: "Bulk and toolbar plan sync through the gateway registry: free plans skipped, ids kept after a partial failure, 200 with per-plan lines, manage_plans, localized 403 without a gateway" + requirement: SC-3 + verification: + - kind: integration + ref: "admin_smoke_test.go#TestAdminPlansSmoke/bulk_sync_skips_free, bulk_sync_partial_failure, toolbar_sync_all, actions_need_manage_plans, no_gateway_is_localized_403" + status: pass + human_judgment: false + - id: D4 + description: "Subscriptions admin: newest first with localized status labels, status and plan filters, registry-resolved subscriber preview (unknown alias = danger notice, soft-deleted user shown), read-only, manage_subscriptions" + requirement: D-08 + verification: + - kind: integration + ref: "admin_smoke_test.go#TestAdminSubscriptionsSmoke" + status: pass + human_judgment: false + - id: D5 + description: "Permissions verbatim, navigation order 510, complete EN/PL phrasebook" + requirement: D-02 + verification: + - kind: unit + ref: "admin_smoke_test.go#TestAdminCatalog; admin_smoke_test.go#TestAdminLangParity" + status: pass + human_judgment: false + - id: D6 + description: "Customer API with the confirmed contract: public plans with money objects, Bearer-only routes, 404/409/422/502/503, configured redirects only, cross-user isolation, no private keys" + requirement: D-13 + verification: + - kind: integration + ref: "customer_api_smoke_test.go#TestCustomerAPISmoke" + status: pass + - kind: unit + ref: "customer_api_smoke_test.go#TestRedirectURLs" + status: pass + human_judgment: false + - id: D7 + description: "Admin screens look and feel in a real browser (taglist chips, flat drag reorder, sync toasts, the danger notice on the preview)" + requirement: D-17 + verification: [] + human_judgment: true + rationale: "The smoke tests drive the admin JSON API; the SPA rendering of these screens is not covered by an automated browser test" + +duration: 22min +completed: 2026-10-14 +status: complete +--- + +# Phase 18.1 Plan 04: pgsub admin screens and customer API Summary + +**This plan adds the pgsub admin and a customer API to `sm-paymentgatewaysubscriptions-plugin`.** +- **Subscription Plans:** features are a taglist, the reorder page is flat, and the Stripe sync actions run through the gateway registry. A partial failure keeps the provider ids. +- **Subscriptions:** a filtered list and a read-only preview whose subscriber is resolved through the registry. +- **Permissions and copy:** the three PHP permission codes, navigation and complete EN/PL copy. +- **Customer API:** a Bearer-only JSON API under `/_paymentgatewaysubscriptions/api/v1` with the confirmed "api-research" contract. Redirect targets come from config only. + +Postgres smoke tests cover all of it. The four commits are on local `main` and nothing was pushed. + +## Performance + +- **Duration:** about 22 min +- **Started:** 2026-10-14T15:55:46Z +- **Completed:** 2026-10-14T16:17:33Z +- **Tasks:** 5 (4 implementation tasks, plus the pre-answered Task 4 decision) +- **Files:** 36 in the plugin repository (+3407/-7), plus `deferred-items.md` in this phase directory + +## Repository + +- Commits are in `../sm-paymentgatewaysubscriptions-plugin`, branch `main`, from `aad4647` to `b728762`. Nothing was pushed, and `origin/main` is still `aad4647`. +- `go.mod` and `go.sum` are unchanged (`go mod tidy -diff` is empty). The API imports `sm-paymentgateway-plugin/api` and `/money` from the module that was already required. +- The framework is compiled through `replace ../summercms.go`; tag v0.1.26 is still held. `sm-pgstripe-plugin` (plan 03, through replace) still passes `go vet` and `go test -short` against the changed pgsub. + +## Task Commits + +| Task | Name | Commit | Repo | +|------|------|--------|------| +| 1 | Tracer: plans admin with permissions, navigation and the taglist form | `ca113be` | sm-paymentgatewaysubscriptions-plugin | +| 2 | Flat reorder and the gateway sync actions | `c0a6ac4` | sm-paymentgatewaysubscriptions-plugin | +| 3 | Subscriptions admin, field types and the EN/PL phrasebook | `ef3d804` | sm-paymentgatewaysubscriptions-plugin | +| 4 | Customer API contract (checkpoint:decision, pre-answered) | none (no files) | n/a | +| 5 | Customer API, README sections and the API smoke | `b728762` | sm-paymentgatewaysubscriptions-plugin | + +**Plan metadata:** the docs commit that adds this SUMMARY, in the framework repo. + +## Task 4: customer API contract sheet and reply + +The orchestrator showed the two options before dispatch, and the human answered before Task 5 began. A draft of this SUMMARY recorded the sheet and the reply before Task 5's first commit (`b728762`). + +**Reply (verbatim):** "api-research" + +All routes live under `/_paymentgatewaysubscriptions/api/v1`, in the throttle bucket `paymentgatewaysubscriptions-api`. The bucket allows 120 requests per minute per frontend user, or per client IP when no user is authenticated. + +| Method and path | Auth | Answer | +|-----------------|------|--------| +| GET `/plans` | public | 200 `{"data": [PlanJSON]}`: the active plans by sort_order, then id | +| GET `/subscription` | Bearer jwt | 200 SubscriptionJSON of the user's latest subscription; 404 `{"error": "Subscription not found"}` when the user has none | +| POST `/subscription/checkout` `{plan_code, currency?}` | Bearer jwt | 200 `{checkout_url, session_id}`; 409 `{"error": "Already subscribed"}` when an active or trialing subscription exists; 404 `{"error": "Plan not found"}` for an unknown or inactive plan; 422 `{"error": "Validation failed", "errors": {...}}` for a missing plan_code, a free plan, or a currency the gateway does not support or the plan has no price for | +| POST `/subscription/portal` | Bearer jwt | 200 `{portal_url}`; 404 `{"error": "No billing account"}` when the user has no gateway customer id | +| POST `/subscription/cancel` | Bearer jwt | 200 SubscriptionJSON with `cancel_at_period_end: true` (cancel at period end only); 404 `{"error": "Subscription not found"}` without an active subscription | + +**PlanJSON** +- `code`, `name`, `description` +- `billing_interval` +- `prices`: a list of `{amount, amount_minor, currency}`. There is one entry per key of currency_prices, sorted by currency. A plan without currency_prices gets one entry from price_cents and currency. The currency is upper-cased and the money shape matches the order API (`MoneyJSON`, a four-decimal amount string). +- `features`: a list of strings + +**SubscriptionJSON** +- `status` +- `plan`: `{code, name, billing_interval}` +- the timestamps `current_period_start`, `current_period_end`, `canceled_at`, `ended_at` and `trial_ends_at`, in the order API's RFC 3339 format, or null +- `cancel_at_period_end` +- `days_until_renewal` +- `has_access` and `in_grace_period`, from the service with grace_period_days + +It carries no gateway subscription id, no gateway customer id and no e-mail. + +**Common answers** +- 401 on the four subscription routes without a Bearer token (`{"error": "Authentication required"}`) or with an invalid one (the user plugin's guard body). Cookies are never read. +- 502 `{"error": "Payment provider unavailable"}` on a gateway error, logged with the user id and plan code only. +- 503 `{"error": "Subscription gateway not installed"}` when no gateway is registered. +- 429 from surf's throttle. + +**Rules** +- Redirect URLs come only from config: `api.success_url`, `api.cancel_url` and `api.portal_return_url`. Each is absolute, or a path joined to app.url, and an empty value means app.url. They are never read from the request. +- The subscriber is always (`user`, the JWT user id). No route takes a subscriber or subscription id. + +**The one difference between the options:** what GET `/subscription` answers when the user has no subscription. Under `api-research` (chosen) it is a plain 404 `{"error": "Subscription not found"}`, the order API's envelope. Under `api-null-subscription` it would be 200 `{"subscription": null}`, a wrapper object on this endpoint only. + +The reply is also recorded in the `api` package doc comment (`api/json.go`). + +## Accomplishments + +- **Plans admin.** Controller `golem15.paymentgatewaysubscriptions.subscriptionplans` (`manage_plans`). + - The form ports the PHP form field by field. `features` is `type: taglist, mode: array` over `models.FeatureList`, so tags are trimmed, de-duplicated and stored as the JSON list. + - The list is sorted by sort_order and has checkboxes. + - `config_reorder.yaml` holds only title, modelClass and nameFrom, and compiles as a flat reorder. +- **Sync actions.** + - The bulk `sync_to_stripe` runs `Service.SyncPlan` for each selected plan inside the bulk transaction. + - The toolbar `sync_all_to_stripe` syncs every active plan, one short transaction each. + - Free plans are skipped and counted, and each failing plan becomes a `code: message` line. A missing gateway is a localized 403. Both actions need `manage_plans`. +- **Subscriptions admin.** Controller `golem15.paymentgatewaysubscriptions.subscriptions` (`manage_subscriptions`). + - The list shows the newest first: subscriber type and id, plan name, localized status and period end. + - Filters by status (the five PHP statuses) and by plan, through `models.Subscription.FilterScope`. + - The preview header `subscription_subscriber` resolves the subscriber through the registry, soft-deleted users included. An unregistered alias is a danger notice naming the alias and is logged at Error. + - There is no create button, and saving or deleting through the admin is refused. +- **Field types.** `subscriptionstatus` and `subscriptiongateway` show the localized label in list cells (ListValue) and on the preview (Load), and refuse input. Their element is `golem15-paymentgatewaysubscriptions-label`, defined in `assets/js/fields.js`. +- **Permissions and navigation.** The three PHP codes are registered verbatim. The top navigation item `subscriptions` (order 510, icon refresh-cw) has the side items subscriptions and plans, and only registered screens are listed. +- **Customer API.** + - Five routes under `APIPrefix`, with the bucket `APIBucket` and `pgapi.RequireJWT` on the four subscription routes. + - Handlers `Plans`, `Current`, `Checkout`, `Portal` and `Cancel`, with the `PlanJSON`, `PlanSummaryJSON`, `SubscriptionJSON` and `CheckoutRequest` shapes. + - `RedirectURLs` and `ResolveRedirectURLs` are resolved at boot and published as `*api.RedirectURLs`. +- **Config.** New keys `api.success_url`, `api.cancel_url` and `api.portal_return_url` (default empty), documented with their `SUMMER_*` names. +- **README.** New sections `## Admin` and `## Customer API` (endpoint table, auth, shapes, status codes, redirect config, an example fetch sequence). `## Configuration` gains the three keys, and the API reference, Dependencies and Testing sections are updated. It names no consuming application. + +## Tests + +All Docker tests ran on testcontainers Postgres 16. Nothing is NOT RUN. + +**Per-task verify chains** + +| Task | Docker-free chain | Docker smoke | +|------|-------------------|--------------| +| 1 | VERIFY1_OK | `TestAdminPlansSmoke` passed; the tracer gate re-ran the chain before expanding | +| 2 | VERIFY1_OK | `TestAdminPlansSmoke` passed, 11 subtests | +| 3 | VERIFY1_OK | 2 of 2 pass (`TestAdminSubscriptionsSmoke`, `TestAdminPlansSmoke`) | +| 5 | VERIFY1_OK, including the README `## Customer API` check and no stripe-go | 4 of 4 pass (`TestCustomerAPISmoke`, `TestAdminPlansSmoke`, `TestAdminSubscriptionsSmoke`, `TestSubscriptionsTracer`) | + +**Docker-free tests:** `TestAdminCatalog`, `TestAdminLangParity` and `TestRedirectURLs`. The parity test was checked by deleting one PL key: it failed on that key, and the file was restored. + +**Full suite:** `go test -count=1 ./...` in the plugin passes, the core smoke tests from plan 02 included. + +**Acceptance greps, all as required:** +- one `type: taglist` and one `mode: array` line; +- 3 permission codes; +- `Order: 510`; +- the reorder YAML has 3 keys; +- `bulkActions` and `sync_to_stripe` are present; +- the `subscriptions/preview/:id` record URL, and 0 create buttons; +- `type: subscriptionstatus`; +- `unknown_subscriber_type` in both lang files; +- 0 consuming-application names in .go, .yaml, .htm, .js and .md files; +- the `APIPrefix` line, 5 `RequireJWT`; +- 0 request URL reads; +- the 3 config keys; +- `## Admin`. + +## Decisions Made + +- The customer API contract follows the human's reply "api-research". +- `manage_placements` stays registered, with no screen and no navigation entry. +- `currency_prices`, `stripe_prices` and `stripe_product_id` are not edited on the plan form, as on PHP. +- `sync_all_to_stripe` has no confirm dialog, because `pact.AdminAction` carries no Confirm. +- The customer API reuses the payment gateway's exported `api.RequireJWT` and `api.NewMoneyJSON`, and copies its unexported error writers. +- The redirect targets work as follows: + - A malformed configured value (another scheme, a scheme-relative URL, a bare word, user info) fails boot and names the key. + - A path or empty value with no usable `app.url` stays unresolved rather than failing boot. Checkout and portal then answer a logged 500. This way an application that does not use the customer API needs no `app.url` just to boot. + +## Deviations from Plan + +### Auto-fixed issues and adaptations + +1. **[Rule 3 - Blocking] No `number` list column type in cabana.** + - **Found during:** Task 1. + - **Issue:** cabana list columns support text, datetime, switch, date, time, money and plugin types only, so boot refused `type: number`. + - **Fix:** the plans list's `price_cents`, `max_quotes_per_month` and `sort_order` columns are `type: text`. The YAML header and the README record this. + - **Commit:** `ca113be`. +2. **[Rule 3 - Blocking] Controller IDs are fully qualified.** + - **Issue:** the plan named the IDs `subscriptionplans` and `subscriptions`, but cabana's admin path is `{vendor}/{plugin}/{controller}`. + - **Fix:** the constants are `golem15.paymentgatewaysubscriptions.subscriptionplans` and `golem15.paymentgatewaysubscriptions.subscriptions`, so the paths are `golem15/paymentgatewaysubscriptions/...`, as on PHP. + - **Commit:** `ca113be`. +3. **[Rule 3 - Blocking] The label element name.** + - **Issue:** pact requires a field type element to start with the owning plugin's prefix, so the plan's `golem15-pgsub-label` would fail boot. + - **Fix:** the element is `golem15-paymentgatewaysubscriptions-label`. + - **Commit:** `ef3d804`. +4. **[Rule 3 - Blocking] Filter scopes live on the model.** + - **Issue:** cabana's scope filters need `pact.FilterScope` on the record model (it has no `type: group` filters). + - **Fix:** the new `models/filters.go` adds `FilterStatus`, `FilterPlan`, `FilterStatuses` and `Subscription.FilterScopes`/`FilterScope`. These are additive: plan 02's exported symbols are unchanged. + - **Commit:** `ef3d804`. +5. **[Rule 1 - Bug] Preview labels need the update context.** + - **Issue:** the record read runs a plugin field type's Load in the `update` context, so a `context: preview` status or gateway field showed the raw code. + - **Fix:** the two label-typed preview fields carry `context: [update, preview]`. The controller refuses every save, and Decode refuses any value with a 422. + - **Commit:** `ef3d804`. +6. **The framework's read-only status codes differ from the plan's guess.** The plan expected 404 or 405 for create and update. For a preview-only controller cabana answers: + - a create without a toolbar `create` button: 403; + - an update: the controller's `FormBeforeUpdate` answers 403 with `errors.subscription_read_only`; + - an update that submits a label-typed field: 422. + + The smoke test asserts these codes and that no row changes. Deletes are refused too. +7. **The list schema's `reorder.flat` (framework defect, deferred).** + - **Issue:** the localized list schema (`modules/cabana/list_schema.go`, around line 618) copies only the title, so `GET .../schema/list` omits `flat`. + - **Impact:** none on the page itself. The reorder page reads `flat` from `GET .../reorder`, which answers `true`, and the smoke test asserts it there. + - **Follow-up:** logged in `deferred-items.md` as open, for plan 06 or a quick task. The fix belongs in the framework repo, which this plan does not commit code to. +8. **Changes outside the listed files.** + - `plugin.go` needed no change: the capability assertions live in `admin.go`, `field_types.go` and `routes.go`. + - `harness_test.go` is untouched: `admin_harness_test.go` adds `bootWeb` and `bootWebWithoutGateway`, which assemble surf over the existing boot. + - Task 5 extended `fixture_plugin_test.go`: the fake records the redirect targets and can fail checkout on demand, for the 502 subtest. + - Task 3 added the `_subscription_plan.htm` partial: plan name and code on the preview, instead of a relation form field. +9. **The bulk list offers cabana's built-in "Delete selected".** It appears because `showCheckboxes` is on. Bulk delete stays refused, because the toolbar has no `delete` button. This is framework behaviour and is not changed here. + +**Total deviations:** 5 auto-fixed issues (4 Rule 3, 1 Rule 1), 2 documented framework behaviours, 1 deferred framework defect and the file-placement notes. +**Impact on plan:** every behaviour the plan asks for is delivered. The deviations adapt to cabana's real contracts, and no scope or dependency was added. + +## Threat Model Coverage + +- **T-18.1-20 (redirects):** URLs come from config only. The smoke test sends a body `success_url`, `return_url` and `subscriber_id`, and they are ignored. The acceptance grep finds 0 request URL reads. +- **T-18.1-21 (IDOR):** the subscriber is always the JWT user. The `cross_user_isolation` subtest covers it. +- **T-18.1-22 (sync abuse):** both actions need `manage_plans`, and cabana logs every bulk action run. +- **T-18.1-23 (disclosure):** the API answers carry no gateway ids and no e-mails, as the `no_private_keys` subtest checks. The subscriber partial needs `manage_subscriptions`. +- **T-18.1-24 (DoS):** the bucket allows 120 per minute. Checkout calls the gateway only after the local 409, 404 and 422 checks; the subtest asserts zero gateway calls for refused checkouts. +- **T-18.1-25 (rollback):** the partial-failure subtest shows both plans' product ids surviving. +- **T-18.1-26 (CSRF):** the API takes the token from the Bearer header only. +- **T-18.1-27 (unknown alias):** the danger notice and the Error log are in place, never a 500. +- **T-18.1-SC (supply chain):** no new module was added. + +## Issues Encountered + +- The PL lang restore after the parity sanity check hit an interactive `cp` alias. It was restored with `command cp -f` and verified (`unknown_title` is present). + +## Known Stubs + +None. + +## User Setup Required + +None. An application that serves the customer API sets `app.url`, optionally the three `api.*` redirect keys, and its Stripe keys from plan 03. + +## Next Phase Readiness + +- Plan 05 (`vue-paymentgateway-layer`) can build its subscriptions client and components on the contract above. It reuses `PgMoney` for `prices` and the order API's error-status handling, and treats 404 on GET `/subscription` as "no subscription". +- Plan 06 owns the unit tests, the framework tag and the pushes. It should also fix the `reorder.flat` list-schema defect in `deferred-items.md`. + +--- +*Phase: 18.1-payment-gateway-subscriptions* +*Completed: 2026-10-14* + +## Self-Check: PASSED + +- All 31 created plugin files exist, and the plugin's working tree is clean. +- Commits `ca113be`, `c0a6ac4`, `ef3d804` and `b728762` exist on `main`. `origin/main` is still `aad4647`, so nothing was pushed or tagged. +- `deferred-items.md` exists in this phase directory. diff --git a/.planning/phases/18.1-payment-gateway-subscriptions/deferred-items.md b/.planning/phases/18.1-payment-gateway-subscriptions/deferred-items.md new file mode 100644 index 0000000..a02c002 --- /dev/null +++ b/.planning/phases/18.1-payment-gateway-subscriptions/deferred-items.md @@ -0,0 +1,9 @@ +# Phase 18.1 deferred items + +## Deferred Items + +- The localized list schema drops `reorder.flat` + status: open + **Found during:** plan 18.1-04 Task 2 (TestAdminPlansSmoke/flat_reorder). + **What:** `modules/cabana/list_schema.go` builds the localized `ListReorder` with the title and the partition only (around line 618), so `GET .../schema/list` answers `"reorder":{"title":...}` without `"flat":true` for a flat reorder page, although the compiled `ListReorder.Flat` is set and documented as part of the list schema (plan 18.1-01). The reorder page itself is unaffected: the SPA reads `flat` from `GET .../reorder` (`AdminReorderTree.Flat`), which answers it correctly. + **Fix:** copy `Flat` into the localized `ListReorder` and add the assertion to `reorder_flat_test.go`'s HTTP path; a framework change for plan 18.1-06 or a quick task (out of scope for the plugin repository plan 04 commits to).