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
This commit is contained in:
parent
1576b699b0
commit
73b07c2c3a
@ -225,6 +225,7 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
||||
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<void> {
|
||||
|
||||
await repo.updateBroadcast(id, productId, {
|
||||
status: BroadcastStatus.SENT,
|
||||
'metrics.sent': sent,
|
||||
'metrics.targetCount': recipients.length,
|
||||
} as Record<string, unknown>);
|
||||
});
|
||||
await repo.updateBroadcastMetrics(id, productId, {
|
||||
sentCount: sent,
|
||||
targetedCount: recipients.length,
|
||||
});
|
||||
log.info(
|
||||
{ broadcastId: id, sent, total: recipients.length },
|
||||
'[broadcasts] Delivery complete'
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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: `
|
||||
<p>Hi {{displayName}},</p>
|
||||
{{body}}
|
||||
<p>— The {{productName}} Team</p>
|
||||
`.trim(),
|
||||
bodyText: `Hi {{displayName}},\n\n{{body}}\n\n— The {{productName}} Team`,
|
||||
variables: ['displayName', 'subject', 'body', 'productName'],
|
||||
},
|
||||
// ── Diagnostics Templates ─────────────────────────────────────
|
||||
{
|
||||
id: 'diagnostics-session-created',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ export interface ExportJobDoc {
|
||||
status: ExportStatus;
|
||||
requestedBy: string;
|
||||
blobUrl?: string;
|
||||
data?: string;
|
||||
fileName?: string;
|
||||
rowCount?: number;
|
||||
fileSizeBytes?: number;
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user