spree_emerchantpay_genesis 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE +21 -0
  4. data/README.md +269 -0
  5. data/Rakefile +50 -0
  6. data/app/assets/images/spree/emerchantpay_logo.png +0 -0
  7. data/app/assets/javascripts/spree/emerchantpay_threeds.js +126 -0
  8. data/app/assets/javascripts/spree/frontend/card.min.js +3 -0
  9. data/app/assets/stylesheets/spree/emerchantpay_threeds.css +40 -0
  10. data/app/assets/stylesheets/spree/frontend/card.css +35 -0
  11. data/app/controllers/spree/api/v2/storefront/checkout_controller_decorator.rb +66 -0
  12. data/app/controllers/spree/api/v2/storefront/emerchantpay_notification_controller.rb +47 -0
  13. data/app/controllers/spree/api/v2/storefront/emerchantpay_threeds_controller.rb +61 -0
  14. data/app/controllers/spree/emerchantpay_threeds_controller.rb +31 -0
  15. data/app/helpers/spree/admin/payment_methods_helper.rb +110 -0
  16. data/app/helpers/spree_emerchantpay_genesis/mappers/genesis.rb +332 -0
  17. data/app/helpers/spree_emerchantpay_genesis/mappers/order.rb +70 -0
  18. data/app/helpers/spree_emerchantpay_genesis/mappers/transaction.rb +45 -0
  19. data/app/helpers/spree_emerchantpay_genesis/threeds_helper.rb +116 -0
  20. data/app/helpers/spree_emerchantpay_genesis/transaction_helper.rb +200 -0
  21. data/app/models/spree/gateway/emerchantpay_direct.rb +66 -0
  22. data/app/models/spree/payment_decorator.rb +13 -0
  23. data/app/models/spree/payment_processing_decorator.rb +28 -0
  24. data/app/models/spree_emerchantpay_genesis/base/data.rb +33 -0
  25. data/app/models/spree_emerchantpay_genesis/base/gateway.rb +65 -0
  26. data/app/models/spree_emerchantpay_genesis/data/address.rb +23 -0
  27. data/app/models/spree_emerchantpay_genesis/data/provider.rb +23 -0
  28. data/app/models/spree_emerchantpay_genesis/data/user.rb +32 -0
  29. data/app/models/spree_emerchantpay_genesis/db/application_record.rb +10 -0
  30. data/app/models/spree_emerchantpay_genesis/db/emerchantpay_payment.rb +35 -0
  31. data/app/models/spree_emerchantpay_genesis/genesis_provider.rb +203 -0
  32. data/app/repositories/spree_emerchantpay_genesis/emerchantpay_payments_repository.rb +120 -0
  33. data/app/repositories/spree_emerchantpay_genesis/spree_order_repository.rb +51 -0
  34. data/app/repositories/spree_emerchantpay_genesis/spree_payments_repository.rb +58 -0
  35. data/app/services/spree/payments/create_decorator.rb +63 -0
  36. data/app/services/spree_emerchantpay_genesis/base/payment_service.rb +83 -0
  37. data/app/services/spree_emerchantpay_genesis/notifications/service_handler.rb +33 -0
  38. data/app/services/spree_emerchantpay_genesis/sources/create_credit_card.rb +47 -0
  39. data/app/services/spree_emerchantpay_genesis/threeds/callback.rb +62 -0
  40. data/app/services/spree_emerchantpay_genesis/threeds/method_continue.rb +57 -0
  41. data/app/views/layouts/spree/method_continue.html.erb +17 -0
  42. data/app/views/spree/admin/payments/source_views/_emerchantpay_direct.html.erb +80 -0
  43. data/app/views/spree/checkout/payment/_emerchantpay_direct.html.erb +51 -0
  44. data/app/views/spree/emerchantpay_threeds/method_continue.html.erb +26 -0
  45. data/config/locales/en.yml +45 -0
  46. data/config/routes.rb +24 -0
  47. data/db/migrate/20230927072418_add_emerchantpay_payments.rb +34 -0
  48. data/db/migrate/20231128153140_add_callback_status_to_emerchantpay_payments.rb +8 -0
  49. data/lib/generators/spree_emerchantpay_genesis/install/install_generator.rb +37 -0
  50. data/lib/spree_emerchantpay_genesis/engine.rb +38 -0
  51. data/lib/spree_emerchantpay_genesis/factories.rb +11 -0
  52. data/lib/spree_emerchantpay_genesis/version.rb +7 -0
  53. data/lib/spree_emerchantpay_genesis.rb +8 -0
  54. metadata +421 -0
@@ -0,0 +1,200 @@
1
+ module SpreeEmerchantpayGenesis
2
+ # Helper methods used in the transaction processing
3
+ class TransactionHelper # rubocop:disable Metrics/ClassLength
4
+
5
+ TRANSACTION_ID_PREFIX = 'sp-'.freeze
6
+ CAPTURE_ACTION = 'capture'.freeze
7
+ REFUND_ACTION = 'refund'.freeze
8
+ VOID_ACTION = 'void'.freeze
9
+
10
+ class << self
11
+
12
+ include Spree::Core::Engine.routes.url_helpers
13
+
14
+ # Generate Transaction Id
15
+ def generate_transaction_id
16
+ "#{TRANSACTION_ID_PREFIX}#{SecureRandom.uuid[TRANSACTION_ID_PREFIX.length..]}"[0..32].downcase
17
+ end
18
+
19
+ # Checks if the given request is asynchronous or not
20
+ def asyn?(genesis_request)
21
+ genesis_request.instance_of?(GenesisRuby::Api::Requests::Financial::Cards::Authorize3d) ||
22
+ genesis_request.instance_of?(GenesisRuby::Api::Requests::Financial::Cards::Sale3d)
23
+ end
24
+
25
+ # Check given response for success result
26
+ def success_result?(response)
27
+ response.approved? || async_result?(response)
28
+ end
29
+
30
+ # Check given response for asynchronous execution
31
+ def async_result?(response)
32
+ response.pending? || response.pending_async? || response.in_progress? || response.pending_hold?
33
+ end
34
+
35
+ # Check given response for Method Continue parameters
36
+ def threeds_secure?(response)
37
+ TransactionHelper.async_result?(response) && response.response_object&.key?(:threeds_method_url)
38
+ end
39
+
40
+ # Check given response for failure
41
+ def failure_result?(response)
42
+ response.error? || response.declined?
43
+ end
44
+
45
+ # Generate Spree Response from Gateway action
46
+ def generate_spree_response(gateway_response)
47
+ return build_spree_response gateway_response if gateway_response.instance_of? GenesisRuby::Api::Response
48
+ return build_failure_error gateway_response if gateway_response.is_a? StandardError
49
+
50
+ gateway_response
51
+ end
52
+
53
+ # Build message from the given response
54
+ def build_message(response)
55
+ result = response.response_object
56
+ message = ''
57
+
58
+ message = result[:message] unless result[:message].nil?
59
+ message = "#{message} (#{result[:technical_message]})" unless result[:technical_message].nil?
60
+
61
+ message
62
+ end
63
+
64
+ # Fetch the given string with Genesis Ruby transaction class
65
+ def fetch_genesis_transaction_class(transaction_type)
66
+ request_class = Mappers::Transaction.for transaction_type
67
+
68
+ raise "Invalid transaction type given for #{self.class}" if request_class.nil?
69
+
70
+ request_class
71
+ end
72
+
73
+ # Check if the given Genesis Response can be stored
74
+ def can_save_genesis_response?(response)
75
+ response_object = response.response_object
76
+
77
+ !(
78
+ response_object[:transaction_id].nil? ||
79
+ response_object[:transaction_type].nil? ||
80
+ response_object[:mode].nil?
81
+ )
82
+ end
83
+
84
+ # Fetch genesis_request_type from plugin options
85
+ def init_genesis_req(configuration, transaction_type)
86
+ request_class = TransactionHelper.fetch_genesis_transaction_class(transaction_type)
87
+
88
+ request_class&.new configuration
89
+ end
90
+
91
+ # Initialize Genesis client based on the action
92
+ def init_reference_req(action, configuration, transaction_type)
93
+ genesis_request = __send__("initialize_#{action}_client", configuration, transaction_type)
94
+
95
+ unless genesis_request
96
+ raise(GenesisRuby::Error, "Invalid #{action.capitalize} action for #{transaction_type.capitalize}")
97
+ end
98
+
99
+ genesis_request
100
+ end
101
+
102
+ # Init Notification object
103
+ def init_notification(configuration, params)
104
+ GenesisRuby::Api::Notification.new configuration, params
105
+ end
106
+
107
+ # Initialize Method Continue Transaction Request
108
+ def init_method_continue_req(configuration)
109
+ GenesisRuby::Api::Requests::Financial::Cards::Threeds::V2::MethodContinue.new configuration
110
+ end
111
+
112
+ # Fetch Redirect Url from Genesis Response
113
+ def fetch_redirect_url(options, response)
114
+ url = ''
115
+ url = options[:return_success_url] if TransactionHelper.success_result? response
116
+ url = options[:return_failure_url] if TransactionHelper.failure_result? response
117
+ url = response.response_object[:redirect_url] if response.response_object&.key? :redirect_url
118
+ url = build_threeds_secure_endpoint options, response if TransactionHelper.threeds_secure? response
119
+
120
+ { redirect_url: url }
121
+ end
122
+
123
+ # Generate Checksum from the response object
124
+ def generate_checksum(response_object)
125
+ Digest::MD5.hexdigest(
126
+ "#{response_object[:unique_id]}#{response_object[:amount]}#{response_object[:currency]}"
127
+ )
128
+ end
129
+
130
+ private
131
+
132
+ # Build Success or Failure Spree Response
133
+ def build_spree_response(response)
134
+ return build_success_response response if success_result? response
135
+
136
+ build_failure_response response
137
+ end
138
+
139
+ # Build Success Spree Response from GenesisRuby::Api::Response
140
+ def build_success_response(response)
141
+ ActiveMerchant::Billing::Response.new true, build_message(response), test: test_mode?(response)
142
+ end
143
+
144
+ # Build Failure Spree Response from GenesisRuby::Api:Response
145
+ def build_failure_response(response)
146
+ ActiveMerchant::Billing::Response.new false, build_message(response), test: test_mode?(response)
147
+ end
148
+
149
+ # Build Failure Spree Response from GenesisRuby::Error
150
+ def build_failure_error(error)
151
+ ActiveMerchant::Billing::Response.new false, error.message
152
+ end
153
+
154
+ # Check the given Genesis Response mode (test/production)
155
+ def test_mode?(response)
156
+ response&.response_object&.[](:mode) == 'test'
157
+ end
158
+
159
+ # Initialize Genesis Client with Capture request
160
+ def initialize_capture_client(configuration, payment_type)
161
+ return nil unless GenesisRuby::Utils::Transactions::References::CapturableTypes.allowed_reference? payment_type
162
+
163
+ init_genesis_req(
164
+ configuration,
165
+ GenesisRuby::Utils::Transactions::References::CapturableTypes.fetch_reference(payment_type)
166
+ )
167
+ end
168
+
169
+ # Initialize Genesis Client with Refund request
170
+ def initialize_refund_client(configuration, payment_type)
171
+ return nil unless GenesisRuby::Utils::Transactions::References::RefundableTypes.allowed_reference? payment_type
172
+
173
+ init_genesis_req(
174
+ configuration,
175
+ GenesisRuby::Utils::Transactions::References::RefundableTypes.fetch_reference(payment_type)
176
+ )
177
+ end
178
+
179
+ # Initialize Genesis with Void request
180
+ def initialize_void_client(configuration, payment_type)
181
+ return nil unless GenesisRuby::Utils::Transactions::References::VoidableTypes.allowed_reference? payment_type
182
+
183
+ init_genesis_req(
184
+ configuration,
185
+ GenesisRuby::Utils::Transactions::References::VoidableTypes.fetch_reference(payment_type)
186
+ )
187
+ end
188
+
189
+ # Build 3DSv2 Method Continue endpoint
190
+ def build_threeds_secure_endpoint(options, response)
191
+ response_object = response.response_object
192
+ checksum = generate_checksum response_object
193
+
194
+ "#{options[:hostname]}#{emerchantpay_threeds_form_path(response_object[:unique_id], checksum)}"
195
+ end
196
+
197
+ end
198
+
199
+ end
200
+ end
@@ -0,0 +1,66 @@
1
+ module Spree
2
+ # Emerchantpay Direct Payment Method
3
+ class Gateway::EmerchantpayDirect < SpreeEmerchantpayGenesis::Base::Gateway # rubocop:disable Style/ClassAndModuleChildren
4
+
5
+ preference :token, :string
6
+
7
+ delegate :load_data, :load_source, :load_payment, to: :provider
8
+
9
+ def method_type
10
+ 'emerchantpay_direct'
11
+ end
12
+
13
+ def provider_class
14
+ SpreeEmerchantpayGenesis::GenesisProvider
15
+ end
16
+
17
+ def provider
18
+ @provider = provider_class.new options if @provider.nil?
19
+
20
+ @provider
21
+ end
22
+
23
+ def purchase(_money_in_cents, source, gateway_options)
24
+ order, payment = order_data_from_options gateway_options
25
+ user = order.user
26
+
27
+ prepare_provider(
28
+ SpreeEmerchantpayGenesis::Mappers::Order.prepare_data(order, user, gateway_options),
29
+ source,
30
+ payment
31
+ )
32
+
33
+ provider.purchase
34
+ end
35
+
36
+ def authorize(money_in_cents, source, gateway_options)
37
+ purchase money_in_cents, source, gateway_options
38
+ end
39
+
40
+ def supports?(_source)
41
+ true
42
+ end
43
+
44
+ def source_required?
45
+ true
46
+ end
47
+
48
+ def payment_source_class
49
+ CreditCard
50
+ end
51
+
52
+ def auto_capture?
53
+ !GenesisRuby::Utils::Transactions::References::CapturableTypes.all.include? options[:transaction_types]
54
+ end
55
+
56
+ private
57
+
58
+ # Prepare provider
59
+ def prepare_provider(data, source, payment)
60
+ load_data SpreeEmerchantpayGenesis::Mappers::Order.for data
61
+ load_source source
62
+ load_payment payment
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ # Spree Payment Decorator
3
+ module PaymentDecorator
4
+
5
+ # Query all Genesis Gateway Payments
6
+ def emerchantpay_payments
7
+ SpreeEmerchantpayGenesis::EmerchantpayPaymentsRepository.find_all_by_order_and_payment order.number, number
8
+ end
9
+
10
+ end
11
+ end
12
+
13
+ ::Spree::Payment.prepend(Spree::PaymentDecorator)
@@ -0,0 +1,28 @@
1
+ module Spree
2
+ # Emerchantpay Payment Processing decorator
3
+ module PaymentProcessingDecorator
4
+
5
+ def gateway_action(source, action, success_state)
6
+ protect_from_connection_error do
7
+ response = payment_method.__send__ action, money.money.cents, source, gateway_options
8
+ success_state = fetch_state success_state, response, action
9
+ result = SpreeEmerchantpayGenesis::TransactionHelper.generate_spree_response response
10
+ handle_response(result, success_state, :failure)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ # Provide Spree success event method that will be executed
17
+ def fetch_state(current_state, response, action)
18
+ return current_state unless %w(purchase authorize).include? action.to_s
19
+ return current_state unless response.instance_of? GenesisRuby::Api::Response
20
+ return 'started_processing' if SpreeEmerchantpayGenesis::TransactionHelper.async_result?(response)
21
+
22
+ current_state
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ ::Spree::Payment.prepend(Spree::PaymentProcessingDecorator)
@@ -0,0 +1,33 @@
1
+ module SpreeEmerchantpayGenesis
2
+ module Base
3
+ # Simple key value hash object
4
+ # Usage: ConfigurableOptions.<name>=, ConfigurableOptions.<name>
5
+ class Data < Hash
6
+
7
+ def []=(key, value)
8
+ super(key.to_sym, value)
9
+ end
10
+
11
+ def [](key)
12
+ super(key.to_sym)
13
+ end
14
+
15
+ # Dynamic accessors for the hash[keys]
16
+ def method_missing(name, *args)
17
+ name_string = name.to_s
18
+ if name_string.chomp!('=')
19
+ self[name_string] = args.first
20
+ else
21
+ self[name_string]
22
+ end
23
+ end
24
+
25
+ # Define ConfigurableOptions.method("<method>") call
26
+ # If we dont have defined hash key we will raise exception
27
+ def respond_to_missing?(name, include_private = false)
28
+ method_missing(name) || super
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,65 @@
1
+ module SpreeEmerchantpayGenesis
2
+ module Base
3
+ # Base Gateway object
4
+ class Gateway < Spree::Gateway
5
+
6
+ preference :username, :string
7
+ preference :password, :string
8
+ preference :transaction_types, :select, default: -> { { values: [:authorize, :authorize3d, :sale, :sale3d] } }
9
+ preference :return_success_url, :string, default: 'http://localhost:4000/orders/|:ORDER:|'
10
+ preference :return_failure_url, :string, default: 'http://localhost:4000/checkout/payment?order_number=|:ORDER:|'
11
+ preference :threeds_allowed, :boolean_select, default: true
12
+ preference :challenge_indicator, :select, default: lambda {
13
+ { values: [:no_preference, :no_challenge_requested, :preference, :mandate] }
14
+ }
15
+ preference :hostname, :string, default: 'http://127.0.0.1:4000'
16
+ preference :test_mode, :boolean_select, default: true
17
+
18
+ # Capture authorized payment
19
+ def capture(amount, transaction_id, gateway_options)
20
+ order, payment = order_data_from_options gateway_options
21
+
22
+ prepare_provider order.attributes.symbolize_keys.merge(gateway_options), payment.source, payment
23
+
24
+ transaction = SpreeEmerchantpayGenesis::EmerchantpayPaymentsRepository.find_by_transaction_id transaction_id
25
+
26
+ provider.capture GenesisRuby::Utils::MoneyFormat.exponent_to_amount(amount, order.currency), transaction
27
+ end
28
+
29
+ # Undo a payment
30
+ def void(transaction_id, gateway_options)
31
+ order, payment = order_data_from_options gateway_options
32
+
33
+ prepare_provider order.attributes.symbolize_keys.merge(gateway_options), payment.source, payment
34
+
35
+ transaction = SpreeEmerchantpayGenesis::EmerchantpayPaymentsRepository.find_final_transaction transaction_id
36
+
37
+ provider.void transaction
38
+ end
39
+
40
+ # Refund a payment
41
+ def credit(credit_cents, transaction_id, refund_object)
42
+ payment = refund_object[:originator].payment
43
+ order = refund_object[:originator].payment.order
44
+
45
+ prepare_provider order.attributes.symbolize_keys, payment.source, payment
46
+
47
+ transaction = SpreeEmerchantpayGenesis::EmerchantpayPaymentsRepository.find_final_transaction transaction_id
48
+
49
+ provider.refund GenesisRuby::Utils::MoneyFormat.exponent_to_amount(credit_cents, order.currency), transaction
50
+ end
51
+
52
+ def payment_profiles_supported?
53
+ false
54
+ end
55
+
56
+ def order_data_from_options(options)
57
+ order_number, payment_number = options[:order_id].split('-')
58
+ order = Spree::Order.find_by(number: order_number)
59
+ payment = order.payments.find_by(number: payment_number)
60
+ [order, payment]
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ module SpreeEmerchantpayGenesis
2
+ module Data
3
+ # Billing and Shipping Address data
4
+ class Address < Base::Data
5
+
6
+ # Override the default zip method behaviour
7
+ def zip
8
+ self[:zip]
9
+ end
10
+
11
+ # Split name into first and last names
12
+ def name=(value)
13
+ names = value.nil? ? [] : value.split
14
+
15
+ self[:first_name] = names.first
16
+ self[:last_name] = names.last
17
+
18
+ self[:name] = value
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module SpreeEmerchantpayGenesis
2
+ module Data
3
+ # Provider data
4
+ class Provider < Base::Data
5
+
6
+ # Predefine Amount accessor (BigDecimal.to_s)
7
+ def amount
8
+ return total.to_s unless total.nil?
9
+
10
+ total
11
+ end
12
+
13
+ # Override default ip accessor
14
+ # Requests via Spree API causing ip = nil
15
+ def ip
16
+ return '127.0.0.1' if self[:ip].nil?
17
+
18
+ self[:ip]
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ module SpreeEmerchantpayGenesis
2
+ module Data
3
+ # User data
4
+ class User < Base::Data
5
+
6
+ # Provide formatted Create At
7
+ def created_at_formatted
8
+ self[:created_at].strftime(GenesisRuby::Api::Constants::DateTimeFormats::YYYY_MM_DD_ISO_8601)
9
+ end
10
+
11
+ # Provide formatted Updated At
12
+ def updated_at_formatted
13
+ self[:updated_at].strftime(GenesisRuby::Api::Constants::DateTimeFormats::YYYY_MM_DD_ISO_8601)
14
+ end
15
+
16
+ # Billing Address object
17
+ def billing_address
18
+ return nil if self[:bill_address_id].nil?
19
+
20
+ Spree::Address.find_by(id: self[:bill_address_id])
21
+ end
22
+
23
+ # Shipping Address object
24
+ def shipping_address
25
+ return nil if self[:ship_address_id].nil?
26
+
27
+ Spree::Address.find_by(id: self[:ship_address_id])
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ module SpreeEmerchantpayGenesis
2
+ module Db
3
+ # Application Record
4
+ class ApplicationRecord < ActiveRecord::Base
5
+
6
+ self.abstract_class = true
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,35 @@
1
+ module SpreeEmerchantpayGenesis
2
+ module Db
3
+ # EmerchantpayPayments DB model
4
+ class EmerchantpayPayment < ApplicationRecord
5
+
6
+ DATE_TIME_FORMAT = '%Y-%m-%d %H:%M'.freeze
7
+
8
+ store :request, coder: JSON
9
+ store :response, coder: JSON
10
+
11
+ # Get amount formatted in major currency
12
+ def major_amount
13
+ GenesisRuby::Utils::MoneyFormat.exponent_to_amount amount, currency
14
+ end
15
+
16
+ # Get created_at formatted
17
+ def formatted_created_at
18
+ created_at.strftime DATE_TIME_FORMAT
19
+ end
20
+
21
+ # Get updated_at formatted
22
+ def formatted_updated_at
23
+ updated_at.strftime DATE_TIME_FORMAT
24
+ end
25
+
26
+ # Get formatted Response Timestamp
27
+ def zulu_response_timestamp
28
+ DateTime.parse(response[:timestamp]).strftime(
29
+ GenesisRuby::Api::Constants::DateTimeFormats::YYYY_MM_DD_H_I_S_ZULU
30
+ )
31
+ end
32
+
33
+ end
34
+ end
35
+ end