Spectrum Smoke (admin) — End-to-end dispatch test
Fires one notification per event in SPECTRUM_SMOKE_EVENTS (26 events default) to a single recipient. Useful after a template change to verify rendering + provider delivery end-to-end. Channel overrides force email-only by default; admin can opt into push / web-push for FCM credential testing.
POST /api/v1/admin/notifications/spectrum-smoke — 👤 Admin · permission notification:manage · Throttle: 5 req / hour
Triggers NotificationService.send for every event in SPECTRUM_SMOKE_EVENTS (or a body-supplied subset) against the user matching to. Each successful call writes a NotificationJob row through the normal pipeline — same template renderer, same provider adapters, same critical-event policy. Used after a Spectrum email template change to verify rendering + outbound delivery end-to-end without waiting for organic traffic.
Critical events bypass channel overrides. 6 events (security_alert, security_alert_token_reuse, account_locked, payout_processed, payout_failed, payout_critical_failure) re-enable email + in_app regardless of the channels override block. This is exact production behaviour by design — the smoke test exercises the live resolveChannels() policy. So when smoke-testing pushes, expect to also see in-app rows for those 6 events.
Default = email-only. When channels is omitted, the smoke dispatch forces { email: true, push: false, inApp: false, webPush: false, sms: false } so it never leaks push / in-app notification rows during a routine template smoke. Wave 2 client-web calls pass { push: true, webPush: true, email: false } to validate FCM credentials end-to-end.
3 events deferred. SPECTRUM_SMOKE_DEFERRED lists 3 events (payment_successful, marketing_campaign, ticket_resolved) without production dispatchers today. They are excluded from the default smoke list to avoid drowning the test in errors.
Request
Body — SpectrumSmokeDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
to | string | ✓ | IsEmail | Recipient email. Must match an existing User.email (lowercased + trimmed). Resolved to user.id for the dispatch. |
events | string[] | — | ArrayMaxSize(50), IsString({ each: true }) | Subset of SPECTRUM_SMOKE_EVENTS to dispatch. Omit for the full 26-event default list. Unknown keys (no fixture) come back with status: 'no-fixture'. |
locale | string | — | IsString | Locale override passed to the template renderer. Defaults to 'tr' so the recipient receives Turkish copy regardless of stored UserSettings.locale. |
channels | object | — | nested SpectrumSmokeChannelsDto | { email?, push?, inApp?, webPush?, sms? } booleans. Omitted flags default false. When the whole block is omitted, default = email-only. |
Throttle / permission
| Layer | Limit |
|---|---|
| Permission | notification:manage |
| Throttle | 5 req / hour |
| HTTP code | 200 |
Response
200 OK — ApiResponseOf<SpectrumSmokeResponseDto>
{
"success": true,
"data": {
"to": "[email protected]",
"userId": "usr-uuid-here",
"count": 26,
"queued": 24,
"errors": 1,
"results": [
{ "eventKey": "email_verification", "status": "queued" },
{ "eventKey": "password_reset", "status": "queued" },
{ "eventKey": "marketing_campaign", "status": "no-fixture" },
{ "eventKey": "kyc_result", "status": "error", "message": "Template render failed: missing variable" }
]
}
}Top-level fields
| Field | Type | Notes |
|---|---|---|
to | string | Echo of the input email |
userId | string (UUID) | Resolved User.id for the recipient |
count | number | Total events attempted (length of resolved events list) |
queued | number | Events where NotificationService.send resolved (job row written) |
errors | number | Events where send threw |
results | array | Per-event status breakdown |
Per-event result item
| Field | Type | Notes |
|---|---|---|
eventKey | string | The event the smoke attempted |
status | enum | queued (success), no-fixture (event has no entry in SPECTRUM_SMOKE_FIXTURES), error (send threw) |
message | string | Present only when status === 'error' — error message from the thrown exception |
Errors
| HTTP | Reason |
|---|---|
400 | Invalid payload (bad email shape, events over 50 entries, etc.) |
401 | Missing / invalid admin session |
403 | Permission denied (notification:manage required) |
404 | No User row found matching the to email |
429 | Throttle exceeded (5 req / hour) |
Side effects
- Lookup
Userby lowercasedtoemail. 404 if missing. - Resolve
events— body subset or the 26-event default. - Compute
channelOverrides— body'schannelsblock or{ email: true, push: false, inApp: false, webPush: false, sms: false }. - For each event:
- Skip with
status: 'no-fixture'ifSPECTRUM_SMOKE_FIXTURES[eventKey]is missing. - Call
NotificationService.send({ eventKey, userId, variables: fixtures[eventKey], locale, channelOverrides, entityRef: { type: 'spectrum_smoke', id: '${eventKey}:${Date.now()}' } }). entityRefsynthesized from timestamp so successive smoke runs for the same event produce distinctdedupeKeys.- On success →
status: 'queued'. On throw →status: 'error'withmessage.
- Skip with
- Log a single summary line with masked email + counts (PII-safe).
Code samples
# Full smoke (26 events, email only, tr locale)
curl -X POST 'https://api.bio.re/api/v1/admin/notifications/spectrum-smoke' \
-H 'Content-Type: application/json' \
-H 'Cookie: admin_session=...' \
-H 'X-CSRF-Token: ...' \
-d '{ "to": "[email protected]" }'
# Push-only smoke (FCM credential validation)
curl -X POST 'https://api.bio.re/api/v1/admin/notifications/spectrum-smoke' \
-H 'Content-Type: application/json' \
-H 'Cookie: admin_session=...' \
-H 'X-CSRF-Token: ...' \
-d '{ "to": "[email protected]", "channels": { "push": true, "webPush": true, "email": false } }'
# Two-event subset, English locale
curl -X POST 'https://api.bio.re/api/v1/admin/notifications/spectrum-smoke' \
-H 'Content-Type: application/json' \
-H 'Cookie: admin_session=...' \
-H 'X-CSRF-Token: ...' \
-d '{ "to": "[email protected]", "events": ["email_verification", "password_reset"], "locale": "en" }'type SpectrumSmokeResult = {
to: string;
userId: string;
count: number;
queued: number;
errors: number;
results: Array<{
eventKey: string;
status: 'queued' | 'no-fixture' | 'error';
message?: string;
}>;
};
async function spectrumSmoke(
to: string,
opts: { events?: string[]; locale?: string; channels?: Record<string, boolean> } = {},
): Promise<SpectrumSmokeResult> {
const res = await fetch('https://api.bio.re/api/v1/admin/notifications/spectrum-smoke', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ to, ...opts }),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Smoke failed'), {
code: json?.error?.code,
});
}
return json.data;
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/notification/admin-notification.controller.ts | 607–626 (spectrumSmoke) |
| Request DTO | apps/api-core/src/modules/notification/dto/spectrum-smoke.dto.ts | 44–89 (SpectrumSmokeDto), 10–35 (SpectrumSmokeChannelsDto) |
| Response DTO | apps/api-core/src/modules/notification/dto/spectrum-smoke.dto.ts | 94–131 (SpectrumSmokeResultItemDto, SpectrumSmokeResponseDto) |
| Service | apps/api-core/src/modules/notification/admin-notification.service.ts | 1798–1883 (spectrumSmoke) |
| Smoke event list | apps/api-core/src/modules/notification/spectrum-smoke-fixtures.ts | 39–73 (SPECTRUM_SMOKE_EVENTS, 26 entries) |
| Deferred events | apps/api-core/src/modules/notification/spectrum-smoke-fixtures.ts | 80–84 (SPECTRUM_SMOKE_DEFERRED, 3 entries) |
| Variable fixtures | apps/api-core/src/modules/notification/spectrum-smoke-fixtures.ts | 97+ (SPECTRUM_SMOKE_FIXTURES map) |
| Prisma model | packages/prisma/prisma/schema.prisma | NotificationJob (2203) — every smoke send writes a row |
| Live response | NOT verified — sourced from DTO at dto/spectrum-smoke.dto.ts:113–131 |
Broadcast (admin) — Submit, queue, approve, reject
Four admin endpoints for sending platform-wide or segmented broadcasts (e.g. system_maintenance, marketing_campaign). Behind a two-person approval gate in production. Submit → pending queue → second admin approves or rejects.
SMS Cost Log + Email Cost Estimate (admin)
Three finance-facing endpoints for tracking outbound notification spend. SmsCostLog provides per-message Twilio/Vonage rows; the summary endpoint groups them by country / provider / event / day. Email cost is computed analytically from NotificationEvent counts × per-1k contract rate.