2025-11-19 14:18:16 -05:00
|
|
|
/**
|
|
|
|
|
* Entity Mapper
|
|
|
|
|
* Maps Autotask API responses to PostgreSQL database schema format
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { EntityType } from '../types/sync';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Autotask field names to PostgreSQL column names
|
|
|
|
|
* Autotask uses camelCase, PostgreSQL uses snake_case
|
|
|
|
|
*/
|
|
|
|
|
export function toSnakeCase(str: string): string {
|
|
|
|
|
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Autotask API response to database format
|
|
|
|
|
* @param entity Entity type
|
|
|
|
|
* @param data Autotask API response data
|
|
|
|
|
* @returns Mapped data for PostgreSQL
|
|
|
|
|
*/
|
|
|
|
|
export function mapAutotaskToDatabase(
|
|
|
|
|
entity: EntityType,
|
|
|
|
|
data: any
|
|
|
|
|
): Record<string, any> {
|
|
|
|
|
// Entity-specific mappings use ORIGINAL data (not snake_cased)
|
|
|
|
|
let mapped: Record<string, any>;
|
|
|
|
|
|
|
|
|
|
switch (entity) {
|
|
|
|
|
case EntityType.COMPANIES:
|
|
|
|
|
mapped = mapCompany(data);
|
|
|
|
|
break;
|
|
|
|
|
case EntityType.TICKETS:
|
|
|
|
|
mapped = mapTicket(data);
|
|
|
|
|
break;
|
|
|
|
|
case EntityType.TASKS:
|
|
|
|
|
mapped = mapTask(data);
|
|
|
|
|
break;
|
|
|
|
|
case EntityType.PROJECTS:
|
|
|
|
|
mapped = mapProject(data);
|
|
|
|
|
break;
|
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>
2026-03-24 13:57:33 -04:00
|
|
|
case EntityType.PROJECT_PHASES:
|
|
|
|
|
mapped = mapProjectPhase(data);
|
|
|
|
|
break;
|
2025-11-19 14:18:16 -05:00
|
|
|
case EntityType.RESOURCES:
|
|
|
|
|
mapped = mapResource(data);
|
|
|
|
|
break;
|
|
|
|
|
case EntityType.CONFIGURATION_ITEMS:
|
|
|
|
|
mapped = mapConfigurationItem(data);
|
|
|
|
|
break;
|
|
|
|
|
case EntityType.CONTACTS:
|
|
|
|
|
mapped = mapContact(data);
|
|
|
|
|
break;
|
|
|
|
|
case EntityType.CONTRACTS:
|
|
|
|
|
mapped = mapContract(data);
|
|
|
|
|
break;
|
2026-03-11 09:34:51 -04:00
|
|
|
case EntityType.CONTRACT_SERVICES:
|
|
|
|
|
mapped = mapContractService(data);
|
|
|
|
|
break;
|
|
|
|
|
case EntityType.AUTOTASK_SERVICES:
|
|
|
|
|
mapped = mapAutotaskService(data);
|
|
|
|
|
break;
|
2025-11-19 14:18:16 -05:00
|
|
|
case EntityType.BILLING_ITEMS:
|
|
|
|
|
mapped = mapBillingItem(data);
|
|
|
|
|
break;
|
|
|
|
|
case EntityType.TIME_ENTRIES:
|
|
|
|
|
mapped = mapTimeEntry(data);
|
|
|
|
|
break;
|
2026-02-20 10:28:15 -05:00
|
|
|
case EntityType.TICKET_NOTES:
|
|
|
|
|
mapped = mapTicketNote(data);
|
|
|
|
|
break;
|
2025-11-19 14:18:16 -05:00
|
|
|
case EntityType.STATUSES:
|
|
|
|
|
case EntityType.ISSUE_TYPES:
|
|
|
|
|
case EntityType.SUB_ISSUE_TYPES:
|
|
|
|
|
case EntityType.WORK_TYPES:
|
|
|
|
|
mapped = mapPicklist(data);
|
|
|
|
|
break;
|
feat: add Autotask tags sync (tag groups, tags, ticket tag associations)
- Migration 057: autotask_tag_groups, autotask_tags, and junction tables
(ticket_tags, company_tags, configuration_item_tags, contact_tags)
- Add TAG_GROUPS and TAGS to EntityType enum and dependency map
- Add mapTagGroup() and mapTag() entity mapper functions
- Add syncTagGroups(), syncTags(), syncTicketTagAssociations() methods
- Wire TicketTagAssociations bulk sync into full/incremental sync flow
- Add 'exist' operator to QueryFilter type
- No FK on ticket_id (tagged tickets may be outside 2yr sync window)
Synced: 26 tag groups, 7299 tags, 10141 ticket-tag associations
2026-03-20 09:22:40 -04:00
|
|
|
case EntityType.TAG_GROUPS:
|
|
|
|
|
mapped = mapTagGroup(data);
|
|
|
|
|
break;
|
|
|
|
|
case EntityType.TAGS:
|
|
|
|
|
mapped = mapTag(data);
|
|
|
|
|
break;
|
2025-11-19 14:18:16 -05:00
|
|
|
default:
|
|
|
|
|
// Fallback: auto-convert camelCase to snake_case
|
|
|
|
|
mapped = {};
|
|
|
|
|
for (const [key, value] of Object.entries(data)) {
|
|
|
|
|
const snakeKey = toSnakeCase(key);
|
|
|
|
|
mapped[snakeKey] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add sync timestamp
|
|
|
|
|
mapped.synced_at = new Date();
|
|
|
|
|
|
|
|
|
|
// Ensure is_deleted is false for new/updated records
|
|
|
|
|
if (mapped.is_deleted === undefined) {
|
|
|
|
|
mapped.is_deleted = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return mapped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Company entity
|
|
|
|
|
*/
|
|
|
|
|
function mapCompany(data: any): Record<string, any> {
|
feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.
entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
|
|
|
// Convert userDefinedFields array → JSONB object { name: value }
|
|
|
|
|
const udfs: Record<string, string | null> = {};
|
|
|
|
|
if (Array.isArray(data.userDefinedFields)) {
|
|
|
|
|
for (const udf of data.userDefinedFields) {
|
|
|
|
|
udfs[udf.name] = udf.value ?? null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
company_name: data.companyName || data.name,
|
|
|
|
|
company_number: data.companyNumber,
|
|
|
|
|
phone: data.phone,
|
|
|
|
|
fax: data.fax,
|
feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.
entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
|
|
|
web_address: data.webAddress,
|
2025-11-19 14:18:16 -05:00
|
|
|
address1: data.address1,
|
|
|
|
|
address2: data.address2,
|
|
|
|
|
city: data.city,
|
|
|
|
|
state: data.state,
|
|
|
|
|
postal_code: data.postalCode,
|
feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.
entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
|
|
|
country_id: data.countryID,
|
2025-11-19 14:18:16 -05:00
|
|
|
is_active: data.isActive !== undefined ? data.isActive : true,
|
|
|
|
|
company_type: data.companyType,
|
feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.
entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
|
|
|
company_category_id: data.companyCategoryID,
|
2025-11-19 14:18:16 -05:00
|
|
|
owner_resource_id: data.ownerResourceID,
|
|
|
|
|
territory_id: data.territoryID,
|
|
|
|
|
market_segment_id: data.marketSegmentID,
|
|
|
|
|
competitor_id: data.competitorID,
|
feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.
entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
|
|
|
parent_company_id: data.parentCompanyID,
|
2025-11-19 14:18:16 -05:00
|
|
|
billing_address1: data.billingAddress1,
|
|
|
|
|
billing_address2: data.billingAddress2,
|
feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.
entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
|
|
|
billing_city: data.billToCity,
|
|
|
|
|
billing_state: data.billToState,
|
|
|
|
|
billing_postal_code: data.billToZipCode,
|
|
|
|
|
bill_to_country_id: data.billToCountryID,
|
|
|
|
|
bill_to_address_to_use: data.billToAddressToUse,
|
|
|
|
|
bill_to_attention: data.billToAttention,
|
|
|
|
|
additional_address_information: data.additionalAddressInformation,
|
|
|
|
|
bill_to_additional_address_info: data.billToAdditionalAddressInformation,
|
2025-11-19 14:18:16 -05:00
|
|
|
tax_id: data.taxID,
|
feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.
entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
|
|
|
tax_exempt: data.isTaxExempt || false,
|
2025-11-19 14:18:16 -05:00
|
|
|
tax_region_id: data.taxRegionID,
|
|
|
|
|
currency_id: data.currencyID,
|
feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.
entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
|
|
|
invoice_method: data.invoiceMethod,
|
|
|
|
|
invoice_template_id: data.invoiceTemplateID,
|
|
|
|
|
invoice_email_message_id: data.invoiceEmailMessageID,
|
|
|
|
|
quote_template_id: data.quoteTemplateID,
|
|
|
|
|
purchase_order_template_id: data.purchaseOrderTemplateID,
|
|
|
|
|
key_account_icon: data.keyAccountIcon,
|
|
|
|
|
is_client_portal_active: data.isClientPortalActive || false,
|
|
|
|
|
is_task_fire_active: data.isTaskFireActive || false,
|
|
|
|
|
is_comanaged: data.isEnabledForComanaged || false,
|
|
|
|
|
sic_code: data.sicCode,
|
|
|
|
|
stock_market: data.stockMarket,
|
|
|
|
|
stock_symbol: data.stockSymbol,
|
|
|
|
|
alternate_phone1: data.alternatePhone1,
|
|
|
|
|
alternate_phone2: data.alternatePhone2,
|
|
|
|
|
survey_company_rating: data.surveyCompanyRating,
|
|
|
|
|
asset_value: data.assetValue,
|
|
|
|
|
last_activity_date: data.lastActivityDate,
|
|
|
|
|
last_tracked_modification_date_time: data.lastTrackedModifiedDateTime,
|
|
|
|
|
api_vendor_id: data.apiVendorID,
|
|
|
|
|
create_date: data.createDate,
|
|
|
|
|
created_by_resource_id: data.createdByResourceID,
|
feat: Duo Security integration — full data sync from Accounts + Admin API
Duo API Client (lib/services/duo-client.ts):
- HMAC-SHA1 request signing, GET/POST, automatic pagination
- Rate-limit handling (429 + Retry-After), configurable timeout
- Accounts API: listAccounts() via POST /accounts/v1/account/list
- Admin API: getUsers, getPhones, getGroups, getIntegrations, getAuthLogs
- Child account access: parent creds signed against child api_hostname + account_id
- Factory helpers: getDuoAccountsClient(), getDuoAdminClient()
Database (migration 058):
- 6 tables: duo_accounts, duo_users, duo_phones, duo_auth_logs, duo_groups, duo_integrations
- All with proper FKs, indexes, JSONB fields for capabilities/location/groups
Sync Service (lib/services/duo-sync-service.ts):
- syncAll() orchestration, per-child sequential sync, incremental auth logs
- Company matching: exact then case-insensitive containment (30/32 = 94% matched)
- Non-blocking with sync ID tracking
API Routes:
- POST/GET /api/duo/sync — trigger sync / check status
- GET /api/duo/accounts — list all accounts with stats + matched company
- GET /api/duo/accounts/[id]/users — users for a specific account
- POST /api/openclaw/sync/duo — OpenClaw trigger with API key auth
Verified data: 33 accounts, 832 users, 925 phones, 5927 auth logs, 46 groups, 78 integrations
Also: entity-mapper company fields update, task list marked complete
2026-03-27 11:10:30 -04:00
|
|
|
classification: data.classification ?? null,
|
|
|
|
|
bill_to_company_location_id: data.billToCompanyLocationID ?? null,
|
|
|
|
|
impersonator_creator_resource_id: data.impersonatorCreatorResourceID ?? null,
|
|
|
|
|
invoice_non_contract_items_to_parent_company: data.invoiceNonContractItemsToParentCompany ?? null,
|
|
|
|
|
quote_email_message_id: data.quoteEmailMessageID ?? null,
|
feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.
entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
|
|
|
user_defined_fields: Object.keys(udfs).length > 0 ? JSON.stringify(udfs) : null,
|
|
|
|
|
is_deleted: data.isDeleted || false,
|
2025-11-19 14:18:16 -05:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Ticket entity
|
|
|
|
|
*/
|
|
|
|
|
function mapTicket(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
company_id: data.companyID,
|
|
|
|
|
ticket_number: data.ticketNumber,
|
|
|
|
|
title: data.title,
|
|
|
|
|
description: data.description,
|
|
|
|
|
status: data.status,
|
|
|
|
|
priority: data.priority,
|
|
|
|
|
queue_id: data.queueID,
|
|
|
|
|
issue_type: data.issueType,
|
|
|
|
|
sub_issue_type: data.subIssueType,
|
|
|
|
|
source: data.source,
|
|
|
|
|
assigned_resource_id: data.assignedResourceID,
|
|
|
|
|
assigned_resource_role_id: data.assignedResourceRoleID,
|
|
|
|
|
contact_id: data.contactID,
|
|
|
|
|
account_physical_location_id: data.companyLocationID,
|
|
|
|
|
due_date_time: data.dueDateTime,
|
|
|
|
|
estimated_hours: data.estimatedHours,
|
|
|
|
|
completed_date: data.completedDate,
|
|
|
|
|
create_date: data.createDate,
|
|
|
|
|
created_by_contact_id: data.createdByContactID,
|
2026-01-26 15:26:34 -05:00
|
|
|
creator_resource_id: data.creatorResourceID,
|
|
|
|
|
creator_type: data.creatorType,
|
|
|
|
|
billing_code_id: data.billingCodeID,
|
|
|
|
|
configuration_item_id: data.configurationItemID,
|
2025-11-19 14:18:16 -05:00
|
|
|
last_activity_date: data.lastActivityDate,
|
|
|
|
|
last_customer_notification_date_time: data.lastCustomerNotificationDateTime,
|
|
|
|
|
last_customer_visible_activity_date_time: data.lastCustomerVisibleActivityDateTime,
|
2026-01-26 15:25:03 -05:00
|
|
|
first_response_date_time: data.firstResponseDateTime,
|
|
|
|
|
resolution_plan_date_time: data.resolutionPlanDateTime,
|
|
|
|
|
resolved_date_time: data.resolvedDateTime,
|
|
|
|
|
first_response_assigned_resource_id: data.firstResponseAssignedResourceID,
|
|
|
|
|
first_response_initiating_resource_id: data.firstResponseInitiatingResourceID,
|
|
|
|
|
project_id: data.projectID,
|
|
|
|
|
opportunity_id: data.opportunityID,
|
|
|
|
|
change_approval_board: data.changeApprovalBoard,
|
|
|
|
|
change_approval_status: data.changeApprovalStatus,
|
|
|
|
|
change_approval_type: data.changeApprovalType,
|
|
|
|
|
change_info_field1: data.changeInfoField1,
|
|
|
|
|
change_info_field2: data.changeInfoField2,
|
|
|
|
|
change_info_field3: data.changeInfoField3,
|
|
|
|
|
change_info_field4: data.changeInfoField4,
|
|
|
|
|
change_info_field5: data.changeInfoField5,
|
|
|
|
|
contract_id: data.contractID,
|
|
|
|
|
monitor_id: data.monitorID,
|
|
|
|
|
monitor_type_id: data.monitorTypeID,
|
|
|
|
|
ticket_type: data.ticketType,
|
|
|
|
|
ticket_category: data.ticketCategory,
|
|
|
|
|
service_level_agreement_id: data.serviceLevelAgreementID,
|
2025-11-19 14:18:16 -05:00
|
|
|
resolution: data.resolution,
|
2026-01-26 15:25:03 -05:00
|
|
|
purchase_order_number: data.purchaseOrderNumber,
|
|
|
|
|
ticket_completion_date: data.ticketCompletionDate,
|
|
|
|
|
last_activity_person_type: data.lastActivityPersonType,
|
|
|
|
|
last_activity_resource_id: data.lastActivityResourceID,
|
|
|
|
|
current_service_thermometer_rating: data.currentServiceThermometerRating,
|
|
|
|
|
previous_service_thermometer_rating: data.previousServiceThermometerRating,
|
|
|
|
|
service_thermometer_temperature: data.serviceThermometerTemperature,
|
|
|
|
|
api_vendor_id: data.apiVendorID,
|
2026-01-26 15:26:34 -05:00
|
|
|
problem_ticket_id: data.problemTicketId,
|
|
|
|
|
rma_status: data.rmaStatus,
|
|
|
|
|
rma_type: data.rmaType,
|
|
|
|
|
service_level_agreement_paused_next_event_hours: data.serviceLevelAgreementPausedNextEventHours,
|
|
|
|
|
is_assigned_to_comanaged: data.isAssignedToComanaged || false,
|
|
|
|
|
is_visible_to_comanaged: data.isVisibleToComanaged || false,
|
2025-11-19 14:18:16 -05:00
|
|
|
synced_at: data.synced_at,
|
|
|
|
|
is_deleted: data.is_deleted || false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 10:28:15 -05:00
|
|
|
/**
|
|
|
|
|
* Map TicketNote entity
|
|
|
|
|
*/
|
|
|
|
|
function mapTicketNote(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
ticket_id: data.ticketID,
|
|
|
|
|
title: data.title,
|
|
|
|
|
description: data.description,
|
|
|
|
|
note_type: data.noteType,
|
|
|
|
|
publish: data.publish,
|
|
|
|
|
creator_resource_id: data.creatorResourceID,
|
|
|
|
|
creator_type: data.creatorType,
|
|
|
|
|
last_activity_date: data.lastActivityDate,
|
|
|
|
|
create_date_time: data.createDateTime,
|
|
|
|
|
synced_at: data.synced_at,
|
|
|
|
|
is_deleted: data.is_deleted || false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
/**
|
|
|
|
|
* Map Task entity
|
|
|
|
|
*/
|
|
|
|
|
function mapTask(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
title: data.title,
|
|
|
|
|
description: data.description,
|
|
|
|
|
status: data.status,
|
|
|
|
|
priority: data.priority,
|
|
|
|
|
assigned_resource_id: data.assignedResourceID,
|
|
|
|
|
assigned_resource_role_id: data.assignedResourceRoleID,
|
|
|
|
|
department_id: data.departmentID,
|
|
|
|
|
estimated_hours: data.estimatedHours,
|
|
|
|
|
remaining_hours: data.remainingHours,
|
|
|
|
|
hours_to_be_scheduled: data.hoursToBeScheduled,
|
|
|
|
|
start_date_time: data.startDateTime,
|
|
|
|
|
end_date_time: data.endDateTime,
|
|
|
|
|
completed_date_time: data.completedDateTime,
|
|
|
|
|
create_date_time: data.createDateTime,
|
|
|
|
|
creator_resource_id: data.creatorResourceID,
|
|
|
|
|
completed_by_resource_id: data.completedByResourceID,
|
|
|
|
|
last_activity_date_time: data.lastActivityDateTime,
|
|
|
|
|
project_id: data.projectID,
|
|
|
|
|
ticket_id: data.ticketID,
|
|
|
|
|
phase_id: data.phaseID,
|
|
|
|
|
allocation_code_id: data.allocationCodeID,
|
|
|
|
|
task_type: data.taskType,
|
|
|
|
|
task_is_billable: data.taskIsBillable !== undefined ? data.taskIsBillable : true,
|
|
|
|
|
task_number: data.taskNumber,
|
|
|
|
|
purchase_order_number: data.purchaseOrderNumber,
|
|
|
|
|
can_client_portal_user_complete_task: data.canClientPortalUserCompleteTask || false,
|
|
|
|
|
creator_type: data.creatorType,
|
|
|
|
|
task_category_id: data.taskCategoryID,
|
|
|
|
|
synced_at: data.synced_at,
|
|
|
|
|
is_deleted: data.is_deleted || false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Project entity
|
|
|
|
|
*/
|
|
|
|
|
function mapProject(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
company_id: data.companyID,
|
|
|
|
|
project_name: data.projectName,
|
|
|
|
|
project_number: data.projectNumber,
|
|
|
|
|
description: data.description,
|
|
|
|
|
start_date_time: data.start_date_time,
|
|
|
|
|
end_date_time: data.end_date_time,
|
|
|
|
|
estimated_time: data.estimated_time,
|
|
|
|
|
actual_hours: data.actual_hours,
|
|
|
|
|
estimated_sale_cost: data.estimated_sale_cost,
|
|
|
|
|
labor_estimated_costs: data.labor_estimated_costs,
|
|
|
|
|
labor_estimated_revenue: data.labor_estimated_revenue,
|
|
|
|
|
project_cost_estimated_margin_percentage: data.project_cost_estimated_margin_percentage,
|
|
|
|
|
status: data.status,
|
|
|
|
|
type: data.type,
|
|
|
|
|
project_lead_resource_id: data.project_lead_resource_id,
|
|
|
|
|
account_executive_resource_id: data.account_executive_resource_id,
|
|
|
|
|
owner_resource_id: data.owner_resource_id,
|
|
|
|
|
creator_resource_id: data.creator_resource_id,
|
|
|
|
|
completed_percentage: data.completed_percentage,
|
|
|
|
|
completed_date_time: data.completed_date_time,
|
|
|
|
|
duration: data.duration,
|
|
|
|
|
original_estimated_revenue: data.original_estimated_revenue,
|
|
|
|
|
estimated_time_cost: data.estimated_time_cost,
|
|
|
|
|
purchase_order_number: data.purchase_order_number,
|
|
|
|
|
business_division_subdivision_id: data.business_division_subdivision_id,
|
|
|
|
|
line_of_business_id: data.line_of_business_id,
|
|
|
|
|
department: data.department,
|
|
|
|
|
last_activity_date_time: data.last_activity_date_time,
|
|
|
|
|
last_activity_person_type: data.last_activity_person_type,
|
|
|
|
|
last_activity_resource_id: data.last_activity_resource_id,
|
|
|
|
|
synced_at: data.synced_at,
|
|
|
|
|
is_deleted: data.is_deleted || false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-03-24 13:57:33 -04:00
|
|
|
/**
|
|
|
|
|
* 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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
/**
|
|
|
|
|
* Map Resource entity
|
|
|
|
|
*/
|
|
|
|
|
function mapResource(data: any): Record<string, any> {
|
2026-01-26 14:48:37 -05:00
|
|
|
// Helper to safely parse integer values (returns null for non-numeric strings)
|
|
|
|
|
const safeInt = (value: any): number | null => {
|
|
|
|
|
if (value === null || value === undefined || value === '') return null;
|
|
|
|
|
const num = typeof value === 'number' ? value : parseInt(value, 10);
|
|
|
|
|
return isNaN(num) ? null : num;
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
return {
|
|
|
|
|
id: data.id,
|
feat: expand Resources entity to include all Autotask API fields
Add comprehensive field support for Resources entity including:
- Name fields: middleInitial, namePrefix, nameSuffix
- Contact: emailAddress2, emailAddress3, homePhone
- Employment: accountingReferenceID, payrollType, internalCost
- System: emailTypeCode, numberFormat, timeFormat, dateFormat
- Demographics: gender
- Security: licenseType, securityLevel
- Location: defaultServiceDeskRoleID
Changes:
- Migration 015: Add new columns to resources table
- Updated entity mapper to map all Autotask Resource fields
- Expanded TypeScript Resource interface with all fields
- Maintains backward compatibility with existing field names
This ensures all data exposed by the Autotask Resources API is
now captured and stored in the database.
2026-01-26 14:28:51 -05:00
|
|
|
// Name fields
|
|
|
|
|
first_name: data.firstName,
|
|
|
|
|
last_name: data.lastName,
|
|
|
|
|
middle_initial: data.middleInitial,
|
|
|
|
|
name_prefix: data.namePrefix,
|
|
|
|
|
name_suffix: data.nameSuffix,
|
|
|
|
|
|
|
|
|
|
// Contact information
|
|
|
|
|
email: data.email || data.emailAddress,
|
|
|
|
|
email_address: data.emailAddress,
|
|
|
|
|
email_address2: data.emailAddress2,
|
|
|
|
|
email_address3: data.emailAddress3,
|
|
|
|
|
office_phone: data.officePhone,
|
|
|
|
|
mobile_phone: data.mobilePhone,
|
|
|
|
|
home_phone: data.homePhone,
|
|
|
|
|
office_extension: data.officeExtension,
|
|
|
|
|
|
|
|
|
|
// System fields
|
|
|
|
|
user_name: data.userName,
|
2025-11-19 14:18:16 -05:00
|
|
|
title: data.title,
|
feat: expand Resources entity to include all Autotask API fields
Add comprehensive field support for Resources entity including:
- Name fields: middleInitial, namePrefix, nameSuffix
- Contact: emailAddress2, emailAddress3, homePhone
- Employment: accountingReferenceID, payrollType, internalCost
- System: emailTypeCode, numberFormat, timeFormat, dateFormat
- Demographics: gender
- Security: licenseType, securityLevel
- Location: defaultServiceDeskRoleID
Changes:
- Migration 015: Add new columns to resources table
- Updated entity mapper to map all Autotask Resource fields
- Expanded TypeScript Resource interface with all fields
- Maintains backward compatibility with existing field names
This ensures all data exposed by the Autotask Resources API is
now captured and stored in the database.
2026-01-26 14:28:51 -05:00
|
|
|
is_active: data.isActive !== undefined ? data.isActive : true,
|
|
|
|
|
|
|
|
|
|
// Employment details
|
2026-01-26 14:48:37 -05:00
|
|
|
resource_type: safeInt(data.resourceType),
|
feat: expand Resources entity to include all Autotask API fields
Add comprehensive field support for Resources entity including:
- Name fields: middleInitial, namePrefix, nameSuffix
- Contact: emailAddress2, emailAddress3, homePhone
- Employment: accountingReferenceID, payrollType, internalCost
- System: emailTypeCode, numberFormat, timeFormat, dateFormat
- Demographics: gender
- Security: licenseType, securityLevel
- Location: defaultServiceDeskRoleID
Changes:
- Migration 015: Add new columns to resources table
- Updated entity mapper to map all Autotask Resource fields
- Expanded TypeScript Resource interface with all fields
- Maintains backward compatibility with existing field names
This ensures all data exposed by the Autotask Resources API is
now captured and stored in the database.
2026-01-26 14:28:51 -05:00
|
|
|
payroll_identifier: data.payrollIdentifier,
|
|
|
|
|
pay_roll_identifier: data.payrollIdentifier, // Keep for backward compatibility
|
2026-01-26 14:48:37 -05:00
|
|
|
payroll_type: safeInt(data.payrollType),
|
feat: expand Resources entity to include all Autotask API fields
Add comprehensive field support for Resources entity including:
- Name fields: middleInitial, namePrefix, nameSuffix
- Contact: emailAddress2, emailAddress3, homePhone
- Employment: accountingReferenceID, payrollType, internalCost
- System: emailTypeCode, numberFormat, timeFormat, dateFormat
- Demographics: gender
- Security: licenseType, securityLevel
- Location: defaultServiceDeskRoleID
Changes:
- Migration 015: Add new columns to resources table
- Updated entity mapper to map all Autotask Resource fields
- Expanded TypeScript Resource interface with all fields
- Maintains backward compatibility with existing field names
This ensures all data exposed by the Autotask Resources API is
now captured and stored in the database.
2026-01-26 14:28:51 -05:00
|
|
|
accounting_reference_id: data.accountingReferenceID,
|
|
|
|
|
internal_cost: data.internalCost,
|
|
|
|
|
hire_date: data.hireDate,
|
|
|
|
|
|
|
|
|
|
// Location and availability
|
2026-01-26 14:48:37 -05:00
|
|
|
location_id: safeInt(data.locationID),
|
feat: expand Resources entity to include all Autotask API fields
Add comprehensive field support for Resources entity including:
- Name fields: middleInitial, namePrefix, nameSuffix
- Contact: emailAddress2, emailAddress3, homePhone
- Employment: accountingReferenceID, payrollType, internalCost
- System: emailTypeCode, numberFormat, timeFormat, dateFormat
- Demographics: gender
- Security: licenseType, securityLevel
- Location: defaultServiceDeskRoleID
Changes:
- Migration 015: Add new columns to resources table
- Updated entity mapper to map all Autotask Resource fields
- Expanded TypeScript Resource interface with all fields
- Maintains backward compatibility with existing field names
This ensures all data exposed by the Autotask Resources API is
now captured and stored in the database.
2026-01-26 14:28:51 -05:00
|
|
|
travel_availability_pct: data.travelAvailabilityPct,
|
2026-01-26 14:48:37 -05:00
|
|
|
default_service_desk_role_id: safeInt(data.defaultServiceDeskRoleID),
|
feat: expand Resources entity to include all Autotask API fields
Add comprehensive field support for Resources entity including:
- Name fields: middleInitial, namePrefix, nameSuffix
- Contact: emailAddress2, emailAddress3, homePhone
- Employment: accountingReferenceID, payrollType, internalCost
- System: emailTypeCode, numberFormat, timeFormat, dateFormat
- Demographics: gender
- Security: licenseType, securityLevel
- Location: defaultServiceDeskRoleID
Changes:
- Migration 015: Add new columns to resources table
- Updated entity mapper to map all Autotask Resource fields
- Expanded TypeScript Resource interface with all fields
- Maintains backward compatibility with existing field names
This ensures all data exposed by the Autotask Resources API is
now captured and stored in the database.
2026-01-26 14:28:51 -05:00
|
|
|
|
|
|
|
|
// System preferences
|
2026-01-26 14:48:37 -05:00
|
|
|
email_type_code: safeInt(data.emailTypeCode),
|
feat: expand Resources entity to include all Autotask API fields
Add comprehensive field support for Resources entity including:
- Name fields: middleInitial, namePrefix, nameSuffix
- Contact: emailAddress2, emailAddress3, homePhone
- Employment: accountingReferenceID, payrollType, internalCost
- System: emailTypeCode, numberFormat, timeFormat, dateFormat
- Demographics: gender
- Security: licenseType, securityLevel
- Location: defaultServiceDeskRoleID
Changes:
- Migration 015: Add new columns to resources table
- Updated entity mapper to map all Autotask Resource fields
- Expanded TypeScript Resource interface with all fields
- Maintains backward compatibility with existing field names
This ensures all data exposed by the Autotask Resources API is
now captured and stored in the database.
2026-01-26 14:28:51 -05:00
|
|
|
number_format: data.numberFormat,
|
|
|
|
|
time_format: data.timeFormat,
|
|
|
|
|
date_format: data.dateFormat,
|
|
|
|
|
|
|
|
|
|
// Demographics and security
|
2026-01-26 14:48:37 -05:00
|
|
|
gender: safeInt(data.gender),
|
|
|
|
|
license_type: safeInt(data.licenseType),
|
|
|
|
|
security_level: safeInt(data.securityLevel),
|
feat: expand Resources entity to include all Autotask API fields
Add comprehensive field support for Resources entity including:
- Name fields: middleInitial, namePrefix, nameSuffix
- Contact: emailAddress2, emailAddress3, homePhone
- Employment: accountingReferenceID, payrollType, internalCost
- System: emailTypeCode, numberFormat, timeFormat, dateFormat
- Demographics: gender
- Security: licenseType, securityLevel
- Location: defaultServiceDeskRoleID
Changes:
- Migration 015: Add new columns to resources table
- Updated entity mapper to map all Autotask Resource fields
- Expanded TypeScript Resource interface with all fields
- Maintains backward compatibility with existing field names
This ensures all data exposed by the Autotask Resources API is
now captured and stored in the database.
2026-01-26 14:28:51 -05:00
|
|
|
|
|
|
|
|
// Ratings
|
|
|
|
|
survey_resource_rating: data.surveyResourceRating,
|
|
|
|
|
|
|
|
|
|
// Audit fields
|
2025-11-19 14:18:16 -05:00
|
|
|
synced_at: data.synced_at,
|
|
|
|
|
is_deleted: data.is_deleted || false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Configuration Item entity
|
|
|
|
|
*/
|
|
|
|
|
function mapConfigurationItem(data: any): Record<string, any> {
|
|
|
|
|
const mapped: Record<string, any> = {
|
|
|
|
|
id: data.id,
|
|
|
|
|
company_id: data.companyID,
|
|
|
|
|
product_id: data.productID,
|
|
|
|
|
reference_title: data.referenceTitle,
|
|
|
|
|
reference_number: data.referenceNumber,
|
|
|
|
|
serial_number: data.serialNumber,
|
|
|
|
|
install_date: data.installDate,
|
|
|
|
|
warranty_expiration_date: data.warrantyExpirationDate,
|
|
|
|
|
is_active: data.isActive !== undefined ? data.isActive : true,
|
fix: configuration_items sync — correct camelCase field mapping, store all UDFs in udfs JSONB
- Fix mapConfigurationItem() to use Autotask camelCase field names (was using
snake_case keys that never matched, leaving costs/Datto/RMM fields empty)
- Map all 60+ fields explicitly: costs, Datto device fields, RMM audit fields,
location, vendor, SNMP, backup dates, source cost, etc.
- Store entire userDefinedFields array as udfs JSONB (keyed by UDF name)
- Retain backup_type_udf VARCHAR column for backward compat
- Migration 052: add udfs JSONB column + GIN index to configuration_items
2026-03-17 15:39:42 -04:00
|
|
|
daily_cost: data.dailyCost,
|
|
|
|
|
hourly_cost: data.hourlyCost,
|
|
|
|
|
monthly_cost: data.monthlyCost,
|
|
|
|
|
per_use_cost: data.perUseCost,
|
|
|
|
|
setup_fee: data.setupFee,
|
2025-11-19 14:18:16 -05:00
|
|
|
contact_id: data.contactID,
|
fix: configuration_items sync — correct camelCase field mapping, store all UDFs in udfs JSONB
- Fix mapConfigurationItem() to use Autotask camelCase field names (was using
snake_case keys that never matched, leaving costs/Datto/RMM fields empty)
- Map all 60+ fields explicitly: costs, Datto device fields, RMM audit fields,
location, vendor, SNMP, backup dates, source cost, etc.
- Store entire userDefinedFields array as udfs JSONB (keyed by UDF name)
- Retain backup_type_udf VARCHAR column for backward compat
- Migration 052: add udfs JSONB column + GIN index to configuration_items
2026-03-17 15:39:42 -04:00
|
|
|
location_id: data.locationID,
|
|
|
|
|
vendor_id: data.vendorID,
|
|
|
|
|
installed_by_id: data.installedByID,
|
|
|
|
|
installed_by_contact_id: data.installedByContactID,
|
|
|
|
|
parent_configuration_item_id: data.parentConfigurationItemID,
|
|
|
|
|
notes: data.notes,
|
|
|
|
|
create_date: data.createDate,
|
|
|
|
|
created_by_person_id: data.createdByPersonID,
|
|
|
|
|
last_modified_time: data.lastModifiedTime,
|
|
|
|
|
last_activity_person_type: data.lastActivityPersonType,
|
|
|
|
|
impersonator_creator_resource_id: data.impersonatorCreatorResourceID,
|
|
|
|
|
configuration_item_category_id: data.configurationItemCategoryID,
|
2025-11-19 14:18:16 -05:00
|
|
|
configuration_item_type: data.type,
|
fix: configuration_items sync — correct camelCase field mapping, store all UDFs in udfs JSONB
- Fix mapConfigurationItem() to use Autotask camelCase field names (was using
snake_case keys that never matched, leaving costs/Datto/RMM fields empty)
- Map all 60+ fields explicitly: costs, Datto device fields, RMM audit fields,
location, vendor, SNMP, backup dates, source cost, etc.
- Store entire userDefinedFields array as udfs JSONB (keyed by UDF name)
- Retain backup_type_udf VARCHAR column for backward compat
- Migration 052: add udfs JSONB column + GIN index to configuration_items
2026-03-17 15:39:42 -04:00
|
|
|
device_type: data.deviceType,
|
|
|
|
|
rmm_device_uid: data.rmmDeviceUID,
|
|
|
|
|
api_vendor_id: data.apiVendorID,
|
|
|
|
|
// Datto fields
|
|
|
|
|
datto_availability: data.dattoAvailability,
|
|
|
|
|
datto_device_memory_megabytes: data.dattoDeviceMemoryMegabytes,
|
|
|
|
|
datto_drives_errors: data.dattoDrivesErrors,
|
|
|
|
|
datto_hostname: data.dattoHostname,
|
|
|
|
|
datto_internal_ip: data.dattoInternalIP,
|
|
|
|
|
datto_kernel_version_id: data.dattoKernelVersionID,
|
|
|
|
|
datto_last_check_in_date_time: data.dattoLastCheckInDateTime,
|
|
|
|
|
datto_nic_speed_kilobits_per_second: data.dattoNicSpeedKilobitsPerSecond,
|
|
|
|
|
datto_number_of_agents: data.dattoNumberOfAgents,
|
|
|
|
|
datto_number_of_drives: data.dattoNumberOfDrives,
|
|
|
|
|
datto_number_of_logical_volumes: data.dattoNumberOfLogicalVolumes,
|
|
|
|
|
datto_number_of_volumes: data.dattoNumberOfVolumes,
|
|
|
|
|
datto_off_site_storage_used_bytes: data.dattoOffSiteStorageUsedBytes,
|
|
|
|
|
datto_os_version_id: data.dattoOsVersionID,
|
|
|
|
|
datto_percentage_used: data.dattoPercentageUsed,
|
|
|
|
|
datto_protected_kilobytes: data.dattoProtectedKilobytes,
|
|
|
|
|
datto_remote_ip: data.dattoRemoteIP,
|
|
|
|
|
datto_serial_number: data.dattoSerialNumber,
|
|
|
|
|
datto_uptime_seconds: data.dattoUptimeSeconds,
|
|
|
|
|
datto_used_kilobytes: data.dattoUsedKilobytes,
|
|
|
|
|
datto_z_pool_percentage: data.dattoZPoolPercentage,
|
|
|
|
|
device_networking_id: data.deviceNetworkingID,
|
|
|
|
|
last_backup_date: data.lastBackupDateTime || data.lastBackupDate,
|
|
|
|
|
last_backup_status: data.lastBackupStatus,
|
|
|
|
|
os_version_id: data.osVersionID,
|
|
|
|
|
service_id: data.serviceID,
|
|
|
|
|
service_bundle_id: data.serviceBundleID,
|
|
|
|
|
snmp_location: data.snmpLocation,
|
|
|
|
|
snmp_name: data.snmpName,
|
|
|
|
|
snmp_contact: data.snmpContact,
|
|
|
|
|
// RMM audit fields
|
|
|
|
|
rmm_device_audit_architecture_id: data.rmmDeviceAuditArchitectureID,
|
|
|
|
|
rmm_device_audit_display_adaptor_id: data.rmmDeviceAuditDisplayAdaptorID,
|
|
|
|
|
rmm_device_audit_domain_id: data.rmmDeviceAuditDomainID,
|
|
|
|
|
rmm_device_audit_external_ip_address: data.rmmDeviceAuditExternalIPAddress,
|
|
|
|
|
rmm_device_audit_hostname: data.rmmDeviceAuditHostname,
|
|
|
|
|
rmm_device_audit_ip_address: data.rmmDeviceAuditIPAddress,
|
|
|
|
|
rmm_device_audit_mac_address: data.rmmDeviceAuditMacAddress,
|
|
|
|
|
rmm_device_audit_manufacturer_id: data.rmmDeviceAuditManufacturerID,
|
|
|
|
|
rmm_device_audit_missing_patch_count: data.rmmDeviceAuditMissingPatchCount,
|
|
|
|
|
rmm_device_audit_mobile_network_operator_id: data.rmmDeviceAuditMobileNetworkOperatorID,
|
|
|
|
|
rmm_device_audit_mobile_number: data.rmmDeviceAuditMobileNumber,
|
|
|
|
|
rmm_device_audit_model_id: data.rmmDeviceAuditModelID,
|
|
|
|
|
rmm_device_audit_motherboard_id: data.rmmDeviceAuditMotherboardID,
|
|
|
|
|
rmm_device_audit_operating_system_id: data.rmmDeviceAuditOperatingSystemID,
|
|
|
|
|
rmm_device_audit_processor_id: data.rmmDeviceAuditProcessorID,
|
|
|
|
|
rmm_device_audit_service_pack_id: data.rmmDeviceAuditServicePackID,
|
|
|
|
|
rmm_device_audit_snmp_contact: data.rmmDeviceAuditSNMPContact,
|
|
|
|
|
rmm_device_audit_snmp_location: data.rmmDeviceAuditSNMPLocation,
|
|
|
|
|
rmm_device_audit_snmp_name: data.rmmDeviceAuditSNMPName,
|
|
|
|
|
rmm_device_audit_software_status_id: data.rmmDeviceAuditSoftwareStatusID,
|
|
|
|
|
rmm_device_audit_storage_bytes: data.rmmDeviceAuditStorageBytes,
|
|
|
|
|
rmm_open_alert_count: data.rmmOpenAlertCount,
|
|
|
|
|
rmm_device_audit_description: data.rmmDeviceAuditDescription,
|
|
|
|
|
rmm_device_audit_device_type_id: data.rmmDeviceAuditDeviceTypeID,
|
|
|
|
|
rmm_device_audit_last_user: data.rmmDeviceAuditLastUser,
|
|
|
|
|
rmm_device_audit_memory_bytes: data.rmmDeviceAuditMemoryBytes,
|
|
|
|
|
source_cost_id: data.sourceCostID,
|
|
|
|
|
source_cost_type: data.sourceCostType,
|
2025-11-19 14:18:16 -05:00
|
|
|
};
|
|
|
|
|
|
fix: configuration_items sync — correct camelCase field mapping, store all UDFs in udfs JSONB
- Fix mapConfigurationItem() to use Autotask camelCase field names (was using
snake_case keys that never matched, leaving costs/Datto/RMM fields empty)
- Map all 60+ fields explicitly: costs, Datto device fields, RMM audit fields,
location, vendor, SNMP, backup dates, source cost, etc.
- Store entire userDefinedFields array as udfs JSONB (keyed by UDF name)
- Retain backup_type_udf VARCHAR column for backward compat
- Migration 052: add udfs JSONB column + GIN index to configuration_items
2026-03-17 15:39:42 -04:00
|
|
|
// Store all UDFs as JSONB
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
if (data.userDefinedFields && Array.isArray(data.userDefinedFields)) {
|
fix: configuration_items sync — correct camelCase field mapping, store all UDFs in udfs JSONB
- Fix mapConfigurationItem() to use Autotask camelCase field names (was using
snake_case keys that never matched, leaving costs/Datto/RMM fields empty)
- Map all 60+ fields explicitly: costs, Datto device fields, RMM audit fields,
location, vendor, SNMP, backup dates, source cost, etc.
- Store entire userDefinedFields array as udfs JSONB (keyed by UDF name)
- Retain backup_type_udf VARCHAR column for backward compat
- Migration 052: add udfs JSONB column + GIN index to configuration_items
2026-03-17 15:39:42 -04:00
|
|
|
const udfMap: Record<string, string> = {};
|
|
|
|
|
for (const udf of data.userDefinedFields) {
|
|
|
|
|
if (udf.name != null) {
|
|
|
|
|
udfMap[String(udf.name)] = udf.value ?? null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
mapped.udfs = Object.keys(udfMap).length > 0 ? udfMap : null;
|
|
|
|
|
|
|
|
|
|
// Also extract the named backup_type_udf for backward compat
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
const backupUdf = data.userDefinedFields.find(
|
|
|
|
|
(udf: any) => udf.name === 'Backup Type' || String(udf.name) === '29693319'
|
|
|
|
|
);
|
fix: configuration_items sync — correct camelCase field mapping, store all UDFs in udfs JSONB
- Fix mapConfigurationItem() to use Autotask camelCase field names (was using
snake_case keys that never matched, leaving costs/Datto/RMM fields empty)
- Map all 60+ fields explicitly: costs, Datto device fields, RMM audit fields,
location, vendor, SNMP, backup dates, source cost, etc.
- Store entire userDefinedFields array as udfs JSONB (keyed by UDF name)
- Retain backup_type_udf VARCHAR column for backward compat
- Migration 052: add udfs JSONB column + GIN index to configuration_items
2026-03-17 15:39:42 -04:00
|
|
|
if (backupUdf?.value) {
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
mapped.backup_type_udf = backupUdf.value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
return mapped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Contact entity
|
|
|
|
|
*/
|
|
|
|
|
function mapContact(data: any): Record<string, any> {
|
feat: contacts sync — add all missing fields + UDFs
Migration 057:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD middle_initial, note, external_id, country_id, company_location_id
- ADD create_date, impersonator_creator_resource_id
- ADD is_opted_out_from_bulk_email, bulk_email_opt_out_time
- ADD solicitation_opt_out_time, survey_opt_out
- ADD receives_email_notifications, billing_contact
entity-mapper.ts mapContact():
- Fix broken snake_case field refs → correct camelCase API names:
alternate_phone → alternatePhone, mobile_phone → mobilePhone
name_prefix/suffix → namePrefix/nameSuffix
facebook/twitter/linkedin_url → facebookUrl/twitterUrl/linkedInUrl
primary_contact → primaryContact, solicitation_opt_out → solicitationOptOut
last_activity/modified_date → lastActivityDate/lastModifiedDate
api_vendor_id → apiVendorID, is_active → isActive, is_deleted → isDeleted
- Add UDF conversion: userDefinedFields[] → JSONB {name: value}
- 17 UDFs stored: UserID, Birthday, O365License, VIP User, Department,
Password, Email Password, Archive Email, User System Profile, etc.
Results: 4234 contacts synced, 3318 with UDFs, 4140 with create_date
2026-03-26 13:16:13 -04:00
|
|
|
// Convert userDefinedFields array → JSONB object { name: value }
|
|
|
|
|
const udfs: Record<string, string | null> = {};
|
|
|
|
|
if (Array.isArray(data.userDefinedFields)) {
|
|
|
|
|
for (const udf of data.userDefinedFields) {
|
|
|
|
|
udfs[udf.name] = udf.value ?? null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
company_id: data.companyID,
|
|
|
|
|
first_name: data.firstName,
|
|
|
|
|
last_name: data.lastName,
|
feat: contacts sync — add all missing fields + UDFs
Migration 057:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD middle_initial, note, external_id, country_id, company_location_id
- ADD create_date, impersonator_creator_resource_id
- ADD is_opted_out_from_bulk_email, bulk_email_opt_out_time
- ADD solicitation_opt_out_time, survey_opt_out
- ADD receives_email_notifications, billing_contact
entity-mapper.ts mapContact():
- Fix broken snake_case field refs → correct camelCase API names:
alternate_phone → alternatePhone, mobile_phone → mobilePhone
name_prefix/suffix → namePrefix/nameSuffix
facebook/twitter/linkedin_url → facebookUrl/twitterUrl/linkedInUrl
primary_contact → primaryContact, solicitation_opt_out → solicitationOptOut
last_activity/modified_date → lastActivityDate/lastModifiedDate
api_vendor_id → apiVendorID, is_active → isActive, is_deleted → isDeleted
- Add UDF conversion: userDefinedFields[] → JSONB {name: value}
- 17 UDFs stored: UserID, Birthday, O365License, VIP User, Department,
Password, Email Password, Archive Email, User System Profile, etc.
Results: 4234 contacts synced, 3318 with UDFs, 4140 with create_date
2026-03-26 13:16:13 -04:00
|
|
|
middle_initial: data.middleInitial,
|
2025-11-19 14:18:16 -05:00
|
|
|
title: data.title,
|
feat: contacts sync — add all missing fields + UDFs
Migration 057:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD middle_initial, note, external_id, country_id, company_location_id
- ADD create_date, impersonator_creator_resource_id
- ADD is_opted_out_from_bulk_email, bulk_email_opt_out_time
- ADD solicitation_opt_out_time, survey_opt_out
- ADD receives_email_notifications, billing_contact
entity-mapper.ts mapContact():
- Fix broken snake_case field refs → correct camelCase API names:
alternate_phone → alternatePhone, mobile_phone → mobilePhone
name_prefix/suffix → namePrefix/nameSuffix
facebook/twitter/linkedin_url → facebookUrl/twitterUrl/linkedInUrl
primary_contact → primaryContact, solicitation_opt_out → solicitationOptOut
last_activity/modified_date → lastActivityDate/lastModifiedDate
api_vendor_id → apiVendorID, is_active → isActive, is_deleted → isDeleted
- Add UDF conversion: userDefinedFields[] → JSONB {name: value}
- 17 UDFs stored: UserID, Birthday, O365License, VIP User, Department,
Password, Email Password, Archive Email, User System Profile, etc.
Results: 4234 contacts synced, 3318 with UDFs, 4140 with create_date
2026-03-26 13:16:13 -04:00
|
|
|
email_address: data.emailAddress,
|
|
|
|
|
email_address2: data.emailAddress2,
|
|
|
|
|
email_address3: data.emailAddress3,
|
2025-11-19 14:18:16 -05:00
|
|
|
phone: data.phone,
|
|
|
|
|
extension: data.extension,
|
feat: contacts sync — add all missing fields + UDFs
Migration 057:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD middle_initial, note, external_id, country_id, company_location_id
- ADD create_date, impersonator_creator_resource_id
- ADD is_opted_out_from_bulk_email, bulk_email_opt_out_time
- ADD solicitation_opt_out_time, survey_opt_out
- ADD receives_email_notifications, billing_contact
entity-mapper.ts mapContact():
- Fix broken snake_case field refs → correct camelCase API names:
alternate_phone → alternatePhone, mobile_phone → mobilePhone
name_prefix/suffix → namePrefix/nameSuffix
facebook/twitter/linkedin_url → facebookUrl/twitterUrl/linkedInUrl
primary_contact → primaryContact, solicitation_opt_out → solicitationOptOut
last_activity/modified_date → lastActivityDate/lastModifiedDate
api_vendor_id → apiVendorID, is_active → isActive, is_deleted → isDeleted
- Add UDF conversion: userDefinedFields[] → JSONB {name: value}
- 17 UDFs stored: UserID, Birthday, O365License, VIP User, Department,
Password, Email Password, Archive Email, User System Profile, etc.
Results: 4234 contacts synced, 3318 with UDFs, 4140 with create_date
2026-03-26 13:16:13 -04:00
|
|
|
alternate_phone: data.alternatePhone,
|
|
|
|
|
mobile_phone: data.mobilePhone,
|
|
|
|
|
fax: data.faxNumber,
|
|
|
|
|
address_line: data.addressLine,
|
|
|
|
|
address_line1: data.addressLine1,
|
2025-11-19 14:18:16 -05:00
|
|
|
city: data.city,
|
|
|
|
|
state: data.state,
|
feat: contacts sync — add all missing fields + UDFs
Migration 057:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD middle_initial, note, external_id, country_id, company_location_id
- ADD create_date, impersonator_creator_resource_id
- ADD is_opted_out_from_bulk_email, bulk_email_opt_out_time
- ADD solicitation_opt_out_time, survey_opt_out
- ADD receives_email_notifications, billing_contact
entity-mapper.ts mapContact():
- Fix broken snake_case field refs → correct camelCase API names:
alternate_phone → alternatePhone, mobile_phone → mobilePhone
name_prefix/suffix → namePrefix/nameSuffix
facebook/twitter/linkedin_url → facebookUrl/twitterUrl/linkedInUrl
primary_contact → primaryContact, solicitation_opt_out → solicitationOptOut
last_activity/modified_date → lastActivityDate/lastModifiedDate
api_vendor_id → apiVendorID, is_active → isActive, is_deleted → isDeleted
- Add UDF conversion: userDefinedFields[] → JSONB {name: value}
- 17 UDFs stored: UserID, Birthday, O365License, VIP User, Department,
Password, Email Password, Archive Email, User System Profile, etc.
Results: 4234 contacts synced, 3318 with UDFs, 4140 with create_date
2026-03-26 13:16:13 -04:00
|
|
|
zip_code: data.zipCode,
|
|
|
|
|
country_id: data.countryID,
|
|
|
|
|
company_location_id: data.companyLocationID,
|
|
|
|
|
is_active: data.isActive !== undefined ? data.isActive : true,
|
|
|
|
|
name_prefix: data.namePrefix,
|
|
|
|
|
name_suffix: data.nameSuffix,
|
|
|
|
|
facebook_url: data.facebookUrl,
|
|
|
|
|
twitter_url: data.twitterUrl,
|
|
|
|
|
linked_in_url: data.linkedInUrl,
|
|
|
|
|
primary_contact: data.primaryContact || false,
|
|
|
|
|
billing_contact: data.billingContact || false,
|
|
|
|
|
solicitation_opt_out: data.solicitationOptOut || false,
|
|
|
|
|
solicitation_opt_out_time: data.solicitationOptOutTime,
|
|
|
|
|
survey_opt_out: data.surveyOptOut || false,
|
|
|
|
|
is_opted_out_from_bulk_email: data.isOptedOutFromBulkEmail || false,
|
|
|
|
|
bulk_email_opt_out_time: data.bulkEmailOptOutTime,
|
|
|
|
|
receives_email_notifications: data.receivesEmailNotifications !== undefined ? data.receivesEmailNotifications : true,
|
|
|
|
|
room_number: data.roomNumber,
|
|
|
|
|
note: data.note,
|
|
|
|
|
external_id: data.externalID,
|
|
|
|
|
api_vendor_id: data.apiVendorID,
|
|
|
|
|
impersonator_creator_resource_id: data.impersonatorCreatorResourceID,
|
|
|
|
|
create_date: data.createDate,
|
|
|
|
|
last_activity_date: data.lastActivityDate,
|
|
|
|
|
last_modified_date: data.lastModifiedDate,
|
|
|
|
|
user_defined_fields: Object.keys(udfs).length > 0 ? JSON.stringify(udfs) : null,
|
|
|
|
|
is_deleted: data.isDeleted || false,
|
2025-11-19 14:18:16 -05:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Contract entity
|
|
|
|
|
*/
|
|
|
|
|
function mapContract(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
company_id: data.companyID,
|
|
|
|
|
contract_name: data.contractName,
|
|
|
|
|
contract_number: data.contractNumber,
|
|
|
|
|
description: data.description,
|
|
|
|
|
start_date: data.start_date,
|
|
|
|
|
end_date: data.end_date,
|
|
|
|
|
time_reporting_requires_start_and_stop_times: data.time_reporting_requires_start_and_stop_times,
|
|
|
|
|
service_level_agreement_id: data.service_level_agreement_id,
|
|
|
|
|
contract_type: data.contract_type,
|
|
|
|
|
contract_category: data.contract_category,
|
|
|
|
|
status: data.status,
|
|
|
|
|
business_division_subdivision_id: data.business_division_subdivision_id,
|
|
|
|
|
contact_id: data.contact_id,
|
|
|
|
|
contact_name: data.contact_name,
|
|
|
|
|
billing_preference: data.billing_preference,
|
|
|
|
|
purchase_order_number: data.purchase_order_number,
|
|
|
|
|
setup_fee: data.setup_fee,
|
|
|
|
|
setup_fee_allocation_code_id: data.setup_fee_allocation_code_id,
|
|
|
|
|
estimated_cost: data.estimated_cost,
|
|
|
|
|
estimated_hours: data.estimated_hours,
|
|
|
|
|
estimated_revenue: data.estimated_revenue,
|
|
|
|
|
over_budget_dollar_amount: data.over_budget_dollar_amount,
|
|
|
|
|
over_budget_hours: data.over_budget_hours,
|
|
|
|
|
contract_period_type: data.contract_period_type,
|
|
|
|
|
opportunity_id: data.opportunity_id,
|
|
|
|
|
renewed_contract_id: data.renewed_contract_id,
|
|
|
|
|
is_default_contract: data.is_default_contract || false,
|
|
|
|
|
internal_currency_setup_fee: data.internal_currency_setup_fee,
|
|
|
|
|
internal_currency_over_budget_dollar_amount: data.internal_currency_over_budget_dollar_amount,
|
|
|
|
|
internal_currency_estimated_cost: data.internal_currency_estimated_cost,
|
|
|
|
|
internal_currency_estimated_revenue: data.internal_currency_estimated_revenue,
|
|
|
|
|
exclusion_contract_id: data.exclusion_contract_id,
|
|
|
|
|
internal_currency_monthly_revenue: data.internal_currency_monthly_revenue,
|
|
|
|
|
internal_currency_quarterly_revenue: data.internal_currency_quarterly_revenue,
|
|
|
|
|
internal_currency_semi_annual_revenue: data.internal_currency_semi_annual_revenue,
|
|
|
|
|
internal_currency_yearly_revenue: data.internal_currency_yearly_revenue,
|
|
|
|
|
internal_currency_one_time_revenue: data.internal_currency_one_time_revenue,
|
|
|
|
|
compliance: data.compliance,
|
|
|
|
|
synced_at: data.synced_at,
|
|
|
|
|
is_deleted: data.is_deleted || false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 09:34:51 -04:00
|
|
|
/**
|
|
|
|
|
* Map Autotask Service (catalog item) entity
|
|
|
|
|
*/
|
|
|
|
|
function mapAutotaskService(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
name: data.name,
|
|
|
|
|
description: data.description,
|
|
|
|
|
unit_price: data.unitPrice,
|
|
|
|
|
unit_cost: data.unitCost,
|
|
|
|
|
period_type: data.periodType,
|
|
|
|
|
is_active: data.isActive !== undefined ? data.isActive : true,
|
|
|
|
|
synced_at: new Date(),
|
|
|
|
|
is_deleted: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Contract Service entity
|
|
|
|
|
*/
|
|
|
|
|
function mapContractService(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
contract_id: data.contractID,
|
|
|
|
|
service_id: data.serviceID,
|
|
|
|
|
unit_price: data.unitPrice,
|
|
|
|
|
unit_cost: data.unitCost,
|
|
|
|
|
adjusted_price: data.internalCurrencyAdjustedPrice,
|
|
|
|
|
invoice_description: data.invoiceDescription,
|
|
|
|
|
internal_currency_price: data.internalCurrencyUnitPrice,
|
|
|
|
|
synced_at: new Date(),
|
|
|
|
|
is_deleted: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
/**
|
|
|
|
|
* Map Billing Item entity
|
|
|
|
|
*/
|
|
|
|
|
function mapBillingItem(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
company_id: data.companyID,
|
|
|
|
|
product_id: data.productID,
|
|
|
|
|
description: data.description,
|
|
|
|
|
quantity: data.quantity,
|
|
|
|
|
rate: data.rate,
|
|
|
|
|
total_amount: data.total_amount,
|
|
|
|
|
line_discount_dollars: data.line_discount_dollars,
|
|
|
|
|
line_discount_percent: data.line_discount_percent,
|
|
|
|
|
tax_category_id: data.tax_category_id,
|
|
|
|
|
internal_currency_line_discount_dollars: data.internal_currency_line_discount_dollars,
|
|
|
|
|
allocation_code_id: data.allocation_code_id,
|
|
|
|
|
invoice_id: data.invoice_id,
|
|
|
|
|
vendor_id: data.vendor_id,
|
|
|
|
|
expense_item: data.expense_item || false,
|
|
|
|
|
task_id: data.task_id,
|
|
|
|
|
ticket_id: data.ticket_id,
|
|
|
|
|
project_id: data.project_id,
|
|
|
|
|
our_cost: data.our_cost,
|
|
|
|
|
list_price: data.list_price,
|
|
|
|
|
unit_cost: data.unit_cost,
|
|
|
|
|
unit_price: data.unit_price,
|
|
|
|
|
extended_price: data.extended_price,
|
|
|
|
|
tax_dollars: data.tax_dollars,
|
|
|
|
|
internal_currency_unit_price: data.internal_currency_unit_price,
|
|
|
|
|
internal_currency_total_amount: data.internal_currency_total_amount,
|
|
|
|
|
synced_at: data.synced_at,
|
|
|
|
|
is_deleted: data.is_deleted || false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Time Entry entity
|
|
|
|
|
*/
|
|
|
|
|
function mapTimeEntry(data: any): Record<string, any> {
|
2026-03-11 09:34:51 -04:00
|
|
|
// Autotask uses isNonBillable; derive billable from it
|
|
|
|
|
const isNonBillable = data.isNonBillable;
|
|
|
|
|
const billable = isNonBillable != null ? !isNonBillable : null;
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
resource_id: data.resourceID,
|
|
|
|
|
ticket_id: data.ticketID,
|
|
|
|
|
task_id: data.taskID,
|
2026-03-11 09:34:51 -04:00
|
|
|
contract_id: data.contractID,
|
|
|
|
|
contract_service_id: data.contractServiceID,
|
|
|
|
|
contract_service_bundle_id: data.contractServiceBundleID,
|
|
|
|
|
entry_date: data.dateWorked,
|
|
|
|
|
hours_worked: data.hoursWorked,
|
|
|
|
|
hours_to_bill: data.hoursToBill,
|
|
|
|
|
notes: data.summaryNotes,
|
2025-11-19 14:18:16 -05:00
|
|
|
internal_notes: data.internalNotes,
|
2026-03-11 09:34:51 -04:00
|
|
|
type: data.timeEntryType,
|
2025-11-19 14:18:16 -05:00
|
|
|
start_date_time: data.startDateTime,
|
|
|
|
|
end_date_time: data.endDateTime,
|
2026-03-11 09:34:51 -04:00
|
|
|
billable,
|
|
|
|
|
non_billable: isNonBillable,
|
|
|
|
|
allocation_code_id: data.billingCodeID,
|
|
|
|
|
approved_by_resource_id: data.billingApprovalResourceID,
|
|
|
|
|
approved_date_time: data.billingApprovalDateTime,
|
2025-11-19 14:18:16 -05:00
|
|
|
role_id: data.roleID,
|
|
|
|
|
synced_at: new Date(),
|
|
|
|
|
is_deleted: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
feat: add Autotask tags sync (tag groups, tags, ticket tag associations)
- Migration 057: autotask_tag_groups, autotask_tags, and junction tables
(ticket_tags, company_tags, configuration_item_tags, contact_tags)
- Add TAG_GROUPS and TAGS to EntityType enum and dependency map
- Add mapTagGroup() and mapTag() entity mapper functions
- Add syncTagGroups(), syncTags(), syncTicketTagAssociations() methods
- Wire TicketTagAssociations bulk sync into full/incremental sync flow
- Add 'exist' operator to QueryFilter type
- No FK on ticket_id (tagged tickets may be outside 2yr sync window)
Synced: 26 tag groups, 7299 tags, 10141 ticket-tag associations
2026-03-20 09:22:40 -04:00
|
|
|
/**
|
|
|
|
|
* Map TagGroup entity
|
|
|
|
|
*/
|
|
|
|
|
function mapTagGroup(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
label: data.label,
|
|
|
|
|
display_color: data.displayColor,
|
|
|
|
|
is_active: data.isActive !== undefined ? data.isActive : true,
|
|
|
|
|
is_system: data.isSystem || false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Tag entity
|
|
|
|
|
*/
|
|
|
|
|
function mapTag(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
|
|
|
|
label: data.label,
|
|
|
|
|
tag_group_id: data.tagGroupID,
|
|
|
|
|
is_active: data.isActive !== undefined ? data.isActive : true,
|
|
|
|
|
is_system: data.isSystem || false,
|
|
|
|
|
is_excluded_from_automatic_tagging: data.isExcludedFromAutomaticTagging || false,
|
|
|
|
|
create_date_time: data.createDateTime,
|
|
|
|
|
last_modified_date_time: data.lastModifiedDateTime,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:18:16 -05:00
|
|
|
/**
|
|
|
|
|
* Map Picklist entity (statuses, issue types, etc.)
|
|
|
|
|
*/
|
|
|
|
|
function mapPicklist(data: any): Record<string, any> {
|
|
|
|
|
return {
|
|
|
|
|
value: data.value,
|
|
|
|
|
label: data.label || data.name,
|
|
|
|
|
is_active: data.isActive !== undefined ? data.isActive : true,
|
|
|
|
|
is_system: data.isSystem || false,
|
|
|
|
|
sort_order: data.sortOrder,
|
|
|
|
|
parent_value: data.parentValue,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Batch map multiple entities
|
|
|
|
|
*/
|
|
|
|
|
export function mapAutotaskBatch(
|
|
|
|
|
entity: EntityType,
|
|
|
|
|
items: any[]
|
|
|
|
|
): Record<string, any>[] {
|
|
|
|
|
return items.map(item => mapAutotaskToDatabase(entity, item));
|
|
|
|
|
}
|