Skip to content

Commit

Permalink
add online payment plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
wvengen committed Nov 28, 2013
1 parent bed9f6a commit ef824ea
Show file tree
Hide file tree
Showing 42 changed files with 2,002 additions and 0 deletions.
7 changes: 7 additions & 0 deletions lib/foodsoft_adyen/.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 lib/foodsoft_adyen/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_adyen.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 lib/foodsoft_adyen/LICENSE

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions lib/foodsoft_adyen/README.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
= FoodsoftAdyen

This project adds support for Adyen payments to Foodsoft.

* Make sure the gem is uncommented in foodsoft's `Gemfile`
* Enter your Adyen account details in `config/environments/production.rb` (or `development.rb`)


== Configuration

The Adyen notifications API is used to credit accounts. That means that you
need to enable notifications the Adyen customer area:

* Log into https://ca-live.adyen.com/
* Choose your merchant account
* Go to `Settings` then `Notifications`
* Enter the following settings:
* URL: `https://your.foodsoft.host/path/f/payments/adyen/notify`
* Active: `yes`
* Method: `HTTP POST (parameters)`
* Populate SOAP header: `no`
* In `Authentication`, specify the user and password you set earlier in
your environment configuration in `config.foodsoft_adyen.notify_username` and
`config.foodsoft_adyen.notify_password`. It anyone knows these, they can
credit accounts, so do make sure to use a long password that's impossible to
remember.
* Press `Save Settings`

Now use the option to test a notification (below in the same Adyen CA screen).
Check your rails log file to see if the notification was received properly.


== PIN payment flow

PIN payments are done using the mobile Adyen app. At the time of writing, this is
somewhat new, and the app may not be fully stabilised yet; but it should be usable.

The flow is like this:

1. In `/f/payments/adyen/pin`, an ordergroup is selected.
2. This redirects to the Adyen app on the mobile platform. The query string in the
redirect is used to pass amount and description.
3. The Adyen app processes the payment.
4. The Adyen app redirects to the foodsoft callback page, with a `result` parameter
indicating success or failure.
5. The foodsoft user-interface shows whether it succeeded or not. No financial
transaction is added, however, since that is taken care of by the
Adyen HTTP POST notification. This also makes sure that no url tampering
on the browser can be used to credit accounts without real payments.
6. Shortly after, that might take a couple of minutes, Adyen will call the
notification URL, where the user's account can be updated.

27 changes: 27 additions & 0 deletions lib/foodsoft_adyen/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/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 = 'FoodsoftAdyen'
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

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Javascript specific to mobile devices
*/
//= require jquery.mobile

$(function() {
$('[data-role="popup"][data-immediate="true"]').popup('open');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/*
*= require jquery.mobile
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require 'base64'

class Payments::AdyenNotificationsController < ApplicationController
skip_before_filter :verify_authenticity_token, :only => [:notify]
skip_before_filter :authenticate, :only => [:notify]
before_filter :authenticate_adyen, :only => [:notify]

class WrongCurrencyException < Exception; end
class UserNotFoundException < Exception; end
class NotificationDataException < Exception; end

def notify
notification = AdyenNotification.log(params)
if notification.successful_authorisation?
data = decode_notification_data(notification.merchant_reference)
(user = User.find(data[:g])) rescue raise UserNotFoundException
notification.currency == Rails.configuration.foodsoft_adyen.currency or raise WrongCurrencyException
notice = "#{notification.payment_method} payment (Adyen #{notification.psp_reference})"
amount = notification.value/100.0
@transaction = FinancialTransaction.new(:user=>user, :ordergroup=>user.ordergroup, :amount=>amount, :note=>notice)
@transaction.add_transaction!
logger.debug 'foodsoft_adyen: handled authorisation notification'
else
logger.debug 'foodsoft_adyen: nothing to do'
end
ws_return :accepted
rescue NotificationDataException => e
ws_return :rejected, "merchant_reference #{e}"
rescue UserNotFoundException
ws_return :rejected, 'merchant_reference does not contain a valid user'
rescue WrongCurrencyException
ws_return :rejected, "foodsoft_adyen configuration only accepts currency #{Rails.configuration.foodsoft_adyen.currency}"
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
# Validation failed, because of the duplicate check.
# So ignore this notification, it is already stored and handled.
logger.debug 'foodsoft_adyen: notification already handled, ignoring'
ws_return :accepted
rescue Exception => e
ws_return :error, e
end

protected
def authenticate_adyen
authenticate_or_request_with_http_basic do |username, password|
username == Rails.configuration.foodsoft_adyen.notify_username && password == Rails.configuration.foodsoft_adyen.notify_password
end
end

def ws_return(status, msg=nil)
if status == :rejected
logger.warn "foodsoft_adyen: #{msg}"
elsif status == :error
logger.error msg
logger.error(Rails.backtrace_cleaner.clean(msg.backtrace).map{|x| " #{x}"}.join("\n")) if msg.is_a? Exception
end
render :text => ("[#{status}]" + (msg.nil? ? '' : " #{msg}"))
end

# returns hash of foodsoft data for transaction
def decode_notification_data(data)
ActiveSupport::JSON.decode Base64.urlsafe_decode64(data.gsub(/^.*\((.*)\)\s*$/,'\1')), {symbolize_names: true}
rescue ActiveSupport::JSON.parse_error
raise NotificationDataException.new('does not contain valid JSON')
rescue ArgumentError
raise NotificationDataException.new('is not a URL-safe base64 encoded string (RFC 4648)')
end

end
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'base64'

class Payments::AdyenPinController < ApplicationController
before_filter :find_ordergroup
layout 'adyen_mobile'

# show list of ordergroups
def index
@ordergroups = Ordergroup.undeleted
@ordergroups = @ordergroups.where('name LIKE ?', "%#{params[:query]}%") unless params[:query].nil?
@ordergroups = @ordergroups.page(params[:page]).per(@per_page)
end

# show form for initiating a new payment
def new
#@adyen_pin_url = adyen_pin_url(@ordergroup.id, 4.99, 'hi there')
create
end

# initiate pin payment using Adyen app
def create
redirect_to adyen_pin_url(@ordergroup, @ordergroup.get_available_funds)
end

# callback url after payment
def created
index
render :index
end


protected

def find_ordergroup
@ordergroup = Ordergroup.find(params[:ordergroup_id]) rescue nil
end


private

def adyen_pin_url(ordergroup, amount)
opts = {
currency: Rails.configuration.foodsoft_adyen.currency,
amount: (amount * 100).to_i,
description: encode_notification_data({g: ordergroup.id}, ordergroup.name),
callback: created_payments_adyen_pin_url(:ordergroup_id => ordergroup.id), # or use opt sessionId
callbackAutomatic: 0,
start_immediately: true
}
if request.user_agent.match '\bAndroid\b'
return "http://www.adyen.com/android-app/payment?#{opts.to_query}"
else #elsif request.user_agent.match '\b(iPod|iPhone|iPad)\b'
return "adyen://payment?#{opts.to_query}"
end
end

def encode_notification_data(data, title=nil)
d = Base64.urlsafe_encode64 data.to_json
return [title, "(#{d})"].compact.join(' ')
end

end
71 changes: 71 additions & 0 deletions lib/foodsoft_adyen/app/models/adyen_notification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# The +AdyenNotification+ class handles notifications sent by Adyen to your servers.
#
# Because notifications contain important payment status information, you should store
# these notifications in your database. For this reason, +AdyenNotification+ inherits
# from +ActiveRecord::Base+, and a migration is included to simply create a suitable table
# to store the notifications in.
#
# Adyen can either send notifications to you via HTTP POST requests, or SOAP requests.
# Because SOAP is not really well supported in Rails and setting up a SOAP server is
# not trivial, only handling HTTP POST notifications is currently supported.
#
# @example
# @notification = AdyenNotification.log(request)
# if @notification.successful_authorisation?
# @invoice = Invoice.find(@notification.merchant_reference)
# @invoice.set_paid!
# end
class AdyenNotification < ActiveRecord::Base

# A notification should always include an event_code
validates_presence_of :event_code

# A notification should always include a psp_reference
validates_presence_of :psp_reference

# A notification should be unique using the composed key of
# [:psp_reference, :event_code, :success]
validates_uniqueness_of :success, :scope => [:psp_reference, :event_code]

# Make sure we don't end up with an original_reference with an empty string
before_validation { |notification| notification.original_reference = nil if notification.original_reference.blank? }

# Logs an incoming notification into the database.
#
# @param [Hash] params The notification parameters that should be stored in the database.
# @return [Adyen::Notification] The initiated and persisted notification instance.
# @raise This method will raise an exception if the notification cannot be stored.
# @see Adyen::Notification::HttpPost.log
def self.log(params)
converted_params = {}

# Assign explicit each attribute from CamelCase notation to notification
# For example, merchantReference will be converted to merchant_reference
self.new.tap do |notification|
params.each do |key, value|
setter = "#{key.to_s.underscore}="
notification.send(setter, value) if notification.respond_to?(setter)
end
notification.save!
end
end

# Returns true if this notification is an AUTHORISATION notification
# @return [true, false] true iff event_code == 'AUTHORISATION'
# @see Adyen.notification#successful_authorisation?
def authorisation?
event_code == 'AUTHORISATION'
end

alias_method :authorization?, :authorisation?

# Returns true if this notification is an AUTHORISATION notification and
# the success status indicates that the authorization was successfull.
# @return [true, false] true iff the notification is an authorization
# and the authorization was successful according to the success field.
def successful_authorisation?
authorisation? && success?
end

alias_method :successful_authorization?, :successful_authorisation?
end
18 changes: 18 additions & 0 deletions lib/foodsoft_adyen/app/views/layouts/adyen_mobile.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
- content_for :head do
= stylesheet_link_tag "payments/adyen_mobile", :media => "all"

- content_for :javascript do
= javascript_include_tag "payments/adyen_mobile"

= render layout: 'layouts/header' do
%div{:data => {:role => :page}}

- if show_title?
%div{:data => {:role => :header, :position => :fixed}}
%h1= yield(:title)

%div#messages
= bootstrap_flash

%div{:data => {:role => :content}}
= yield
21 changes: 21 additions & 0 deletions lib/foodsoft_adyen/app/views/payments/adyen_pin/_created.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
- if params[:result] == 'APPROVED'
%h3.ui-title Payment done
%p Thanks! Your account will be updated in a couple of minutes.

- elsif params[:result] == 'CANCELLED'
%h3.ui-title Payment cancelled
%p The transaction was cancelled. #{params[:cancelMessage]}

- elsif params[:result] == 'DECLINED'
%h3.ui-title Payment declined
%p I'm sorry, it didn't work out. #{params[:declineMessage]}

- else
%h3.ui-title Payment failed
%p Something went wrong, sorry! #{params[:errorMessage]}


= link_to t('ui.close'), '#', :data => { :role => :button, :rel => :back, :theme => 'b' }
- if params[:result] != 'APPROVED' and @ordergroup
= link_to 'Retry', new_payments_adyen_pin_path(:ordergroup_id => @ordergroup.id), :data => {:role => :button, :ajax => :false}

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
%ul{:data => {:role => :listview, :filter => :true}}
- for ordergroup in @ordergroups
%li
= link_to new_payments_adyen_pin_path(:ordergroup_id => ordergroup.id), :data => { :ajax => :false } do
= ordergroup.name
%p.ui-li-aside= ordergroup.users.map(&:name).join ','
18 changes: 18 additions & 0 deletions lib/foodsoft_adyen/app/views/payments/adyen_pin/index.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
- title t('.title')

-# payment response dialog
- unless params[:result].blank?
%div#paymentResult{:data => {:role => :popup, 'overlay-theme' => 'a', :shadow => :true, :immediate => :true, :transition => 'pop'}}
%div{:data => {:role => :content}}
= render :partial => "created"

- content_for :javascript do
:javascript
// auto-close dialog so next member can use it
setTimeout(function() {
$('#paymentResult').popup('close');
}, 8*1000);

#orderGroupsTable
= render :partial => "ordergroups"

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$('#ordergroupsTable').html('#{escape_javascript(render("ordergroups"))}');
4 changes: 4 additions & 0 deletions lib/foodsoft_adyen/app/views/payments/adyen_pin/new.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- title t('.title')

= link_to "do the payment", @adyen_pin_url

7 changes: 7 additions & 0 deletions lib/foodsoft_adyen/config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
en:
payments:
navigation:
pin: PIN terminal
adyen_pin:
index:
title: PIN payment
Loading

0 comments on commit ef824ea

Please sign in to comment.