workarea-authorize_cim 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +20 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. data/.gitignore +13 -0
  7. data/.rails-rubocop.yml +130 -0
  8. data/.rubocop.yml +13 -0
  9. data/CHANGELOG.md +90 -0
  10. data/CODE_OF_CONDUCT.md +3 -0
  11. data/CONTRIBUTING.md +3 -0
  12. data/Gemfile +12 -0
  13. data/LICENSE +52 -0
  14. data/LICENSE.md +3 -0
  15. data/README.md +40 -0
  16. data/Rakefile +50 -0
  17. data/app/errors/workarea/payment/create_profile_error.rb +19 -0
  18. data/app/models/workarea/payment.decorator +14 -0
  19. data/app/models/workarea/payment/authorize/credit_card.decorator +61 -0
  20. data/app/models/workarea/payment/capture/credit_card.decorator +38 -0
  21. data/app/models/workarea/payment/profile.decorator +169 -0
  22. data/app/models/workarea/payment/purchase/credit_card.decorator +61 -0
  23. data/app/models/workarea/payment/refund.decorator +19 -0
  24. data/app/models/workarea/payment/refund/credit_card.decorator +39 -0
  25. data/app/models/workarea/payment/store_credit_card.decorator +122 -0
  26. data/app/models/workarea/payment/tender/credit_card.decorator +25 -0
  27. data/app/models/workarea/payment/transaction.decorator +16 -0
  28. data/bin/rails +17 -0
  29. data/bin/rake +17 -0
  30. data/bin/rspec +17 -0
  31. data/bin/rubocop +17 -0
  32. data/config/initializers/configuration.rb +3 -0
  33. data/lib/active_merchant/billing/bogus_authorize_net_cim_gateway.rb +99 -0
  34. data/lib/workarea/authorize_cim.rb +47 -0
  35. data/lib/workarea/authorize_cim/engine.rb +10 -0
  36. data/lib/workarea/authorize_cim/error.rb +12 -0
  37. data/lib/workarea/authorize_cim/version.rb +7 -0
  38. data/test/dummy/Rakefile +6 -0
  39. data/test/dummy/app/assets/config/manifest.js +4 -0
  40. data/test/dummy/app/assets/images/.keep +0 -0
  41. data/test/dummy/app/assets/javascripts/application.js +13 -0
  42. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  43. data/test/dummy/app/controllers/application_controller.rb +3 -0
  44. data/test/dummy/app/controllers/concerns/.keep +0 -0
  45. data/test/dummy/app/helpers/application_helper.rb +2 -0
  46. data/test/dummy/app/jobs/application_job.rb +2 -0
  47. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  48. data/test/dummy/app/models/concerns/.keep +0 -0
  49. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  50. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  51. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  52. data/test/dummy/bin/bundle +3 -0
  53. data/test/dummy/bin/rails +4 -0
  54. data/test/dummy/bin/rake +4 -0
  55. data/test/dummy/bin/setup +34 -0
  56. data/test/dummy/bin/update +29 -0
  57. data/test/dummy/config.ru +5 -0
  58. data/test/dummy/config/application.rb +20 -0
  59. data/test/dummy/config/boot.rb +5 -0
  60. data/test/dummy/config/cable.yml +9 -0
  61. data/test/dummy/config/environment.rb +5 -0
  62. data/test/dummy/config/environments/development.rb +56 -0
  63. data/test/dummy/config/environments/production.rb +86 -0
  64. data/test/dummy/config/environments/test.rb +43 -0
  65. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  66. data/test/dummy/config/initializers/assets.rb +11 -0
  67. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  68. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  69. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  70. data/test/dummy/config/initializers/inflections.rb +16 -0
  71. data/test/dummy/config/initializers/mime_types.rb +4 -0
  72. data/test/dummy/config/initializers/new_framework_defaults.rb +21 -0
  73. data/test/dummy/config/initializers/session_store.rb +3 -0
  74. data/test/dummy/config/initializers/workarea.rb +5 -0
  75. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  76. data/test/dummy/config/locales/en.yml +23 -0
  77. data/test/dummy/config/puma.rb +47 -0
  78. data/test/dummy/config/routes.rb +5 -0
  79. data/test/dummy/config/secrets.yml +22 -0
  80. data/test/dummy/config/spring.rb +6 -0
  81. data/test/dummy/db/seeds.rb +3 -0
  82. data/test/dummy/lib/assets/.keep +0 -0
  83. data/test/dummy/log/.keep +0 -0
  84. data/test/dummy/public/404.html +67 -0
  85. data/test/dummy/public/422.html +67 -0
  86. data/test/dummy/public/500.html +66 -0
  87. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  88. data/test/dummy/public/apple-touch-icon.png +0 -0
  89. data/test/dummy/public/favicon.ico +0 -0
  90. data/test/factories/workarea/factories/authorize_cim_factory.rb +14 -0
  91. data/test/models/workarea/payment/authorize/credit_card_test.decorator +17 -0
  92. data/test/models/workarea/payment/authorize_cim_integration_test.rb +137 -0
  93. data/test/models/workarea/payment/capture/credit_card_test.decorator +80 -0
  94. data/test/models/workarea/payment/credit_card_integration_test.decorator +30 -0
  95. data/test/models/workarea/payment/profile_test.rb +60 -0
  96. data/test/models/workarea/payment/purchase/credit_card_test.decorator +17 -0
  97. data/test/models/workarea/payment/refund/credit_card_test.decorator +95 -0
  98. data/test/models/workarea/payment/refund_test.decorator +14 -0
  99. data/test/models/workarea/payment/store_credit_card_test.decorator +7 -0
  100. data/test/services/workarea/cancel_order_test.decorator +55 -0
  101. data/test/support/workarea/authorize_cim_gateway_duplicate_window_patch.rb +14 -0
  102. data/test/support/workarea/authorize_cim_gateway_vcr_config.rb +22 -0
  103. data/test/support/workarea/workarea_3_2_backports.rb +57 -0
  104. data/test/system/workarea/storefront/orders_system_test.decorator +28 -0
  105. data/test/test_helper.rb +17 -0
  106. data/test/vcr_cassettes/authorize_net_cim/auth_capture.yml +435 -0
  107. data/test/vcr_cassettes/authorize_net_cim/auth_void.yml +436 -0
  108. data/test/vcr_cassettes/authorize_net_cim/purchase_void.yml +436 -0
  109. data/test/vcr_cassettes/authorize_net_cim/store_auth.yml +371 -0
  110. data/test/vcr_cassettes/authorize_net_cim/store_purchase.yml +371 -0
  111. data/test/vcr_cassettes/credit_card/auth_capture.yml +438 -0
  112. data/test/vcr_cassettes/credit_card/auth_void.yml +439 -0
  113. data/test/vcr_cassettes/credit_card/purchase_void.yml +439 -0
  114. data/test/vcr_cassettes/credit_card/store_auth.yml +374 -0
  115. data/test/vcr_cassettes/credit_card/store_purchase.yml +374 -0
  116. data/test/workers/workarea/send_refund_email_test.decorator +70 -0
  117. data/workarea-authorize_cim.gemspec +27 -0
  118. metadata +187 -0
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ require "rdoc/task"
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = "rdoc"
11
+ rdoc.title = "Category Overview Content Block"
12
+ rdoc.options << "--line-numbers"
13
+ rdoc.rdoc_files.include("README.md")
14
+ rdoc.rdoc_files.include("lib/**/*.rb")
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+ load 'rails/tasks/statistics.rake'
20
+ load "workarea/changelog.rake"
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |t|
24
+ t.libs << 'lib'
25
+ t.libs << 'test'
26
+ t.pattern = 'test/**/*_test.rb'
27
+ t.verbose = false
28
+ end
29
+ task default: :test
30
+
31
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
32
+ require 'workarea/authorize_cim/version'
33
+
34
+ desc "Release version #{Workarea::AuthorizeCim::VERSION} of the gem"
35
+ task :release do
36
+ host = "https://#{ENV['BUNDLE_GEMS__WEBLINC__COM']}@gems.weblinc.com"
37
+
38
+ #Rake::Task["workarea:changelog"].execute
39
+ #system "git add CHANGELOG.md"
40
+ #system 'git commit -m "Update CHANGELOG"'
41
+ #system "git push origin HEAD"
42
+
43
+ system "git tag -a v#{Workarea::AuthorizeCim::VERSION} -m 'Tagging #{Workarea::AuthorizeCim::VERSION}'"
44
+ system 'git push --tags'
45
+
46
+ system 'gem build workarea-authorize_cim.gemspec'
47
+ system "gem push workarea-authorize_cim-#{Workarea::AuthorizeCim::VERSION}.gem"
48
+ system "gem push workarea-authorize_cim-#{Workarea::AuthorizeCim::VERSION}.gem --host #{host}"
49
+ system "rm workarea-authorize_cim-#{Workarea::AuthorizeCim::VERSION}.gem"
50
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workarea
4
+ class Payment
5
+ # Thrown when Authorize.net can't create the payment profile for
6
+ # some reason. This is typically caught and hidden from erroring out
7
+ # to the user, instead preferring to notify the developers of the
8
+ # issue via a reporting service.
9
+ #
10
+ # The web application will defer to the frontend to actually report
11
+ # this error to the user.
12
+ class CreateProfileError < StandardError
13
+ def initialize(message, parameters: {})
14
+ @parameters = parameters
15
+ super "Payment profile could not be created: #{message}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ module Workarea
3
+ decorate Payment, with: :cim do
4
+ def successful_captures
5
+ transactions.select { |t| t.success? && (t.capture? || t.purchase?) }
6
+ end
7
+
8
+ def eligible_for_refund?
9
+ return true unless credit_card? && successful_captures.present?
10
+
11
+ successful_captures.first.created_at < Time.now - 24.hours
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ module Workarea
3
+ decorate Payment::Authorize::CreditCard, with: :cim do
4
+ def complete!
5
+ return unless Workarea::Payment::StoreCreditCard.new(tender, options).save!
6
+
7
+ transaction.response = handle_active_merchant_errors do
8
+ gateway.create_customer_profile_transaction(auth_args)
9
+ end
10
+ end
11
+
12
+ def cancel!
13
+ return unless transaction.success?
14
+
15
+ transaction.cancellation = handle_active_merchant_errors do
16
+ gateway.create_customer_profile_transaction(void_args)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def auth_args
23
+ {
24
+ transaction: {
25
+ type: :auth_only,
26
+ customer_profile_id: customer_profile_id,
27
+ customer_payment_profile_id: customer_payment_profile_id,
28
+ amount: auth_amount,
29
+ order: {
30
+ invoice_number: tender.payment.id.first(20) # auth net has max length 20 for this field
31
+ }
32
+ }
33
+ }
34
+ end
35
+
36
+ def void_args
37
+ {
38
+ transaction: {
39
+ type: :void,
40
+ customer_profile_id: customer_profile_id,
41
+ customer_payment_profile_id: customer_payment_profile_id,
42
+ trans_id: transaction.response.authorization
43
+ }
44
+ }
45
+ end
46
+
47
+ def customer_profile_id
48
+ tender.gateway_profile_id
49
+ end
50
+
51
+ def customer_payment_profile_id
52
+ tender.token
53
+ end
54
+
55
+ # cim requeires dollar amount (not cents)
56
+ # eg $4.25
57
+ def auth_amount
58
+ tender.amount.cents / 100.00
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ module Workarea
3
+ decorate Payment::Capture::CreditCard, with: :cim do
4
+ def complete!
5
+ validate_reference!
6
+
7
+ transaction.response = handle_active_merchant_errors do
8
+ gateway.create_customer_profile_transaction(capture_args)
9
+ end
10
+ end
11
+
12
+ def cancel!
13
+ # noop, can't cancel a capture
14
+ end
15
+
16
+ private
17
+
18
+ def capture_args
19
+ {
20
+ transaction: {
21
+ type: :prior_auth_capture,
22
+ amount: auth_amount,
23
+ trans_id: transaction_ref_id
24
+ }
25
+ }
26
+ end
27
+
28
+ def transaction_ref_id
29
+ transaction.reference.response.params['direct_response']['transaction_id']
30
+ end
31
+
32
+ # cim requires dollar amount (not cents)
33
+ # eg $4.25
34
+ def auth_amount
35
+ transaction.amount.to_s.to_f
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+ module Workarea
3
+ decorate Payment::Profile, with: :cim do
4
+ decorated do
5
+ before_validation :create_gateway_profile, if: :needs_gateway_profile?, on: :create
6
+ before_destroy :delete_gateway_profile, if: :on_gateway?
7
+ end
8
+
9
+ protected
10
+
11
+ # Create a new payment profile on the gateway, and log an error when
12
+ # this cannot be accomplished. First, this checks to make sure a
13
+ # +Payment::Profile+ doesn't already exist locally for the current
14
+ # email. If that is true, we attempt to either find an existing
15
+ # profile on Authorize.net or create a new one. Eventually,
16
+ # +gateway_id+ should be set to something after this method is
17
+ # called, and if not, a validation error is logged.
18
+ #
19
+ # @protected
20
+ # @return [Boolean] +true+
21
+ def create_gateway_profile
22
+ self.gateway_id = previous_profile_id || remote_profile_id
23
+ errors.add :gateway, 'error occurred' unless gateway_id.present?
24
+ end
25
+
26
+ # When destroying this record, also delete the payment profile from
27
+ # Authorize.net.
28
+ #
29
+ # @protected
30
+ # @return [Boolean] whether the operation was successful
31
+ def delete_gateway_profile
32
+ gateway.delete_customer_profile(
33
+ customer_profile_id: gateway_id
34
+ ).tap do |response|
35
+ errors.add :response, response.inspect unless response.success?
36
+ end.success?
37
+ end
38
+
39
+ # Test whether we need to ask Authorize.Net for a payment profile.
40
+ #
41
+ # @protected
42
+ # @return [Boolean] whether we need a payment profile
43
+ def needs_gateway_profile?
44
+ email.present? && gateway_id.blank?
45
+ end
46
+
47
+ # Test whether we have a gateway ID.
48
+ #
49
+ # @protected
50
+ # @return [Boolean]
51
+ def on_gateway?
52
+ gateway_id.present?
53
+ end
54
+
55
+ private
56
+
57
+ # The credit card gateway we're using to communicate with
58
+ # Authorize.Net
59
+ #
60
+ # @private
61
+ # @return [ActiveMerchant::Billing::Gateway]
62
+ def gateway
63
+ Workarea.config.gateways.credit_card
64
+ end
65
+
66
+ # Find an existing +Payment::Profile+ record with a gateway_id for
67
+ # this User, and use it in this record to save a roundtrip to
68
+ # Authorize.Net.
69
+ #
70
+ # @private
71
+ # @return [String] or +nil+ if none can be found.
72
+ def previous_profile_id
73
+ @ppid ||= Workarea::Payment::Profile.where(
74
+ email: email,
75
+ :gateway_id.ne => nil
76
+ ).pluck(:gateway_id).first
77
+ end
78
+
79
+ # Attempt to create a new customer profile on Authorize.Net, either
80
+ # by doing it outright or going through the duplicate payment
81
+ # profile motions.
82
+ #
83
+ # @private
84
+ # @return [String] The new payment profile ID from Authorize.net or
85
+ # +nil+ if it cannot be created.
86
+ def remote_profile_id
87
+ response = gateway.create_customer_profile(profile: { email: email })
88
+ return response.params['customer_profile_id'] if response.success?
89
+ return duplicate_payment_profile_id(response) if duplicate_profile?(response)
90
+ raise Payment::CreateProfileError.new(
91
+ response.message,
92
+ parameters: {
93
+ email: email,
94
+ response: debug_response(response)
95
+ }
96
+ )
97
+ end
98
+
99
+ # Attempt to pull the existing customer profile Authorize.Net using
100
+ # the original response's message and another API call.
101
+ #
102
+ # @private
103
+ # @param original_response [ActiveMerchant::Billing::Response]
104
+ # @return [String] Existing profile ID for this user
105
+ # @raise [Payment::CreateProfileError] if still can't be created.
106
+ def duplicate_payment_profile_id(original_response)
107
+ gateway_id = get_id_from_message(original_response)
108
+ response = gateway.get_customer_profile(customer_profile_id: gateway_id)
109
+
110
+ if response.success? && email_match?(response)
111
+ gateway_id
112
+ else
113
+ raise Payment::CreateProfileError, response.message
114
+ end
115
+ end
116
+
117
+ # Test whether the email in this response matches the email in the
118
+ # +Payment::Profile+ we're trying to create, so as not to
119
+ # accidentally assign the wrong gateway_id to the wrong user.
120
+ #
121
+ # @private
122
+ # @param response [ActiveMerchant::Billing::Response]
123
+ # @return [Boolean] whether the email matches
124
+ def email_match?(response)
125
+ response.params['profile']['email'] == email
126
+ end
127
+
128
+ # Test whether the response's error code matches the duplicate
129
+ # profile creation error code of +E00039+, which triggers a lookup
130
+ # in the error message of the payment profile ID.
131
+ #
132
+ # @private
133
+ # @param response [ActiveMerchant::Billing::Response]
134
+ # @return [Boolean] whether the error code matches E00039
135
+ def duplicate_profile?(response)
136
+ response.params['messages'].try(:[], 'message').try(:[], 'code') == 'E00039'
137
+ end
138
+
139
+ # Attempt to parse out the gateway ID from the error message that is
140
+ # given for duplicate record creation.
141
+ #
142
+ # @private
143
+ # @param response [ActiveMerchant::Billing::Response]
144
+ # @return [String] ID of the existing profile.
145
+ def get_id_from_message(response)
146
+ text = response.params['messages']['message']['text']
147
+ return unless text.present?
148
+ text.match(/A duplicate record with ID (\d+) already exists/)[1]
149
+ end
150
+
151
+ # Picks out the relevant pieces of the ActiveMerchant response for
152
+ # an exception object.
153
+ #
154
+ # @private
155
+ # @param response [ActiveMerchant::Billing::Response]
156
+ # @return [Hash]
157
+ def debug_response(response)
158
+ {
159
+ authorization: response.authorization,
160
+ avs_result: response.avs_result,
161
+ cvv_result: response.cvv_result,
162
+ error_code: response.error_code,
163
+ message: response.message,
164
+ params: response.params,
165
+ test: response.test
166
+ }
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ module Workarea
3
+ decorate Payment::Purchase::CreditCard, with: :cim do
4
+ def complete!
5
+ return unless Workarea::Payment::StoreCreditCard.new(tender, options).save!
6
+
7
+ transaction.response = handle_active_merchant_errors do
8
+ gateway.create_customer_profile_transaction(auth_capture_args)
9
+ end
10
+ end
11
+
12
+ def cancel!
13
+ return unless transaction.success?
14
+
15
+ transaction.cancellation = handle_active_merchant_errors do
16
+ gateway.create_customer_profile_transaction(void_args)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def auth_capture_args
23
+ {
24
+ transaction: {
25
+ type: :auth_capture,
26
+ customer_profile_id: customer_profile_id,
27
+ customer_payment_profile_id: customer_payment_profile_id,
28
+ amount: auth_amount,
29
+ order: {
30
+ invoice_number: tender.payment.id.first(20) # auth net has max length 20 for this field
31
+ }
32
+ }
33
+ }
34
+ end
35
+
36
+ def void_args
37
+ {
38
+ transaction: {
39
+ type: :void,
40
+ customer_profile_id: customer_profile_id,
41
+ customer_payment_profile_id: customer_payment_profile_id,
42
+ trans_id: transaction.response.authorization
43
+ }
44
+ }
45
+ end
46
+
47
+ def customer_profile_id
48
+ tender.gateway_profile_id
49
+ end
50
+
51
+ def customer_payment_profile_id
52
+ tender.token
53
+ end
54
+
55
+ # cim requeires dollar amount (not cents)
56
+ # eg $4.25
57
+ def auth_amount
58
+ tender.amount.to_s.to_f
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ module Workarea
2
+ decorate Payment::Refund, with: :cim do
3
+ decorated { validate :has_refundable_transactions }
4
+
5
+ private
6
+
7
+ def has_refundable_transactions
8
+ amounts_with_tenders.each do |tender, _amount|
9
+ next unless tender.slug == :credit_card
10
+
11
+ tender.transactions.successful.not_canceled.captures_or_purchased.each do |transaction|
12
+ if transaction.created_at >= Time.now - 24.hours
13
+ errors.add(:credit_card, "This transaction hasn't been settled yet, and isn't eligble for refunding")
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ module Workarea
3
+ decorate Payment::Refund::CreditCard, with: :cim do
4
+ def complete!
5
+ return false unless tender.valid_capture_date?
6
+
7
+ validate_reference!
8
+
9
+ transaction.response = handle_active_merchant_errors do
10
+ gateway.create_customer_profile_transaction_for_refund(options)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def options
17
+ {
18
+ transaction: {
19
+ customer_profile_id: customer_profile_id,
20
+ customer_payment_profile_id: customer_payment_profile_id,
21
+ amount: refund_amount,
22
+ trans_id: transaction.reference.response.params['direct_response']['transaction_id']
23
+ }
24
+ }
25
+ end
26
+
27
+ def refund_amount
28
+ transaction.amount.to_s.to_f
29
+ end
30
+
31
+ def customer_profile_id
32
+ tender.gateway_profile_id
33
+ end
34
+
35
+ def customer_payment_profile_id
36
+ tender.token
37
+ end
38
+ end
39
+ end