docs: add project_phases table, fix tasks.project_id resolution

- Add project_phases section: schema, purpose, why it exists
- Document the tasks.project_id backfill mechanism (Autotask Tasks API
  omits projectID; resolved post-sync via phase_id join)
- Add query for tasks whose parent project is archived (project_id NULL
  but resolvable via project_phases)
- Update domain table to include project_phases (~2.7K rows)
This commit is contained in:
Claude 2026-03-24 13:53:58 -04:00
parent 4d75a099d7
commit b25557a310

View file

@ -16,7 +16,7 @@
| Domain | Key Tables | Approx Rows | Description |
|---|---|---|---|
| **Autotask PSA** | tickets, time_entries, companies, contacts, resources, configuration_items, contracts, projects, tasks, ticket_notes | 152K tickets, 156K time entries, 7K CIs | Service desk, billing, contracts, clients |
| **Autotask PSA** | tickets, time_entries, companies, contacts, resources, configuration_items, contracts, projects, project_phases, tasks, ticket_notes | 152K tickets, 156K time entries, 7K CIs, 2.7K phases | Service desk, billing, contracts, clients |
| **Datto RMM** | datto_rmm_alerts, datto_rmm_devices, datto_rmm_sites | 21K alerts, 3.6K devices | Remote monitoring & management |
| **Auvik** | auvik_tenants, auvik_tenant_mappings | 17 tenants, 15 mappings | Network management — device inventory fetched live from API; tenant/company links stored in DB |
| **SentinelOne** | s1_agents, s1_threats, s1_sites | 2.8K agents, 4.1K threats | Endpoint security |
@ -222,6 +222,35 @@ Planned engagements for client work — distinct from reactive service tickets.
**Relationship to tickets:** A ticket can optionally be linked to a project via `tickets.project_id`. This is typically used when a ticket was generated as part of project work (e.g. a change request within a project).
### project_phases (~2.7K rows, 19 columns)
Phases group tasks within a project (e.g. "Equipment Selection", "Installation", "Testing"). Every phase belongs to exactly one project.
**Key columns:**
- `id` (bigint PK) — Autotask phase ID (shares namespace with tasks)
- `project_id` (bigint) — FK to `projects.id` (no FK constraint — may reference archived projects)
- `parent_phase_id` (bigint) — parent phase for nested phase trees
- `title` (varchar) — phase name
- `description` (text)
- `phase_number` (varchar) — e.g. `T20251222.0116`
- `estimated_hours` (numeric)
- `start_date_time`, `due_date` (timestamp) — scheduled window
- `create_date_time`, `last_activity_date_time` (timestamp)
- `is_scheduled` (boolean)
- `creator_resource_id` (bigint) — FK to `resources.id`
- `is_deleted` (boolean)
**Why this table matters:** The Autotask Tasks bulk API omits `projectID` from its response. After each `project_phases` sync, an automatic backfill updates `tasks.project_id` by joining `tasks.phase_id = project_phases.id`. This is the only way to resolve which project a task belongs to.
**Join to get a task's project via phase:**
```sql
SELECT t.id, t.title, pp.title AS phase, p.project_name
FROM tasks t
JOIN project_phases pp ON pp.id = t.phase_id
LEFT JOIN projects p ON p.id = pp.project_id
WHERE t.is_deleted IS NOT TRUE;
```
### tasks (~5K rows, 34 columns)
Tasks are discrete units of work that live **inside** a project or a ticket. They are NOT standalone items — every task belongs to either a project (`project_id`) or a ticket (`ticket_id`), not both simultaneously.
@ -254,13 +283,21 @@ Tasks are discrete units of work that live **inside** a project or a ticket. The
**Time entries on tasks:** `time_entries.task_id` links work logs to specific tasks. Task time entries also carry `project_id` and `company_id` for quick aggregation without joins.
**Known sync behaviour — `project_id` population:**
The Autotask bulk Tasks API does not return `projectID` in its response. `project_id` is instead populated post-sync via a backfill that joins `tasks.phase_id → project_phases.id → project_phases.project_id`. This backfill runs automatically every time `project_phases` is synced. Tasks whose parent project is completed/archived (and therefore absent from our `projects` table) will still have `project_id = NULL` — use `phase_id → project_phases.project_id` directly in those cases.
**Finding project tasks vs ticket tasks:**
```sql
-- Project tasks only
-- Project tasks only (project_id populated via phase backfill)
SELECT t.*, p.project_name FROM tasks t
JOIN projects p ON p.id = t.project_id
WHERE t.project_id IS NOT NULL AND t.is_deleted IS NOT TRUE;
-- Tasks whose project is archived (project_id NULL but phase_id resolvable)
SELECT t.*, pp.project_id AS phase_project_id, pp.title AS phase_title
FROM tasks t JOIN project_phases pp ON pp.id = t.phase_id
WHERE t.project_id IS NULL AND t.is_deleted IS NOT TRUE;
-- Ticket sub-tasks only
SELECT t.*, tk.title AS ticket_title FROM tasks t
JOIN tickets tk ON tk.id = t.ticket_id