diff --git a/services/platform-service/src/modules/broadcasts/repository.ts b/services/platform-service/src/modules/broadcasts/repository.ts index 2e1d0916..1547c98d 100644 --- a/services/platform-service/src/modules/broadcasts/repository.ts +++ b/services/platform-service/src/modules/broadcasts/repository.ts @@ -324,7 +324,7 @@ export async function recordReadReceipt( userId: string, productId: string, action: 'read' | 'click' | 'dismiss' -): Promise { +): Promise { const container = getContainer('broadcast_reads'); const id = `${broadcastId}:${userId}`; const now = new Date().toISOString(); @@ -334,6 +334,13 @@ export async function recordReadReceipt( if (existing) { const receipt = existing as BroadcastRead; + const alreadyRecorded = + (action === 'read' && !!receipt.readAt) || + (action === 'click' && !!receipt.clickedAt) || + (action === 'dismiss' && !!receipt.dismissedAt); + if (alreadyRecorded) { + return false; + } const updates: Partial & { updatedAt: string } = { updatedAt: now, }; @@ -345,6 +352,7 @@ export async function recordReadReceipt( ...receipt, ...updates, }); + return true; } else { const receipt: BroadcastRead = { id, @@ -357,6 +365,7 @@ export async function recordReadReceipt( createdAt: now, }; await container.items.create(receipt); + return true; } } catch (err) { if ((err as { code?: number }).code === 404) { @@ -372,6 +381,7 @@ export async function recordReadReceipt( createdAt: now, }; await container.items.create(receipt); + return true; } else { throw err; } diff --git a/services/platform-service/src/modules/broadcasts/routes.ts b/services/platform-service/src/modules/broadcasts/routes.ts index b734a1e9..8c44201e 100644 --- a/services/platform-service/src/modules/broadcasts/routes.ts +++ b/services/platform-service/src/modules/broadcasts/routes.ts @@ -3,6 +3,7 @@ * @module broadcasts/routes */ +import { randomUUID } from 'node:crypto'; import type { FastifyInstance } from 'fastify'; import { UnauthorizedError, @@ -78,7 +79,7 @@ async function adminRoutes(app: FastifyInstance): Promise { const now = new Date().toISOString(); const broadcast: Broadcast = { - id: `bcast_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, + id: `bcast_${randomUUID()}`, productId, ...input, status: input.scheduledAt ? BroadcastStatus.SCHEDULED : BroadcastStatus.DRAFT, @@ -317,12 +318,12 @@ async function adminRoutes(app: FastifyInstance): Promise { const now = new Date().toISOString(); const cloned: Broadcast = { ...existing, - id: `bcast_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, + id: `bcast_${randomUUID()}`, title: `${existing.title} (Clone)`, status: BroadcastStatus.DRAFT, variant: variant ?? 'treatment', parentBroadcastId: existing.id, - experimentId: existing.experimentId ?? `exp_${Date.now()}`, + experimentId: existing.experimentId ?? `exp_${randomUUID()}`, metrics: { targetedCount: 0, sentCount: 0, @@ -380,12 +381,14 @@ async function publicRoutes(app: FastifyInstance): Promise { await repo.updateInAppMessageStatus(id, userId, 'read'); // Record read receipt - await repo.recordReadReceipt(message.broadcastId, userId, productId, 'read'); + const recorded = await repo.recordReadReceipt(message.broadcastId, userId, productId, 'read'); // Update broadcast metrics - await repo.updateBroadcastMetrics(message.broadcastId, productId, { - openedCount: 1, // Will be incremented properly in real implementation - }); + if (recorded) { + await repo.updateBroadcastMetrics(message.broadcastId, productId, { + openedCount: 1, + }); + } return { success: true }; }); @@ -401,11 +404,18 @@ async function publicRoutes(app: FastifyInstance): Promise { if (!message) throw new NotFoundError('Message not found'); await repo.updateInAppMessageStatus(id, userId, 'dismissed'); - await repo.recordReadReceipt(message.broadcastId, userId, productId, 'dismiss'); + const recorded = await repo.recordReadReceipt( + message.broadcastId, + userId, + productId, + 'dismiss' + ); - await repo.updateBroadcastMetrics(message.broadcastId, productId, { - dismissedCount: 1, - }); + if (recorded) { + await repo.updateBroadcastMetrics(message.broadcastId, productId, { + dismissedCount: 1, + }); + } return { success: true }; }); @@ -420,11 +430,13 @@ async function publicRoutes(app: FastifyInstance): Promise { const message = messages.find(m => m.id === id); if (!message) throw new NotFoundError('Message not found'); - await repo.recordReadReceipt(message.broadcastId, userId, productId, 'click'); + const recorded = await repo.recordReadReceipt(message.broadcastId, userId, productId, 'click'); - await repo.updateBroadcastMetrics(message.broadcastId, productId, { - clickedCount: 1, - }); + if (recorded) { + await repo.updateBroadcastMetrics(message.broadcastId, productId, { + clickedCount: 1, + }); + } return { success: true, redirectUrl: message.ctaUrl }; }); diff --git a/services/platform-service/src/modules/exports/routes.ts b/services/platform-service/src/modules/exports/routes.ts index 46be5f25..1ccb02e4 100644 --- a/services/platform-service/src/modules/exports/routes.ts +++ b/services/platform-service/src/modules/exports/routes.ts @@ -64,7 +64,7 @@ export async function exportRoutes(app: FastifyInstance) { const log = req.log; process.nextTick(async () => { try { - await repo.updateExportJob({ + const processingJob = await repo.updateExportJob({ ...created, status: 'processing', startedAt: new Date().toISOString(), @@ -73,7 +73,7 @@ export async function exportRoutes(app: FastifyInstance) { const serialized = created.format === 'json' ? JSON.stringify(rows, null, 2) : toCsv(rows); const fileName = `${created.type}-${access.productId}-${Date.now()}.${created.format}`; await repo.updateExportJob({ - ...created, + ...processingJob, status: 'ready', data: serialized, rowCount: rows.length,