mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-04-02 08:21:57 -04:00
Compare commits
5 Commits
40f7a3335c
...
eb6954ddf1
| Author | SHA1 | Date | |
|---|---|---|---|
| eb6954ddf1 | |||
| be1a69217f | |||
| 99669f7baa | |||
| 29530e2c95 | |||
| beb4a66a93 |
@@ -55,7 +55,7 @@ class QboBaseModel < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Repsonds to missing methods by delegating to the QBO customer details object if the method is defined there.
|
# Repsonds to missing methods by delegating to the QBO entity calss if the method is defined there.
|
||||||
# This allows for dynamic access to any attributes or methods of the QBO customer without having to explicitly define them in the Subclass model, providing flexibility and reducing boilerplate code.
|
# This allows for dynamic access to any attributes or methods of the QBO customer without having to explicitly define them in the Subclass model, providing flexibility and reducing boilerplate code.
|
||||||
def respond_to_missing?(method_name, include_private = false)
|
def respond_to_missing?(method_name, include_private = false)
|
||||||
qbo_model_class.method_defined?(method_name) || super
|
qbo_model_class.method_defined?(method_name) || super
|
||||||
@@ -73,12 +73,15 @@ class QboBaseModel < ActiveRecord::Base
|
|||||||
job.perform_later(id: id)
|
job.perform_later(id: id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Flag used to update local without pushing to QBO.
|
||||||
|
# This is used to prevent loops with the webhook
|
||||||
def skip_qbo_push?
|
def skip_qbo_push?
|
||||||
!!skip_qbo_push
|
!!skip_qbo_push
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Log messages with a standarized prefix
|
||||||
def log(msg)
|
def log(msg)
|
||||||
Rails.logger.info "[#{model_name.name}] #{msg}"
|
Rails.logger.info "[#{model_name.name}] #{msg}"
|
||||||
end
|
end
|
||||||
@@ -90,6 +93,7 @@ class QboBaseModel < ActiveRecord::Base
|
|||||||
service_class.new(qbo: qbo, local: self).pull()
|
service_class.new(qbo: qbo, local: self).pull()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Pushs the entity's details from QuickBooks Online.
|
||||||
def push_to_qbo
|
def push_to_qbo
|
||||||
log "Starting push for #{model_name.name} ##{id}..."
|
log "Starting push for #{model_name.name} ##{id}..."
|
||||||
qbo = QboConnectionService.current!
|
qbo = QboConnectionService.current!
|
||||||
@@ -98,10 +102,12 @@ class QboBaseModel < ActiveRecord::Base
|
|||||||
return reslut
|
return reslut
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Dynamically get the Quickbooks Model Class
|
||||||
def qbo_model_class
|
def qbo_model_class
|
||||||
@qbo_model_class ||= "Quickbooks::Model::#{model_name.name}".constantize
|
@qbo_model_class ||= "Quickbooks::Model::#{model_name.name}".constantize
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Dynamically get the Service Class
|
||||||
def service_class
|
def service_class
|
||||||
@service_class ||= "#{model_name.name}Service".constantize
|
@service_class ||= "#{model_name.name}Service".constantize
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,16 +17,13 @@ class CustomerSyncService < SyncServiceBase
|
|||||||
Customer
|
Customer
|
||||||
end
|
end
|
||||||
|
|
||||||
# Determine if the remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
|
# Determine if the local entity should be deleted (e.g. if it's marked inactive in QBO)
|
||||||
def destroy_remote?(remote)
|
def destroy_local?(remote)
|
||||||
!remote.active?
|
!remote.active?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Map relevant attributes from the QBO Customer to the local Customer model
|
map_attribute :name, ->(remote) { remote.display_name }
|
||||||
def process_attributes(local, remote)
|
map_attribute :phone_number, ->(remote) { remote.primary_phone&.free_form_number&.gsub(/\D/, '') }
|
||||||
local.name = remote.display_name
|
map_attribute :mobile_phone_number, ->(remote) { remote.mobile_phone&.free_form_number&.gsub(/\D/, '') }
|
||||||
local.phone_number = remote.primary_phone&.free_form_number&.gsub(/\D/, '')
|
|
||||||
local.mobile_phone_number = remote.mobile_phone&.free_form_number&.gsub(/\D/, '')
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -17,14 +17,11 @@ class EmployeeSyncService < SyncServiceBase
|
|||||||
Employee
|
Employee
|
||||||
end
|
end
|
||||||
|
|
||||||
# Determine if the remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
|
# Determine if the local entity should be deleted (e.g. if it's marked inactive in QBO)
|
||||||
def destroy_remote?(remote)
|
def destroy_local?(remote)
|
||||||
!remote.active?
|
!remote.active?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Map relevant attributes from the QBO Employee to the local Employee model
|
map_attribute :name, ->(remote) { remote.display_name }
|
||||||
def process_attributes(local, remote)
|
|
||||||
local.name = remote.display_name
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -17,11 +17,6 @@ class EstimateSyncService < SyncServiceBase
|
|||||||
Estimate
|
Estimate
|
||||||
end
|
end
|
||||||
|
|
||||||
# Map relevant attributes from the QBO Estimate to the local Estimate model
|
map_attribute :customer, ->(remote) { Customer.find_by(id: remote.customer_ref&.value) }
|
||||||
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
|
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -16,20 +16,13 @@ class InvoiceSyncService < SyncServiceBase
|
|||||||
def self.model_class
|
def self.model_class
|
||||||
Invoice
|
Invoice
|
||||||
end
|
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
|
# Attach QBO Invoices to the local Issues
|
||||||
def attach_documents(local, remote)
|
def attach_documents(local, remote)
|
||||||
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_attribute :total_amount, ->(remote) { remote.total }
|
||||||
|
map_attribute :qbo_updated_at, ->(remote) { remote.meta_data&.last_updated_time }
|
||||||
end
|
end
|
||||||
@@ -14,10 +14,10 @@ class ServiceBase
|
|||||||
# The QBO client is used to communicate with QuickBooks Online, while the local record contains the data that needs to be pushed to QBO.
|
# The QBO client is used to communicate with QuickBooks Online, while the local record contains the data that needs to be pushed to QBO.
|
||||||
# If no local is provided, the service will not perform any operations.
|
# If no local is provided, the service will not perform any operations.
|
||||||
def initialize(qbo:, local: nil)
|
def initialize(qbo:, local: nil)
|
||||||
|
@entity = local.class.name
|
||||||
raise "No QBO configuration found" unless qbo
|
raise "No QBO configuration found" unless qbo
|
||||||
raise "#{@entity} record is required for push operation" unless local
|
raise "#{@entity} record is required for push operation" unless local
|
||||||
@qbo = qbo
|
@qbo = qbo
|
||||||
@entity = local.class.name
|
|
||||||
@local = local
|
@local = local
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -31,13 +31,7 @@ class ServiceBase
|
|||||||
return build_qbo_remote unless @local.present?
|
return build_qbo_remote unless @local.present?
|
||||||
return build_qbo_remote unless @local.id
|
return build_qbo_remote unless @local.id
|
||||||
log "Fetching details for #{@entity} ##{@local.id} from QBO..."
|
log "Fetching details for #{@entity} ##{@local.id} from QBO..."
|
||||||
qbo = QboConnectionService.current!
|
with_qbo_service do |service|
|
||||||
qbo.perform_authenticated_request do |access_token|
|
|
||||||
service_class = "Quickbooks::Service::#{@entity}".constantize
|
|
||||||
service = service_class.new(
|
|
||||||
company_id: qbo.realm_id,
|
|
||||||
access_token: access_token
|
|
||||||
)
|
|
||||||
service.fetch_by_id(@local.id)
|
service.fetch_by_id(@local.id)
|
||||||
end
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
@@ -51,13 +45,7 @@ class ServiceBase
|
|||||||
# If the push is successful, it returns the remote record; otherwise, it logs the error and returns false.
|
# If the push is successful, it returns the remote record; otherwise, it logs the error and returns false.
|
||||||
def push
|
def push
|
||||||
log "Pushing #{@entity} ##{@local.id} to QBO..."
|
log "Pushing #{@entity} ##{@local.id} to QBO..."
|
||||||
|
remote = with_qbo_service do |service|
|
||||||
remote = @qbo.perform_authenticated_request do |access_token|
|
|
||||||
service_class = "Quickbooks::Service::#{@entity}".constantize
|
|
||||||
service = service_class.new(
|
|
||||||
company_id: @qbo.realm_id,
|
|
||||||
access_token: access_token
|
|
||||||
)
|
|
||||||
if @local.id.present?
|
if @local.id.present?
|
||||||
log "Updating #{@entity}"
|
log "Updating #{@entity}"
|
||||||
service.update(@local.details)
|
service.update(@local.details)
|
||||||
@@ -66,17 +54,29 @@ class ServiceBase
|
|||||||
service.create(@local.details)
|
service.create(@local.details)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@local.id = remote.id unless @local.persisted?
|
@local.id = remote.id unless @local.persisted?
|
||||||
log "Push for remote ##{@local.id} completed."
|
log "Push for remote ##{@local.id} completed."
|
||||||
return @local
|
@local
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Performs authenticaed requests with QBO service
|
||||||
|
def with_qbo_service
|
||||||
|
@qbo.perform_authenticated_request do |access_token|
|
||||||
|
service = service_class.new( company_id: @qbo.realm_id, access_token: access_token )
|
||||||
|
yield service
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Log messages with the entity type for better traceability
|
# Log messages with the entity type for better traceability
|
||||||
def log(msg)
|
def log(msg)
|
||||||
Rails.logger.info "[#{@entity}Service] #{msg}"
|
Rails.logger.info "[#{@entity}Service] #{msg}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Dynamically get the Quickbooks Service Class
|
||||||
|
def service_class
|
||||||
|
@service_class ||= "Quickbooks::Service::#{@entity}".constantize
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -32,17 +32,11 @@ class SyncServiceBase
|
|||||||
# Sync all entities, or only those updated since the last sync
|
# Sync all entities, or only those updated since the last sync
|
||||||
def sync(full_sync: false)
|
def sync(full_sync: false)
|
||||||
log "Starting #{full_sync ? 'full' : 'incremental'} #{@entity.name} sync with page size of: #{@page_size}"
|
log "Starting #{full_sync ? 'full' : 'incremental'} #{@entity.name} sync with page size of: #{@page_size}"
|
||||||
|
with_qbo_service do |service|
|
||||||
@qbo.perform_authenticated_request do |access_token|
|
query = build_query(full_sync)
|
||||||
service_class = "Quickbooks::Service::#{@entity.name}".constantize
|
|
||||||
service = service_class.new(company_id: @qbo.realm_id, access_token: access_token)
|
|
||||||
|
|
||||||
query = build_query(full_sync)
|
|
||||||
|
|
||||||
service.query_in_batches(query, per_page: @page_size) do |batch|
|
service.query_in_batches(query, per_page: @page_size) do |batch|
|
||||||
entries = Array(batch)
|
entries = Array(batch)
|
||||||
log "Processing batch of #{entries.size} #{@entity.name}"
|
log "Processing batch of #{entries.size} #{@entity.name}"
|
||||||
|
|
||||||
entries.each do |remote|
|
entries.each do |remote|
|
||||||
persist(remote)
|
persist(remote)
|
||||||
end
|
end
|
||||||
@@ -55,10 +49,7 @@ class SyncServiceBase
|
|||||||
# Sync a single entity by its QBO ID (webhook usage)
|
# Sync a single entity by its QBO ID (webhook usage)
|
||||||
def sync_by_id(id)
|
def sync_by_id(id)
|
||||||
log "Syncing #{@entity.name} with ID #{id}"
|
log "Syncing #{@entity.name} with ID #{id}"
|
||||||
|
with_qbo_service do |service|
|
||||||
@qbo.perform_authenticated_request do |access_token|
|
|
||||||
service_class = "Quickbooks::Service::#{@entity.name}".constantize
|
|
||||||
service = service_class.new(company_id: @qbo.realm_id, access_token: access_token)
|
|
||||||
remote = service.fetch_by_id(id)
|
remote = service.fetch_by_id(id)
|
||||||
persist(remote)
|
persist(remote)
|
||||||
end
|
end
|
||||||
@@ -66,6 +57,10 @@ class SyncServiceBase
|
|||||||
|
|
||||||
private
|
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
|
# Builds a QBO query for retrieving entities
|
||||||
def build_query(full_sync)
|
def build_query(full_sync)
|
||||||
if full_sync
|
if full_sync
|
||||||
@@ -81,13 +76,28 @@ class SyncServiceBase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_documents(local, remote)
|
# Determine if a remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
|
||||||
# Override in subclasses if the entity has attachments (e.g. Invoice)
|
def destroy_local?(remote)
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
# Determine if a remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
|
def extract_value(remote, remote_attr)
|
||||||
def destroy_remote?(remote)
|
case remote_attr
|
||||||
false
|
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
|
end
|
||||||
|
|
||||||
# Log messages with the entity type for better traceability
|
# Log messages with the entity type for better traceability
|
||||||
@@ -99,7 +109,7 @@ class SyncServiceBase
|
|||||||
def persist(remote)
|
def persist(remote)
|
||||||
local = @entity.find_or_initialize_by(id: remote.id)
|
local = @entity.find_or_initialize_by(id: remote.id)
|
||||||
|
|
||||||
if destroy_remote?(remote)
|
if destroy_local?(remote)
|
||||||
if local.persisted?
|
if local.persisted?
|
||||||
local.destroy
|
local.destroy
|
||||||
log "Deleted #{@entity.name} #{remote.id}"
|
log "Deleted #{@entity.name} #{remote.id}"
|
||||||
@@ -120,8 +130,25 @@ class SyncServiceBase
|
|||||||
log "Failed to sync #{@entity.name} #{remote.id}: #{e.message}"
|
log "Failed to sync #{@entity.name} #{remote.id}: #{e.message}"
|
||||||
end
|
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)
|
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
|
||||||
|
def service_class
|
||||||
|
@service_class ||= "Quickbooks::Service::#{@entity}".constantize
|
||||||
|
end
|
||||||
|
|
||||||
|
# Performs authenticaed requests with QBO service
|
||||||
|
def with_qbo_service
|
||||||
|
@qbo.perform_authenticated_request do |access_token|
|
||||||
|
service = service_class.new( company_id: @qbo.realm_id, access_token: access_token )
|
||||||
|
yield service
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Reference in New Issue
Block a user