spree_mollie_gateway 0.1.1 → 1.0.0.pre.beta3

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 (32) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -1
  3. data/.travis.yml +12 -1
  4. data/CONTRIBUTING.md +28 -0
  5. data/Gemfile.lock +322 -2
  6. data/README.md +29 -54
  7. data/Rakefile +19 -4
  8. data/app/controllers/spree/api/v1/mollie_controller.rb +16 -0
  9. data/app/controllers/spree/checkout_controller_decorator.rb +5 -6
  10. data/app/controllers/spree/mollie_controller.rb +20 -8
  11. data/app/models/mollie/client_decorator.rb +13 -0
  12. data/app/models/spree/gateway/mollie_gateway.rb +108 -53
  13. data/app/models/spree/mollie_logger.rb +9 -0
  14. data/app/models/spree/{mollie_transaction.rb → mollie_payment_source.rb} +7 -3
  15. data/app/models/spree/order_decorator.rb +24 -0
  16. data/app/models/spree/payment/processing_decorator.rb +8 -2
  17. data/app/models/spree/payment_decorator.rb +16 -0
  18. data/app/models/spree/user_decorator.rb +4 -2
  19. data/app/views/spree/checkout/payment/_molliegateway.html.erb +40 -3
  20. data/app/views/spree/shared/_payment.html.erb +1 -1
  21. data/config/routes.rb +11 -1
  22. data/db/migrate/{20180214133044_create_spree_mollie_transactions.rb → 20180214133044_create_spree_mollie_payment_sources.rb} +2 -2
  23. data/db/migrate/20180301084841_add_mollie_customer_id_to_spree_user.rb +2 -1
  24. data/docs/api/methods.md +61 -0
  25. data/docs/api/readme.md +7 -0
  26. data/docs/debugging.md +26 -0
  27. data/lib/spree_mollie_gateway/engine.rb +13 -3
  28. data/lib/spree_mollie_gateway/factories.rb +10 -0
  29. data/lib/spree_mollie_gateway/version.rb +1 -1
  30. data/spree_mollie_gateway.gemspec +27 -12
  31. metadata +231 -8
  32. data/app/models/mollie/provider.rb +0 -5
data/Rakefile CHANGED
@@ -1,6 +1,21 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
3
 
4
- RSpec::Core::RakeTask.new(:spec)
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/extension_rake'
5
6
 
6
- task :default => :spec
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default do
10
+ if Dir['spec/dummy'].empty?
11
+ Rake::Task[:test_app].invoke
12
+ Dir.chdir('../../')
13
+ end
14
+ Rake::Task[:spec].invoke
15
+ end
16
+
17
+ desc 'Generates a dummy app for testing'
18
+ task :test_app do
19
+ ENV['LIB_NAME'] = 'spree_mollie_gateway'
20
+ Rake::Task['extension:test_app'].invoke
21
+ end
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ module Api
3
+ module V1
4
+ class MollieController < BaseController
5
+ def methods
6
+ mollie = Spree::PaymentMethod.find_by_type 'Spree::Gateway::MollieGateway'
7
+ payment_methods = mollie.available_payment_methods
8
+
9
+ puts payment_methods
10
+
11
+ render json: payment_methods
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,12 +1,15 @@
1
1
  module Spree
2
2
  module CheckoutWithMollie
3
+ # If we're currently in the checkout
3
4
  def update
4
5
  if payment_params_valid? && paying_with_mollie?
5
6
  if @order.update_from_params(params, permitted_checkout_attributes, request.headers.env)
6
7
  payment = @order.payments.last
7
- payment.create_transaction!
8
+ payment.process!
8
9
  mollie_payment_url = payment.payment_source.payment_url
9
10
 
11
+ MollieLogger.debug("For order #{@order.number} redirect user to payment URL: #{mollie_payment_url}")
12
+
10
13
  redirect_to mollie_payment_url
11
14
  else
12
15
  render :edit
@@ -27,15 +30,11 @@ module Spree
27
30
 
28
31
  def paying_with_mollie?
29
32
  payment_method = PaymentMethod.find(payment_method_id_param)
30
- mollie_payment_method?(payment_method)
33
+ payment_method.is_a? Gateway::MollieGateway
31
34
  end
32
35
 
33
36
  def payment_params_valid?
34
37
  (params[:state] === 'payment') && params[:order][:payments_attributes]
35
38
  end
36
-
37
- def mollie_payment_method?(payment_method)
38
- payment_method.is_a?(Gateway::MollieGateway)
39
- end
40
39
  end
41
40
  end
@@ -5,10 +5,13 @@ module Spree
5
5
  # When the user is redirected from Mollie back to the shop, we can check the
6
6
  # mollie transaction status and set the Spree order state accordingly.
7
7
  def validate_payment
8
- order = Spree::Order.find_by_number(params[:order_number])
9
- payment = order.payments.last
10
- mollie = Spree::PaymentMethod.find_by_type('Spree::Gateway::MollieGateway')
11
- mollie.update_payment_status(payment)
8
+ order_number, payment_number = split_payment_identifier params[:order_number]
9
+ payment = Spree::Payment.find_by_number payment_number
10
+ order = Spree::Order.find_by_number order_number
11
+ mollie = Spree::PaymentMethod.find_by_type 'Spree::Gateway::MollieGateway'
12
+ mollie.update_payment_status payment
13
+
14
+ MollieLogger.debug("Redirect URL visited for order #{params[:order_number]}")
12
15
 
13
16
  redirect_to order.reload.paid? ? order_path(order) : checkout_state_path(:payment)
14
17
  end
@@ -16,11 +19,20 @@ module Spree
16
19
  # Mollie might send us information about a transaction through the webhook.
17
20
  # We should update the payment state accordingly.
18
21
  def update_payment_status
19
- payment = Spree::Payment.find_by_response_code(params[:id])
20
- mollie = Spree::PaymentMethod.find_by_type('Spree::Gateway::MollieGateway')
21
- mollie.update_payment_status(payment)
22
+ MollieLogger.debug("Webhook called for payment #{params[:id]}")
23
+
24
+ payment = Spree::MolliePaymentSource.find_by_payment_id(params[:id]).payments.first
25
+ mollie = Spree::PaymentMethod.find_by_type 'Spree::Gateway::MollieGateway'
26
+ mollie.update_payment_status payment
27
+
28
+ head :ok
29
+ end
30
+
31
+ private
22
32
 
23
- render json: 'OK'
33
+ # Payment identifier is a combination of order_number and payment_id.
34
+ def split_payment_identifier(payment_identifier)
35
+ payment_identifier.split '-'
24
36
  end
25
37
  end
26
38
  end
@@ -0,0 +1,13 @@
1
+ Mollie::Client.class_eval do
2
+ attr_accessor :version_strings
3
+
4
+ def initialize(api_key = nil)
5
+ @api_endpoint = Mollie::Client::API_ENDPOINT
6
+ @api_key = api_key
7
+ @version_strings = []
8
+
9
+ add_version_string 'MollieSpreeCommerce/' << SpreeMollieGateway::VERSION
10
+ add_version_string 'Ruby/' << RUBY_VERSION
11
+ add_version_string OpenSSL::OPENSSL_VERSION.split(' ').slice(0, 2).join '/'
12
+ end
13
+ end
@@ -3,16 +3,26 @@ module Spree
3
3
  preference :api_key, :string
4
4
  preference :hostname, :string
5
5
 
6
- has_many :spree_mollie_transactions, class_name: 'Spree::MollieTransaction'
6
+ has_many :spree_mollie_payment_sources, class_name: 'Spree::MolliePaymentSource'
7
+
8
+ # Only enable one-click payments if spree_auth_devise is installed
9
+ def self.allow_one_click_payments?
10
+ Gem.loaded_specs.has_key?('spree_auth_devise')
11
+ end
7
12
 
8
13
  def payment_source_class
9
- Spree::MollieTransaction
14
+ Spree::MolliePaymentSource
15
+ end
16
+
17
+ def actions
18
+ %w{credit}
10
19
  end
11
20
 
12
21
  def provider_class
13
- ::Mollie::Provider
22
+ ::Mollie::Client
14
23
  end
15
24
 
25
+ # Always create a source which references to the selected Mollie payment method.
16
26
  def source_required?
17
27
  true
18
28
  end
@@ -21,41 +31,59 @@ module Spree
21
31
  true
22
32
  end
23
33
 
24
- # Create a new transaction
25
- def create_transaction(money, source, gateway_options)
26
- payment = payments.last
27
- transaction = ::Mollie::Payment.create(
28
- prepare_transaction_params(payment.order, source)
29
- )
30
-
31
- invalidate_prev_transactions(payment.id)
32
-
33
- payment.response_code = transaction.id
34
- payment.save!
35
-
36
- source.payment_id = transaction.id
37
- source.payment_url = transaction.payment_url
38
- source.save!
34
+ def auto_capture?
35
+ true
36
+ end
39
37
 
40
- ActiveMerchant::Billing::Response.new(true, 'Transaction created')
38
+ # Create a new Mollie payment.
39
+ def create_transaction(money_in_cents, source, gateway_options)
40
+ MollieLogger.debug("About to create payment for order #{gateway_options[:order_id]}")
41
+
42
+ begin
43
+ mollie_payment = ::Mollie::Payment.create(
44
+ prepare_payment_params(money_in_cents, source, gateway_options)
45
+ )
46
+ MollieLogger.debug("Payment #{mollie_payment.id} created for order #{gateway_options[:order_id]}")
47
+
48
+ source.status = mollie_payment.status
49
+ source.payment_id = mollie_payment.id
50
+ source.payment_url = mollie_payment.payment_url
51
+ source.save!
52
+ ActiveMerchant::Billing::Response.new(true, 'Payment created')
53
+ rescue Mollie::Exception => e
54
+ MollieLogger.debug("Could not create payment for order #{gateway_options[:order_id]}: #{e.message}")
55
+ ActiveMerchant::Billing::Response.new(false, "Payment could not be created: #{e.message}")
56
+ end
41
57
  end
42
58
 
59
+ # Create a Mollie customer which can be passed with a payment.
60
+ # Required for one-click Mollie payments.
43
61
  def create_customer(user)
44
- Mollie::Customer.create(
62
+ customer = Mollie::Customer.create(
45
63
  email: user.email,
46
64
  api_key: get_preference(:api_key),
47
65
  )
66
+ MollieLogger.debug("Created a Mollie Customer for Spree user with ID #{customer.id}")
67
+ customer
48
68
  end
49
69
 
50
- def prepare_transaction_params(order, source)
70
+ def prepare_payment_params(money_in_cents, source, gateway_options)
51
71
  spree_routes = ::Spree::Core::Engine.routes.url_helpers
72
+ order_number = gateway_options[:order_id]
73
+ customer_id = gateway_options[:customer_id]
74
+ amount = money_in_cents / 100.0
52
75
 
53
- order_number = order.number
54
76
  order_params = {
55
- amount: order.total.to_f,
77
+ amount: amount,
56
78
  description: "Spree Order ID: #{order_number}",
57
- redirectUrl: spree_routes.mollie_validate_payment_mollie_url(order_number: order_number, host: get_preference(:hostname)),
58
- webhookUrl: spree_routes.mollie_update_payment_status_mollie_url(order_number: order_number, host: get_preference(:hostname)),
79
+ redirectUrl: spree_routes.mollie_validate_payment_mollie_url(
80
+ order_number: order_number,
81
+ host: get_preference(:hostname)
82
+ ),
83
+ webhookUrl: spree_routes.mollie_update_payment_status_mollie_url(
84
+ order_number: order_number,
85
+ host: get_preference(:hostname)
86
+ ),
59
87
  method: source.payment_method_name,
60
88
  metadata: {
61
89
  order_id: order_number
@@ -63,24 +91,54 @@ module Spree
63
91
  api_key: get_preference(:api_key),
64
92
  }
65
93
 
66
- if order.user_id.present?
94
+ source.issuer.present?
95
+ order_params.merge! ({
96
+ issuer: source.issuer
97
+ })
98
+
99
+ if customer_id.present?
67
100
  if source.payment_method_name.match(Regexp.union([::Mollie::Method::BITCOIN, ::Mollie::Method::BANKTRANSFER, ::Mollie::Method::GIFTCARD]))
68
101
  order_params.merge! ({
69
- billingEmail: order.user.email
102
+ billingEmail: gateway_options[:email]
70
103
  })
71
104
  end
72
105
 
73
- # Allow single click payments by passing Mollie customer ID
74
- if order.user.mollie_customer_id.present?
75
- order_params.merge! ({
76
- customerId: order.user.mollie_customer_id
77
- })
106
+ if Spree::Gateway::MollieGateway.allow_one_click_payments?
107
+ mollie_customer_id = Spree.user_class.find(customer_id).try(:mollie_customer_id)
108
+
109
+ # Allow one-click payments by passing Mollie customer ID.
110
+ if mollie_customer_id.present?
111
+ order_params.merge! ({
112
+ customerId: customer_id
113
+ })
114
+ end
78
115
  end
79
116
  end
80
117
 
81
118
  order_params
82
119
  end
83
120
 
121
+ # Create a new Mollie refund
122
+ def credit(credit_cents, payment_id, options)
123
+ order_number = options[:originator].try(:payment).try(:order).try(:number)
124
+ MollieLogger.debug("Starting refund for order #{order_number}")
125
+
126
+ begin
127
+ amount = credit_cents / 100.0
128
+ Mollie::Payment::Refund.create(
129
+ payment_id: payment_id,
130
+ amount: amount,
131
+ description: "Refund Spree Order ID: #{order_number}",
132
+ api_key: get_preference(:api_key)
133
+ )
134
+ MollieLogger.debug("Successfully refunded #{amount} for order #{order_number}")
135
+ ActiveMerchant::Billing::Response.new(true, 'Refund successful')
136
+ rescue Mollie::Exception => e
137
+ MollieLogger.debug("Refund failed for order #{order_number}: #{e.message}")
138
+ ActiveMerchant::Billing::Response.new(false, 'Refund unsuccessful')
139
+ end
140
+ end
141
+
84
142
  def available_payment_methods
85
143
  ::Mollie::Method.all(
86
144
  api_key: get_preference(:api_key),
@@ -89,35 +147,32 @@ module Spree
89
147
  end
90
148
 
91
149
  def update_payment_status(payment)
92
- mollie_transaction_id = payment.response_code
93
- transaction = ::Mollie::Payment.get(
150
+ mollie_transaction_id = payment.source.payment_id
151
+ mollie_payment = ::Mollie::Payment.get(
94
152
  mollie_transaction_id,
95
153
  api_key: get_preference(:api_key)
96
154
  )
97
155
 
98
- unless payment.completed?
99
- case transaction.status
100
- when 'paid'
101
- payment.complete! unless payment.completed?
102
- payment.order.finalize!
103
- payment.order.update_attributes(:state => 'complete', :completed_at => Time.now)
104
- when 'cancelled', 'expired', 'failed'
105
- payment.failure! unless payment.failed?
106
- else
107
- logger.debug 'Unhandled Mollie payment state received. Therefore we did not update the payment state.'
108
- end
109
- end
156
+ MollieLogger.debug("Updating order state for payment. Payment has state #{mollie_payment.status}")
110
157
 
111
- payment.source.update(status: payment.state)
158
+ update_by_mollie_status!(mollie_payment, payment)
112
159
  end
113
160
 
114
- private
115
- def invalidate_prev_transactions(current_payment_id)
116
- # Cancel all previous payment which are pending or are still being processed
117
- payments.with_state('processing').or(payments.with_state('pending')).where.not(id: current_payment_id).each do |payment|
118
- # Set internal payment state to failed
119
- payment.failure! unless payment.store_credit?
161
+ def update_by_mollie_status!(mollie_payment, payment)
162
+ case mollie_payment.status
163
+ when 'paid'
164
+ payment.complete! unless payment.completed?
165
+ payment.order.finalize!
166
+ payment.order.update_attributes(:state => 'complete', :completed_at => Time.now)
167
+ when 'cancelled', 'expired', 'failed'
168
+ payment.failure! unless payment.failed?
169
+ when 'refunded'
170
+ payment.void! unless payment.void?
171
+ else
172
+ MollieLogger.debug('Unhandled Mollie payment state received. Therefore we did not update the payment state.')
120
173
  end
174
+
175
+ payment.source.update(status: payment.state)
121
176
  end
122
177
  end
123
178
  end
@@ -0,0 +1,9 @@
1
+ module Spree
2
+ class MollieLogger
3
+ def self.debug(message = nil)
4
+ return unless message.present?
5
+ @logger ||= Logger.new(File.join(Rails.root, 'log', 'mollie.log'))
6
+ @logger.debug(message)
7
+ end
8
+ end
9
+ end
@@ -1,14 +1,18 @@
1
1
  module Spree
2
- class MollieTransaction < Spree::Base
2
+ class MolliePaymentSource < Spree::Base
3
3
  belongs_to :payment_method
4
- has_many :payment, as: :source
4
+ has_many :payments, as: :source
5
5
 
6
6
  def actions
7
7
  []
8
8
  end
9
9
 
10
+ def transaction_id
11
+ payment_id
12
+ end
13
+
10
14
  def method_type
11
- 'mollie_transaction'
15
+ 'mollie_payment_source'
12
16
  end
13
17
 
14
18
  def name
@@ -0,0 +1,24 @@
1
+ Spree::Order.class_eval do
2
+ # Make sure the order confirmation is delivered when the order has been paid for.
3
+ def finalize!
4
+ # lock all adjustments (coupon promotions, etc.)
5
+ all_adjustments.each(&:close)
6
+
7
+ # update payment and shipment(s) states, and save
8
+ updater.update_payment_state
9
+ shipments.each do |shipment|
10
+ shipment.update!(self)
11
+ shipment.finalize!
12
+ end
13
+
14
+ updater.update_shipment_state
15
+ save!
16
+ updater.run_hooks
17
+
18
+ touch :completed_at
19
+
20
+ deliver_order_confirmation_email unless confirmation_delivered? or !paid?
21
+
22
+ consider_risk
23
+ end
24
+ end
@@ -1,6 +1,12 @@
1
1
  Spree::Payment::Processing.module_eval do
2
- def create_transaction!
2
+ def process!(amount = nil)
3
+ amount ||= money.money.cents
3
4
  started_processing!
4
- gateway_action(source, :create_transaction, :pend)
5
+ response = payment_method.create_transaction(
6
+ amount,
7
+ source,
8
+ gateway_options
9
+ )
10
+ handle_response(response, :pend, :failure)
5
11
  end
6
12
  end
@@ -0,0 +1,16 @@
1
+ Spree::Payment.class_eval do
2
+ delegate :transaction_id, to: :source
3
+
4
+ def build_source
5
+ return unless new_record?
6
+ if source_attributes.present? && source.blank? && payment_method.try(:payment_source_class)
7
+ self.source = payment_method.payment_source_class.new(source_attributes)
8
+ source.payment_method_id = payment_method.id
9
+ source.user_id = order.user_id if order
10
+
11
+ # Spree will not process payments if order is completed.
12
+ # We should call process! for completed orders to create a new Mollie payment.
13
+ process! if order.completed?
14
+ end
15
+ end
16
+ end