diff --git a/src/app/api/gamification/achievements/route.ts b/src/app/api/gamification/achievements/route.ts index a04e2f5b0..e58567730 100644 --- a/src/app/api/gamification/achievements/route.ts +++ b/src/app/api/gamification/achievements/route.ts @@ -531,7 +531,8 @@ export const GET = apiHandler(async (request: NextRequest) => { "auth.login.password", "bugreport.submit", // v1.4.18 — hidden Easter-egg triggers - "doctor-report.export", + "doctor-report.generate", + "doctor-report.pdf.generate", "settings.locale.update", ], }, diff --git a/src/lib/auth/profile-update.ts b/src/lib/auth/profile-update.ts index 03c0de7c3..07391127d 100644 --- a/src/lib/auth/profile-update.ts +++ b/src/lib/auth/profile-update.ts @@ -86,6 +86,21 @@ export async function applyProfileUpdate( if (data.locale !== undefined) updates.locale = data.locale; if (data.timezone !== undefined) updates.timezone = data.timezone; + // v1.4.18 — capture the prior locale so we can emit a separate + // `settings.locale.update` audit row when the locale actually + // changes. The hidden "polyglot" achievement watches that action, + // and we don't want to fire it on profile updates that happen to + // touch other fields. + const priorLocale = + data.locale !== undefined + ? ( + await prisma.user.findUnique({ + where: { id: userId }, + select: { locale: true }, + }) + )?.locale ?? null + : null; + const updatedUser = await prisma.user.update({ where: { id: userId }, data: updates, @@ -96,6 +111,14 @@ export async function applyProfileUpdate( ipAddress: ipAddress ?? null, }); + if (data.locale !== undefined && priorLocale !== data.locale) { + await auditLog("settings.locale.update", { + userId: updatedUser.id, + ipAddress: ipAddress ?? null, + details: { from: priorLocale, to: data.locale ?? null }, + }); + } + return { ok: true, user: { diff --git a/src/lib/gamification/__tests__/expansion-metrics.test.ts b/src/lib/gamification/__tests__/expansion-metrics.test.ts index f6b3ff7ab..ab71c28e4 100644 --- a/src/lib/gamification/__tests__/expansion-metrics.test.ts +++ b/src/lib/gamification/__tests__/expansion-metrics.test.ts @@ -235,8 +235,9 @@ describe("getHiddenMetrics", () => { moodEntries: [], intakeEvents: [], auditEvents: [ - { action: "doctor-report.export", createdAt: new Date() }, - { action: "doctor-report.export", createdAt: new Date() }, + // The route's auditLog filter accepts both `generate` actions. + { action: "doctor-report.generate", createdAt: new Date() }, + { action: "doctor-report.pdf.generate", createdAt: new Date() }, { action: "settings.locale.update", createdAt: new Date() }, { action: "auth.login.passkey", createdAt: new Date() }, // ignored ], diff --git a/src/lib/gamification/expansion-metrics.ts b/src/lib/gamification/expansion-metrics.ts index 6157b5e30..899135d72 100644 --- a/src/lib/gamification/expansion-metrics.ts +++ b/src/lib/gamification/expansion-metrics.ts @@ -276,7 +276,16 @@ export function getHiddenMetrics(input: { let doctorPdfCount = 0; let localeFlipCount = 0; for (const ev of input.auditEvents) { - if (ev.action === "doctor-report.export") doctorPdfCount += 1; + // Both "generate" + "pdf.generate" count — the PDF route emits the + // latter; the JSON-only generate route emits the former. We accept + // either as the trigger so the badge fires the first time the user + // exercises the doctor-report feature in any shape. + if ( + ev.action === "doctor-report.generate" || + ev.action === "doctor-report.pdf.generate" + ) { + doctorPdfCount += 1; + } if (ev.action === "settings.locale.update") localeFlipCount += 1; } diff --git a/tests/integration/achievements-expansion.test.ts b/tests/integration/achievements-expansion.test.ts index 3c15aec35..5a5c36d35 100644 --- a/tests/integration/achievements-expansion.test.ts +++ b/tests/integration/achievements-expansion.test.ts @@ -159,12 +159,12 @@ describe("achievements expansion — fresh user, real prisma rows", () => { await prisma.auditLog.create({ data: { userId: user.id, - action: "doctor-report.export", + action: "doctor-report.pdf.generate", }, }); const auditEvents = await prisma.auditLog.findMany({ - where: { userId: user.id, action: "doctor-report.export" }, + where: { userId: user.id, action: "doctor-report.pdf.generate" }, select: { action: true, createdAt: true }, });