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

@ -2,7 +2,7 @@
RewriteEngine On RewriteEngine On
# Installation directory # Installation directory
RewriteBase /osb/ RewriteBase /
# Protect hidden files from being viewed # Protect hidden files from being viewed
<Files .*> <Files .*>

View File

@ -90,7 +90,7 @@ if (isset($_SERVER['KOHANA_ENV']))
* - boolean expose set the X-Powered-By header FALSE * - boolean expose set the X-Powered-By header FALSE
*/ */
Kohana::init(array( Kohana::init(array(
'base_url' => '/osb/', 'base_url' => '/',
'caching' => TRUE, 'caching' => TRUE,
'index_file' => '', 'index_file' => '',
)); ));

View File

@ -254,21 +254,13 @@ class Auth_OSB extends Auth_ORM {
$this->complete_login($user); $this->complete_login($user);
// Do we need to update databases with our new sesion ID // Do we need to update databases with our new sesion ID
// @todo figure out where this is best to go $sct = Kohana::$config->load('config')->session_change_trigger;
$session_change_trigger = array('Cart'=>'session_id'); if (session_id() != $oldsess AND count($sct))
foreach ($sct as $t => $c)
if (count($session_change_trigger) AND (session_id() != $oldsess)) { if (Config::moduleexist($t))
foreach ($session_change_trigger as $t => $c) { foreach (ORM::factory(ucwords($t))->where($c,'=',$oldsess)->find_all() as $o)
if (Config::moduleexist($c)) {
$orm = ORM::factory($t)
->where($c,'=',$oldsess);
foreach ($orm->find_all() as $o)
$o->set('session_id',session_id()) $o->set('session_id',session_id())
->update(); ->update();
}
}
}
return TRUE; return TRUE;
} }

View File

@ -1,16 +0,0 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class overrides Kohana's Cache
*
* @package OSB/Modifications
* @category Classes
* @category Helpers
* @author Deon George
* @copyright (c) 2010 Deon George
* @license http://dev.leenooks.net/license.html
*/
abstract class Cache extends Kohana_Cache {
public static $default = 'apc';
}
?>

View File

@ -124,7 +124,7 @@ class Config extends Kohana_Config {
} }
public static function moduleexist($module) { public static function moduleexist($module) {
return array_key_exists($module,static::modules()) ? TRUE : FALSE; return array_key_exists(strtolower($module),static::modules()) ? TRUE : FALSE;
} }
public static function copywrite() { public static function copywrite() {

View File

@ -12,6 +12,11 @@
*/ */
class Controller_Login extends lnApp_Controller_Login { class Controller_Login extends lnApp_Controller_Login {
/**
* Enable site registration
*
* @todo Needs to be written
*/
public function action_register() { public function action_register() {
// If user already signed-in // If user already signed-in
if (Auth::instance()->logged_in()!= 0) { if (Auth::instance()->logged_in()!= 0) {
@ -19,53 +24,7 @@ class Controller_Login extends lnApp_Controller_Login {
HTTP::redirect('welcome/index'); HTTP::redirect('welcome/index');
} }
// Instantiate a new user HTTP::redirect('login');
$account = ORM::factory('Account');
// If there is a post and $_POST is not empty
if ($_POST) {
// Check Auth
$status = $account->values($_POST)->check();
if (! $status) {
foreach ($account->validation()->errors('form/register') as $f => $r) {
// $r[0] has our reason for validation failure
switch ($r[0]) {
// Generic validation reason
default:
SystemMessage::add(array(
'title'=>_('Validation failed'),
'type'=>'error',
'body'=>sprintf(_('The defaults on your submission were not valid for field %s (%s).'),$f,$r)
));
}
}
}
$ido = ORM::factory('Module')
->where('name','=','account')
->find();
$account->id = $ido->record_id->next_id($ido->id);
// Save the user details
if ($account->save()) {}
}
SystemMessage::add(array(
'title'=>_('Already have an account?'),
'type'=>'info',
'body'=>_('If you already have an account, please login..')
));
Block::add(array(
'title'=>_('Register'),
'body'=>View::factory('register')
->set('account',$account)
->set('errors',$account->validation()->errors('form/register')),
));
$this->template->left = HTML::anchor('login','Login').'...';
} }
/** /**

View File

@ -39,17 +39,11 @@ class Controller_TemplateDefault extends lnApp_Controller_TemplateDefault {
} }
protected function _right() { protected function _right() {
if ($this->template->right) return ($this->template->right) ? $this->template->right : $this->_cart();
return $this->template->right;
else
return $this->_cart();
} }
private function _cart() { private function _cart() {
if (! Config::moduleexist('cart') OR ! class_exists('cart') OR ! Cart::instance()->contents()->reset(FALSE)->count_all()) return (! Config::moduleexist('cart') OR ! class_exists('Cart') OR ! count(Cart::instance()->contents()) OR strtolower(Request::current()->controller()) == 'cart') ? '' : Cart::instance()->cart_block();
return '';
return Cart::instance()->cart_block();
} }
} }
?> ?>

View File

@ -11,21 +11,16 @@
* @license http://dev.leenooks.net/license.html * @license http://dev.leenooks.net/license.html
*/ */
class HTTP_Exception_404 extends Kohana_HTTP_Exception_404 { class HTTP_Exception_404 extends Kohana_HTTP_Exception_404 {
public function __construct($message = NULL, array $variables = NULL, Exception $previous = NULL) public function get_response() {
{ $response = Response::factory();
set_exception_handler(array(get_class($this),'handler')); $response->status($this->_code);
parent::__construct($message, $variables, $previous);
}
public static function handler(Exception $e) $view = View::factory('errors/404');
{ $view->message = $this->getMessage();
SystemMessage::add(array(
'title'=>_('Page not found'),
'type'=>'error',
'body'=>sprintf(_('The page [%s] you requested was not found?'),Request::detect_uri()),
));
HTTP::redirect('welcome'); $response->body($view->render());
return $response;
} }
} }
?> ?>

View File

@ -46,5 +46,12 @@ abstract class Kohana extends Kohana_Core {
return $result; return $result;
} }
/**
* Work out our Class Name as per Kohana's standards
*/
public static function classname($name) {
return str_replace(' ','_',ucwords(strtolower(str_replace('_',' ',$name))));
}
} }
?> ?>

View File

@ -36,6 +36,15 @@ class Model_Module extends ORM_OSB {
), ),
); );
/**
* Return an instance of this Module's Model
*
* @param $id PK of Model
*/
public function instance($id=NULL) {
return ORM::factory(ucwords($this->name),$id);
}
public function list_external() { public function list_external() {
return $this->_where_active()->where('external','=',TRUE)->find_all(); return $this->_where_active()->where('external','=',TRUE)->find_all();
} }

View File

@ -37,77 +37,8 @@ abstract class ORM_OSB extends ORM {
); );
} }
/**
* This function will enhance the [Validate::filter], since it always passes
* the value as the first argument and sometimes functions need that to not
* be the first argument.
*
* Currently this implements:
* [date()][date-ref]
*
* [date-ref]: http://www.php.net/date
*
* This function will throw an exception if called without a function
* defined.
*
* @param mixed $val Value to be processed
* @param string $func Name of function to call
* @param string $arg Other arguments for the function
* @todo This has probably changed in KH 3.1
*/
final public static function _filters($val,$func,$arg) {
switch ($func) {
case 'date':
return date($arg,$val);
default:
throw new Exception(sprintf(_('Unknown function: %s (%s,%s)'),$func,$arg,$val));
}
}
final public static function form($table,$blank=FALSE) {
return ORM::factory($table)->formselect($blank);
}
/**
* Get Next record id
*
* @param array Validate object
* @param string Primary Key
*/
public static function get_next_id($model,$field) {
if (! is_null($model->$field))
return TRUE;
$model->_changed[$field] = $field;
$ido = ORM::factory('Module')
->where('name','=',$model->_table_name)
->find();
if (! $ido->loaded())
throw new Kohana_Exception('Problem getting record_id for :table',array(':table'=>$model->_table_name));
$model->$field = $ido->record_id->next_id($ido->id);
return TRUE;
}
/**
* Set the site ID attribute for each row update
*/
public static function set_site_id($model,$field) {
if (! is_null($model->$field))
return TRUE;
$model->_changed[$field] = $field;
$model->$field = Config::siteid();
return TRUE;
}
public function __get($column) { public function __get($column) {
if (array_key_exists($column,$this->_table_columns)) { if (array_key_exists($column,$this->_table_columns)) {
// If the column is a blob, we'll decode it automatically // If the column is a blob, we'll decode it automatically
if ( if (
$this->_table_columns[$column]['data_type'] == 'blob' $this->_table_columns[$column]['data_type'] == 'blob'
@ -155,7 +86,94 @@ abstract class ORM_OSB extends ORM {
return parent::__get($column); return parent::__get($column);
} }
public function formselect($blank) { /**
* This function will enhance the [Validate::filter], since it always passes
* the value as the first argument and sometimes functions need that to not
* be the first argument.
*
* Currently this implements:
* [date()][date-ref]
*
* [date-ref]: http://www.php.net/date
*
* This function will throw an exception if called without a function
* defined.
*
* @param mixed $val Value to be processed
* @param string $func Name of function to call
* @param string $arg Other arguments for the function
* @todo This has probably changed in KH 3.1
*/
final public static function x_filters($val,$func,$arg) {
switch ($func) {
case 'date':
return date($arg,$val);
default:
throw new Exception(sprintf(_('Unknown function: %s (%s,%s)'),$func,$arg,$val));
}
}
final public static function xform($table,$blank=FALSE) {
return ORM::factory($table)->formselect($blank);
}
/**
* Retrieve and Store DB BLOB data.
*/
private function blob($data,$set=FALSE) {
try {
return $set ? gzcompress(serialize($data)) : unserialize(gzuncompress($data));
// Maybe the data isnt compressed?
} catch (Exception $e) {
return $set ? serialize($data) : unserialize($data);
}
}
public function config($key) {
$mc = Config::instance()->so->module_config($this->_object_name);
return empty($mc[$key]) ? '' : $mc[$key];
}
/**
* Get Next record id
*
* @param array Validate object
* @param string Primary Key
*/
final public static function get_next_id($model,$field) {
if (! is_null($model->$field))
return TRUE;
$model->_changed[$field] = $field;
$ido = ORM::factory('Module')
->where('name','=',$model->_table_name)
->find();
if (! $ido->loaded())
throw new Kohana_Exception('Problem getting record_id for :table',array(':table'=>$model->_table_name));
$model->$field = $ido->record_id->next_id($ido->id);
return TRUE;
}
/**
* Set the site ID attribute for each row update
*/
final public static function set_site_id($model,$field) {
if (! is_null($model->$field))
return TRUE;
$model->_changed[$field] = $field;
$model->$field = Config::siteid();
return TRUE;
}
public function xformselect($blank) {
$result = array(); $result = array();
if ($blank) if ($blank)
@ -174,6 +192,10 @@ abstract class ORM_OSB extends ORM {
return array_key_exists($key,$this->$column) ? $this->{$column}[$key] : NULL; return array_key_exists($key,$this->$column) ? $this->{$column}[$key] : NULL;
} }
final public function mid() {
return ORM::factory('Module',array('name'=>$this->_table_name));
}
public function save(Validation $validation = NULL) { public function save(Validation $validation = NULL) {
// Find any fields that have changed, and process them. // Find any fields that have changed, and process them.
if ($this->_changed) if ($this->_changed)
@ -194,19 +216,6 @@ abstract class ORM_OSB extends ORM {
return parent::save($validation); return parent::save($validation);
} }
/**
* Retrieve and Store DB BLOB data.
*/
private function blob($data,$set=FALSE) {
return $set ? gzcompress(serialize($data)) : unserialize(gzuncompress($data));
}
public function config($key) {
$mc = Config::instance()->so->module_config($this->_object_name);
return empty($mc[$key]) ? '' : $mc[$key];
}
public function list_active() { public function list_active() {
return $this->_where_active()->find_all(); return $this->_where_active()->find_all();
} }

View File

@ -1,5 +0,0 @@
<?php
class Cache extends Kohana_Cache {
public static $default = 'apc';
}
?>

View File

@ -19,7 +19,7 @@ return array(
'file' => array( 'file' => array(
'driver' => 'file', 'driver' => 'file',
'cache_dir' => Kohana::$cache_dir ? Kohana::$cache_dir : '/dev/shm/lnapp', 'cache_dir' => Kohana::$cache_dir ? Kohana::$cache_dir : APPPATH.'cache/',
'default_expire' => 3600, 'default_expire' => 3600,
'ignore_on_delete' => array( 'ignore_on_delete' => array(
'.gitignore', '.gitignore',

View File

@ -16,7 +16,7 @@ return array(
'cache_type' => 'file', 'cache_type' => 'file',
'email_from' => array('noreply@graytech.net.au'=>'Graytech Hosting'), 'email_from' => array('noreply@graytech.net.au'=>'Graytech Hosting'),
'email_admin_only'=> array( 'email_admin_only'=> array(
'adsl_traffic_notice'=>array('deon@c5t61p.leenooks.vpn'=>'Deon George'), // 'adsl_traffic_notice'=>array('deon@leenooks.vpn'=>'Deon George'),
), ),
'method_directory'=> array( // Out method paths for the different functions 'method_directory'=> array( // Out method paths for the different functions
'admin', 'admin',
@ -26,6 +26,9 @@ return array(
'user', 'user',
), ),
'method_security' => TRUE, // Enables Method Security. Setting to false means any method can be run without authentication 'method_security' => TRUE, // Enables Method Security. Setting to false means any method can be run without authentication
'session_change_trigger'=>array( // Updates to tables to make when our session ID is changed
'Cart'=>'session_id',
),
'site' => array( 'site' => array(
'172.31.9.4'=>1, '172.31.9.4'=>1,
'www.graytech.net.au'=>1, 'www.graytech.net.au'=>1,

View File

@ -13,10 +13,11 @@
return array return array
( (
'ajax'=>FALSE, // AJAX actions can only be run by ajax calls if set to FALSE 'ajax'=>FALSE, // AJAX actions can only be run by ajax calls if set to FALSE
'etag'=>FALSE, // Force generating ETAGS 'etag'=>FALSE, // Force generating ETAGS
'invoice'=>0, // Number of invoices to generate in a pass 'checkout_notify'=>FALSE, // Test mode to test a particular checkout_notify item
'show_inactive'=>FALSE, // Show Inactive Items 'invoice'=>0, // Number of invoices to generate in a pass
'task_sim'=>FALSE, // Simulate running tasks 'show_inactive'=>FALSE, // Show Inactive Items
'task_sim'=>FALSE, // Simulate running tasks
); );
?> ?>

View File

@ -0,0 +1 @@
<?php echo $message; ?>

View File

@ -11,8 +11,14 @@
* @license http://dev.leenooks.net/license.html * @license http://dev.leenooks.net/license.html
*/ */
class Cart { class Cart {
public static function instance() { private $id = NULL;
return new Cart;
public function __construct($id=NULL) {
$this->id = is_null($id) ? Session::instance()->id() : $id;
}
public static function instance($id=NULL) {
return new Cart($id);
} }
/** /**
@ -20,7 +26,34 @@ class Cart {
*/ */
public function contents() { public function contents() {
return ORM::factory('Cart') return ORM::factory('Cart')
->where('session_id','=',Session::instance()->id()); ->where('session_id','=',$this->id)
->find_all();
}
public function delete() {
foreach (ORM::factory('Cart')->where('session_id','=',$this->id)->find_all() as $co)
$co->delete();
}
public function get($mid,$item) {
return ORM::factory('Cart')
->where('session_id','=',$this->id)
->and_where('module_id','=',$mid)
->and_where('module_item','=',$item)
->find_all();
}
public function id() {
return $this->id;
}
public function total($format=FALSE) {
$total = 0;
foreach ($this->contents() as $cio)
$total += $cio->item()->t;
return $format ? Currency::display($total) : $total;
} }
/** /**
@ -29,8 +62,10 @@ class Cart {
* @param bool $detail List a detailed cart or a summary cart * @param bool $detail List a detailed cart or a summary cart
*/ */
public function cart_block() { public function cart_block() {
// @todo To implement.
return '';
// If the cart is empty, we'll return here. // If the cart is empty, we'll return here.
if (! $this->contents()->count_all()) if (! count($this->contents()))
return 'The cart is empty.'; return 'The cart is empty.';
Style::add(array( Style::add(array(
@ -39,7 +74,7 @@ class Cart {
)); ));
$output = '<table class="cart_blocklist" border="0">'; $output = '<table class="cart_blocklist" border="0">';
foreach ($this->contents()->find_all() as $item) { foreach ($this->contents() as $item) {
$ppa = $item->product->get_price_array(); $ppa = $item->product->get_price_array();
$pdata = Period::details($item->recurr_schedule,$item->product->price_recurr_weekday,time(),TRUE); $pdata = Period::details($item->recurr_schedule,$item->product->price_recurr_weekday,time(),TRUE);
@ -58,48 +93,5 @@ class Cart {
return $output; return $output;
} }
/**
* Test to see if the cart has some trial options
*
* @return boolean
*/
public function has_trial() {
foreach ($this->contents()->find_all() as $item)
if ($item->product->is_trial())
return TRUE;
return FALSE;
}
public function subtotal() {
$total = 0;
foreach ($this->contents()->find_all() as $item) {
$ppa = $item->product->get_price_array();
$period = Period::details($item->recurr_schedule,$item->product->price_recurr_weekday,time(),TRUE);
$total += $item->quantity*$ppa[$item->recurr_schedule]['price_base']*$period['prorata'];
$total += $item->quantity*$ppa[$item->recurr_schedule]['price_setup'];
}
return $total;
}
/**
* Calculate Tax for the cart items
*
* @return unknown_type
* @uses Tax
*/
public function tax() {
// @todo Tax zone should come from somewhere else
return Tax::detail(61,NULL,$this->subtotal());
}
public function total() {
// @todo Tax zone should come from somewhere else
return $this->subtotal()+Tax::total(61,NULL,$this->subtotal());
}
} }
?> ?>

View File

@ -0,0 +1,37 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides a Cart Item
*
* @package OSB
* @subpackage Cart
* @category Helpers
* @author Deon George
* @copyright (c) 2010 Deon George
* @license http://dev.leenooks.net/license.html
*/
class Cart_Item {
// Quantity
private $q = 0;
// Item
private $i = 0;
// Total
private $t = 0;
public function __construct($q,$i,$t) {
$this->q = $q;
$this->i = $i;
$this->t = $t;
}
public function __get($key) {
switch($key) {
case 'i':
case 'q': return $this->{$key};
case 't': return Currency::display($this->{$key});
default: throw new Kohana_Exception('Unknown Key :key',array(':key',$key));
}
}
}
?>

View File

@ -0,0 +1,19 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This interface ensures that all cart processing objects have the right methods
*
* @package OSB
* @subpackage Cart
* @category Helpers
* @author Deon George
* @copyright (c) 2010 Deon George
* @license http://dev.leenooks.net/license.html
*/
interface Cartable {
// Render a cart line item.
public function cart_item();
// Return if this invoice is already in the cart
public function cart_exists();
}
?>

View File

@ -12,71 +12,56 @@
*/ */
class Controller_Cart extends Controller_TemplateDefault { class Controller_Cart extends Controller_TemplateDefault {
/** /**
* Default action when called * List the cart contents
*/ */
public function action_index() { public function action_index() {
return $this->action_list(); $output = '';
} $co = Cart::instance();
/**
* List items in the cart
*/
public function action_list() {
// @todo - this should be a global config item
$mediapath = Route::get('default/media');
// If the cart is empty, we'll return here. // If the cart is empty, we'll return here.
if (! Cart::instance()->contents()->count_all()) if (! count($co->contents()))
Block::add(array( Block::add(array(
'title'=>_('Empty Cart'), 'title'=>_('Empty Cart'),
'body'=>_('The cart is empty') 'body'=>_('The cart is empty')
)); ));
else { else {
Style::add(array( Block::add(array(
'type'=>'file', 'title'=>_('Cart Items'),
'data'=>'css/cart_contents.css', 'body'=>Table::display(
)); $co->contents(),
NULL,
array(
'item()->q'=>array('label'=>'Quantity'),
'item()->i'=>array('label'=>'Item'),
'item()->t'=>array('label'=>'Total','class'=>'right'),
),
array(
'type'=>'list',
)
),
));
$output = Form::open('checkout/noready'); $checkout = ORM::factory('Checkout')->where_active()->find_all()->as_array();
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);
$price_box = View::factory('cart/list_pricebox') foreach ($co->contents() as $cio)
->set('price_recurring',Currency::display($item->quantity*$ppa[$item->recurr_schedule]['price_base'])) $checkout = array_intersect($checkout,$cio->checkout()->as_array());
->set('price_firstinvoice',Currency::display($item->quantity*$ppa[$item->recurr_schedule]['price_base']*$pdata['prorata']))
->set('price_setup',Currency::display($item->quantity*$ppa[$item->recurr_schedule]['price_setup']))
->set('item',$item)
->set('mediapath',$mediapath);
$output .= View::factory('cart/list_item') $payopt = array();
->set('price_box',$price_box) foreach ($checkout as $cko)
->set('service_start',$pdata['date']) $payopt[$cko->id] = $cko->name;
->set('service_end',$pdata['end'])
->set('price_recurring',Currency::display($item->quantity*$ppa[$item->recurr_schedule]['price_base']))
->set('item',$item)
->set('mediapath',$mediapath);
// If we are a plugin product, we might need more information $output .= _('Total amount due for payment').' '.$co->total(TRUE);
// @todo If an admin, show a system message if cart_info doesnt exist. $output .= Form::open('checkout/before');
if ($item->product->prod_plugin AND method_exists($item->product->prod_plugin_file,'product_cart') AND Kohana::find_file('views',sprintf('%s/cart_info',strtolower($item->product->prod_plugin_file)))) { $output .= Form::select('checkout_id',$payopt);
$output .= View::factory(sprintf('%s/cart_info',strtolower($item->product->prod_plugin_file))); $output .= Form::submit('submit',_('Checkout'));
// @todo JS validation will need to verify data before submission
}
}
$output .= '<div>'.Form::submit('submit',_('Checkout')).'</div>';
$output .= Form::close(); $output .= Form::close();
Block::add(array( Block::add(array(
'title'=>_('Your Items'), 'title'=>_('Payment'),
'body'=>$output, 'body'=>$output,
)); ));
} }
// Suppress our right hand tab
$this->template->right = ' ';
} }
/** /**
@ -87,13 +72,10 @@ class Controller_Cart extends Controller_TemplateDefault {
$cart->session_id = Session::instance()->id(); $cart->session_id = Session::instance()->id();
if (Auth::instance()->logged_in()) if ($cart->values(Request::current()->post())->check())
$cart->account_id = Auth::instance()->get_user()->id;
if ($cart->values($_POST)->check())
$cart->save(); $cart->save();
else else
echo Kohana::debug($cart->validate()->errors()); throw new Kohana_Exception('Unable to add to cart');
if ($cart->saved()) if ($cart->saved())
HTTP::redirect('cart/index'); HTTP::redirect('cart/index');
@ -102,10 +84,8 @@ class Controller_Cart extends Controller_TemplateDefault {
} }
public function action_empty() { public function action_empty() {
$cart = ORM::factory('Cart') foreach (ORM::factory('Cart')->where('session_id','=',Session::instance()->id())->find_all() as $co)
->where('session_id','=',session_id()); $co->delete();
$cart->delete_all();
$this->template->content = _('Cart Emptied'); $this->template->content = _('Cart Emptied');
} }

View File

@ -18,6 +18,10 @@ class Model_Cart extends ORM_OSB {
// Cart doesnt use the update column // Cart doesnt use the update column
protected $_updated_column = FALSE; protected $_updated_column = FALSE;
protected $_serialize_column = array(
'module_data',
);
/** /**
* Filters used to format the display of values into friendlier values * Filters used to format the display of values into friendlier values
*/ */
@ -26,5 +30,47 @@ class Model_Cart extends ORM_OSB {
array('StaticList_RecurSchedule::display',array(':value')), array('StaticList_RecurSchedule::display',array(':value')),
), ),
); );
private $mo;
public function __construct($id = NULL) {
// Load our Model
parent::__construct($id);
// Autoload our Sub Items
if ($this->loaded())
$this->_load_sub_items();
return $this;
}
private function _load_sub_items() {
$this->mo = ORM::factory('Module',$this->module_id)->instance($this->module_item);
if (! $this->mo->loaded())
throw new Kohana_Exception('Item :item not loaded?',array(':item'=>$this->module_item));
}
public function checkout() {
if (! method_exists($this->mo,'checkout'))
throw new Kohana_Exception('Module :module doesnt implement checkout?',array(':module'=>get_class($this->mo)));
return $this->mo->checkout();
}
public function item() {
if (! method_exists($this->mo,'cart_item'))
throw new Kohana_Exception('Module :module doesnt implement cart_item?',array(':module'=>get_class($this->mo)));
return $this->mo->cart_item();
}
public function mo() {
return $this->mo;
}
public function motype() {
return strtolower(preg_replace('/^Model_/','',get_class($this->mo)));
}
} }
?> ?>

View File

@ -1,32 +0,0 @@
/** Cart Block Contents Style Sheet **/
table.cart_blocklist {
/* margin-left: auto; */
/* margin-right: auto; */
width: 100%;
background-color: #F9F9FA;
border: 0px solid #AAAACC;
padding: 2px;
}
table.cart_blocklist tr td.sku {
color: #000000;
font-size: 75%;
}
table.cart_blocklist tr td.price {
font-weight: bold;
text-align: right;
}
table.cart_blocklist tr td.schedule {
font-size: 60%;
}
table.cart_blocklist tr.submit td {
text-align: center;
}
table.cart_blocklist tr.submit td button {
font-size: 60%;
font-weight: bold;
}

View File

@ -1,46 +0,0 @@
/** Cart Contents Style Sheet **/
table.cart_contents {
/* margin-left: auto; */
/* margin-right: auto; */
width: 100%;
background-color: #F9F9FA;
border: 0px solid #AAAACC;
padding: 2px;
}
table.cart_contents tr td.title {
color: #000000;
font-size: 125%;
}
table.cart_contents tr td.title a {
text-decoration: none;
color: #0000AA;
}
table.cart_contents tr td {
vertical-align: top;
}
table.cart_contents tr td.icon {
width: 22px;
}
table.cart_contents tr td.price_box {
width: 20%;
}
table.cart_contents tr td.price_box table.cart_detail_pricebox {
width: 100%;
background-color: #FAFAFB;
}
table.cart_contents tr td.price_box table.cart_detail_pricebox td.head {
text-align: left;
}
table.cart_contents tr td.price_box table.cart_detail_pricebox td.value {
font-weight: bold;
text-align: right;
}

View File

@ -1,34 +0,0 @@
/** Checkout Cart Style Sheet **/
table.checkout_cartlist {
/* margin-left: auto; */
/* margin-right: auto; */
width: 100%;
background-color: #F9F9FA;
border: 0px solid #AAAACC;
padding: 2px;
}
table.checkout_cartlist tr td.title {
color: #000000;
font-size: 125%;
}
table.checkout_cartlist tr td.title a {
text-decoration: none;
color: #0000AA;
}
table.checkout_cartlist tr td {
vertical-align: top;
}
table.checkout_cartlist tr td.icon {
width: 22px;
}
table.checkout_cartlist tr td.value {
font-weight: bold;
font-size: 120%;
text-align: right;
}

View File

@ -1,5 +0,0 @@
<tr>
<td class="sku"><?php echo $item->product->sku; ?></td>
<td class="schedule"><?php echo $item->display('recurr_schedule');?></td>
<td class="price"><?php echo Currency::display($price_firstinvoice+$price_setup); ?></td>
</tr>

View File

@ -1,11 +0,0 @@
<!-- @todo Translation required -->
<tr>
<td colspan="3" class="title"><b><?php echo HTML::anchor(sprintf('product/view/%s',$item->product->id),$item->product->product_translate->find()->name); ?></b></td>
<td class="value"><?php echo Currency::display($price_firstinvoice+$price_setup); ?></td>
</tr>
<tr>
<td>&nbsp;</td>
<td>Current Service Period:</td>
<td><b><?php printf('%s -> %s',$service_start,$service_end);?></b></td>
<td>&nbsp;</td>
</tr>

View File

@ -1,25 +0,0 @@
<table>
<!-- @todo This rounding should be a global configuration item -->
<tr>
<td><?php echo Country::icon($country); ?></td>
<td>Cart Sub-Total:</td>
<td><?php echo Currency::display($cart->subtotal()); ?></td>
</tr>
<?php if ($cart->tax()) { ?>
<?php foreach ($cart->tax() as $tax) { ?>
<!-- @todo This rounding should be a global configuration item -->
<!-- @todo Tax details should come from central configuration -->
<tr>
<td>&nbsp;</td>
<td>Tax (<?php echo $tax['description']; ?>):</td>
<td><?php echo Currency::display($tax['amount']); ?></td>
</tr>
<?php } ?>
<?php } ?>
<!-- @todo This rounding should be a global configuration item -->
<tr>
<td>&nbsp;</td>
<td>Cart Total:</td>
<td><?php echo Currency::display($cart->total()); ?></td>
</tr>
</table>

View File

@ -1,17 +0,0 @@
<!-- @todo Translation required -->
<table class="cart_contents" border="0">
<tr>
<td colspan="3" class="title"><b><?php echo HTML::anchor(sprintf('product/view/%s',$item->product->id),$item->product->product_translate->find()->name); ?></b></td>
<td class="icon"><?php echo HTML::image($mediapath->uri(array('file'=>'img/edit-delete.png')),array('alt'=>_('Remove'))); ?></td>
<td rowspan="4" class="price_box"><?php echo $price_box; ?></td>
</tr>
<tr>
<td>&nbsp;</td><td>Pricing Structure:</td><td colspan="2"><b><?php echo $item->product->display('price_type'); ?></b></td>
</tr>
<tr>
<td>&nbsp;</td><td>Invoice Frequency:</td><td colspan="2"><b><?php echo $item->display('recurr_schedule'); ?> (<?php echo $price_recurring; ?>)</b></td>
</tr>
<tr>
<td>&nbsp;</td><td>Current Service Period:</td><td><b><?php printf('%s -> %s',$service_start,$service_end); ?></b></td>
</tr>
</table>

View File

@ -1,27 +0,0 @@
<!-- @todo translation needed -->
<table class="cart_detail_pricebox" border="0">
<tr>
<td class="head">Re-Occuring Price</td>
<td class="value"><?php echo $price_recurring; ?></td>
<td>&nbsp;</td>
</tr>
<tr>
<td colspan="2">&nbsp;</td></tr>
<tr>
<td class="head">First Invoice</td>
<td class="value"><?php echo $price_firstinvoice; ?></td>
<td>&nbsp;</td>
</tr>
<?php if ($price_setup) { ?>
<tr>
<td class="head">Setup</td>
<td class="value"><?php echo $price_setup; ?></td>
<td>&nbsp;</td>
</tr>
<?php } ?>
<tr>
<td class="head">Quantity</td><!-- // @todo Quantity cannot be changed -->
<td class="value"><?php echo Form::input('quantity',$item->quantity,array('size'=>2,'disabled'=>'disabled')); ?></td>
<td class="icon"><?php echo HTML::image($mediapath->uri(array('file'=>'img/accessories-calculator-small.png')),array('alt'=>_('Re-Calc'))); ?></td>
</tr>
</table>

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 * @license http://dev.osbill.net/license.html
*/ */
class Controller_Checkout extends Controller_TemplateDefault { class Controller_Checkout extends Controller_TemplateDefault {
protected $auth_required = TRUE; protected $auth_required = FALSE;
protected $noauth_redirect = 'login/register'; 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() { public function action_index() {
if ($_POST) HTTP::redirect('cart');
return $this->checkout(); }
// @todo - this should be a global config item public function action_before() {
$mediapath = Route::get('default/media'); // 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. Block::add(array(
if (! Cart::instance()->contents()->count_all()) 'title'=>'Checkout',
Block::add(array( 'body'=>$co->plugin()->before(Cart::instance()),
'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,
));
}
// Suppress our right hand tab // Suppress our right hand tab
$this->template->right = ' '; $this->template->right = ' ';
} }
/** public function action_after() {
* Process checkout $co = ORM::factory('Checkout',$this->request->param('id'));
*/
private function checkout() {
$invoice = ORM::factory('Invoice');
// Add our individual items to the invoice if (! $co->loaded())
foreach (Cart::instance()->contents()->find_all() as $item) { HTTP::redirect('/');
$invoice_item = $invoice->add_item();
$invoice_item->product_id = $item->product_id; return method_exists($co->plugin(),'after') ? $co->plugin()->after(Cart::instance()) : HTTP::redirect('/');
$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;
$ppa = $item->product->get_price_array(); public function action_cancel() {
$period = Period::details($item->recurr_schedule,$item->product->price_recurr_weekday,time(),TRUE); $co = ORM::factory('Checkout',$this->request->param('id'));
// @todo rounding should be a global config
$invoice_item->price_base = round($item->quantity*$ppa[$item->recurr_schedule]['price_base']*$period['prorata'],2); if (! $co->loaded())
$invoice_item->price_setup = round($item->quantity*$ppa[$item->recurr_schedule]['price_setup'],2); 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; if (! $cno->loaded())
$invoice->type = 2; // INVOICED VIA CHECKOUT throw HTTP_Exception::factory(500,'Unable to save!');
$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 ($invoice->check()) // Process our Notify
$invoice->save(); try {
else $this->response->body($cno->process());
throw new Kohana_Exception('Problem saving invoice - Failed check()');
} 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 * @license http://dev.osbill.net/license.html
*/ */
class Model_Checkout extends ORM_OSB { class Model_Checkout extends ORM_OSB {
protected $_has_many = array( /**
'account'=>array('through'=>'account_billing','foreign_key'=>'checkout_plugin_id'), * Calcuale the fee for this checkout method
'payment'=>array(), *
); * @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 * Return the object of the checkout plugin
*
* Trial Products are NEW products
* Cart items are NEW products
* Invoice items are RE-OCCURING items (ie: carts are not re-occuring)
*
*/ */
public function payment_options_cart() { public function plugin($type='') {
$cart = Cart::instance(); $c = Kohana::classname('Checkout_Plugin_'.$this->plugin);
$available_payments = array(); if (! $this->plugin OR ! class_exists($c))
return NULL;
if ($cart->has_trial()) $o = new $c($this);
$this->and_where('allow_trial','=',TRUE);
$this->and_where('allow_new','=',TRUE); return $type ? $o->$type : $o;
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;
} }
} }
?> ?>

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);
}
}
?>

View File

@ -0,0 +1,32 @@
<p>Paypal will be used to pay for the following items:</p>
</br>
<?php
echo Table::display(
$cart->contents(),
NULL,
array(
'item()->q'=>array('label'=>'Quantity'),
'item()->i'=>array('label'=>'Item'),
'item()->t'=>array('label'=>'Total','class'=>'right'),
),
array(
'type'=>'list',
)
);
?>
</br>
<p>Please Note: Paypal charges a fee to receive payments, and that fee will be added to your payment.</p>
<table class="list-box-left">
<tr class="list-data">
<th>Cart Total</th>
<td class="right"><?php echo $t=$cart->total(TRUE); ?></td>
</tr>
<tr class="list-data">
<th>Payment Fee</th>
<td class="right"><?php echo Currency::display($f=$checkout->fee($t)); ?></td>
</tr>
<tr class="list-data">
<th>Total</th>
<td class="right"><?php echo Currency::display($t+$f); ?></td>
</tr>
</table>

View File

@ -61,6 +61,12 @@ class Controller_User_Invoice extends Controller_TemplateDefault_User {
->set('mediapath',Route::get('default/media')) ->set('mediapath',Route::get('default/media'))
->set('io',$io); ->set('io',$io);
if ($io->due() AND ! $io->cart_exists()) {
$output .= View::factory($this->viewpath().'/pay')
->set('mid',$io->mid())
->set('o',$io);
}
if (! $io->status) { if (! $io->status) {
// Add a gribber popup // Add a gribber popup
// @todo Make a gribber popup a class on its own. // @todo Make a gribber popup a class on its own.

View File

@ -10,7 +10,7 @@
* @copyright (c) 2010 Open Source Billing * @copyright (c) 2010 Open Source Billing
* @license http://dev.osbill.net/license.html * @license http://dev.osbill.net/license.html
*/ */
class Model_Invoice extends ORM_OSB { class Model_Invoice extends ORM_OSB implements Cartable {
protected $_belongs_to = array( protected $_belongs_to = array(
'account'=>array() 'account'=>array()
); );
@ -47,74 +47,68 @@ class Model_Invoice extends ORM_OSB {
), ),
); );
// Items belonging to an invoice /** INTERFACE REQUIREMENTS **/
private $invoice_items = array(); public function cart_item() {
private $subitems_load = FALSE; return new Cart_Item(1,sprintf('Invoice: %s',$this->refnum()),$this->due());
public function __construct($id = NULL) {
// Load our model.
parent::__construct($id);
return $this->load_sub_items();
} }
private function load_sub_items() { /**
// Load our sub items * Return if this invoice is already in the cart
if (! $this->subitems_load AND $this->loaded()) { */
$this->invoice_items = $this->invoice_item->find_all()->as_array(); public function cart_exists() {
$this->subitems_load = TRUE; return count(Cart::instance()->get($this->mid(),$this->id));
} }
// Items belonging to an invoice
private $invoice_items = array();
public function __construct($id = NULL) {
// Load our Model
parent::__construct($id);
// Autoload our Sub Items
if ($this->loaded())
$this->_load_sub_items();
return $this; return $this;
} }
/** /**
* Return a list of invoice items for this payment. * Load our invoice items
* We need these so that we can calculate totals, etc
*/ */
public function items() { private function _load_sub_items() {
$this->load_sub_items(); // Load our sub items
$this->invoice_items = $this->invoice_item->find_all()->as_array();
return $this->invoice_items;
} }
/** /**
* Display the Invoice Number * Add an item to an invoice
*/ */
public function id() { public function add_item() {
return sprintf('%06s',$this->id); if ($this->loaded() and ! $this->invoice_items)
throw new Kohana_Exception('Need to load invoice_items?');
$c = count($this->invoice_items);
$this->invoice_items[$c] = ORM::factory('Invoice_Item');
return $this->invoice_items[$c];
} }
/** /**
* Display the Invoice Reference Number * Return a list of valid checkout options for this invoice
*/ */
public function refnum() { public function checkout() {
return sprintf('%s-%06s',$this->account->accnum(),$this->id); $due = $this->due();
}
/** return ORM::factory('Checkout')
* Display the amount due ->where_active()
*/ ->where('amount_min','<=',$due)
public function due($format=FALSE) { ->where_open()
// If the invoice is active calculate the due amount ->and_where('amount_max','>=',$due)
$result = $this->status ? $this->total()-$this->payments_total() : 0; ->or_where('amount_max','is',null)
->where_close()->find_all();
// @todo This should not be required.
if ((Currency::round($result) == .01) or Currency::round($result) == .02)
$result = 0;
return $format ? Currency::display($result) : Currency::round($result);
}
/**
* Return the subtotal of all items
*/
public function subtotal($format=FALSE) {
$result = 0;
foreach ($this->items() as $ito)
$result += $ito->subtotal();
return $format ? Currency::display($result) : Currency::round($result);
} }
public function credits() { public function credits() {
@ -139,41 +133,32 @@ class Model_Invoice extends ORM_OSB {
return $format ? Currency::display($result) : Currency::round($result); return $format ? Currency::display($result) : Currency::round($result);
} }
public function tax($format=FALSE) { /**
$result = 0; * Display the amount due
*/
public function due($format=FALSE) {
// If the invoice is active calculate the due amount
$result = $this->status ? $this->total()-$this->payments_total() : 0;
foreach ($this->items() as $ito) // @todo This should not be required.
$result += $ito->tax(); if ((Currency::round($result) == .01) or Currency::round($result) == .02)
$result = 0;
return $format ? Currency::display($result) : Currency::round($result); return $format ? Currency::display($result) : Currency::round($result);
} }
/** /**
* Return the total of all items * Display the Invoice Number
*/ */
public function total($format=FALSE) { public function id() {
$result = 0; return sprintf('%06s',$this->id);
foreach ($this->items() as $ito)
$result += $ito->total();
// Reduce by any credits
$result -= $this->credit_amt;
return $format ? Currency::display($result) : Currency::round($result);
} }
public function payments() { /**
return $this->payment_item->find_all(); * Return a list of invoice items for this payment.
} */
public function items() {
public function payments_total($format=FALSE) { return $this->invoice_items;
$result = 0;
foreach ($this->payments() as $po)
$result += $po->alloc_amt;
return $format ? Currency::display($result) : Currency::round($result);
} }
/** /**
@ -345,79 +330,28 @@ class Model_Invoice extends ORM_OSB {
return $total; return $total;
} }
/**
* Return a list of taxes used on this invoice
* @todo Move some of this to invoice_item_tax.
*/
public function tax_summary() {
$summary = array();
foreach ($this->items() as $ito) {
foreach ($ito->invoice_item_tax->find_all() as $item_tax) {
if (! isset($summary[$item_tax->tax_id]))
$summary[$item_tax->tax_id] = $item_tax->amount;
else
$summary[$item_tax->tax_id] += $item_tax->amount;
}
}
// @todo This should be removed eventually
if (! $summary)
$summary[1] = $this->tax();
return $summary;
}
/**
* Add an item to an invoice
*/
public function add_item() {
if ($this->loaded() and ! $this->invoice_items)
throw new Kohana_Exception('Need to load invoice_items?');
$c = count($this->invoice_items);
$this->invoice_items[$c] = ORM::factory('Invoice_Item');
return $this->invoice_items[$c];
}
public function min_due($date) { public function min_due($date) {
return strtotime(date('Y-M-d',($date < time()) ? time()+ORM::factory('Invoice')->config('DUE_DAYS_MIN')*86400 : $date)); return strtotime(date('Y-M-d',($date < time()) ? time()+ORM::factory('Invoice')->config('DUE_DAYS_MIN')*86400 : $date));
} }
public function save(Validation $validation = NULL) { /**
// Our items will be clobbered once we save the object, so we need to save it here. * Display the Invoice Reference Number
$items = $this->items(); */
public function refnum() {
return sprintf('%s-%06s',$this->account->accnum(),$this->id);
}
// Save the invoice public function payments() {
parent::save($validation); return $this->payment_item->find_all();
}
// Need to save the associated items and their taxes public function payments_total($format=FALSE) {
if ($this->saved()) { $result = 0;
foreach ($items as $iio) {
$iio->invoice_id = $this->id;
if (! $iio->check()) { foreach ($this->payments() as $po)
// @todo Mark invoice as cancelled and write a memo, then... $result += $po->alloc_amt;
throw new Kohana_Exception('Problem saving invoice_item for invoice :invoice - Failed check()',array(':invoice'=>$invoice->id));
}
$iio->save(); return $format ? Currency::display($result) : Currency::round($result);
if (! $iio->saved()) {
// @todo Mark invoice as cancelled and write a memo, then...
throw new Kohana_Exception('Problem saving invoice_item for invoice :invoice - Failed save()',array(':invoice'=>$invoice->id));
}
// @todo Need to save discount information
}
} else
throw new Kohana_Exception('Couldnt save invoice for some reason?');
return TRUE;
} }
/** /**
@ -441,6 +375,44 @@ class Model_Invoice extends ORM_OSB {
return FALSE; return FALSE;
} }
public function save(Validation $validation = NULL) {
// Our items will be clobbered once we save the object, so we need to save it here.
$items = $this->items();
// Save the invoice
if ($this->changed())
parent::save($validation);
// Need to save the associated items and their taxes
if ($this->loaded()) {
foreach ($items as $iio) {
$iio->invoice_id = $this->id;
if (! $iio->changed())
continue;
if (! $iio->check()) {
// @todo Mark invoice as cancelled and write a memo, then...
throw new Kohana_Exception('Problem saving invoice_item for invoice :invoice - Failed check()',array(':invoice'=>$this->id));
}
$iio->save();
if (! $iio->saved()) {
// @todo Mark invoice as cancelled and write a memo, then...
throw new Kohana_Exception('Problem saving invoice_item for invoice :invoice - Failed save()',array(':invoice'=>$this->id));
}
// @todo Need to save discount information
}
} else
throw new Kohana_Exception('Couldnt save invoice for some reason?');
return TRUE;
}
public function set_remind($key,$value,$add=FALSE) { public function set_remind($key,$value,$add=FALSE) {
if (! $this->loaded()) if (! $this->loaded())
throw new Kohana_Exception('Cant call :method when a record not loaded.',array(':method',__METHOD__)); throw new Kohana_Exception('Cant call :method when a record not loaded.',array(':method',__METHOD__));
@ -484,6 +456,69 @@ class Model_Invoice extends ORM_OSB {
return $return; return $return;
} }
/**
* Return the subtotal of all items
*/
public function subtotal($format=FALSE) {
$result = 0;
foreach ($this->items() as $ito)
$result += $ito->subtotal();
return $format ? Currency::display($result) : Currency::round($result);
}
public function tax($format=FALSE) {
$result = 0;
foreach ($this->items() as $ito)
$result += $ito->tax();
return $format ? Currency::display($result) : Currency::round($result);
}
/**
* Return a list of taxes used on this invoice
* @todo Move some of this to invoice_item_tax.
*/
public function tax_summary() {
$summary = array();
foreach ($this->items() as $ito) {
foreach ($ito->invoice_item_tax->find_all() as $item_tax) {
if (! isset($summary[$item_tax->tax_id]))
$summary[$item_tax->tax_id] = $item_tax->amount;
else
$summary[$item_tax->tax_id] += $item_tax->amount;
}
}
// @todo This should be removed eventually
if (! $summary)
$summary[1] = $this->tax();
return $summary;
}
/**
* Return the total of all items
*/
public function total($format=FALSE) {
$result = 0;
// @todo - This should be required, but during checkout payment processing $pio->invoice->total() showed no invoice items?
if ($this->loaded() AND ! count($this->items()))
$this->_load_sub_items();
foreach ($this->items() as $ito)
$result += $ito->total();
// Reduce by any credits
$result -= $this->credit_amt;
return $format ? Currency::display($result) : Currency::round($result);
}
/** LIST FUNCTIONS **/ /** LIST FUNCTIONS **/
/** /**

View File

@ -142,6 +142,8 @@ class Model_Invoice_Item extends ORM_OSB {
case 6: return _('Service Excess Fee'); case 6: return _('Service Excess Fee');
case 125: return _('Payment Fee');
case 126: return _('Rounding'); case 126: return _('Rounding');
case 127: return _('Late Payment Fee'); case 127: return _('Late Payment Fee');
@ -164,6 +166,9 @@ class Model_Invoice_Item extends ORM_OSB {
} }
public function save(Validation $validation = NULL) { public function save(Validation $validation = NULL) {
if (! $this->changed())
return;
// Save the invoice item // Save the invoice item
parent::save($validation); parent::save($validation);

View File

@ -0,0 +1,7 @@
<?php
echo Form::open('cart/add');
echo Form::hidden('module_id',$mid);
echo Form::hidden('module_item',$o->id);
?>
Add to cart for payment: <?php echo StaticList_YesNo::form('cart_add',true); ?>
<?php echo Form::submit('submit','Add to Cart'); echo Form::close('cart/add'); ?>

View File

@ -156,7 +156,8 @@ class Model_Payment extends ORM_OSB {
// Our items will be clobbered once we save the object, so we need to save it here. // Our items will be clobbered once we save the object, so we need to save it here.
$items = $this->items(); $items = $this->items();
$this->source_id = Auth::instance()->get_user()->id; // @todo This should not be mandatory - or there should be a source for non-users (automatic postings)
$this->source_id = Auth::instance()->get_user() ? Auth::instance()->get_user()->id : 1;
$this->ip = Request::$client_ip; $this->ip = Request::$client_ip;
// Make sure we dont over allocate // Make sure we dont over allocate

View File

@ -11,6 +11,13 @@
* @license http://dev.osbill.net/license.html * @license http://dev.osbill.net/license.html
*/ */
class Model_Payment_Item extends ORM_OSB { class Model_Payment_Item extends ORM_OSB {
protected $_belongs_to = array('payment'=>array(),'invoice'=>array()); // Relationships
protected $_has_one = array(
'invoice'=>array('far_key'=>'invoice_id','foreign_key'=>'id'),
);
protected $_belongs_to = array(
'payment'=>array(),
);
} }
?> ?>