refactor(api): extract dispo management support
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
DispoStagedRecordType,
|
||||||
|
ImportBatchStatus,
|
||||||
|
StagedRecordStatus,
|
||||||
|
} from "@capakraken/db";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
assertImportBatchCancelable,
|
||||||
|
buildCancelledImportBatchUpdateData,
|
||||||
|
buildDispoImportCommitAuditSummary,
|
||||||
|
buildResolvedStagedRecordUpdateData,
|
||||||
|
resolveDispoStagedRecordStatus,
|
||||||
|
resolveDispoStagedRecordStoreKey,
|
||||||
|
terminalImportBatchStatuses,
|
||||||
|
} from "../router/dispo-management-support.js";
|
||||||
|
|
||||||
|
describe("dispo management support", () => {
|
||||||
|
it("guards terminal import batches from cancellation", () => {
|
||||||
|
expect(terminalImportBatchStatuses).toEqual([
|
||||||
|
ImportBatchStatus.COMMITTED,
|
||||||
|
ImportBatchStatus.CANCELLED,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(() => assertImportBatchCancelable({
|
||||||
|
id: "batch_1",
|
||||||
|
status: ImportBatchStatus.PENDING,
|
||||||
|
})).not.toThrow();
|
||||||
|
|
||||||
|
expect(() => assertImportBatchCancelable({
|
||||||
|
id: "batch_1",
|
||||||
|
status: ImportBatchStatus.CANCELLED,
|
||||||
|
})).toThrowError(
|
||||||
|
new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: 'Cannot cancel batch in status "CANCELLED"',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(buildCancelledImportBatchUpdateData()).toEqual({
|
||||||
|
status: ImportBatchStatus.CANCELLED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps staged-record actions and store keys", () => {
|
||||||
|
expect(resolveDispoStagedRecordStatus("APPROVE")).toBe(StagedRecordStatus.APPROVED);
|
||||||
|
expect(resolveDispoStagedRecordStatus("REJECT")).toBe(StagedRecordStatus.REJECTED);
|
||||||
|
expect(resolveDispoStagedRecordStatus("SKIP")).toBe(StagedRecordStatus.REJECTED);
|
||||||
|
|
||||||
|
expect(buildResolvedStagedRecordUpdateData("APPROVE")).toEqual({
|
||||||
|
status: StagedRecordStatus.APPROVED,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.RESOURCE)).toBe("stagedResource");
|
||||||
|
expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.AVAILABILITY_RULE)).toBe("stagedAvailabilityRule");
|
||||||
|
expect(resolveDispoStagedRecordStoreKey(DispoStagedRecordType.UNRESOLVED)).toBe("stagedUnresolvedRecord");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds commit audit summaries", () => {
|
||||||
|
expect(buildDispoImportCommitAuditSummary({
|
||||||
|
created: 4,
|
||||||
|
rejected: 1,
|
||||||
|
})).toBe('Committed import batch ({"created":4,"rejected":1})');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
DispoStagedRecordType,
|
||||||
|
ImportBatchStatus,
|
||||||
|
StagedRecordStatus,
|
||||||
|
} from "@capakraken/db";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
type StagedRecordAction = "APPROVE" | "REJECT" | "SKIP";
|
||||||
|
|
||||||
|
export const terminalImportBatchStatuses: readonly ImportBatchStatus[] = [
|
||||||
|
ImportBatchStatus.COMMITTED,
|
||||||
|
ImportBatchStatus.CANCELLED,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function assertImportBatchCancelable(input: {
|
||||||
|
id: string;
|
||||||
|
status: ImportBatchStatus;
|
||||||
|
}): void {
|
||||||
|
if (terminalImportBatchStatuses.includes(input.status)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Cannot cancel batch in status "${input.status}"`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDispoStagedRecordStatus(
|
||||||
|
action: StagedRecordAction,
|
||||||
|
): StagedRecordStatus {
|
||||||
|
switch (action) {
|
||||||
|
case "APPROVE":
|
||||||
|
return StagedRecordStatus.APPROVED;
|
||||||
|
case "REJECT":
|
||||||
|
case "SKIP":
|
||||||
|
return StagedRecordStatus.REJECTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDispoStagedRecordStoreKey(
|
||||||
|
recordType: DispoStagedRecordType,
|
||||||
|
): keyof DispoStagedRecordStores {
|
||||||
|
switch (recordType) {
|
||||||
|
case DispoStagedRecordType.RESOURCE:
|
||||||
|
return "stagedResource";
|
||||||
|
case DispoStagedRecordType.CLIENT:
|
||||||
|
return "stagedClient";
|
||||||
|
case DispoStagedRecordType.PROJECT:
|
||||||
|
return "stagedProject";
|
||||||
|
case DispoStagedRecordType.ASSIGNMENT:
|
||||||
|
return "stagedAssignment";
|
||||||
|
case DispoStagedRecordType.VACATION:
|
||||||
|
return "stagedVacation";
|
||||||
|
case DispoStagedRecordType.AVAILABILITY_RULE:
|
||||||
|
return "stagedAvailabilityRule";
|
||||||
|
case DispoStagedRecordType.UNRESOLVED:
|
||||||
|
return "stagedUnresolvedRecord";
|
||||||
|
default:
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Unknown record type: ${recordType as string}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DispoStagedRecordStores = {
|
||||||
|
stagedResource: { update: Function };
|
||||||
|
stagedClient: { update: Function };
|
||||||
|
stagedProject: { update: Function };
|
||||||
|
stagedAssignment: { update: Function };
|
||||||
|
stagedVacation: { update: Function };
|
||||||
|
stagedAvailabilityRule: { update: Function };
|
||||||
|
stagedUnresolvedRecord: { update: Function };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildCancelledImportBatchUpdateData() {
|
||||||
|
return { status: ImportBatchStatus.CANCELLED } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildResolvedStagedRecordUpdateData(
|
||||||
|
action: StagedRecordAction,
|
||||||
|
) {
|
||||||
|
return { status: resolveDispoStagedRecordStatus(action) } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDispoImportCommitAuditSummary(
|
||||||
|
counts: Record<string, unknown>,
|
||||||
|
): string {
|
||||||
|
return `Committed import batch (${JSON.stringify(counts)})`;
|
||||||
|
}
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
DispoStagedRecordType,
|
DispoStagedRecordType,
|
||||||
ImportBatchStatus,
|
|
||||||
StagedRecordStatus,
|
|
||||||
} from "@capakraken/db";
|
} from "@capakraken/db";
|
||||||
import { commitDispoImportBatch } from "@capakraken/application";
|
import { commitDispoImportBatch } from "@capakraken/application";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { createAuditEntry } from "../lib/audit.js";
|
import { createAuditEntry } from "../lib/audit.js";
|
||||||
|
import {
|
||||||
|
assertImportBatchCancelable,
|
||||||
|
buildCancelledImportBatchUpdateData,
|
||||||
|
buildDispoImportCommitAuditSummary,
|
||||||
|
buildResolvedStagedRecordUpdateData,
|
||||||
|
resolveDispoStagedRecordStoreKey,
|
||||||
|
type DispoStagedRecordStores,
|
||||||
|
} from "./dispo-management-support.js";
|
||||||
|
|
||||||
type AuditDb = Parameters<typeof createAuditEntry>[0]["db"];
|
type AuditDb = Parameters<typeof createAuditEntry>[0]["db"];
|
||||||
type CommitDb = Parameters<typeof commitDispoImportBatch>[0];
|
type CommitDb = Parameters<typeof commitDispoImportBatch>[0];
|
||||||
@@ -24,21 +30,11 @@ export async function cancelImportBatch(
|
|||||||
if (!batch) {
|
if (!batch) {
|
||||||
throw new TRPCError({ code: "NOT_FOUND", message: `Import batch "${input.id}" not found` });
|
throw new TRPCError({ code: "NOT_FOUND", message: `Import batch "${input.id}" not found` });
|
||||||
}
|
}
|
||||||
|
assertImportBatchCancelable({ id: input.id, status: batch.status });
|
||||||
const terminalStatuses: ImportBatchStatus[] = [
|
|
||||||
ImportBatchStatus.COMMITTED,
|
|
||||||
ImportBatchStatus.CANCELLED,
|
|
||||||
];
|
|
||||||
if (terminalStatuses.includes(batch.status)) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `Cannot cancel batch in status "${batch.status}"`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelled = await db.importBatch.update({
|
const cancelled = await db.importBatch.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: { status: ImportBatchStatus.CANCELLED },
|
data: buildCancelledImportBatchUpdateData(),
|
||||||
});
|
});
|
||||||
|
|
||||||
void createAuditEntry({
|
void createAuditEntry({
|
||||||
@@ -56,66 +52,14 @@ export async function cancelImportBatch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveStagedRecord(
|
export async function resolveStagedRecord(
|
||||||
db: {
|
db: DispoStagedRecordStores,
|
||||||
stagedResource: { update: Function };
|
|
||||||
stagedClient: { update: Function };
|
|
||||||
stagedProject: { update: Function };
|
|
||||||
stagedAssignment: { update: Function };
|
|
||||||
stagedVacation: { update: Function };
|
|
||||||
stagedAvailabilityRule: { update: Function };
|
|
||||||
stagedUnresolvedRecord: { update: Function };
|
|
||||||
},
|
|
||||||
input: { action: "APPROVE" | "REJECT" | "SKIP"; id: string; recordType: DispoStagedRecordType },
|
input: { action: "APPROVE" | "REJECT" | "SKIP"; id: string; recordType: DispoStagedRecordType },
|
||||||
) {
|
) {
|
||||||
const statusMap: Record<string, StagedRecordStatus> = {
|
const storeKey = resolveDispoStagedRecordStoreKey(input.recordType);
|
||||||
APPROVE: StagedRecordStatus.APPROVED,
|
return db[storeKey].update({
|
||||||
REJECT: StagedRecordStatus.REJECTED,
|
|
||||||
SKIP: StagedRecordStatus.REJECTED,
|
|
||||||
};
|
|
||||||
const nextStatus = statusMap[input.action]!;
|
|
||||||
|
|
||||||
switch (input.recordType) {
|
|
||||||
case DispoStagedRecordType.RESOURCE:
|
|
||||||
return db.stagedResource.update({
|
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: { status: nextStatus },
|
data: buildResolvedStagedRecordUpdateData(input.action),
|
||||||
});
|
});
|
||||||
case DispoStagedRecordType.CLIENT:
|
|
||||||
return db.stagedClient.update({
|
|
||||||
where: { id: input.id },
|
|
||||||
data: { status: nextStatus },
|
|
||||||
});
|
|
||||||
case DispoStagedRecordType.PROJECT:
|
|
||||||
return db.stagedProject.update({
|
|
||||||
where: { id: input.id },
|
|
||||||
data: { status: nextStatus },
|
|
||||||
});
|
|
||||||
case DispoStagedRecordType.ASSIGNMENT:
|
|
||||||
return db.stagedAssignment.update({
|
|
||||||
where: { id: input.id },
|
|
||||||
data: { status: nextStatus },
|
|
||||||
});
|
|
||||||
case DispoStagedRecordType.VACATION:
|
|
||||||
return db.stagedVacation.update({
|
|
||||||
where: { id: input.id },
|
|
||||||
data: { status: nextStatus },
|
|
||||||
});
|
|
||||||
case DispoStagedRecordType.AVAILABILITY_RULE:
|
|
||||||
return db.stagedAvailabilityRule.update({
|
|
||||||
where: { id: input.id },
|
|
||||||
data: { status: nextStatus },
|
|
||||||
});
|
|
||||||
case DispoStagedRecordType.UNRESOLVED:
|
|
||||||
return db.stagedUnresolvedRecord.update({
|
|
||||||
where: { id: input.id },
|
|
||||||
data: { status: nextStatus },
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `Unknown record type: ${input.recordType as string}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function commitImportBatch(
|
export async function commitImportBatch(
|
||||||
@@ -140,7 +84,7 @@ export async function commitImportBatch(
|
|||||||
entityId: input.importBatchId,
|
entityId: input.importBatchId,
|
||||||
entityName: input.importBatchId,
|
entityName: input.importBatchId,
|
||||||
action: "IMPORT",
|
action: "IMPORT",
|
||||||
summary: `Committed import batch (${JSON.stringify(counts)})`,
|
summary: buildDispoImportCommitAuditSummary(counts),
|
||||||
after: counts,
|
after: counts,
|
||||||
source: "ui",
|
source: "ui",
|
||||||
...(input.userId !== undefined ? { userId: input.userId } : {}),
|
...(input.userId !== undefined ? { userId: input.userId } : {}),
|
||||||
|
|||||||
Reference in New Issue
Block a user