| 12-tender-notifications |
03 |
api |
| nestjs |
| prisma |
| tender-radar |
| notifications |
| instant-alert |
| tdd |
|
| phase |
provides |
| 12-01 |
TenderMatch model (single nullable notifiedAt eligibility gate) + TenderMatchingService.matchDelta |
|
| phase |
provides |
| 12-02 |
TenderMailService.sendInstant (already implemented, no-throw boolean signal) + TenderDigestScheduler reading the same notifiedAt gate |
|
|
| TenderMatchingService.matchDelta — instant-alert dispatch at end of the poll tick for instantAlert=true profiles |
| Per-profile bundled instant mail (D-05): all of a profile's freshly-matched tenders in one tick become ONE sendInstant call |
| tender-notifications.integration.spec.ts — proves instant+digest together always send exactly one mail (NOTIFY-03 core invariant, D-06) |
|
| 12-04-tender-notification-settings-ui |
|
| added |
patterns |
|
|
| Instant dispatch runs AFTER all match upserts of the tick, filtering the same savedSearches list already loaded for matching (no second tenderSavedSearch.findMany) |
| notifiedAt/notifiedChannel='instant' stamped ONLY on a successful (non-skipped, non-thrown) sendInstant — the exact same gate the digest reads, so the two channels can never double-notify a pair |
| Per-profile try/catch around instant dispatch — a send failure/thrown error for one profile never aborts the tick or the remaining profiles' dispatch |
|
|
| created |
modified |
| apps/api/src/tenders/tender-notifications.integration.spec.ts |
|
| apps/api/src/tenders/tender-matching.service.ts |
| apps/api/src/tenders/tender-matching.service.spec.ts |
|
|
| Instant dispatch filters the savedSearches array already loaded at the top of matchDelta (instantAlert===true) rather than a second DB query — one findMany covers both matching and instant-eligibility |
| TenderMailService injected as a second constructor parameter (Nest DI resolves it automatically via the existing tenders.module.ts provider registration — no module change needed, exactly as 12-02's SUMMARY anticipated) |
| A profile with instantAlert=true but zero fresh (notifiedAt=NULL) matches this tick is a silent no-op (no sendInstant call) — checked before the user lookup to avoid an unnecessary query |
|
| Shared hand-rolled Prisma fake across matchDelta and runDigest in the integration spec: one in-memory Map-backed tenderMatch store lets both services' real state transitions (upsert -> instant stamp -> digest candidate exclusion) be asserted end-to-end without a live DB |
|
|
| id |
description |
requirement |
verification |
human_judgment |
| D1 |
matchDelta dispatches instant alerts only for instantAlert=true profiles (D-04); instantAlert=false profiles never trigger sendInstant |
NOTIFY-02 |
| kind |
ref |
status |
| unit |
apps/api/src/tenders/tender-matching.service.spec.ts#Instant-Dispatch nur instantAlert=true (D-04) |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D2 |
Multiple freshly-matched tenders of the same profile in one tick bundle into exactly ONE sendInstant call (D-05), never one mail per match |
NOTIFY-02 |
| kind |
ref |
status |
| unit |
apps/api/src/tenders/tender-matching.service.spec.ts#Bündelung pro Profil/Tick (D-05) |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D3 |
notifiedAt/notifiedChannel='instant' stamped only after a successful send; a send failure/thrown error leaves notifiedAt NULL for that profile and does not abort the tick for other profiles |
NOTIFY-03 |
| kind |
ref |
status |
| unit |
apps/api/src/tenders/tender-matching.service.spec.ts#Stempelung nach Erfolg (D-06) / Retry-Sicherheit (Robustheit) |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D4 |
An instantAlert=true profile with no fresh notifiedAt=NULL matches this tick never calls sendInstant (no spurious mail) |
NOTIFY-02 |
| kind |
ref |
status |
| unit |
apps/api/src/tenders/tender-matching.service.spec.ts#nichts Neues löst keinen Alert aus |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D5 |
Cross-channel single-email guarantee: instantAlert=true yields sendInstant=1/sendDigest=0; instantAlert=false yields sendInstant=0/sendDigest=1 — never both, never zero |
NOTIFY-03 |
| kind |
ref |
status |
| integration |
apps/api/src/tenders/tender-notifications.integration.spec.ts (both scenarios) |
pass |
|
|
false |
|
| id |
description |
verification |
human_judgment |
rationale |
| D6 |
Actual SMTP delivery of a real instant-alert email against a live mailbox (Mailhog), and true multi-tenant instant-alert isolation with two real users/tenants |
|
true |
Real mail delivery cannot be observed with a mocked nodemailer transport (TenderMailService is unit-tested with mocks in 12-02); needs a live SMTP relay and real per-tenant SmtpConfig rows — deferred to the phase-end UAT pass per 12-VALIDATION.md Manual-Only Verifications, consistent with 12-01/12-02's precedent. |
|
|
~15min |
2026-07-22 |
complete |