Skip to content

Commit 51910b9

Browse files
authored
Merge branch 'master' into fix/normalized-translations
2 parents 3c9de3d + 201ebc3 commit 51910b9

File tree

16 files changed

+101
-62
lines changed

16 files changed

+101
-62
lines changed

Gemfile

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ gem 'impressionist', github: 'charlotte-ruby/impressionist'
2727
# rests calls for mailgun
2828
gem 'rest-client'
2929

30+
# mollie
31+
gem 'mollie-api-ruby'
32+
3033
gem 'responders'
3134

3235
# pagination

Gemfile.lock

+2
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ GEM
179179
mini_mime (1.1.2)
180180
mini_portile2 (2.7.1)
181181
minitest (5.15.0)
182+
mollie-api-ruby (4.13.0)
182183
netrc (0.11.0)
183184
nio4r (2.5.8)
184185
nokogiri (1.13.1)
@@ -352,6 +353,7 @@ DEPENDENCIES
352353
impressionist!
353354
listen
354355
mimemagic (= 0.3.9)
356+
mollie-api-ruby
355357
pagy
356358
pg
357359
pg_search

app/assets/stylesheets/cards.css

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
font-size: 30px;
2727
}
2828

29+
.size-small {
30+
font-size: 10px;
31+
}
32+
2933
.card .table {
3034
margin-bottom: 0;
3135
}

app/controllers/members/payments_controller.rb

+35-6
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,35 @@ def pay_activities
3535
redirect_to(member_payments_path)
3636
return
3737
end
38+
3839
payment = Payment.new(
3940
description: description,
4041
amount: amount,
41-
issuer: transaction_params[:bank],
4242
member: member,
4343
payment_type: :ideal,
4444
transaction_id: unpaid.pluck(:activity_id),
4545
transaction_type: :activity,
4646
redirect_uri: member_payments_path
4747
)
48+
4849
if payment.save
49-
redirect_to(payment.payment_uri)
50+
# Check URI for safety (supresses brakeman warning)
51+
url = begin
52+
URI.parse(payment.payment_uri)
53+
rescue StandardError
54+
nil
55+
end
56+
57+
# Check if it's a valid URI and matches your whitelist of acceptable domains (e.g., only http(s)://example.com)
58+
if url.is_a?(URI::HTTP) && [
59+
'www.mollie.com', # staging
60+
'pay.ideal.nl' # production
61+
].include?(url.host)
62+
redirect_to(url.to_s)
63+
else
64+
# Fallback to a safe default redirect if the URI is invalid or not in the whitelist
65+
redirect_to(root_path)
66+
end
5067
else
5168
flash[:notice] = I18n.t('failed', scope: 'activerecord.errors.models.payment')
5269
redirect_to(member_payments_path)
@@ -97,15 +114,27 @@ def add_funds
97114
description: description,
98115
amount: amount,
99116
member: member,
100-
issuer: transaction_params[:bank],
101117
payment_type: :ideal,
102-
103118
transaction_id: nil,
104119
transaction_type: :checkout,
105120
redirect_uri: member_payments_path
106121
)
122+
107123
if payment.save
108-
redirect_to(payment.payment_uri)
124+
# Check URI for safety (supresses brakeman warning)
125+
url = begin
126+
URI.parse(payment.payment_uri)
127+
rescue StandardError
128+
nil
129+
end
130+
131+
# Check if it's a valid URI and matches your whitelist of acceptable domains (e.g., only http(s)://example.com)
132+
if url.is_a?(URI::HTTP) && ['mollie.com'].include?(url.host)
133+
redirect_to(url)
134+
else
135+
# Fallback to a safe default redirect if the URI is invalid or not in the whitelist
136+
redirect_to(root_path)
137+
end
109138
else
110139
flash[:warning] = I18n.t('failed', scope: 'activerecord.errors.models.payment')
111140
redirect_to(members_home_path)
@@ -115,6 +144,6 @@ def add_funds
115144
private
116145

117146
def transaction_params
118-
params.permit(:amount, :bank, :activity_ids, :payment_type)
147+
params.permit(:amount, :activity_ids, :payment_type)
119148
end
120149
end

app/models/payment.rb

+21-47
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#:nodoc:
22
class Payment < ApplicationRecord
3-
require 'request'
4-
53
self.primary_key = :token
64

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

1412
enum status: { failed: 0, in_progress: 1, successful: 2 }
13+
1514
# Keep payconiq_online because it is still present in the database
1615
enum payment_type: { ideal: 0, payconiq_online: 1, pin: 3 }
1716
enum transaction_type: { checkout: 0, activity: 1 }
@@ -26,6 +25,7 @@ class Payment < ApplicationRecord
2625
after_validation :request_payment, on: :create
2726

2827
include PgSearch::Model
28+
2929
pg_search_scope :search_by_name,
3030
against: [:trxid],
3131
associated_against: {
@@ -47,54 +47,43 @@ def request_payment
4747

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

5352
webhook_url = if Rails.env.development?
5453
"#{ ENV['NGROK_HOST'] }/api/hook/mollie"
5554
else
5655
Rails.application.routes.url_helpers.mollie_hook_url
5756
end
57+
redirect_url = Rails.application.routes.url_helpers.payment_redirect_url(token: token)
5858

59-
request = http.post("/#{ ENV['MOLLIE_VERSION'] }/payments",
60-
amount: amount,
61-
description: description,
62-
63-
method: 'ideal',
64-
issuer: issuer,
59+
payment = Mollie::Payment.create(
60+
amount: { value: format('%.2f', amount), currency: 'EUR' },
61+
method: 'ideal', # only ideal for now
62+
description: description,
63+
webhook_url: webhook_url,
64+
redirect_url: redirect_url
65+
)
6566

66-
metadata: {
67-
member: member.name,
68-
transaction_type: transaction_type,
69-
transaction_id: transaction_id
67+
self.trxid = payment.id
7068

71-
},
72-
webhookUrl: webhook_url,
73-
redirectUrl: Rails.application.routes.url_helpers.payment_redirect_url(token: token))
74-
75-
request['Authorization'] = "Bearer #{ ENV['MOLLIE_TOKEN'] }"
76-
response = http.send!(request)
77-
78-
self.trxid = response.id
79-
self.payment_uri = response.links.paymentUrl
69+
# The host of this url is `www.mollie.com` so it will redirect to the mollie payment page
70+
# if this ever chanes, the redirect_uri whitelist in the controller should be updated
71+
self.payment_uri = payment._links['checkout']['href']
8072
self.status = :in_progress
81-
# pin payment shouldn't have any extra work
73+
74+
# pin payment shouldn't have any extra work
8275
when :pin
8376
end
8477
end
8578

8679
def update_transaction!
8780
case payment_type.to_sym
8881
when :ideal
89-
http = ConstipatedKoala::Request.new(ENV['MOLLIE_DOMAIN'])
9082
@status = status
9183

92-
request = http.get("/#{ ENV['MOLLIE_VERSION'] }/payments/#{ trxid }")
93-
request['Authorization'] = "Bearer #{ ENV['MOLLIE_TOKEN'] }"
84+
payment = Mollie::Payment.get(trxid)
9485

95-
response = http.send!(request)
96-
97-
status_update(response.status)
86+
status_update(payment.status)
9887

9988
save!
10089

@@ -156,32 +145,17 @@ def transaction_fee
156145
end
157146
end
158147

159-
def self.ideal_issuers
160-
# cache the payment issuers for 12 hours, don't request it to often. Stored in tmp/cache
161-
return [] if ENV['MOLLIE_TOKEN'].blank?
162-
163-
Rails.cache.fetch('mollie_issuers', expires_in: 12.hours) do
164-
http = ConstipatedKoala::Request.new(ENV['MOLLIE_DOMAIN'])
165-
166-
request = http.get("/#{ ENV['MOLLIE_VERSION'] }/issuers")
167-
request['Authorization'] = "Bearer #{ ENV['MOLLIE_TOKEN'] }"
168-
169-
response = http.send!(request)
170-
response.data.map { |issuer| [issuer.name, issuer.id] }
171-
end
172-
end
173-
174148
def activities
175149
Activity.find(transaction_id) if activity?
176150
end
177151

178152
private
179153

180154
def status_update(new_status)
181-
self.status = case new_status.downcase
182-
when "succeeded", "paid"
155+
self.status = case new_status
156+
when "paid", "authorized"
183157
:successful
184-
when "expired", "canceled", "failed", "cancelled", "authorization_failed"
158+
when "expired", "failed", "canceled"
185159
:failed
186160
else
187161
:in_progress

app/views/admin/activities/index.html.haml

+10-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
- elsif activity.group.committee?
2828
%span.badge.badge-success= activity.group.name.upcase
2929
- else
30-
%span.label.badge-warning= activity.group.name.upcase
30+
%span.badge.badge-warning= activity.group.name.upcase
3131
- else
3232
%td
3333
%span
@@ -43,3 +43,12 @@
4343
- else
4444
%td.d-none.d-md-block
4545
%span
46+
47+
%td
48+
- if activity.is_payable && activity.price != 0
49+
%span.badge.badge-success.size-small= I18n.t('admin.activities.is_payable')
50+
- elsif activity.price == 0
51+
%span.badge.badge-primary.size-small= I18n.t('admin.activities.free')
52+
- else
53+
%span.badge.badge-warning.size-small= I18n.t('admin.activities.is_not_payable')
54+

app/views/layouts/doorkeeper.html.haml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
%body.h-full.flex.flex-col.bg-gray-100.dark:bg-gray-900
2020
.flex.flex-col.flex-grow.justify-center
2121
%img.w-64.mx-auto.p-4.dark:hidden{src:"https://public.svsticky.nl/logos/logo_compact_outline_kleur.svg"}
22-
%img.w-64.mx-auto.p-4.hidden.dark:block{src:"https://public.svsticky.nl/logos/logo_compact_outline_wit.svg"}
22+
%img.w-64.mx-auto.p-4.hidden.dark:block{src:"https://public.svsticky.nl/logos/logo_compact_outline_wit_kleur.svg"}
2323

2424
.shadow.px-10.py-4.rounded-md.bg-white.mx-auto.dark:bg-gray-800.dark
2525

app/views/layouts/mailer.html.haml

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
.content{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;"}
6161
%table.main{:bgcolor => "#fff", :cellpadding => "0", :cellspacing => "0", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;", :width => "100%"}
6262
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
63-
%td.alert.alert-warning{:align => "center", :bgcolor => "#61518f", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background-color: #61518f; margin: 0; padding: 20px;", :valign => "top"}
63+
%td.alert.alert-warning{:align => "center", :bgcolor => "#197052", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background-color: #197052; margin: 0; padding: 20px;", :valign => "top"}
6464
= I18n.t('association_name')
6565

6666
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}

app/views/members/payments/index.html.haml

+1-4
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,10 @@
4242
%tr
4343
%td
4444
%td
45-
.ideal-activities
46-
= I18n.t("members.payments.unpaid_activity.footer.bank")
47-
.float-xl-right= f.select :bank, options_for_select(Payment::ideal_issuers), {}, {style: '', class:'ideal-activities'}
4845
%td
4946
%div
5047
= I18n.t("members.payments.unpaid_activity.footer.transactioncosts")
51-
%span.transaction_cost_activities.ideal-activities{:price =>@transaction_costs}= number_to_currency(@transaction_costs, :unit => '€')
48+
%span.transaction_cost_activities.ideal-activities{:price => @transaction_costs}= number_to_currency(@transaction_costs, :unit => '€')
5249
%tr
5350
%td
5451
%td

config/application.rb

+5
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,8 @@ class Application < Rails::Application
7575
config.middleware.use(I18n::JS::Middleware)
7676
end
7777
end
78+
79+
# Mollie configuration
80+
Mollie::Client.configure do |config|
81+
config.api_key = ENV['MOLLIE_TOKEN']
82+
end

config/locales/admin.en.yml

+3
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ en:
77
ended: Ended
88
filters: Filters
99
financial: Financial
10+
free: Free
1011
has_not_paid: has not yet paid
1112
has_paid: has paid
1213
info:
1314
already_added: This participant was already added
1415
price_changed: The participants fee has changed
1516
price_error: No connection or not a number
17+
is_not_payable: Not payable
18+
is_payable: Payable
1619
new: New activity
1720
remove_participant: Do you want to remove %{user} as participant?
1821
save: Save activity?

config/locales/admin.nl.yml

+3
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ nl:
77
ended: Afgelopen
88
filters: Filters
99
financial: Financieel
10+
free: Gratis
1011
has_not_paid: heeft nog niet betaald
1112
has_paid: heeft betaald
1213
info:
1314
already_added: Deze persoon is al toegevoegd
1415
price_changed: Het deelname bedrag is veranderd
1516
price_error: Geen verbinding of geen nummer
17+
is_not_payable: Niet betaalbaar
18+
is_payable: Betaalbaar
1619
new: Nieuwe activiteit
1720
remove_participant: Deelname van %{user} verwijderen?
1821
save: Activiteit opslaan?

config/locales/members.en.yml

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ en:
9595
vacancies: Vacancies
9696
voeljeveilig: VoelJeVeilig
9797
wiki: Stickypedia
98+
voeljeveilig: VoelJeVeilig
9899
payments:
99100
mongoose:
100101
credit: Credit

config/locales/members.nl.yml

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ nl:
9595
vacancies: Vacatures
9696
voeljeveilig: VoelJeVeilig
9797
wiki: Stickypedia
98+
voeljeveilig: VoelJeVeilig
9899
payments:
99100
mongoose:
100101
credit: Tegoed

gemset.nix

+10
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,16 @@
713713
};
714714
version = "5.15.0";
715715
};
716+
mollie-api-ruby = {
717+
groups = ["default"];
718+
platforms = [];
719+
source = {
720+
remotes = ["https://rubygems.org"];
721+
sha256 = "0z4z0cf5lq3bmdbjsdzjj2spvg351b32nzwrn9rf3zqm9rldai6w";
722+
type = "gem";
723+
};
724+
version = "4.13.0";
725+
};
716726
netrc = {
717727
groups = ["default"];
718728
platforms = [];

sample.env

-2
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ MAILCHIMP_TEACHER_ID=
7676
NGROK_HOST=http
7777

7878
# MOLLIE credentials for the iDEAL integration.
79-
MOLLIE_DOMAIN=https://api.mollie.nl
80-
MOLLIE_VERSION=v1
8179
MOLLIE_TOKEN=
8280

8381
# Secret for error reporting.

0 commit comments

Comments
 (0)