625a842d89
Dashboard: expanded chargeability widget, resource/project table widgets with sorting and filters, stat cards with formatMoney integration. Chargeability: new report client with filtering, chargeability-bookings use case, updated dashboard overview logic. Dispo import: TBD project handling, parse-dispo-matrix improvements, stage-dispo-projects resource value scores, new tests. Estimates: CommercialTermsEditor component, commercial-terms engine module, expanded estimate schemas and types. UI: AppShell navigation updates, timeline filter/toolbar enhancements, role management improvements, signin page redesign, Tailwind/globals polish, SystemSettings SMTP section, anonymization support. Tests: new router tests (anonymization, chargeability, effort-rule, entitlement, estimate, experience-multiplier, notification, resource, staffing, vacation). Co-Authored-By: claude-flow <ruv@ruv.net>
709 lines
20 KiB
TypeScript
709 lines
20 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { commitDispoImportBatch } from "../index.js";
|
|
import { deriveTbdDispoProjectIdentity } from "../use-cases/dispo-import/tbd-projects.js";
|
|
|
|
function createCommitDb(overrides: Record<string, unknown> = {}) {
|
|
const tx = {
|
|
importBatch: {
|
|
update: vi.fn().mockResolvedValue({}),
|
|
},
|
|
utilizationCategory: {
|
|
upsert: vi.fn().mockResolvedValue({}),
|
|
findMany: vi.fn().mockResolvedValue([{ id: "util_chg", code: "Chg" }]),
|
|
},
|
|
role: {
|
|
upsert: vi.fn().mockResolvedValue({}),
|
|
findMany: vi.fn().mockResolvedValue([{ id: "role_ad", name: "Art Director" }]),
|
|
},
|
|
stagedResource: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
|
},
|
|
stagedProject: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
|
},
|
|
stagedAssignment: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
|
},
|
|
stagedVacation: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
|
},
|
|
stagedAvailabilityRule: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
|
},
|
|
user: {
|
|
findFirst: vi.fn().mockResolvedValue({ id: "admin_1" }),
|
|
},
|
|
country: {
|
|
findMany: vi.fn().mockResolvedValue([{ id: "country_de", code: "DE" }]),
|
|
},
|
|
metroCity: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
},
|
|
managementLevelGroup: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
},
|
|
managementLevel: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
},
|
|
client: {
|
|
findMany: vi.fn().mockResolvedValue([{ id: "client_bmw", code: "BMW", name: "BMW" }]),
|
|
},
|
|
orgUnit: {
|
|
findMany: vi.fn().mockResolvedValue([{ id: "org_ad", level: 7, name: "Art Direction" }]),
|
|
},
|
|
resource: {
|
|
upsert: vi.fn().mockResolvedValue({
|
|
id: "res_1",
|
|
availability: {
|
|
monday: 8,
|
|
tuesday: 8,
|
|
wednesday: 8,
|
|
thursday: 8,
|
|
friday: 8,
|
|
},
|
|
}),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
},
|
|
resourceRole: {
|
|
upsert: vi.fn().mockResolvedValue({}),
|
|
},
|
|
vacationEntitlement: {
|
|
upsert: vi.fn().mockResolvedValue({}),
|
|
},
|
|
project: {
|
|
upsert: vi.fn().mockResolvedValue({ id: "proj_1" }),
|
|
},
|
|
assignment: {
|
|
upsert: vi.fn().mockResolvedValue({}),
|
|
},
|
|
vacation: {
|
|
findFirst: vi.fn().mockResolvedValue(null),
|
|
create: vi.fn().mockResolvedValue({}),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
},
|
|
...overrides,
|
|
};
|
|
|
|
const db = {
|
|
importBatch: {
|
|
findUnique: vi.fn().mockResolvedValue({ id: "batch_1", status: "STAGED", summary: {} }),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
},
|
|
stagedUnresolvedRecord: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
},
|
|
$transaction: vi.fn(async (callback: (client: typeof tx) => Promise<unknown>) => callback(tx)),
|
|
...overrides,
|
|
};
|
|
|
|
return { db, tx };
|
|
}
|
|
|
|
describe("commitDispoImportBatch", () => {
|
|
it("commits resolved staged data and keeps [tbd] unresolved rows staged", async () => {
|
|
const { db, tx } = createCommitDb();
|
|
|
|
db.stagedUnresolvedRecord.findMany.mockResolvedValue([
|
|
{
|
|
id: "unresolved_1",
|
|
recordType: "PROJECT",
|
|
message: 'Planning token "[tbd]" references [tbd] and requires project resolution',
|
|
resolutionHint: "Resolve [tbd] rows before final project creation",
|
|
normalizedData: { rawToken: "[tbd]" },
|
|
status: "UNRESOLVED",
|
|
},
|
|
]);
|
|
|
|
tx.stagedResource.findMany.mockResolvedValue([
|
|
{
|
|
id: "sr_charge",
|
|
sourceKind: "CHARGEABILITY",
|
|
canonicalExternalId: "ada.director",
|
|
displayName: "Ada Director",
|
|
email: null,
|
|
chapter: "Art Direction",
|
|
chargeabilityTarget: 77.5,
|
|
clientUnitName: null,
|
|
countryCode: "DE",
|
|
fte: 1,
|
|
lcrCents: null,
|
|
managementLevelGroupName: null,
|
|
managementLevelName: null,
|
|
metroCityName: null,
|
|
resourceType: "EMPLOYEE",
|
|
roleTokens: ["AD"],
|
|
ucrCents: null,
|
|
availability: null,
|
|
rawPayload: {},
|
|
warnings: [],
|
|
},
|
|
{
|
|
id: "sr_roster",
|
|
sourceKind: "ROSTER",
|
|
canonicalExternalId: "ada.director",
|
|
displayName: "Ada Director",
|
|
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: {
|
|
sapOrgUnitLevelSeven: "Art Direction",
|
|
vacationDaysPerYear: 30,
|
|
},
|
|
warnings: [],
|
|
},
|
|
]);
|
|
|
|
tx.stagedProject.findMany.mockResolvedValue([
|
|
{
|
|
id: "sp_1",
|
|
shortCode: "11035763",
|
|
projectKey: "11035763",
|
|
name: "BMW Launch",
|
|
clientCode: "BMW",
|
|
utilizationCategoryCode: "Chg",
|
|
allocationType: "EXT",
|
|
orderType: "CHARGEABLE",
|
|
winProbability: 100,
|
|
isInternal: false,
|
|
isTbd: false,
|
|
startDate: new Date("2026-01-05T00:00:00.000Z"),
|
|
endDate: new Date("2026-01-06T00:00:00.000Z"),
|
|
warnings: [],
|
|
},
|
|
]);
|
|
|
|
tx.stagedAssignment.findMany.mockResolvedValue([
|
|
{
|
|
id: "sa_1",
|
|
resourceExternalId: "ada.director",
|
|
projectKey: "11035763",
|
|
assignmentDate: new Date("2026-01-05T00:00:00.000Z"),
|
|
startDate: new Date("2026-01-05T00:00:00.000Z"),
|
|
endDate: new Date("2026-01-05T00:00:00.000Z"),
|
|
hoursPerDay: 4,
|
|
percentage: 50,
|
|
roleToken: "AD",
|
|
roleName: "Art Director",
|
|
utilizationCategoryCode: "Chg",
|
|
isInternal: false,
|
|
isUnassigned: false,
|
|
isTbd: false,
|
|
},
|
|
{
|
|
id: "sa_2",
|
|
resourceExternalId: "ada.director",
|
|
projectKey: "11035763",
|
|
assignmentDate: new Date("2026-01-06T00:00:00.000Z"),
|
|
startDate: new Date("2026-01-06T00:00:00.000Z"),
|
|
endDate: new Date("2026-01-06T00:00:00.000Z"),
|
|
hoursPerDay: 4,
|
|
percentage: 50,
|
|
roleToken: "AD",
|
|
roleName: "Art Director",
|
|
utilizationCategoryCode: "Chg",
|
|
isInternal: false,
|
|
isUnassigned: false,
|
|
isTbd: false,
|
|
},
|
|
]);
|
|
|
|
tx.stagedVacation.findMany.mockResolvedValue([
|
|
{
|
|
id: "sv_1",
|
|
resourceExternalId: "ada.director",
|
|
vacationType: "PUBLIC_HOLIDAY",
|
|
startDate: new Date("2026-01-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-01-01T00:00:00.000Z"),
|
|
note: "New Year",
|
|
holidayName: "New Year",
|
|
isHalfDay: false,
|
|
halfDayPart: null,
|
|
},
|
|
]);
|
|
|
|
tx.stagedAvailabilityRule.findMany.mockResolvedValue([
|
|
{
|
|
id: "sar_1",
|
|
resourceExternalId: "ada.director",
|
|
effectiveStartDate: new Date("2026-01-05T00:00:00.000Z"),
|
|
effectiveEndDate: new Date("2026-01-05T00:00:00.000Z"),
|
|
availableHours: 4,
|
|
percentage: 50,
|
|
ruleType: "PART_TIME",
|
|
},
|
|
{
|
|
id: "sar_2",
|
|
resourceExternalId: "ada.director",
|
|
effectiveStartDate: new Date("2026-01-12T00:00:00.000Z"),
|
|
effectiveEndDate: new Date("2026-01-12T00:00:00.000Z"),
|
|
availableHours: 4,
|
|
percentage: 50,
|
|
ruleType: "PART_TIME",
|
|
},
|
|
]);
|
|
|
|
const result = await commitDispoImportBatch(db as never, {
|
|
importBatchId: "batch_1",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
batchId: "batch_1",
|
|
counts: {
|
|
committedAssignments: 1,
|
|
committedProjects: 1,
|
|
committedResources: 1,
|
|
committedVacations: 1,
|
|
updatedEntitlements: 1,
|
|
updatedResourceAvailabilities: 1,
|
|
upsertedResourceRoles: 2,
|
|
},
|
|
unresolved: {
|
|
blocked: 0,
|
|
skippedTbd: 1,
|
|
},
|
|
});
|
|
|
|
expect(tx.resource.upsert).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
create: expect.objectContaining({
|
|
eid: "ada.director",
|
|
email: "ada.director@accenture.com",
|
|
enterpriseId: "ada.director",
|
|
lcrCents: 1000,
|
|
ucrCents: 1500,
|
|
}),
|
|
}),
|
|
);
|
|
expect(tx.resource.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
availability: expect.objectContaining({
|
|
monday: 4,
|
|
tuesday: 8,
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
expect(tx.assignment.upsert).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
create: expect.objectContaining({
|
|
dailyCostCents: 4000,
|
|
endDate: new Date("2026-01-06T00:00:00.000Z"),
|
|
startDate: new Date("2026-01-05T00:00:00.000Z"),
|
|
}),
|
|
}),
|
|
);
|
|
expect(tx.vacationEntitlement.upsert).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
create: expect.objectContaining({
|
|
entitledDays: 30,
|
|
year: 2026,
|
|
}),
|
|
}),
|
|
);
|
|
expect(tx.importBatch.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
status: "COMMITTED",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("commits [tbd] rows as provisional projects when explicitly enabled", async () => {
|
|
const { db, tx } = createCommitDb();
|
|
const rawToken = "[DAI] 590 C AMG GT Stills [tbd]{CH} HB_";
|
|
const tbdProject = deriveTbdDispoProjectIdentity(rawToken, "Chg");
|
|
|
|
db.stagedUnresolvedRecord.findMany.mockResolvedValue([
|
|
{
|
|
id: "unresolved_1",
|
|
recordType: "PROJECT",
|
|
message: `Planning token "${rawToken}" references [tbd] and requires project resolution`,
|
|
resolutionHint: "Resolve [tbd] rows to a real WBS/project before commit",
|
|
normalizedData: { rawToken },
|
|
status: "UNRESOLVED",
|
|
},
|
|
]);
|
|
|
|
tx.stagedResource.findMany.mockResolvedValue([
|
|
{
|
|
id: "sr_roster",
|
|
sourceKind: "ROSTER",
|
|
canonicalExternalId: "h.noerenberg",
|
|
displayName: "Hartmut Norenberg",
|
|
email: "h.noerenberg@accenture.com",
|
|
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: {},
|
|
warnings: [],
|
|
},
|
|
]);
|
|
|
|
tx.stagedProject.findMany.mockResolvedValue([
|
|
{
|
|
id: "sp_tbd_1",
|
|
shortCode: tbdProject.shortCode,
|
|
projectKey: tbdProject.projectKey,
|
|
name: tbdProject.name,
|
|
clientCode: "DAI",
|
|
utilizationCategoryCode: "Chg",
|
|
allocationType: "EXT",
|
|
orderType: "CHARGEABLE",
|
|
winProbability: 100,
|
|
isInternal: false,
|
|
isTbd: true,
|
|
startDate: new Date("2026-02-03T00:00:00.000Z"),
|
|
endDate: new Date("2026-02-04T00:00:00.000Z"),
|
|
warnings: [],
|
|
rawPayload: {
|
|
rawTokens: [rawToken],
|
|
},
|
|
},
|
|
]);
|
|
|
|
tx.stagedAssignment.findMany.mockResolvedValue([
|
|
{
|
|
id: "sa_tbd_1",
|
|
resourceExternalId: "h.noerenberg",
|
|
projectKey: null,
|
|
assignmentDate: new Date("2026-02-03T00:00:00.000Z"),
|
|
startDate: new Date("2026-02-03T00:00:00.000Z"),
|
|
endDate: new Date("2026-02-03T00:00:00.000Z"),
|
|
hoursPerDay: 4,
|
|
percentage: 50,
|
|
roleToken: "AD",
|
|
roleName: "Art Director",
|
|
utilizationCategoryCode: "Chg",
|
|
isInternal: false,
|
|
isUnassigned: false,
|
|
isTbd: true,
|
|
rawPayload: {
|
|
rawToken,
|
|
},
|
|
},
|
|
{
|
|
id: "sa_tbd_2",
|
|
resourceExternalId: "h.noerenberg",
|
|
projectKey: null,
|
|
assignmentDate: new Date("2026-02-04T00:00:00.000Z"),
|
|
startDate: new Date("2026-02-04T00:00:00.000Z"),
|
|
endDate: new Date("2026-02-04T00:00:00.000Z"),
|
|
hoursPerDay: 4,
|
|
percentage: 50,
|
|
roleToken: "AD",
|
|
roleName: "Art Director",
|
|
utilizationCategoryCode: "Chg",
|
|
isInternal: false,
|
|
isUnassigned: false,
|
|
isTbd: true,
|
|
rawPayload: {
|
|
rawToken,
|
|
},
|
|
},
|
|
]);
|
|
|
|
const result = await commitDispoImportBatch(db as never, {
|
|
importBatchId: "batch_1",
|
|
importTbdProjects: true,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
batchId: "batch_1",
|
|
counts: {
|
|
committedAssignments: 1,
|
|
committedProjects: 1,
|
|
committedResources: 1,
|
|
committedVacations: 0,
|
|
updatedEntitlements: 0,
|
|
updatedResourceAvailabilities: 0,
|
|
upsertedResourceRoles: 2,
|
|
},
|
|
unresolved: {
|
|
blocked: 0,
|
|
skippedTbd: 1,
|
|
},
|
|
});
|
|
expect(tx.project.upsert).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { shortCode: tbdProject.shortCode },
|
|
create: expect.objectContaining({
|
|
name: tbdProject.name,
|
|
shortCode: tbdProject.shortCode,
|
|
status: "DRAFT",
|
|
}),
|
|
}),
|
|
);
|
|
expect(tx.assignment.upsert).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
create: expect.objectContaining({
|
|
endDate: new Date("2026-02-04T00:00:00.000Z"),
|
|
startDate: new Date("2026-02-03T00:00:00.000Z"),
|
|
}),
|
|
}),
|
|
);
|
|
expect(tx.stagedProject.updateMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { importBatchId: "batch_1" },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("blocks commit when non-[tbd] unresolved rows remain", async () => {
|
|
const { db } = createCommitDb();
|
|
|
|
db.stagedUnresolvedRecord.findMany.mockResolvedValue([
|
|
{
|
|
id: "unresolved_blocker",
|
|
recordType: "ASSIGNMENT",
|
|
message: "Unable to resolve project key from planning token",
|
|
resolutionHint: "Add a WBS token",
|
|
normalizedData: { rawToken: "[BMW] Launch" },
|
|
status: "UNRESOLVED",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
commitDispoImportBatch(db as never, { importBatchId: "batch_1" }),
|
|
).rejects.toThrow(/blocking unresolved staged record/);
|
|
|
|
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",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|