docs(18.1-04): complete pgsub admin screens and customer API plan
This commit is contained in:
@@ -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.
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user