999626cf70
Move core entitlement business logic (syncEntitlement, balance reading, year summary, set/bulk-set) into packages/application/src/use-cases/entitlement/ using the deps-injection pattern. Audit logging stays in the router support file; authorization check for getBalance/getBalanceDetail stays in the router layer. The router support file becomes a thin wiring adapter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
192 lines
6.4 KiB
TypeScript
192 lines
6.4 KiB
TypeScript
import { z } from "zod";
|
|
import { TRPCError } from "@trpc/server";
|
|
import {
|
|
getEntitlementBalance as getEntitlementBalanceUseCase,
|
|
getEntitlementBalanceDetail as getEntitlementBalanceDetailUseCase,
|
|
getEntitlementSync,
|
|
getEntitlementYearSummary as getEntitlementYearSummaryUseCase,
|
|
getEntitlementYearSummaryDetail as getEntitlementYearSummaryDetailUseCase,
|
|
setEntitlement as setEntitlementUseCase,
|
|
bulkSetEntitlements as bulkSetEntitlementsUseCase,
|
|
type ReadEntitlementBalanceDeps,
|
|
} from "@capakraken/application";
|
|
import { createAuditEntry } from "../lib/audit.js";
|
|
import { loadResourceHolidayContext } from "../lib/resource-holiday-context.js";
|
|
import { countCalendarDaysInPeriod, countVacationChargeableDays } from "../lib/vacation-day-count.js";
|
|
import {
|
|
countVacationChargeableDaysFromSnapshot,
|
|
parseVacationSnapshotDateList,
|
|
} from "../lib/vacation-deduction-snapshot.js";
|
|
import type { TRPCContext } from "../trpc.js";
|
|
import { buildVacationPreview } from "./vacation-read-support.js";
|
|
|
|
export const EntitlementBalanceInputSchema = z.object({
|
|
resourceId: z.string(),
|
|
year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()),
|
|
});
|
|
|
|
export const EntitlementGetInputSchema = z.object({
|
|
resourceId: z.string(),
|
|
year: z.number().int(),
|
|
});
|
|
|
|
export const EntitlementSetInputSchema = z.object({
|
|
resourceId: z.string(),
|
|
year: z.number().int(),
|
|
entitledDays: z.number().min(0).max(365),
|
|
});
|
|
|
|
export const EntitlementBulkSetInputSchema = z.object({
|
|
year: z.number().int(),
|
|
entitledDays: z.number().min(0).max(365),
|
|
resourceIds: z.array(z.string()).optional(),
|
|
});
|
|
|
|
export const EntitlementYearSummaryInputSchema = z.object({
|
|
year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()),
|
|
chapter: z.string().optional(),
|
|
});
|
|
|
|
export const EntitlementYearSummaryDetailInputSchema = EntitlementYearSummaryInputSchema.extend({
|
|
resourceName: z.string().optional(),
|
|
});
|
|
|
|
function buildReadDeps(db: TRPCContext["db"]): ReadEntitlementBalanceDeps {
|
|
return {
|
|
loadResourceHolidayContext: (innerDb, resourceId, periodStart, periodEnd) =>
|
|
loadResourceHolidayContext(innerDb as TRPCContext["db"], resourceId, periodStart, periodEnd),
|
|
countCalendarDaysInPeriod,
|
|
countVacationChargeableDays,
|
|
countVacationChargeableDaysFromSnapshot,
|
|
parseVacationSnapshotDateList,
|
|
buildVacationPreview,
|
|
};
|
|
}
|
|
|
|
type EntitlementReadContext = Pick<TRPCContext, "db" | "dbUser">;
|
|
type EntitlementWriteContext = Pick<TRPCContext, "db" | "dbUser">;
|
|
|
|
export async function getEntitlementBalance(
|
|
ctx: EntitlementReadContext,
|
|
input: z.infer<typeof EntitlementBalanceInputSchema>,
|
|
) {
|
|
if (ctx.dbUser) {
|
|
const allowedRoles = ["ADMIN", "MANAGER", "CONTROLLER"];
|
|
if (!allowedRoles.includes(ctx.dbUser.systemRole)) {
|
|
const resource = await ctx.db.resource.findUnique({
|
|
where: { id: input.resourceId },
|
|
select: { userId: true },
|
|
});
|
|
if (!resource || resource.userId !== ctx.dbUser.id) {
|
|
throw new TRPCError({
|
|
code: "FORBIDDEN",
|
|
message: "You can only view your own vacation balance",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return getEntitlementBalanceUseCase(ctx.db, input, buildReadDeps(ctx.db));
|
|
}
|
|
|
|
export async function getEntitlementBalanceDetail(
|
|
ctx: EntitlementReadContext,
|
|
input: z.infer<typeof EntitlementBalanceInputSchema>,
|
|
) {
|
|
if (ctx.dbUser) {
|
|
const allowedRoles = ["ADMIN", "MANAGER", "CONTROLLER"];
|
|
if (!allowedRoles.includes(ctx.dbUser.systemRole)) {
|
|
const resource = await ctx.db.resource.findUnique({
|
|
where: { id: input.resourceId },
|
|
select: { userId: true },
|
|
});
|
|
if (!resource || resource.userId !== ctx.dbUser.id) {
|
|
throw new TRPCError({
|
|
code: "FORBIDDEN",
|
|
message: "You can only view your own vacation balance",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return getEntitlementBalanceDetailUseCase(ctx.db, input, buildReadDeps(ctx.db));
|
|
}
|
|
|
|
export async function getEntitlement(
|
|
ctx: Pick<TRPCContext, "db">,
|
|
input: z.infer<typeof EntitlementGetInputSchema>,
|
|
) {
|
|
return getEntitlementSync(ctx.db, input, buildReadDeps(ctx.db));
|
|
}
|
|
|
|
export async function setEntitlement(
|
|
ctx: EntitlementWriteContext,
|
|
input: z.infer<typeof EntitlementSetInputSchema>,
|
|
) {
|
|
const { result, existing } = await setEntitlementUseCase(ctx.db, input);
|
|
|
|
if (existing) {
|
|
void createAuditEntry({
|
|
db: ctx.db,
|
|
entityType: "VacationEntitlement",
|
|
entityId: result.id,
|
|
entityName: `Entitlement ${input.resourceId} / ${input.year}`,
|
|
action: "UPDATE",
|
|
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
|
before: existing as unknown as Record<string, unknown>,
|
|
after: result as unknown as Record<string, unknown>,
|
|
source: "ui",
|
|
summary: `Updated entitlement from ${existing.entitledDays} to ${input.entitledDays} days (${input.year})`,
|
|
});
|
|
} else {
|
|
void createAuditEntry({
|
|
db: ctx.db,
|
|
entityType: "VacationEntitlement",
|
|
entityId: result.id,
|
|
entityName: `Entitlement ${input.resourceId} / ${input.year}`,
|
|
action: "CREATE",
|
|
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
|
after: result as unknown as Record<string, unknown>,
|
|
source: "ui",
|
|
summary: `Set entitlement to ${input.entitledDays} days (${input.year})`,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export async function bulkSetEntitlements(
|
|
ctx: EntitlementWriteContext,
|
|
input: z.infer<typeof EntitlementBulkSetInputSchema>,
|
|
) {
|
|
const result = await bulkSetEntitlementsUseCase(ctx.db, input);
|
|
|
|
void createAuditEntry({
|
|
db: ctx.db,
|
|
entityType: "VacationEntitlement",
|
|
entityId: `bulk-${input.year}`,
|
|
entityName: `Bulk Entitlement ${input.year}`,
|
|
action: "UPDATE",
|
|
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
|
|
after: { year: input.year, entitledDays: input.entitledDays, resourceCount: result.updated } as unknown as Record<string, unknown>,
|
|
source: "ui",
|
|
summary: `Bulk set entitlement to ${input.entitledDays} days for ${result.updated} resources (${input.year})`,
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
export async function getEntitlementYearSummary(
|
|
ctx: Pick<TRPCContext, "db">,
|
|
input: z.infer<typeof EntitlementYearSummaryInputSchema>,
|
|
) {
|
|
return getEntitlementYearSummaryUseCase(ctx.db, input, buildReadDeps(ctx.db));
|
|
}
|
|
|
|
export async function getEntitlementYearSummaryDetail(
|
|
ctx: Pick<TRPCContext, "db">,
|
|
input: z.infer<typeof EntitlementYearSummaryDetailInputSchema>,
|
|
) {
|
|
return getEntitlementYearSummaryDetailUseCase(ctx.db, input, buildReadDeps(ctx.db));
|
|
}
|