Skip to content

Commit

Permalink
add mollie plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
yksflip committed Oct 5, 2023
1 parent 4df78de commit 0de7feb
Show file tree
Hide file tree
Showing 21 changed files with 1,105 additions and 0 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ gem 'foodsoft_wiki', path: 'plugins/wiki'
# gem 'foodsoft_current_orders', path: 'plugins/current_orders'
# gem 'foodsoft_printer', path: 'plugins/printer'
# gem 'foodsoft_uservoice', path: 'plugins/uservoice'
# gem 'foodsoft_mollie', path: 'plugins/mollie'

group :development do
gem 'listen'
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ GEM
actionpack (>= 5.0)
less (~> 2.6.0)
sprockets (~> 3.0)
libv8 (3.16.14.19)
libv8 (3.16.14.19-x86_64-linux)
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
Expand Down Expand Up @@ -325,6 +326,7 @@ GEM
mime-types-data (3.2022.0105)
mini_magick (4.12.0)
mini_mime (1.1.2)
mini_portile2 (2.8.2)
minitest (5.18.0)
mono_logger (1.1.1)
msgpack (1.6.0)
Expand Down
7 changes: 7 additions & 0 deletions plugins/mollie/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.bundle/
log/*.log
pkg/
test/dummy/db/*.sqlite3
test/dummy/log/*.log
test/dummy/tmp/
test/dummy/.sass-cache
6 changes: 6 additions & 0 deletions plugins/mollie/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source "http://rubygems.org"

# Declare your gem's dependencies in foodsoft_mollie.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec
635 changes: 635 additions & 0 deletions plugins/mollie/LICENSE

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions plugins/mollie/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
FoodsoftMollie
==============

This project adds support for iDEAL payments using Mollie to Foodsoft.

* Make sure the gem is uncommented in foodsoft's `Gemfile`
* Enter your Mollie account details in `config/app_config.yml`

```yaml
# Mollie payment settings
mollie:
# API key for account: 1234567, website profile: FooInc
api_key: test_1234567890abcdef1234567890abcd
# Transaction fee per payment method, fixed rate and/or percentage.
# This is substracted from the amount actually credited to the ordergroup's account balance.
fee:
# example fees from May 2014 incl. 21% VAT (verify before using!)
ideal: 1.20
banktransfer: 0.30
creditcard: 3.39% + 0.05
paypal: 0.18 + 0.35 + 3.4%
```
The transaction fee is sub will be added on each payment when set; it is not set by default,
meaning that the foodcoop will pay any transaction costs (out of the margin).
To initiate a payment, redirect to `new_payments_mollie_path` at `/:foodcoop/payments/mollie/new`.
The following url parameters are recognised:
* ''amount'' - default amount to charge (optional)
* ''fixed'' - when "true", the amount cannot be changed (optional)
* ''title'' - page title (optional)
* ''label'' - label for amount (optional)
* ''min'' - minimum amount accepted (optional)

This plugin also introduces the foodcoop config option `use_mollie`, which can
be set to `false` to disable this plugin's functionality. May be useful in
multicoop deployments.
26 changes: 26 additions & 0 deletions plugins/mollie/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env rake
begin
require 'bundler/setup'
rescue LoadError
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end
begin
require 'rdoc/task'
rescue LoadError
require 'rdoc/rdoc'
require 'rake/rdoctask'
RDoc::Task = Rake::RDocTask
end

RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'FoodsoftMollie'
rdoc.options << '--line-numbers'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end

# APP_RAKEFILE = File.expand_path("../../../Rakefile", __FILE__)
# load 'rails/tasks/engine.rake'

Bundler::GemHelper.install_tasks
160 changes: 160 additions & 0 deletions plugins/mollie/app/controllers/payments/mollie_ideal_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Mollie payment page
class Payments::MollieIdealController < ApplicationController
before_action -> { require_plugin_enabled FoodsoftMollie }
skip_before_action :authenticate, :only => [:check]
skip_before_action :verify_authenticity_token, :only => [:check]
before_action :accept_return_to, only: [:new]
before_action :ordergroup, only: [:new, :create, :result]
before_action :transaction, only: [:result]
before_action :configure_api_key

def new
@amount = (params[:amount] or [10, -@ordergroup.get_available_funds].max)
@amount = [params[:min], params[:amount]].max if params[:min]
end

def create
# store parameters so we can redirect to original form on problems
session[:mollie_params] = params.select { |k, _| %w[amount label title fixed min text].include?(k) }.to_h

amount = params[:amount].to_f
amount = [params[:min].to_f, amount].max if params[:min]

redirect_to new_payments_mollie_path(session[:mollie_params]), alert: t('.invalid_amount') and return if amount <= 0

ft_type = FinancialTransactionType.find_by_id(FoodsoftConfig[:mollie][:financial_transaction_type]) || FinancialTransactionType.first

@transaction = FinancialTransaction.create!(
amount: nil,
ordergroup: @ordergroup,
user: @current_user,
payment_plugin: 'mollie',
payment_amount: amount,
# @todo payment_currency
payment_state: 'created',
financial_transaction_type_id: ft_type.id,
note: t('.controller.transaction_note', method: nil)
)

payment = Mollie::Payment.create(
amount: {
value: format('%.2f', amount),
currency: 'EUR' # @todo payment_currency
},
description: "#{@transaction.id}, #{@ordergroup.id}, #{FoodsoftConfig[:name]}",
redirectUrl: result_payments_mollie_url(id: @transaction.id),
webhookUrl: (request.local? ? check_payments_mollie_url : 'https://example.com'), # Mollie doesn't accept localhost here
metadata: {
scope: FoodsoftConfig.scope,
transaction_id: @transaction.id,
user: @current_user.id,
ordergroup: @ordergroup.id
}
)
@transaction.update payment_id: payment.id, payment_state: 'pending'

logger.info "Mollie start: #{amount} for ##{@current_user.id} (#{@current_user.display})"

redirect_to payment.checkout_url, allow_other_host: true
rescue Mollie::Exception => e
Rails.logger.info "Mollie create warning: #{e}"
redirect_to new_payments_mollie_path(session[:mollie_params]), :alert => t('errors.general_msg', msg: e.message)
end

# Endpoint that Mollie calls when a payment status changes.
def check
logger.info "Mollie check: #{params[:id]}"
@transaction = FinancialTransaction.find_by_payment_plugin_and_payment_id!('mollie', params[:id])
logger.debug " financial transaction: #{@transaction.inspect}"
render plain: update_transaction(@transaction)
rescue StandardError => e
Rails.logger.error "Mollie check error: #{e}"
render plain: "Error: #{e.message}"
end

# User is redirect here after payment
def result
update_transaction @transaction # @todo if request.local? # so localhost works too
logger.info "Mollie result: transaction #{@transaction.id} (#{@transaction.payment_id}) is #{@transaction.payment_state}"
case @transaction.payment_state
when 'paid'
redirect_to_return_or root_path, :notice => t('.controller.result.notice', amount: @transaction.amount, fee: @transaction.payment_fee)
when 'pending'
redirect_to_return_or root_path, :notice => t('.controller.result.wait')
else
pms = { foodcoop: FoodsoftConfig.scope }.merge((session[:mollie_params] or {}))
session[:mollie_params] = nil
redirect_to new_payments_mollie_path(pms), :alert => t('.controller.result.failed')
end
end

def cancel
redirect_to_return_or root_path
end

protected

def ordergroup
@ordergroup = current_user.ordergroup
redirect_to root_path, alert: t('.no_ordergroup') and return if @ordergroup.nil?
end

def transaction
@transaction = @ordergroup.financial_transactions.find(params[:id])
end

# @todo move this to ApplicationController, use it in SessionController too
# @todo use a stack of return_to urls
def accept_return_to
session[:return_to] = nil # or else an unfollowed previous return_to may interfere
return if params[:return_to].blank?

return unless params[:return_to].starts_with?(root_path) || params[:return_to].starts_with?(root_url)

session[:return_to] = params[:return_to]
end

def redirect_to_return_or(fallback_url, options = {})
if session[:return_to].present?
redirect_to_url = session[:return_to]
session[:return_to] = nil
else
redirect_to_url = fallback_url
end
redirect_to redirect_to_url, options
end

# Query Mollie status and update financial transaction
def update_transaction(transaction)
payment = Mollie::Payment.get transaction.payment_id
logger.debug "Mollie update_transaction: #{payment.inspect}"
update_transaction_details(transaction, payment)
update_transaction_amount(transaction, payment)
payment.status
end

def update_transaction_amount(transaction, payment)
if payment.status == 'paid'
payment_fee = FoodsoftMollie.payment_fee payment.amount.value, transaction.payment_method
amount = payment.amount.value.to_f - payment_fee.to_f
end
transaction.update payment_state: payment.status, amount: amount || 0, payment_fee: payment_fee || nil
end

def update_transaction_details(transaction, payment)
if payment.details
transaction.payment_acct_number = payment.details.consumerAccount
transaction.payment_acct_name = payment.details.consumerName
end
transaction.payment_method = payment.method
transaction.note = t('.controller.transaction_note', method: payment.method)
end

private

def configure_api_key
Mollie::Client.configure do |config|
config.api_key = FoodsoftConfig[:mollie]['api_key']
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/ insert_after 'erb:contains(":webstats_tracking_code")'
%h4= 'Mollie'
= config_input form, :use_mollie, as: :boolean
= form.fields_for :mollie do |fields|
= config_input fields, :api_key, input_html: {class: 'input-xlarge'}
= config_input fields, :financial_transaction_type, :as => :select, :collection => FinancialTransactionType.order(:name).map { |t| [ t.name, t.id ] }
%h5= 'Fee'
= fields.fields_for :fee do |fields|
= config_input fields, :ideal, input_html: {class: 'input-xlarge'}
= config_input fields, :banktransfer, input_html: {class: 'input-xlarge'}
= config_input fields, :creditcard, input_html: {class: 'input-xlarge'}
= config_input fields, :paypal, input_html: {class: 'input-xlarge'}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/ insert_after 'erb[silent]:contains("<dashboard_ordergroup_mark>")'
= link_to new_payments_mollie_path do
= t '.credit_your_account'
%i.icon.icon-chevron-right
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/ insert_after 'erb[silent]:contains("<home_ordergroup_well_mark>")'
= link_to t('.credit_your_account'), new_payments_mollie_path, class: 'btn btn-secondary'
60 changes: 60 additions & 0 deletions plugins/mollie/app/views/payments/mollie_ideal/_form.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
- content_for :javascript do
:javascript
// @todo code duplication of foodsoft_mollie.rb
function paymentFee(amount, feeSpec) {
if (typeof feeSpec === 'number') {
return feeSpec;
}

return feeSpec.split('+').reduce((sum, c) => {
const parsed = c.trim().match(/^(.*)\s*%\s*$/);
if (parsed) {
return sum + (parseFloat(parsed[1]) / 100 * parseFloat(amount));
} else {
return sum + parseFloat(c);
}
}, 0).toFixed(2);
}

function handleInputAmount(){
var amount = parseFloat($('#amount').val());
var isError = false;
$('#fee_list').children('#fee').each(function(){
var fee = $(this).data('fee');
if (amount){
$(this).text(I18n.l("currency", paymentFee(amount, fee)));
} else {
$(this).text(fee);
}
});
}

$('#amount').on('keyup', handleInputAmount);
$(document).ready(handleInputAmount);

%h5= t '.fee'
%dl.dl-horizontal#fee_list
- FoodsoftConfig[:mollie][:fee].each do |k, v|
%dt= k
%dd{id: "fee", data: { fee: v}}"
= form_tag payments_mollie_path, method: :post do
- if params[:text]
.well= params[:text]
.control-group
.control-label
= label_tag 'amount', ((params[:label] or t('.amount_pay')))
.controls
.input-prepend
%span.add-on= t 'number.currency.format.unit'
= text_field_tag 'amount', @amount, readonly: (params[:fixed]=='true'), class: 'input-mini'
- if params[:min]
.help-inline{style: 'margin-bottom: 10px'}
= "(min #{number_to_currency params[:min], precision: 0})"
.control-group
.controls
= submit_tag t('.submit')
= link_to t('ui.or_cancel'), cancel_payments_mollie_path

-# pass through options to allow reusing on error
- %w(label title fixed min text).each do |k|
= hidden_field_tag k, params[k] if params[k]
2 changes: 2 additions & 0 deletions plugins/mollie/app/views/payments/mollie_ideal/new.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- title (params[:title] or t('.title'))
= render :partial => 'form'
35 changes: 35 additions & 0 deletions plugins/mollie/config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
en:
activerecord:
attributes:
home:
credit_your_account: 'Credit your Account'
payments:
mollie_ideal:
new:
title: 'Credit your account'
no_ordergroup: 'You need to be member of an ordergroup'
create:
invalid_amount: 'Invalid amount'
form:
amount_pay: 'Amount to pay'
method: 'Pay using'
submit: 'Pay at your bank'
financial_transaction_type: 'Financial Transaction Type'
fee: 'The transaction fee depends on your payment method'
controller:
result:
notice: 'Your account was credited %{amount} (transaction fee of %{fee}).'
failed: 'Payment failed.'
wait: 'Your account will be credited when the payment is received.'
transaction_note: '%{method} payment'
config:
keys:
use_mollie: 'Use Mollie'
mollie:
api_key: 'API key'
financial_transaction_type: 'Transaction type'
fee:
ideal: 'Ideal'
banktransfer: 'banktransfer'
creditcard: 'creditcard'
paypal: 'paypal'
Loading

0 comments on commit 0de7feb

Please sign in to comment.