/** * 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 { // Entity-specific mappings use ORIGINAL data (not snake_cased) let mapped: Record; 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.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.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; 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 { return { id: data.id, company_name: data.companyName || data.name, company_number: data.companyNumber, phone: data.phone, fax: data.fax, website: data.webSiteURL, address1: data.address1, address2: data.address2, city: data.city, state: data.state, postal_code: data.postalCode, country: data.country, is_active: data.isActive !== undefined ? data.isActive : true, company_type: data.companyType, owner_resource_id: data.ownerResourceID, territory_id: data.territoryID, market_segment_id: data.marketSegmentID, competitor_id: data.competitorID, billing_address1: data.billingAddress1, billing_address2: data.billingAddress2, billing_city: data.billingCity, billing_state: data.billingState, billing_postal_code: data.billingPostalCode, billing_country: data.billingCountry, tax_id: data.taxID, tax_exempt: data.taxExempt || false, tax_region_id: data.taxRegionID, currency_id: data.currencyID, invoice_method: data.invoice_method, invoice_template_id: data.invoice_template_id, quote_template_id: data.quote_template_id, key_account_icon: data.key_account_icon, last_activity_date: data.last_activity_date, last_tracked_modification_date_time: data.last_tracked_modification_date_time || data.last_modified_date, api_vendor_id: data.api_vendor_id, synced_at: data.synced_at, is_deleted: data.is_deleted || false, }; } /** * Map Ticket entity */ function mapTicket(data: any): Record { 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 { 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 { 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 { 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 Resource entity */ function mapResource(data: any): Record { // 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 { // Configuration items have many fields, map all of them const mapped: Record = { 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, contact_id: data.contactID, configuration_item_type: data.type, synced_at: data.synced_at, is_deleted: data.is_deleted || false, }; // Extract backup-type UDF (ID 29693319) from userDefinedFields if (data.userDefinedFields && Array.isArray(data.userDefinedFields)) { const backupUdf = data.userDefinedFields.find( (udf: any) => udf.name === 'Backup Type' || String(udf.name) === '29693319' ); if (backupUdf && backupUdf.value) { mapped.backup_type_udf = backupUdf.value; } } // Add all other fields dynamically const fieldsToInclude = [ 'daily_cost', 'hourly_cost', 'monthly_cost', 'per_use_cost', 'setup_fee', 'location_id', 'vendor_id', 'installed_by_id', 'installed_by_contact_id', 'parent_configuration_item_id', 'notes', 'create_date', 'created_by_person_id', 'last_modified_time', 'last_activity_person_type', 'impersonator_creator_resource_id', 'configuration_item_category_id', 'configuration_item_type', 'device_type', 'rmm_device_uid', 'api_vendor_id', ]; // Add Datto RMM fields const dattoFields = Object.keys(data).filter(key => key.startsWith('datto_') || key.startsWith('rmm_')); fieldsToInclude.push(...dattoFields); fieldsToInclude.forEach(field => { if (data[field] !== undefined) { mapped[field] = data[field]; } }); return mapped; } /** * Map Contact entity */ function mapContact(data: any): Record { 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 { 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 Billing Item entity */ function mapBillingItem(data: any): Record { 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 { return { id: data.id, resource_id: data.resourceID, ticket_id: data.ticketID, task_id: data.taskID, project_id: data.projectID, company_id: data.companyID, entry_date: data.dateWorked, // Autotask API returns dateWorked hours_worked: data.hoursWorked, // Autotask API returns hoursWorked notes: data.summaryNotes, // Autotask uses summaryNotes internal_notes: data.internalNotes, title: data.title, type: data.type, start_date_time: data.startDateTime, end_date_time: data.endDateTime, billable: data.billable, // Autotask returns billable billing_rate: data.billingRate, billing_rate_currency_id: data.billingRateCurrencyID, cost_rate: data.costRate, cost_rate_currency_id: data.costRateCurrencyID, cost: data.cost, cost_currency_id: data.costCurrencyID, revenue: data.revenue, revenue_currency_id: data.revenueCurrencyID, margin: data.margin, margin_currency_id: data.marginCurrencyID, approved: data.approved, approved_by_resource_id: data.approvedByResourceID, approved_date_time: data.approvedDateTime, non_billable: data.nonBillable, contract_service_id: data.contractServiceID, contract_service_bundle_id: data.contractServiceBundleID, role_id: data.roleID, department_id: data.departmentID, location_id: data.locationID, allocation_code_id: data.allocationCodeID, imp_project_schedule_id: data.impProjectScheduleID, imp_project_schedule_task_id: data.impProjectScheduleTaskID, api_vendor_id: data.apiVendorID, synced_at: new Date(), is_deleted: false, }; } /** * Map Picklist entity (statuses, issue types, etc.) */ function mapPicklist(data: any): Record { 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[] { return items.map(item => mapAutotaskToDatabase(entity, item)); }