From 73b07c2c3aac601c44d5f1e3691e0cf5340cdf87 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 22 Mar 2026 01:14:55 -0700 Subject: [PATCH] fix(platform-service): 5 bugs in recent P2/P3 implementations - diagnostics/subscribers: use correct template IDs 'diagnostics-session-cancelled' and 'diagnostics-session-completed' instead of non-existent 'generic' (would throw at runtime) - delivery/templates: add missing 'broadcast' email template used by broadcast delivery route (dispatchEmail would throw on unknown ID) - broadcasts/routes: replace broken dot-path 'metrics.sent' update with proper updateBroadcastMetrics() call, add productName variable - exports/routes: store serialized data on job doc, add download endpoint GET /exports/:id/download with content-type headers, exclude data payload from metadata GET endpoint - waitlist/routes: store invitation doc ID (inv_...) instead of code string (WL-...) in invitationCodeId field - delivery/delivery.test.ts: update template count 12 -> 13 - Typecheck clean, 1483/1483 tests pass --- .../src/modules/broadcasts/routes.ts | 9 +++-- .../src/modules/delivery/delivery.test.ts | 4 +-- .../src/modules/delivery/templates.ts | 13 +++++++ .../src/modules/diagnostics/subscribers.ts | 36 +++++++++++-------- .../src/modules/exports/routes.ts | 23 ++++++++++-- .../src/modules/exports/types.ts | 1 + .../src/modules/waitlist/routes.ts | 2 +- 7 files changed, 65 insertions(+), 23 deletions(-) diff --git a/services/platform-service/src/modules/broadcasts/routes.ts b/services/platform-service/src/modules/broadcasts/routes.ts index 2cf0236c..b734a1e9 100644 --- a/services/platform-service/src/modules/broadcasts/routes.ts +++ b/services/platform-service/src/modules/broadcasts/routes.ts @@ -225,6 +225,7 @@ async function adminRoutes(app: FastifyInstance): Promise { displayName: user.displayName, subject: broadcast.title, body: broadcast.body ?? '', + productName: productId, }, productId, userId: user.id, @@ -242,9 +243,11 @@ async function adminRoutes(app: FastifyInstance): Promise { await repo.updateBroadcast(id, productId, { status: BroadcastStatus.SENT, - 'metrics.sent': sent, - 'metrics.targetCount': recipients.length, - } as Record); + }); + await repo.updateBroadcastMetrics(id, productId, { + sentCount: sent, + targetedCount: recipients.length, + }); log.info( { broadcastId: id, sent, total: recipients.length }, '[broadcasts] Delivery complete' diff --git a/services/platform-service/src/modules/delivery/delivery.test.ts b/services/platform-service/src/modules/delivery/delivery.test.ts index 9e00fa3e..cf409e21 100644 --- a/services/platform-service/src/modules/delivery/delivery.test.ts +++ b/services/platform-service/src/modules/delivery/delivery.test.ts @@ -123,8 +123,8 @@ describe('SendSlackSchema', () => { // ── Template Tests ─────────────────────────────────────────── describe('templates', () => { - it('should have 12 built-in templates', () => { - expect(BUILT_IN_TEMPLATES.length).toBe(12); + it('should have 13 built-in templates', () => { + expect(BUILT_IN_TEMPLATES.length).toBe(13); }); it('should find template by ID', () => { diff --git a/services/platform-service/src/modules/delivery/templates.ts b/services/platform-service/src/modules/delivery/templates.ts index f28c1ba0..d10eda51 100644 --- a/services/platform-service/src/modules/delivery/templates.ts +++ b/services/platform-service/src/modules/delivery/templates.ts @@ -122,6 +122,19 @@ export const BUILT_IN_TEMPLATES: EmailTemplate[] = [ bodyText: `License expires in {{daysLeft}} days.\n\nRenew: {{renewUrl}}\n\n— The {{productName}} Team`, variables: ['displayName', 'productName', 'daysLeft', 'renewUrl'], }, + // ── Broadcast Template ────────────────────────────────────────── + { + id: 'broadcast', + name: 'Broadcast Message', + subject: '{{subject}}', + bodyHtml: ` +

Hi {{displayName}},

+ {{body}} +

— The {{productName}} Team

+ `.trim(), + bodyText: `Hi {{displayName}},\n\n{{body}}\n\n— The {{productName}} Team`, + variables: ['displayName', 'subject', 'body', 'productName'], + }, // ── Diagnostics Templates ───────────────────────────────────── { id: 'diagnostics-session-created', diff --git a/services/platform-service/src/modules/diagnostics/subscribers.ts b/services/platform-service/src/modules/diagnostics/subscribers.ts index 2308842c..5e19f949 100644 --- a/services/platform-service/src/modules/diagnostics/subscribers.ts +++ b/services/platform-service/src/modules/diagnostics/subscribers.ts @@ -132,11 +132,14 @@ export function registerDiagnosticsSubscribers( await dispatchEmail( { to: admin.email, - templateId: 'generic', + templateId: 'diagnostics-session-cancelled', variables: { - displayName: admin.displayName, - subject: 'Debug session cancelled', - body: `Your debug session (${event.payload.sessionId}) was cancelled${event.payload.reason ? `: ${event.payload.reason}` : '.'}.`, + productName: event.payload.productId, + sessionId: event.payload.sessionId, + targetUserId: session.targetUserId ?? 'unknown', + cancelledBy: event.payload.cancelledBy, + reason: event.payload.reason ?? 'No reason provided', + cancelledAt: new Date().toISOString(), }, productId: event.payload.productId, userId: session.createdBy, @@ -185,21 +188,24 @@ export function registerDiagnosticsSubscribers( const admin = await getUserById(session.createdBy); if (admin) { const stats = event.payload.stats; - const summary = [ - `Debug session ${event.payload.sessionId} completed.`, - `Logs collected: ${stats.logCount}`, - `Traces collected: ${stats.traceCount}`, - `Screenshots captured: ${stats.screenshotCount}`, - `Ended at: ${event.payload.endedAt}`, - ].join('\n'); + const startedAt = session.startedAt ?? session.createdAt; + const endedAt = event.payload.endedAt; + const durationMs = + startedAt && endedAt + ? new Date(endedAt).getTime() - new Date(startedAt).getTime() + : 0; + const durationMinutes = String(Math.round(durationMs / 60_000)); await dispatchEmail( { to: admin.email, - templateId: 'generic', + templateId: 'diagnostics-session-completed', variables: { - displayName: admin.displayName, - subject: 'Debug session completed — summary', - body: summary, + productName: event.payload.productId, + sessionId: event.payload.sessionId, + durationMinutes, + logCount: String(stats.logCount), + traceCount: String(stats.traceCount), + screenshotCount: String(stats.screenshotCount), }, productId: event.payload.productId, userId: session.createdBy, diff --git a/services/platform-service/src/modules/exports/routes.ts b/services/platform-service/src/modules/exports/routes.ts index 04475765..b2ddb59f 100644 --- a/services/platform-service/src/modules/exports/routes.ts +++ b/services/platform-service/src/modules/exports/routes.ts @@ -75,6 +75,7 @@ export async function exportRoutes(app: FastifyInstance) { await repo.updateExportJob({ ...created, status: 'ready', + data: serialized, rowCount: rows.length, fileSizeBytes: Buffer.byteLength(serialized, 'utf8'), fileName, @@ -106,13 +107,31 @@ export async function exportRoutes(app: FastifyInstance) { return { exports: jobs, count: jobs.length }; }); - // Get a specific export job + // Get a specific export job (metadata only, no data payload) app.get('/exports/:id', async req => { const access = await requireExportRead(req); const { id } = req.params as { id: string }; const job = await repo.getExportJob(id, access.productId); if (!job) throw new BadRequestError('Export job not found'); - return job; + const meta = { ...job }; + delete meta.data; + return meta; + }); + + // Download export data + app.get('/exports/:id/download', async (req, reply) => { + const access = await requireExportRead(req); + const { id } = req.params as { id: string }; + const job = await repo.getExportJob(id, access.productId); + if (!job) throw new BadRequestError('Export job not found'); + if (job.status !== 'ready' || !job.data) { + throw new BadRequestError('Export is not ready for download'); + } + const contentType = job.format === 'json' ? 'application/json' : 'text/csv'; + return reply + .header('Content-Type', contentType) + .header('Content-Disposition', `attachment; filename="${job.fileName || 'export'}"`) + .send(job.data); }); } diff --git a/services/platform-service/src/modules/exports/types.ts b/services/platform-service/src/modules/exports/types.ts index 1b3709c6..a069be2d 100644 --- a/services/platform-service/src/modules/exports/types.ts +++ b/services/platform-service/src/modules/exports/types.ts @@ -15,6 +15,7 @@ export interface ExportJobDoc { status: ExportStatus; requestedBy: string; blobUrl?: string; + data?: string; fileName?: string; rowCount?: number; fileSizeBytes?: number; diff --git a/services/platform-service/src/modules/waitlist/routes.ts b/services/platform-service/src/modules/waitlist/routes.ts index e5b1f43d..bea0e5e7 100644 --- a/services/platform-service/src/modules/waitlist/routes.ts +++ b/services/platform-service/src/modules/waitlist/routes.ts @@ -455,7 +455,7 @@ export async function waitlistRoutes(app: FastifyInstance) { await repo.update(entry.id, entry.email, { status: 'invited', invitedAt: now, - invitationCodeId: code, + invitationCodeId: invDoc.id, }); invited++; } catch {