Cart work for payments and Paypal work to test

This commit is contained in:
Deon George
2013-01-15 17:07:54 +11:00
parent 133ae4d5c6
commit 69645c4eea
42 changed files with 968 additions and 801 deletions

View File

@@ -0,0 +1,35 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides CHECKOUT Plugin Support
*
* @package OSB
* @subpackage Checkout Plugins
* @category Plugins
* @author Deon George
* @copyright (c) 2010 Deon George
* @license http://dev.leenooks.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,206 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides PAYPAL support
*
* @package OSB
* @subpackage Plugins/Paypal
* @category Plugins
* @author Deon George
* @copyright (c) 2010 Deon George
* @license http://dev.leenooks.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',$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,56 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides PAYPAL CART support
*
* @package OSB
* @subpackage Plugins/Paypal
* @category Plugins
* @author Deon George
* @copyright (c) 2010 Deon George
* @license http://dev.leenooks.net/license.html
*/
class Checkout_Plugin_Paypal_Cart extends Checkout_Plugin_Paypal {
private $test_mode = TRUE;
/**
* 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

@@ -11,137 +11,85 @@
* @license http://dev.osbill.net/license.html
*/
class Controller_Checkout extends Controller_TemplateDefault {
protected $auth_required = TRUE;
protected $noauth_redirect = 'login/register';
protected $auth_required = FALSE;
protected $secure_actions = array(
'before'=>TRUE,
'after'=>TRUE,
'cancel'=>TRUE,
);
/**
* This is the main call to export, providing a list of items to export and
* setting up the page to call the export plugin when submitted.
*/
public function action_index() {
if ($_POST)
return $this->checkout();
HTTP::redirect('cart');
}
// @todo - this should be a global config item
$mediapath = Route::get('default/media');
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');
// @todo Items in the cart dont have account_id if they were put in the cart when the user was not logged in
$co = ORM::factory('Checkout',$cid);
// If the cart is empty, we'll return here.
if (! Cart::instance()->contents()->count_all())
Block::add(array(
'title'=>_('Empty Cart'),
'body'=>_('The cart is empty')
));
else {
Style::add(array(
'type'=>'file',
'data'=>'css/checkout_cartlist.css',
));
// Show a list of items in the cart
$output = '<table class="checkout_cartlist" border="0">';
foreach (Cart::instance()->contents()->find_all() as $item) {
$ppa = $item->product->get_price_array();
$pdata = Period::details($item->recurr_schedule,$item->product->price_recurr_weekday,time(),TRUE);
$output .= View::factory('cart/checkout_list')
->set('price_firstinvoice',$item->quantity*$ppa[$item->recurr_schedule]['price_base']*$pdata['prorata'])
->set('price_setup',$item->quantity*$ppa[$item->recurr_schedule]['price_setup'])
->set('service_start',$pdata['date'])
->set('service_end',$pdata['end'])
->set('price_recurring',$item->quantity*$ppa[$item->recurr_schedule]['price_base'])
->set('item',$item)
->set('mediapath',$mediapath);
}
$output .= '</table>';
Block::add(array(
'title'=>_('Your Items'),
'body'=>$output,
));
$po = ORM::factory('Checkout')
->payment_options_cart();
// @todo Country value should come from somewhere?
Block::add(array(
'title'=>_('Order Total'),
'body'=>View::factory('cart/checkout_total')
->set('cart',Cart::instance())
->set('country',61),
));
$output = Form::open();
$output .= '<table class="payment_options_box" border="0">';
foreach ($po as $payment) {
$output .= View::factory('checkout/payment_option')
->set('payment',$payment);
}
// @todo Add Javascript to stop submission if something not selected
$output .= '<tr><td>&nbsp;</td></tr>';
$output .= '<tr>';
$output .= sprintf('<td>%s</td>',Form::submit('submit',_('Submit Order')));
$output .= '</tr>';
$output .= '</table>';
$output .= Form::close();
Block::add(array(
'title'=>_('Available Payment Methods'),
'body'=>$output,
));
}
Block::add(array(
'title'=>'Checkout',
'body'=>$co->plugin()->before(Cart::instance()),
));
// Suppress our right hand tab
$this->template->right = ' ';
}
/**
* Process checkout
*/
private function checkout() {
$invoice = ORM::factory('Invoice');
public function action_after() {
$co = ORM::factory('Checkout',$this->request->param('id'));
// Add our individual items to the invoice
foreach (Cart::instance()->contents()->find_all() as $item) {
$invoice_item = $invoice->add_item();
if (! $co->loaded())
HTTP::redirect('/');
$invoice_item->product_id = $item->product_id;
$invoice_item->product_attr = $item->product_attr;
$invoice_item->product_attr_cart = $item->product_attr;
$invoice_item->quantity = $item->quantity;
$invoice_item->recurring_schedule = $item->recurr_schedule;
return method_exists($co->plugin(),'after') ? $co->plugin()->after(Cart::instance()) : HTTP::redirect('/');
}
$ppa = $item->product->get_price_array();
$period = Period::details($item->recurr_schedule,$item->product->price_recurr_weekday,time(),TRUE);
// @todo rounding should be a global config
$invoice_item->price_base = round($item->quantity*$ppa[$item->recurr_schedule]['price_base']*$period['prorata'],2);
$invoice_item->price_setup = round($item->quantity*$ppa[$item->recurr_schedule]['price_setup'],2);
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();
}
$invoice->account_id = Auth::instance()->get_user()->id;
$invoice->type = 2; // INVOICED VIA CHECKOUT
$invoice->status = 1; // INVOICE IS NOT CANCELLED
$invoice->due_date = time(); // DATE INVOICE MUST BE PAID
$invoice->billed_currency_id = 6; // @todo This should come from the site config or the currency selected
/*
$invoice->process_status = NULL; // TO BE PROCESSED
$invoice->billing_status = NULL; // UNPAID
$invoice->refund_status = NULL; // NOT REFUNDED
$invoice->print_status = NULL; // NOT YET PRINTED
$invoice->discount_amt = NULL; // @todo CALCULATE DISCOUNTS
$invoice->checkout_plugin_id = NULL; // @todo Update the selected checkout plugin
$invoice->checkout_plugin_data = NULL; // @todo Data required for the checkout plugin
*/
if (! $cno->loaded())
throw HTTP_Exception::factory(500,'Unable to save!');
if ($invoice->check())
$invoice->save();
else
throw new Kohana_Exception('Problem saving invoice - Failed check()');
// 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

@@ -11,58 +11,38 @@
* @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);
}
/**
* Give a cart, this will present the available checkout options
*
* Trial Products are NEW products
* Cart items are NEW products
* Invoice items are RE-OCCURING items (ie: carts are not re-occuring)
*
* Return the object of the checkout plugin
*/
public function payment_options_cart() {
$cart = Cart::instance();
public function plugin($type='') {
$c = Kohana::classname('Checkout_Plugin_'.$this->plugin);
$available_payments = array();
if (! $this->plugin OR ! class_exists($c))
return NULL;
if ($cart->has_trial())
$this->and_where('allow_trial','=',TRUE);
$o = new $c($this);
$this->and_where('allow_new','=',TRUE);
foreach ($this->list_active() as $item) {
// Check that the cart total meets the minimum requirement
if ($item->total_minimum AND $cart->total() < $item->total_minimum)
continue;
// Check the cart total meets the maximum requirement
if (($item->total_maximum AND $cart->total() > $item->total_maximum) OR ($item->total_maximum == '0' AND $cart->total()))
continue;
// Check that the payment option is available to this client based on groups
// @todo Enable this test
// Check that the payment option is not prohibited by an SKU item
// @todo Enable this test
// Check if this payment method is a default payment method
// @todo Enable this test
// By Amount
// By Currency
// By Group
// By Country
// This payment option is valid
array_push($available_payments,$item);
// Sort the checkout options
// @todo Is this required?
}
return $available_payments;
return $type ? $o->$type : $o;
}
}
?>

View File

@@ -0,0 +1,23 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides checkout capabilities.
*
* @package OSB
* @subpackage Checkout
* @category Models
* @author Deon George
* @copyright (c) 2010 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);
}
}
?>