feat(12-04): tender-radar notification-pref service + routes, instantAlert passthrough
- TenderNotificationPrefService: per-user digestInterval CRUD (default
'daily', upsert on @@unique userId, D-01/D-03)
- UpdateNotificationPrefDto: @IsIn(['daily','weekly','off']) validation (V5)
- GET/PUT /modules/tender-radar/notification-pref, declared before
@Get(':id') (route-order pitfall)
- instantAlert passthrough in Create/UpdateSavedSearchDto and
TenderSavedSearchService.create/update (NOTIFY-02, D-04)
- All pref/profile routes scoped strictly via extractTriageContext(req),
never from body/query (T-12-14, IDOR)
- Updated tenders.controller.spec.ts fakes for the new constructor param
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
/**
|
||||
* Service for managing the per-user Tender digest interval preference
|
||||
* (NOTIFY-01, D-01/D-03).
|
||||
*
|
||||
* Access control (T-12-14 / V4 — IDOR): scoped by userId exactly like
|
||||
* TenderSavedSearchService/TenderTriageService (T-11-14/T-08-06) — NOT
|
||||
* forTenant()/RLS (Pitfall 4). userId must always be derived from the
|
||||
* caller's auth context (controller), never accepted as a body/query
|
||||
* parameter here.
|
||||
*
|
||||
* `TenderNotificationPref` has a per-user `@@unique` on `userId` (one row
|
||||
* per user, D-03: the interval is a user setting, not per-profile) — this
|
||||
* service upserts on that key.
|
||||
*/
|
||||
@Injectable()
|
||||
export class TenderNotificationPrefService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Returns this user's digest interval preference. When no row exists yet
|
||||
* (user has never touched the setting), returns the default
|
||||
* `{ digestInterval: 'daily' }` (D-01) WITHOUT writing a row — consistent
|
||||
* with the digest scheduler's own default-daily due-check semantics, no
|
||||
* autowrite needed to represent "using the default".
|
||||
*/
|
||||
async getForUser(userId: string): Promise<{ digestInterval: string }> {
|
||||
const existing = await this.prisma.tenderNotificationPref.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return { digestInterval: 'daily' };
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upserts this user's digest interval preference on the @@unique userId
|
||||
* (D-03) — a second call for the same user updates the same row rather
|
||||
* than creating a new one.
|
||||
*/
|
||||
async setForUser(userId: string, tenantId: string, digestInterval: string) {
|
||||
return this.prisma.tenderNotificationPref.upsert({
|
||||
where: { userId },
|
||||
create: { userId, tenantId, digestInterval },
|
||||
update: { digestInterval },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user