# Sync Behavior Documentation ## Overview This document provides comprehensive details about how the Autotask sync system works, including the differences between sync types, date range filtering, database operations, and entity-specific behaviors. --- ## Table of Contents - [Sync Types](#sync-types) - [Date Range Filtering](#date-range-filtering) - [Database Operations](#database-operations) - [Entity-Specific Behaviors](#entity-specific-behaviors) - [Foreign Key Validation](#foreign-key-validation) - [Soft Deletes](#soft-deletes) - [Performance Considerations](#performance-considerations) --- ## Sync Types ### Full Sync **What it does:** - Fetches **ALL** records from Autotask that match the configured filters - Performs an **UPSERT** operation on every fetched record - Optionally performs **soft deletes** for missing records (entity-dependent) **When to use:** - Initial setup or first-time sync - After major data changes in Autotask - To ensure complete data consistency - When you suspect data drift between systems **Characteristics:** - Processes all records within filter criteria (date range, status, etc.) - Does NOT check if records have changed - upserts everything - Slower than incremental sync - More thorough and ensures data consistency **Example:** ``` Full Sync of Tickets (last 2 years): 1. Fetch all tickets created in last 2 years from Autotask 2. Upsert all fetched tickets to PostgreSQL 3. Skip soft deletes (has date filter) ``` ### Incremental Sync **What it does:** - Fetches only records **modified since the last successful sync** - Uses `lastModifiedDate` or similar timestamp fields - Performs UPSERT on changed records only - Does NOT perform soft deletes **When to use:** - Regular scheduled syncs (hourly, daily) - To keep data up-to-date with minimal overhead - When you only need recent changes **Characteristics:** - Only processes records that changed since last sync - Much faster than full sync - Requires a previous successful sync to establish baseline - Falls back to full sync if no previous sync found **Important Limitations:** - **Companies and Resources** do NOT support incremental sync - These entities automatically fall back to full sync - Autotask API does not support date-based filtering for these entities - This is by design and prevents sync failures **Example:** ``` Incremental Sync of Tickets: 1. Get last sync time: 2026-01-24 08:00:00 2. Fetch tickets where lastActivityDate >= 2026-01-24 08:00:00 3. Upsert only the changed tickets 4. No soft deletes Incremental Sync of Companies (automatic fallback): 1. Request incremental sync 2. System detects Companies doesn't support incremental 3. Automatically performs full sync instead 4. Logs: "companies does not support incremental sync, performing full sync instead" ``` ### Sync Selected (Entity-Specific) **What it does:** - Syncs only the selected entities - Uses full sync behavior for each selected entity - Respects all entity-specific filters and behaviors **When to use:** - To sync specific entities without syncing everything - After fixing entity-specific issues - For testing or troubleshooting --- ## Date Range Filtering ### Entities with Required Filters These entities **REQUIRE** filters - the Autotask API will return an error without them: | Entity | Filter Field | Default Range | Reason | |--------|-------------|---------------|---------| | **Time Entries** | `dateWorked` | Last 2 years | API requirement | | **Billing Items** | `itemDate` | Last 2 years | API requirement | | **Contracts** | `status` | Active only (status=1) | API requirement | | **Projects** | `status` | Non-completed (status≠5) | API requirement | ### Entities with Optional Date Filters These entities have date filters applied for **performance** and **data management**: | Entity | Filter Field | Default Range | Configurable | |--------|-------------|---------------|--------------| | **Tickets** | `createDate` | Last 2 years | Yes (via yearsBack) | | **Tasks** | `createDateTime` | Last 2 years | Yes (via yearsBack) | ### Entities Without Date Filters These entities sync **ALL** records (no date filtering): - **Companies** - All active companies - **Resources** - All active resources - **Contacts** - All active contacts - **Configuration Items** - All active items - **Picklists** (Statuses, Issue Types, Sub-Issue Types, Work Types) - All values ### Configuring Date Range The `yearsBack` parameter controls the date range: ```typescript // Default: 2 years syncService.fullSync(2); // Custom: 5 years syncService.fullSync(5); // Via API POST /api/sync/full { "yearsBack": 3, "triggeredBy": "user@example.com" } ``` **Important Notes:** 1. **Date filters apply to BOTH full and incremental sync** - Full sync: Fetches all records within date range - Incremental sync: Fetches changed records within date range 2. **Soft deletes are skipped for date-filtered entities** - Prevents deleting old records outside the sync window - See [Soft Deletes](#soft-deletes) section --- ## Database Operations ### UPSERT Behavior All syncs use PostgreSQL's `ON CONFLICT DO UPDATE` (UPSERT): ```sql INSERT INTO table_name (id, field1, field2, ...) VALUES ($1, $2, $3, ...) ON CONFLICT (id) DO UPDATE SET field1 = EXCLUDED.field1, field2 = EXCLUDED.field2, ... synced_at = NOW() ``` **What this means:** - **If record exists**: All fields are updated with new values from Autotask - **If record doesn't exist**: Record is inserted as new - **Every fetched record is processed**, regardless of whether data actually changed - The `synced_at` timestamp is always updated ### Record Counts Sync history tracks three metrics: | Metric | Description | How It's Calculated | |--------|-------------|---------------------| | `records_added` | New records inserted | **Estimated** (~10% of upserted) | | `records_updated` | Existing records updated | **Estimated** (~90% of upserted) | | `records_deleted` | Records soft-deleted | **Actual count** from soft delete operation | **Important:** `records_added` and `records_updated` are **estimates** based on the total upserted count. PostgreSQL doesn't easily distinguish between inserts and updates in bulk UPSERT operations. ### Bulk Processing Records are processed in batches for performance: - **Fetch batch size**: 500 records per API call - **Upsert batch size**: 100 records per database transaction - **Pagination**: Automatic for large datasets --- ## Entity-Specific Behaviors ### Picklist Entities **Entities:** Statuses, Issue Types, Sub-Issue Types, Work Types **Special Handling:** - Fetched from `/entity/entityInformation/fields` endpoint (not `/query`) - No date filtering - No soft deletes - Synced as key-value pairs **Example:** ```typescript // Statuses from Tickets.status field GET /Tickets/entityInformation/fields // Maps to: { value: 1, label: "New", is_active: true } { value: 5, label: "Complete", is_active: true } ``` ### Tickets **Filters:** - Date: `createDate >= (now - 2 years)` - Active: `isActive = true` (if applicable) **Foreign Key Validation:** - `assigned_resource_id` → Resources - `first_response_assigned_resource_id` → Resources - `first_response_initiating_resource_id` → Resources - Invalid references are set to `null` **Soft Deletes:** ❌ Skipped (has date filter) ### Tasks **Filters:** - Date: `createDateTime >= (now - 2 years)` **Foreign Key Validation:** - `project_id` → Projects - `assigned_resource_id` → Resources - `creator_resource_id` → Resources - `completed_by_resource_id` → Resources - `last_activity_resource_id` → Resources - Invalid references are set to `null` **Soft Deletes:** ❌ Skipped (has date filter) ### Time Entries **Filters:** - **Required**: `dateWorked >= (now - 2 years)` **Special Notes:** - `company_id` is nullable (time entries can exist without a company) - No foreign key validation needed **Soft Deletes:** ❌ Skipped (has date filter) ### Billing Items **Filters:** - **Required**: `itemDate >= (now - 2 years)` **Special Notes:** - API requires a filter parameter - Returns 500 error without filter **Soft Deletes:** ❌ Skipped (has date filter) ### Companies **Filters:** - Active: `isActive = true` **Special Notes:** - No date filtering - syncs all active companies - Foundation entity for many foreign keys **Soft Deletes:** ✅ Yes (no date filter) ### Resources **Filters:** - Active: `isActive = true` **Special Notes:** - No date filtering - syncs all active resources - Referenced by Tickets, Tasks, Time Entries **Soft Deletes:** ✅ Yes (no date filter) ### Contacts **Filters:** - Active: `isActive = true` **Special Notes:** - No date filtering - syncs all active contacts - Referenced by Configuration Items **Soft Deletes:** ✅ Yes (no date filter) ### Configuration Items **Filters:** - Active: `isActive = true` **Foreign Key Validation:** - `contact_id` → Contacts - Invalid references are set to `null` **Soft Deletes:** ✅ Yes (no date filter) ### Contracts **Filters:** - **Required**: `status = 1` (Active) **Special Notes:** - API requires status filter - Only syncs active contracts **Soft Deletes:** ❌ Skipped (has status filter) ### Projects **Filters:** - **Required**: `status != 5` (Not Complete) **Special Notes:** - API requires status filter - Excludes completed projects **Soft Deletes:** ❌ Skipped (has status filter) --- ## Foreign Key Validation ### Why It's Needed Autotask data may reference entities that: - Haven't been synced yet - Are outside the sync date range - Have been deleted in Autotask - Don't exist due to data inconsistencies Without validation, these references cause PostgreSQL foreign key constraint violations. ### How It Works Before upserting records, the sync validates foreign key references: 1. **Fetch valid IDs** from the database ```typescript const validResourceIds = await getValidResourceIds(); // Returns Set of all resource IDs where is_deleted = false ``` 2. **Validate each record** ```typescript if (task.assigned_resource_id && !validResourceIds.has(task.assigned_resource_id)) { task.assigned_resource_id = null; // Nullify invalid reference } ``` 3. **Log warnings** for nullified references ``` [WARN] Nullified invalid resource references: 15 records ``` ### Validated Relationships | Entity | Foreign Key Field | References | Action on Invalid | |--------|------------------|------------|-------------------| | **Tickets** | `assigned_resource_id` | Resources | Set to null | | **Tickets** | `first_response_assigned_resource_id` | Resources | Set to null | | **Tickets** | `first_response_initiating_resource_id` | Resources | Set to null | | **Tasks** | `project_id` | Projects | Set to null | | **Tasks** | `assigned_resource_id` | Resources | Set to null | | **Tasks** | `creator_resource_id` | Resources | Set to null | | **Tasks** | `completed_by_resource_id` | Resources | Set to null | | **Tasks** | `last_activity_resource_id` | Resources | Set to null | | **Configuration Items** | `contact_id` | Contacts | Set to null | ### Performance Impact Foreign key validation queries are: - Executed once per sync (cached) - Indexed queries (fast) - Minimal overhead compared to API fetching --- ## Soft Deletes ### What Are Soft Deletes? Instead of permanently deleting records, the system marks them as deleted: ```sql UPDATE table_name SET is_deleted = true, deleted_at = NOW() WHERE id NOT IN (fetched_ids) AND is_deleted = false ``` ### When Soft Deletes Occur **Only during Full Sync** for entities **without date filters**: | Entity | Soft Deletes? | Reason | |--------|--------------|---------| | Companies | ✅ Yes | No date filter - safe to delete | | Resources | ✅ Yes | No date filter - safe to delete | | Contacts | ✅ Yes | No date filter - safe to delete | | Configuration Items | ✅ Yes | No date filter - safe to delete | | Tickets | ❌ No | Has date filter - would delete old records | | Tasks | ❌ No | Has date filter - would delete old records | | Time Entries | ❌ No | Has date filter - would delete old records | | Billing Items | ❌ No | Has date filter - would delete old records | | Projects | ❌ No | Has status filter - would delete completed | | Contracts | ❌ No | Has status filter - would delete inactive | | Picklists | ❌ No | Picklist values don't get deleted | ### Why Skip Soft Deletes for Date-Filtered Entities? **Problem:** If we soft-deleted records not in the fetched set for date-filtered entities, we would incorrectly delete old records outside the sync window. **Example:** ``` Full Sync of Tickets (last 2 years): 1. Fetch tickets created >= 2024-01-24 2. Ticket #12345 created in 2020 exists in DB 3. Ticket #12345 NOT in fetched set (outside date range) 4. ❌ Would incorrectly soft-delete Ticket #12345 Solution: Skip soft deletes for date-filtered entities ``` ### Restoring Soft-Deleted Records If a record is soft-deleted but appears in a later sync: ```sql -- Record is automatically restored UPDATE table_name SET is_deleted = false, deleted_at = NULL, ... WHERE id = $1 ``` --- ## Performance Considerations ### Sync Duration Factors | Factor | Impact | Mitigation | |--------|--------|------------| | **Number of records** | High | Use date range filters | | **API rate limits** | Medium | Built-in rate limiting (10 req/sec) | | **Network latency** | Medium | Pagination, batch processing | | **Database operations** | Low | Bulk upserts, indexed queries | | **Foreign key validation** | Low | Cached, indexed queries | ### Estimated Sync Times Based on typical data volumes: | Entity | Record Count | Full Sync Time | Incremental Sync Time | |--------|-------------|----------------|----------------------| | Companies | 500 | ~30 seconds | ~5 seconds | | Resources | 100 | ~10 seconds | ~2 seconds | | Tickets (2 years) | 10,000 | ~10 minutes | ~1 minute | | Tasks (2 years) | 5,000 | ~5 minutes | ~30 seconds | | Time Entries (2 years) | 50,000 | ~45 minutes | ~5 minutes | | Billing Items (2 years) | 50,000+ | ~45-60 minutes | ~5 minutes | **Note:** Times vary based on data volume, network speed, and system load. ### Optimization Tips 1. **Use Incremental Sync for regular updates** - Schedule incremental syncs hourly or daily - Reserve full syncs for weekly/monthly maintenance 2. **Adjust date range based on needs** - Default 2 years is usually sufficient - Reduce to 1 year for faster syncs if older data isn't needed - Increase to 5+ years only if historical data is required 3. **Sync entities in dependency order** - Companies → Resources → Tickets → Tasks - Reduces foreign key validation overhead 4. **Monitor sync history** - Check for failures and patterns - Identify slow entities for optimization 5. **Use Sync Selected for troubleshooting** - Test individual entities - Isolate and fix issues without full sync --- ## Sync Workflow Examples ### Example 1: Initial Setup ``` 1. Full Sync - All Entities (2 years) ├─ Companies (all active) ├─ Resources (all active) ├─ Contacts (all active) ├─ Configuration Items (all active) ├─ Contracts (active only) ├─ Projects (non-completed) ├─ Tickets (last 2 years) ├─ Tasks (last 2 years) ├─ Time Entries (last 2 years) ├─ Billing Items (last 2 years) └─ Picklists (all values) Result: Complete database populated with all relevant data ``` ### Example 2: Daily Maintenance ``` 1. Incremental Sync - All Entities ├─ Fetch only records modified since last sync ├─ Upsert changed records └─ No soft deletes Result: Database updated with latest changes (fast) ``` ### Example 3: Fixing Specific Entity ``` 1. Identify failed entity: Billing Items 2. Sync Selected - Billing Items only ├─ Full sync with date filter (last 2 years) ├─ Upsert all billing items └─ No soft deletes Result: Billing Items data refreshed and consistent ``` ### Example 4: Historical Data Sync ``` 1. Full Sync - Tickets (5 years) ├─ Adjust yearsBack parameter to 5 ├─ Fetch tickets created >= 2021-01-24 ├─ Upsert all fetched tickets └─ Skip soft deletes Result: 5 years of ticket history in database ``` --- ## Troubleshooting ### Common Issues #### Issue: Foreign Key Constraint Violations **Symptoms:** ``` [DATABASE_CONSTRAINT_ERROR] violates foreign key constraint "tasks_project_id_fkey" ``` **Cause:** Referenced entity hasn't been synced or is outside date range **Solution:** 1. Sync dependency entities first (Companies → Resources → Projects) 2. Foreign key validation will nullify invalid references 3. Check logs for nullified reference warnings #### Issue: API Filter Required Error **Symptoms:** ``` [API_ERROR] Value cannot be null. Parameter name: filters ``` **Cause:** Entity requires a filter (Time Entries, Billing Items, Contracts, Projects) **Solution:** Filters are automatically applied - this shouldn't occur. If it does, check entity-sync.ts for proper filter configuration. #### Issue: Slow Sync Performance **Symptoms:** Sync takes longer than expected **Solutions:** 1. Reduce `yearsBack` parameter (e.g., from 2 to 1 year) 2. Use incremental sync instead of full sync 3. Sync entities individually instead of all at once 4. Check network connectivity and API response times #### Issue: Records Not Appearing **Symptoms:** Records exist in Autotask but not in database **Possible Causes:** 1. Outside date range filter 2. Not active (filtered out by `isActive`) 3. Foreign key validation nullified references 4. Sync failed (check sync history) **Solutions:** 1. Check sync history for errors 2. Verify record meets filter criteria 3. Check logs for validation warnings 4. Run full sync for the entity --- ## Best Practices ### 1. Sync Strategy - **Initial Setup:** Full sync all entities (2 years) - **Daily Maintenance:** Incremental sync all entities - **Weekly/Monthly:** Full sync for data consistency - **Troubleshooting:** Sync selected entities as needed ### 2. Date Range Configuration - **Default (2 years):** Good for most use cases - **1 year:** Faster syncs, less historical data - **3-5 years:** More historical data, slower syncs - **Adjust based on:** Business needs, performance requirements ### 3. Monitoring - Review sync history regularly - Set up alerts for failed syncs - Monitor sync duration trends - Check for foreign key validation warnings ### 4. Dependency Management Sync entities in this order for best results: 1. Companies 2. Resources 3. Contacts 4. Configuration Items 5. Contracts 6. Projects 7. Tickets 8. Tasks 9. Time Entries 10. Billing Items 11. Picklists ### 5. Error Handling - Failed syncs are logged in `sync_history` table - Check error messages for specific issues - Re-run failed entity syncs individually - Contact support if errors persist --- ## API Reference ### Full Sync ```typescript POST /api/sync/full { "yearsBack": 2, "triggeredBy": "user@example.com" } ``` ### Incremental Sync ```typescript POST /api/sync/incremental { "triggeredBy": "user@example.com" } ``` ### Sync Selected Entities ```typescript POST /api/sync/entity { "entities": ["tickets", "tasks"], "yearsBack": 2, "triggeredBy": "user@example.com" } ``` ### Get Sync History ```typescript GET /api/sync/history?limit=50&offset=0 ``` ### Get Sync Status ```typescript GET /api/sync/status ``` --- ## Related Documentation - [Sync Interface Guide](./SYNC_INTERFACE_GUIDE.md) - User guide for the sync UI - [Sync Logging Improvements](./SYNC_LOGGING_IMPROVEMENTS.md) - Structured logging details - [Database Schema](../migrations/001_initial_schema.sql) - PostgreSQL table definitions --- ## Changelog ### 2026-01-24 - Added billing items filter requirement - Added task foreign key validation - Documented sync behavior comprehensively ### 2026-01-23 - Fixed statuses and work types sync (picklist API) - Added structured logging - Created sync interface documentation --- ## Support For issues or questions: 1. Check sync history in the admin interface 2. Review error messages and logs 3. Consult this documentation 4. Contact system administrator