Refactor invoice processing and synchronization; implement InvoiceSyncJob and related services for improved background processing and logging

This commit is contained in:
2026-02-27 22:33:04 -05:00
parent a1cbf9a0a9
commit 03109d5775
9 changed files with 359 additions and 195 deletions

View File

@@ -9,200 +9,26 @@
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class Invoice < ActiveRecord::Base
has_and_belongs_to_many :issues
belongs_to :customer
validates_presence_of :doc_number, :id, :customer_id, :txn_date
belongs_to :customer
validates :id, presence: true, uniqueness: true
validates :doc_number, :txn_date, presence: true
self.primary_key = :id
# returns a human readable string
# Return the invoice's document number as its string representation
def to_s
return self[:doc_number]
doc_number
end
# sync ALL the invoices
def self.sync
logger.info "Syncing all invoices"
last = Qbo.first.last_sync
query = "SELECT Id, DocNumber FROM Invoice"
query << " WHERE Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
# TODO actually do something with the above query
# .all() is never called since count is never initialized
qbo = Qbo.first
invoices = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
service.all
end
return unless invoices
invoices.each { | invoice |
process_invoice invoice
}
InvoiceSyncJob.perform_later(full_sync: true)
end
#sync by invoice ID
# Sync a single invoice by ID, typically triggered by a webhook notification or manual sync request
def self.sync_by_id(id)
logger.info "Syncing invoice #{id}"
qbo = Qbo.first
qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
invoice = service.fetch_by_id(id)
process_invoice invoice
end
end
private
# Attach the invoice to the issue
def self.attach_to_issue(issue, invoice)
return if issue.nil?
# skip this issue if the issue customer is not the same as the invoice customer
return if issue.customer_id != invoice.customer_ref.value.to_i
logger.info "Attaching invoice #{invoice.id} to issue #{issue.id}"
invoice = Invoice.find_or_create_by(id: invoice.id)
unless issue.invoices.include?(invoice)
issue.invoices << invoice
issue.save!
end
compare_custom_fields(issue, invoice)
end
# processes the invoice into the database
def self.process_invoice(i)
logger.info "Processing invoice #{i.id}"
# Load the invoice into the database
invoice = Invoice.find_or_create_by(id: i.id)
invoice.doc_number = i.doc_number
invoice.id = i.id
invoice.customer_id = i.customer_ref
invoice.txn_date = i.txn_date
invoice.save!
# Scan the private notes for hashtags and attach to the applicable issues
if not i.private_note.nil?
i.private_note.scan(/#(\w+)/).flatten.each { |issue|
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
}
end
# Scan the line items for hashtags and attach to the applicable issues
i.line_items.each { |line|
if line.description
line.description.scan(/#(\w+)/).flatten.each { |issue|
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
}
end
}
end
# compares the custome fields on invoices & issues and updates the invoice as needed
#
# the issue here is when two or more issues share an invoice with the same custom field, but diffrent values
# this condions causes an infinite loop as the webhook is called when an invoice is updated
# TODO maybe add a cf_sync_confict flag to invoices
def self.compare_custom_fields(issue, invoice)
logger.info "Comparing custom fields"
# TODO break if Invoice.find(invoice.id).cf_sync_confict
is_changed = false
logger.debug "Calling :process_invoice_custom_fields hook"
context = Redmine::Hook.call_hook :process_invoice_custom_fields, { issue: issue, invoice: invoice }
# Process updates from the hooks
context.each do |c|
unless c.nil?
logger.debug "Invoice.compare_custom_fields: We have a responce from a hook"
push_updates c[:invoice] if c[:is_changed]
end
end
# Process Issue Custom Values
begin
value = issue.custom_values.find_by(custom_field_id: CustomField.find_by_name(cf.name).id)
# Check to see if the value is blank...
if not value.value.to_s.blank?
# Check to see if the value is diffrent
if not cf.string_value.to_s.eql? value.value.to_s
# update the custom field on the invoice
cf.string_value = value.value.to_s
is_changed = true
end
end
rescue
# Nothing to do here, there is no match
end
push_updates invoice if is_changed
InvoiceSyncJob.perform_later(id: id)
end
# pushes invoice updates
def self.push_updates(invoice)
begin
logger.info "Invoice.push_updates"
qbo = Qbo.first
qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
service.update invoice
end
rescue
# Do nothing, probaly custome field sync confict on the invoice.
# This is a problem with how it's billed
# TODO Add notes in memo area
# TODO flag Invoice.cf_sync_confict here
logger.error "Failed to update invoice"
end
end
# download the pdf from quickbooks
def pdf
qbo = Qbo.first
qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
invoice = service.fetch_by_id(id)
return service.pdf(invoice)
end
end
# Magic Method
# Maps Get/Set methods to QBO invoice object
def method_missing(sym, *arguments)
# Check to see if the method exists
if Quickbooks::Model::Invoice.method_defined?(sym)
# download details if required
pull unless @details
method_name = sym.to_s
# Setter
if method_name[-1, 1] == "="
@details.method(method_name).call(arguments[0])
# Getter
else
return @details.method(method_name).call
end
end
end
# pull the details from quickbooks
def pull
begin
raise Exception unless self.id
qbo = Qbo.first
@details = qbo.perform_authenticated_request do |access_token|
service = Quickbooks::Service::Invoice.new(company_id: qbo.realm_id, access_token: access_token)
service.fetch_by_id(self.id)
end
rescue Exception => e
@details = Quickbooks::Model::Invoice.new
end
end
end
end