feat: add project_phases entity sync with task project_id backfill

The Autotask Tasks bulk API does not return projectID in its response,
causing all tasks.project_id to be NULL. This fixes it by:

- Adding project_phases as a synced entity (Autotask endpoint: /Phases)
- Migration 059: project_phases table with project_id, phase_number,
  estimated_hours, start/due dates, parent_phase_id, is_scheduled
- EntityType.PROJECT_PHASES added to all sync maps and dependency graph
  (depends on PROJECTS, runs before TASKS in sync order)
- buildProjectPhasesFilter: Phases endpoint requires a filter (id > 0)
- mapProjectPhase: maps Autotask field names to DB columns
- Post-sync backfill in syncEntity: after each project_phases sync,
  UPDATE tasks SET project_id = pp.project_id FROM project_phases pp
  JOIN projects p WHERE tasks.phase_id = pp.id
  Only backfills where the project exists in our DB (FK constraint on
  tasks.project_id; archived projects are skipped gracefully)

Result: 2,455 of 4,966 tasks now have project_id populated. Tasks
belonging to archived/completed projects have phase_id resolvable via
project_phases even when project_id remains NULL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
lorentz 2026-03-24 13:57:33 -04:00
parent 44db9a3019
commit dd4cf68def
5 changed files with 131 additions and 0 deletions

View file

@ -39,6 +39,9 @@ export function mapAutotaskToDatabase(
case EntityType.PROJECTS:
mapped = mapProject(data);
break;
case EntityType.PROJECT_PHASES:
mapped = mapProjectPhase(data);
break;
case EntityType.RESOURCES:
mapped = mapResource(data);
break;
@ -344,6 +347,30 @@ function mapProject(data: any): Record<string, any> {
};
}
/**
* Map ProjectPhase entity
*/
function mapProjectPhase(data: any): Record<string, any> {
return {
id: data.id,
project_id: data.projectID,
title: data.title,
description: data.description,
phase_number: data.phaseNumber,
estimated_hours: data.estimatedHours,
start_date_time: data.startDate,
due_date: data.dueDate,
create_date_time: data.createDate,
last_activity_date_time: data.lastActivityDateTime,
parent_phase_id: data.parentPhaseID,
is_scheduled: data.isScheduled,
creator_resource_id: data.creatorResourceID,
external_id: data.externalID,
synced_at: data.synced_at,
is_deleted: data.is_deleted || false,
};
}
/**
* Map Resource entity
*/