ed4293d852
Match both channels case-insensitively while keeping API and delivery lookups exact. Ref: IT-1627
355 lines
21 KiB
Markdown
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.
|