Refactored QBO service calls

This commit is contained in:
2026-03-14 00:10:10 -04:00
parent 164252cb97
commit cd109f16b5
8 changed files with 25 additions and 60 deletions

View File

@@ -16,11 +16,9 @@ class QboSyncJob < ApplicationJob
def perform(full_sync: false, id: nil, entity: nil, doc_number: nil) def perform(full_sync: false, id: nil, entity: nil, doc_number: nil)
raise "An entity to sync is required" unless entity raise "An entity to sync is required" unless entity
@entity = entity @entity = entity
qbo = QboConnectionService.current!
raise "No QBO configuration found" unless qbo
log "Starting #{full_sync ? 'full' : 'incremental'} sync for #{entity} ##{id || doc_number || 'all'}..." log "Starting #{full_sync ? 'full' : 'incremental'} sync for #{entity} ##{id || doc_number || 'all'}..."
service = "#{entity}SyncService".constantize.new(qbo: qbo) service = "#{entity}SyncService".constantize.new
if id.present? if id.present?
service.sync_by_id(id) service.sync_by_id(id)

View File

@@ -98,15 +98,13 @@ class QboBaseModel < ActiveRecord::Base
# Fetches the entity's details from QuickBooks Online. # Fetches the entity's details from QuickBooks Online.
def fetch_details def fetch_details
log "Fetching details for #{model_name.name} ##{id} from QBO..." log "Fetching details for #{model_name.name} ##{id} from QBO..."
qbo = QboConnectionService.current! service_class.new(local: self).pull()
service_class.new(qbo: qbo, local: self).pull()
end end
# Pushs the entity's details from QuickBooks Online. # 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! reslut = service_class.new(local: self).push
reslut = service_class.new(qbo: qbo, local: self).push
Rails.cache.delete(details_cache_key) Rails.cache.delete(details_cache_key)
return reslut return reslut
end end

View File

@@ -13,7 +13,7 @@ class EstimateSyncService < SyncServiceBase
# sync only one estimate # sync only one estimate
def sync_by_doc_number(number) def sync_by_doc_number(number)
log "Syncing estimate by doc number #{number}" log "Syncing estimate by doc number #{number}"
with_qbo_service do |service| QboConnectionService.with_qbo_service(entity: @entity) do |service|
persist(service.find_by( :doc_number, number).first) persist(service.find_by( :doc_number, number).first)
end end
end end

View File

@@ -17,20 +17,11 @@ class InvoicePushService
# Push invoice changes to QBO if the invoice is linked to any issues with custom field changes that need to be synced # Push invoice changes to QBO if the invoice is linked to any issues with custom field changes that need to be synced
def push def push
return if @invoice.qbo_sync_locked? return if @invoice.qbo_sync_locked?
log "Pushing invoice ##{@invoice.id} to QBO due to linked issue custom field changes" log "Pushing invoice ##{@invoice.id} to QBO due to linked issue custom field changes"
@invoice.update_column(:qbo_sync_locked, true) @invoice.update_column(:qbo_sync_locked, true)
remote = QboConnectionService.with_qbo_service(entity: Invoice) do |service|
qbo = QboConnectionService.current!
qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new( company_id: qbo.realm_id, access_token: access_token)
remote = service.fetch_by_id(@invoice.id) remote = service.fetch_by_id(@invoice.id)
# modify remote object here if needed # modify remote object here if needed
service.update(remote) service.update(remote)
end end
rescue => e rescue => e

View File

@@ -15,8 +15,6 @@ class PdfService
def initialize(entity: entity) def initialize(entity: entity)
raise "An entity to sync is required" unless entity raise "An entity to sync is required" unless entity
@entity = entity @entity = entity
@qbo = QboConnectionService.current!
raise "No QBO configuration found" unless @qbo
end end
# Fetches the PDF for the given entity IDs. If multiple IDs are provided, their PDFs are combined into a single document. # Fetches the PDF for the given entity IDs. If multiple IDs are provided, their PDFs are combined into a single document.

View File

@@ -10,6 +10,11 @@
class QboConnectionService class QboConnectionService
# Returns the current QBO connection record. Raises an error if no connection exists.
def self.current!
Qbo.first || raise("QBO not connected")
end
# Replaces the existing QBO connection with new credentials. Deletes all existing records and creates a new one with the provided token, refresh token, and realm ID. Refreshes the token immediately after creation. # Replaces the existing QBO connection with new credentials. Deletes all existing records and creates a new one with the provided token, refresh token, and realm ID. Refreshes the token immediately after creation.
def self.replace!(token:, refresh_token:, realm_id:) def self.replace!(token:, refresh_token:, realm_id:)
Qbo.transaction do Qbo.transaction do
@@ -24,9 +29,14 @@ class QboConnectionService
end end
end end
# Returns the current QBO connection record. Raises an error if no connection exists. # Performs authenticaed requests with QBO service
def self.current! def self.with_qbo_service(entity: nil)
Qbo.first || raise("QBO not connected") qbo = current!
raise "An entity to sync is required" unless entity
service_class ||= "Quickbooks::Service::#{entity}".constantize
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

View File

@@ -13,11 +13,9 @@ class ServiceBase
# Subclasses should Initializes the service with a QBO client and a local record. # Subclasses should Initializes the service with a QBO client and a local record.
# 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(local: nil)
@entity = local.class.name @entity = local.class.name
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
@local = local @local = local
end end
@@ -31,7 +29,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..."
with_qbo_service do |service| QboConnectionService.with_qbo_service(entity: @entity) do |service|
service.fetch_by_id(@local.id) service.fetch_by_id(@local.id)
end end
rescue => e rescue => e
@@ -45,7 +43,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 = QboConnectionService.with_qbo_service(entity: @entity) do |service|
if @local.id.present? if @local.id.present?
log "Updating #{@entity}" log "Updating #{@entity}"
service.update(@local.details) service.update(@local.details)
@@ -61,22 +59,9 @@ class ServiceBase
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

View File

@@ -12,9 +12,7 @@ class SyncServiceBase
PAGE_SIZE = 1000 PAGE_SIZE = 1000
# Subclasses should initialize with a QBO client instance # Subclasses should initialize with a QBO client instance
def initialize(qbo:) def initialize()
raise "No QBO configuration found" unless qbo
@qbo = qbo
@entity = self.class.model_class @entity = self.class.model_class
@page_size = self.class.page_size @page_size = self.class.page_size
end end
@@ -32,7 +30,7 @@ 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| QboConnectionService.with_qbo_service(entity: @entity) do |service|
query = build_query(full_sync) 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)
@@ -49,7 +47,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| QboConnectionService.with_qbo_service(entity: @entity) do |service|
remote = service.fetch_by_id(id) remote = service.fetch_by_id(id)
persist(remote) persist(remote)
end end
@@ -240,17 +238,4 @@ class SyncServiceBase
end end
end end
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