cd78f72f33
Complete rename of all technical identifiers across the codebase: Package names (11 packages): - @planarchy/* → @capakraken/* in all package.json, tsconfig, imports Import statements: 277 files, 548 occurrences replaced Database & Docker: - PostgreSQL user/db: planarchy → capakraken - Docker volumes: planarchy_pgdata → capakraken_pgdata - Connection strings updated in docker-compose, .env, CI CI/CD: - GitHub Actions workflow: all filter commands updated - Test database credentials updated Infrastructure: - Redis channel: planarchy:sse → capakraken:sse - Logger service name: planarchy-api → capakraken-api - Anonymization seed updated - Start/stop/restart scripts updated Test data: - Seed emails: @planarchy.dev → @capakraken.dev - E2E test credentials: all 11 spec files updated - Email defaults: @planarchy.app → @capakraken.app - localStorage keys: planarchy_* → capakraken_* Documentation: 30+ .md files updated Verification: - pnpm install: workspace resolution works - TypeScript: only pre-existing TS2589 (no new errors) - Engine: 310/310 tests pass - Staffing: 37/37 tests pass Co-Authored-By: claude-flow <ruv@ruv.net>
574 lines
22 KiB
TypeScript
574 lines
22 KiB
TypeScript
import {
|
|
DISPO_REQUIRED_ROLE_SEEDS,
|
|
DISPO_UTILIZATION_CATEGORIES,
|
|
normalizeDispoRoleToken,
|
|
} from "@capakraken/shared";
|
|
import type { Prisma } from "@capakraken/db";
|
|
import {
|
|
AllocationStatus,
|
|
ImportBatchStatus,
|
|
ProjectStatus,
|
|
StagedRecordStatus,
|
|
VacationStatus,
|
|
} from "@capakraken/db";
|
|
import {
|
|
buildBatchSummaryEntry,
|
|
buildFallbackAccentureEmail,
|
|
toJsonObject,
|
|
} from "./shared.js";
|
|
import { recomputeResourceValueScores } from "../resource/recompute-resource-value-scores.js";
|
|
import { classifyDispoProject } from "./tbd-projects.js";
|
|
import type { CommitDbClient, MergedStagedResource, TxClient } from "./commit-dispo-batch-types.js";
|
|
import { validateDispoBatch } from "./validate-dispo-batch.js";
|
|
import type { ReferenceDataMaps } from "./build-dispo-maps.js";
|
|
import {
|
|
buildReferenceDataMaps,
|
|
inferRoleNameFromResource,
|
|
mergeStagedResources,
|
|
parseWeekdayAvailability,
|
|
} from "./build-dispo-maps.js";
|
|
import {
|
|
aggregateAssignments,
|
|
deriveOverlayAvailability,
|
|
} from "./determine-placement.js";
|
|
|
|
export interface CommitDispoImportBatchInput {
|
|
allowTbdUnresolved?: boolean;
|
|
importTbdProjects?: boolean;
|
|
importBatchId: string;
|
|
}
|
|
|
|
export interface CommitDispoImportBatchResult {
|
|
batchId: string;
|
|
counts: {
|
|
committedAssignments: number;
|
|
committedProjects: number;
|
|
committedResources: number;
|
|
committedVacations: number;
|
|
updatedEntitlements: number;
|
|
updatedResourceAvailabilities: number;
|
|
upsertedResourceRoles: number;
|
|
};
|
|
unresolved: {
|
|
blocked: number;
|
|
skippedTbd: number;
|
|
};
|
|
}
|
|
|
|
function asObject(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: {};
|
|
}
|
|
|
|
function normalizeDate(date: Date): Date {
|
|
return new Date(`${date.toISOString().slice(0, 10)}T00:00:00.000Z`);
|
|
}
|
|
|
|
function roundToOneDecimal(value: number): number {
|
|
return Math.round(value * 10) / 10;
|
|
}
|
|
|
|
async function upsertRoleSeeds(tx: TxClient) {
|
|
for (const role of DISPO_REQUIRED_ROLE_SEEDS) {
|
|
await tx.role.upsert({
|
|
where: { name: role.name },
|
|
update: { description: role.description, isActive: true },
|
|
create: { ...role, isActive: true },
|
|
});
|
|
}
|
|
}
|
|
|
|
async function upsertUtilizationCategories(tx: TxClient) {
|
|
for (const category of DISPO_UTILIZATION_CATEGORIES) {
|
|
await tx.utilizationCategory.upsert({
|
|
where: { code: category.code },
|
|
update: {
|
|
description: category.description,
|
|
isActive: true,
|
|
isDefault: category.isDefault,
|
|
name: category.name,
|
|
sortOrder: category.sortOrder,
|
|
},
|
|
create: category,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function ensureInferredRolesExist(
|
|
tx: TxClient,
|
|
mergedResources: Map<string, MergedStagedResource>,
|
|
roleIdByName: Map<string, string>,
|
|
) {
|
|
const inferredRoleNames = new Set<string>();
|
|
for (const resource of mergedResources.values()) {
|
|
const name = inferRoleNameFromResource(resource);
|
|
if (name) inferredRoleNames.add(name);
|
|
}
|
|
for (const roleName of inferredRoleNames) {
|
|
if (roleIdByName.has(roleName)) continue;
|
|
const role = await tx.role.upsert({
|
|
where: { name: roleName },
|
|
update: { description: "Imported Dispo resource role", isActive: true },
|
|
create: { name: roleName, description: "Imported Dispo resource role", isActive: true },
|
|
select: { id: true, name: true },
|
|
});
|
|
roleIdByName.set(role.name, role.id);
|
|
}
|
|
}
|
|
|
|
function buildResourceData(
|
|
resource: MergedStagedResource,
|
|
maps: ReferenceDataMaps,
|
|
batchId: string,
|
|
) {
|
|
const managementGroup = resource.managementLevelGroupName
|
|
? maps.managementLevelGroupByName.get(resource.managementLevelGroupName)
|
|
: null;
|
|
const defaultChargeabilityTarget = managementGroup
|
|
? roundToOneDecimal(managementGroup.targetPercentage * 100)
|
|
: 80;
|
|
const availability = parseWeekdayAvailability(resource.availability, resource.fte);
|
|
const rawPayload = resource.rawPayload;
|
|
const orgUnitId =
|
|
(typeof rawPayload.sapOrgUnitLevelSeven === "string"
|
|
? maps.orgUnitIdByLevelAndName.get(`7:${rawPayload.sapOrgUnitLevelSeven.toLowerCase()}`)
|
|
: null) ??
|
|
(typeof rawPayload.sapOrgUnitLevelSix === "string"
|
|
? maps.orgUnitIdByLevelAndName.get(`6:${rawPayload.sapOrgUnitLevelSix.toLowerCase()}`)
|
|
: null) ??
|
|
(typeof rawPayload.sapOrgUnitLevelFive === "string"
|
|
? maps.orgUnitIdByLevelAndName.get(`5:${rawPayload.sapOrgUnitLevelFive.toLowerCase()}`)
|
|
: null) ??
|
|
null;
|
|
|
|
const shared = {
|
|
availability: availability as unknown as Prisma.InputJsonValue,
|
|
chapter: resource.chapter,
|
|
chargeabilityTarget: resource.chargeabilityTarget ?? defaultChargeabilityTarget,
|
|
clientUnitId: resource.clientUnitName
|
|
? (maps.clientIdByCode.get(resource.clientUnitName) ?? maps.clientIdByName.get(resource.clientUnitName.toLowerCase()) ?? null)
|
|
: null,
|
|
countryId: resource.countryCode ? (maps.countryIdByCode.get(resource.countryCode) ?? null) : null,
|
|
displayName: resource.displayName ?? resource.canonicalExternalId,
|
|
email: resource.email ?? buildFallbackAccentureEmail(resource.canonicalExternalId),
|
|
fte: resource.fte ?? 1,
|
|
lcrCents: resource.lcrCents ?? 0,
|
|
managementLevelGroupId: resource.managementLevelGroupName ? (managementGroup?.id ?? null) : null,
|
|
managementLevelId: resource.managementLevelName
|
|
? (maps.managementLevelIdByName.get(resource.managementLevelName) ?? null)
|
|
: null,
|
|
metroCityId: resource.metroCityName
|
|
? (maps.metroCityIdByName.get(resource.metroCityName.toLowerCase()) ?? null)
|
|
: null,
|
|
orgUnitId,
|
|
resourceType: resource.resourceType ?? "EMPLOYEE",
|
|
ucrCents: resource.ucrCents ?? 0,
|
|
} as const;
|
|
|
|
const dynamicFields = {
|
|
dispoImport: {
|
|
importBatchId: batchId,
|
|
sourceKinds: resource.sourceKinds,
|
|
warnings: resource.warnings,
|
|
},
|
|
} as Prisma.InputJsonValue;
|
|
|
|
return { shared, dynamicFields, managementGroup };
|
|
}
|
|
|
|
interface CommitResourcesResult {
|
|
resourceIdByKey: Map<string, string>;
|
|
resourceRoleNameByKey: Map<string, string>;
|
|
updatedEntitlements: number;
|
|
updatedResourceAvailabilities: number;
|
|
upsertedResourceRoles: number;
|
|
}
|
|
|
|
async function commitResources(
|
|
tx: TxClient,
|
|
mergedResources: Map<string, MergedStagedResource>,
|
|
maps: ReferenceDataMaps,
|
|
batchId: string,
|
|
stagedVacations: Awaited<ReturnType<TxClient["stagedVacation"]["findMany"]>>,
|
|
stagedAvailabilityRules: Awaited<ReturnType<TxClient["stagedAvailabilityRule"]["findMany"]>>,
|
|
): Promise<CommitResourcesResult> {
|
|
const resourceIdByKey = new Map<string, string>();
|
|
const resourceRoleNameByKey = new Map<string, string>();
|
|
let upsertedResourceRoles = 0;
|
|
let updatedResourceAvailabilities = 0;
|
|
let updatedEntitlements = 0;
|
|
|
|
for (const resource of mergedResources.values()) {
|
|
const inferredRoleName = inferRoleNameFromResource(resource);
|
|
if (inferredRoleName) {
|
|
resourceRoleNameByKey.set(resource.canonicalExternalId, inferredRoleName);
|
|
}
|
|
|
|
const { shared, dynamicFields } = buildResourceData(resource, maps, batchId);
|
|
|
|
const committed = await tx.resource.upsert({
|
|
where: { eid: resource.canonicalExternalId },
|
|
update: { ...shared, enterpriseId: resource.canonicalExternalId, dynamicFields },
|
|
create: {
|
|
...shared,
|
|
dynamicFields,
|
|
eid: resource.canonicalExternalId,
|
|
enterpriseId: resource.canonicalExternalId,
|
|
},
|
|
select: { id: true, availability: true },
|
|
});
|
|
|
|
resourceIdByKey.set(resource.canonicalExternalId, committed.id);
|
|
|
|
// Upsert resource roles
|
|
const resourceRoleNames = new Set(
|
|
Array.from(resource.roleTokens)
|
|
.map((roleToken) => normalizeDispoRoleToken(roleToken))
|
|
.filter((roleName): roleName is string => Boolean(roleName)),
|
|
);
|
|
if (inferredRoleName) resourceRoleNames.add(inferredRoleName);
|
|
|
|
for (const roleName of resourceRoleNames) {
|
|
const roleId = maps.roleIdByName.get(roleName);
|
|
if (!roleId) continue;
|
|
await tx.resourceRole.upsert({
|
|
where: { resourceId_roleId: { resourceId: committed.id, roleId } },
|
|
update: {},
|
|
create: { resourceId: committed.id, roleId },
|
|
});
|
|
upsertedResourceRoles += 1;
|
|
}
|
|
|
|
// Upsert vacation entitlements
|
|
if (resource.vacationDaysPerYear !== null && resource.vacationDaysPerYear !== undefined) {
|
|
const entitlementYears = new Set<number>(
|
|
stagedVacations
|
|
.filter((v) => v.resourceExternalId === resource.canonicalExternalId)
|
|
.map((v) => v.startDate.getUTCFullYear()),
|
|
);
|
|
if (entitlementYears.size === 0) entitlementYears.add(new Date().getUTCFullYear());
|
|
for (const year of entitlementYears) {
|
|
await tx.vacationEntitlement.upsert({
|
|
where: { resourceId_year: { resourceId: committed.id, year } },
|
|
update: { entitledDays: resource.vacationDaysPerYear },
|
|
create: { resourceId: committed.id, year, entitledDays: resource.vacationDaysPerYear },
|
|
});
|
|
updatedEntitlements += 1;
|
|
}
|
|
}
|
|
|
|
// Apply availability overlay rules
|
|
const resourceRules = stagedAvailabilityRules.filter(
|
|
(rule) => rule.resourceExternalId === resource.canonicalExternalId,
|
|
);
|
|
if (resourceRules.length > 0) {
|
|
const overlaidAvailability = deriveOverlayAvailability(
|
|
parseWeekdayAvailability(committed.availability, resource.fte),
|
|
resourceRules,
|
|
);
|
|
await tx.resource.update({
|
|
where: { id: committed.id },
|
|
data: {
|
|
availability: overlaidAvailability as unknown as Prisma.InputJsonValue,
|
|
dynamicFields: {
|
|
dispoImport: {
|
|
availabilityRules: resourceRules.map((rule) => ({
|
|
availableHours: rule.availableHours,
|
|
effectiveEndDate: rule.effectiveEndDate?.toISOString().slice(0, 10) ?? null,
|
|
effectiveStartDate: rule.effectiveStartDate?.toISOString().slice(0, 10) ?? null,
|
|
percentage: rule.percentage,
|
|
ruleType: rule.ruleType,
|
|
})),
|
|
importBatchId: batchId,
|
|
sourceKinds: resource.sourceKinds,
|
|
warnings: resource.warnings,
|
|
},
|
|
} as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
updatedResourceAvailabilities += 1;
|
|
}
|
|
}
|
|
|
|
return { resourceIdByKey, resourceRoleNameByKey, updatedEntitlements, updatedResourceAvailabilities, upsertedResourceRoles };
|
|
}
|
|
|
|
async function commitProjects(
|
|
tx: TxClient,
|
|
stagedProjects: Awaited<ReturnType<TxClient["stagedProject"]["findMany"]>>,
|
|
maps: ReferenceDataMaps,
|
|
batchId: string,
|
|
importTbdProjects: boolean,
|
|
): Promise<Map<string, string>> {
|
|
const projectIdByShortCode = new Map<string, string>();
|
|
|
|
for (const stagedProject of stagedProjects) {
|
|
if (stagedProject.isTbd && !importTbdProjects) continue;
|
|
const shortCode = stagedProject.shortCode ?? stagedProject.projectKey;
|
|
const classification = classifyDispoProject(stagedProject.utilizationCategoryCode ?? null);
|
|
const projectStatus = stagedProject.isTbd ? ProjectStatus.DRAFT : ProjectStatus.ACTIVE;
|
|
const dynamicFields = {
|
|
dispoImport: {
|
|
importBatchId: batchId,
|
|
isInternal: stagedProject.isInternal,
|
|
isTbd: stagedProject.isTbd,
|
|
projectKey: stagedProject.projectKey,
|
|
rawTokens: Array.isArray(asObject(stagedProject.rawPayload).rawTokens)
|
|
? asObject(stagedProject.rawPayload).rawTokens
|
|
: [],
|
|
warnings: stagedProject.warnings,
|
|
},
|
|
} as Prisma.InputJsonValue;
|
|
|
|
const project = await tx.project.upsert({
|
|
where: { shortCode },
|
|
update: {
|
|
allocationType: stagedProject.allocationType ?? classification.allocationType,
|
|
budgetCents: 0,
|
|
clientId: stagedProject.clientCode ? (maps.clientIdByCode.get(stagedProject.clientCode) ?? null) : null,
|
|
dynamicFields,
|
|
endDate: normalizeDate(stagedProject.endDate ?? stagedProject.startDate ?? new Date()),
|
|
name: stagedProject.name ?? shortCode,
|
|
orderType: stagedProject.orderType ?? classification.orderType,
|
|
startDate: normalizeDate(stagedProject.startDate ?? stagedProject.endDate ?? new Date()),
|
|
status: projectStatus,
|
|
utilizationCategoryId: stagedProject.utilizationCategoryCode
|
|
? (maps.utilizationCategoryIdByCode.get(stagedProject.utilizationCategoryCode) ?? null)
|
|
: null,
|
|
winProbability: stagedProject.winProbability ?? 100,
|
|
},
|
|
create: {
|
|
shortCode,
|
|
name: stagedProject.name ?? shortCode,
|
|
orderType: stagedProject.orderType ?? classification.orderType,
|
|
allocationType: stagedProject.allocationType ?? classification.allocationType,
|
|
budgetCents: 0,
|
|
startDate: normalizeDate(stagedProject.startDate ?? stagedProject.endDate ?? new Date()),
|
|
endDate: normalizeDate(stagedProject.endDate ?? stagedProject.startDate ?? new Date()),
|
|
status: projectStatus,
|
|
winProbability: stagedProject.winProbability ?? 100,
|
|
utilizationCategoryId: stagedProject.utilizationCategoryCode
|
|
? (maps.utilizationCategoryIdByCode.get(stagedProject.utilizationCategoryCode) ?? null)
|
|
: null,
|
|
clientId: stagedProject.clientCode ? (maps.clientIdByCode.get(stagedProject.clientCode) ?? null) : null,
|
|
dynamicFields,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
projectIdByShortCode.set(shortCode, project.id);
|
|
}
|
|
|
|
return projectIdByShortCode;
|
|
}
|
|
|
|
export async function commitDispoImportBatch(
|
|
db: CommitDbClient,
|
|
input: CommitDispoImportBatchInput,
|
|
): Promise<CommitDispoImportBatchResult> {
|
|
const validation = await validateDispoBatch(db, input);
|
|
|
|
const result = await db.$transaction(async (tx) => {
|
|
await tx.importBatch.update({
|
|
where: { id: validation.batchId },
|
|
data: { status: ImportBatchStatus.COMMITTING },
|
|
});
|
|
|
|
await upsertUtilizationCategories(tx);
|
|
await upsertRoleSeeds(tx);
|
|
|
|
const [
|
|
stagedResources, stagedProjects, stagedAssignments, stagedVacations,
|
|
stagedAvailabilityRules, adminUser, countries, metroCities,
|
|
managementLevelGroups, managementLevels, clients, orgUnits, roles,
|
|
utilizationCategories,
|
|
] = await Promise.all([
|
|
tx.stagedResource.findMany({ where: { importBatchId: validation.batchId } }),
|
|
tx.stagedProject.findMany({ where: { importBatchId: validation.batchId } }),
|
|
tx.stagedAssignment.findMany({ where: { importBatchId: validation.batchId } }),
|
|
tx.stagedVacation.findMany({ where: { importBatchId: validation.batchId } }),
|
|
tx.stagedAvailabilityRule.findMany({ where: { importBatchId: validation.batchId } }),
|
|
tx.user.findFirst({ where: { systemRole: "ADMIN" }, select: { id: true } }),
|
|
tx.country.findMany({ select: { id: true, code: true } }),
|
|
tx.metroCity.findMany({ select: { id: true, name: true } }),
|
|
tx.managementLevelGroup.findMany({ select: { id: true, name: true, targetPercentage: true } }),
|
|
tx.managementLevel.findMany({ select: { id: true, name: true } }),
|
|
tx.client.findMany({ select: { id: true, code: true, name: true } }),
|
|
tx.orgUnit.findMany({ select: { id: true, level: true, name: true } }),
|
|
tx.role.findMany({ select: { id: true, name: true } }),
|
|
tx.utilizationCategory.findMany({ select: { id: true, code: true } }),
|
|
]);
|
|
|
|
if (!adminUser) {
|
|
throw new Error("Cannot commit Dispo import without an ADMIN user in the database");
|
|
}
|
|
|
|
const maps = buildReferenceDataMaps({
|
|
clients, countries, managementLevelGroups, managementLevels,
|
|
metroCities, orgUnits, roles, utilizationCategories,
|
|
});
|
|
|
|
const mergedResources = mergeStagedResources(stagedResources);
|
|
await ensureInferredRolesExist(tx, mergedResources, maps.roleIdByName);
|
|
|
|
const resourceResult = await commitResources(
|
|
tx, mergedResources, maps, validation.batchId,
|
|
stagedVacations, stagedAvailabilityRules,
|
|
);
|
|
|
|
const projectIdByShortCode = await commitProjects(
|
|
tx, stagedProjects, maps, validation.batchId, input.importTbdProjects ?? false,
|
|
);
|
|
|
|
// Commit assignments
|
|
const aggregatedAssignments = aggregateAssignments(
|
|
stagedAssignments, resourceResult.resourceIdByKey, projectIdByShortCode,
|
|
maps.roleIdByName, resourceResult.resourceRoleNameByKey, input.importTbdProjects ?? false,
|
|
);
|
|
|
|
for (const assignment of aggregatedAssignments) {
|
|
const dailyCostCents = Math.round(
|
|
assignment.hoursPerDay * (mergedResources.get(assignment.resourceKey)?.lcrCents ?? 0),
|
|
);
|
|
const metadata = {
|
|
dispoImport: {
|
|
importBatchId: validation.batchId,
|
|
sourceDates: assignment.sourceDates,
|
|
utilizationCategoryCode: assignment.utilizationCategoryCode,
|
|
},
|
|
} as Prisma.InputJsonValue;
|
|
|
|
await tx.assignment.upsert({
|
|
where: {
|
|
unique_assignment: {
|
|
resourceId: assignment.resourceId,
|
|
projectId: assignment.projectId,
|
|
startDate: assignment.startDate,
|
|
endDate: assignment.endDate,
|
|
},
|
|
},
|
|
update: {
|
|
dailyCostCents, metadata,
|
|
percentage: assignment.percentage,
|
|
role: assignment.roleName,
|
|
roleId: assignment.roleId,
|
|
status: AllocationStatus.PROPOSED,
|
|
},
|
|
create: {
|
|
dailyCostCents, metadata,
|
|
endDate: assignment.endDate,
|
|
hoursPerDay: assignment.hoursPerDay,
|
|
percentage: assignment.percentage,
|
|
projectId: assignment.projectId,
|
|
resourceId: assignment.resourceId,
|
|
role: assignment.roleName,
|
|
roleId: assignment.roleId,
|
|
startDate: assignment.startDate,
|
|
status: AllocationStatus.PROPOSED,
|
|
},
|
|
});
|
|
|
|
await tx.resourceRole.upsert({
|
|
where: { resourceId_roleId: { resourceId: assignment.resourceId, roleId: assignment.roleId } },
|
|
update: {},
|
|
create: { resourceId: assignment.resourceId, roleId: assignment.roleId },
|
|
});
|
|
resourceResult.upsertedResourceRoles += 1;
|
|
}
|
|
|
|
// Commit vacations
|
|
for (const stagedVacation of stagedVacations) {
|
|
const resourceId = resourceResult.resourceIdByKey.get(stagedVacation.resourceExternalId);
|
|
if (!resourceId) {
|
|
throw new Error(`Unable to resolve resource "${stagedVacation.resourceExternalId}" during vacation commit`);
|
|
}
|
|
|
|
const existing = await tx.vacation.findFirst({
|
|
where: {
|
|
endDate: stagedVacation.endDate, halfDayPart: stagedVacation.halfDayPart,
|
|
isHalfDay: stagedVacation.isHalfDay, note: stagedVacation.note, resourceId,
|
|
startDate: stagedVacation.startDate, status: VacationStatus.APPROVED,
|
|
type: stagedVacation.vacationType,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
if (existing) {
|
|
await tx.vacation.update({
|
|
where: { id: existing.id },
|
|
data: { approvedAt: new Date(), approvedById: adminUser.id, note: stagedVacation.note, requestedById: adminUser.id },
|
|
});
|
|
} else {
|
|
await tx.vacation.create({
|
|
data: {
|
|
approvedAt: new Date(), approvedById: adminUser.id,
|
|
endDate: stagedVacation.endDate, halfDayPart: stagedVacation.halfDayPart,
|
|
isHalfDay: stagedVacation.isHalfDay, note: stagedVacation.note,
|
|
requestedById: adminUser.id, resourceId,
|
|
startDate: stagedVacation.startDate, status: VacationStatus.APPROVED,
|
|
type: stagedVacation.vacationType,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// Mark staged records as committed
|
|
await Promise.all([
|
|
tx.stagedResource.updateMany({ where: { importBatchId: validation.batchId }, data: { status: StagedRecordStatus.COMMITTED } }),
|
|
tx.stagedProject.updateMany({
|
|
where: input.importTbdProjects ? { importBatchId: validation.batchId } : { importBatchId: validation.batchId, isTbd: false },
|
|
data: { status: StagedRecordStatus.COMMITTED },
|
|
}),
|
|
tx.stagedAssignment.updateMany({
|
|
where: input.importTbdProjects
|
|
? { importBatchId: validation.batchId, isUnassigned: false }
|
|
: { importBatchId: validation.batchId, isTbd: false, isUnassigned: false },
|
|
data: { status: StagedRecordStatus.COMMITTED },
|
|
}),
|
|
tx.stagedVacation.updateMany({ where: { importBatchId: validation.batchId }, data: { status: StagedRecordStatus.COMMITTED } }),
|
|
tx.stagedAvailabilityRule.updateMany({ where: { importBatchId: validation.batchId }, data: { status: StagedRecordStatus.COMMITTED } }),
|
|
]);
|
|
|
|
await tx.importBatch.update({
|
|
where: { id: validation.batchId },
|
|
data: {
|
|
committedAt: new Date(),
|
|
status: ImportBatchStatus.COMMITTED,
|
|
summary: buildBatchSummaryEntry({
|
|
...toJsonObject(validation.batchSummary),
|
|
commit: {
|
|
committedAssignments: aggregatedAssignments.length,
|
|
committedProjects: projectIdByShortCode.size,
|
|
committedResources: mergedResources.size,
|
|
committedVacations: stagedVacations.length,
|
|
skippedTbdUnresolved: validation.skippedTbdUnresolved,
|
|
updatedEntitlements: resourceResult.updatedEntitlements,
|
|
updatedResourceAvailabilities: resourceResult.updatedResourceAvailabilities,
|
|
upsertedResourceRoles: resourceResult.upsertedResourceRoles,
|
|
},
|
|
}),
|
|
},
|
|
});
|
|
|
|
return {
|
|
batchId: validation.batchId,
|
|
counts: {
|
|
committedAssignments: aggregatedAssignments.length,
|
|
committedProjects: projectIdByShortCode.size,
|
|
committedResources: mergedResources.size,
|
|
committedVacations: stagedVacations.length,
|
|
updatedEntitlements: resourceResult.updatedEntitlements,
|
|
updatedResourceAvailabilities: resourceResult.updatedResourceAvailabilities,
|
|
upsertedResourceRoles: resourceResult.upsertedResourceRoles,
|
|
},
|
|
unresolved: { blocked: 0, skippedTbd: validation.skippedTbdUnresolved },
|
|
} satisfies CommitDispoImportBatchResult;
|
|
}, { maxWait: 30_000, timeout: 600_000 });
|
|
|
|
if ("resource" in db && db.resource) {
|
|
await recomputeResourceValueScores(db);
|
|
}
|
|
|
|
return result;
|
|
}
|