Skip to content

Commit

Permalink
feat: add online payment provider mollie plugin (#1008 , PR #1031)
Browse files Browse the repository at this point in the history
Co-authored-by: wvengen <willem@thequestionmark.org>
  • Loading branch information
yksflip and wvengen committed Apr 8, 2024
1 parent 113a14d commit 0b7898d
Show file tree
Hide file tree
Showing 20 changed files with 1,090 additions and 0 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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
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.

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

This project adds support for various online payment methods 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
use_mollie: true
# Mollie payment settings
mollie:
# API key for account: 1234567, website profile: FooInc
api_key: test_1234567890abcdef1234567890abcd
# Transaction fee as provided by mollie api (only EUR supported)
charge_fees: true
currency: EUR # should match the foodcoop's currency
```
When charge_fees is set true, the transaction fee will be added on each payment. At the moment fees are only supported with EUR.
It is disabled 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
144 changes: 144 additions & 0 deletions plugins/mollie/app/controllers/payments/mollie_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Mollie payment page
class Payments::MollieController < ApplicationController
before_action -> { require_plugin_enabled FoodsoftMollie }
skip_before_action :authenticate, only: [:check]
skip_before_action :verify_authenticity_token, only: [:check]
before_action :validate_ordergroup_presence, only: %i[new create result]
before_action :payment_methods, only: %i[new create]

def new
params.permit(:amount, :min)
# if amount or minimum is given use that, otherwise use a default based on the ordergroups funds or 10
@amount = [params[:min], params[:amount]].compact.max || [FoodsoftMollie.default_amount, @ordergroup.get_available_funds * -1].max # @todo extract
end

def create
# store parameters so we can redirect to original form on problems
session[:mollie_params] = params.permit(:amount, :payment_method, :label, :title, :fixed, :min, :text, :payment_fee)

amount = [params[:min].to_f, params[:amount].to_f].compact.max
payment_fee = params[:payment_fee].to_f
amount += payment_fee

redirect_on_error(t('.invalid_amount')) and return if amount <= 0

method = fetch_mollie_methods.find { |m| m.id == params[:payment_method] }
transaction = create_transaction(amount, payment_fee, method)
payment = create_payment(transaction, amount, method)
transaction.update payment_id: payment.id
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_on_error t('errors.general_msg', msg: e.message)
end

# Endpoint that Mollie calls when a payment status changes.
# See: https://docs.mollie.com/overview/webhooks
def check
transaction = FinancialTransaction.find_by_payment_plugin_and_payment_id!('mollie', params.require(:id))
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
transaction = @ordergroup.financial_transactions.find(params.require(:id))
update_transaction transaction
case transaction.payment_state
when 'paid'
redirect_to root_path, notice: t('.controller.result.notice', amount: "#{transaction.payment_currency} #{transaction.amount}")
when 'open', 'pending'
redirect_to root_path, notice: t('.controller.result.wait')
else
redirect_on_error t('.controller.result.failed')
end
end

def cancel
redirect_to root_path
end

private

# Query Mollie status and update financial transaction
def update_transaction(transaction)
payment = Mollie::Payment.get(transaction.payment_id, api_key: FoodsoftMollie.api_key)
logger.debug "Mollie update_transaction: #{transaction.inspect} with payment: #{payment.inspect}"
if payment.paid?
amount = payment.amount.value.to_f
amount -= transaction.payment_fee if FoodsoftMollie.charge_fees?
transaction.update! amount: amount
end
transaction.update! payment_state: payment.status
end

def payment_methods
@payment_methods = fetch_mollie_methods
@payment_methods_fees = @payment_methods.to_h do |method|
[method.id, method.pricing.map do |pricing|
{
description: pricing.description,
fixed: { currency: pricing.fixed.currency, value: pricing.fixed.value.to_f },
variable: pricing.variable.to_f
}
end.to_json]
end
end

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

def create_transaction(amount, payment_fee, method)
financial_transaction_type = FinancialTransactionType.find_by_id(FoodsoftConfig[:mollie][:financial_transaction_type]) || FinancialTransactionType.first
note = t('.controller.transaction_note', method: method.description)

FinancialTransaction.create!(
amount: nil,
ordergroup: @ordergroup,
user: @current_user,
payment_plugin: 'mollie',
payment_amount: amount,
payment_fee: payment_fee,
payment_currency: FoodsoftMollie.currency,
payment_state: 'open',
payment_method: method.id,
financial_transaction_type: financial_transaction_type,
note: note
)
end

def create_payment(transaction, amount, method)
Mollie::Payment.create(
amount: {
value: format('%.2f', amount),
currency: FoodsoftMollie.currency
},
method: method.id,
description: "#{FoodsoftConfig[:name]}: Continue to add credit to #{@ordergroup.name}",
redirectUrl: result_payments_mollie_url(id: transaction.id),
webhookUrl: request.local? ? 'https://localhost.com' : check_payments_mollie_url, # Workaround for local development
metadata: {
scope: FoodsoftConfig.scope,
transaction_id: transaction.id,
user: @current_user.id,
ordergroup: @ordergroup.id
},
api_key: FoodsoftMollie.api_key
)
end

def redirect_on_error(alert_message)
pms = { foodcoop: FoodsoftConfig.scope }.merge((session[:mollie_params] || {}))
session[:mollie_params] = nil
redirect_to new_payments_mollie_path(pms), alert: alert_message
end

def fetch_mollie_methods
Mollie::Method.all(include: 'pricing,issuers', amount: { currency: FoodsoftMollie.currency, value: format('%.2f', FoodsoftMollie.default_amount) }, api_key: FoodsoftMollie.api_key)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/ 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, as: :string, input_html: {class: 'input-xlarge'}
= config_input fields, :financial_transaction_type, :as => :select, :collection => FinancialTransactionType.order(:name).map { |t| [ t.name, t.id ] }
= config_input fields, :charge_fees, as: :boolean
= config_input fields, :currency, as: :string, input_html: {class: 'input-xlarge'}
= config_input fields, :default_amount, as: :float
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'
57 changes: 57 additions & 0 deletions plugins/mollie/app/views/payments/mollie/_form.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
- content_for :javascript do
- if FoodsoftMollie.charge_fees?
:javascript
function paymentFee(amount, fixed, variable) {
return fixed + (amount * (variable/100));
}

function handleInputAmount(){
const payment_method = $('#payment_method').val();
const amount = parseFloat($('#amount').val());
$('#payment_fee').empty();
$('#fee_list').data(payment_method).forEach (fee => {
const calculatedFee = paymentFee(amount, fee.fixed.value, fee.variable).toFixed(2);
const currency = fee.fixed.currency;
$('#payment_fee')
.append($("<option></option>")
.attr("value", calculatedFee)
.text(`${currency} ${calculatedFee} (${fee.description})`));
});
}
$('#amount').on('keyup', handleInputAmount);
$('#payment_method').on('change', handleInputAmount);
$(document).ready(handleInputAmount);

= 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})"
= hidden_field_tag 'min', params[:min]
.control-group
.control-label
= label_tag 'payment_method', t('.payment_method')
.controls
= select_tag 'payment_method', options_for_select(@payment_methods.map { |p| [p.description, p.id] }, params[:payment_method]), class: 'input-large'
- if FoodsoftMollie.charge_fees?
.control-group
.control-label
= label_tag 'payment_fee', t('.fee')
.controls
#fee_list{data: @payment_methods_fees }= select_tag 'payment_fee'
.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/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'
46 changes: 46 additions & 0 deletions plugins/mollie/config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
en:
activerecord:
attributes:
home:
credit_your_account: Credit your Account
home:
index:
credit_your_account: Credit your Account
ordergroup:
credit_your_account: Credit your Account
payments:
mollie:
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 online
financial_transaction_type: Financial Transaction Type
fee: Select the appropriate transaction costs
controller:
result:
notice: Your account was credited %{amount}.
failed: Payment failed.
wait: Your account will be credited when the payment is received.
transaction_note: '%{method} payment'
config:
hints:
use_mollie: Let members credit their own Foodsoft ordergroup account with online payments using Mollie.
mollie:
api_key: You find the API-Key in the Mollie Dashboard. Use the Live API-Key here (or the Test API-Key for a demo instance).
financial_transaction_type: Choose the transaction type mollie payments should be assigned.
charge_fees: Charge ordergroups the transaction fees applied by mollie. This is currently only available for EUR.
currency: Choose the currency mollie should use (ISO code)
default_amount: The default amount to credit the ordergroup with.
keys:
use_mollie: Use Mollie
mollie:
api_key: API key
financial_transaction_type: Transaction type
charge_fees: Charge transaction fees
currency: Currency
default_amount: Default amount
Loading

0 comments on commit 0b7898d

Please sign in to comment.