Fase 1a: microservicio tcef-notifications (workers FCM v1 + email)
Sustituye el envío de push por la API legacy de GCM (muerta desde jun-2024) y saca el envío del observer de Meteor a una cola BullMQ controlada. - poller: consume 'notifications' pendientes (campos correctos; el cron viejo tenía un typo nofitied/emailNofitied que nunca casaba) - fcm-worker: firebase-admin (FCM HTTP v1), payload compatible con la app Flutter (FLUTTER_NOTIFICATION_CLICK, collapseKey=_id, data); purga tokens muertos sin reintentar - email-worker: nodemailer + plantillas new-fire portadas verbatim - idempotencia persistente: notification_sends, indice unico (notificationId, channel) -> sobrevive reinicios y replays - salvaguardas anti-spam: modos dry-run/shadow/canary/full, exclusion mutua por canal (OWNED_CHANNELS + notifDisabledChannels en Meteor), kill switch, limites por usuario/dia y circuit breaker por batch - docs/legacy-behavior.md (caracterizacion) + docs/rollout.md - 47 tests (vitest + mongodb-memory-server), typecheck y build OK - deploy: systemd + pm2 (Node 22 standalone) Falta el service account de Firebase para envio real de push (pedir al usuario).
This commit is contained in:
commit
d20e168c90
39 changed files with 8378 additions and 0 deletions
57
src/types.ts
Normal file
57
src/types.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { ObjectId } from 'mongodb';
|
||||
|
||||
/** A `notifications` document, as written by node-red (see docs/legacy-behavior.md). */
|
||||
export interface NotificationDoc {
|
||||
_id: ObjectId;
|
||||
userId: string;
|
||||
subsId?: ObjectId;
|
||||
content: string;
|
||||
geo: { type: 'Point'; coordinates: [number, number] }; // [lng, lat]
|
||||
type: 'mobile' | 'web';
|
||||
notified?: boolean;
|
||||
notifiedAt?: Date;
|
||||
emailNotified?: boolean;
|
||||
emailNotifiedAt?: Date;
|
||||
when: Date;
|
||||
sealed: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
// provenance set by our poller (invisible to the old system)
|
||||
claimedBy?: string;
|
||||
claimedAt?: Date;
|
||||
}
|
||||
|
||||
export interface UserDoc {
|
||||
_id: string;
|
||||
lang?: string;
|
||||
fireBaseToken?: string | null;
|
||||
fireBaseTokenDeadAt?: Date;
|
||||
profile?: { name?: { first?: string } };
|
||||
emails?: Array<{ address: string; verified?: boolean }>;
|
||||
services?: { password?: { bcrypt?: string } };
|
||||
}
|
||||
|
||||
export type Channel = 'fcm' | 'email';
|
||||
|
||||
/** Maps a notification `type` to a channel. */
|
||||
export function channelOfType(type: NotificationDoc['type']): Channel {
|
||||
return type === 'mobile' ? 'fcm' : 'email';
|
||||
}
|
||||
|
||||
/** Record in `notification_sends` — persistent idempotency (unique on notificationId+channel). */
|
||||
export interface SendRecord {
|
||||
notificationId: ObjectId;
|
||||
channel: Channel;
|
||||
status: 'sending' | 'sent' | 'skipped' | 'failed' | 'dry-run' | 'dead-token' | 'no-recipient';
|
||||
mode: OperatingMode;
|
||||
detail?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export type OperatingMode = 'dry-run' | 'shadow' | 'canary' | 'full';
|
||||
|
||||
/** A queued send job (payload persisted in Redis by BullMQ). */
|
||||
export interface SendJob {
|
||||
notificationId: string; // ObjectId hex
|
||||
channel: Channel;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue