From 383a8dad32e69a87882c269a00853b4b85c410fc Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 1 Mar 2026 07:55:02 -0800 Subject: [PATCH] feat(peak-sessions): add clientId for idempotent sync, findByClientId repo method, explicit 204 returns --- .../src/modules/peak-routes/routes.ts | 1 + .../peak-sessions/peak-sessions.test.ts | 19 ++++++++++++++++ .../src/modules/peak-sessions/repository.ts | 16 ++++++++++++++ .../src/modules/peak-sessions/routes.ts | 22 +++++++++++++++++-- .../src/modules/peak-sessions/types.ts | 2 ++ 5 files changed, 58 insertions(+), 2 deletions(-) diff --git a/services/platform-service/src/modules/peak-routes/routes.ts b/services/platform-service/src/modules/peak-routes/routes.ts index 91763437..48f5f3a1 100644 --- a/services/platform-service/src/modules/peak-routes/routes.ts +++ b/services/platform-service/src/modules/peak-routes/routes.ts @@ -70,5 +70,6 @@ export async function peakRouteRoutes(app: FastifyInstance) { const deleted = await repo.deleteRoute(sessionId, route.id); if (!deleted) throw new NotFoundError('Route delete failed'); reply.code(204); + return; }); } diff --git a/services/platform-service/src/modules/peak-sessions/peak-sessions.test.ts b/services/platform-service/src/modules/peak-sessions/peak-sessions.test.ts index 14675051..be74fa53 100644 --- a/services/platform-service/src/modules/peak-sessions/peak-sessions.test.ts +++ b/services/platform-service/src/modules/peak-sessions/peak-sessions.test.ts @@ -120,6 +120,25 @@ describe('CreatePeakSessionSchema', () => { }); expect(result.success).toBe(false); }); + + it('accepts optional clientId for idempotent sync', () => { + const result = CreatePeakSessionSchema.safeParse({ + ...validMinimal, + clientId: '550e8400-e29b-41d4-a716-446655440000', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.clientId).toBe('550e8400-e29b-41d4-a716-446655440000'); + } + }); + + it('rejects clientId exceeding max length', () => { + const result = CreatePeakSessionSchema.safeParse({ + ...validMinimal, + clientId: 'x'.repeat(129), + }); + expect(result.success).toBe(false); + }); }); // ── UpdatePeakSessionSchema ── diff --git a/services/platform-service/src/modules/peak-sessions/repository.ts b/services/platform-service/src/modules/peak-sessions/repository.ts index 2b5dc467..2cd877f3 100644 --- a/services/platform-service/src/modules/peak-sessions/repository.ts +++ b/services/platform-service/src/modules/peak-sessions/repository.ts @@ -28,6 +28,22 @@ export async function getSession( } } +export async function findByClientId( + userId: string, + clientId: string +): Promise { + const { resources } = await container() + .items.query({ + query: 'SELECT * FROM c WHERE c.userId = @userId AND c.clientId = @clientId', + parameters: [ + { name: '@userId', value: userId }, + { name: '@clientId', value: clientId }, + ], + }) + .fetchAll(); + return resources[0] ?? null; +} + export async function listSessions( userId: string, query: PeakSessionQuery diff --git a/services/platform-service/src/modules/peak-sessions/routes.ts b/services/platform-service/src/modules/peak-sessions/routes.ts index c25e03fa..cb6ec336 100644 --- a/services/platform-service/src/modules/peak-sessions/routes.ts +++ b/services/platform-service/src/modules/peak-sessions/routes.ts @@ -49,7 +49,7 @@ export async function peakSessionRoutes(app: FastifyInstance) { return session; }); - // Create session + // Create session (idempotent — if clientId is provided and already exists, return existing) app.post('/peak/sessions', async (req, reply) => { const auth = await extractAuth(req); const pid = getRequestProductId(req); @@ -58,10 +58,24 @@ export async function peakSessionRoutes(app: FastifyInstance) { throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); } const input = parsed.data; + + // Idempotency check: if clientId provided, look for existing session + if (input.clientId) { + const existing = await repo.findByClientId(auth.sub, input.clientId); + if (existing) { + req.log.info( + { sessionId: existing.id, clientId: input.clientId }, + 'Returning existing peak session (idempotent)' + ); + return existing; + } + } + const now = new Date().toISOString(); const doc: PeakSessionDoc = { id: `ps_${crypto.randomUUID()}`, + clientId: input.clientId, userId: auth.sub, productId: pid, activityType: input.activityType, @@ -92,7 +106,10 @@ export async function peakSessionRoutes(app: FastifyInstance) { updatedAt: now, }; - req.log.info({ sessionId: doc.id, activityType: doc.activityType }, 'Creating peak session'); + req.log.info( + { sessionId: doc.id, clientId: doc.clientId, activityType: doc.activityType }, + 'Creating peak session' + ); const created = await repo.createSession(doc); reply.code(201); return created; @@ -127,5 +144,6 @@ export async function peakSessionRoutes(app: FastifyInstance) { const deleted = await repo.deleteSession(auth.sub, id); if (!deleted) throw new NotFoundError('Peak session delete failed'); reply.code(204); + return; }); } diff --git a/services/platform-service/src/modules/peak-sessions/types.ts b/services/platform-service/src/modules/peak-sessions/types.ts index 66e3099b..c74eb676 100644 --- a/services/platform-service/src/modules/peak-sessions/types.ts +++ b/services/platform-service/src/modules/peak-sessions/types.ts @@ -57,6 +57,7 @@ export interface SkiMetricsDoc { export interface PeakSessionDoc { id: string; + clientId?: string; userId: string; productId: string; activityType: ActivityType; @@ -105,6 +106,7 @@ const SkiMetricsSchema = z.object({ }); export const CreatePeakSessionSchema = z.object({ + clientId: z.string().max(128).optional(), activityType: z.enum(ACTIVITY_TYPES), status: z.enum(SESSION_STATUSES).default('completed'), startTime: z.string().datetime(),