feat(application): add dispo import commit flow
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { commitDispoImportBatch } from "../index.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("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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user