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).
57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
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;
|
|
}
|