fix(api): wrap audit log writes inside their parent transactions

Prevents mutations from committing without an audit trail if the
auditLog.create call fails after the main write already succeeded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 16:40:10 +02:00
parent a01f99561d
commit 3c0179fcec
25 changed files with 758 additions and 656 deletions
@@ -275,42 +275,46 @@ export async function applyExperienceMultiplierRules(
const inputs = demandLines.map((line) => buildExperienceMultiplierInput(line));
const batch = applyExperienceMultipliersBatch(inputs, engineRules);
let updatedCount = 0;
for (let i = 0; i < demandLines.length; i++) {
const line = demandLines[i]!;
const result = batch.results[i]!;
const updatedCount = await ctx.db.$transaction(async (tx) => {
let count = 0;
for (let i = 0; i < demandLines.length; i++) {
const line = demandLines[i]!;
const result = batch.results[i]!;
if (hasExperienceMultiplierChanges(line, result)) {
await ctx.db.estimateDemandLine.update({
where: { id: line.id },
data: buildExperienceMultiplierDemandLineUpdateData({
line,
result,
multiplierSet,
}),
});
updatedCount++;
if (hasExperienceMultiplierChanges(line, result)) {
await tx.estimateDemandLine.update({
where: { id: line.id },
data: buildExperienceMultiplierDemandLineUpdateData({
line,
result,
multiplierSet,
}),
});
count++;
}
}
}
await ctx.db.auditLog.create({
data: {
entityType: "Estimate",
entityId: estimate.id,
action: "UPDATE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: {
after: {
experienceMultipliersApplied: {
setId: multiplierSet.id,
setName: multiplierSet.name,
linesUpdated: updatedCount,
totalOriginalHours: batch.totalOriginalHours,
totalAdjustedHours: batch.totalAdjustedHours,
await tx.auditLog.create({
data: {
entityType: "Estimate",
entityId: estimate.id,
action: "UPDATE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: {
after: {
experienceMultipliersApplied: {
setId: multiplierSet.id,
setName: multiplierSet.name,
linesUpdated: count,
totalOriginalHours: batch.totalOriginalHours,
totalAdjustedHours: batch.totalAdjustedHours,
},
},
},
},
},
});
return count;
});
return {