effective_qb_online 0.9.10 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6a3e221e9f86bab6fec4bbdf698d486a12924850960c797e897a79a9dde137e
4
- data.tar.gz: eceb1df9ab98fd686e43e23572ef6be5ace97720bddaf35df4878dee8c5fafbe
3
+ metadata.gz: 78a04b8a3964143fa09e0cae9d54c29619a9b55d8f28b8079bb0b9c900301199
4
+ data.tar.gz: 9e52ef89f8402f6648b21063804deb9b9a8818facf74f877c0bff40992a3ab15
5
5
  SHA512:
6
- metadata.gz: 481a68ddeda1c7dd2fc07f6c5a44758df77f7bed84a84313f8f33850c3b061a59444c5f3f9654e35f114677752e12b750909f0fb45e5298a8d6a7282e59a19ab
7
- data.tar.gz: 6840d8757d7d237283957334a868f447348395972f42e09c036bd6b6f16ac6e7baf5b84dea43419d9653e2e9968527c8dfba2a10424ee4d41f87978c1800fa2c
6
+ metadata.gz: 633be21ff66eecb59f7fbc422145f163e311ed65e9e16ed8b9b215e40fdf278c6b6449bd72cfee7172d0d2f967aac871f8ffdbae1332652cd1a14dff9152a493
7
+ data.tar.gz: '0843e178990e9c31a99e6d4d2b4875fdd0a202bfffcf4240c7e7fbd05e711513c1301739611bf4ad0de26a30f2b1603c92d68c97e29733cfa510e3c7d835641a'
@@ -10,10 +10,11 @@ module Effective
10
10
  before_action { EffectiveResources.authorize!(self, :admin, :effective_qb_online) }
11
11
 
12
12
  def authorize
13
+ session[:qb_oauth_state] = SecureRandom.hex(24)
13
14
  grant_url = client.auth_code.authorize_url(
14
15
  redirect_uri: redirect_uri,
15
16
  response_type: 'code',
16
- state: SecureRandom.hex(12),
17
+ state: session[:qb_oauth_state],
17
18
  scope: 'com.intuit.quickbooks.accounting'
18
19
  )
19
20
 
@@ -22,44 +23,77 @@ module Effective
22
23
 
23
24
  # This matches the QuickBooks Redirect URI and we have to set it up ahead of time.
24
25
  def callback
25
- return unless params[:code].present? && params[:realmId].present? && params[:state].present?
26
+ expected_state = session.delete(:qb_oauth_state)
27
+ unless expected_state.present? && params[:state].present? && ActiveSupport::SecurityUtils.secure_compare(expected_state, params[:state].to_s)
28
+ flash[:danger] = 'Invalid QuickBooks authorization response. Please start the connection again.'
29
+ return redirect_to(effective_qb_online.admin_quickbooks_path)
30
+ end
26
31
 
27
- token = client.auth_code.get_token(params[:code], redirect_uri: redirect_uri)
28
- return unless token
32
+ unless params[:code].present? && params[:realmId].present?
33
+ flash[:danger] = 'QuickBooks authorization was not completed. Please try again.'
34
+ return redirect_to(effective_qb_online.admin_quickbooks_path)
35
+ end
29
36
 
30
37
  realm = Effective::QbRealm.all.first_or_initialize
38
+ if realm.persisted? && realm.realm_id != params[:realmId].to_s
39
+ flash[:danger] = 'Please reconnect the existing QuickBooks company. Selecting another company would invalidate your account settings and receipt history.'
40
+ return redirect_to(effective_qb_online.admin_quickbooks_path)
41
+ end
31
42
 
32
- realm.update!(
33
- realm_id: params[:realmId],
34
- access_token: token.token,
35
- refresh_token: token.refresh_token,
36
- access_token_expires_at: Time.at(token.expires_at),
37
- refresh_token_expires_at: (Time.at(token.expires_at) + 100.days)
38
- )
43
+ token = client.auth_code.get_token(params[:code], redirect_uri: redirect_uri)
44
+
45
+ realm.with_lock do
46
+ realm.update!(
47
+ realm_id: params[:realmId],
48
+ access_token: token.token,
49
+ refresh_token: token.refresh_token,
50
+ access_token_expires_at: Time.at(token.expires_at),
51
+ refresh_token_expires_at: Time.current + token.params.fetch('x_refresh_token_expires_in', 100.days).to_i.seconds,
52
+ reconnection_required_at: nil,
53
+ reconnection_reason: nil
54
+ )
55
+ end
39
56
 
40
57
  flash[:success] = 'Successfully connected with QuickBooks Online'
41
58
 
42
59
  redirect_to(effective_qb_online.admin_quickbooks_path, allow_other_host: true)
60
+ rescue OAuth2::Error, Faraday::Error, ActiveRecord::RecordNotFound
61
+ flash[:danger] = 'QuickBooks authorization could not be completed. Please start the connection again.'
62
+ redirect_to(effective_qb_online.admin_quickbooks_path)
43
63
  end
44
64
 
45
65
  def revoke
46
- realm = EffectiveQbOnline.api.realm
47
- return unless realm
48
-
49
- # Instantiate the token
50
- token = OAuth2::AccessToken.new(client, realm.access_token, refresh_token: realm.refresh_token)
66
+ realm = Effective::QbRealm.first
51
67
 
52
- # Revoke
53
- response = token.post('/o/oauth2/revoke', params: { token: realm.refresh_token })
54
-
55
- if response.status == 200
56
- flash[:success] = 'Successfully revoked from QuickBooks Online'
57
- realm.destroy!
68
+ if realm.blank?
69
+ flash[:success] = 'Already disconnected from QuickBooks Online'
58
70
  else
59
- flash[:danger] = 'Unable to revoke'
71
+ revoked_refresh_token = realm.refresh_token
72
+ unless EffectiveQbOnline.revoke_token!(realm)
73
+ flash[:danger] = 'Unable to revoke QuickBooks access. Please try again or reconnect using a current company administrator.'
74
+ return redirect_to(effective_qb_online.admin_quickbooks_path)
75
+ end
76
+
77
+ realm.with_lock do
78
+ if realm.refresh_token == revoked_refresh_token
79
+ realm.destroy!
80
+ flash[:success] = 'Successfully revoked from QuickBooks Online'
81
+ else
82
+ flash[:danger] = 'The QuickBooks connection changed while revoking access. Your newer connection has been preserved.'
83
+ end
84
+ end
60
85
  end
61
86
 
62
- redirect_to(effective_qb_online.admin_quickbooks_path, allow_other_host: true)
87
+ redirect_to(effective_qb_online.admin_quickbooks_path)
88
+ rescue ActiveRecord::RecordNotFound
89
+ flash[:success] = 'Already disconnected from QuickBooks Online'
90
+ redirect_to(effective_qb_online.admin_quickbooks_path)
91
+ rescue OAuth2::Error
92
+ flash[:danger] = 'QuickBooks could not revoke access. Your connection settings have been preserved. Please reconnect or try again.'
93
+ redirect_to(effective_qb_online.admin_quickbooks_path)
94
+ rescue Faraday::Error
95
+ flash[:danger] = 'Unable to reach QuickBooks Online. Please try again.'
96
+ redirect_to(effective_qb_online.admin_quickbooks_path)
63
97
  end
64
98
 
65
99
  private
@@ -2,6 +2,16 @@ module Effective
2
2
  class QbOnlineMailer < EffectiveQbOnline.parent_mailer_class
3
3
  include EffectiveMailer
4
4
 
5
+ def reconnection_required(resource, opts = {})
6
+ raise('expected an Effective::QbRealm') unless resource.kind_of?(Effective::QbRealm)
7
+ @qb_realm = resource
8
+
9
+ to = EffectiveOrders.qb_online_sync_error_recipients.presence || EffectiveOrders.mailer_admin
10
+ cc = EffectiveQbOnline.sync_error_cc_recipients.presence
11
+ subject = subject_for(__method__, 'QuickBooks Online reconnection required', resource, opts)
12
+ mail(to: to, cc: cc, subject: subject, **headers_for(resource, opts))
13
+ end
14
+
5
15
  def sync_error(resource, opts = {})
6
16
  raise('expected an Effective::QbReceipt') unless resource.kind_of?(Effective::QbReceipt)
7
17
 
@@ -223,31 +223,61 @@ module Effective
223
223
  end
224
224
  end
225
225
 
226
- def with_authenticated_request(max_attempts: 3, &block)
226
+ def with_authenticated_request(max_attempts: 2)
227
227
  attempts = 0
228
228
 
229
229
  begin
230
+ realm.reload
231
+ if realm.reconnection_required?
232
+ raise EffectiveQbOnline::ReconnectionRequired, 'QuickBooks Online requires reconnection by a current company administrator. Retry this receipt manually after reconnecting.'
233
+ end
234
+
235
+ if @access_token && (@access_token.token != realm.access_token || @access_token.refresh_token != realm.refresh_token)
236
+ @access_token = nil
237
+ end
238
+
230
239
  @access_token ||= OAuth2::AccessToken.new(EffectiveQbOnline.oauth2_client, realm.access_token, refresh_token: realm.refresh_token)
231
240
  yield(@access_token)
232
- rescue OAuth2::Error, Quickbooks::AuthorizationFailure => e
233
- puts "QuickBooks OAuth Error: #{e.message}"
241
+ rescue OAuth2::Error, Quickbooks::AuthorizationFailure => error
242
+ # Only authentication failures justify refreshing. Other OAuth/API errors
243
+ # retain their original meaning and must not cause a token rotation.
244
+ raise unless error.kind_of?(Quickbooks::AuthorizationFailure) || error.response.status == 401
234
245
 
235
246
  attempts += 1
236
- raise "unable to refresh QuickBooks OAuth2 token" if attempts >= max_attempts
247
+ raise if attempts >= max_attempts
237
248
 
238
- # Refresh
239
- refreshed = @access_token.refresh!
240
- @access_token = nil # Clear memoized token so it's rebuilt with new credentials
249
+ refresh_access_token!
250
+ retry
251
+ end
252
+ end
241
253
 
254
+ def refresh_access_token!
255
+ attempted_refresh_token = @access_token.refresh_token
256
+
257
+ # Refresh tokens rotate. Reload under a lock so another worker's successful
258
+ # refresh is reused instead of overwritten with stale credentials.
259
+ realm.with_lock do
260
+ return if realm.reconnection_required?
261
+ return if realm.access_token != @access_token.token || realm.refresh_token != attempted_refresh_token
262
+
263
+ refreshed = @access_token.refresh!
242
264
  realm.update!(
243
265
  access_token: refreshed.token,
244
266
  refresh_token: refreshed.refresh_token,
245
267
  access_token_expires_at: Time.at(refreshed.expires_at),
246
- refresh_token_expires_at: (Time.at(refreshed.expires_at) + 100.days)
268
+ refresh_token_expires_at: Time.current + refreshed.params.fetch('x_refresh_token_expires_in', 100.days).to_i.seconds
247
269
  )
248
-
249
- retry
250
270
  end
271
+ rescue OAuth2::Error => error
272
+ raise unless error.code == 'invalid_grant'
273
+
274
+ reason = error.response.parsed['x_error_reason'].presence || 'invalid_grant'
275
+ realm.require_reconnection!(reason: reason, refresh_token: attempted_refresh_token)
276
+ return unless realm.reconnection_required?
277
+
278
+ raise EffectiveQbOnline::ReconnectionRequired, 'QuickBooks Online authorization is no longer valid. Reconnect using a current company administrator, then retry errored receipts manually.'
279
+ ensure
280
+ @access_token = nil
251
281
  end
252
282
 
253
283
  def scrub(value, sql: false)
@@ -18,6 +18,9 @@ module Effective
18
18
  payment_method_id :string
19
19
  order_number_as_transaction_number :boolean, default: false
20
20
 
21
+ reconnection_required_at :datetime
22
+ reconnection_reason :string
23
+
21
24
  timestamps
22
25
  end
23
26
 
@@ -34,6 +37,26 @@ module Effective
34
37
  'QuickBooks Online Settings'
35
38
  end
36
39
 
40
+ def reconnection_required?
41
+ reconnection_required_at.present?
42
+ end
43
+
44
+ # Only the first failure for the current grant sends a notification. A failed
45
+ # request using old credentials must not disable a newly reconnected grant.
46
+ def require_reconnection!(reason:, refresh_token:)
47
+ changed = false
48
+
49
+ with_lock do
50
+ if !reconnection_required? && self.refresh_token == refresh_token
51
+ update!(reconnection_required_at: Time.current, reconnection_reason: reason)
52
+ changed = true
53
+ end
54
+ end
55
+
56
+ EffectiveQbOnline.send_email(:reconnection_required, self) if changed
57
+ changed
58
+ end
59
+
37
60
  def configured?
38
61
  realm_id.present? && access_token.present? && deposit_to_account_id.present? && payment_method_id.present?
39
62
  end
@@ -96,7 +96,7 @@ module Effective
96
96
  rescue => e
97
97
  result = [e.message, ("(intuit_tid: #{e.intuit_tid})" if e.try(:intuit_tid).present?)].compact.join("\n\n")
98
98
  assign_attributes(result: result)
99
- error!
99
+ error!(notify: !e.kind_of?(EffectiveQbOnline::ReconnectionRequired))
100
100
  end
101
101
 
102
102
  end
@@ -110,10 +110,10 @@ module Effective
110
110
  completed!
111
111
  end
112
112
 
113
- def error!
113
+ def error!(notify: true)
114
114
  errored!
115
115
  EffectiveLogger.error(result, associated: self) if defined?(EffectiveLogger)
116
- EffectiveQbOnline.send_email(:sync_error, self) if EffectiveOrders.send_qb_online_sync_error
116
+ EffectiveQbOnline.send_email(:sync_error, self) if notify && EffectiveOrders.send_qb_online_sync_error
117
117
 
118
118
  false
119
119
  end
@@ -165,13 +165,14 @@ module Effective
165
165
  end
166
166
 
167
167
  # Order item names may contain HTML (e.g. an EventRegistrant's "Member" badge).
168
- # QuickBooks line item descriptions are plain text, so strip the markup while keeping
169
- # the badge text. Convert line breaks to spaces first so adjacent text doesn't run together.
168
+ # QuickBooks line item descriptions are plain text, so strip the markup while keeping the badge text.
169
+ # <br> and &nbsp; carry spacing, so they become spaces rather than being dropped or passed through.
170
+ # The sanitizer re-encodes as it strips, so unescape after or a title with an & syncs as &amp;
170
171
  def self.strip_html(value)
171
172
  return value if value.blank?
172
173
 
173
- text = value.to_s.gsub(/<br\s*\/?>/i, ' ')
174
- ActionView::Base.full_sanitizer.sanitize(text).squish
174
+ text = ActionView::Base.full_sanitizer.sanitize(value.to_s.gsub(/<br\s*\/?>/i, ' '))
175
+ CGI.unescapeHTML(text.gsub('&nbsp;', ' ')).squish
175
176
  end
176
177
 
177
178
  end
@@ -1,4 +1,11 @@
1
- - company_info = (api.company_info rescue nil)
1
+ - company_info = (api.company_info rescue nil) unless api.realm.reconnection_required? || local_assigns[:api_error].present?
2
+
3
+ - if api.realm.reconnection_required?
4
+ .alert.alert-warning
5
+ %p QuickBooks Online authorization is no longer valid. Reconnect using a current company administrator.
6
+ %p Your company settings and receipts have been preserved. After reconnecting, retry errored receipts manually.
7
+ = link_to 'Reconnect QuickBooks', effective_qb_online.quickbooks_oauth_path, class: 'btn btn-primary'
8
+
2
9
 
3
10
  - if company_info.present?
4
11
  %table.table.table-sm
@@ -50,7 +57,7 @@
50
57
  %tbody
51
58
  %tr
52
59
  %td QuickBooks API Error
53
- %td= api.api_error
60
+ %td= api.realm.reconnection_required? ? 'Reconnection required' : (local_assigns[:api_error] || api.api_error)
54
61
 
55
62
  %tr
56
63
  %td QuickBooks Online
@@ -1,6 +1,7 @@
1
1
  %h1.effective-admin-heading= @page_title
2
2
 
3
3
  - api = EffectiveQbOnline.api
4
+ - api_error = api.api_error if api.present? && !api.realm.reconnection_required?
4
5
 
5
6
  - if api.blank?
6
7
  = card('Connect to QuickBooks Online') do
@@ -11,13 +12,13 @@
11
12
  = card('Test Credentials') do
12
13
  = render('test_credentials', api: api)
13
14
 
14
- - if api.realm&.configured?
15
+ - if api.realm&.configured? && !api.realm.reconnection_required?
15
16
  .mb-2
16
17
  = collapse('Show Company Settings') do
17
- = render('company', api: api)
18
+ = render('company', api: api, api_error: api_error)
18
19
  - else
19
20
  = card('Company') do
20
- = render('company', api: api)
21
+ = render('company', api: api, api_error: api_error)
21
22
 
22
23
  = card('Sales Receipts') do
23
24
  .text-right.mb-2
@@ -0,0 +1,7 @@
1
+ %p QuickBooks Online authorization is no longer valid for company #{@qb_realm.realm_id}.
2
+ %p Sign in as a website administrator and reconnect using a current QuickBooks company administrator.
3
+ %p Background QuickBooks requests are paused until reconnection. Company settings and receipts have been preserved.
4
+ %p After reconnecting, review errored receipts and retry them manually.
5
+ %p= link_to 'Reconnect QuickBooks Online', effective_qb_online.admin_quickbooks_url
6
+
7
+ %p Have a great day!
@@ -14,6 +14,9 @@ class CreateEffectiveQbOnline < ActiveRecord::Migration[6.0]
14
14
 
15
15
  t.boolean :order_number_as_transaction_number, default: false
16
16
 
17
+ t.datetime :reconnection_required_at
18
+ t.string :reconnection_reason
19
+
17
20
  t.timestamps
18
21
  end
19
22
 
@@ -0,0 +1,11 @@
1
+ class UpgradeEffectiveQbOnlineZeroNineTwelve < ActiveRecord::Migration[6.0]
2
+ def up
3
+ add_column :qb_realms, :reconnection_required_at, :datetime unless column_exists?(:qb_realms, :reconnection_required_at)
4
+ add_column :qb_realms, :reconnection_reason, :string unless column_exists?(:qb_realms, :reconnection_reason)
5
+ end
6
+
7
+ def down
8
+ remove_column :qb_realms, :reconnection_reason if column_exists?(:qb_realms, :reconnection_reason)
9
+ remove_column :qb_realms, :reconnection_required_at if column_exists?(:qb_realms, :reconnection_required_at)
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module EffectiveQbOnline
2
- VERSION = '0.9.10'.freeze
2
+ VERSION = '0.10.0'.freeze
3
3
  end
@@ -21,19 +21,35 @@ module EffectiveQbOnline
21
21
  mailer&.constantize || Effective::QbOnlineMailer
22
22
  end
23
23
 
24
+ class ReconnectionRequired < StandardError; end
25
+
24
26
  def self.oauth2_client
25
- @oauth2_client ||= OAuth2::Client.new(
27
+ OAuth2::Client.new(
26
28
  oauth_client_id,
27
29
  oauth_client_secret,
28
30
  site: 'https://appcenter.intuit.com/connect/oauth2',
29
31
  authorize_url: 'https://appcenter.intuit.com/connect/oauth2',
30
32
  token_url: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer',
33
+ revoke_url: 'https://developer.api.intuit.com/v2/oauth2/tokens/revoke',
31
34
  connection_opts: {
32
35
  request: { open_timeout: 10, timeout: 30 }
33
36
  }
34
37
  )
35
38
  end
36
39
 
40
+ # Intuit requires client authentication rather than an access-token bearer header.
41
+ # https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0
42
+ def self.revoke_token!(realm)
43
+ response = oauth2_client.revoke_token(
44
+ realm.refresh_token,
45
+ nil,
46
+ token_method: :post,
47
+ headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
48
+ )
49
+
50
+ response.status == 200
51
+ end
52
+
37
53
  def self.api(realm: nil)
38
54
  realm ||= Effective::QbRealm.first
39
55
  return nil if realm.blank?
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_qb_online
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.10
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-07-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -28,30 +27,82 @@ dependencies:
28
27
  name: quickbooks-ruby
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
31
33
  - - ">="
32
34
  - !ruby/object:Gem::Version
33
- version: '0'
35
+ version: 2.0.7
34
36
  type: :runtime
35
37
  prerelease: false
36
38
  version_requirements: !ruby/object:Gem::Requirement
37
39
  requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '2.0'
38
43
  - - ">="
39
44
  - !ruby/object:Gem::Version
40
- version: '0'
45
+ version: 2.0.7
46
+ - !ruby/object:Gem::Dependency
47
+ name: oauth2
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '2.0'
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.0.25
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '2.0'
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 2.0.25
66
+ - !ruby/object:Gem::Dependency
67
+ name: faraday
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '2.14'
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 2.14.4
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.14'
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.14.4
41
86
  - !ruby/object:Gem::Dependency
42
87
  name: faraday-net_http_persistent
43
88
  requirement: !ruby/object:Gem::Requirement
44
89
  requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '2.3'
45
93
  - - ">="
46
94
  - !ruby/object:Gem::Version
47
- version: '0'
95
+ version: 2.3.1
48
96
  type: :runtime
49
97
  prerelease: false
50
98
  version_requirements: !ruby/object:Gem::Requirement
51
99
  requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.3'
52
103
  - - ">="
53
104
  - !ruby/object:Gem::Version
54
- version: '0'
105
+ version: 2.3.1
55
106
  - !ruby/object:Gem::Dependency
56
107
  name: effective_bootstrap
57
108
  requirement: !ruby/object:Gem::Requirement
@@ -258,10 +309,12 @@ files:
258
309
  - app/views/admin/qb_receipts/_form.html.haml
259
310
  - app/views/admin/qb_receipts/edit.html.haml
260
311
  - app/views/admin/qb_receipts/new.html.haml
312
+ - app/views/effective/qb_online_mailer/reconnection_required.html.haml
261
313
  - app/views/effective/qb_online_mailer/sync_error.html.haml
262
314
  - config/effective_qb_online.rb
263
315
  - config/routes.rb
264
316
  - db/migrate/101_create_effective_qb_online.rb
317
+ - db/migrate/20261013140000_upgrade_effective_qb_online_zero_nine_twelve.rb
265
318
  - db/seeds.rb
266
319
  - lib/effective_qb_online.rb
267
320
  - lib/effective_qb_online/engine.rb
@@ -273,7 +326,6 @@ homepage: https://github.com/code-and-effect/effective_qb_online
273
326
  licenses:
274
327
  - MIT
275
328
  metadata: {}
276
- post_install_message:
277
329
  rdoc_options: []
278
330
  require_paths:
279
331
  - lib
@@ -288,8 +340,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
288
340
  - !ruby/object:Gem::Version
289
341
  version: '0'
290
342
  requirements: []
291
- rubygems_version: 3.5.9
292
- signing_key:
343
+ rubygems_version: 4.0.17
293
344
  specification_version: 4
294
345
  summary: Create QuickBooks Online SalesReceipts for purchased effective orders
295
346
  test_files: []