Class: QuickbooksApiService

Inherits:
Object
  • Object
show all
Defined in:
app/services/quickbooks_api_service.rb

Overview

QuickbooksApiService provides methods to interact with the QuickBooks Online API.

The service handles OAuth2 authentication, token management, and API operations for integrating with QuickBooks Online. It manages the authorization flow, automatically refreshes expired tokens, and provides methods to interact with QuickBooks resources.

Example usage:

# Get the authorization URL for QuickBooks OAuth flow
auth_url = QuickbooksApiService.consent_url(state: 'your-state-param')

# Initialize the service for a specific company
service = QuickbooksApiService.new(company_id: company.id)

# Complete integration after receiving authorization code
integration = service.integrate(code: 'authorization_code', realm_id: 'quickbooks_company_id')

# Get company integration information
company_integration = service.get_company_integration

# Fetch paginated resources
service.get_each_paginated_resource(
  path: 'Invoice',
  query: "SELECT * FROM Invoice WHERE Balance > '0'"
) do |invoice|
  # Process each invoice
  puts invoice['Id']
end

# Get invoices for a specific customer
invoices = service.get_invoices_for_customer(customer_id: 'customer_ref_id')

# Update an invoice
updated_invoice = service.update_invoice(
  invoice_id: 'invoice_id',
  attributes: { 'CustomField' => [{ 'Name' => 'SPS Ready', 'StringValue' => 'Y' }] }
)

Constant Summary collapse

CLIENT_ID =

QuickBooks API credentials and endpoints from Rails credentials These should be stored in config/credentials.yml.enc

Rails.application.credentials.quickbooks_client_id
CLIENT_SECRET =
Rails.application.credentials.quickbooks_client_secret
REDIRECT_URI =
Rails.application.credentials.quickbooks_redirect_uri
AUTH_URL =
Rails.application.credentials.quickbooks_auth_url
TOKEN_URL =
Rails.application.credentials.quickbooks_token_url
API_BASE_URL =
Rails.application.credentials.quickbooks_api_url
SCOPE =
Rails.application.credentials.quickbooks_scope
MINIMUM_TOKEN_REMAINING_TIME =

Minimum time remaining before token expiration to trigger a refresh Tokens will be refreshed if they expire within this time window

15.minutes.to_i

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(company_id:) ⇒ QuickbooksApiService

Initializes a new QuickbooksApiService instance

Parameters:

  • company_id (Integer)

    The ID of the company in our system



87
88
89
# File 'app/services/quickbooks_api_service.rb', line 87

def initialize(company_id:)
  @company_id = company_id
end

Instance Attribute Details

#company_idObject (readonly)

The company ID that this service instance is working with



82
83
84
# File 'app/services/quickbooks_api_service.rb', line 82

def company_id
  @company_id
end

Class Method Details

Builds the QuickBooks OAuth consent URL

This URL is used to redirect users to the QuickBooks authorization page where they can authorize our application to access their QuickBooks data.

Parameters:

  • state (String)

    A value to maintain state between the request and callback

Returns:

  • (String)

    The fully formed authorization URL



66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'app/services/quickbooks_api_service.rb', line 66

def consent_url(state:)
  URI::HTTPS.build(
    host: URI(AUTH_URL).host,
    path: URI(AUTH_URL).path,
    query: URI.encode_www_form(
      response_type: 'code',
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      scope: SCOPE,
      state: state
    )
  ).to_s
end

Instance Method Details

#get_company_integrationCompanyIntegration

Retrieves the company integration record

Returns:



184
185
186
# File 'app/services/quickbooks_api_service.rb', line 184

def get_company_integration
  company_integration
end

#get_each_paginated_resource(path:, query:) {|resource| ... } ⇒ void

This method returns an undefined value.

Fetches paginated resources from QuickBooks API

This method handles pagination automatically and yields each resource to the provided block. It continues fetching pages until no more results are available or the maximum results per page is not reached.

Parameters:

  • path (String)

    The resource path/type (e.g., ‘Invoice’, ‘Customer’)

  • query (String)

    The QuickBooks query string without pagination parameters

Yields:

  • (resource)

    Each resource from the response



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'app/services/quickbooks_api_service.rb', line 126

def get_each_paginated_resource(path:, query:, &block)
  tokens = valid_tokens
  realm_id = tokens['realm_id']
  # start_position and max_results are used for pagination they should be strings
  start_position = '1'
  max_results = '1000' # 1000 is the maximum allowed by QuickBooks

  loop do
    paginated_query = "#{query} STARTPOSITION #{start_position} MAXRESULTS #{max_results}"
    response = Faraday.get("#{API_BASE_URL}/#{realm_id}/query") do |req|
      req.headers['Authorization'] = "Bearer #{tokens['access_token']}"
      req.headers['Accept'] = 'application/json'
      req.headers['Content-Type'] = 'application/text'
      req.params['query'] = paginated_query
    end

    raise "Get #{path} failed: #{response.body}" unless response.success?

    parsed = JSON.parse(response.body)
    resources = parsed.dig('QueryResponse', path.capitalize)

    break if resources.blank?

    resources.each(&block)

    break unless resources.size == max_results

    start_position += max_results
  end
end

#get_invoices_for_customer(customer_id:) {|invoice| ... } ⇒ Array?

Retrieves all invoices for a specific customer with a zero balance

Parameters:

  • customer_id (String)

    The QuickBooks ID of the customer

Yields:

  • (invoice)

    Each invoice from the response if a block is given

Returns:

  • (Array, nil)

    Array of invoice resources or nil if no invoices found



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'app/services/quickbooks_api_service.rb', line 162

def get_invoices_for_customer(customer_id:)
  # get_each_paginated_resource(
  #   path: 'Invoice',
  #   query: "SELECT * FROM Invoice WHERE CustomerRef = '#{customer_id}' AND Balance = '0'"
  # ) do |invoice|
  #   custom_fields = invoice['CustomField'] || []
  #   sps_ready = custom_fields.find { |field| field['Name'] == 'SPS Ready' }&.dig('StringValue')
  #   yield invoice if sps_ready == 'Y'
  # end

  # This method retrieves all invoices for a specific customer that have a balance of 0 without filtering by SPS Ready.
  get_each_paginated_resource(
    path: 'Invoice',
    query: "SELECT * FROM Invoice WHERE CustomerRef = '#{customer_id}' AND Balance = '0'"
  ) do |invoice|
    yield invoice if block_given?
  end
end

#integrate(code:, realm_id:) ⇒ CompanyIntegration

Completes the OAuth integration process after receiving an authorization code

This method exchanges the authorization code for access/refresh tokens and creates or updates the company integration record.

Parameters:

  • code (String)

    The authorization code from QuickBooks OAuth callback

  • realm_id (String)

    The QuickBooks company ID (realm ID)

Returns:



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'app/services/quickbooks_api_service.rb', line 99

def integrate(code:, realm_id:)
  tokens = exchange_code_for_tokens(code)

  integration = CompanyIntegration.find_or_initialize_by(
    company_id: company_id,
    integration_id: Integration.quickbooks.id
  )

  integration.assign_attributes(
    credentials: tokens.merge('realm_id' => realm_id),
    active: true
  )

  integration.save!
  integration
end

#update_invoice(invoice_id:, attributes:) ⇒ Hash

Updates an invoice in QuickBooks with the given attributes

This method first retrieves the current invoice to get the SyncToken, then merges the provided attributes and sends the update to QuickBooks. The SyncToken is included to prevent concurrency issues.

Parameters:

  • invoice_id (String)

    The QuickBooks ID of the invoice to update

  • attributes (Hash)

    The attributes to update on the invoice

Returns:

  • (Hash)

    The updated invoice data from QuickBooks



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'app/services/quickbooks_api_service.rb', line 197

def update_invoice(invoice_id:, attributes:)
  tokens = valid_tokens
  realm_id = tokens['realm_id']

  # First, we need to get the current invoice to merge with our updates
  response = Faraday.get("#{API_BASE_URL}/#{realm_id}/invoice/#{invoice_id}") do |req|
    req.headers['Authorization'] = "Bearer #{tokens['access_token']}"
    req.headers['Accept'] = 'application/json'
    req.headers['Content-Type'] = 'application/json'
  end

  raise "Get invoice failed: #{response.body}" unless response.success?

  current_invoice = JSON.parse(response.body)['Invoice']

  # Merge the current invoice with our updates
  # We need to include the SyncToken to avoid concurrency issues
  update_data = {
    'Id' => invoice_id,
    'SyncToken' => current_invoice['SyncToken'],
    'sparse' => true
  }.merge(attributes)

  # POST the updated invoice back to QuickBooks
  response = Faraday.post("#{API_BASE_URL}/#{realm_id}/invoice") do |req|
    req.headers['Authorization'] = "Bearer #{tokens['access_token']}"
    req.headers['Accept'] = 'application/json'
    req.headers['Content-Type'] = 'application/json'
    req.body = update_data.to_json
  end

  raise "Update invoice failed: #{response.body}" unless response.success?

  JSON.parse(response.body)['Invoice']
end