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:
@@ -302,16 +302,16 @@ export async function batchUpdateAllocationStatusWithAudit(
|
||||
).allocation),
|
||||
);
|
||||
|
||||
return updatedAllocations;
|
||||
});
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Allocation",
|
||||
entityId: input.ids.join(","),
|
||||
action: "UPDATE",
|
||||
changes: { after: { status: input.status, ids: input.ids } },
|
||||
},
|
||||
});
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Allocation",
|
||||
entityId: input.ids.join(","),
|
||||
action: "UPDATE",
|
||||
changes: { after: { status: input.status, ids: input.ids } },
|
||||
},
|
||||
return updatedAllocations;
|
||||
});
|
||||
|
||||
return updated;
|
||||
|
||||
@@ -238,41 +238,43 @@ export async function applyEffortRules(
|
||||
const rules = toEffortRuleEngineInputs(ruleSet.rules);
|
||||
const result = expandScopeToEffort(scopeItems, rules);
|
||||
|
||||
if (input.mode === "replace") {
|
||||
await ctx.db.estimateDemandLine.deleteMany({
|
||||
where: { estimateVersionId: version.id },
|
||||
});
|
||||
}
|
||||
await ctx.db.$transaction(async (tx) => {
|
||||
if (input.mode === "replace") {
|
||||
await tx.estimateDemandLine.deleteMany({
|
||||
where: { estimateVersionId: version.id },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.lines.length > 0) {
|
||||
await ctx.db.estimateDemandLine.createMany({
|
||||
data: buildEstimateDemandLineRows({
|
||||
estimateVersionId: version.id,
|
||||
currency: estimate.baseCurrency,
|
||||
ruleSet: { id: ruleSet.id, name: ruleSet.name },
|
||||
lines: result.lines,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (result.lines.length > 0) {
|
||||
await tx.estimateDemandLine.createMany({
|
||||
data: buildEstimateDemandLineRows({
|
||||
estimateVersionId: version.id,
|
||||
currency: estimate.baseCurrency,
|
||||
ruleSet: { id: ruleSet.id, name: ruleSet.name },
|
||||
lines: result.lines,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
after: {
|
||||
effortRulesApplied: {
|
||||
ruleSetId: ruleSet.id,
|
||||
ruleSetName: ruleSet.name,
|
||||
mode: input.mode,
|
||||
linesGenerated: result.lines.length,
|
||||
warnings: result.warnings,
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
after: {
|
||||
effortRulesApplied: {
|
||||
ruleSetId: ruleSet.id,
|
||||
ruleSetName: ruleSet.name,
|
||||
mode: input.mode,
|
||||
linesGenerated: result.lines.length,
|
||||
warnings: result.warnings,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -65,22 +65,24 @@ export const estimateCommercialProcedures = {
|
||||
|
||||
const validated = CommercialTermsSchema.parse(input.terms);
|
||||
|
||||
await ctx.db.estimateVersion.update({
|
||||
where: { id: version.id },
|
||||
data: { commercialTerms: validated as unknown as Prisma.InputJsonValue },
|
||||
});
|
||||
await ctx.db.$transaction(async (tx) => {
|
||||
await tx.estimateVersion.update({
|
||||
where: { id: version.id },
|
||||
data: { commercialTerms: validated as unknown as Prisma.InputJsonValue },
|
||||
});
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
field: "commercialTerms",
|
||||
after: validated,
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
field: "commercialTerms",
|
||||
after: validated,
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { versionId: version.id, terms: validated };
|
||||
|
||||
@@ -100,28 +100,32 @@ export async function createEstimateRecord(
|
||||
await autoFillDemandLineRates(ctx.db, input.demandLines, input.projectId);
|
||||
const enrichedInput = { ...input, demandLines: enrichedLines };
|
||||
|
||||
const estimate = await createEstimate(
|
||||
ctx.db as unknown as Parameters<typeof createEstimate>[0],
|
||||
withComputedMetrics(enrichedInput, input.baseCurrency),
|
||||
);
|
||||
const estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const created = await createEstimate(
|
||||
tx as unknown as Parameters<typeof createEstimate>[0],
|
||||
withComputedMetrics(enrichedInput, input.baseCurrency),
|
||||
);
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "CREATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
name: estimate.name,
|
||||
status: estimate.status,
|
||||
projectId: estimate.projectId,
|
||||
latestVersionNumber: estimate.latestVersionNumber,
|
||||
autoFilledRateCardLines: autoFilledIndices.length,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: created.id,
|
||||
action: "CREATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: created.id,
|
||||
name: created.name,
|
||||
status: created.status,
|
||||
projectId: created.projectId,
|
||||
latestVersionNumber: created.latestVersionNumber,
|
||||
autoFilledRateCardLines: autoFilledIndices.length,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
return estimate;
|
||||
@@ -133,10 +137,30 @@ export async function cloneEstimateRecord(
|
||||
) {
|
||||
let estimate;
|
||||
try {
|
||||
estimate = await cloneEstimate(
|
||||
ctx.db as unknown as Parameters<typeof cloneEstimate>[0],
|
||||
input,
|
||||
);
|
||||
estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const cloned = await cloneEstimate(
|
||||
tx as unknown as Parameters<typeof cloneEstimate>[0],
|
||||
input,
|
||||
);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: cloned.id,
|
||||
action: "CREATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: cloned.id,
|
||||
name: cloned.name,
|
||||
clonedFrom: input.sourceEstimateId,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return cloned;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -146,22 +170,6 @@ export async function cloneEstimateRecord(
|
||||
]);
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "CREATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
name: estimate.name,
|
||||
clonedFrom: input.sourceEstimateId,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return estimate;
|
||||
}
|
||||
|
||||
@@ -194,10 +202,35 @@ export async function updateEstimateDraftRecord(
|
||||
|
||||
let estimate;
|
||||
try {
|
||||
estimate = await updateEstimateDraft(
|
||||
ctx.db as unknown as Parameters<typeof updateEstimateDraft>[0],
|
||||
withComputedMetrics(enrichedInput, input.baseCurrency ?? "EUR"),
|
||||
);
|
||||
estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const updated = await updateEstimateDraft(
|
||||
tx as unknown as Parameters<typeof updateEstimateDraft>[0],
|
||||
withComputedMetrics(enrichedInput, input.baseCurrency ?? "EUR"),
|
||||
);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: updated.id,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: updated.id,
|
||||
name: updated.name,
|
||||
status: updated.status,
|
||||
latestVersionNumber: updated.latestVersionNumber,
|
||||
workingVersionId: updated.versions.find(
|
||||
(version) => version.status === "WORKING",
|
||||
)?.id,
|
||||
autoFilledRateCardLines: autoFilledIndices.length,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return updated;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -211,27 +244,6 @@ export async function updateEstimateDraftRecord(
|
||||
]);
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
name: estimate.name,
|
||||
status: estimate.status,
|
||||
latestVersionNumber: estimate.latestVersionNumber,
|
||||
workingVersionId: estimate.versions.find(
|
||||
(version) => version.status === "WORKING",
|
||||
)?.id,
|
||||
autoFilledRateCardLines: autoFilledIndices.length,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return estimate;
|
||||
}
|
||||
|
||||
@@ -241,10 +253,35 @@ export async function createEstimateExportRecord(
|
||||
) {
|
||||
let estimate;
|
||||
try {
|
||||
estimate = await createEstimateExport(
|
||||
ctx.db as unknown as Parameters<typeof createEstimateExport>[0],
|
||||
input,
|
||||
);
|
||||
estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const exported = await createEstimateExport(
|
||||
tx as unknown as Parameters<typeof createEstimateExport>[0],
|
||||
input,
|
||||
);
|
||||
|
||||
const exportedVersion = input.versionId
|
||||
? exported.versions.find((version) => version.id === input.versionId)
|
||||
: exported.versions[0];
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: exported.id,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: exported.id,
|
||||
exportFormat: input.format,
|
||||
exportCount: exportedVersion?.exports.length ?? null,
|
||||
versionId: exportedVersion?.id ?? null,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return exported;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -258,27 +295,6 @@ export async function createEstimateExportRecord(
|
||||
]);
|
||||
}
|
||||
|
||||
const exportedVersion = input.versionId
|
||||
? estimate.versions.find((version) => version.id === input.versionId)
|
||||
: estimate.versions[0];
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
exportFormat: input.format,
|
||||
exportCount: exportedVersion?.exports.length ?? null,
|
||||
versionId: exportedVersion?.id ?? null,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return estimate;
|
||||
}
|
||||
|
||||
@@ -288,10 +304,36 @@ export async function createEstimatePlanningHandoffRecord(
|
||||
) {
|
||||
let result;
|
||||
try {
|
||||
result = await createEstimatePlanningHandoff(
|
||||
ctx.db as unknown as Parameters<typeof createEstimatePlanningHandoff>[0],
|
||||
input,
|
||||
);
|
||||
result = await ctx.db.$transaction(async (tx) => {
|
||||
const handoff = await createEstimatePlanningHandoff(
|
||||
tx as unknown as Parameters<typeof createEstimatePlanningHandoff>[0],
|
||||
input,
|
||||
);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: handoff.estimateId,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
planningHandoff: {
|
||||
versionId: handoff.estimateVersionId,
|
||||
versionNumber: handoff.estimateVersionNumber,
|
||||
projectId: handoff.projectId,
|
||||
createdCount: handoff.createdCount,
|
||||
assignedCount: handoff.assignedCount,
|
||||
placeholderCount: handoff.placeholderCount,
|
||||
fallbackPlaceholderCount: handoff.fallbackPlaceholderCount,
|
||||
},
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return handoff;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -319,28 +361,6 @@ export async function createEstimatePlanningHandoffRecord(
|
||||
]);
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: result.estimateId,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
planningHandoff: {
|
||||
versionId: result.estimateVersionId,
|
||||
versionNumber: result.estimateVersionNumber,
|
||||
projectId: result.projectId,
|
||||
createdCount: result.createdCount,
|
||||
assignedCount: result.assignedCount,
|
||||
placeholderCount: result.placeholderCount,
|
||||
fallbackPlaceholderCount: result.fallbackPlaceholderCount,
|
||||
},
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
for (const allocation of result.allocations) {
|
||||
emitAllocationCreated({
|
||||
id: allocation.id,
|
||||
|
||||
@@ -21,10 +21,32 @@ export const estimateVersionWorkflowProcedures = {
|
||||
|
||||
let estimate;
|
||||
try {
|
||||
estimate = await submitEstimateVersion(
|
||||
ctx.db as unknown as Parameters<typeof submitEstimateVersion>[0],
|
||||
input,
|
||||
);
|
||||
estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const submitted = await submitEstimateVersion(
|
||||
tx as unknown as Parameters<typeof submitEstimateVersion>[0],
|
||||
input,
|
||||
);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: submitted.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
after: {
|
||||
id: submitted.id,
|
||||
status: submitted.status,
|
||||
submittedVersionId: submitted.versions.find(
|
||||
(version) => version.status === "SUBMITTED",
|
||||
)?.id,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return submitted;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -41,24 +63,6 @@ export const estimateVersionWorkflowProcedures = {
|
||||
]);
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
status: estimate.status,
|
||||
submittedVersionId: estimate.versions.find(
|
||||
(version) => version.status === "SUBMITTED",
|
||||
)?.id,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return estimate;
|
||||
}),
|
||||
|
||||
@@ -69,10 +73,32 @@ export const estimateVersionWorkflowProcedures = {
|
||||
|
||||
let estimate;
|
||||
try {
|
||||
estimate = await approveEstimateVersion(
|
||||
ctx.db as unknown as Parameters<typeof approveEstimateVersion>[0],
|
||||
input,
|
||||
);
|
||||
estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const approved = await approveEstimateVersion(
|
||||
tx as unknown as Parameters<typeof approveEstimateVersion>[0],
|
||||
input,
|
||||
);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: approved.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
after: {
|
||||
id: approved.id,
|
||||
status: approved.status,
|
||||
approvedVersionId: approved.versions.find(
|
||||
(version) => version.status === "APPROVED",
|
||||
)?.id,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return approved;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -89,24 +115,6 @@ export const estimateVersionWorkflowProcedures = {
|
||||
]);
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
status: estimate.status,
|
||||
approvedVersionId: estimate.versions.find(
|
||||
(version) => version.status === "APPROVED",
|
||||
)?.id,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return estimate;
|
||||
}),
|
||||
|
||||
@@ -117,10 +125,33 @@ export const estimateVersionWorkflowProcedures = {
|
||||
|
||||
let estimate;
|
||||
try {
|
||||
estimate = await createEstimateRevision(
|
||||
ctx.db as unknown as Parameters<typeof createEstimateRevision>[0],
|
||||
input,
|
||||
);
|
||||
estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const revision = await createEstimateRevision(
|
||||
tx as unknown as Parameters<typeof createEstimateRevision>[0],
|
||||
input,
|
||||
);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: revision.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
after: {
|
||||
id: revision.id,
|
||||
status: revision.status,
|
||||
latestVersionNumber: revision.latestVersionNumber,
|
||||
workingVersionId: revision.versions.find(
|
||||
(version) => version.status === "WORKING",
|
||||
)?.id,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return revision;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -138,25 +169,6 @@ export const estimateVersionWorkflowProcedures = {
|
||||
]);
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
status: estimate.status,
|
||||
latestVersionNumber: estimate.latestVersionNumber,
|
||||
workingVersionId: estimate.versions.find(
|
||||
(version) => version.status === "WORKING",
|
||||
)?.id,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return estimate;
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -159,36 +159,38 @@ export async function importCsv(ctx: ImportExportMutationContext, input: ImportC
|
||||
return { ...results, message: `Dry run: ${input.rows.length} rows validated` };
|
||||
}
|
||||
|
||||
for (let index = 0; index < input.rows.length; index += 1) {
|
||||
const row = input.rows[index];
|
||||
if (!row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (input.entityType === "resources") {
|
||||
const outcome = await importResourceRow(ctx, row);
|
||||
if (outcome.updated) {
|
||||
results.updated += 1;
|
||||
} else if (outcome.error) {
|
||||
results.errors.push({ row: index + 1, message: outcome.error });
|
||||
}
|
||||
await ctx.db.$transaction(async (tx) => {
|
||||
for (let index = 0; index < input.rows.length; index += 1) {
|
||||
const row = input.rows[index];
|
||||
if (!row) {
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
results.errors.push({
|
||||
row: index + 1,
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: input.entityType,
|
||||
entityId: "bulk-import",
|
||||
action: "IMPORT",
|
||||
changes: { summary: results },
|
||||
},
|
||||
try {
|
||||
if (input.entityType === "resources") {
|
||||
const outcome = await importResourceRow({ ...ctx, db: tx as unknown as typeof ctx.db }, row);
|
||||
if (outcome.updated) {
|
||||
results.updated += 1;
|
||||
} else if (outcome.error) {
|
||||
results.errors.push({ row: index + 1, message: outcome.error });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
results.errors.push({
|
||||
row: index + 1,
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: input.entityType,
|
||||
entityId: "bulk-import",
|
||||
action: "IMPORT",
|
||||
changes: { summary: results },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
|
||||
@@ -123,19 +123,23 @@ export function createProjectLifecycleProcedures(
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
const updated = await ctx.db.$transaction(
|
||||
input.ids.map((id) =>
|
||||
ctx.db.project.update({ where: { id }, data: { status: input.status } }),
|
||||
),
|
||||
);
|
||||
const updated = await ctx.db.$transaction(async (tx) => {
|
||||
const results = await Promise.all(
|
||||
input.ids.map((id) =>
|
||||
tx.project.update({ where: { id }, data: { status: input.status } }),
|
||||
),
|
||||
);
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Project",
|
||||
entityId: input.ids.join(","),
|
||||
action: "UPDATE",
|
||||
changes: { after: { status: input.status, ids: input.ids } },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Project",
|
||||
entityId: input.ids.join(","),
|
||||
action: "UPDATE",
|
||||
changes: { after: { status: input.status, ids: input.ids } },
|
||||
},
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
dependencies.invalidateDashboardCacheInBackground();
|
||||
|
||||
@@ -80,17 +80,21 @@ export function createProjectMutationProcedures(
|
||||
target: BlueprintTarget.PROJECT,
|
||||
});
|
||||
|
||||
const project = await ctx.db.project.create({
|
||||
data: buildProjectCreateData(input),
|
||||
});
|
||||
const project = await ctx.db.$transaction(async (tx) => {
|
||||
const created = await tx.project.create({
|
||||
data: buildProjectCreateData(input),
|
||||
});
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Project",
|
||||
entityId: project.id,
|
||||
action: "CREATE",
|
||||
changes: { after: project },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Project",
|
||||
entityId: created.id,
|
||||
action: "CREATE",
|
||||
changes: { after: created },
|
||||
},
|
||||
});
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
backgroundEffects.invalidateDashboardCacheInBackground();
|
||||
@@ -124,18 +128,22 @@ export function createProjectMutationProcedures(
|
||||
target: BlueprintTarget.PROJECT,
|
||||
});
|
||||
|
||||
const updated = await ctx.db.project.update({
|
||||
where: { id: input.id },
|
||||
data: buildProjectUpdateData(input.data),
|
||||
});
|
||||
const updated = await ctx.db.$transaction(async (tx) => {
|
||||
const result = await tx.project.update({
|
||||
where: { id: input.id },
|
||||
data: buildProjectUpdateData(input.data),
|
||||
});
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Project",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { before: existing, after: updated },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Project",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { before: existing, after: result },
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
backgroundEffects.invalidateDashboardCacheInBackground();
|
||||
|
||||
@@ -34,62 +34,66 @@ export const resourceMutationProcedures = {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "A resource can have at most one primary role" });
|
||||
}
|
||||
|
||||
const resource = await ctx.db.resource.create({
|
||||
data: {
|
||||
eid: input.eid,
|
||||
displayName: input.displayName,
|
||||
email: input.email,
|
||||
chapter: input.chapter,
|
||||
lcrCents: input.lcrCents,
|
||||
ucrCents: input.ucrCents,
|
||||
currency: input.currency,
|
||||
chargeabilityTarget: input.chargeabilityTarget,
|
||||
availability: input.availability,
|
||||
skills: input.skills as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
dynamicFields: input.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
blueprintId: input.blueprintId,
|
||||
portfolioUrl: input.portfolioUrl || undefined,
|
||||
roleId: input.roleId || undefined,
|
||||
...(input.postalCode !== undefined ? { postalCode: input.postalCode } : {}),
|
||||
...(input.postalCode && !input.federalState
|
||||
? { federalState: inferStateFromPostalCode(input.postalCode) }
|
||||
: input.federalState !== undefined
|
||||
? { federalState: input.federalState }
|
||||
: {}),
|
||||
...(input.countryId !== undefined ? { countryId: input.countryId || null } : {}),
|
||||
...(input.metroCityId !== undefined ? { metroCityId: input.metroCityId || null } : {}),
|
||||
...(input.orgUnitId !== undefined ? { orgUnitId: input.orgUnitId || null } : {}),
|
||||
...(input.managementLevelGroupId !== undefined ? { managementLevelGroupId: input.managementLevelGroupId || null } : {}),
|
||||
...(input.managementLevelId !== undefined ? { managementLevelId: input.managementLevelId || null } : {}),
|
||||
...(input.resourceType !== undefined ? { resourceType: input.resourceType } : {}),
|
||||
...(input.chgResponsibility !== undefined ? { chgResponsibility: input.chgResponsibility } : {}),
|
||||
...(input.rolledOff !== undefined ? { rolledOff: input.rolledOff } : {}),
|
||||
...(input.departed !== undefined ? { departed: input.departed } : {}),
|
||||
...(input.enterpriseId !== undefined ? { enterpriseId: input.enterpriseId || null } : {}),
|
||||
...(input.clientUnitId !== undefined ? { clientUnitId: input.clientUnitId || null } : {}),
|
||||
...(input.fte !== undefined ? { fte: input.fte } : {}),
|
||||
resourceRoles: input.roles?.length
|
||||
? {
|
||||
create: input.roles.map((role) => ({
|
||||
roleId: role.roleId,
|
||||
isPrimary: role.isPrimary,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
} as unknown as Parameters<typeof ctx.db.resource.create>[0]["data"],
|
||||
include: {
|
||||
resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } } },
|
||||
},
|
||||
});
|
||||
const resource = await ctx.db.$transaction(async (tx) => {
|
||||
const created = await tx.resource.create({
|
||||
data: {
|
||||
eid: input.eid,
|
||||
displayName: input.displayName,
|
||||
email: input.email,
|
||||
chapter: input.chapter,
|
||||
lcrCents: input.lcrCents,
|
||||
ucrCents: input.ucrCents,
|
||||
currency: input.currency,
|
||||
chargeabilityTarget: input.chargeabilityTarget,
|
||||
availability: input.availability,
|
||||
skills: input.skills as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
dynamicFields: input.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
blueprintId: input.blueprintId,
|
||||
portfolioUrl: input.portfolioUrl || undefined,
|
||||
roleId: input.roleId || undefined,
|
||||
...(input.postalCode !== undefined ? { postalCode: input.postalCode } : {}),
|
||||
...(input.postalCode && !input.federalState
|
||||
? { federalState: inferStateFromPostalCode(input.postalCode) }
|
||||
: input.federalState !== undefined
|
||||
? { federalState: input.federalState }
|
||||
: {}),
|
||||
...(input.countryId !== undefined ? { countryId: input.countryId || null } : {}),
|
||||
...(input.metroCityId !== undefined ? { metroCityId: input.metroCityId || null } : {}),
|
||||
...(input.orgUnitId !== undefined ? { orgUnitId: input.orgUnitId || null } : {}),
|
||||
...(input.managementLevelGroupId !== undefined ? { managementLevelGroupId: input.managementLevelGroupId || null } : {}),
|
||||
...(input.managementLevelId !== undefined ? { managementLevelId: input.managementLevelId || null } : {}),
|
||||
...(input.resourceType !== undefined ? { resourceType: input.resourceType } : {}),
|
||||
...(input.chgResponsibility !== undefined ? { chgResponsibility: input.chgResponsibility } : {}),
|
||||
...(input.rolledOff !== undefined ? { rolledOff: input.rolledOff } : {}),
|
||||
...(input.departed !== undefined ? { departed: input.departed } : {}),
|
||||
...(input.enterpriseId !== undefined ? { enterpriseId: input.enterpriseId || null } : {}),
|
||||
...(input.clientUnitId !== undefined ? { clientUnitId: input.clientUnitId || null } : {}),
|
||||
...(input.fte !== undefined ? { fte: input.fte } : {}),
|
||||
resourceRoles: input.roles?.length
|
||||
? {
|
||||
create: input.roles.map((role) => ({
|
||||
roleId: role.roleId,
|
||||
isPrimary: role.isPrimary,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
} as unknown as Parameters<typeof tx.resource.create>[0]["data"],
|
||||
include: {
|
||||
resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } } },
|
||||
},
|
||||
});
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: resource.id,
|
||||
action: "CREATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
changes: { after: resource },
|
||||
} as unknown as Parameters<typeof ctx.db.auditLog.create>[0]["data"],
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: created.id,
|
||||
action: "CREATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
changes: { after: created },
|
||||
} as unknown as Parameters<typeof tx.auditLog.create>[0]["data"],
|
||||
});
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
return resource;
|
||||
@@ -121,67 +125,71 @@ export const resourceMutationProcedures = {
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await ctx.db.resource.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
...(input.data.displayName !== undefined ? { displayName: input.data.displayName } : {}),
|
||||
...(input.data.email !== undefined ? { email: input.data.email } : {}),
|
||||
...(input.data.chapter !== undefined ? { chapter: input.data.chapter } : {}),
|
||||
...(input.data.lcrCents !== undefined ? { lcrCents: input.data.lcrCents } : {}),
|
||||
...(input.data.ucrCents !== undefined ? { ucrCents: input.data.ucrCents } : {}),
|
||||
...(input.data.currency !== undefined ? { currency: input.data.currency } : {}),
|
||||
...(input.data.chargeabilityTarget !== undefined ? { chargeabilityTarget: input.data.chargeabilityTarget } : {}),
|
||||
...(input.data.availability !== undefined ? { availability: input.data.availability as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
|
||||
...(input.data.skills !== undefined ? { skills: input.data.skills as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
|
||||
...(input.data.dynamicFields !== undefined ? { dynamicFields: input.data.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
|
||||
...(input.data.blueprintId !== undefined ? { blueprintId: input.data.blueprintId } : {}),
|
||||
...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}),
|
||||
...(input.data.portfolioUrl !== undefined ? { portfolioUrl: input.data.portfolioUrl || null } : {}),
|
||||
...(input.data.roleId !== undefined ? { roleId: input.data.roleId || null } : {}),
|
||||
...(input.data.postalCode !== undefined ? { postalCode: input.data.postalCode } : {}),
|
||||
...(input.data.postalCode && !input.data.federalState
|
||||
? { federalState: inferStateFromPostalCode(input.data.postalCode) }
|
||||
: input.data.federalState !== undefined
|
||||
? { federalState: input.data.federalState }
|
||||
: {}),
|
||||
...(input.data.countryId !== undefined ? { countryId: input.data.countryId || null } : {}),
|
||||
...(input.data.metroCityId !== undefined ? { metroCityId: input.data.metroCityId || null } : {}),
|
||||
...(input.data.orgUnitId !== undefined ? { orgUnitId: input.data.orgUnitId || null } : {}),
|
||||
...(input.data.managementLevelGroupId !== undefined ? { managementLevelGroupId: input.data.managementLevelGroupId || null } : {}),
|
||||
...(input.data.managementLevelId !== undefined ? { managementLevelId: input.data.managementLevelId || null } : {}),
|
||||
...(input.data.resourceType !== undefined ? { resourceType: input.data.resourceType } : {}),
|
||||
...(input.data.chgResponsibility !== undefined ? { chgResponsibility: input.data.chgResponsibility } : {}),
|
||||
...(input.data.rolledOff !== undefined ? { rolledOff: input.data.rolledOff } : {}),
|
||||
...(input.data.departed !== undefined ? { departed: input.data.departed } : {}),
|
||||
...(input.data.enterpriseId !== undefined ? { enterpriseId: input.data.enterpriseId || null } : {}),
|
||||
...(input.data.clientUnitId !== undefined ? { clientUnitId: input.data.clientUnitId || null } : {}),
|
||||
...(input.data.fte !== undefined ? { fte: input.data.fte } : {}),
|
||||
} as unknown as Parameters<typeof ctx.db.resource.update>[0]["data"],
|
||||
include: {
|
||||
resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } } },
|
||||
},
|
||||
});
|
||||
const updated = await ctx.db.$transaction(async (tx) => {
|
||||
const result = await tx.resource.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
...(input.data.displayName !== undefined ? { displayName: input.data.displayName } : {}),
|
||||
...(input.data.email !== undefined ? { email: input.data.email } : {}),
|
||||
...(input.data.chapter !== undefined ? { chapter: input.data.chapter } : {}),
|
||||
...(input.data.lcrCents !== undefined ? { lcrCents: input.data.lcrCents } : {}),
|
||||
...(input.data.ucrCents !== undefined ? { ucrCents: input.data.ucrCents } : {}),
|
||||
...(input.data.currency !== undefined ? { currency: input.data.currency } : {}),
|
||||
...(input.data.chargeabilityTarget !== undefined ? { chargeabilityTarget: input.data.chargeabilityTarget } : {}),
|
||||
...(input.data.availability !== undefined ? { availability: input.data.availability as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
|
||||
...(input.data.skills !== undefined ? { skills: input.data.skills as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
|
||||
...(input.data.dynamicFields !== undefined ? { dynamicFields: input.data.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
|
||||
...(input.data.blueprintId !== undefined ? { blueprintId: input.data.blueprintId } : {}),
|
||||
...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}),
|
||||
...(input.data.portfolioUrl !== undefined ? { portfolioUrl: input.data.portfolioUrl || null } : {}),
|
||||
...(input.data.roleId !== undefined ? { roleId: input.data.roleId || null } : {}),
|
||||
...(input.data.postalCode !== undefined ? { postalCode: input.data.postalCode } : {}),
|
||||
...(input.data.postalCode && !input.data.federalState
|
||||
? { federalState: inferStateFromPostalCode(input.data.postalCode) }
|
||||
: input.data.federalState !== undefined
|
||||
? { federalState: input.data.federalState }
|
||||
: {}),
|
||||
...(input.data.countryId !== undefined ? { countryId: input.data.countryId || null } : {}),
|
||||
...(input.data.metroCityId !== undefined ? { metroCityId: input.data.metroCityId || null } : {}),
|
||||
...(input.data.orgUnitId !== undefined ? { orgUnitId: input.data.orgUnitId || null } : {}),
|
||||
...(input.data.managementLevelGroupId !== undefined ? { managementLevelGroupId: input.data.managementLevelGroupId || null } : {}),
|
||||
...(input.data.managementLevelId !== undefined ? { managementLevelId: input.data.managementLevelId || null } : {}),
|
||||
...(input.data.resourceType !== undefined ? { resourceType: input.data.resourceType } : {}),
|
||||
...(input.data.chgResponsibility !== undefined ? { chgResponsibility: input.data.chgResponsibility } : {}),
|
||||
...(input.data.rolledOff !== undefined ? { rolledOff: input.data.rolledOff } : {}),
|
||||
...(input.data.departed !== undefined ? { departed: input.data.departed } : {}),
|
||||
...(input.data.enterpriseId !== undefined ? { enterpriseId: input.data.enterpriseId || null } : {}),
|
||||
...(input.data.clientUnitId !== undefined ? { clientUnitId: input.data.clientUnitId || null } : {}),
|
||||
...(input.data.fte !== undefined ? { fte: input.data.fte } : {}),
|
||||
} as unknown as Parameters<typeof tx.resource.update>[0]["data"],
|
||||
include: {
|
||||
resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } } },
|
||||
},
|
||||
});
|
||||
|
||||
if (input.data.roles !== undefined) {
|
||||
await ctx.db.resourceRole.deleteMany({ where: { resourceId: input.id } });
|
||||
if (input.data.roles.length > 0) {
|
||||
await ctx.db.resourceRole.createMany({
|
||||
data: input.data.roles.map((role) => ({
|
||||
resourceId: input.id,
|
||||
roleId: role.roleId,
|
||||
isPrimary: role.isPrimary,
|
||||
})),
|
||||
});
|
||||
if (input.data.roles !== undefined) {
|
||||
await tx.resourceRole.deleteMany({ where: { resourceId: input.id } });
|
||||
if (input.data.roles.length > 0) {
|
||||
await tx.resourceRole.createMany({
|
||||
data: input.data.roles.map((role) => ({
|
||||
resourceId: input.id,
|
||||
roleId: role.roleId,
|
||||
isPrimary: role.isPrimary,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { before: existing, after: updated },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { before: existing, after: result },
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return updated;
|
||||
@@ -191,18 +199,22 @@ export const resourceMutationProcedures = {
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
||||
const resource = await ctx.db.resource.update({
|
||||
where: { id: input.id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
const resource = await ctx.db.$transaction(async (tx) => {
|
||||
const result = await tx.resource.update({
|
||||
where: { id: input.id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { after: { isActive: false } },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { after: { isActive: false } },
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return resource;
|
||||
@@ -212,19 +224,23 @@ export const resourceMutationProcedures = {
|
||||
.input(z.object({ ids: z.array(z.string()).min(1).max(100) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
||||
const updated = await ctx.db.$transaction(
|
||||
input.ids.map((id) =>
|
||||
ctx.db.resource.update({ where: { id }, data: { isActive: false } }),
|
||||
),
|
||||
);
|
||||
const updated = await ctx.db.$transaction(async (tx) => {
|
||||
const results = await Promise.all(
|
||||
input.ids.map((id) =>
|
||||
tx.resource.update({ where: { id }, data: { isActive: false } }),
|
||||
),
|
||||
);
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.ids.join(","),
|
||||
action: "UPDATE",
|
||||
changes: { after: { isActive: false, ids: input.ids } },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.ids.join(","),
|
||||
action: "UPDATE",
|
||||
changes: { after: { isActive: false, ids: input.ids } },
|
||||
},
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
return { count: updated.length };
|
||||
@@ -238,23 +254,25 @@ export const resourceMutationProcedures = {
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_RESOURCES);
|
||||
|
||||
await ctx.db.$transaction(
|
||||
input.ids.map((id) =>
|
||||
ctx.db.$executeRaw`
|
||||
UPDATE "Resource"
|
||||
SET "dynamicFields" = "dynamicFields" || ${JSON.stringify(input.fields)}::jsonb
|
||||
WHERE id = ${id}
|
||||
`,
|
||||
),
|
||||
);
|
||||
await ctx.db.$transaction(async (tx) => {
|
||||
await Promise.all(
|
||||
input.ids.map((id) =>
|
||||
tx.$executeRaw`
|
||||
UPDATE "Resource"
|
||||
SET "dynamicFields" = "dynamicFields" || ${JSON.stringify(input.fields)}::jsonb
|
||||
WHERE id = ${id}
|
||||
`,
|
||||
),
|
||||
);
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.ids.join(","),
|
||||
action: "UPDATE",
|
||||
changes: { after: { dynamicFields: input.fields, ids: input.ids } } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.ids.join(","),
|
||||
action: "UPDATE",
|
||||
changes: { after: { dynamicFields: input.fields, ids: input.ids } } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { updated: input.ids.length };
|
||||
@@ -271,20 +289,20 @@ export const resourceMutationProcedures = {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
|
||||
}
|
||||
|
||||
await ctx.db.$transaction([
|
||||
ctx.db.assignment.deleteMany({ where: { resourceId: input.id } }),
|
||||
ctx.db.vacation.deleteMany({ where: { resourceId: input.id } }),
|
||||
ctx.db.resource.delete({ where: { id: input.id } }),
|
||||
]);
|
||||
await ctx.db.$transaction(async (tx) => {
|
||||
await tx.assignment.deleteMany({ where: { resourceId: input.id } });
|
||||
await tx.vacation.deleteMany({ where: { resourceId: input.id } });
|
||||
await tx.resource.delete({ where: { id: input.id } });
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.id,
|
||||
action: "DELETE",
|
||||
userId: ctx.dbUser?.id,
|
||||
changes: { before: resource } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Resource",
|
||||
entityId: input.id,
|
||||
action: "DELETE",
|
||||
userId: ctx.dbUser?.id,
|
||||
changes: { before: resource } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { deleted: true };
|
||||
@@ -298,20 +316,20 @@ export const resourceMutationProcedures = {
|
||||
select: { id: true, displayName: true, eid: true },
|
||||
});
|
||||
|
||||
await ctx.db.$transaction([
|
||||
ctx.db.assignment.deleteMany({ where: { resourceId: { in: input.ids } } }),
|
||||
ctx.db.vacation.deleteMany({ where: { resourceId: { in: input.ids } } }),
|
||||
ctx.db.resource.deleteMany({ where: { id: { in: input.ids } } }),
|
||||
]);
|
||||
await ctx.db.$transaction(async (tx) => {
|
||||
await tx.assignment.deleteMany({ where: { resourceId: { in: input.ids } } });
|
||||
await tx.vacation.deleteMany({ where: { resourceId: { in: input.ids } } });
|
||||
await tx.resource.deleteMany({ where: { id: { in: input.ids } } });
|
||||
|
||||
await ctx.db.auditLog.createMany({
|
||||
data: resources.map((r) => ({
|
||||
entityType: "Resource",
|
||||
entityId: r.id,
|
||||
action: "DELETE",
|
||||
userId: ctx.dbUser?.id,
|
||||
changes: { before: r } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
})),
|
||||
await tx.auditLog.createMany({
|
||||
data: resources.map((r) => ({
|
||||
entityType: "Resource",
|
||||
entityId: r.id,
|
||||
action: "DELETE",
|
||||
userId: ctx.dbUser?.id,
|
||||
changes: { before: r } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
return { deleted: resources.length };
|
||||
|
||||
@@ -135,18 +135,22 @@ export async function createRole(
|
||||
requirePermission(ctx, PermissionKey.MANAGE_ROLES);
|
||||
await assertRoleNameAvailable(ctx.db, input.name);
|
||||
|
||||
const role = await ctx.db.role.create({
|
||||
data: buildRoleCreateData(input),
|
||||
include: { _count: { select: { resourceRoles: true } } },
|
||||
});
|
||||
const role = await ctx.db.$transaction(async (tx) => {
|
||||
const created = await tx.role.create({
|
||||
data: buildRoleCreateData(input),
|
||||
include: { _count: { select: { resourceRoles: true } } },
|
||||
});
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Role",
|
||||
entityId: role.id,
|
||||
action: "CREATE",
|
||||
changes: { after: role },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Role",
|
||||
entityId: created.id,
|
||||
action: "CREATE",
|
||||
changes: { after: created },
|
||||
},
|
||||
});
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
emitRoleCreated({ id: role.id, name: role.name });
|
||||
@@ -168,19 +172,23 @@ export async function updateRole(
|
||||
await assertRoleNameAvailable(ctx.db, input.data.name, input.id);
|
||||
}
|
||||
|
||||
const updated = await ctx.db.role.update({
|
||||
where: { id: input.id },
|
||||
data: buildRoleUpdateData(input.data),
|
||||
include: { _count: { select: { resourceRoles: true } } },
|
||||
});
|
||||
const updated = await ctx.db.$transaction(async (tx) => {
|
||||
const result = await tx.role.update({
|
||||
where: { id: input.id },
|
||||
data: buildRoleUpdateData(input.data),
|
||||
include: { _count: { select: { resourceRoles: true } } },
|
||||
});
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Role",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { before: existing, after: updated },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Role",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { before: existing, after: result },
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
emitRoleUpdated({ id: updated.id, name: updated.name });
|
||||
@@ -213,15 +221,17 @@ export async function deleteRole(
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.role.delete({ where: { id: input.id } });
|
||||
await ctx.db.$transaction(async (tx) => {
|
||||
await tx.role.delete({ where: { id: input.id } });
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Role",
|
||||
entityId: input.id,
|
||||
action: "DELETE",
|
||||
changes: { before: role },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Role",
|
||||
entityId: input.id,
|
||||
action: "DELETE",
|
||||
changes: { before: role },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
emitRoleDeleted(input.id);
|
||||
@@ -234,19 +244,23 @@ export async function deactivateRole(
|
||||
input: z.infer<typeof RoleIdInputSchema>,
|
||||
) {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_ROLES);
|
||||
const role = await ctx.db.role.update({
|
||||
where: { id: input.id },
|
||||
data: { isActive: false },
|
||||
include: { _count: { select: { resourceRoles: true } } },
|
||||
});
|
||||
const role = await ctx.db.$transaction(async (tx) => {
|
||||
const result = await tx.role.update({
|
||||
where: { id: input.id },
|
||||
data: { isActive: false },
|
||||
include: { _count: { select: { resourceRoles: true } } },
|
||||
});
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Role",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { after: { isActive: false } },
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Role",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
changes: { after: { isActive: false } },
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
emitRoleUpdated({ id: role.id, isActive: false });
|
||||
|
||||
Reference in New Issue
Block a user