docs(18-03): complete payment gateway admin screens plan

This commit is contained in:
2026-10-13 14:01:16 +02:00
parent 49fef6c35a
commit 9b193c1c4c

View File

@@ -0,0 +1,290 @@
---
phase: 18-payment-gateway
plan: 03
subsystem: admin
tags: [paymentgateway, cabana, admin, orders, payments, quotes, shippings, money, partials, record-actions, widget, phrasebook]
requires:
- phase: 18-01
provides: cabana money field and column, danger record actions, the preview Edit rule, the table kit
- phase: 18-02
provides: sm-paymentgateway-plugin domain core (models, orders.Service, payments.Service, operators, cart v2, hashids)
provides:
- five admin controllers in ../sm-paymentgateway-plugin (orders, payments, quotes, quoteables, shippings) with YAML, server partials, record actions and the quote cart widget action
- the seven PHP permissions, the Payments & orders navigation entry, the complete en/pl phrasebook (594 leaf keys per locale, including the P8/P9 settings copy plan 04 uses)
- list-only field types orderstate, paymentstate, operatorname, taxrate
- money.Stored (struct column form of Amount4 that the cabana money field and column accept; JSON "39.8400")
- Order AfterCreate hook writing hash_id and guest_code (models.SetHashids), shared by Orders.Make and the admin quote create
- the golem15-paymentgateway-quote-cart custom element and its stylesheet
- README Admin section
affects: [18-04 settings pages and mailers, 18-05 stripe operator (accept_override, payment_data keys), 18-06 site integration, 18-07 unit tests]
actuals:
tokens: 78400
tasks: 3
commits: 3
plan_head_before: "plugin f725439; framework 49fef6c (no framework code commit in this plan)"
plan_head_after: "plugin ce6969b; framework: this SUMMARY commit"
tech-stack:
added: []
patterns:
- "Admin view models are curated structs with every text pre-translated (support.T in the request's primary locale) and every amount pre-formatted (support.FormatAmount), so templates only place strings"
- "List columns never show stored codes: list-only plugin field types (Decode refuses) translate them in ListValue"
- "A computed list column is a gorm '-' field with a column tag, filled in AfterFind (Shipping.Gross)"
- "One widget field drives several operations through the payload's action; the action opens its own transaction and locks the row (widget actions get no transaction from cabana)"
- "Write permissions on cabana controllers are enforced in FormBeforeCreate/Update/Delete with cabana.ForbiddenError (no write-permission seam)"
- "Partial templates need //go:embed all:controllers (a directory pattern drops _-prefixed files)"
key-files:
created:
- ../sm-paymentgateway-plugin/admin.go
- ../sm-paymentgateway-plugin/admin_permissions.go
- ../sm-paymentgateway-plugin/admin_navigation.go
- ../sm-paymentgateway-plugin/field_types.go
- ../sm-paymentgateway-plugin/controllers/base.go
- ../sm-paymentgateway-plugin/controllers/admin_registry.go
- ../sm-paymentgateway-plugin/controllers/orders.go
- ../sm-paymentgateway-plugin/controllers/orders_partials.go
- ../sm-paymentgateway-plugin/controllers/payments.go
- ../sm-paymentgateway-plugin/controllers/shippings.go
- ../sm-paymentgateway-plugin/controllers/quoteables.go
- ../sm-paymentgateway-plugin/controllers/quotes.go
- ../sm-paymentgateway-plugin/controllers/quotes_cart.go
- ../sm-paymentgateway-plugin/models/hashids.go
- ../sm-paymentgateway-plugin/models/labels.go
- ../sm-paymentgateway-plugin/money/stored.go
- ../sm-paymentgateway-plugin/support/format.go
- ../sm-paymentgateway-plugin/support/locale.go
- ../sm-paymentgateway-plugin/assets/js/quote-cart.js
- ../sm-paymentgateway-plugin/assets/css/quote-cart.css
- ../sm-paymentgateway-plugin/admin_harness_test.go
- ../sm-paymentgateway-plugin/admin_smoke_test.go
- ../sm-paymentgateway-plugin/admin_crud_smoke_test.go
- ../sm-paymentgateway-plugin/admin_quotes_smoke_test.go
- ../sm-paymentgateway-plugin/admin_short_test.go
- ../sm-paymentgateway-plugin/controllers/{orders,payments,quotes,quoteables,shippings}/*.yaml and _*.htm
- ../sm-paymentgateway-plugin/models/{order,payment,shipping,quoteable}/*.yaml
modified:
- ../sm-paymentgateway-plugin/boot.go
- ../sm-paymentgateway-plugin/models/order.go
- ../sm-paymentgateway-plugin/models/payment.go
- ../sm-paymentgateway-plugin/models/shipping.go
- ../sm-paymentgateway-plugin/models/quoteable.go
- ../sm-paymentgateway-plugin/orders/service.go
- ../sm-paymentgateway-plugin/payments/service.go
- ../sm-paymentgateway-plugin/notification_route_test.go
- ../sm-paymentgateway-plugin/lang/en/lang.yaml
- ../sm-paymentgateway-plugin/lang/pl/lang.yaml
- ../sm-paymentgateway-plugin/README.md
key-decisions:
- "money.Stored (struct around Amount4) is the model column type of every stored amount: cabana's money field and column accept only string or struct Scanner/Valuer columns, and Amount4 is an int64"
- "Money list columns are named after the model columns (amount_stored, native_amount_stored, cost_stored): cabana list columns must be model columns"
- "Partials are named without the underscore in YAML (headerPartial: orders_stats -> controllers/orders/_orders_stats.htm), the cabana resolution rule"
- "The quote cart widget field uses cabana's widget:/action: keys and one action, quote_cart, whose payload names the operation (load, add_quoteable, change_item, remove_item, change_details)"
- "A refused quote cart change answers 403 (cabana.ForbiddenError) with the localized reason, because cabana's 422 envelope carries the fixed message 'Validation failed' and the admin toasts the error message"
- "Quote e-mail and description are form-only on create; on update the quote cart element edits them (change_details), since form-only values never come back in a record response"
- "create_cash_payment and create_bank_payment also require Purchase to be allowed and the operator to be on (PHP createPaymentManually threw otherwise)"
- "No order or payment action applies while orders_enabled is off; order and payment forms refuse saves and deletes (state changes only through the services)"
- "Shipping gross cost is the PHP getGrossCost (MoneyRight multiplication rounded to cents): 12.50 at 23% is 15.38, not 15.375"
patterns-established:
- "Plugin admin copy keys follow golem15.paymentgateway::lang.*; the generator is reproducible from the frozen lang.php (scratch script, not committed)"
- "Admin record actions build their toast with params in Go (text, not a key), translated in the request locale"
requirements-completed: [SC-3, D-02, D-05, D-07, D-15]
coverage:
- id: D1
description: "Orders screen: localized state, exact money columns, state filter, stats header, the ten P2 record actions per state with danger flags and the manage_orders gate, reject cancels cash payments, create_cash_payment sends a cash payment, preview partials without return URL or secure parameters, navigation"
requirement: SC-3
verification:
- kind: integration
ref: "../sm-paymentgateway-plugin/admin_smoke_test.go#TestOrdersAdminSmoke (Postgres, run and passed)"
status: pass
human_judgment: false
- id: D2
description: "Payments screen: accept/accept_override/refund applies, accept pays the order through the bridge, unknown operator callout, public payment data only, Polish labels, operator filter, stats"
requirement: SC-3
verification:
- kind: integration
ref: "../sm-paymentgateway-plugin/admin_crud_smoke_test.go#TestPaymentsAdminSmoke (Postgres, run and passed)"
status: pass
human_judgment: false
- id: D3
description: "Shipping methods and quote items CRUD: logo upload, exact money, default currency, computed gross, 23% column, repeater rows, 403 without the manage permission"
requirement: D-07
verification:
- kind: integration
ref: "../sm-paymentgateway-plugin/admin_crud_smoke_test.go#TestShippingsAdminSmoke (Postgres, run and passed)"
status: pass
- kind: integration
ref: "../sm-paymentgateway-plugin/admin_crud_smoke_test.go#TestQuoteablesAdminSmoke (Postgres, run and passed)"
status: pass
human_judgment: false
- id: D4
description: "Quotes: admin create writes a Quote with a v2 cart and hash, quote cart operations with exact prices and refusals, process_quote only with items, placed quote read-only, asset allowlist"
requirement: SC-3
verification:
- kind: integration
ref: "../sm-paymentgateway-plugin/admin_quotes_smoke_test.go#TestQuotesAdminSmoke (Postgres, run and passed)"
status: pass
human_judgment: false
- id: D5
description: "Phrasebook loads in phrasebook, no markup, permission codes agree, partials embedded; amount format and Stored JSON"
requirement: D-02
verification:
- kind: unit
ref: "../sm-paymentgateway-plugin/admin_short_test.go#TestAdminCatalogSmoke"
status: pass
- kind: unit
ref: "../sm-paymentgateway-plugin/admin_short_test.go#TestAdminFormatSmoke"
status: pass
human_judgment: false
- id: D6
description: "Rendering in the admin SPA: the quote cart element (inputs, busy row, restore on refusal, read-only), the partials with the kit classes, light and dark mode"
verification: []
human_judgment: true
rationale: "No browser test drives the custom element or the partial rendering; the element and CSS were checked with node --check and the hex/fetch greps only"
duration: ~45min
completed: 2026-10-13
status: complete
---
# Phase 18 Plan 03: Payment gateway admin screens Summary
**Orders, Payments, Quotes, Quote items and Shipping methods as cabana screens in `sm-paymentgateway-plugin`: localized lists over exact `money.Stored` columns, server-rendered stats and previews, the PHP side-panel transitions as record actions through the services, an inline quote cart custom element, the seven PHP permissions, the navigation entry and the complete en/pl phrasebook.**
## Performance
- **Duration:** about 45 minutes (plugin task commits 13:44, 13:50 and 13:59 CEST)
- **Completed:** 2026-10-13
- **Tasks:** 3 of 3
- **Files:** 75 files changed in the plugin repository (6836 insertions, 90 deletions); this SUMMARY in the framework
## Commits
Plugin repository `../sm-paymentgateway-plugin` (branch `main`, no remote, nothing pushed); measured `git rev-list --count f725439..ce6969b` = 3:
| Task | Commit | Subject |
|------|--------|---------|
| 1 | 8e47570 | feat(18-03): orders admin screen with permissions, navigation, phrasebook, stats, preview partials and record actions (task 1) |
| 2 | b15e17b | feat(18-03): payments list, preview and actions; shippings and quote items CRUD (task 2) |
| 3 | ce6969b | feat(18-03): quotes with the inline quote cart element, process_quote, plugin assets and the README admin section (task 3) |
Framework repository `summercms.go`: no code change; this SUMMARY and the state updates only.
## Accomplishments
- **Task 1 (tracer, orders end to end):** `admin.go` (`//go:embed controllers models assets` plus `all:controllers` for the `_*.htm` templates), `Permissions()` with the seven PHP codes, `Navigation()` (credit-card, order 500, five side items; top-level Orders or Payments from `orders_enabled` at boot), the list-only field types, `money.Stored`, the Order `AfterCreate` hash hook (Orders.Make drops its own update), `support.FormatAmount`/`FormatPercent`, the orders controller with list, state filter, `orders_stats`, the preview (`order_hint`, `order_cart`, `order_payments`, `order_details`, `order_description`) and the ten record actions, the phrasebook (594 leaf keys in each of en and pl), the admin harness and `TestOrdersAdminSmoke`. Tracer gate: automated-only verify re-run green, expanded.
- **Task 2:** payments controller (list, filters, stats, preview partials, accept / accept_override / refund), shippings (logo upload, money net cost, computed `gross_cost`, default currency, manage_shippings gate), quote items (money default cost, VAT, `custom_fields` repeater on `lagoon.Jsonable[[]map[string]any]`, manage_orders gate), the `taxrate` list type and three smoke tests.
- **Task 3:** quotes controller (Quote-only list, create form with customer relation and form-only e-mail/description, EmployeeCreatedOrder after the insert, preview with summary and cart, `process_quote`), the `quote_cart` widget action with five payload operations on the locked order, the `golem15-paymentgateway-quote-cart` element and stylesheet, the README Admin section and `TestQuotesAdminSmoke`.
## Controller IDs, actions and partials
- **Controllers:** `golem15.paymentgateway.orders`, `.payments`, `.quotes`, `.quoteables`, `.shippings`.
- **Record actions:** orders `reject`, `execute`, `ship`, `direct_refund`, `grant_refund`, `wait_for_return`, `refund_after_return`, `guarantee_refund`, `create_cash_payment`, `create_bank_payment`; payments `accept`, `accept_override`, `refund`; quotes `process_quote`.
- **Admin action:** quotes `quote_cart` (widget field `_quote_cart`) with payload operations `load`, `add_quoteable`, `change_item`, `remove_item`, `change_details`.
- **Partials:** `orders_stats`, `order_hint`, `order_cart`, `order_payments`, `order_details`, `order_description`; `payments_stats`, `payment_hint`, `payment_data`, `payment_uuid`, `payment_operator`; `quotes_notice`, `quote_summary`, `quote_cart`; `quoteables_notice`; `shippings_notice`.
- **Lang:** `lang/en/lang.yaml` 594 leaf keys, `lang/pl/lang.yaml` 594 leaf keys (every PHP key of either locale, corrected, plus the new admin keys). New keys plan 04 should use for the settings pages: `settings.tab_general`, `settings.tab_orders`, `settings.tab_payment_methods`, `settings.default_currency_comment`, `settings.default_currency_required`, `settings.orders_only`, `settings.shippings_enabled_comment`, `settings.use_quotes_comment`, `settings.use_underway_label`, `settings.use_underway_comment`, `settings.order_placed_timeout_comment`, `settings.payment_created_timeout_comment`, `settings.payment_sent_timeout_comment`, `settings.order_shipped_timeout_comment`, `settings.order_url_template`, `settings.order_url_template_comment`, `settings.cash_enabled`, `settings.manual_transfer_enabled`, `settings.manual_transfer_comment`, `settings.log_requests`, `settings.log_requests_comment`, `settings.label`, `settings.description`, `settings.mailer_label`, `settings.mailer_description`, `settings.category`; mailer: `mailer.tab_general`, `mailer.tab_order_customer`, `mailer.tab_order_staff`, `mailer.tab_payment_customer`, `mailer.tab_payment_staff`, `mailer.admin_email`, `mailer.admin_group`, `mailer.staff_recipient_comment`, `mailer.orders_off_only`, `mailer.order.placed`, `mailer.order.underway`; the existing PHP `settings.*` labels and `mailer.*` switches are kept.
## Verification
- `go vet ./...` and `go test -count=1 -short ./...` in the plugin: green at every commit.
- Docker suites, all RUN and PASS on this host (testcontainers Postgres 16): `TestOrdersAdminSmoke`, `TestPaymentsAdminSmoke`, `TestShippingsAdminSmoke`, `TestQuoteablesAdminSmoke`, `TestQuotesAdminSmoke`, and the plan 02 suites (`TestPaymentGatewayTracer`, `TestRefundBridgeSmoke`, `TestNotificationRouteSmoke`, `TestPGCommandsSmoke`, `TestImportWinterSmoke`); full `go test -count=1 ./...` green.
- Plan verify commands: Task 1 sampled lang keys present and 7 permission codes; Task 2 `accept_override`, `type: fileupload`, `type: repeater` present, 3 smoke passes; Task 3 `customElements.define('golem15-paymentgateway-quote-cart'` present, no `fetch(`, no hex colour in the CSS, `TestQuotesAdminSmoke` and `TestOrdersAdminSmoke` pass.
- Acceptance greps passed except the three noted below (cabana key or naming rules).
- `node --check assets/js/quote-cart.js` clean. No consuming application is named in the plugin's code, YAML, copy or README.
- Framework repository: no Go code touched by this plan.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] `money.Stored` replaces `money.Amount4` as the model column type**
- **Found during:** Task 1
- **Issue:** cabana's money field and column (plan 01) accept a string column or a struct whose pointer implements sql.Scanner and that implements driver.Valuer; plan 02's `Amount4` is an `int64`, so boot failed, and its JSON form was a number.
- **Fix:** `money.Stored` (a struct embedding Amount4, JSON "39.8400"), used by the stored amount fields of Order, Payment, Shipping and Quoteable; `Money.Stored()`, `money.StoredOf`. Arithmetic code keeps Amount4.
- **Commit:** 8e47570
**2. [Rule 3 - Blocking] Column, partial and widget names follow cabana's rules**
- Money list columns are `amount_stored` / `native_amount_stored` / `cost_stored` (cabana list columns must be model columns), not `amount` / `native_amount` / `cost` / `default_cost`.
- `headerPartial: orders_stats` (cabana resolves a name to `_<name>.htm`), so the acceptance grep `headerPartial: _orders_stats` does not match; the template file is `_orders_stats.htm` as planned.
- The quote cart field uses `widget: golem15-paymentgateway-quote-cart` and `action: quote_cart` (cabana's widget keys), so the grep `element: golem15-paymentgateway-quote-cart` does not match; one widget runs one action, so add_quoteable, change_item and remove_item are payload operations of `quote_cart`, not three admin actions.
- The order relation column is `relation: successfulpayment` (cabana matches the Go field name case-insensitively).
- **Commits:** 8e47570, ce6969b
**3. [Rule 3 - Blocking] `//go:embed all:controllers` beside `//go:embed controllers models assets`**
- A directory pattern leaves out `_`-prefixed files, so the partial templates were missing at boot.
- **Commit:** 8e47570
**4. [Rule 3 - Blocking] Navigation lists only registered controllers**
- cabana fails boot on a navigation item naming an unknown controller; the side menu is filtered to registered controllers so each task booted (all five are registered after Task 3).
- **Commit:** 8e47570
**5. [Rule 1 - Spec fidelity] Refused quote cart changes answer 403 with the reason, not 422**
- cabana writes a 422 with the fixed message "Validation failed" and the admin toasts `error.message`, so the UI-SPEC's "the host shows the server message as a danger toast" needs the D-27 refusal path (`cabana.ForbiddenError`, localized message). `TestQuotesAdminSmoke` asserts 403 and the `money.too_precise` text for `12.34567` and the count message for `0`.
- **Commit:** ce6969b
**6. [Rule 1 - Spec fidelity] Shipping gross is the PHP figure 15.38, not 15.375**
- `Shipping.GrossCost` (plan 02, PHP getGrossCost) multiplies with MoneyRight rounding to cents; the plan's 15.3750 would have been a different formula. The test asserts `15.3800`.
- **Commit:** b15e17b
**7. [Rule 1 - Spec fidelity] A payment of an unknown operator offers no action**
- The plan expected "only refund when Accepted"; the payment machine guards Refund with the operator switch (PHP side panel the same), so nothing is offered; an Accepted cash payment offers only refund, as tested.
- **Commit:** b15e17b
**8. [Rule 2 - Correctness] Create-payment and order actions are gated further**
- create_cash_payment and create_bank_payment also need Purchase allowed and the operator turned on (PHP threw otherwise); no order or payment action applies while orders are off; the order and payment forms refuse saves and deletes (the F6 rule already hides Edit; this closes the PUT route); FormRules on the order and payment forms are empty (the model rules made a no-op PUT a 500).
- **Commit:** 8e47570, b15e17b
**9. [Rule 3 - Blocking] Quote create resolves the customer after the insert**
- cabana writes a belongsTo foreign key after FormBeforeCreate, so the customer's e-mail default and the "customer or e-mail" check run in FormAfterCreate (a 422 rolls the insert back); FormAfterCreate also fires EmployeeCreatedOrder as PHP did.
- **Commit:** ce6969b
**10. [Rule 2 - UX] Quote e-mail and description edited in the element**
- Form-only fields never come back in a record response, so the update form could not show them; they are create-only fields and the element edits them on update (`change_details`). The quote preview shows them (`quote_summary`).
- **Commit:** ce6969b
**11. [Rule 3 - Blocking] Model adjustments**
- `Order` gained the `User`, `Payments` and `SuccessfulPayment` relations for the list columns; `Shipping.Rules` and `Quoteable.Rules` name `cost_stored` and bound the tax to 0..100; `Quoteable.CustomFields` is a `lagoon.Jsonable` list (the PHP repeater rows) with `Fields()`; `Shipping.Gross` is the computed list column.
- `notification_route_test.go`'s no-database boot sets the admin JWT secret, because the router now mounts cabana for the plugin's controllers.
- **Commits:** 8e47570, b15e17b
**12. [Rule 2 - Copy] Extra phrase keys**
- Besides the UI-SPEC keys: the order and payment preview subtitles (`text.preview_order`, `text.preview_payment`), action result messages (`actions.order_moved`, `actions.payment_created`, `actions.payment_accepted`, `actions.payment_refunded`, `actions.failed`, `actions.feature_off`), the quote cart copy and refusals (`quote.*`), `payment.data.*`, `errors.manageShippingsRequired`, `errors.manageOrdersRequired`, `labels.data`, `text.disabled_operator`, and the settings/mailer keys listed above for plan 04.
- **Commits:** 8e47570, ce6969b
---
**Total deviations:** 12 auto-fixed (6 blocking, 4 spec fidelity, 2 correctness/UX). **Impact:** the admin contract matches the UI-SPEC behaviour on the framework as it is; three acceptance greps encode names cabana does not use (listed in 2).
## Issues Encountered
- Record action toasts built with params are passed to cabana as text; cabana runs every message through the translator, which logs a "missing translation key" warning outside production for such text. Harmless; the alternative was dropping the state or id from the toast.
- The admin SPA rendering (the quote cart element, the partials in light and dark mode) has no browser test; coverage D6 is human judgment.
## Known Stubs
None.
## Threat Flags
None beyond the plan's register. T-18-17 (every action lists manage_orders or manage_payments; writes on shippings, quote items and quotes need their manage permission), T-18-18 (field allowlist, money grammar without rounding, count ≥ 1, tax 0..100, non-Quote orders read-only, amounts recomputed by orders.Service.Save), T-18-19 (curated view models; payment_data reads only five public keys; asserted), T-18-20 (html/template; the element writes text nodes only), T-18-22 (asset allowlist; traversal asserted) are implemented and tested.
## User Setup Required
None.
## Next Phase Readiness
- Plan 04 (settings pages and mailers) can embed its settings YAML in the same `adminFS` (`models/settings/` is already under the embedded `models` tree) and use the `settings.*` and `mailer.*` keys listed above without adding copy.
- Plan 05 (Stripe): `accept_override` applies to api-mode operators; `payment_data` shows `stripe_checkout_session_id`, `payment_intent_id`, `charge_id`, `email`, `is_paid_in_stripe` and never `client_secret`.
- Plan 07 (unit tests): the admin smoke tests cover the screens end to end; view-model builders and the quote cart operations are good unit-test targets.
## Self-Check: PASSED
- Files exist: admin.go, admin_permissions.go, admin_navigation.go, field_types.go, controllers/{base,admin_registry,orders,orders_partials,payments,shippings,quoteables,quotes,quotes_cart}.go, models/hashids.go, money/stored.go, support/format.go, assets/js/quote-cart.js, assets/css/quote-cart.css, lang/{en,pl}/lang.yaml, the four admin test files.
- Commits exist in ../sm-paymentgateway-plugin: 8e47570, b15e17b, ce6969b.