Files
hrynco-notification-service/docs/channel-template-model.md
T
agrynco ed4293d852 fix(admin): filter template keys by substring
Match both channels case-insensitively while keeping API and delivery lookups exact.

Ref: IT-1627
2026-10-31 10:14:48 +02:00

355 lines
21 KiB
Markdown

# 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.
## Push template management REST API (IT-1627, 2026-10-31)
Push definitions now have the same service-scoped management operations as Email:
| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/api/v1/services/{serviceId}/push-template-definitions?key={key}` | Read one exact key |
| GET | `/api/v1/services/{serviceId}/push-template-definitions/all` | List Push definitions |
| POST | `/api/v1/services/{serviceId}/push-template-definitions/upsert?key={key}` | Create or atomically patch a definition |
| DELETE | `/api/v1/services/{serviceId}/push-template-definitions?key={key}&revision={revision}` | Delete only Push, preserving Email |
Requests and responses use camelCase and the existing `success/data/error` envelope.
Create with omitted/null `revision`; an existing key returns 409. Update with the
last read nonempty revision; stale/missing definitions return 409. Successful
creation returns 201 with Location; update/read/delete return 200. Unknown services
or absent reads/deletes return 404; invalid requests return 400.
Example creation body:
```json
{
"revision": null,
"variables": [{ "name": "ActorName", "required": true }],
"translations": [{
"languageCode": "uk",
"title": "Довірені контакти",
"body": "{{ActorName}} додав вас до довірених контактів.",
"actions": {
"defaultAction": "contacts",
"buttons": [{ "action": "contacts", "label": "Перейти в розділ" }]
}
}],
"removeLanguageCodes": []
}
```
The action key above illustrates a producer/client agreement; this endpoint neither
registers actions nor activates any Invemory producer. Object IDs are per-message
metadata, not template content. `actions` is a structured object, not an escaped JSON
string. Omission means no actions for a supplied translation; explicit null is invalid.
`variables` and `translations` arrays are required (empty is valid). Variables replace
the channel-wide contract. Supplied translations replace their full content, including
actions. Omitted translations remain; only `removeLanguageCodes` removes versions.
Removing every language retains the definition and variables. Codes exactly match the
service catalog; duplicate/unknown codes and simultaneous replace/remove are rejected.
The final merged definition is validated, including retained translations' placeholders,
so a variable change cannot invalidate an omitted translation. Invalid/stale requests
leave content and revision unchanged. The existing transaction/advisory lock covers
catalog checks, merge and revision-guarded persistence. API and administration share
validation and storage, including explicit EF insertion when adding a new language.
Existing Email endpoints remain unchanged. Both channels can independently use the
same service/key. No migration, new registry, access boundary or Android change is
needed for these management endpoints. Keep them within the existing private
Notification Service administration network boundary.
Validation of the management API increment: 84 service/unit and 187 PostgreSQL
integration tests passed (271 total). After review cleanup, all 29 focused Push API,
editor and model tests passed again. Coverage includes parallel create/update conflicts,
atomic invalid writes, retained translations, structured localized actions, explicit
language removal and Email preservation. Rider reported no errors in inspected files;
manual structural complexity review was performed, with no numeric score recorded.
Reuse review compared Email definition controllers/handlers, API validation/envelopes,
TemplateMutationRunner and the existing Push editor/storage path.
A separate local Web launch for additional browser validation was rejected by the
automatic execution policy without further reason. No standalone-browser or installed
Android validation is claimed for this increment; API requests were exercised through
the ASP.NET integration host with real PostgreSQL. Changes await owner testing and
remain uncommitted; the previously delivered service does not yet include these endpoints.
Owner follow-up: authorized code delivery and another Kubernetes deployment after
verification, and authorized stopping the Rider Web process. The Rider process was
stopped. A second background Web launch was rejected by the execution tool with only
`blocked by policy`; this is an execution-tool restriction, not a request for more
owner permission. The automated API/PostgreSQL checks remain available.
## Administration key substring filter (2026-10-31)
The service Templates page filters both Email and Push definitions by a literal,
case-insensitive substring of Template Key. Leading/trailing search whitespace is
ignored; empty/whitespace-only input and Clear show the full service catalog.
For example, `contact` matches `TrustedContactAdded` and `trusted_contact_removed`.
`%` and `_` are ordinary characters, not SQL wildcard patterns. The filter stays in
navigation links as before. Exact template identity, management API reads and delivery
lookups are unchanged; substring matching is limited to this administration list.