const { AppError } = require("../utils/AppError"); const BUDGET_SCOPE_TYPES = Object.freeze({ global: "global", departmentLegacy: "department_legacy", costCenterScoped: "cost_center_scoped", }); const BUDGET_SCOPE_PRIORITY = [ BUDGET_SCOPE_TYPES.costCenterScoped, BUDGET_SCOPE_TYPES.departmentLegacy, BUDGET_SCOPE_TYPES.global, ]; function buildValidationError(details, statusCode = 400) { return new AppError({ code: "VALID_001", message: "参数校验失败", statusCode, details, }); } function resolveBudgetScope({ departmentId = null, legalEntityId = null, costCenterId = null } = {}) { const hasDepartment = Boolean(departmentId); const hasLegalEntity = Boolean(legalEntityId); const hasCostCenter = Boolean(costCenterId); if (hasCostCenter && !hasLegalEntity) { throw buildValidationError({ costCenterId: "legalEntityId_required" }); } if (hasDepartment && (hasLegalEntity || hasCostCenter)) { throw buildValidationError({ scope: "mixed_dimensions_not_allowed" }, 409); } if (hasLegalEntity && !hasCostCenter) { throw buildValidationError({ costCenterId: "required_with_legalEntityId" }); } if (hasLegalEntity && hasCostCenter) { return { scopeType: BUDGET_SCOPE_TYPES.costCenterScoped, departmentId: null, legalEntityId, costCenterId, }; } if (hasDepartment) { return { scopeType: BUDGET_SCOPE_TYPES.departmentLegacy, departmentId, legalEntityId: null, costCenterId: null, }; } return { scopeType: BUDGET_SCOPE_TYPES.global, departmentId: null, legalEntityId: null, costCenterId: null, }; } function resolveBudgetScopeType(record) { const hasDepartment = Boolean(record?.department_id); const hasLegalEntity = Boolean(record?.legal_entity_id); const hasCostCenter = Boolean(record?.cost_center_id); if (hasLegalEntity && hasCostCenter && !hasDepartment) { return BUDGET_SCOPE_TYPES.costCenterScoped; } if (hasDepartment && !hasLegalEntity && !hasCostCenter) { return BUDGET_SCOPE_TYPES.departmentLegacy; } if (!hasDepartment && !hasLegalEntity && !hasCostCenter) { return BUDGET_SCOPE_TYPES.global; } return "invalid"; } function matchesBudgetPoolContext(record, context) { const scopeType = resolveBudgetScopeType(record); if (scopeType === BUDGET_SCOPE_TYPES.costCenterScoped) { return ( Boolean(context.legalEntityId) && Boolean(context.costCenterId) && record.legal_entity_id === context.legalEntityId && record.cost_center_id === context.costCenterId ); } if (scopeType === BUDGET_SCOPE_TYPES.departmentLegacy) { return Boolean(context.departmentId) && record.department_id === context.departmentId; } if (scopeType === BUDGET_SCOPE_TYPES.global) { return true; } return false; } function normalizeBudgetContext({ departmentId = null, legalEntityId = null, costCenterId = null } = {}) { if (costCenterId && !legalEntityId) { throw buildValidationError({ costCenterId: "legalEntityId_required" }); } return { departmentId, legalEntityId, costCenterId, }; } function selectBudgetPoolCandidate(records, context = {}) { const normalizedContext = normalizeBudgetContext(context); for (const scopeType of BUDGET_SCOPE_PRIORITY) { const matches = (records ?? []).filter( (record) => resolveBudgetScopeType(record) === scopeType && matchesBudgetPoolContext(record, normalizedContext), ); if (matches.length === 1) { return matches[0]; } if (matches.length > 1) { if (scopeType === BUDGET_SCOPE_TYPES.costCenterScoped) { throw buildValidationError({ budget: "budget_pool_ambiguous", scopeType }, 409); } return matches[0]; } } return null; } async function resolveApplicableBudgetPoolByContext( tx, { departmentId = null, legalEntityId = null, costCenterId = null, effectiveDate }, ) { if (!(effectiveDate instanceof Date) || Number.isNaN(effectiveDate.getTime())) { throw buildValidationError({ effectiveDate: "invalid_date" }); } const records = await tx.budgetPool.findMany({ where: { status: "active", archived_at: null, period_start: { lte: effectiveDate }, period_end: { gte: effectiveDate }, }, orderBy: [{ created_at: "asc" }, { code: "asc" }], }); return selectBudgetPoolCandidate(records, { departmentId, legalEntityId, costCenterId, }); } module.exports = { BUDGET_SCOPE_TYPES, resolveBudgetScope, resolveBudgetScopeType, selectBudgetPoolCandidate, resolveApplicableBudgetPoolByContext, };