const normalizeDateOnly = (value) => { if (typeof value !== 'string') { return null; } const trimmed = value.trim(); if (!trimmed) { return null; } if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { return trimmed; } if (trimmed.includes('T')) { return trimmed.slice(0, 10); } return trimmed; }; const compareAssignmentOrder = (left, right) => { const leftDate = normalizeDateOnly(left.start_date) ?? '9999-12-31'; const rightDate = normalizeDateOnly(right.start_date) ?? '9999-12-31'; if (leftDate !== rightDate) { return leftDate.localeCompare(rightDate); } const leftId = typeof left.client_contract_id === 'string' ? left.client_contract_id : ''; const rightId = typeof right.client_contract_id === 'string' ? right.client_contract_id : ''; return leftId.localeCompare(rightId); }; const sortAssignmentsDeterministically = (assignments) => [...assignments].sort(compareAssignmentOrder); const toPositiveCount = (value) => { const numeric = typeof value === 'string' ? Number(value) : value; if (typeof numeric !== 'number' || !Number.isFinite(numeric)) { return 0; } return Math.max(0, Math.trunc(numeric)); }; const distinctClientCount = (rows) => new Set(rows.map((row) => row.client_id).filter(Boolean)).size; function detectSharedNonTemplateContractGroups(rows) { const grouped = new Map(); for (const row of rows) { if (row?.is_template === true) { continue; } const key = `${row.tenant}:${row.contract_id}`; const existing = grouped.get(key) ?? []; existing.push(row); grouped.set(key, existing); } return [...grouped.values()] .filter((groupRows) => distinctClientCount(groupRows) > 1) .map((groupRows) => sortAssignmentsDeterministically(groupRows)); } function selectPreservedAssignment(assignments) { if (!Array.isArray(assignments) || assignments.length === 0) { throw new Error('At least one assignment is required to select the preserved contract owner'); } const ordered = sortAssignmentsDeterministically(assignments); const invoicedAssignments = ordered.filter((assignment) => toPositiveCount(assignment.invoice_count) > 0); if (invoicedAssignments.length === 1) { return { preservedAssignment: invoicedAssignments[0], reason: 'single_invoiced_assignment', }; } return { preservedAssignment: ordered[0], reason: 'earliest_start_date', }; } function assertCloneTargetsSupported(params) { const { tenant, contractId, cloneTargets, contractDocumentAssociationsCount = 0, pricingScheduleCount = 0, timeEntryCount = 0, usageTrackingCount = 0, } = params; if (!Array.isArray(cloneTargets) || cloneTargets.length === 0) { return; } const unsupportedReasons = []; if (toPositiveCount(contractDocumentAssociationsCount) > 0) { unsupportedReasons.push('contract-scoped document associations exist'); } if (toPositiveCount(pricingScheduleCount) > 0) { unsupportedReasons.push('contract pricing schedules exist'); } if (toPositiveCount(timeEntryCount) > 0) { unsupportedReasons.push('contract-line time entries exist'); } if (toPositiveCount(usageTrackingCount) > 0) { unsupportedReasons.push('contract-line usage tracking exists'); } if (unsupportedReasons.length > 0) { throw new Error( `Cannot split shared contract ${contractId} in tenant ${tenant}: ${unsupportedReasons.join( '; ' )}.` ); } } function remapRows(rows, transform) { return Array.isArray(rows) ? rows.map((row) => transform({ ...row })) : []; } function buildSharedContractClonePlan(params, options = {}) { const { sourceContract, assignments, contractLines = [], contractLineServices = [], contractLineServiceDefaults = [], contractLineDiscounts = [], contractLineServiceConfigurations = [], contractLineServiceBucketConfigs = [], contractLineServiceFixedConfigs = [], contractLineServiceHourlyConfig = [], contractLineServiceHourlyConfigs = [], contractLineServiceRateTiers = [], contractLineServiceUsageConfig = [], } = params; if (!sourceContract?.contract_id || !sourceContract?.tenant) { throw new Error('A source contract with tenant and contract_id is required'); } const orderedAssignments = sortAssignmentsDeterministically(assignments); const { preservedAssignment, reason } = selectPreservedAssignment(orderedAssignments); const cloneTargets = orderedAssignments.filter( (assignment) => assignment.client_contract_id !== preservedAssignment.client_contract_id ); const createId = typeof options.createId === 'function' ? options.createId : () => { throw new Error('createId option is required'); }; const clones = cloneTargets.map((cloneTarget) => { const newContractId = createId('contract'); const lineIdMap = new Map(); const configIdMap = new Map(); const clonedContract = { ...sourceContract, contract_id: newContractId, owner_client_id: cloneTarget.client_id, }; const clonedContractLines = remapRows(contractLines, (row) => { const newLineId = createId('contract_line'); lineIdMap.set(row.contract_line_id, newLineId); return { ...row, contract_id: newContractId, contract_line_id: newLineId, cadence_owner: typeof row.cadence_owner === 'string' && row.cadence_owner.length > 0 ? row.cadence_owner : 'client', }; }); const clonedContractLineServices = remapRows(contractLineServices, (row) => ({ ...row, contract_line_id: lineIdMap.get(row.contract_line_id), })); const clonedContractLineServiceDefaults = remapRows(contractLineServiceDefaults, (row) => ({ ...row, default_id: createId('contract_line_default'), contract_line_id: lineIdMap.get(row.contract_line_id), })); const clonedContractLineDiscounts = remapRows(contractLineDiscounts, (row) => ({ ...row, discount_id: createId('contract_line_discount'), contract_line_id: lineIdMap.get(row.contract_line_id), })); const clonedContractLineServiceConfigurations = remapRows( contractLineServiceConfigurations, (row) => { const newConfigId = createId('contract_line_service_config'); configIdMap.set(row.config_id, newConfigId); return { ...row, config_id: newConfigId, contract_line_id: lineIdMap.get(row.contract_line_id), }; } ); const remapConfigIdRow = (row) => ({ ...row, config_id: configIdMap.get(row.config_id), }); const clonedContractLineServiceBucketConfigs = remapRows( contractLineServiceBucketConfigs, remapConfigIdRow ); const clonedContractLineServiceFixedConfigs = remapRows( contractLineServiceFixedConfigs, remapConfigIdRow ); const clonedContractLineServiceHourlyConfig = remapRows( contractLineServiceHourlyConfig, remapConfigIdRow ); const clonedContractLineServiceHourlyConfigs = remapRows( contractLineServiceHourlyConfigs, remapConfigIdRow ); const clonedContractLineServiceUsageConfig = remapRows( contractLineServiceUsageConfig, remapConfigIdRow ); const clonedContractLineServiceRateTiers = remapRows(contractLineServiceRateTiers, (row) => ({ ...row, tier_id: createId('contract_line_service_rate_tier'), config_id: configIdMap.get(row.config_id), })); return { targetClientContractId: cloneTarget.client_contract_id, targetClientId: cloneTarget.client_id, sourceAssignment: cloneTarget, contract: clonedContract, clientContractUpdate: { client_contract_id: cloneTarget.client_contract_id, contract_id: newContractId, }, contractLines: clonedContractLines, contractLineServices: clonedContractLineServices, contractLineServiceDefaults: clonedContractLineServiceDefaults, contractLineDiscounts: clonedContractLineDiscounts, contractLineServiceConfigurations: clonedContractLineServiceConfigurations, contractLineServiceBucketConfigs: clonedContractLineServiceBucketConfigs, contractLineServiceFixedConfigs: clonedContractLineServiceFixedConfigs, contractLineServiceHourlyConfig: clonedContractLineServiceHourlyConfig, contractLineServiceHourlyConfigs: clonedContractLineServiceHourlyConfigs, contractLineServiceRateTiers: clonedContractLineServiceRateTiers, contractLineServiceUsageConfig: clonedContractLineServiceUsageConfig, }; }); return { contractId: sourceContract.contract_id, tenant: sourceContract.tenant, reason, preservedAssignment, preservedContractUpdate: { contract_id: sourceContract.contract_id, owner_client_id: preservedAssignment.client_id, }, clones, }; } module.exports = { detectSharedNonTemplateContractGroups, selectPreservedAssignment, assertCloneTargetsSupported, buildSharedContractClonePlan, };