feat(application): complete dispo import operator flow

This commit is contained in:
2026-03-14 15:14:55 +01:00
parent 6a2f552ccb
commit 4dabb9d4ce
11 changed files with 1034 additions and 32 deletions
@@ -351,4 +351,201 @@ describe("commitDispoImportBatch", () => {
expect(db.$transaction).not.toHaveBeenCalled();
});
it("falls back to the staged resource role when an assignment row has no role", async () => {
const { db, tx } = createCommitDb({
role: {
upsert: vi.fn().mockResolvedValue({}),
findMany: vi.fn().mockResolvedValue([
{ id: "role_ad", name: "Art Director" },
{ id: "role_pm", name: "Project Manager" },
]),
},
});
tx.stagedResource.findMany.mockResolvedValue([
{
id: "sr_roster",
sourceKind: "ROSTER",
canonicalExternalId: "catharina.voelkle",
displayName: "Catharina Voelkle",
email: null,
chapter: "Art Direction",
chargeabilityTarget: null,
clientUnitName: null,
countryCode: "DE",
fte: 1,
lcrCents: 1000,
managementLevelGroupName: null,
managementLevelName: null,
metroCityName: null,
resourceType: "EMPLOYEE",
roleTokens: ["AD"],
ucrCents: 1500,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
},
rawPayload: {
department: "Digital Designer",
},
warnings: [],
},
]);
tx.stagedProject.findMany.mockResolvedValue([
{
id: "sp_1",
shortCode: "generic",
projectKey: "generic",
name: "Generic Work",
clientCode: "BMW",
utilizationCategoryCode: "Chg",
allocationType: "EXT",
orderType: "CHARGEABLE",
winProbability: 100,
isInternal: false,
isTbd: false,
startDate: new Date("2026-03-12T00:00:00.000Z"),
endDate: new Date("2026-03-12T00:00:00.000Z"),
warnings: [],
},
]);
tx.stagedAssignment.findMany.mockResolvedValue([
{
id: "sa_missing_role",
resourceExternalId: "catharina.voelkle",
projectKey: "generic",
assignmentDate: new Date("2026-03-12T00:00:00.000Z"),
startDate: new Date("2026-03-12T00:00:00.000Z"),
endDate: new Date("2026-03-12T00:00:00.000Z"),
hoursPerDay: 8,
percentage: 100,
roleToken: null,
roleName: null,
utilizationCategoryCode: "Chg",
isInternal: false,
isUnassigned: false,
isTbd: false,
},
]);
const result = await commitDispoImportBatch(db as never, {
importBatchId: "batch_1",
});
expect(result.counts.committedAssignments).toBe(1);
expect(tx.assignment.upsert).toHaveBeenCalledWith(
expect.objectContaining({
create: expect.objectContaining({
role: "Art Director",
}),
}),
);
});
it("creates a role from the roster department when no canonical dispo role exists", async () => {
const { db, tx } = createCommitDb({
role: {
upsert: vi.fn(async ({ where }: { where: { name: string } }) => ({
id: `role_${where.name.toLowerCase().replace(/\s+/g, "_")}`,
name: where.name,
})),
findMany: vi.fn().mockResolvedValue([{ id: "role_ad", name: "Art Director" }]),
},
});
tx.stagedResource.findMany.mockResolvedValue([
{
id: "sr_roster",
sourceKind: "ROSTER",
canonicalExternalId: "dennis.steinschulte",
displayName: "Dennis Steinschulte",
email: null,
chapter: "Product Data Management",
chargeabilityTarget: null,
clientUnitName: null,
countryCode: "DE",
fte: 1,
lcrCents: 1000,
managementLevelGroupName: null,
managementLevelName: null,
metroCityName: null,
resourceType: "EMPLOYEE",
roleTokens: [],
ucrCents: 1500,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
},
rawPayload: {
department: "Vis Logic",
},
warnings: [],
},
]);
tx.stagedProject.findMany.mockResolvedValue([
{
id: "sp_1",
shortCode: "INT-MO",
projectKey: "generic",
name: "Management & Operations",
clientCode: "BMW",
utilizationCategoryCode: "M&O",
allocationType: "INT",
orderType: "NONCHARGEABLE",
winProbability: 100,
isInternal: true,
isTbd: false,
startDate: new Date("2026-01-12T00:00:00.000Z"),
endDate: new Date("2026-01-12T00:00:00.000Z"),
warnings: [],
},
]);
tx.stagedAssignment.findMany.mockResolvedValue([
{
id: "sa_missing_role",
resourceExternalId: "dennis.steinschulte",
projectKey: "generic",
assignmentDate: new Date("2026-01-12T00:00:00.000Z"),
startDate: new Date("2026-01-12T00:00:00.000Z"),
endDate: new Date("2026-01-12T00:00:00.000Z"),
hoursPerDay: 8,
percentage: 100,
roleToken: null,
roleName: null,
utilizationCategoryCode: "M&O",
isInternal: true,
isUnassigned: false,
isTbd: false,
},
]);
const result = await commitDispoImportBatch(db as never, {
importBatchId: "batch_1",
});
expect(result.counts.committedAssignments).toBe(1);
expect(tx.role.upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { name: "Vis Logic" },
}),
);
expect(tx.assignment.upsert).toHaveBeenCalledWith(
expect.objectContaining({
create: expect.objectContaining({
role: "Vis Logic",
}),
}),
);
});
});
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { deriveRoleTokens } from "../use-cases/dispo-import/shared.js";
describe("deriveRoleTokens", () => {
it("recognizes common roster department and title variants", () => {
expect(deriveRoleTokens("2D Nuke")).toEqual(["2D"]);
expect(deriveRoleTokens("Motion Design")).toEqual(["2D"]);
expect(deriveRoleTokens("3D Modeling")).toEqual(["3D"]);
expect(deriveRoleTokens("Digital Producer")).toEqual(["PM"]);
expect(deriveRoleTokens("Head of Delivery")).toEqual(["PM"]);
expect(deriveRoleTokens("Digital Designer")).toEqual(["AD"]);
});
});