Open Source Billing

This commit is contained in:
Deon George
2013-10-10 13:44:53 +11:00
commit b02d70adf0
2344 changed files with 392978 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides CHECKOUT Plugin Support
*
* @package Checkout
* @category Plugins
* @author Deon George
* @copyright (c) 2009-2013 Open Source Billing
* @license http://dev.osbill.net/license.html
* @todo Does this need to be Serializable?
*/
abstract class Checkout_Plugin implements Serializable {
protected $co; // Our Checkout Object
protected $_object;
// Our required abstract classes
public function serialize() {
return (string)$this->_object;
}
public function unserialize($s) {
$this->_object = XML::factory(NULL,NULL,$s);
}
// Required abstract classes
// Present pre-plugin processing information
abstract public function before(Cart $co);
abstract public function notify(Model_Checkout_Notify $cno);
public function __construct(Model_Checkout $co) {
$this->co = $co;
}
}
?>

View File

@@ -0,0 +1,204 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides PAYPAL support
*
* @package Checkout
* @category Plugins
* @author Deon George
* @copyright (c) 2009-2013 Open Source Billing
* @license http://dev.osbill.net/license.html
*/
abstract class Checkout_Plugin_Paypal extends Checkout_Plugin {
protected $url_prod = 'www.paypal.com';
protected $url_test = 'www.sandbox.paypal.com';
private $ipn_test = '173.0.82.126';
protected $curlopts = array(
CURLOPT_CONNECTTIMEOUT => 60,
CURLOPT_FAILONERROR => TRUE,
CURLOPT_FOLLOWLOCATION => FALSE,
CURLOPT_HEADER => FALSE,
CURLOPT_HTTPPROXYTUNNEL => FALSE,
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYHOST => FALSE,
CURLOPT_SSL_VERIFYPEER => FALSE,
CURLOPT_VERBOSE => FALSE,
);
/**
* User return from Paypal after payment
*/
public function after(Cart $co) {
SystemMessage::add(array(
'title'=>_('Payment Processing'),
'type'=>'info',
'body'=>sprintf('Thank you for your payment with paypal. It will be processed and applied to your cart items automatically in due course.'),
));
HTTP::redirect('/');
}
/**
* User cancelled from Paypal and returned
*/
public function cancel(Cart $co) {
SystemMessage::add(array(
'title'=>_('Payment Cancelled'),
'type'=>'info',
'body'=>sprintf('Payment with Paypal was cancelled at your request.'),
));
HTTP::redirect('cart');
}
/**
* Paypal payment notification and verification
*/
public function notify(Model_Checkout_Notify $cno) {
$debug_mode = Kohana::$config->load('debug')->checkout_notify;
// If testing
if (! $cno->status OR $cno->processed OR ($debug_mode AND Request::$client_ip == $this->ipn_test))
return ('Thank you');
$co = Cart::instance(isset($cno->data['custom']) ? $cno->data['custom'] : '');
if (! $co->contents())
return _('Thank you!');
if (! $debug_mode) {
$request = Request::factory(sprintf('https://%s/cgi-bin/webscr',isset($cno->data['test_ipn']) ? $this->url_test : $this->url_prod))
->method('POST');
$request->client()->options(Arr::merge($this->curlopts,array(
CURLOPT_POSTFIELDS => Arr::merge(array('cmd'=>'_notify-validate'),$cno->data),
)));
$response = $request->execute();
}
switch ($debug_mode ? 'VERIFIED' : $response->body()) {
case 'VERIFIED':
// Verify that the IPN is for us.
// @todo This should be in the DB.
if ($cno->data['business'] == 'deon_1260578114_biz@graytech.net.au') {
switch ($cno->data['payment_status']) {
case 'Completed':
// Our cart items total.
$total = $co->total();
$po = ORM::factory('Payment');
// Does the payment cover the cart total?
if ($this->co->fee_passon AND $cno->data['mc_gross'] == $total+$this->co->fee($total)) {
// Store the amounts in an array, so we can pro-rata the fee to each item.
$amts = array();
// Add the fee to each item (pro-rated)
for ($c=1;$c<=$cno->data['num_cart_items'];$c++) {
// The payment fee - there should only be 1 of these, and it should be the last item.
// We assume fees are added to $po->items() which are invoices.
if (preg_match('/^0:/',$cno->data['item_number'.$c])) {
$i = $j = 0;
foreach ($po->items() as $pio) {
$io = ORM::factory('Invoice',$pio->invoice_id);
// @todo Need to do tax.
$iio = $io->add_item();
$iio->quantity = 1;
$iio->module_id = $cno->mid()->id;
$iio->item_type = 125; // Payment Fee
$iio->price_base = (++$j==count($amts)) ? $cno->data['mc_gross_'.$c]-$i : Currency::round($pio->alloc_amt/array_sum($amts)*$cno->data['mc_gross_'.$c]);
$iio->date_start = $iio->date_stop = time();
// @todo Validate Save
$io->save();
$pio->alloc_amt = ($pio->alloc_amt+$iio->price_base > $pio->invoice->total()) ? $pio->alloc_amt : $pio->alloc_amt+$iio->price_base;
$i += $iio->price_base;
}
} elseif (is_numeric($cno->data['item_number'.$c])) {
array_push($amts,$cno->data['mc_gross_'.$c]);
$cio = ORM::factory('cart',$cno->data['item_number'.$c]);
if ($cio->loaded())
switch ($cio->motype()) {
case 'invoice':
// Validate we are all the same account
// @todo Need to handle if the cart has more than 1 account.
if (! $po->account_id AND $cio->mo()->account_id) {
$po->account_id = $cio->mo()->account_id;
} elseif ($po->account_id != $cio->mo()->account_id) {
throw new Kohana_Exception('Unable to handle payments for multiple accounts');
}
$po->add_item($cio->module_item)->alloc_amt = $cno->data['mc_gross_'.$c];
break;
default:
throw new Kohana_Exception('Dont know how to handle :item',array(':item',$cio->motype()));
}
// Dont know how to handle this item.
} else {
// @todo
}
}
// @todo Validate Save
$po->account_id = $cio->mo()->account_id;
$po->fees_amt = $cno->data['mc_fee'];
$po->total_amt = $cno->data['mc_gross'];
$po->date_payment = strtotime($cno->data['payment_date']);
$po->checkout_plugin_id = $this->co->id;
$po->notes = $cno->data['txn_id'];
$po->save();
// Clear the cart
if (! $debug_mode)
$co->delete();
} elseif (! $this->co->fee_passon AND $cno->data['mc_gross']-$cno->data['mc_fee'] == $total) {
// Ignore the fee
} else {
echo Debug::vars('IPN doesnt match cart total');
// If there is more than 1 item in the cart, we'll leave it to an admin to process.
if ($cno->data['num_cart_items'] == 1) {
echo Debug::vars('Apply to cart item');
} else {
// @todo - add the payment, with no payment items
echo Debug::vars('Leave for admin');
}
}
break;
case 'Refunded':
default:
throw new Kohana_Exception('Unable to handle payments of type :type',array(':type'=>$cno->data['payment_status']));
}
} else {
$cno->status = FALSE;
}
break;
case 'INVALID':
default:
$cno->status = FALSE;
}
$cno->processed = TRUE;
if (! $debug_mode)
$cno->save();
return _('Processed, thank you!');
}
}
?>

View File

@@ -0,0 +1,55 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides PAYPAL CART support
*
* @package Checkout
* @category Plugins
* @author Deon George
* @copyright (c) 2009-2013 Open Source Billing
* @license http://dev.osbill.net/license.html
*/
class Checkout_Plugin_Paypal_Cart extends Checkout_Plugin_Paypal {
private $test_mode = FALSE;
/**
* Set payment via Paypal
*/
public function before(Cart $co) {
$output = '';
$output .= View::factory('checkout/plugin/paypal/before')
->set('checkout',$this->co)
->set('cart',$co);
$output .= Form::open(sprintf('https://%s/cgi-bin/webscr',$this->test_mode ? $this->url_test : $this->url_prod),array('method'=>'POST'));
$output .= Form::hidden('cmd','_cart');
$output .= Form::hidden('business',$this->test_mode ? 'deon_1260578114_biz@graytech.net.au' : 'deon@graytech.net.au');
$output .= Form::hidden('bn','Graytech_BuyNow_WPS_AU');
$output .= Form::hidden('cancel_return',URL::site('checkout/cancel/'.$this->co->id,TRUE));
$output .= Form::hidden('custom',$co->id());
// @todo This should be dynamic
$output .= Form::hidden('currency_code','AUD');
$output .= Form::hidden('notify_url',URL::site('checkout/notify/'.$this->co->id,TRUE));
$output .= Form::hidden('return',URL::site('checkout/after/'.$this->co->id,TRUE));
$output .= Form::hidden('upload','1');
$c = 1;
foreach ($co->contents() as $cio) {
$output .= Form::hidden('item_number_'.$c,$cio->id);
$output .= Form::hidden('item_name_'.$c,$cio->item()->i);
$output .= Form::hidden('amount_'.$c,$cio->item()->t);
$c++;
}
$output .= Form::hidden('item_number_'.$c,'0:PAYFEE');
$output .= Form::hidden('item_name_'.$c,'Paypal Fee');
$output .= Form::hidden('amount_'.$c,$this->co->fee($co->total()));
$output .= Form::submit('submit','Pay Now');
$output .= Form::close();
return $output;
}
}
?>

View File

@@ -0,0 +1,94 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides checkout capabilities.
*
* @package Checkout
* @category Controllers
* @author Deon George
* @copyright (c) 2009-2013 Open Source Billing
* @license http://dev.osbill.net/license.html
*/
class Controller_Checkout extends Controller_TemplateDefault {
protected $auth_required = FALSE;
protected $secure_actions = array(
'before'=>TRUE,
'after'=>TRUE,
'cancel'=>TRUE,
);
public function action_index() {
HTTP::redirect('cart');
}
public function action_before() {
// If we are not here by a POST operation, we'll redirect to the cart.
if (! $cid=Request::current()->post('checkout_id'))
HTTP::redirect('cart');
$co = ORM::factory('Checkout',$cid);
Block::add(array(
'title'=>'Checkout',
'body'=>$co->plugin()->before(Cart::instance()),
));
// Suppress our right hand tab
$this->template->right = ' ';
}
public function action_after() {
$co = ORM::factory('Checkout',$this->request->param('id'));
if (! $co->loaded())
HTTP::redirect('/');
return method_exists($co->plugin(),'after') ? $co->plugin()->after(Cart::instance()) : HTTP::redirect('/');
}
public function action_cancel() {
$co = ORM::factory('Checkout',$this->request->param('id'));
if (! $co->loaded())
HTTP::redirect('cart');
return method_exists($co->plugin(),'cancel') ? $co->plugin()->cancel(Cart::instance()) : HTTP::redirect('cart');
}
public function action_notify() {
$test_id = FALSE;
$co = ORM::factory('Checkout',$this->request->param('id'));
if ((! $co->loaded() OR ! Request::current()->post()) AND ! ($test_id=Kohana::$config->load('debug')->checkout_notify))
throw HTTP_Exception::factory(404,'Payment not found!');
$this->auto_render = FALSE;
$cno = ORM::factory('Checkout_Notify');
if (! $test_id) {
$cno->checkout_id = $co->id;
$cno->status = 1;
$cno->data = Request::current()->post();
$cno->save();
} else {
$cno->where('id','=',$test_id)->find();
}
if (! $cno->loaded())
throw HTTP_Exception::factory(500,'Unable to save!');
// Process our Notify
try {
$this->response->body($cno->process());
} catch (Exception $e) {
$this->response->body('Received, thank you!');
}
$this->response->headers('Content-Type','text/plain');
$this->response->headers('Content-Length',(string)$this->response->content_length());
$this->response->headers('Last-Modified',time());
}
}
?>

View File

@@ -0,0 +1,52 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides checkout capabilities.
*
* @package Checkout
* @category Models
* @author Deon George
* @copyright (c) 2009-2013 Open Source Billing
* @license http://dev.osbill.net/license.html
*/
class Model_Checkout extends ORM_OSB {
protected $_has_many = array(
'account'=>array('through'=>'account_billing','foreign_key'=>'checkout_plugin_id'),
'payment'=>array(),
);
/**
* Calcuale the fee for this checkout method
*
* @param $amt The amount the fee will be based on
*/
public function fee($amt) {
if (! $this->fee_passon)
return 0;
$net = $amt;
if (! is_null($this->fee_fixed))
$net += $this->fee_fixed;
if (! is_null($this->fee_variable))
$net /= (1-$this->fee_variable);
return Currency::round($net-$amt);
}
/**
* Return the object of the checkout plugin
*/
public function plugin($type='') {
$c = Kohana::classname('Checkout_Plugin_'.$this->plugin);
if (! $this->plugin OR ! class_exists($c))
return NULL;
$o = new $c($this);
return $type ? $o->$type : $o;
}
}
?>

View File

@@ -0,0 +1,22 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides checkout capabilities.
*
* @package Checkout
* @category Models
* @author Deon George
* @copyright (c) 2009-2013 Open Source Billing
* @license http://dev.osbill.net/license.html
*/
class Model_Checkout_Notify extends ORM_OSB {
// Relationships
protected $_has_one = array(
'checkout'=>array('far_key'=>'checkout_id','foreign_key'=>'id'),
);
public function process() {
return $this->checkout->plugin()->notify($this);
}
}
?>