feat(push): add service-owned firebase channels and delivery

Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation.

Ref: IT-1074
This commit is contained in:
2026-10-24 12:31:35 +03:00
parent b30dba93e8
commit 1631786ce2
105 changed files with 3838 additions and 147 deletions
+9 -4
View File
@@ -21,7 +21,7 @@ A variable is `{ "name": "Name", "required": true }`. A translation contains `id
`POST /api/v1/services` accepts `{ "name": "ExampleService", "languages": [{ "code": "en" }, { "code": "uk" }] }`. It returns 201 with a Location to the service read and the complete service DTO. Names are exact and unique. The required languages array may be empty; new services cannot reference persisted language IDs.
`PUT /api/v1/services/{serviceId}` accepts the last-read nonempty `revision`, `name`, and a required full `languages` array. Retain each existing language's ID from the read DTO; omit/empty the ID for a new code. The returned service DTO includes the new revision and generated language IDs. Omitted unused catalog rows are removed; omitting a used language returns 409 with no changes. A stale revision returns 409; a missing service returns 404. Existing migrated nonstandard codes remain valid unchanged, and the existing stricter syntax validation applies only to added/renamed codes. Renames do not rewrite independently configured channel service names.
`PUT /api/v1/services/{serviceId}` accepts the last-read nonempty `revision`, `name`, and a required full `languages` array. Retain each existing language's ID from the read DTO; omit/empty the ID for a new code. The returned service DTO includes the new revision and generated language IDs. Omitted unused catalog rows are removed; omitting a used language returns 409 with no changes. A stale revision returns 409; a missing service returns 404. Existing migrated nonstandard codes remain valid unchanged, and the existing stricter syntax validation applies only to added/renamed codes. Email channels reference the stable service ID; their displayed service name updates immediately after a rename. Publishers must use the current name.
`DELETE /api/v1/services/{serviceId}/languages/{languageId}?revision={serviceRevision}` removes an unused catalog row only. Used languages return 409; use the explicit destructive endpoint when the versions should be deleted:
@@ -40,7 +40,7 @@ All three body properties are required. The nonnegative count and service revisi
- `DELETE /api/v1/services/{serviceId}/email-template-definitions?key={encodedKey}&revision={lastReadDefinitionRevision}` removes the selected definition and all of its versions atomically.
- `DELETE /api/v1/services/{serviceId}?revision={lastReadServiceRevision}` requires **every definition** to have been removed first, including zero-version definitions; otherwise it returns 409. It removes the remaining catalog languages and service atomically.
Both endpoints return HTTP 200 with the existing `ApiResponse<Unit>` success envelope (`data: {}`). A missing resource returns 404, missing/empty/malformed revision or invalid key returns 400, and stale revision returns 409. Every check and mutation runs under the common transaction advisory lock. Concurrent definition creation cannot race past the service's nonempty check. These operations do not remove email channels, whose service name is a separate identifier without a service foreign key.
Both endpoints return HTTP 200 with the existing `ApiResponse<Unit>` success envelope (`data: {}`). A missing resource returns 404, missing/empty/malformed revision or invalid key returns 400, and stale revision returns 409. Every check and mutation runs under the common transaction advisory lock. Concurrent definition creation cannot race past the service's nonempty check. These operations do not remove email channels. A service with linked channels returns 409; reassign or explicitly remove its channels before deleting it. The foreign key restricts deletion.
## Atomic upsert
@@ -77,10 +77,12 @@ In the admin channel editor, **Send Test Email** tests the form's current values
Saving an existing channel follows the same password rule: a blank password keeps the saved value when host, port, username, and SSL/TLS mode are unchanged, so sender name/address and limit edits do not require re-entering it. A changed SMTP connection needs an explicitly entered password. Creating a channel in the admin editor still requires a password. Validation errors redisplay the editor without rendering the saved or submitted password.
- `GET /api/v1/email-channels/all` enumerates **all** channels, including channels whose exact `serviceName` has no service row.
- `GET /api/v1/email-channels/all` enumerates all channels, each linked to an existing service by its UUID `serviceId`. Read DTOs include `serviceId` and the current `serviceName`.
- Existing `GET /api/v1/email-channels?serviceName={exactName}`, GET by ID, POST, PUT by ID and DELETE by ID remain available. Create returns 201/Location and its existing GUID response; update/delete return 200/Unit.
- Create supports SMTP (`channelType: 1`). Request/response `settings` explicitly maps `host`, `port`, `username`, `password`, `useSsl`, `fromEmail`, and `fromName`; reads no longer lose derived SMTP fields through an abstract JSON type. Host/fromEmail and a valid port are required. The optional empty username/password support unauthenticated development SMTP.
- Existing PUT behavior is retained: it changes settings, priority, limits, warning threshold and active state, **not** serviceName or channel type. Service names have no service FK and no rename cascade. Read all channels separately when performing an explicitly authorized cleanup; deleting a service does not delete its channels.
- POST and PUT require a nonempty `serviceId` belonging to an existing service; a missing, empty or unknown ID returns 400/InvalidRequest. A raw `serviceName` cannot create or reassign a channel. PUT changes service assignment, settings, priority, limits, warning threshold and active state, retaining channel ID/type and usage. Create/Edit offer the same existing-services dropdown, including editable assignment on Edit; validation errors reload it. A blank password on reassignment preserves the saved value if the SMTP connection is unchanged.
- RabbitMQ `SendEmailMessageData.ServiceName` is unchanged. The worker resolves the exact current name to a service UUID and chooses active channels by that ID and priority. The filtered GET resolves the same way (an unknown name returns an empty list). The migration-only legacy alias is not a runtime alias.
- See [email channel service migration](email-channel-service-link.md) for backfill, failure handling and rollout compatibility.
- SMTP HTTP lifecycle tests cover create/read/update/filter/all/delete. The repository ID lookup is a no-tracking domain projection so an update can attach its mapped entity without an EF tracking conflict. No SMTP send or owner-channel cleanup is implied by these tests.
## Responses
@@ -111,3 +113,6 @@ The revision is an optimistic-concurrency token, not a historical snapshot or re
Disposable PostgreSQL integration tests cover service lookup/rename, whole-definition reads, create/update, required arrays/boolean presence, exact codes, explicit removals, empty definitions, retention of omitted versions and their IDs/content/timestamps, shared contract replacement, invalid-save atomicity, duplicate/stale conflicts, concurrent requests, encoded keys and creation-Location round trips, and legacy-write revision invalidation. The full existing suite remains the regression gate for administration, migrations and the worker. Separate local HTTP checks exercise the host Web against task-owned infrastructure; no owner database or running Rider environment is used. Deployment/tunnel/Kubernetes validation is not implied by these tests.
On 2026-10-17 the solution build and all 148 tests passed after the REST CRUD, 201/Location and administration follow-ups (32 service/worker tests and 116 PostgreSQL integration tests, including 29 new definition-API cases and 3 administration-navigation checks). A separate host Web at `127.0.0.1:52656`, backed by the task-owned PostgreSQL container on port `58519`, passed service reads, exact encoded-key round trips (slash/literal `%2F`/Unicode), 201-created Location round trips and 200-updates, omitted-version retention, duplicate/stale conflicts, required-boolean rejection with no changes, final-version removal retaining the definition, required-key errors, and exact-name case checks. Its OpenAPI document exposed the typed response schemas and required request fields. The task Web and disposable database were stopped/removed afterward. No owner database, containers or Rider process were modified. Existing Microsoft.OpenApi NU1903 dependency warnings remain.
Push channels also reference the service by ID and block service deletion while linked.
Reassign the push channel first; see [Push Channels](android-push.md).