mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-04-02 08:21:57 -04:00
Refactor logging across controllers and jobs to use a centralized log method; improve consistency and readability of log messages
This commit is contained in:
@@ -225,4 +225,8 @@ class CustomersController < ApplicationController
|
||||
return string
|
||||
end
|
||||
|
||||
def log(msg)
|
||||
Rails.logger.info "[CustomersController] #{msg}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class EstimateController < ApplicationController
|
||||
Estimate.sync_by_doc_number(params[:search])
|
||||
e = Estimate.find_by_doc_number(params[:search])
|
||||
rescue
|
||||
logger.info "Estimate.find_by_doc_number failed"
|
||||
log "Estimate.find_by_doc_number failed"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,7 +35,7 @@ class EstimateController < ApplicationController
|
||||
Estimate.sync_by_id(params[:id])
|
||||
e = Estimate.find_by_id(params[:id])
|
||||
rescue
|
||||
logger.info "Estimate.find_by_id failed"
|
||||
log "Estimate.find_by_id failed"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,4 +68,10 @@ class EstimateController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log(msg)
|
||||
Rails.logger.info "[EstimateController] #{msg}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
class BillIssueTimeJob < ActiveJob::Base
|
||||
queue_as :default
|
||||
|
||||
# Perform billing of unbilled time entries for a given issue by creating corresponding TimeActivity records in QuickBooks Online, and then marking those entries as billed in Redmine. This job is typically triggered after an invoice is created or updated to ensure all relevant time is captured for billing.
|
||||
def perform(issue_id)
|
||||
issue = Issue.find(issue_id)
|
||||
|
||||
Rails.logger.debug "QBO: Starting billing for issue ##{issue.id}"
|
||||
log "Starting billing for issue ##{issue.id}"
|
||||
|
||||
issue.with_lock do
|
||||
unbilled_entries = issue.time_entries.where(billed: [false, nil]).lock
|
||||
@@ -23,6 +24,7 @@ class BillIssueTimeJob < ActiveJob::Base
|
||||
|
||||
totals = aggregate_hours(unbilled_entries)
|
||||
return if totals.blank?
|
||||
log "Aggregated hours for billing: #{totals.inspect}"
|
||||
|
||||
qbo = Qbo.first
|
||||
raise "No QBO configuration found" unless qbo
|
||||
@@ -35,14 +37,16 @@ class BillIssueTimeJob < ActiveJob::Base
|
||||
unbilled_entries.update_all(billed: true)
|
||||
end
|
||||
|
||||
Rails.logger.debug "QBO: Completed billing for issue ##{issue.id}"
|
||||
log "Completed billing for issue ##{issue.id}"
|
||||
Qbo.update_time_stamp
|
||||
rescue => e
|
||||
Rails.logger.error "QBO: Billing failed for issue ##{issue_id} - #{e.message}"
|
||||
log "Billing failed for issue ##{issue_id} - #{e.message}"
|
||||
raise e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Aggregate time entries by activity name and sum their hours
|
||||
def aggregate_hours(entries)
|
||||
entries.includes(:activity)
|
||||
.group_by { |e| e.activity&.name }
|
||||
@@ -50,7 +54,9 @@ class BillIssueTimeJob < ActiveJob::Base
|
||||
.compact
|
||||
end
|
||||
|
||||
# Create TimeActivity records in QBO for each activity type with the appropriate hours and link them to the issue's assigned employee and customer
|
||||
def create_time_activities(issue, totals, access_token, qbo)
|
||||
log "Creating TimeActivity records in QBO for issue ##{issue.id}"
|
||||
time_service = Quickbooks::Service::TimeActivity.new(
|
||||
company_id: qbo.realm_id,
|
||||
access_token: access_token
|
||||
@@ -82,12 +88,13 @@ class BillIssueTimeJob < ActiveJob::Base
|
||||
time_entry.hourly_rate = item.unit_price
|
||||
time_entry.item_id = item.id
|
||||
|
||||
Rails.logger.debug "QBO: Creating TimeActivity for #{activity_name} (#{hours}h #{minutes}m)"
|
||||
log "Creating TimeActivity for #{activity_name} (#{hours}h #{minutes}m)"
|
||||
|
||||
time_service.create(time_entry)
|
||||
end
|
||||
end
|
||||
|
||||
# Convert a decimal hours float into separate hours and minutes components for QBO TimeActivity
|
||||
def convert_hours(hours_float)
|
||||
total_minutes = (hours_float.to_f * 60).round
|
||||
hours = total_minutes / 60
|
||||
@@ -95,14 +102,20 @@ class BillIssueTimeJob < ActiveJob::Base
|
||||
[hours, minutes]
|
||||
end
|
||||
|
||||
# Build a descriptive string for the TimeActivity based on the issue's tracker, ID, subject, and completion status
|
||||
def build_description(issue)
|
||||
base = "#{issue.tracker} ##{issue.id}: #{issue.subject}"
|
||||
return base if issue.closed?
|
||||
"#{base} (Partial @ #{issue.done_ratio}%)"
|
||||
end
|
||||
|
||||
# Find an item in QBO by name, escaping single quotes to prevent query issues. Returns nil if not found.
|
||||
def find_item(item_service, name)
|
||||
safe = name.gsub("'", "\\\\'")
|
||||
item_service.query("SELECT * FROM Item WHERE Name = '#{safe}'").first
|
||||
end
|
||||
|
||||
def log(msg)
|
||||
Rails.logger.info "[BillIssueTimeJob] #{msg}"
|
||||
end
|
||||
end
|
||||
@@ -12,11 +12,12 @@ class EstimateSyncJob < ApplicationJob
|
||||
queue_as :default
|
||||
retry_on StandardError, wait: 5.minutes, attempts: 5
|
||||
|
||||
# Sync estimates from QuickBooks Online to local database
|
||||
def perform(full_sync: false)
|
||||
qbo = Qbo.first
|
||||
return unless qbo
|
||||
|
||||
logger.info "[EstimateSyncJob] Starting #{full_sync ? 'full' : 'incremental'} sync..."
|
||||
log "Starting #{full_sync ? 'full' : 'incremental'} sync..."
|
||||
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
@@ -36,11 +37,12 @@ class EstimateSyncJob < ApplicationJob
|
||||
break if entries.size < 1000
|
||||
end
|
||||
|
||||
logger.info "[EstimateSyncJob] Completed sync."
|
||||
log "Completed sync."
|
||||
Qbo.update_time_stamp
|
||||
end
|
||||
rescue => e
|
||||
logger.error "[EstimateSyncJob] Fatal error: #{e.message}"
|
||||
logger.error e.backtrace.join("\n")
|
||||
log "Fatal error: #{e.message}"
|
||||
log e.backtrace.join("\n")
|
||||
raise # allows retry
|
||||
end
|
||||
|
||||
@@ -62,13 +64,13 @@ class EstimateSyncJob < ApplicationJob
|
||||
SQL
|
||||
end
|
||||
rescue => e
|
||||
logger.error "[EstimateSyncJob] Failed to fetch page #{page}: #{e.message}"
|
||||
log "Failed to fetch page #{page}: #{e.message}"
|
||||
nil
|
||||
end
|
||||
|
||||
# Sync a single estimate record
|
||||
def sync_estimates(e)
|
||||
logger.info "[EstimateSyncJob] Processing estimate #{e.id} doc=#{e.doc_number} status=#{e.txn_status}"
|
||||
log "Processing estimate #{e.id} doc=#{e.doc_number} status=#{e.txn_status}"
|
||||
|
||||
estimate = Estimate.find_or_initialize_by(id: e.id)
|
||||
|
||||
@@ -78,13 +80,17 @@ class EstimateSyncJob < ApplicationJob
|
||||
|
||||
if estimate.changed?
|
||||
estimate.save
|
||||
logger.info "[EstimateSyncJob] Updated estimate #{e.id}"
|
||||
log "Updated estimate #{e.id}"
|
||||
end
|
||||
end
|
||||
|
||||
# TODO remove deleted estimates
|
||||
|
||||
rescue => error
|
||||
logger.error "[EstimateSyncJob] Failed to sync estimate #{e.id}: #{error.message}"
|
||||
log "Failed to sync estimate #{e.id}: #{error.message}"
|
||||
end
|
||||
|
||||
def log(msg)
|
||||
Rails.logger.info "[EstimateSyncJob] #{msg}"
|
||||
end
|
||||
end
|
||||
@@ -17,7 +17,9 @@ class WebhookProcessJob < ActiveJob::Base
|
||||
Estimate
|
||||
].freeze
|
||||
|
||||
# Process incoming QBO webhook notifications and sync relevant data to Redmine
|
||||
def perform(raw_body)
|
||||
log "Received webhook: #{raw_body}"
|
||||
data = JSON.parse(raw_body)
|
||||
|
||||
data.fetch('eventNotifications', []).each do |notification|
|
||||
@@ -33,7 +35,9 @@ class WebhookProcessJob < ActiveJob::Base
|
||||
|
||||
private
|
||||
|
||||
# Process a single entity from the webhook payload and sync it to Redmine if it's an allowed type
|
||||
def process_entity(entity)
|
||||
log "Processing entity: #{entity}"
|
||||
name = entity['name']
|
||||
id = entity['id']&.to_i
|
||||
|
||||
@@ -53,7 +57,10 @@ class WebhookProcessJob < ActiveJob::Base
|
||||
model.sync_by_id(id)
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "QBO Webhook entity processing failed"
|
||||
Rails.logger.error e.message
|
||||
log "#{e.message}"
|
||||
end
|
||||
|
||||
def log(msg)
|
||||
Rails.logger.info "[WebhookProcessJob] #{msg}"
|
||||
end
|
||||
end
|
||||
@@ -13,12 +13,13 @@ module QuickbooksOauth
|
||||
|
||||
#== Instance Methods
|
||||
|
||||
# This method will attempt to execute the block and if it encounters an OAuth2::Error or Quickbooks::AuthorizationFailure it will attempt to refresh the token and retry the block. It will try this up to 3 times before giving up and raising an exception.
|
||||
def perform_authenticated_request(&block)
|
||||
attempts = 0
|
||||
begin
|
||||
yield oauth_access_token
|
||||
rescue OAuth2::Error, Quickbooks::AuthorizationFailure => ex
|
||||
Rails.logger.error("QuickbooksOauth.perform: #{ex.message}")
|
||||
log "perform_authenticated_request: #{ex.message}"
|
||||
|
||||
# to prevent an infinite loop here keep a counter and bail out after N times...
|
||||
attempts += 1
|
||||
@@ -32,8 +33,9 @@ module QuickbooksOauth
|
||||
end
|
||||
end
|
||||
|
||||
# This method will attempt to refresh the access token and update the record with the new access token, refresh token and their respective expiration times. If the refresh token expires in more than 0 seconds then we will set the refresh token expiration time to that value, otherwise we will set it to 100 days from now.
|
||||
def refresh_token!
|
||||
Rails.logger.info("QuickbooksOauth.refresh_token!")
|
||||
log "refresh_token!"
|
||||
t = oauth_access_token
|
||||
refreshed = t.refresh!
|
||||
|
||||
@@ -43,7 +45,7 @@ module QuickbooksOauth
|
||||
oauth2_refresh_token_expires_at = 100.days.from_now
|
||||
end
|
||||
|
||||
Rails.logger.info("QuickbooksOauth.refresh_token!: #{oauth2_refresh_token_expires_at}")
|
||||
log "refresh_token!: #{oauth2_refresh_token_expires_at}"
|
||||
|
||||
update!(
|
||||
oauth2_access_token: refreshed.token,
|
||||
@@ -53,20 +55,24 @@ module QuickbooksOauth
|
||||
)
|
||||
end
|
||||
|
||||
# This method will return an instance of the OAuth2::Client class that is configured with the consumer key, consumer secret and the appropriate URLs for the Intuit OAuth2 service. It will also set the sandbox mode based on the plugin settings.
|
||||
def oauth_client
|
||||
self.class.construct_oauth2_client
|
||||
end
|
||||
|
||||
# This method will return an instance of the OAuth2::AccessToken class that is configured with the current access token, refresh token and the OAuth2 client. This access token can be used to make authenticated requests to the Intuit API.
|
||||
def oauth_access_token
|
||||
OAuth2::AccessToken.new(oauth_client, oauth2_access_token, refresh_token: oauth2_refresh_token)
|
||||
end
|
||||
|
||||
# This method is an alias for the oauth_access_token method and is used to provide a more intuitive name for the access token when making authenticated requests.
|
||||
def consumer
|
||||
oauth_access_token
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
# This method will construct and return an instance of the OAuth2::Client class that is configured with the consumer key, consumer secret and the appropriate URLs for the Intuit OAuth2 service. It will also set the sandbox mode based on the plugin settings. This method is used by the instance method oauth_client to create a new OAuth2 client for each instance of the model that includes this concern.
|
||||
def construct_oauth2_client
|
||||
|
||||
oauth_consumer_key = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
|
||||
@@ -74,7 +80,7 @@ module QuickbooksOauth
|
||||
|
||||
# Are we are playing in the sandbox?
|
||||
Quickbooks.sandbox_mode = Setting.plugin_redmine_qbo[:sandbox] ? true : false
|
||||
logger.info "Sandbox mode: #{Quickbooks.sandbox_mode}"
|
||||
log "Sandbox mode: #{Quickbooks.sandbox_mode}"
|
||||
|
||||
options = {
|
||||
site: "https://appcenter.intuit.com/connect/oauth2",
|
||||
@@ -85,4 +91,11 @@ module QuickbooksOauth
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def log(msg)
|
||||
Rails.logger.info "[QuickbooksOauth] #{msg}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,7 +27,7 @@ class Estimate < ActiveRecord::Base
|
||||
|
||||
# sync only one estimate
|
||||
def self.sync_by_id(id)
|
||||
logger.info "Syncing estimate #{id}"
|
||||
log "Syncing estimate #{id}"
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Estimate.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
@@ -37,7 +37,7 @@ class Estimate < ActiveRecord::Base
|
||||
|
||||
# sync only one estimate
|
||||
def self.sync_by_doc_number(number)
|
||||
logger.info "Syncing estimate by doc number #{number}"
|
||||
log "Syncing estimate by doc number #{number}"
|
||||
qbo = Qbo.first
|
||||
qbo.perform_authenticated_request do |access_token|
|
||||
service = Quickbooks::Service::Estimate.new(company_id: qbo.realm_id, access_token: access_token)
|
||||
@@ -63,7 +63,7 @@ class Estimate < ActiveRecord::Base
|
||||
|
||||
# process an estimate into the database
|
||||
def self.process_estimate(qbo_estimate)
|
||||
logger.info "Processing estimate #{qbo_estimate.id}"
|
||||
log "Processing estimate #{qbo_estimate.id}"
|
||||
estimate = find_or_create_by(id: qbo_estimate.id)
|
||||
estimate.doc_number = qbo_estimate.doc_number
|
||||
estimate.customer_id = qbo_estimate.customer_ref.value
|
||||
@@ -115,5 +115,9 @@ class Estimate < ActiveRecord::Base
|
||||
@details = Quickbooks::Model::Estimate.new
|
||||
end
|
||||
end
|
||||
|
||||
def log(msg)
|
||||
Rails.logger.info "[Estimate] #{msg}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ class Qbo < ActiveRecord::Base
|
||||
# Updates last sync time stamp
|
||||
def self.update_time_stamp
|
||||
date = DateTime.now
|
||||
logger.info "Updating QBO timestamp to #{date}"
|
||||
log "Updating QBO timestamp to #{date}"
|
||||
qbo = Qbo.first
|
||||
qbo.last_sync = date
|
||||
qbo.save
|
||||
@@ -25,4 +25,11 @@ class Qbo < ActiveRecord::Base
|
||||
def self.last_sync
|
||||
format_time(Qbo.first.last_sync)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.log(msg)
|
||||
logger.info "[QBO] #{msg}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user