diff --git a/.planning/phases/18-payment-gateway/18-03-SUMMARY.md b/.planning/phases/18-payment-gateway/18-03-SUMMARY.md new file mode 100644 index 0000000..94f1229 --- /dev/null +++ b/.planning/phases/18-payment-gateway/18-03-SUMMARY.md @@ -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 `_.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.