docs(18-04): complete payment gateway settings and mail plan

This commit is contained in:
2026-10-13 14:31:53 +02:00
parent 21c5618dd1
commit 79dd3a0bb1

View File

@@ -0,0 +1,281 @@
---
phase: 18-payment-gateway
plan: 04
subsystem: payments
tags: [paymentgateway, settings, mailer, postcard, mail-templates, lagoon-aftercommit, cabana, import-winter, seaglass]
requires:
- phase: 18-01
provides: cabana settings forms with tab, richeditor (seaglass) and password
- phase: 18-02
provides: models.Settings singleton, the 24 typed events, orders/payments services, the bridge, paymentgateway:import-winter
- phase: 18-03
provides: adminFS embed, permissions, the settings.* and mailer.* phrase keys, the orders_stats setup notice
provides:
- payment_gateway settings page (three tabs, required default currency replacing the SetUp wizard, number timeouts, sanitized bank details, order_url_template)
- payment_gateway_mailer settings page on golem15_paymentgateway_mailer_settings (sender, staff e-mail or backend role, 27 switches)
- 27 postcard mail templates in Polish and English (54 files) under the PHP codes plus the paymentgateway layout
- four mailer groups (events.RegisterMailers) with PHP gating, recipients, locale suffix and lagoon.AfterCommit sending; events.MailSender seam
- paymentgateway:import-winter maps mailer.credentials.*, mailgroup and every mailer.<group>.<event> switch
- framework: cabana settings forms take method-backed dropdown options from the settings model
affects: [18-05 stripe operator (Stripe settings page on the same settings features), 18-06 site integration (order_url_template, customer mails), 18-07 unit tests]
actuals:
tokens: 52790
tasks: 3
commits: 4
plan_head_before: "plugin ce6969b; framework dff931c"
plan_head_after: "plugin 4b4b7c7; framework 21c5618 (before this SUMMARY commit)"
tech-stack:
added: []
patterns:
- "Settings models serve method-backed dropdowns through pact.DropdownOptionsProvider; data the models package cannot import (currency.ISO, the translator, backend roles) is installed by setters at init or boot"
- "A settings model's BeforeValidate is the per-field 422 seam: it returns *cabana.ValidationError with the plugin's translated message"
- "A dropdown-backed integer column on a settings form is a Scanner type (models.NullID) so lagoon.Fill accepts the option's text value"
- "Mailers read switches through the event transaction, build the message in the transaction and send it in lagoon.AfterCommit; send errors are logged, never returned"
- "Mail tests publish a recording events.MailSender that renders through the plugin's own postcard catalog"
key-files:
created:
- ../sm-paymentgateway-plugin/admin_settings.go
- ../sm-paymentgateway-plugin/mail.go
- ../sm-paymentgateway-plugin/models/mailer_settings.go
- ../sm-paymentgateway-plugin/models/null_id.go
- ../sm-paymentgateway-plugin/models/settings/fields.yaml
- ../sm-paymentgateway-plugin/models/settings/mailer_fields.yaml
- ../sm-paymentgateway-plugin/updates/202610130007_create_golem15_paymentgateway_mailer_settings.go
- ../sm-paymentgateway-plugin/events/mailers.go
- ../sm-paymentgateway-plugin/events/recipients.go
- ../sm-paymentgateway-plugin/views/mail/layouts/paymentgateway.htm
- ../sm-paymentgateway-plugin/views/mail/{order,payment,admin_order,admin_payment}/*.htm (54 files)
- ../sm-paymentgateway-plugin/settings_smoke_test.go
- ../sm-paymentgateway-plugin/mail_smoke_test.go
- modules/cabana/settings_options_test.go
modified:
- ../sm-paymentgateway-plugin/boot.go
- ../sm-paymentgateway-plugin/models/settings.go
- ../sm-paymentgateway-plugin/console/import_winter.go
- ../sm-paymentgateway-plugin/commands_smoke_test.go
- ../sm-paymentgateway-plugin/lang/en/lang.yaml
- ../sm-paymentgateway-plugin/lang/pl/lang.yaml
- ../sm-paymentgateway-plugin/README.md
- modules/cabana/settings.go
- modules/cabana/http.go
- modules/cabana/README.md
- docs/backend/settings.md
key-decisions:
- "cabana settings forms take method-backed dropdown options from the settings model (pact.DropdownOptionsProvider); boot refuses the method otherwise. Needed for the currency and staff group dropdowns; framework tag still held to the phase gate"
- "default_currency is validated as required|in:<every ISO code> plus a BeforeValidate 422 with settings.default_currency_required; lagoon has no string or size rule, so the plan's required|string|size:3 would have been a 500"
- "Timeouts show the PHP effective defaults 7/7/7/14 (initSettingsData and the seeded row), not the knob default 2 for payment_created that PHP never displayed"
- "Order mails (customer and staff) go out only with orders_enabled on and payment mails (customer and staff) only with it off, as PHP registerSubscribers did; staff placed only without quotes, staff customerCreatedOrder only with quotes"
- "postcard sends from mail.from; the PHP sender (mailer_from_email) becomes the reply-to and the posName/posEmail variables"
- "A guest@ user's order mails the details address (plan rule; PHP mailed nobody)"
- "events.MailSender: an application may publish one to receive the gateway's messages instead of the postcard mailer (queue, copy); the tests use it because backpack cannot replace the published postcard.Mailer"
- "admin_group_id is a backend role id stored through models.NullID (a Scanner) so the dropdown's text value fills the INTEGER column"
- "order_url_template must be an absolute http(s) URL of at most 500 characters: postcard refuses mails with any other link scheme, so a bad template would stop every customer mail"
- "import-winter sanitizes the bank details through seaglass and stores the PHP rich-text receiver address as plain text"
patterns-established:
- "Mail variables are curated maps with the PHP names (order, user, posName, posEmail, orderUrl, payment, employee, customer); rich text reaches Markdown mails only as plain lines (events.PlainText)"
requirements-completed: [SC-3, D-02, D-05, D-08, D-13]
coverage:
- id: D1
description: "payment_gateway settings page: three tabs, ISO currency options, required default currency with its 422 copy (en and pl), unknown code refused, fresh page loads without a currency, bank details stored sanitized with a notice, bad order URL template refused, timeouts 1..30, orders header drops the setup callout after the save"
requirement: D-08
verification:
- kind: integration
ref: "../sm-paymentgateway-plugin/settings_smoke_test.go#TestPGSettingsSmoke (Postgres, run and passed)"
status: pass
human_judgment: false
- id: D2
description: "payment_gateway_mailer settings page: five tabs, 27 switches, backend roles in the staff group dropdown, sender and role id stored, invalid admin e-mail refused, 403 without access_settings"
requirement: D-08
verification:
- kind: integration
ref: "../sm-paymentgateway-plugin/settings_smoke_test.go#TestMailerSettingsSmoke (Postgres, run and passed)"
status: pass
human_judgment: false
- id: D3
description: "54 templates registered, parsed and rendered in both languages with the mailers' variables"
requirement: SC-3
verification:
- kind: unit
ref: "../sm-paymentgateway-plugin/mail_smoke_test.go#TestMailCatalog"
status: pass
human_judgment: false
- id: D4
description: "OrderPaid through the services mails the English customer once after commit with the order code and order URL; switch off sends nothing; guest@ user mails the details address; rollback sends nothing"
requirement: SC-3
verification:
- kind: integration
ref: "../sm-paymentgateway-plugin/mail_smoke_test.go#TestMailTracer (Postgres, run and passed)"
status: pass
human_judgment: false
- id: D5
description: "Every row of the gating table: template, recipient class, locale suffix, orders on/off gating, quotes gating, staff group fan-out, purchased bank details as plain text, rollback"
requirement: SC-3
verification:
- kind: integration
ref: "../sm-paymentgateway-plugin/mail_smoke_test.go#TestMailersMatrixSmoke (Postgres, run and passed, 27 subtests)"
status: pass
human_judgment: false
- id: D6
description: "import-winter maps mailer.credentials.*, the mailgroup pair (role checked), three switches, reports unknown mailer keys, sanitizes bank details"
requirement: SC-3
verification:
- kind: integration
ref: "../sm-paymentgateway-plugin/commands_smoke_test.go#TestImportWinterSmoke (Postgres, run and passed)"
status: pass
human_judgment: false
- id: D7
description: "cabana settings forms serve method-backed dropdown options from the settings model and refuse the method without DropdownOptions"
requirement: D-08
verification:
- kind: unit
ref: "modules/cabana/settings_options_test.go#TestSettingsDropdownMethod"
status: pass
human_judgment: false
- id: D8
description: "Mail wording in Polish and English, the settings pages in the admin SPA (tabs, dropdowns, comments, the richeditor) in light and dark mode"
verification: []
human_judgment: true
rationale: "The Polish copy of the templates is a new translation of the English PHP texts, and no browser test renders the two settings pages"
duration: ~40min
completed: 2026-10-13
status: complete
---
# Phase 18 Plan 04: Payment gateway settings pages and mail Summary
**Two settings pages for `sm-paymentgateway-plugin` (the gateway settings with a required default currency replacing the SetUp wizard, and the mailer settings on their own singleton with 27 switches), 27 postcard templates in Polish and English under the PHP codes, and the four PHP mailer groups ported as festival listeners that send through `lagoon.AfterCommit`, plus the mailer keys in `paymentgateway:import-winter`.**
## Performance
- **Duration:** about 40 minutes of execution (framework commit 14:08, plugin commits 14:19, 14:25 and 14:27 CEST)
- **Completed:** 2026-10-13
- **Tasks:** 3 of 3
- **Files:** 73 files changed in the plugin repository (3686 insertions, 37 deletions); 5 files in the framework
## Commits
Plugin repository `../sm-paymentgateway-plugin` (branch `main`, no remote, nothing pushed); measured `git rev-list --count ce6969b..4b4b7c7` = 3:
| Task | Commit | Subject |
|------|--------|---------|
| 1 | dee26dd | feat(18-04): payment gateway and mailer settings pages, the paid mail in PL and EN sent after commit (task 1) |
| 2 | 37d91f1 | feat(18-04): the 27 mail templates in PL and EN, the four mailer groups and the mailer keys in import-winter (task 2) |
| 3 | 4b4b7c7 | docs(18-04): README Settings and Mail sections, the mailer cutover keys and the setup callout check (task 3) |
Framework repository `summercms.go` (branch `master`); measured `git rev-list --count dff931c..21c5618` = 1:
| Task | Commit | Subject |
|------|--------|---------|
| 1 | 21c5618 | feat(18-04): settings forms take method-backed dropdown options from the settings model (task 1) |
| - | (this SUMMARY) | docs(18-04): complete payment gateway settings and mail plan |
## Accomplishments
- **Task 1 (tracer):** `Plugin.Settings()` with `payment_gateway` and `payment_gateway_mailer`; `models/settings/fields.yaml` (General, Orders, Payment methods; 23 fields each with a tab), `Settings.Rules`, `BeforeValidate`, `DropdownOptions`, `GetDefaultCurrencyOptions`, `GetExchangeRateProviderOptions`, `OrderURL`; `models.MailerSettings`, migration `202610130007`, `mailer_fields.yaml` (five tabs, 27 switches); `mail.go`, the layout and the paid templates; `events.RegisterMailers` boot step after the bridge with `CustomerOrderMailer` on `OrderPaid`; recipients and locale helpers; the framework settings dropdown fix; `TestPGSettingsSmoke`, `TestMailerSettingsSmoke`, `TestMailTracer`, `TestMailCatalog`. Tracer gate: automated-only verify re-run green, expanded.
- **Task 2:** the remaining 52 templates (customer order, staff order, customer payment, staff payment; `order.placed` registered, `order.underway` added), all 54 names in `MailTemplates()`, the four mailer groups with the PHP gating, the purchased mail's bank transfer details, `PaymentView`, staff fan-out with admin preview links, the mailer keys in import-winter, `TestMailersMatrixSmoke` and the extended `TestImportWinterSmoke`.
- **Task 3:** README `## Settings` (both pages, every column and PHP key), `## Mail` (template table, gating, recipients, locale, variables, after-commit sending, `events.MailSender`, the PHP bugs fixed), the Cutover mailer keys and `mailgroup`, API reference and Testing updates; `TestPGSettingsSmoke` checks that the orders header drops "Orders are paused" once PLN is saved (`orders_stats` reads the settings row per request; no code change needed).
## Settings codes and columns
- **`payment_gateway`** (`golem15_paymentgateway_settings`): `default_currency`, `exchange_rate_provider`, `currency_auto_create`, `enable_guest`, `backend_prefer_local`, `frontend_prefer_local`, `orders_enabled`, `shippings_enabled`, `use_quotes`, `use_underway`, `order_placed_to_canceled_timeout`, `payment_created_to_canceled_timeout`, `payment_sent_to_canceled_timeout`, `order_shipped_to_finished_timeout`, `order_url_template`, `cash_enabled`, `manual_transfer_enabled`, `manual_transfer_name`, `manual_transfer_receiver_address`, `manual_transfer_bank_acc_no`, `manual_transfer_swift`, `manual_transfer_bank_details`, `log_requests`.
- **`payment_gateway_mailer`** (`golem15_paymentgateway_mailer_settings`): `mailer_from_email`, `mailer_from_name`, `admin_email`, `admin_group_id`, and the switches `mailer_admin_payment_accepted`, `mailer_admin_order_customer_created_order`, `mailer_admin_order_placed`, `mailer_admin_order_paid`, `mailer_admin_order_requested_refund`, `mailer_order_employee_created_order`, `mailer_order_rejected_received_payment`, `mailer_order_guarantee_refund`, `mailer_order_deferred_refund`, `mailer_order_refunded_after_return`, `mailer_order_waiting_for_return`, `mailer_order_granted_refund`, `mailer_order_direct_refund`, `mailer_order_requested_refund`, `mailer_order_finished`, `mailer_order_shipped`, `mailer_order_paid`, `mailer_order_canceled_not_active`, `mailer_order_canceled`, `mailer_order_rejected`, `mailer_order_purchased`, `mailer_order_placed`, `mailer_order_underway`, `mailer_payment_accepted`, `mailer_payment_canceled`, `mailer_payment_refunded`, `mailer_payment_rejected`.
- **Templates:** 27 mails, 54 files (`views/mail/{order,payment,admin_order,admin_payment}`), plus the layout `views/mail/layouts/paymentgateway.htm`.
## 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): `TestPGSettingsSmoke`, `TestMailerSettingsSmoke`, `TestMailTracer`, `TestMailersMatrixSmoke` (27 subtests), `TestImportWinterSmoke`; full plugin `go test -count=1 ./...` green (all plan 02 and 03 suites included).
- Framework: `go vet ./...` and `go test -short ./...` green; `go test -count=1 ./modules/cabana` (with Docker) green; `summer docs:build --check` no problems.
- Plan verify commands: Task 1 (27 switches, richeditor, `paid-en.htm`, three smoke passes), Task 2 (54 template files, 55 `golem15.paymentgateway::mail.` names in `mail.go` including the layout, two smoke passes), Task 3 (`## Settings` and `## Mail`, `order.underway` in the README, `TestPGSettingsSmoke` pass).
- Acceptance greps: all pass except `required|string|size:3` (see deviation 2). No consuming application is named anywhere in the plugin (the plan's `horoskopia|fonoteka|grzyby` grep exits clean).
- Sibling settings forms (journal, sitemap, translate) declare no method dropdown, so the cabana boot check changes nothing for them.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] cabana settings forms could not serve method-backed dropdowns**
- **Found during:** Task 1
- **Issue:** the settings schema localized the form with a nil options provider, so `options: getDefaultCurrencyOptions` (and the staff group dropdown) would have been a 500; the plan listed ADMIN-01 model-method options as a consumed contract.
- **Fix:** the settings schema passes the settings model as the `pact.DropdownOptionsProvider` (the relation-form rule); boot refuses a dropdown method on a settings model without it. README, `docs/backend/settings.md` and `TestSettingsDropdownMethod` updated in the same commit.
- **Commit:** 21c5618 (framework)
**2. [Rule 1 - Bug] `required|string|size:3` is not a lagoon rule**
- **Found during:** Task 1
- **Issue:** lagoon.Validate has no `string` or `size` token and fails loudly on unknown tokens (a 500 on save); the plan 02 rule `nullable|string|size:3` had never been exercised.
- **Fix:** `default_currency: required|in:<every ISO code of the dropdown>` (stricter) plus `Settings.BeforeValidate` answering an empty value with `settings.default_currency_required`, translated through a translator installed at boot. The acceptance grep for `required|string|size:3` does not match. `order_url_template` length and scheme are checked in the same hook (lagoon `max` does not apply to strings).
- **Commit:** dee26dd
**3. [Rule 1 - Spec fidelity] Timeout defaults 7/7/7/14, not 7/2/7/14**
- **Issue:** PHP `initSettingsData` set `paymentCreatedToCanceledTimeout = 7`; the knob's `default: 2` was never shown because the settings model initialized the attribute. Plan 02's seeded row holds 7.
- **Fix:** form defaults and the smoke assert 7/7/7/14; a save of 2 is accepted and stored.
- **Commit:** dee26dd
**4. [Rule 1 - Spec fidelity] Payment mailers gated like PHP**
- The plan gated only the customer payment mails on `orders_enabled` off; PHP `registerSubscribers` subscribed both payment mailers only with orders off and both order mailers only with orders on. The port does the same per event; staff placed only without quotes and staff customerCreatedOrder only with quotes (PHP `useQuotes`).
- **Commit:** 37d91f1
**5. [Rule 3 - Blocking] `events.MailSender`**
- backpack cannot replace the published `postcard.Mailer`, and its memory driver is unreachable, so a test could not see template names. The mailers send through a published `events.MailSender` when there is one, else the postcard mailer; the tests publish a recorder that renders through the plugin's catalog. Documented in the README as an application extension point.
- **Commit:** dee26dd
**6. [Rule 3 - Blocking] `admin_group_id` as `models.NullID`**
- The dropdown sends the option value as text and `lagoon.Fill` cannot put a string into `*uint`; `models.NullID` (Scanner, Valuer, JSON as the option text) fills the INTEGER column and serves the stored choice back. The `nullable|integer` rule was dropped (the type takes only an id).
- **Commit:** dee26dd
**7. [Rule 2 - Correctness] Empty sender and staff addresses stored as NULL**
- An emptied text field arrives as `""`, which failed `nullable|email`; `MailerSettings.BeforeValidate` trims and nils them.
- **Commit:** dee26dd
**8. [Rule 2 - Security] Import sanitizes rich text**
- The prohibition says rich text is stored only after seaglass; the plan 02 import stored the PHP bank details as exported. import-winter now runs them through `seaglass.Sanitize` and stores the PHP rich-text receiver address as plain text (a text field here). `TestImportWinterSmoke` asserts both.
- **Commit:** 37d91f1
**9. [Rule 2 - Copy] One new phrase key**
- `settings.order_url_template_invalid` (en, pl) for the refused order URL template; every other label and comment reuses the plan 03 keys.
- **Commit:** dee26dd
**10. [Rule 2 - Correctness] Mail details beyond the plan**
- `ReplyTo` is the mailer sender address (postcard has no per-message From). The purchased mail carries `manual` and `banking_details` (PHP), with the bank details reduced to plain lines (`events.PlainText`), because goldmark drops raw HTML and postcard refuses unsafe markup. Staff mails link to the admin preview (`app.url` + admin prefix + `/golem15/paymentgateway/orders/<id>/preview`), as PHP linked `Backend::url`.
- **Commits:** dee26dd, 37d91f1
---
**Total deviations:** 10 auto-fixed (4 blocking, 3 spec fidelity or bug, 3 correctness/security/copy). **Impact:** the contracts named in the plan (table, columns, template codes, variables, switch keys) are as planned; one framework change was needed and is covered by tests and docs.
## Issues Encountered
- The phrasebook logs a one-time "missing translation key" warning outside production for each dropdown option label that is plain text (the currency names, the role names), because cabana runs option labels through the translator. Harmless; the same as the plan 03 record action toasts.
- The Polish template copy is a new translation (PHP shipped English only); coverage D8 leaves it to human review.
## Known Stubs
None.
## Threat Flags
None beyond the plan's register. T-18-23 (richeditor sanitized on save and on import, plain text in mail; asserted), T-18-24 (recipients from the order, details or payment data and the settings, never from request input; e-mail rules), T-18-25 (variables rendered by postcard's html/template, addresses through `net/mail`, no header concatenation), T-18-26 (AfterCommit; rollback cases asserted in `TestMailTracer` and `TestMailersMatrixSmoke`), T-18-27 (both pages behind `access_settings`; 403 asserted) are implemented and tested. `order_url_template` is restricted to http(s) so a stored template cannot inject a dangerous link scheme.
## User Setup Required
None.
## Next Phase Readiness
- Plan 05 (Stripe): the Stripe settings page can use the same settings features (tabs, password secrets, method dropdowns now work on settings models). The OrderPaid host test can assert the paid mail through an `events.MailSender` recorder.
- Plan 06 (site integration): set `order_url_template` to the site's order page; customer mails link there.
- Plan 07 (unit tests): `events.CustomerRecipient`, `MailTemplate`, `PlainText`, `decodeMailGroup`, `ValidOrderURLTemplate` and `NullID` are pure functions worth unit tests.
## Self-Check: PASSED
- Files exist: admin_settings.go, mail.go, models/mailer_settings.go, models/null_id.go, models/settings/fields.yaml, models/settings/mailer_fields.yaml, updates/202610130007_create_golem15_paymentgateway_mailer_settings.go, events/mailers.go, events/recipients.go, views/mail/layouts/paymentgateway.htm, the 54 templates, settings_smoke_test.go, mail_smoke_test.go, modules/cabana/settings_options_test.go.
- Commits exist: plugin dee26dd, 37d91f1, 4b4b7c7; framework 21c5618.