qbo_rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +23 -0
  4. data/lib/generators/qbo_rails/install/install_generator.rb +32 -0
  5. data/lib/generators/qbo_rails/install/templates/config/qbo_rails.rb +17 -0
  6. data/lib/generators/qbo_rails/install/templates/db/migrate/create_qbo_errors.rb +12 -0
  7. data/lib/generators/qbo_rails/install/templates/models/qbo_error.rb +3 -0
  8. data/lib/generators/qbo_rails/install/usage.md +3 -0
  9. data/lib/qbo_rails.rb +106 -0
  10. data/lib/qbo_rails/error_handler.rb +4 -0
  11. data/lib/qbo_rails/version.rb +3 -0
  12. data/lib/tasks/qbo_rails_tasks.rake +4 -0
  13. data/spec/dummy/README.rdoc +28 -0
  14. data/spec/dummy/Rakefile +6 -0
  15. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  16. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  17. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  18. data/spec/dummy/app/controllers/settings_controller.rb +33 -0
  19. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  20. data/spec/dummy/app/models/account.rb +2 -0
  21. data/spec/dummy/app/models/customer.rb +2 -0
  22. data/spec/dummy/app/models/qbo_error.rb +3 -0
  23. data/spec/dummy/app/services/qbo_rails.rb +15 -0
  24. data/spec/dummy/app/services/qbo_rails/error_handler.rb +32 -0
  25. data/spec/dummy/app/views/layouts/application.html.erb +26 -0
  26. data/spec/dummy/app/views/settings/close_and_redirect.html.erb +14 -0
  27. data/spec/dummy/app/views/settings/index.html.erb +29 -0
  28. data/spec/dummy/bin/bundle +3 -0
  29. data/spec/dummy/bin/rails +8 -0
  30. data/spec/dummy/bin/rake +8 -0
  31. data/spec/dummy/bin/setup +29 -0
  32. data/spec/dummy/config.ru +4 -0
  33. data/spec/dummy/config/application.rb +41 -0
  34. data/spec/dummy/config/boot.rb +5 -0
  35. data/spec/dummy/config/database.yml +25 -0
  36. data/spec/dummy/config/environment.rb +5 -0
  37. data/spec/dummy/config/environments/development.rb +41 -0
  38. data/spec/dummy/config/environments/production.rb +79 -0
  39. data/spec/dummy/config/environments/test.rb +42 -0
  40. data/spec/dummy/config/initializers/assets.rb +11 -0
  41. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  42. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  43. data/spec/dummy/config/initializers/dotenv.rb +3 -0
  44. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  45. data/spec/dummy/config/initializers/inflections.rb +16 -0
  46. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  47. data/spec/dummy/config/initializers/qbo_rails.rb +17 -0
  48. data/spec/dummy/config/initializers/session_store.rb +3 -0
  49. data/spec/dummy/config/initializers/webmock.rb +2 -0
  50. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  51. data/spec/dummy/config/locales/en.yml +23 -0
  52. data/spec/dummy/config/routes.rb +5 -0
  53. data/spec/dummy/config/secrets.yml +22 -0
  54. data/spec/dummy/db/development.sqlite3 +0 -0
  55. data/spec/dummy/db/migrate/20150319132530_create_accounts.rb +12 -0
  56. data/spec/dummy/db/migrate/20150319132650_create_customers.rb +17 -0
  57. data/spec/dummy/db/migrate/20150328125013_add_qbo_id_to_customer.rb +5 -0
  58. data/spec/dummy/db/migrate/20150418134516_create_qbo_errors.rb +12 -0
  59. data/spec/dummy/db/production.sqlite3 +0 -0
  60. data/spec/dummy/db/schema.rb +50 -0
  61. data/spec/dummy/db/test.sqlite3 +0 -0
  62. data/spec/dummy/log/development.log +584 -0
  63. data/spec/dummy/log/production.log +0 -0
  64. data/spec/dummy/log/test.log +6502 -0
  65. data/spec/dummy/public/404.html +67 -0
  66. data/spec/dummy/public/422.html +67 -0
  67. data/spec/dummy/public/500.html +66 -0
  68. data/spec/dummy/public/favicon.ico +0 -0
  69. data/spec/dummy/tmp/cache/222/7E0/token +2 -0
  70. data/spec/dummy/tmp/cache/assets/development/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
  71. data/spec/dummy/tmp/cache/assets/development/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
  72. data/spec/dummy/tmp/cache/assets/development/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
  73. data/spec/dummy/tmp/cache/assets/development/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
  74. data/spec/dummy/tmp/cache/assets/development/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
  75. data/spec/dummy/tmp/cache/assets/development/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
  76. data/spec/factories/accounts.rb +8 -0
  77. data/spec/factories/customers.rb +15 -0
  78. data/spec/rails_helper.rb +92 -0
  79. data/spec/requests/qbo_rails_spec.rb +118 -0
  80. data/spec/spec_helper.rb +86 -0
  81. data/spec/vcr/qbo_rails/auth/forbidden.yml +119 -0
  82. data/spec/vcr/qbo_rails/customer/create.yml +75 -0
  83. data/spec/vcr/qbo_rails/customer/create_and_then_delete.yml +225 -0
  84. data/spec/vcr/qbo_rails/customer/delete.yml +167 -0
  85. data/spec/vcr/qbo_rails/customer/error_customer_in_use.yml +257 -0
  86. data/spec/vcr/qbo_rails/customer/update.yml +138 -0
  87. metadata +358 -0
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/404.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The page you were looking for doesn't exist.</h1>
62
+ <p>You may have mistyped the address or the page may have moved.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/422.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The change you wanted was rejected.</h1>
62
+ <p>Maybe you tried to change something you didn't have access to.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/500.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>We're sorry, but something went wrong.</h1>
62
+ </div>
63
+ <p>If you are the application owner check the logs for more information.</p>
64
+ </div>
65
+ </body>
66
+ </html>
File without changes
@@ -0,0 +1,2 @@
1
+ o: ActiveSupport::Cache::Entry: @valueo:OAuth::RequestToken : @tokenI"5qyprdsbsHPZNViCb0vqEioyKMfQLD65wB9koW9KLjPysIphH:ET: @secretI"-BLa09K3SI9qESOgWJJCi22mIwboXQoso7MtjtRbW; T:@consumerU:OAuth::Consumer{:keyI"#qyprdhobqczMYO48EHGKyw40TRExem; T: secretI"-dPeYy2UNbMzyaRnNKpFX6bw6PIl6scQU06M07k0I; T: options{:signature_methodI"HMAC-SHA1; T:request_token_pathI" /oauth/v1/get_request_token; T:authorize_pathI"/oauth/authorize; T:access_token_pathI"/oauth/v1/get_access_token; T:
2
+ proxy0: scheme: header:http_method: post:oauth_versionI"1.0; T: siteI"https://oauth.intuit.com; T:authorize_urlI"/https://appcenter.intuit.com/Connect/Begin; T: @params{ :oauth_token_secret@I"oauth_token_secret; T@:oauth_callback_confirmedI" true; TI"oauth_callback_confirmed; T@:oauth_token@I"oauth_token; T@:@created_atf1438085447.409708:@expires_in0
@@ -0,0 +1,8 @@
1
+ FactoryGirl.define do
2
+ factory :account do
3
+ company_name 'Nelson Hockey Club'
4
+ qb_token Rails.application.secrets.qbo_sandbox_access_token
5
+ qb_secret Rails.application.secrets.qbo_sandbox_access_token_secret
6
+ qb_company_id Rails.application.secrets.qbo_sandbox_company_id
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ FactoryGirl.define do
4
+ factory :customer do
5
+ firstname "Emer"
6
+ lastname "Rackowski"
7
+ email "erack@mail.com"
8
+ phone "333-444-5555"
9
+ mobile "666-777-8888"
10
+ address_1 "1 East Haskins Road"
11
+ city "Nelson"
12
+ state "NH"
13
+ zip "03457-4555"
14
+ end
15
+ end
@@ -0,0 +1,92 @@
1
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
2
+ ENV["RAILS_ENV"] ||= 'test'
3
+ require 'spec_helper'
4
+ require File.expand_path("../dummy/config/environment", __FILE__)
5
+ require 'rspec/rails'
6
+ require 'factory_girl_rails'
7
+ require 'vcr'
8
+
9
+ VCR.configure do |config|
10
+ config.cassette_library_dir = File.expand_path("../vcr", __FILE__)
11
+ config.hook_into :webmock
12
+ s = Rails.application.secrets
13
+ config.filter_sensitive_data('<ACCESS_TOKEN>') { URI.encode_www_form_component(s.qbo_sandbox_access_token) }
14
+ config.filter_sensitive_data('<CONSUMER_KEY>') { URI.encode_www_form_component(s.qbo_app_consumer_key) }
15
+ config.filter_sensitive_data('<COMPANY_ID>') { URI.encode_www_form_component(s.qbo_sandbox_company_id) }
16
+
17
+ uri_matcher = VCR.request_matchers[:uri]
18
+ # Don't check sandbox company id or trailing URL id
19
+ # This enables multiple different sandboxes to be used
20
+ # in testing
21
+ config.register_request_matcher(:for_intuit) do |req_1, req_2|
22
+ uri1, uri2 = req_1.uri, req_2.uri
23
+ if uri1 =~ /intuit\.com/ && uri2 =~ /intuit\.com/
24
+ strip_url_company_id(req_1.uri, req_2.uri)
25
+ regexp_trail_id = %r(/\d+/?\z)
26
+ if uri1.match(regexp_trail_id)
27
+ r1_without_id = uri1.gsub(regexp_trail_id, "")
28
+ r2_without_id = uri2.gsub(regexp_trail_id, "")
29
+ uri1.match(regexp_trail_id) && uri2.match(regexp_trail_id) && r1_without_id == r2_without_id
30
+ else
31
+ uri_matcher.matches?(req_1, req_2)
32
+ end
33
+ else
34
+ uri_matcher.matches?(req_1, req_2)
35
+ end
36
+ end
37
+
38
+ def strip_url_company_id(*args)
39
+ regexp_company_id = %r((company)/\d+/?)
40
+ args.each do |ref|
41
+ ref.sub!(regexp_company_id, '')
42
+ end
43
+ end
44
+ config.default_cassette_options = { match_requests_on: [:method, :for_intuit] }
45
+ end
46
+
47
+ # Add additional requires below this line. Rails is not loaded until this point!
48
+
49
+ # Requires supporting ruby files with custom matchers and macros, etc, in
50
+ # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
51
+ # run as spec files by default. This means that files in spec/support that end
52
+ # in _spec.rb will both be required and run as specs, causing the specs to be
53
+ # run twice. It is recommended that you do not name files matching this glob to
54
+ # end with _spec.rb. You can configure this pattern with the --pattern
55
+ # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
56
+ #
57
+ # The following line is provided for convenience purposes. It has the downside
58
+ # of increasing the boot-up time by auto-requiring all files in the support
59
+ # directory. Alternatively, in the individual `*_spec.rb` files, manually
60
+ # require only the support files necessary.
61
+ #
62
+ # Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
63
+
64
+ # Checks for pending migrations before tests are run.
65
+ # If you are not using ActiveRecord, you can remove this line.
66
+ ActiveRecord::Migration.maintain_test_schema!
67
+
68
+ RSpec.configure do |config|
69
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
70
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
71
+ config.include FactoryGirl::Syntax::Methods
72
+
73
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
74
+ # examples within a transaction, remove the following line or assign false
75
+ # instead of true.
76
+ config.use_transactional_fixtures = true
77
+
78
+ # RSpec Rails can automatically mix in different behaviours to your tests
79
+ # based on their file location, for example enabling you to call `get` and
80
+ # `post` in specs under `spec/controllers`.
81
+ #
82
+ # You can disable this behaviour by removing the line below, and instead
83
+ # explicitly tag your specs with their type, e.g.:
84
+ #
85
+ # RSpec.describe UsersController, :type => :controller do
86
+ # # ...
87
+ # end
88
+ #
89
+ # The different available types are documented in the features, such as in
90
+ # https://relishapp.com/rspec/rspec-rails/docs
91
+ config.infer_spec_type_from_file_location!
92
+ end
@@ -0,0 +1,118 @@
1
+ require 'rails_helper'
2
+
3
+ describe 'QBO request' do
4
+
5
+ describe '.create_or_update' do
6
+ it 'for a basic create' do
7
+ account = create(:account)
8
+ record = create(:customer, firstname: 'Edward')
9
+ qbo_rails, qb_customer = map_to_qbo(account, record)
10
+ expect(record.qbo_id).to be_nil
11
+ VCR.use_cassette("qbo_rails/customer/create", record: :none) do
12
+ qbo_rails.create_or_update(record, qb_customer)
13
+ record.reload
14
+ expect(record.qbo_id).to_not be_nil
15
+ expect(qbo_rails.result.sync_token).to eq 0
16
+ end
17
+ end
18
+
19
+ it 'for a basic update' do
20
+ account = create(:account)
21
+ # Get the qbo_id from spec above
22
+ record = create(:customer, firstname: 'Edward', qbo_id: 60)
23
+ qbo_rails, qb_customer = map_to_qbo(account, record)
24
+ #Quickbooks.log = true
25
+ VCR.use_cassette("qbo_rails/customer/update", record: :none) do
26
+ result = qbo_rails.create_or_update(record, qb_customer)
27
+ expect(result.sync_token).to be > 0
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ describe '.delete' do
34
+ # At this time you can't delete (or really make inactive) builtin entities
35
+ # in the the sandbox. So it returns a 400, bad request
36
+ it 'a sandbox customer record, Paulsen Medical Supplies, id 18' do
37
+ account = create(:account)
38
+ qbo_rails = QboRails.new(account, :customer)
39
+ #Quickbooks.log = true
40
+ VCR.use_cassette("qbo_rails/customer/delete", record: :none) do
41
+ expect{ result = qbo_rails.delete(18) }.to change{ QboError.count }.by(1)
42
+ end
43
+ end
44
+
45
+ it 'deletes a newly created customer' do
46
+ account = create(:account)
47
+ record = create(:customer, firstname: 'Mathis')
48
+ qbo_rails, qb_customer = map_to_qbo(account, record)
49
+ #Quickbooks.log = true
50
+ VCR.use_cassette("qbo_rails/customer/create_and_then_delete", record: :none) do
51
+ qbo_rails.create_or_update(record, qb_customer)
52
+ qbo_rails.delete(record.reload)
53
+ expect(Nokogiri::XML(qbo_rails.result.to_xml.to_s).at('Active').content).to eq 'false'
54
+ end
55
+ end
56
+ end
57
+
58
+ describe QboRails::ErrorHandler do
59
+
60
+ # You can see this is error in spec/dummy/app/service/qbo_rails/error_handler.rb
61
+ it 'initial request is an add request for existing customer that fails. Catch error
62
+ and resend request as an update' do
63
+ account = create(:account)
64
+ record = create(:customer)
65
+ qbo_rails, qb_customer = map_to_qbo(account, record)
66
+ expect(record.qbo_id).to be_nil
67
+ #Quickbooks.log = true
68
+ VCR.use_cassette("qbo_rails/customer/error_customer_in_use", record: :none) do
69
+ expect { qbo_rails.create_or_update(record, qb_customer) }.to change{ QboError.count }.by(0)
70
+ record.reload
71
+ expect(record.qbo_id).to_not be_nil
72
+ expect(qbo_rails.result.id).to eq record.qbo_id.to_s
73
+ expect(qbo_rails.error).to be_nil
74
+ end
75
+ end
76
+
77
+ it 'does not match a custom error handler and records error' do
78
+ account = create(:account)
79
+ record = create(:customer)
80
+ qbo_rails, qb_customer = map_to_qbo(account, record)
81
+ qb_customer.display_name = 'Emer:Rack'
82
+ #Quickbooks.log = true
83
+ VCR.use_cassette("qbo_rails/customer/error_customer_bad_display_name", record: :none) do
84
+ expect{ qbo_rails.create_or_update(record, qb_customer) }.to change{ QboError.count }.by(1)
85
+ expect(qbo_rails.error).to_not be_nil
86
+ end
87
+ end
88
+
89
+ # You can see this is error in spec/dummy/app/service/qbo_rails/error_handler.rb
90
+ it 'runs a custom error handler and verifies that @only_run_once prevents endless looping' do
91
+ account = create(:account)
92
+ account.qb_token = nil
93
+ account.qb_secret = nil
94
+ record = create(:customer)
95
+ qbo_rails, qb_customer = map_to_qbo(account, record)
96
+ expect(qbo_rails.only_run_once).to be nil
97
+ #Quickbooks.log = true
98
+ VCR.use_cassette("qbo_rails/auth/forbidden", record: :none) do
99
+ qbo_rails.create_or_update(record, qb_customer)
100
+ expect(qbo_rails.only_run_once).to be true
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ def map_to_qbo(account, record)
107
+ qbo_rails = QboRails.new(account, :customer)
108
+ qb_customer = qbo_rails.base.qr_model(:customer)
109
+ qb_customer.display_name = "#{record.firstname} #{record.lastname}"
110
+ address = qbo_rails.base.qr_model :physical_address
111
+ address.line1 = record.address_1
112
+ address.city = record.city
113
+ address.country_sub_division_code = record.state
114
+ address.postal_code = record.zip
115
+ qb_customer.billing_address = address
116
+ [qbo_rails, qb_customer]
117
+ end
118
+ end