workarea-authorize_cim 2.1.1

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.
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