Skip to content

Commit

Permalink
add online payment provider mollie plugin
Browse files Browse the repository at this point in the history
Co-authored-by: wvengen <[email protected]>
  • Loading branch information
yksflip and wvengen committed Nov 14, 2023
1 parent d0d943c commit f777310
Show file tree
Hide file tree
Showing 19 changed files with 1,089 additions and 0 deletions.
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
charge_fees: true
currency: EUR # should match the foodcoop's currency
```
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
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: [:new, :create, :result]
before_action :payment_methods, only: [: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
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
44 changes: 44 additions & 0 deletions plugins/mollie/config/locales/nl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
nl:
activerecord:
attributes:
home:
credit_your_account: Geld storten
home:
index:
credit_your_account: Geld storten
ordergroup:
credit_your_account: Geld storten
payments:
mollie:
new:
title: Stort geld op je account
no_ordergroup: U dient lid te zijn van een huishouden
create:
invalid_amount: Ongeldig bedrag
form:
amount_pay: Te betalen bedrag
method: Hoe te betalen
submit: Betaal online
financial_transaction_type: Financiële-transactietype
fee: Selecteer de juiste transactiekosten
controller:
result:
notice: Er is €%{amount} bijgeschreven op uw account.
failed: Betaling mislukt.
config:
hints:
use_mollie: Laat gebruikers zelf geld op de rekening van hun Foodsoft huishouden zetten met online betalingen via Mollie.
mollie:
api_key: Vul hier de Live API-Key in, te vinden in het Mollie Dashboard (of de Test API-Key voor een Foodsoft demo).
financial_transaction_type: Kies het transactietype waaraan Mollie Payments moeten worden toegewezen
charge_fees: Breng huishouden de door Mollie gehanteerde transactiekosten in rekening. Dit is momenteel alleen beschikbaar voor EUR.
currency: Kies de valuta die Mollie moet gebruiken
default_amount: Het standaardbedrag waarmee de ordergroep wordt gecrediteerd.
keys:
use_mollie: Accepteer online betalingen via Mollie
mollie:
api_key: Mollie API-Key
financial_transaction_type: Transactietype
charge_fees: Transactiekosten in rekening brengen
currency: Valuta
default_amount: Standaard bedrag
Loading

0 comments on commit f777310

Please sign in to comment.