refactor(sync): improve attribute mapping to support dynamic fields and custom transformations

This commit is contained in:
2026-03-12 21:38:06 -04:00
parent be1a69217f
commit eb6954ddf1
5 changed files with 44 additions and 39 deletions

View File

@@ -17,16 +17,13 @@ class CustomerSyncService < SyncServiceBase
Customer
end
# Determine if the remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
def destroy_remote?(remote)
# Determine if the local entity should be deleted (e.g. if it's marked inactive in QBO)
def destroy_local?(remote)
!remote.active?
end
# Map relevant attributes from the QBO Customer to the local Customer model
def process_attributes(local, remote)
local.name = remote.display_name
local.phone_number = remote.primary_phone&.free_form_number&.gsub(/\D/, '')
local.mobile_phone_number = remote.mobile_phone&.free_form_number&.gsub(/\D/, '')
end
map_attribute :name, ->(remote) { remote.display_name }
map_attribute :phone_number, ->(remote) { remote.primary_phone&.free_form_number&.gsub(/\D/, '') }
map_attribute :mobile_phone_number, ->(remote) { remote.mobile_phone&.free_form_number&.gsub(/\D/, '') }
end

View File

@@ -17,14 +17,11 @@ class EmployeeSyncService < SyncServiceBase
Employee
end
# Determine if the remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
def destroy_remote?(remote)
# Determine if the local entity should be deleted (e.g. if it's marked inactive in QBO)
def destroy_local?(remote)
!remote.active?
end
# Map relevant attributes from the QBO Employee to the local Employee model
def process_attributes(local, remote)
local.name = remote.display_name
end
map_attribute :name, ->(remote) { remote.display_name }
end

View File

@@ -17,11 +17,6 @@ class EstimateSyncService < SyncServiceBase
Estimate
end
# Map relevant attributes from the QBO Estimate to the local Estimate model
def process_attributes(local, remote)
local.doc_number = remote.doc_number
local.txn_date = remote.txn_date
local.customer = Customer.find_by(id: remote.customer_ref&.value)
end
map_attribute :customer, ->(remote) { Customer.find_by(id: remote.customer_ref&.value) }
end

View File

@@ -17,19 +17,12 @@ class InvoiceSyncService < SyncServiceBase
Invoice
end
# Map relevant attributes from the QBO Invoice to the local Invoice model
def process_attributes(local, remote)
local.doc_number = remote.doc_number
local.txn_date = remote.txn_date
local.due_date = remote.due_date
local.total_amount = remote.total
local.balance = remote.balance
local.qbo_updated_at = remote.meta_data&.last_updated_time
local.customer = Customer.find_by(id: remote.customer_ref&.value)
end
# Attach QBO Invoices to the local Issues
def attach_documents(local, remote)
InvoiceAttachmentService.new(local, remote).attach
end
map_attribute :customer, ->(remote) { Customer.find_by(id: remote.customer_ref&.value) }
map_attribute :total_amount, ->(remote) { remote.total }
map_attribute :qbo_updated_at, ->(remote) { remote.meta_data&.last_updated_time }
end

View File

@@ -57,6 +57,10 @@ class SyncServiceBase
private
def attach_documents(local, remote)
# Override in subclasses if the entity has attachments (e.g. Invoice)
end
# Builds a QBO query for retrieving entities
def build_query(full_sync)
if full_sync
@@ -72,13 +76,28 @@ class SyncServiceBase
end
end
def attach_documents(local, remote)
# Override in subclasses if the entity has attachments (e.g. Invoice)
# Determine if a remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
def destroy_local?(remote)
false
end
# Determine if a remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
def destroy_remote?(remote)
false
def extract_value(remote, remote_attr)
case remote_attr
when Proc
remote_attr.call(remote)
else
remote.public_send(remote_attr)
end
end
class << self
def map_attribute(local, remote = nil, &block)
attribute_map[local] = block || remote
end
def attribute_map
@attribute_map ||= {}
end
end
# Log messages with the entity type for better traceability
@@ -90,7 +109,7 @@ class SyncServiceBase
def persist(remote)
local = @entity.find_or_initialize_by(id: remote.id)
if destroy_remote?(remote)
if destroy_local?(remote)
if local.persisted?
local.destroy
log "Deleted #{@entity.name} #{remote.id}"
@@ -111,9 +130,13 @@ class SyncServiceBase
log "Failed to sync #{@entity.name} #{remote.id}: #{e.message}"
end
# This method should be implemented in subclasses to map remote attributes to local model
# Maps remote attributes to local model
def process_attributes(local, remote)
raise NotImplementedError, "Subclasses must implement process_attributes"
log "Processing #{@entity} ##{remote.id}"
self.class.attribute_map.each do |local_attr, remote_attr|
value = extract_value(remote, remote_attr)
local.public_send("#{local_attr}=", value)
end
end
# Dynamically get the Quickbooks Service Class