spree_mollie_gateway 0.1.1 → 1.0.0.pre.beta3

Sign up to get free protection for your applications and to get access to all the features.
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