mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-04-02 08:21:57 -04:00
Added Attribute Mapping DSL
This commit is contained in:
@@ -22,8 +22,8 @@ class CustomerSyncService < SyncServiceBase
|
|||||||
!remote.active?
|
!remote.active?
|
||||||
end
|
end
|
||||||
|
|
||||||
map_attribute :name, ->(remote) { remote.display_name }
|
map_attribute :name, :display_name
|
||||||
map_attribute :phone_number, ->(remote) { remote.primary_phone&.free_form_number&.gsub(/\D/, '') }
|
map_phone :phone_number, :primary_phone
|
||||||
map_attribute :mobile_phone_number, ->(remote) { remote.mobile_phone&.free_form_number&.gsub(/\D/, '') }
|
map_phone :mobile_phone_number, :mobile_phone
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -17,6 +17,7 @@ class EstimateSyncService < SyncServiceBase
|
|||||||
Estimate
|
Estimate
|
||||||
end
|
end
|
||||||
|
|
||||||
map_attribute :customer, ->(remote) { Customer.find_by(id: remote.customer_ref&.value) }
|
map_attributes :doc_number, :txn_date
|
||||||
|
map_belongs_to :customer
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -22,7 +22,9 @@ class InvoiceSyncService < SyncServiceBase
|
|||||||
InvoiceAttachmentService.new(local, remote).attach
|
InvoiceAttachmentService.new(local, remote).attach
|
||||||
end
|
end
|
||||||
|
|
||||||
map_attribute :customer, ->(remote) { Customer.find_by(id: remote.customer_ref&.value) }
|
map_attributes :balance, :doc_number, :due_date, :txn_date
|
||||||
map_attribute :total_amount, ->(remote) { remote.total }
|
map_attribute :total_amount, :total
|
||||||
map_attribute :qbo_updated_at, ->(remote) { remote.meta_data&.last_updated_time }
|
map_attribute :qbo_updated_at, "meta_data.last_updated_time"
|
||||||
|
map_belongs_to :customer
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -90,14 +90,111 @@ class SyncServiceBase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Attribute Mapping DSL
|
||||||
|
#
|
||||||
|
# This DSL defines how attributes from a QuickBooks Online (QBO) entity
|
||||||
|
# are mapped onto a local ActiveRecord model during synchronization.
|
||||||
|
#
|
||||||
|
# Each mapping registers a lambda in `attribute_map`. When a remote QBO
|
||||||
|
# object is processed, the lambda is executed to extract and transform
|
||||||
|
# the value that will be assigned to the local model attribute.
|
||||||
|
#
|
||||||
|
# The DSL supports several mapping patterns:
|
||||||
|
#
|
||||||
|
# 1. Direct attribute mapping (same name)
|
||||||
|
#
|
||||||
|
# map_attribute :doc_number
|
||||||
|
#
|
||||||
|
# Equivalent to:
|
||||||
|
#
|
||||||
|
# local.doc_number = remote.doc_number
|
||||||
|
#
|
||||||
|
# 2. Renamed attribute mapping
|
||||||
|
#
|
||||||
|
# map_attribute :total_amount, :total
|
||||||
|
#
|
||||||
|
# Equivalent to:
|
||||||
|
#
|
||||||
|
# local.total_amount = remote.total
|
||||||
|
#
|
||||||
|
# 3. Custom transformation logic
|
||||||
|
#
|
||||||
|
# map_attribute :qbo_updated_at do |remote|
|
||||||
|
# remote.meta_data&.last_updated_time
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# Useful for nested fields or computed values.
|
||||||
|
#
|
||||||
|
# 4. Bulk attribute mapping
|
||||||
|
#
|
||||||
|
# map_attributes :doc_number, :txn_date, :due_date
|
||||||
|
#
|
||||||
|
# Convenience helper that maps multiple attributes with identical names.
|
||||||
|
#
|
||||||
|
# 5. Foreign key / reference mapping
|
||||||
|
#
|
||||||
|
# map_belongs_to :customer
|
||||||
|
#
|
||||||
|
# Resolves a QBO reference object (e.g. `customer_ref.value`) and finds
|
||||||
|
# the associated local ActiveRecord model.
|
||||||
|
#
|
||||||
|
# 6. Specialized helpers
|
||||||
|
#
|
||||||
|
# map_phone :phone_number, :primary_phone
|
||||||
|
#
|
||||||
|
# Extracts and normalizes phone numbers by stripping non-digit characters.
|
||||||
|
#
|
||||||
|
# Internally, the mappings are stored in `attribute_map` and executed by the
|
||||||
|
# SyncService during `process_attributes`, which iterates through each mapping
|
||||||
|
# and assigns the computed value to the local record.
|
||||||
|
#
|
||||||
|
# This design keeps synchronization services declarative, readable, and easy
|
||||||
|
# to extend while centralizing transformation logic in a single DSL.
|
||||||
class << self
|
class << self
|
||||||
def map_attribute(local, remote = nil, &block)
|
|
||||||
attribute_map[local] = block || remote
|
def map_attributes(*attrs)
|
||||||
|
attrs.each do |attr|
|
||||||
|
map_attribute(attr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def map_attribute(local_attr, remote_attr = nil, &block)
|
||||||
|
attribute_map[local_attr] =
|
||||||
|
if block_given?
|
||||||
|
block
|
||||||
|
elsif remote_attr
|
||||||
|
->(remote) do
|
||||||
|
remote_attr.to_s.split('.').reduce(remote) do |obj, method|
|
||||||
|
obj&.public_send(method)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
->(remote) { remote.public_send(local_attr) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def attribute_map
|
def attribute_map
|
||||||
@attribute_map ||= {}
|
@attribute_map ||= {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def map_belongs_to(local_attr, ref: nil, model: nil)
|
||||||
|
ref ||= "#{local_attr}_ref"
|
||||||
|
model ||= local_attr.to_s.classify.constantize
|
||||||
|
|
||||||
|
attribute_map[local_attr] = lambda do |remote|
|
||||||
|
ref_obj = remote.public_send(ref)
|
||||||
|
id = ref_obj&.value
|
||||||
|
id ? model.find_by(id: id) : nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def map_phone(local_attr, remote_attr)
|
||||||
|
attribute_map[local_attr] = lambda do |remote|
|
||||||
|
phone = remote.public_send(remote_attr)
|
||||||
|
phone&.free_form_number&.gsub(/\D/, '')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Log messages with the entity type for better traceability
|
# Log messages with the entity type for better traceability
|
||||||
@@ -119,10 +216,11 @@ class SyncServiceBase
|
|||||||
|
|
||||||
process_attributes(local, remote)
|
process_attributes(local, remote)
|
||||||
|
|
||||||
if local.changed?
|
if local.new_record? || local.changed?
|
||||||
|
was_new = local.new_record?
|
||||||
local.skip_qbo_push = true
|
local.skip_qbo_push = true
|
||||||
local.save
|
local.save!
|
||||||
log "Updated #{@entity.name} #{remote.id}"
|
log "#{was_new ? 'Created' : 'Updated'} #{@entity.name} #{remote.id}"
|
||||||
attach_documents(local, remote)
|
attach_documents(local, remote)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -133,11 +231,15 @@ class SyncServiceBase
|
|||||||
# Maps remote attributes to local model
|
# Maps remote attributes to local model
|
||||||
def process_attributes(local, remote)
|
def process_attributes(local, remote)
|
||||||
log "Processing #{@entity} ##{remote.id}"
|
log "Processing #{@entity} ##{remote.id}"
|
||||||
self.class.attribute_map.each do |local_attr, remote_attr|
|
|
||||||
value = extract_value(remote, remote_attr)
|
self.class.attribute_map.each do |local_attr, mapper|
|
||||||
|
value = mapper.call(remote)
|
||||||
|
|
||||||
|
if local.public_send(local_attr) != value
|
||||||
local.public_send("#{local_attr}=", value)
|
local.public_send("#{local_attr}=", value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Dynamically get the Quickbooks Service Class
|
# Dynamically get the Quickbooks Service Class
|
||||||
def service_class
|
def service_class
|
||||||
|
|||||||
Reference in New Issue
Block a user