Compare commits

...

4 Commits

Author SHA1 Message Date
cd88ce6217 2026.3.5 2026-03-09 08:22:26 -04:00
b10665355d Allow Subclasses to override the page size 2026-03-08 23:34:37 -04:00
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 36 additions and 36 deletions

View File

@@ -16,6 +16,12 @@ class SyncServiceBase
raise "No QBO configuration found" unless qbo
@qbo = qbo
@entity = self.class.model_class
@page_size = self.class.page_size
end
# Subclasses can implement this to overide the default page size
def self.page_size
@page_size = PAGE_SIZE
end
# Subclasses must implement this to specify which local model they sync (e.g. Customer, Invoice)
@@ -25,34 +31,35 @@ class SyncServiceBase
# Sync all entities, or only those updated since the last sync
def sync(full_sync: false)
log "Starting #{full_sync ? 'full' : 'incremental'} #{@entity.name} sync"
log "Starting #{full_sync ? 'full' : 'incremental'} #{@entity.name} sync with page size of: #{@page_size}"
@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)
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
page += 1
service.query_in_batches(query, per_page: @page_size) do |batch|
entries = Array(batch)
log "Processing batch of #{entries.size} #{@entity.name}"
entries.each do |remote|
persist(remote)
end
end
end
log "#{@entity.name} sync complete"
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)
log "Syncing #{@entity.name} with ID #{id}"
@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)
persist(remote)
end
@@ -60,6 +67,21 @@ class SyncServiceBase
private
# Builds a QBO query for retrieving entities
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)
# Override in subclasses if the entity has attachments (e.g. Invoice)
end
@@ -74,24 +96,6 @@ class SyncServiceBase
Rails.logger.info "[#{@entity.name}SyncService] #{msg}"
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
def persist(remote)
local = @entity.find_or_initialize_by(id: remote.id)
@@ -104,24 +108,20 @@ class SyncServiceBase
return
end
# Map remote attributes to local model fields, this should be implemented in subclasses
process_attributes(local, remote)
if local.changed?
local.save!
log "Updated #{@entity.name} #{remote.id}"
# Handle attaching documents if applicable to invoices
attach_documents(local, remote)
attach_documents(local, remote)
end
rescue => e
log "Failed to sync #{@entity.name} #{remote.id}: #{e.message}"
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
def process_attributes(local, remote)
raise NotImplementedError, "Subclasses must implement process_attributes"
end
end

View File

@@ -14,7 +14,7 @@ Redmine::Plugin.register :redmine_qbo do
name 'Redmine QBO plugin'
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'
version '2026.3.3'
version '2026.3.5'
url 'https://github.com/rickbarrette/redmine_qbo'
author_url 'https://barrettefabrication.com'
settings default: {empty: true}, partial: 'qbo/settings'