Custom payment gateway

From GroupScript documentation

Jump to: navigation, search

Implementing a custom payment gateway is not an easy task - it involves an in-depth knowledge of programming and data exchange protocol used by the chosen payment gateway.

Contents

Requirements

Our software is not a simple shopping cart on the side of payment processing. We need your chosen payment gateway to support either of these modes of operation:

  1. User's credit card get's authorized, and we automatically capture it if deal tips (e.g. 30 people sign up for deal). If the deal does not tip, the transactions for this deal are voided and money is released back to customers.
  2. User adds his credit card, and it is only charged when the deal tips. BUT the API needs to provide a way to store user's card data on their servers - it is too much work and responsibility for us to securely store credit card data on customer's server.

Getting started

All payment backends are stored in folder system/application/payment_backends/
To add a new payment backend, you need to create a file which will contain class with the same name as file name, but the first letter needs to be in upper-case (e.g. paypal_dp.php contains class Paypal_dp). This class has to be inherited from Base_backend (the base_backend.php file is not included automatically - you have to include it manually).

Required methods

name()

Returns gateway name.
Accepted return type:

string - gateway name.

description()

Returns gateway description.
Accepted return type:

string - gateway description.

button()

Returns HTML code that is displayed in payment method selection view.
Accepted return type:

string - html code for button.

view_checkout()

To display view with order total, receiver and further instructions for buyer.
This method is called after gateway select button is pressed and order is created and saved to database.
In there you can display form to enter credit card (or any other data) or redirect user somewhere.

do_capture()

Is called from cronjob.
Must not use die() or any other code that causes script to die.
Backend method that does 'capture' of the current order because deal tipped. In this method you should "capture" the order, by charging the required amount from client's account.
And then if capture is successful you need to call:

$this->order->setStatusCaptured();
$this->postCaptureSuccessful();

To send coupons and assign money to affiliates.

do_void()

Is called from cronjob.
Must not use die() or any other code that causes script to die.
Backend method that 'voids' the current order, because deal did not tip. In this method you should "void" the order, by making a refund, cancelling authorization and/or simply marking order as "voided" - so admin can refund it manually.

Optional methods

Overriding these is optional.

__construct($order=null)

Class constructor - to load stuff that will be needed for correct operation of gateway (Custom APIs, settings, etc).

view_confirm()

To display final confirmation for buyer.
User can be redirected to this view by calling:

redirect(site_url('frontend/buy/view_confirm'));

is_active()

If this gateway is active and is shown on payment method selection view.
Accepted return type:

Boolean - true if active

Base backend methods

These should not be overridden, but should be called by your backend at some points.

postTransactionSuccessful()

Should be called after user is paid for order successfully (payment or authorization is successful).
This method assigns pending credit transfers for referrals.
Handles discount coupons.
Sends confirmation email to buyer.
If deal has tipped - calls do_capture();

postCaptureSuccessful()

Should be called after successful "capture". Sends coupons and assgins money to affiliates.

Payment notification listeners

Some payment gateways (Paypal Website Payments Standard for example) send notifications about payment status directly to your server. In that case you need to implement a notification listener.
In pattern:

callback_your_gateway()

Where your_gateway is your gateway's class name in lower-case. It then can be accessed from outside by url like this

http://yoursite.com/index.php/frontend/buy/callback_your_gateway

This listener can be accessed by anyone - and they can send anything to it.
Basic flow:

  1. You need to make sure that request is legitimate and really comes from your desired payment gateway.
  2. If request is legitimate load order from database like this:
$this->order = Doctrine_Core::getTable('Order')->findOneById($order_id);
  1. Set order status according to received message (see below).
  2. Check that payment amount is greater or equal than order netto amount.
  3. If payment status is successful then - set order status to 'transaction_successful' like this:
	
$this->order->setStatusSuccessful();
$this->postTransactionSuccessful();

Order object

Is available in all gateway methods - except listener callback where it should be loaded manually. Can be accessed with:

$this->order

Fields

It contains these fields

integer id - order id - should be loaded from db by this id
integer campaign_id - campaign's id that is associated with this order
integer receiver_id - user id that will receive this order (if not bought for friend - the same as buyer id)
integer unregistered_receiver_id - receiver id if he is not registered (integer when bought for friend - otherwise null)
integer buyer_id - user id that bought and paid for this order
integer coupon_count - how many coupons are bought 
decimal ammount - order amount without discount
decimal discount - discount amount - can be passed separately to payment gateway
decimal netto_ammount - full order amount with quantity and discounts taken into account
string status - you can use this field to check order status. (do not use it for setting status - use provided methods instead).
string payment_method - payment gateway name
object Campaign - campaign which is associated with this order
object OrderBuyerUser -  user who bought this order
object OrderReceiverUser - user who received this order

Methods

To load order manually call:

$this->order = Doctrine_Core::getTable('Order')->findOneById($order_id);

findOneById($order_id)

parameter

integer $order_id - id of requested order.

To set order status - call methods below. Order can be set to following statuses:

setStatusSuccessful()

Sets order status to 'transaction_successful'.
Increases campaign's bought coupon count.
Creates coupons for sending.

Other statuses

Sets order status - does not do anything else.
setStatusPending() - 'transaction_pending'
setStatusCaptured() - 'captured'
setStatusVoided() - 'voided'
cancel() - 'cancelled'
setStatusVoidFailed() - 'void_failed'
setStatusCaptureFailed() - 'capture_failed'

Campaign object

Can be accessed:

$this->order->Campaign

Fields

integer id - campaign id - can be passed as item number.
string name - campaign name - can be passed as item name

Transaction logging

You can log all successful, unsuccessful transactions, hacking attempts, etc. using this method.

Doctrine_Core::getTable('TransactionLog')->createTransactionLog($request, $response, $order_id);

createTransactionLog($request, $response, $order_id)

parameters:

string $request - request to remote server
string $response - response from server
mixed $order_id - order id, or false

This log can later be viewed in backend > payments > transaction log. It is highly recommended that you log each request to/from remote servers using this method.

Examples

These examples show how to implement custom backends using fictional payment gateways.

Payment gateway without notification listener

This payment gateway functions similar to paypal direct payment.

  1. Collects user's credit card data.
  2. Sends them to gateway server to authorize order.
  3. Receives response from server.
    1. redirects to checkout success view.
    2. shows error message.

Then when cron job runs - if deal is marked as tipped or voided:

  1. when deal is tipped do_capture() is called.
  2. when deal is voided do_void() is called.

GroupScript does NOT contain example library and its functions - you should consult your payment gateway's manual on how to implement communication with remote server.


<?php

include_once(APPPATH . '/payment_backends/base_backend.php');
/*
 * Example gateway 1 (accepts credit cards)
 */

class Example_1 extends Base_backend {

	private $username;
	private $password;

//constructor is overridden to load custom library and settings
//fictional API library is loaded as $this->CI->example;
//this API also requires username and password that are configured in backend
	public function __construct() {
		$args = func_get_args();
		call_user_func_array(array('parent', '__construct'), $args);
		$this->CI->load->library('example');
		$this->username = get_option('example_username');
		$this->password = get_option('example_password');
	}

	//returns backend name
	public function name() {
		return _('Example gateway');
	}

	//shows form to enter cc data
	public function view_checkout() {
		parent::view_checkout();
		//checks if coupons are available
		if ($this->available_coupons()) {

			$campaign = $this->order->Campaign;
			$this->CI->ocular->set('campaign', $campaign);
			$this->CI->load->library('form_validation');
			$this->CI->config->load('payments');
			//form is not valid or user just clicked on payment selection button
			if (!$this->CI->form_validation->run('example_payment')) {
				$this->CI->ocular->view_name = 'Example/payment_form';
				$this->CI->ocular->set('title', _('Buy') . ' "' . $campaign->name . '" ' . _('now!'));
			}
			else { //valid form data submitted - post request to remote server
				//fictional API function that requests order authorization and receives a response from remote server.
				$response = $this->CI->example->do_authorization($_POST, $this->order, $this->username, $this->password);

				if ($response['success']) { //successful transaction - set order status as transaction_successful and redirect user to success view
					$this->order->setStatusSuccessful();
					$this->postTransactionSuccessful();

					redirect('frontend/buy/view_checkout_success');
				}
				else { //error - display cc form again and include response from remote server
					$this->CI->ocular->set('gateway_errors', $response['errors']);
				}
			}
			$this->CI->_render();
		}
	}

	//description that is shown in payment selection screen
	public function description() {
		return _('Purchase the coupon with example backend!');
	}

	//html code for button
	public function button() {
		return '<img onclick="choose_backend(\'' . __CLASS__ . '\');" id="id_' . __CLASS__ . '" src="' . CURRENT_THEME_URL . 'images/icons/buynow.gif" />';
	}

	public function do_capture() {
		parent::do_capture();
//fictional API function that captures authorized transaction and charges money from customer's account
		$response = $this->CI->example->capture_transaction($this->order, $this->username, $this->password);

		if ($response['success']) { //successful capture - mark as captured and send coupons
			$this->order->setStatusCaptured();
			$this->postCaptureSuccessful();
		}
		else {//check error code there. maybe transaction is already captured? maybe transaction is voided??
			if ($response['error'] == 'already_voided') { //transaction voided
				$this->order->setStatusVoided();
			}
			elseif ($response['error'] == 'already_captured') { //already captured
				$this->order->setStatusCaptured();
			}
			else { //capture failed for some other reason
				$this->order->setStatusCaptureFailed();
			}
		}
	}

	public function do_void() {
//fictional API function that cancels authorization - no money is charged
		$response = $this->CI->example->void_transaction($this->order, $this->username, $this->password);
		if ($response['success']) { //successful void - mark order as voided
			$this->order->setStatusVoided();
		}
		else { //void failure - something went wrong
			$this->order->setStatusVoidFailed();
		}
	}

	public function is_active() { //get option value to enable or disable this gateway
		return get_option('example_1_enabled');
	}

}

Payment gateway with notification listener

This gateway functions similar to Paypal Website Payments Standard.

  1. when order is created - it does not ask for additional data, but redirects user to gateway server using GET request.
  2. when user has either done payment or cancelled order remote server redirects him to:
    1. success url.
    2. cancel url.

Note that on success view we do not assume that transaction really was successful, but we wait for notification from gateway's server instead.

  1. when notification is received - mark order accordingly.

Then when cron job runs - if deal is marked as tipped or voided:

  1. when deal is tipped do_capture() is called.
  2. when deal is voided do_void() is called.
<?php

include_once(APPPATH . '/payment_backends/base_backend.php');

//example gateway with notification listener
class Example_two extends Base_backend {

	private $username;

	public function name() {
		return 'Example gateway with notification listener';
	}

	public function __construct($order = null) {
		$args = func_get_args();
		call_user_func_array(array('parent', '__construct'), $args);
		$this->CI->load->library('example');
		$this->username = get_option('example_two_username');
	}

	public function view_checkout() {
		$this->CI->load->helper('url');
		$order = $this->order;
		$campaign = $this->order->Campaign;

		$url = 'https://www.example.com/pay?'; //fictional url where user will be redirected when he presses payment method select button
		//append parameters to url
		$url.='cmd=checkout'; //fictional command to initiate checkout on remote server 
		$url.='&user=' . urlencode($this->username); 
		$url.='&item_name=' . urlencode($campaign->name); //pass campaign name as item name
		$url.='&item_number=' . urlencode($campaign->id); //campaign id as item number
		$url.='&invoice=' . urlencode($order->id); //order id as invoice
		$url.='&amount=' . urlencode($order->netto_ammount); //full order amount
		$url.='&success=' . urlencode(site_url('frontend/buy/view_confirm')); //url to redirect after successful payment
		$url.='&cancel=' . urlencode(site_url('frontend/buy/view_cancel')); //redirect after user cancels payment
		$url.='&listener_url=' . urlencode(site_url('frontend/buy/callback_example_two')); //notification listener url
		redirect($url); //redirect to payment gateway;
	}

	//notification listener callback
	function callback_example_two() {
		//fictional API function to if request is valid
		$response = $this->CI->example->validate_notification($_POST);
		$notification = $_POST;
		if ($response['valid']) { //request is valid - process it

			$notification['log_message_type'] = 'example_two_listener_message';
			$order_id = $ipn_message['invoice'];
			if (is_numeric($order_id)) { //check if order id is valid to load order from db
				//load order
				$this->order = Doctrine_Core::getTable('Order')->findOneById($order_id);
                               //check status and mark order accordingly
				if ($notification['status'] == 'refunded' ||
						$notification['status'] == 'reversed' ||
						$notification['status'] == 'failed' ||
						$notification['status'] == 'expired' ||
						$notification['status'] == 'denied' ||
						$notification['status'] == 'voided') {
					$this->order->setStatusVoided();
				}
				elseif ($notification['status'] == 'completed') {
					//check that order amount is OK
					if ($notification['amount'] >= $this->order->netto_ammount) {
						$this->order->setStatusSuccessful(); //set successful and generate coupons
						$this->postTransactionSuccessful();
					}
					else {
						$this->order->setStatusCaptureFailed();
					}
				}
				elseif ($notification['_status'] == 'pending') {
					$this->order->setStatusPending();
				}
			}
		}
		else {
			$notification['log_message_type'] = 'invalid_request';
		}
		//log all transactions
		$this->log_transaction($notification);
	}

	//logs request to database
	public function log_transaction($request) { //array
		$transaction = '';
		foreach ($request as $key => $value) {
			$transaction .= ' & ' . $key . ' = ' . urldecode($value);
		}
		if (isset($request['invoice'])) {
			$order_id = $request['invoice'];
		}
		else {
			$order_id = 0;
		}

		Doctrine_Core::getTable('TransactionLog')->createTransactionLog($transaction, '', $order_id);
	}

	public function do_capture() {
		//check if transaction was successful to send coupons
		if ($this->order->status == 'transaction_successful') {
			$this->order->setStatusCaptured();
			$this->postCaptureSuccessful();
		}
	}

	public function do_void() {
		$this->order->setStatusVoided(); //mark order as voided - so it can be refunded manually
	}

	public function description() {
		return _('Example gateway with notification listener');
	}

	public function button() {
		return '<img onclick="choose_backend(\'' . __CLASS__ . '\');" id="id_' . __CLASS__ . '" src="/public/images/icons/example_button.gif" />';
	}

	public function is_active() {
		return get_option('example_two_enabled');
	}

}
Personal tools

Groupon Clone