wulf-pulse/lib/utils/entity-mapper.ts
lorentz dd4cf68def 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

811 lines
29 KiB
TypeScript

/**
* 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;
case EntityType.PROJECT_PHASES:
mapped = mapProjectPhase(data);
break;
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;
case EntityType.CONTRACT_SERVICES:
mapped = mapContractService(data);
break;
case EntityType.AUTOTASK_SERVICES:
mapped = mapAutotaskService(data);
break;
case EntityType.BILLING_ITEMS:
mapped = mapBillingItem(data);
break;
case EntityType.TIME_ENTRIES:
mapped = mapTimeEntry(data);
break;
case EntityType.TICKET_NOTES:
mapped = mapTicketNote(data);
break;
case EntityType.STATUSES:
case EntityType.ISSUE_TYPES:
case EntityType.SUB_ISSUE_TYPES:
case EntityType.WORK_TYPES:
mapped = mapPicklist(data);
break;
case EntityType.TAG_GROUPS:
mapped = mapTagGroup(data);
break;
case EntityType.TAGS:
mapped = mapTag(data);
break;
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> {
// 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;
}
}
return {
id: data.id,
company_name: data.companyName || data.name,
company_number: data.companyNumber,
phone: data.phone,
fax: data.fax,
web_address: data.webAddress,
address1: data.address1,
address2: data.address2,
city: data.city,
state: data.state,
postal_code: data.postalCode,
country_id: data.countryID,
is_active: data.isActive !== undefined ? data.isActive : true,
company_type: data.companyType,
company_category_id: data.companyCategoryID,
owner_resource_id: data.ownerResourceID,
territory_id: data.territoryID,
market_segment_id: data.marketSegmentID,
competitor_id: data.competitorID,
parent_company_id: data.parentCompanyID,
billing_address1: data.billingAddress1,
billing_address2: data.billingAddress2,
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,
tax_id: data.taxID,
tax_exempt: data.isTaxExempt || false,
tax_region_id: data.taxRegionID,
currency_id: data.currencyID,
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,
user_defined_fields: Object.keys(udfs).length > 0 ? JSON.stringify(udfs) : null,
is_deleted: data.isDeleted || false,
};
}
/**
* 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,
creator_resource_id: data.creatorResourceID,
creator_type: data.creatorType,
billing_code_id: data.billingCodeID,
configuration_item_id: data.configurationItemID,
last_activity_date: data.lastActivityDate,
last_customer_notification_date_time: data.lastCustomerNotificationDateTime,
last_customer_visible_activity_date_time: data.lastCustomerVisibleActivityDateTime,
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,
resolution: data.resolution,
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,
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,
synced_at: data.synced_at,
is_deleted: data.is_deleted || false,
};
}
/**
* 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,
};
}
/**
* 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,
};
}
/**
* Map ProjectPhase entity
*/
function mapProjectPhase(data: any): Record<string, any> {
return {
id: data.id,
project_id: data.projectID,
title: data.title,
description: data.description,
phase_number: data.phaseNumber,
estimated_hours: data.estimatedHours,
start_date_time: data.startDate,
due_date: data.dueDate,
create_date_time: data.createDate,
last_activity_date_time: data.lastActivityDateTime,
parent_phase_id: data.parentPhaseID,
is_scheduled: data.isScheduled,
creator_resource_id: data.creatorResourceID,
external_id: data.externalID,
synced_at: data.synced_at,
is_deleted: data.is_deleted || false,
};
}
/**
* Map Resource entity
*/
function mapResource(data: any): Record<string, any> {
// 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;
};
return {
id: data.id,
// 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,
title: data.title,
is_active: data.isActive !== undefined ? data.isActive : true,
// Employment details
resource_type: safeInt(data.resourceType),
payroll_identifier: data.payrollIdentifier,
pay_roll_identifier: data.payrollIdentifier, // Keep for backward compatibility
payroll_type: safeInt(data.payrollType),
accounting_reference_id: data.accountingReferenceID,
internal_cost: data.internalCost,
hire_date: data.hireDate,
// Location and availability
location_id: safeInt(data.locationID),
travel_availability_pct: data.travelAvailabilityPct,
default_service_desk_role_id: safeInt(data.defaultServiceDeskRoleID),
// System preferences
email_type_code: safeInt(data.emailTypeCode),
number_format: data.numberFormat,
time_format: data.timeFormat,
date_format: data.dateFormat,
// Demographics and security
gender: safeInt(data.gender),
license_type: safeInt(data.licenseType),
security_level: safeInt(data.securityLevel),
// Ratings
survey_resource_rating: data.surveyResourceRating,
// Audit fields
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,
daily_cost: data.dailyCost,
hourly_cost: data.hourlyCost,
monthly_cost: data.monthlyCost,
per_use_cost: data.perUseCost,
setup_fee: data.setupFee,
contact_id: data.contactID,
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,
configuration_item_type: data.type,
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,
};
// Store all UDFs as JSONB
if (data.userDefinedFields && Array.isArray(data.userDefinedFields)) {
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
const backupUdf = data.userDefinedFields.find(
(udf: any) => udf.name === 'Backup Type' || String(udf.name) === '29693319'
);
if (backupUdf?.value) {
mapped.backup_type_udf = backupUdf.value;
}
}
return mapped;
}
/**
* Map Contact entity
*/
function mapContact(data: any): Record<string, any> {
return {
id: data.id,
company_id: data.companyID,
first_name: data.firstName,
last_name: data.lastName,
title: data.title,
email_address: data.emailAddress || data.email_address || data.email,
email_address2: data.emailAddress2 || data.email_address2,
email_address3: data.emailAddress3 || data.email_address3,
phone: data.phone,
extension: data.extension,
alternate_phone: data.alternate_phone,
mobile_phone: data.mobile_phone,
fax: data.faxNumber || data.fax,
address_line: data.addressLine || data.address_line,
address_line1: data.addressLine1 || data.address_line1 || data.address_line_1,
city: data.city,
state: data.state,
zip_code: data.zipCode || data.zip_code || data.postal_code,
country: data.country,
is_active: data.is_active !== undefined ? data.is_active : true,
name_prefix: data.name_prefix,
name_suffix: data.name_suffix,
facebook_url: data.facebook_url,
twitter_url: data.twitter_url,
linked_in_url: data.linked_in_url,
primary_contact: data.primary_contact || false,
account_physical_location_id: data.account_physical_location_id,
solicitation_opt_out: data.solicitation_opt_out || false,
room_number: data.room_number,
last_activity_date: data.last_activity_date,
last_modified_date: data.last_modified_date,
api_vendor_id: data.api_vendor_id,
synced_at: data.synced_at,
is_deleted: data.is_deleted || false,
};
}
/**
* 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,
};
}
/**
* 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,
};
}
/**
* 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> {
// Autotask uses isNonBillable; derive billable from it
const isNonBillable = data.isNonBillable;
const billable = isNonBillable != null ? !isNonBillable : null;
return {
id: data.id,
resource_id: data.resourceID,
ticket_id: data.ticketID,
task_id: data.taskID,
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,
internal_notes: data.internalNotes,
type: data.timeEntryType,
start_date_time: data.startDateTime,
end_date_time: data.endDateTime,
billable,
non_billable: isNonBillable,
allocation_code_id: data.billingCodeID,
approved_by_resource_id: data.billingApprovalResourceID,
approved_date_time: data.billingApprovalDateTime,
role_id: data.roleID,
synced_at: new Date(),
is_deleted: false,
};
}
/**
* 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,
};
}
/**
* 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));
}