Compare commits

...

2 Commits

Author SHA1 Message Date
17ac19e435 2026.3.4 2026-03-08 21:59:47 -04:00
ef5089438c Fixed query_in_batches 2026-03-08 21:51:31 -04:00
2 changed files with 29 additions and 36 deletions

View File

@@ -30,29 +30,30 @@ class SyncServiceBase
@qbo.perform_authenticated_request do |access_token| @qbo.perform_authenticated_request do |access_token|
service_class = "Quickbooks::Service::#{@entity.name}".constantize service_class = "Quickbooks::Service::#{@entity.name}".constantize
service = service_class.new(company_id: @qbo.realm_id, access_token: access_token) service = service_class.new(company_id: @qbo.realm_id, access_token: access_token)
page = 1
loop do
collection = fetch_page(service, page, full_sync)
entries = Array(collection&.entries)
break if entries.empty?
entries.each { |remote| persist(remote) } query = build_query(full_sync)
break if entries.size < PAGE_SIZE service.query_in_batches(query, per_page: self.class::PAGE_SIZE) do |batch|
page += 1 entries = Array(batch)
log "Processing batch of #{entries.size} #{@entity.name}"
entries.each do |remote|
persist(remote)
end
end end
end end
log "#{@entity.name} sync complete" log "#{@entity.name} sync complete"
end end
# Sync a single entity by its QBO ID, used for webhook updates # 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}"
@qbo.perform_authenticated_request do |access_token| @qbo.perform_authenticated_request do |access_token|
service_class = "Quickbooks::Service::#{@entity.name}".constantize service_class = "Quickbooks::Service::#{@entity.name}".constantize
service = service_class.new(company_id: @qbo.realm_id, access_token: access_token) 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
@@ -60,8 +61,22 @@ class SyncServiceBase
private private
def build_query(full_sync)
if full_sync
"SELECT * FROM #{@entity.name} ORDER BY Id"
else
last_update = @entity.maximum(:updated_at) || 1.year.ago
<<~SQL.squish
SELECT * FROM #{@entity.name}
WHERE MetaData.LastUpdatedTime > '#{last_update.utc.iso8601}'
ORDER BY MetaData.LastUpdatedTime
SQL
end
end
def attach_documents(local, remote) def attach_documents(local, remote)
# Override in subclasses if the entity has attachments (e.g. Invoice) # Override in subclasses if the entity has attachments (e.g. Invoice)
end end
# Determine if a remote entity should be deleted locally (e.g. if it's marked inactive in QBO) # Determine if a remote entity should be deleted locally (e.g. if it's marked inactive in QBO)
@@ -74,24 +89,6 @@ class SyncServiceBase
Rails.logger.info "[#{@entity.name}SyncService] #{msg}" Rails.logger.info "[#{@entity.name}SyncService] #{msg}"
end end
# Fetch a page of entities, either all or only those updated since the last sync
def fetch_page(service, page, full_sync)
log "Fetching page #{page} of #{@entity.name} from QBO (#{full_sync ? 'full' : 'incremental'} sync)"
start_position = (page - 1) * PAGE_SIZE + 1
if full_sync
service.query("SELECT * FROM #{@entity.name} STARTPOSITION #{start_position} MAXRESULTS #{PAGE_SIZE}")
else
last_update = @entity.maximum(:updated_at) || 1.year.ago
service.query(<<~SQL.squish)
SELECT * FROM #{@entity.name}
WHERE MetaData.LastUpdatedTime > '#{last_update.utc.iso8601}'
STARTPOSITION #{start_position}
MAXRESULTS #{PAGE_SIZE}
SQL
end
end
# Create or update a local entity record based on the QBO remote data # Create or update a local entity record based on the QBO remote data
def persist(remote) def persist(remote)
local = @entity.find_or_initialize_by(id: remote.id) local = @entity.find_or_initialize_by(id: remote.id)
@@ -104,24 +101,20 @@ class SyncServiceBase
return return
end end
# Map remote attributes to local model fields, this should be implemented in subclasses
process_attributes(local, remote) process_attributes(local, remote)
if local.changed? if local.changed?
local.save! local.save!
log "Updated #{@entity.name} #{remote.id}" log "Updated #{@entity.name} #{remote.id}"
attach_documents(local, remote)
# Handle attaching documents if applicable to invoices
attach_documents(local, remote)
end end
rescue => e rescue => e
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 # This method should be implemented in subclasses to map remote attributes to local model
def process_attributes(local, remote) def process_attributes(local, remote)
raise NotImplementedError, "Subclasses must implement process_attributes" raise NotImplementedError, "Subclasses must implement process_attributes"
end end
end end

View File

@@ -14,7 +14,7 @@ Redmine::Plugin.register :redmine_qbo do
name 'Redmine QBO plugin' name 'Redmine QBO plugin'
author 'Rick Barrette' author 'Rick Barrette'
description 'A pluging for Redmine to connect with QuickBooks Online to create Time Activity Entries for billable hours logged when an Issue is closed' description 'A pluging for Redmine to connect with QuickBooks Online to create Time Activity Entries for billable hours logged when an Issue is closed'
version '2026.3.3' version '2026.3.4'
url 'https://github.com/rickbarrette/redmine_qbo' url 'https://github.com/rickbarrette/redmine_qbo'
author_url 'https://barrettefabrication.com' author_url 'https://barrettefabrication.com'
settings default: {empty: true}, partial: 'qbo/settings' settings default: {empty: true}, partial: 'qbo/settings'