diff --git a/services/platform-service/src/modules/flags/evaluator.ts b/services/platform-service/src/modules/flags/evaluator.ts index 63fd6b56..2729c5da 100644 --- a/services/platform-service/src/modules/flags/evaluator.ts +++ b/services/platform-service/src/modules/flags/evaluator.ts @@ -352,8 +352,8 @@ export function evaluateFlag(opts: EvaluateFlagOptions): EvaluationResult { return offResult(flag, 'off'); } - // 10. Default (no userId, full rollout) - return variationResult(flag, flag.defaultVariation, 'default'); + // 10. No userId with partial rollout — can't hash, return off + return offResult(flag, 'off'); } /** diff --git a/services/platform-service/src/modules/flags/repository.ts b/services/platform-service/src/modules/flags/repository.ts index 5e313d4d..b6efaf2d 100644 --- a/services/platform-service/src/modules/flags/repository.ts +++ b/services/platform-service/src/modules/flags/repository.ts @@ -35,10 +35,11 @@ export async function create(doc: FeatureFlagDoc): Promise { export async function update( id: string, + productId: string, updates: Partial ): Promise { try { - return await flagCollection().update(id, id, { + return await flagCollection().update(id, productId, { ...updates, updatedAt: new Date().toISOString(), }); @@ -47,9 +48,9 @@ export async function update( } } -export async function remove(id: string): Promise { +export async function remove(id: string, productId: string): Promise { try { - await flagCollection().delete(id, id); + await flagCollection().delete(id, productId); return true; } catch { return false; @@ -81,10 +82,11 @@ export async function createSegment(doc: SegmentDoc): Promise { export async function updateSegment( id: string, + productId: string, updates: Partial ): Promise { try { - return await segmentCollection().update(id, id, { + return await segmentCollection().update(id, productId, { ...updates, updatedAt: new Date().toISOString(), }); @@ -93,9 +95,9 @@ export async function updateSegment( } } -export async function removeSegment(id: string): Promise { +export async function removeSegment(id: string, productId: string): Promise { try { - await segmentCollection().delete(id, id); + await segmentCollection().delete(id, productId); return true; } catch { return false; diff --git a/services/platform-service/src/modules/flags/routes.ts b/services/platform-service/src/modules/flags/routes.ts index 7ac843bd..7b4b7f7a 100644 --- a/services/platform-service/src/modules/flags/routes.ts +++ b/services/platform-service/src/modules/flags/routes.ts @@ -292,9 +292,9 @@ export async function flagRoutes(app: FastifyInstance) { } const actor = getActor(req); - const before = { ...flag }; + const before = JSON.parse(JSON.stringify(flag)) as FeatureFlagDoc; const updates = { ...parsed.data, updatedBy: actor, version: flag.version + 1 }; - const updated = await repo.update(flag.id, updates); + const updated = await repo.update(flag.id, productId, updates); if (!updated) throw new NotFoundError('Flag update failed'); await recordAudit(productId, key, 'updated', actor, before, updated); @@ -322,8 +322,8 @@ export async function flagRoutes(app: FastifyInstance) { if (!flag) throw new NotFoundError('Flag not found'); const actor = getActor(req); - const before = { ...flag }; - const updated = await repo.update(flag.id, { + const before = JSON.parse(JSON.stringify(flag)) as FeatureFlagDoc; + const updated = await repo.update(flag.id, productId, { enabled: !flag.enabled, updatedBy: actor, version: flag.version + 1, @@ -358,8 +358,8 @@ export async function flagRoutes(app: FastifyInstance) { const newArchived = archived ?? !flag.archived; const actor = getActor(req); - const before = { ...flag }; - const updated = await repo.update(flag.id, { + const before = JSON.parse(JSON.stringify(flag)) as FeatureFlagDoc; + const updated = await repo.update(flag.id, productId, { archived: newArchived, enabled: newArchived ? false : flag.enabled, updatedBy: actor, @@ -381,7 +381,7 @@ export async function flagRoutes(app: FastifyInstance) { if (!flag) throw new NotFoundError('Flag not found'); const actor = getActor(req); - await repo.remove(flag.id); + await repo.remove(flag.id, productId); await recordAudit(productId, key, 'deleted', actor, flag, undefined); broadcastFlagChange(productId, key, 'deleted'); @@ -414,9 +414,20 @@ export async function flagRoutes(app: FastifyInstance) { const disabled: string[] = []; for (const f of toDisable) { - const before = { ...f }; - await repo.update(f.id, { enabled: false, updatedBy: actor, version: f.version + 1 }); - await recordAudit(productId, f.key, 'kill_switch', actor, before, { ...f, enabled: false }); + const before = JSON.parse(JSON.stringify(f)) as FeatureFlagDoc; + const afterDoc = await repo.update(f.id, productId, { + enabled: false, + updatedBy: actor, + version: f.version + 1, + }); + await recordAudit( + productId, + f.key, + 'kill_switch', + actor, + before, + afterDoc ?? { ...f, enabled: false } + ); broadcastFlagChange(productId, f.key, 'kill_switch'); disabled.push(f.key); } @@ -479,7 +490,7 @@ export async function flagRoutes(app: FastifyInstance) { if (!parsed.success) { throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); } - const updated = await repo.updateSegment(segment.id, parsed.data); + const updated = await repo.updateSegment(segment.id, productId, parsed.data); if (!updated) throw new NotFoundError('Segment update failed'); return updated; }); @@ -489,7 +500,7 @@ export async function flagRoutes(app: FastifyInstance) { const productId = getRequestProductId(req); const segment = await repo.getSegmentByKey(key, productId); if (!segment) throw new NotFoundError('Segment not found'); - await repo.removeSegment(segment.id); + await repo.removeSegment(segment.id, productId); return { success: true }; });