docs: add comprehensive sync behavior documentation

Created detailed documentation covering:
- Sync types (Full, Incremental, Sync Selected)
- Date range filtering and entity-specific behaviors
- Database operations (UPSERT, soft deletes)
- Foreign key validation mechanisms
- Performance considerations and optimization tips
- Entity-specific filters and requirements
- Troubleshooting guide and best practices
- Workflow examples and API reference

This complements the existing SYNC_INTERFACE_GUIDE.md with
technical implementation details and behavioral specifications.
This commit is contained in:
root 2026-01-24 08:43:35 -05:00
parent 2f20b99195
commit 8b50cae71c

747
docs/SYNC_BEHAVIOR.md Normal file
View file

@ -0,0 +1,747 @@
# 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
**Example:**
```
Incremental Sync of Companies:
1. Get last sync time: 2026-01-24 08:00:00
2. Fetch companies where lastModifiedDate >= 2026-01-24 08:00:00
3. Upsert only the changed companies
4. No soft deletes
```
### 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