feat(templates): add localized push templates and actions
Share template keys across email and push with independent editors and revisions. Render localized actions and preserve version-one delivery. Ref: IT-1627
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
# Channel template model — IT-1627
|
||||
|
||||
## Current compatibility decision — 2026-10-31 (supersedes earlier checkpoints)
|
||||
|
||||
The owner subsequently required independently deliverable Notification Service
|
||||
changes. Request `version` now selects the format: omitted or `1` accepts the
|
||||
original title/body/route request and emits the original version=1 data payload with
|
||||
route=/home, without template lookup or actions. Version `2` requires templateKey,
|
||||
languageCode and templateData and emits localized buttons. Unsupported versions
|
||||
are explicit validation errors, never a fallback. One Firebase channel serves both.
|
||||
The earlier no-legacy decision below is historical and no longer applies.
|
||||
|
||||
An action metadata entry is still required for each selected action, but its
|
||||
objectId is optional. Section-only actions use `{}`; no synthetic object IDs.
|
||||
For Invemory, the agreed first button label is «Перейти в розділ», localized in the
|
||||
template, and the key determines contacts/containers/basket navigation. No mutation
|
||||
or direct object-opening behavior is added in this increment.
|
||||
|
||||
Push Channel test-send has a format selector. Legacy preserves its existing simple
|
||||
Firebase notification test (token only), distinct from the production version=1
|
||||
data-envelope flow. Version 2 emits the actual action data envelope with one button
|
||||
and matching primary action, requiring the actual device binding ID, action key and
|
||||
label. It never invents a binding or object ID. Unknown versions and missing binding
|
||||
are rejected before provider calls. Test fields are not channel configuration;
|
||||
token is cleared after send, and token/binding are cleared on modal close.
|
||||
|
||||
Version 2 requires a client that supports this payload. Current Invemory Android
|
||||
only accepts version=1 and /home; producer capability routing and client updates
|
||||
remain outside this Notification Service increment. Keeping version 1 as the
|
||||
default permits independent service delivery without changing existing producers.
|
||||
|
||||
## Scope and sequence
|
||||
|
||||
The owner agreed on 2026-10-30 to implement the model first, then adapt the
|
||||
Notification Service administration. The service page is called Templates;
|
||||
the Template editor has Email and Push channel navigation, each containing language tabs.
|
||||
A key may have Email only, Push only, or both. Presence never selects a delivery
|
||||
channel automatically. Transport rendering and Invemory producers are later work.
|
||||
|
||||
The model and administration increments supply persistence and Email/Push editing,
|
||||
while preserving existing email API behavior. The action increment adds push
|
||||
rendering/forwarding; Invemory producers and native action handling are still pending.
|
||||
|
||||
## Owner decision: action metadata — 2026-10-31
|
||||
|
||||
The template-data object has two properties: binding values and structured action
|
||||
metadata supplied by the producer. Actions belong to the individual template;
|
||||
there is no service-level action registry or registration endpoint. Template authors
|
||||
know the action keys. Notification Service validates structure without implementing
|
||||
consumer-specific behavior. The receiving application interprets actions and checks
|
||||
current access. Localized button labels belong to the template language version.
|
||||
The final owner-approved metadata is only an objectId; the dictionary key identifies
|
||||
the action. There is no action type field, arbitrary parameters, or translation-key
|
||||
registry. Authoring actions requires knowledge of the consuming application's keys.
|
||||
|
||||
The owner explicitly rejected legacy push-format compatibility branches. This is
|
||||
a new template format; existing Email behavior remains unchanged. Work proceeds
|
||||
through contract/editor/model, rendering/forwarding, then client handling coordinated
|
||||
with IT-1628. Saving or previewing buttons does not itself add native Android actions.
|
||||
Read/unread state and automatic business mutations are not introduced.
|
||||
|
||||
For subsequent owner-authorized local dev migrations, do not create backups, per
|
||||
the owner's explicit preference. This does not change production rollout rules.
|
||||
|
||||
## Storage details
|
||||
|
||||
- templates: shared identity, service ID, exact key and timestamps. The composite
|
||||
service/key alternate key is unique. Service deletion is restricted.
|
||||
- email_template_definitions: retained email channel identity, revision, variables,
|
||||
and translations. Its service/key now references the shared template.
|
||||
- push_template_definitions: independent channel identity, revision and variables;
|
||||
unique service/key referencing the same shared template. No email is required.
|
||||
- push_templates: language versions with plain-text title/body. Unique
|
||||
definition/language; composite foreign keys ensure that the definition and
|
||||
language belong to the same service. Content remains text because placeholders
|
||||
are rendered before the existing final-payload size limits are checked.
|
||||
|
||||
Each channel owns its variable contract, shared across that channel's languages.
|
||||
A missing channel has no definition row. An existing channel with no translations
|
||||
has a definition row and an empty version collection. The frontend must distinguish
|
||||
these states and must not create a channel merely by opening its tab.
|
||||
|
||||
## Compatibility and mutations
|
||||
|
||||
Existing email creation paths create or reuse the shared key under the existing
|
||||
transaction advisory lock 1601. Existing IDs, keys, revisions, variables and
|
||||
language content are unchanged by migration. Email APIs continue to expose only
|
||||
email definitions and translations. Creating email for an existing push-only key
|
||||
reuses that catalog entry.
|
||||
|
||||
Deleting an email definition preserves a matching push definition and its shared
|
||||
key. If no push channel exists, that compatibility operation also removes the
|
||||
now-unused catalog entry. Deleting an email translation retains its definition.
|
||||
Service deletion checks the shared catalog, including push-only keys.
|
||||
|
||||
Service language usage counts include both channels. Ordinary removal rejects any
|
||||
used language; explicitly confirmed deletion removes both channels' translations
|
||||
atomically and retains their definitions. Service/catalog edits refresh both
|
||||
channels' revisions, so editors cannot silently save against an outdated catalog.
|
||||
|
||||
## Migration and rollout
|
||||
|
||||
AddChannelTemplates creates the shared catalog from existing email definitions,
|
||||
retaining their IDs as the initial shared template IDs, and adds push tables and
|
||||
foreign keys. It does not touch the existing email content or transport payloads.
|
||||
No owner or production database is migrated as part of source implementation.
|
||||
|
||||
Stop administration writes and the migrator's development seeding while upgrading;
|
||||
apply the migration and run the matching Web/Migrator build before resuming writes.
|
||||
The previous Web binary cannot create email keys against the new foreign key.
|
||||
Existing email reads and sends remain compatible. Use the existing backup and
|
||||
explicit migration procedure in email-template-localizations.md.
|
||||
|
||||
Downgrade preserves existing email rows but refuses to discard push definitions,
|
||||
including empty ones. Export and explicitly remove push data before an intentional
|
||||
rollback; otherwise restore the verified backup with its matching application.
|
||||
|
||||
## Verification
|
||||
|
||||
ChannelTemplateModelTests uses disposable PostgreSQL databases to cover migration
|
||||
and rerun, independent channel contracts, email-only/push-only/combined keys,
|
||||
adding email to a push-only key, email deletion preserving push, language deletion,
|
||||
duplicate language rejection, cross-service language rejection, and rollback guards.
|
||||
Existing migration/API/worker tests provide email regression coverage.
|
||||
|
||||
## Administration increment — 2026-10-31
|
||||
|
||||
After separate owner authorization on 2026-10-31, AddChannelTemplates was applied
|
||||
to the local Development database at localhost:5433/notification_service using
|
||||
update-development-database.bat. A custom-format backup was taken first with Web
|
||||
stopped. Before/after data-only dumps confirmed unchanged services, service
|
||||
languages, email definitions and translations. All eight email definitions have
|
||||
shared catalog rows. The matching task Web build was restarted on localhost:5188;
|
||||
production was not accessed.
|
||||
|
||||
The service table has one row per exact key and separate Email/Push columns. Each
|
||||
channel shows its configured-language statuses or Not configured. Existing email
|
||||
URLs and key filters remain supported. The editor has service/key context followed
|
||||
by Email/Push navigation, then channel-specific variables and language tabs.
|
||||
|
||||
Channel navigation uses normal server-rendered links styled as tabs, with
|
||||
aria-current identifying the selected channel. Opening a missing channel shows a
|
||||
Create Email Template or Create Push Template action; GET requests never persist
|
||||
anything. The new-template page defaults to an unsaved Email form and can switch
|
||||
to Push before saving. A shared key is fixed when adding a channel to an existing
|
||||
key. A channel may retain zero translations.
|
||||
|
||||
Push editing supplies Title and Body, independent variables, exact catalog-language
|
||||
validation, revision checks and unknown/incomplete placeholder validation. Blank
|
||||
Title and Body omit/remove that version; a partial version rejects the whole save.
|
||||
Failed saves retain entered values, active language and original revision. Removing
|
||||
Push has a separate antiforgery-protected confirmation and preserves Email; Email
|
||||
confirmation explicitly states that Push is retained.
|
||||
|
||||
Both editors reuse the existing language keyboard behavior and sample-variable
|
||||
preview script. Interpolation is single-pass: sample values containing placeholders
|
||||
are not interpreted again. Push preview writes plain text; email HTML remains
|
||||
encoded and sandboxed. A modal Keep editing / Discard changes choice guards links
|
||||
when fields are dirty, including after validation errors. Language switching stays
|
||||
inside the page and retains entered values. Saving affects only the selected channel.
|
||||
The existing browser unload warning remains supplementary.
|
||||
|
||||
Validation on 2026-10-31: all 67 service/worker and 166 PostgreSQL integration
|
||||
tests passed. Chrome against a temporary host and disposable database covered
|
||||
creating Push first, adding Email under the same key, independent content and
|
||||
variables, validation with retained input, language keyboard navigation, preview,
|
||||
and keeping unsaved edits through the navigation dialog. Desktop 1440px and narrow
|
||||
390/320px layouts were checked; the table scrolls locally without page overflow.
|
||||
This was administration browser validation, not installed Android or end-to-end
|
||||
push delivery validation. Owner databases were not migrated. Cognitive complexity
|
||||
was structurally reviewed; numeric analyzer scores were not measured.
|
||||
|
||||
The table has a minimum readable width and scrolls within its wrapper at narrow
|
||||
widths. The shared stylesheet is fingerprinted to avoid cached pre-change layouts.
|
||||
No editor is added to the Invemory web or installed Android application.
|
||||
## Push template actions and rendering — 2026-10-31
|
||||
|
||||
`SendPushMessageData` now requires `templateKey`, `languageCode` and `templateData`
|
||||
instead of ready-made title/body/route. Delivery identity, expiry and registration
|
||||
fields remain. `templateData` has exactly two model properties:
|
||||
|
||||
```json
|
||||
{
|
||||
"bindings": { "Name": "Alex" },
|
||||
"actions": { "openNeed": { "objectId": "need-42" } }
|
||||
}
|
||||
```
|
||||
|
||||
Each language version stores `actions` as JSON, edited alongside title/body:
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultAction": "openNeed",
|
||||
"buttons": [{ "action": "openNeed", "label": "View need" }]
|
||||
}
|
||||
```
|
||||
|
||||
Labels belong to that language and may reference declared text bindings. The order
|
||||
of buttons is their display order. This first editor supports up to three buttons,
|
||||
unique action keys and nonempty labels up to 100 template characters. `{}` means
|
||||
no actions. Clear title/body and reset actions to `{}` to remove a translation.
|
||||
Missing action metadata fails the send; it never silently creates a dead button.
|
||||
Only actions selected in the template are forwarded. IDs are opaque strings;
|
||||
Notification Service neither constructs URLs nor interprets consumer actions.
|
||||
|
||||
The worker resolves the exact language then English, checks required bindings,
|
||||
substitutes once, and truncates title/body/label to 100/500/100 UTF-16 code units
|
||||
without splitting a text element. Missing optional bindings become empty text;
|
||||
empty final content fails. User-supplied placeholder text is not reinterpreted.
|
||||
FCM receives data payload version 2 with title/body and an `actions` JSON string
|
||||
containing the resolved default action and buttons (action, objectId, label).
|
||||
The serialized data dictionary is capped at 3500 UTF-8 bytes as a conservative
|
||||
application budget; oversized payloads fail before contacting Firebase.
|
||||
The RabbitMQ command type remains Notification.SendPush.v1; its payload contract
|
||||
is replaced directly, with no old-title/body acceptance branch per owner decision.
|
||||
|
||||
AddPushTemplateActions adds a JSONB column with `{}` for existing versions. It was
|
||||
applied to the local dev database without a backup, per owner instruction. Matching
|
||||
Web is running on localhost:5188. This is not a production deployment. Do not send
|
||||
this new contract from Invemory until its producers and installed client handling
|
||||
are updated together. Native version 2/action handling remains separate work;
|
||||
provider serialization tests do not demonstrate Android display or navigation.
|
||||
|
||||
Validation: 76 service/worker tests and 168 PostgreSQL integration tests passed.
|
||||
New tests cover language fallback, single-pass substitution, Unicode truncation,
|
||||
missing bindings/action IDs, malformed action definitions, forwarding and localized
|
||||
editor persistence/errors. Rider found no errors in inspected production files;
|
||||
remaining findings were style suggestions and the existing Firebase Token warning.
|
||||
Cognitive complexity was structurally reviewed, not numerically measured. Reuse
|
||||
review compared the existing email renderer and shared editor: push keeps a separate
|
||||
single-pass plain-text renderer because email uses HTML encoding and repeated
|
||||
replacement; channel editing, preview layout and revision handling are reused.
|
||||
|
||||
Browser validation against the updated local dev Web covered the Ukrainian action
|
||||
label and primary-action preview. The 390px layout kept page width within the
|
||||
viewport and the button remained visible and clickable as a preview-only control.
|
||||
Server-side invalid JSON retention and save/reload were verified by integration
|
||||
tests; an attempted browser submission lost its automation connection, so no
|
||||
successful browser save is claimed for this increment. No QA push template was
|
||||
persisted into the owner's catalog. No installed Android/real FCM send was tested.
|
||||
|
||||
## Compatibility increment verification — 2026-10-31
|
||||
|
||||
The complete solution passed 84 service/worker tests and 172 PostgreSQL integration
|
||||
checks. Added coverage deserializes original requests with omitted/explicit version,
|
||||
compares the seven-field legacy FCM envelope, proves legacy never queries templates,
|
||||
rejects unsupported versions before provider calls, renders section actions without
|
||||
objectId, and exercises both test-send formats through the antiforgery-protected
|
||||
admin endpoint using a recording Firebase boundary. The unchanged authorization
|
||||
and saved-credential behavior remain covered. No new migration is required.
|
||||
|
||||
Chrome against temporary host Web on 127.0.0.1:52658 verified default legacy mode,
|
||||
version-2 fields, server rejection of missing binding before Firebase access, token
|
||||
clearing, switching modes, and the scrollable 390px modal without horizontal page
|
||||
overflow. The host was stopped afterward; the owner's Rider Web on 5188 was left
|
||||
running. No real Firebase send was performed. No phone was connected through ADB,
|
||||
and the existing Android source accepts only version 1; device display/tap behavior
|
||||
for version 2 remains unverified and needs the separate client implementation.
|
||||
|
||||
Review reused existing version, sender, channel credential resolver, provider client,
|
||||
and admin test endpoint rather than introducing another channel type. Compared the
|
||||
legacy input validator and FCM builder against Git HEAD and the current Android
|
||||
allowlist. Rider reported no errors in inspected changed production files; warnings
|
||||
remain for existing Firebase Token deprecation, defensive JSON null checks and style.
|
||||
Cognitive complexity was reviewed structurally against the limit of 10; numeric
|
||||
scores were not measured. All changes remain uncommitted for owner review/testing.
|
||||
Reference in New Issue
Block a user