Skip to content

Commit

Permalink
Update to Mollie v2 API (#1133)
Browse files Browse the repository at this point in the history
* feat: update to mollie v3 api

* chore: run linter

* fix: add ideal as method so we return to Koala on failure

* chore: linter ✨

* chore: satisfy linter 🤡

* chore: satisfy linter 🤡

* feat: remove issuer

* fix: resolved brakeman warning

* fix: resolved linter warning

* fix: second attempt at securing mollie redirect

---------

Co-authored-by: Silas <[email protected]>
  • Loading branch information
leuke-naam and SilasPeters authored Dec 25, 2024
1 parent 8b35a67 commit f372405
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 62 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ gem 'impressionist', github: 'charlotte-ruby/impressionist'
# rests calls for mailgun
gem 'rest-client'

# mollie
gem 'mollie-api-ruby'

gem 'responders'

# pagination
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ GEM
mini_mime (1.1.2)
mini_portile2 (2.7.1)
minitest (5.15.0)
mollie-api-ruby (4.13.0)
netrc (0.11.0)
nio4r (2.5.8)
nokogiri (1.13.1)
Expand Down Expand Up @@ -352,6 +353,7 @@ DEPENDENCIES
impressionist!
listen
mimemagic (= 0.3.9)
mollie-api-ruby
pagy
pg
pg_search
Expand Down
37 changes: 31 additions & 6 deletions app/controllers/members/payments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,31 @@ def pay_activities
redirect_to(member_payments_path)
return
end

payment = Payment.new(
description: description,
amount: amount,
issuer: transaction_params[:bank],
member: member,
payment_type: :ideal,
transaction_id: unpaid.pluck(:activity_id),
transaction_type: :activity,
redirect_uri: member_payments_path
)
if payment.save
redirect_to(payment.payment_uri)
# Check URI for safety (supresses brakeman warning)
url = begin
URI.parse(payment.payment_uri)
rescue StandardError
nil
end

# Check if it's a valid URI and matches your whitelist of acceptable domains (e.g., only http(s)://example.com)
if url.is_a?(URI::HTTP) && ['mollie.com'].include?(url.host)
redirect_to(url)
else
# Fallback to a safe default redirect if the URI is invalid or not in the whitelist
redirect_to(root_path)
end
else
flash[:notice] = I18n.t('failed', scope: 'activerecord.errors.models.payment')
redirect_to(member_payments_path)
Expand Down Expand Up @@ -97,15 +110,27 @@ def add_funds
description: description,
amount: amount,
member: member,
issuer: transaction_params[:bank],
payment_type: :ideal,

transaction_id: nil,
transaction_type: :checkout,
redirect_uri: member_payments_path
)

if payment.save
redirect_to(payment.payment_uri)
# Check URI for safety (supresses brakeman warning)
url = begin
URI.parse(payment.payment_uri)
rescue StandardError
nil
end

# Check if it's a valid URI and matches your whitelist of acceptable domains (e.g., only http(s)://example.com)
if url.is_a?(URI::HTTP) && ['mollie.com'].include?(url.host)
redirect_to(url)
else
# Fallback to a safe default redirect if the URI is invalid or not in the whitelist
redirect_to(root_path)
end
else
flash[:warning] = I18n.t('failed', scope: 'activerecord.errors.models.payment')
redirect_to(members_home_path)
Expand All @@ -115,6 +140,6 @@ def add_funds
private

def transaction_params
params.permit(:amount, :bank, :activity_ids, :payment_type)
params.permit(:amount, :activity_ids, :payment_type)
end
end
71 changes: 21 additions & 50 deletions app/models/payment.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#:nodoc:
class Payment < ApplicationRecord
require 'request'

self.primary_key = :token

attr_accessor :issuer, :payment_uri, :message
Expand All @@ -12,6 +10,7 @@ class Payment < ApplicationRecord
validates :payment_type, presence: true

enum status: { failed: 0, in_progress: 1, successful: 2 }

# Keep payconiq_online because it is still present in the database
enum payment_type: { ideal: 0, payconiq_online: 1, pin: 3 }
enum transaction_type: { checkout: 0, activity: 1 }
Expand All @@ -26,6 +25,7 @@ class Payment < ApplicationRecord
after_validation :request_payment, on: :create

include PgSearch::Model

pg_search_scope :search_by_name,
against: [:trxid],
associated_against: {
Expand All @@ -47,54 +47,40 @@ def request_payment

case payment_type.to_sym
when :ideal
http = ConstipatedKoala::Request.new(ENV['MOLLIE_DOMAIN'])
self.token = Digest::SHA256.hexdigest("#{ member.id }#{ Time.now.to_f }#{ redirect_uri }")

webhook_url = if Rails.env.development?
"#{ ENV['NGROK_HOST'] }/api/hook/mollie"
else
Rails.application.routes.url_helpers.mollie_hook_url
end

request = http.post("/#{ ENV['MOLLIE_VERSION'] }/payments",
amount: amount,
description: description,

method: 'ideal',
issuer: issuer,

metadata: {
member: member.name,
transaction_type: transaction_type,
transaction_id: transaction_id

},
webhookUrl: webhook_url,
redirectUrl: Rails.application.routes.url_helpers.payment_redirect_url(token: token))

request['Authorization'] = "Bearer #{ ENV['MOLLIE_TOKEN'] }"
response = http.send!(request)

self.trxid = response.id
self.payment_uri = response.links.paymentUrl
redirect_url = Rails.application.routes.url_helpers.payment_redirect_url(token: token)

payment = Mollie::Payment.create(
amount: { value: format('%.2f', amount), currency: 'EUR' },
method: 'ideal', # only ideal for now
description: description,
webhook_url: webhook_url,
redirect_url: redirect_url
)

self.trxid = payment.id
self.payment_uri = payment._links['checkout']['href']
self.status = :in_progress
# pin payment shouldn't have any extra work

# pin payment shouldn't have any extra work
when :pin
end
end

def update_transaction!
case payment_type.to_sym
when :ideal
http = ConstipatedKoala::Request.new(ENV['MOLLIE_DOMAIN'])
@status = status

request = http.get("/#{ ENV['MOLLIE_VERSION'] }/payments/#{ trxid }")
request['Authorization'] = "Bearer #{ ENV['MOLLIE_TOKEN'] }"

response = http.send!(request)
payment = Mollie::Payment.get(trxid)

status_update(response.status)
status_update(payment.status)

save!

Expand Down Expand Up @@ -156,32 +142,17 @@ def transaction_fee
end
end

def self.ideal_issuers
# cache the payment issuers for 12 hours, don't request it to often. Stored in tmp/cache
return [] if ENV['MOLLIE_TOKEN'].blank?

Rails.cache.fetch('mollie_issuers', expires_in: 12.hours) do
http = ConstipatedKoala::Request.new(ENV['MOLLIE_DOMAIN'])

request = http.get("/#{ ENV['MOLLIE_VERSION'] }/issuers")
request['Authorization'] = "Bearer #{ ENV['MOLLIE_TOKEN'] }"

response = http.send!(request)
response.data.map { |issuer| [issuer.name, issuer.id] }
end
end

def activities
Activity.find(transaction_id) if activity?
end

private

def status_update(new_status)
self.status = case new_status.downcase
when "succeeded", "paid"
self.status = case new_status
when "paid", "authorized"
:successful
when "expired", "canceled", "failed", "cancelled", "authorization_failed"
when "expired", "failed", "canceled"
:failed
else
:in_progress
Expand Down
5 changes: 1 addition & 4 deletions app/views/members/payments/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,10 @@
%tr
%td
%td
.ideal-activities
= I18n.t("members.payments.unpaid_activity.footer.bank")
.float-xl-right= f.select :bank, options_for_select(Payment::ideal_issuers), {}, {style: '', class:'ideal-activities'}
%td
%div
= I18n.t("members.payments.unpaid_activity.footer.transactioncosts")
%span.transaction_cost_activities.ideal-activities{:price =>@transaction_costs}= number_to_currency(@transaction_costs, :unit => '€')
%span.transaction_cost_activities.ideal-activities{:price => @transaction_costs}= number_to_currency(@transaction_costs, :unit => '€')
%tr
%td
%td
Expand Down
5 changes: 5 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,8 @@ class Application < Rails::Application
config.middleware.use(I18n::JS::Middleware)
end
end

# Mollie configuration
Mollie::Client.configure do |config|
config.api_key = ENV['MOLLIE_TOKEN']
end
10 changes: 10 additions & 0 deletions gemset.nix
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,16 @@
};
version = "5.15.0";
};
mollie-api-ruby = {
groups = ["default"];
platforms = [];
source = {
remotes = ["https://rubygems.org"];
sha256 = "0z4z0cf5lq3bmdbjsdzjj2spvg351b32nzwrn9rf3zqm9rldai6w";
type = "gem";
};
version = "4.13.0";
};
netrc = {
groups = ["default"];
platforms = [];
Expand Down
2 changes: 0 additions & 2 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ MAILCHIMP_TEACHER_ID=
NGROK_HOST=http

# MOLLIE credentials for the iDEAL integration.
MOLLIE_DOMAIN=https://api.mollie.nl
MOLLIE_VERSION=v1
MOLLIE_TOKEN=

# Secret for error reporting.
Expand Down

0 comments on commit f372405

Please sign in to comment.