From 69645c4eeadb0944dff3994bbfda1518b1abf103 Mon Sep 17 00:00:00 2001 From: Deon George Date: Tue, 15 Jan 2013 17:07:54 +1100 Subject: [PATCH] Cart work for payments and Paypal work to test --- .htaccess | 2 +- application/bootstrap.php | 2 +- application/classes/Auth/OSB.php | 18 +- application/classes/Cache.php | 16 - application/classes/Config.php | 2 +- application/classes/Controller/Login.php | 53 +-- .../classes/Controller/TemplateDefault.php | 10 +- application/classes/HTTP/Exception/404.php | 23 +- application/classes/Kohana.php | 7 + application/classes/Model/Module.php | 9 + application/classes/ORM/OSB.php | 175 +++++----- application/classes/cache.php | 5 - application/config/cache.php | 2 +- application/config/config.php | 5 +- application/config/debug.php | 11 +- application/views/errors/404.php | 1 + modules/cart/classes/Cart.php | 88 +++-- modules/cart/classes/Cart/Item.php | 37 ++ modules/cart/classes/Cartable.php | 19 ++ modules/cart/classes/Controller/Cart.php | 88 ++--- modules/cart/classes/Model/Cart.php | 46 +++ modules/cart/media/css/cart_blocklist.css | 32 -- modules/cart/media/css/cart_contents.css | 46 --- modules/cart/media/css/checkout_cartlist.css | 34 -- modules/cart/views/cart/block_list.php | 5 - modules/cart/views/cart/checkout_list.php | 11 - modules/cart/views/cart/checkout_total.php | 25 -- modules/cart/views/cart/list_item.php | 17 - modules/cart/views/cart/list_pricebox.php | 27 -- modules/checkout/classes/Checkout/Plugin.php | 35 ++ .../classes/Checkout/Plugin/Paypal.php | 206 +++++++++++ .../classes/Checkout/Plugin/Paypal/Cart.php | 56 +++ .../checkout/classes/Controller/Checkout.php | 178 ++++------ modules/checkout/classes/Model/Checkout.php | 72 ++-- .../classes/Model/Checkout/Notify.php | 23 ++ .../views/checkout/plugin/paypal/before.php | 32 ++ .../classes/Controller/User/Invoice.php | 6 + modules/invoice/classes/Model/Invoice.php | 321 ++++++++++-------- .../invoice/classes/Model/Invoice/Item.php | 5 + .../invoice/views/invoice/user/view/pay.php | 7 + modules/payment/classes/Model/Payment.php | 3 +- .../payment/classes/Model/Payment/Item.php | 9 +- 42 files changed, 968 insertions(+), 801 deletions(-) delete mode 100644 application/classes/Cache.php delete mode 100644 application/classes/cache.php create mode 100644 application/views/errors/404.php create mode 100644 modules/cart/classes/Cart/Item.php create mode 100644 modules/cart/classes/Cartable.php delete mode 100644 modules/cart/media/css/cart_blocklist.css delete mode 100644 modules/cart/media/css/cart_contents.css delete mode 100644 modules/cart/media/css/checkout_cartlist.css delete mode 100644 modules/cart/views/cart/block_list.php delete mode 100644 modules/cart/views/cart/checkout_list.php delete mode 100644 modules/cart/views/cart/checkout_total.php delete mode 100644 modules/cart/views/cart/list_item.php delete mode 100644 modules/cart/views/cart/list_pricebox.php create mode 100644 modules/checkout/classes/Checkout/Plugin.php create mode 100644 modules/checkout/classes/Checkout/Plugin/Paypal.php create mode 100644 modules/checkout/classes/Checkout/Plugin/Paypal/Cart.php create mode 100644 modules/checkout/classes/Model/Checkout/Notify.php create mode 100644 modules/checkout/views/checkout/plugin/paypal/before.php create mode 100644 modules/invoice/views/invoice/user/view/pay.php diff --git a/.htaccess b/.htaccess index 6e25c67a..c8dada9c 100644 --- a/.htaccess +++ b/.htaccess @@ -2,7 +2,7 @@ RewriteEngine On # Installation directory -RewriteBase /osb/ +RewriteBase / # Protect hidden files from being viewed diff --git a/application/bootstrap.php b/application/bootstrap.php index e28f1f37..b479650c 100644 --- a/application/bootstrap.php +++ b/application/bootstrap.php @@ -90,7 +90,7 @@ if (isset($_SERVER['KOHANA_ENV'])) * - boolean expose set the X-Powered-By header FALSE */ Kohana::init(array( - 'base_url' => '/osb/', + 'base_url' => '/', 'caching' => TRUE, 'index_file' => '', )); diff --git a/application/classes/Auth/OSB.php b/application/classes/Auth/OSB.php index eddee3bf..62caffb4 100644 --- a/application/classes/Auth/OSB.php +++ b/application/classes/Auth/OSB.php @@ -254,21 +254,13 @@ class Auth_OSB extends Auth_ORM { $this->complete_login($user); // Do we need to update databases with our new sesion ID - // @todo figure out where this is best to go - $session_change_trigger = array('Cart'=>'session_id'); - - if (count($session_change_trigger) AND (session_id() != $oldsess)) { - foreach ($session_change_trigger as $t => $c) { - if (Config::moduleexist($c)) { - $orm = ORM::factory($t) - ->where($c,'=',$oldsess); - - foreach ($orm->find_all() as $o) + $sct = Kohana::$config->load('config')->session_change_trigger; + if (session_id() != $oldsess AND count($sct)) + foreach ($sct as $t => $c) + if (Config::moduleexist($t)) + foreach (ORM::factory(ucwords($t))->where($c,'=',$oldsess)->find_all() as $o) $o->set('session_id',session_id()) ->update(); - } - } - } return TRUE; } diff --git a/application/classes/Cache.php b/application/classes/Cache.php deleted file mode 100644 index f8ef4abe..00000000 --- a/application/classes/Cache.php +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/application/classes/Config.php b/application/classes/Config.php index cf9658d6..1238ec44 100644 --- a/application/classes/Config.php +++ b/application/classes/Config.php @@ -124,7 +124,7 @@ class Config extends Kohana_Config { } 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() { diff --git a/application/classes/Controller/Login.php b/application/classes/Controller/Login.php index a365f4a2..819ec985 100644 --- a/application/classes/Controller/Login.php +++ b/application/classes/Controller/Login.php @@ -12,6 +12,11 @@ */ class Controller_Login extends lnApp_Controller_Login { + /** + * Enable site registration + * + * @todo Needs to be written + */ public function action_register() { // If user already signed-in if (Auth::instance()->logged_in()!= 0) { @@ -19,53 +24,7 @@ class Controller_Login extends lnApp_Controller_Login { HTTP::redirect('welcome/index'); } - // Instantiate a new user - $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').'...'; + HTTP::redirect('login'); } /** diff --git a/application/classes/Controller/TemplateDefault.php b/application/classes/Controller/TemplateDefault.php index 5f6f420c..f8ee500d 100644 --- a/application/classes/Controller/TemplateDefault.php +++ b/application/classes/Controller/TemplateDefault.php @@ -39,17 +39,11 @@ class Controller_TemplateDefault extends lnApp_Controller_TemplateDefault { } protected function _right() { - if ($this->template->right) - return $this->template->right; - else - return $this->_cart(); + return ($this->template->right) ? $this->template->right : $this->_cart(); } private function _cart() { - if (! Config::moduleexist('cart') OR ! class_exists('cart') OR ! Cart::instance()->contents()->reset(FALSE)->count_all()) - return ''; - - return Cart::instance()->cart_block(); + return (! Config::moduleexist('cart') OR ! class_exists('Cart') OR ! count(Cart::instance()->contents()) OR strtolower(Request::current()->controller()) == 'cart') ? '' : Cart::instance()->cart_block(); } } ?> diff --git a/application/classes/HTTP/Exception/404.php b/application/classes/HTTP/Exception/404.php index 138a58dc..c81b1cdf 100644 --- a/application/classes/HTTP/Exception/404.php +++ b/application/classes/HTTP/Exception/404.php @@ -11,21 +11,16 @@ * @license http://dev.leenooks.net/license.html */ class HTTP_Exception_404 extends Kohana_HTTP_Exception_404 { - public function __construct($message = NULL, array $variables = NULL, Exception $previous = NULL) - { - set_exception_handler(array(get_class($this),'handler')); - parent::__construct($message, $variables, $previous); - } + public function get_response() { + $response = Response::factory(); + $response->status($this->_code); - public static function handler(Exception $e) - { - 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'); + $view = View::factory('errors/404'); + $view->message = $this->getMessage(); + + $response->body($view->render()); + + return $response; } } ?> diff --git a/application/classes/Kohana.php b/application/classes/Kohana.php index ec5c80b8..05bae198 100644 --- a/application/classes/Kohana.php +++ b/application/classes/Kohana.php @@ -46,5 +46,12 @@ abstract class Kohana extends Kohana_Core { 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)))); + } } ?> diff --git a/application/classes/Model/Module.php b/application/classes/Model/Module.php index 4fe6fa80..b999d7a9 100644 --- a/application/classes/Model/Module.php +++ b/application/classes/Model/Module.php @@ -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() { return $this->_where_active()->where('external','=',TRUE)->find_all(); } diff --git a/application/classes/ORM/OSB.php b/application/classes/ORM/OSB.php index 2f3afd4b..ce02122c 100644 --- a/application/classes/ORM/OSB.php +++ b/application/classes/ORM/OSB.php @@ -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) { if (array_key_exists($column,$this->_table_columns)) { - // If the column is a blob, we'll decode it automatically if ( $this->_table_columns[$column]['data_type'] == 'blob' @@ -155,7 +86,94 @@ abstract class ORM_OSB extends ORM { 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(); if ($blank) @@ -174,6 +192,10 @@ abstract class ORM_OSB extends ORM { 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) { // Find any fields that have changed, and process them. if ($this->_changed) @@ -194,19 +216,6 @@ abstract class ORM_OSB extends ORM { 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() { return $this->_where_active()->find_all(); } diff --git a/application/classes/cache.php b/application/classes/cache.php deleted file mode 100644 index 09c92e3e..00000000 --- a/application/classes/cache.php +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/application/config/cache.php b/application/config/cache.php index e73ad4a4..266b3f1a 100644 --- a/application/config/cache.php +++ b/application/config/cache.php @@ -19,7 +19,7 @@ return array( 'file' => array( '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, 'ignore_on_delete' => array( '.gitignore', diff --git a/application/config/config.php b/application/config/config.php index 59865936..8aca5644 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -16,7 +16,7 @@ return array( 'cache_type' => 'file', 'email_from' => array('noreply@graytech.net.au'=>'Graytech Hosting'), '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 'admin', @@ -26,6 +26,9 @@ return array( 'user', ), '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( '172.31.9.4'=>1, 'www.graytech.net.au'=>1, diff --git a/application/config/debug.php b/application/config/debug.php index db6ffbf0..4865b5c2 100644 --- a/application/config/debug.php +++ b/application/config/debug.php @@ -13,10 +13,11 @@ return array ( - 'ajax'=>FALSE, // AJAX actions can only be run by ajax calls if set to FALSE - 'etag'=>FALSE, // Force generating ETAGS - 'invoice'=>0, // Number of invoices to generate in a pass - 'show_inactive'=>FALSE, // Show Inactive Items - 'task_sim'=>FALSE, // Simulate running tasks + 'ajax'=>FALSE, // AJAX actions can only be run by ajax calls if set to FALSE + 'etag'=>FALSE, // Force generating ETAGS + 'checkout_notify'=>FALSE, // Test mode to test a particular checkout_notify item + 'invoice'=>0, // Number of invoices to generate in a pass + 'show_inactive'=>FALSE, // Show Inactive Items + 'task_sim'=>FALSE, // Simulate running tasks ); ?> diff --git a/application/views/errors/404.php b/application/views/errors/404.php new file mode 100644 index 00000000..3c0463d8 --- /dev/null +++ b/application/views/errors/404.php @@ -0,0 +1 @@ + diff --git a/modules/cart/classes/Cart.php b/modules/cart/classes/Cart.php index 37fbd0d2..816ea108 100644 --- a/modules/cart/classes/Cart.php +++ b/modules/cart/classes/Cart.php @@ -11,8 +11,14 @@ * @license http://dev.leenooks.net/license.html */ class Cart { - public static function instance() { - return new Cart; + private $id = NULL; + + 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() { 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 */ public function cart_block() { + // @todo To implement. + return ''; // If the cart is empty, we'll return here. - if (! $this->contents()->count_all()) + if (! count($this->contents())) return 'The cart is empty.'; Style::add(array( @@ -39,7 +74,7 @@ class Cart { )); $output = ''; - foreach ($this->contents()->find_all() as $item) { + foreach ($this->contents() as $item) { $ppa = $item->product->get_price_array(); $pdata = Period::details($item->recurr_schedule,$item->product->price_recurr_weekday,time(),TRUE); @@ -58,48 +93,5 @@ class Cart { 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()); - } } ?> diff --git a/modules/cart/classes/Cart/Item.php b/modules/cart/classes/Cart/Item.php new file mode 100644 index 00000000..92e6666d --- /dev/null +++ b/modules/cart/classes/Cart/Item.php @@ -0,0 +1,37 @@ +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)); + } + } +} +?> diff --git a/modules/cart/classes/Cartable.php b/modules/cart/classes/Cartable.php new file mode 100644 index 00000000..1d930154 --- /dev/null +++ b/modules/cart/classes/Cartable.php @@ -0,0 +1,19 @@ + diff --git a/modules/cart/classes/Controller/Cart.php b/modules/cart/classes/Controller/Cart.php index 4ae91047..9078b687 100644 --- a/modules/cart/classes/Controller/Cart.php +++ b/modules/cart/classes/Controller/Cart.php @@ -12,71 +12,56 @@ */ class Controller_Cart extends Controller_TemplateDefault { /** - * Default action when called + * List the cart contents */ public function action_index() { - return $this->action_list(); - } - - /** - * List items in the cart - */ - public function action_list() { - // @todo - this should be a global config item - $mediapath = Route::get('default/media'); + $output = ''; + $co = Cart::instance(); // If the cart is empty, we'll return here. - if (! Cart::instance()->contents()->count_all()) + if (! count($co->contents())) Block::add(array( 'title'=>_('Empty Cart'), 'body'=>_('The cart is empty') )); else { - Style::add(array( - 'type'=>'file', - 'data'=>'css/cart_contents.css', - )); + Block::add(array( + 'title'=>_('Cart Items'), + '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'); - 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); + $checkout = ORM::factory('Checkout')->where_active()->find_all()->as_array(); - $price_box = View::factory('cart/list_pricebox') - ->set('price_recurring',Currency::display($item->quantity*$ppa[$item->recurr_schedule]['price_base'])) - ->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); + foreach ($co->contents() as $cio) + $checkout = array_intersect($checkout,$cio->checkout()->as_array()); - $output .= View::factory('cart/list_item') - ->set('price_box',$price_box) - ->set('service_start',$pdata['date']) - ->set('service_end',$pdata['end']) - ->set('price_recurring',Currency::display($item->quantity*$ppa[$item->recurr_schedule]['price_base'])) - ->set('item',$item) - ->set('mediapath',$mediapath); + $payopt = array(); + foreach ($checkout as $cko) + $payopt[$cko->id] = $cko->name; - // If we are a plugin product, we might need more information - // @todo If an admin, show a system message if cart_info doesnt exist. - 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 .= View::factory(sprintf('%s/cart_info',strtolower($item->product->prod_plugin_file))); - - // @todo JS validation will need to verify data before submission - } - } - $output .= '
'.Form::submit('submit',_('Checkout')).'
'; + $output .= _('Total amount due for payment').' '.$co->total(TRUE); + $output .= Form::open('checkout/before'); + $output .= Form::select('checkout_id',$payopt); + $output .= Form::submit('submit',_('Checkout')); $output .= Form::close(); Block::add(array( - 'title'=>_('Your Items'), + 'title'=>_('Payment'), '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(); - if (Auth::instance()->logged_in()) - $cart->account_id = Auth::instance()->get_user()->id; - - if ($cart->values($_POST)->check()) + if ($cart->values(Request::current()->post())->check()) $cart->save(); else - echo Kohana::debug($cart->validate()->errors()); + throw new Kohana_Exception('Unable to add to cart'); if ($cart->saved()) HTTP::redirect('cart/index'); @@ -102,10 +84,8 @@ class Controller_Cart extends Controller_TemplateDefault { } public function action_empty() { - $cart = ORM::factory('Cart') - ->where('session_id','=',session_id()); - - $cart->delete_all(); + foreach (ORM::factory('Cart')->where('session_id','=',Session::instance()->id())->find_all() as $co) + $co->delete(); $this->template->content = _('Cart Emptied'); } diff --git a/modules/cart/classes/Model/Cart.php b/modules/cart/classes/Model/Cart.php index 4bfc6d3f..3bed881d 100644 --- a/modules/cart/classes/Model/Cart.php +++ b/modules/cart/classes/Model/Cart.php @@ -18,6 +18,10 @@ class Model_Cart extends ORM_OSB { // Cart doesnt use the update column protected $_updated_column = FALSE; + protected $_serialize_column = array( + 'module_data', + ); + /** * 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')), ), ); + + 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))); + } } ?> diff --git a/modules/cart/media/css/cart_blocklist.css b/modules/cart/media/css/cart_blocklist.css deleted file mode 100644 index 25b47eee..00000000 --- a/modules/cart/media/css/cart_blocklist.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/modules/cart/media/css/cart_contents.css b/modules/cart/media/css/cart_contents.css deleted file mode 100644 index fa596bba..00000000 --- a/modules/cart/media/css/cart_contents.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/modules/cart/media/css/checkout_cartlist.css b/modules/cart/media/css/checkout_cartlist.css deleted file mode 100644 index a7bd2250..00000000 --- a/modules/cart/media/css/checkout_cartlist.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/modules/cart/views/cart/block_list.php b/modules/cart/views/cart/block_list.php deleted file mode 100644 index 3c6be4e4..00000000 --- a/modules/cart/views/cart/block_list.php +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/modules/cart/views/cart/checkout_list.php b/modules/cart/views/cart/checkout_list.php deleted file mode 100644 index 5c405fa0..00000000 --- a/modules/cart/views/cart/checkout_list.php +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/modules/cart/views/cart/checkout_total.php b/modules/cart/views/cart/checkout_total.php deleted file mode 100644 index 44025f16..00000000 --- a/modules/cart/views/cart/checkout_total.php +++ /dev/null @@ -1,25 +0,0 @@ -
product->sku; ?>display('recurr_schedule');?>
product->id),$item->product->product_translate->find()->name); ?>
 Current Service Period: %s',$service_start,$service_end);?> 
- - - - - - - tax()) { ?> - tax() as $tax) { ?> - - - - - - - - - - - - - - - -
Cart Sub-Total:subtotal()); ?>
 Tax ():
 Cart Total:total()); ?>
diff --git a/modules/cart/views/cart/list_item.php b/modules/cart/views/cart/list_item.php deleted file mode 100644 index 8532e2ee..00000000 --- a/modules/cart/views/cart/list_item.php +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - -
product->id),$item->product->product_translate->find()->name); ?>uri(array('file'=>'img/edit-delete.png')),array('alt'=>_('Remove'))); ?>
 Pricing Structure:product->display('price_type'); ?>
 Invoice Frequency:display('recurr_schedule'); ?> ()
 Current Service Period: %s',$service_start,$service_end); ?>
diff --git a/modules/cart/views/cart/list_pricebox.php b/modules/cart/views/cart/list_pricebox.php deleted file mode 100644 index b1fececa..00000000 --- a/modules/cart/views/cart/list_pricebox.php +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - -
Re-Occuring Price 
 
First Invoice 
Setup 
Quantityquantity,array('size'=>2,'disabled'=>'disabled')); ?>uri(array('file'=>'img/accessories-calculator-small.png')),array('alt'=>_('Re-Calc'))); ?>
diff --git a/modules/checkout/classes/Checkout/Plugin.php b/modules/checkout/classes/Checkout/Plugin.php new file mode 100644 index 00000000..ce1e9d56 --- /dev/null +++ b/modules/checkout/classes/Checkout/Plugin.php @@ -0,0 +1,35 @@ +_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; + } +} +?> diff --git a/modules/checkout/classes/Checkout/Plugin/Paypal.php b/modules/checkout/classes/Checkout/Plugin/Paypal.php new file mode 100644 index 00000000..37c68b13 --- /dev/null +++ b/modules/checkout/classes/Checkout/Plugin/Paypal.php @@ -0,0 +1,206 @@ + 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!'); + } +} +?> diff --git a/modules/checkout/classes/Checkout/Plugin/Paypal/Cart.php b/modules/checkout/classes/Checkout/Plugin/Paypal/Cart.php new file mode 100644 index 00000000..dc7da88c --- /dev/null +++ b/modules/checkout/classes/Checkout/Plugin/Paypal/Cart.php @@ -0,0 +1,56 @@ +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; + } +} +?> diff --git a/modules/checkout/classes/Controller/Checkout.php b/modules/checkout/classes/Controller/Checkout.php index b5383b10..883a1fe8 100644 --- a/modules/checkout/classes/Controller/Checkout.php +++ b/modules/checkout/classes/Controller/Checkout.php @@ -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 = ''; - 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 .= '
'; - - 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 .= ''; - - foreach ($po as $payment) { - $output .= View::factory('checkout/payment_option') - ->set('payment',$payment); - } - - // @todo Add Javascript to stop submission if something not selected - $output .= ''; - $output .= ''; - $output .= sprintf('',Form::submit('submit',_('Submit Order'))); - $output .= ''; - - $output .= '
 
%s
'; - $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()); } } ?> diff --git a/modules/checkout/classes/Model/Checkout.php b/modules/checkout/classes/Model/Checkout.php index f493ad94..f357d241 100644 --- a/modules/checkout/classes/Model/Checkout.php +++ b/modules/checkout/classes/Model/Checkout.php @@ -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; } } ?> diff --git a/modules/checkout/classes/Model/Checkout/Notify.php b/modules/checkout/classes/Model/Checkout/Notify.php new file mode 100644 index 00000000..8b49600a --- /dev/null +++ b/modules/checkout/classes/Model/Checkout/Notify.php @@ -0,0 +1,23 @@ +array('far_key'=>'checkout_id','foreign_key'=>'id'), + ); + + public function process() { + return $this->checkout->plugin()->notify($this); + } +} +?> diff --git a/modules/checkout/views/checkout/plugin/paypal/before.php b/modules/checkout/views/checkout/plugin/paypal/before.php new file mode 100644 index 00000000..94cc7c5a --- /dev/null +++ b/modules/checkout/views/checkout/plugin/paypal/before.php @@ -0,0 +1,32 @@ +

Paypal will be used to pay for the following items:

+
+contents(), + NULL, + array( + 'item()->q'=>array('label'=>'Quantity'), + 'item()->i'=>array('label'=>'Item'), + 'item()->t'=>array('label'=>'Total','class'=>'right'), + ), + array( + 'type'=>'list', + ) + ); +?> +
+

Please Note: Paypal charges a fee to receive payments, and that fee will be added to your payment.

+ + + + + + + + + + + + + +
Cart Totaltotal(TRUE); ?>
Payment Feefee($t)); ?>
Total
diff --git a/modules/invoice/classes/Controller/User/Invoice.php b/modules/invoice/classes/Controller/User/Invoice.php index b50a2da4..f60eaa64 100644 --- a/modules/invoice/classes/Controller/User/Invoice.php +++ b/modules/invoice/classes/Controller/User/Invoice.php @@ -61,6 +61,12 @@ class Controller_User_Invoice extends Controller_TemplateDefault_User { ->set('mediapath',Route::get('default/media')) ->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) { // Add a gribber popup // @todo Make a gribber popup a class on its own. diff --git a/modules/invoice/classes/Model/Invoice.php b/modules/invoice/classes/Model/Invoice.php index 21280d39..ecd73b96 100644 --- a/modules/invoice/classes/Model/Invoice.php +++ b/modules/invoice/classes/Model/Invoice.php @@ -10,7 +10,7 @@ * @copyright (c) 2010 Open Source Billing * @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( 'account'=>array() ); @@ -47,74 +47,68 @@ class Model_Invoice extends ORM_OSB { ), ); - // Items belonging to an invoice - private $invoice_items = array(); - private $subitems_load = FALSE; - - public function __construct($id = NULL) { - // Load our model. - parent::__construct($id); - - return $this->load_sub_items(); + /** INTERFACE REQUIREMENTS **/ + public function cart_item() { + return new Cart_Item(1,sprintf('Invoice: %s',$this->refnum()),$this->due()); } - private function load_sub_items() { - // Load our sub items - if (! $this->subitems_load AND $this->loaded()) { - $this->invoice_items = $this->invoice_item->find_all()->as_array(); - $this->subitems_load = TRUE; - } + /** + * Return if this invoice is already in the cart + */ + public function cart_exists() { + 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 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() { - $this->load_sub_items(); - - return $this->invoice_items; + private function _load_sub_items() { + // Load our sub items + $this->invoice_items = $this->invoice_item->find_all()->as_array(); } /** - * Display the Invoice Number + * Add an item to an invoice */ - public function id() { - return sprintf('%06s',$this->id); + 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]; } /** - * Display the Invoice Reference Number + * Return a list of valid checkout options for this invoice */ - public function refnum() { - return sprintf('%s-%06s',$this->account->accnum(),$this->id); - } + public function checkout() { + $due = $this->due(); - /** - * 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; - - // @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); + return ORM::factory('Checkout') + ->where_active() + ->where('amount_min','<=',$due) + ->where_open() + ->and_where('amount_max','>=',$due) + ->or_where('amount_max','is',null) + ->where_close()->find_all(); } public function credits() { @@ -139,41 +133,32 @@ class Model_Invoice extends ORM_OSB { 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) - $result += $ito->tax(); + // @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 total of all items + * Display the Invoice Number */ - public function total($format=FALSE) { - $result = 0; - - 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 id() { + return sprintf('%06s',$this->id); } - public function payments() { - return $this->payment_item->find_all(); - } - - public function payments_total($format=FALSE) { - $result = 0; - - foreach ($this->payments() as $po) - $result += $po->alloc_amt; - - return $format ? Currency::display($result) : Currency::round($result); + /** + * Return a list of invoice items for this payment. + */ + public function items() { + return $this->invoice_items; } /** @@ -345,79 +330,28 @@ class Model_Invoice extends ORM_OSB { 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) { 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. - $items = $this->items(); + /** + * Display the Invoice Reference Number + */ + public function refnum() { + return sprintf('%s-%06s',$this->account->accnum(),$this->id); + } - // Save the invoice - parent::save($validation); + public function payments() { + return $this->payment_item->find_all(); + } - // Need to save the associated items and their taxes - if ($this->saved()) { - foreach ($items as $iio) { - $iio->invoice_id = $this->id; + public function payments_total($format=FALSE) { + $result = 0; - 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'=>$invoice->id)); - } + foreach ($this->payments() as $po) + $result += $po->alloc_amt; - $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'=>$invoice->id)); - } - - // @todo Need to save discount information - } - - - } else - throw new Kohana_Exception('Couldnt save invoice for some reason?'); - - return TRUE; + return $format ? Currency::display($result) : Currency::round($result); } /** @@ -441,6 +375,44 @@ class Model_Invoice extends ORM_OSB { 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) { if (! $this->loaded()) 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 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 **/ /** diff --git a/modules/invoice/classes/Model/Invoice/Item.php b/modules/invoice/classes/Model/Invoice/Item.php index 049c3870..4c6bfe82 100644 --- a/modules/invoice/classes/Model/Invoice/Item.php +++ b/modules/invoice/classes/Model/Invoice/Item.php @@ -142,6 +142,8 @@ class Model_Invoice_Item extends ORM_OSB { case 6: return _('Service Excess Fee'); + case 125: return _('Payment Fee'); + case 126: return _('Rounding'); case 127: return _('Late Payment Fee'); @@ -164,6 +166,9 @@ class Model_Invoice_Item extends ORM_OSB { } public function save(Validation $validation = NULL) { + if (! $this->changed()) + return; + // Save the invoice item parent::save($validation); diff --git a/modules/invoice/views/invoice/user/view/pay.php b/modules/invoice/views/invoice/user/view/pay.php new file mode 100644 index 00000000..6149db1f --- /dev/null +++ b/modules/invoice/views/invoice/user/view/pay.php @@ -0,0 +1,7 @@ +id); +?> +Add to cart for payment: + diff --git a/modules/payment/classes/Model/Payment.php b/modules/payment/classes/Model/Payment.php index 0c2d49ca..a509e5bb 100644 --- a/modules/payment/classes/Model/Payment.php +++ b/modules/payment/classes/Model/Payment.php @@ -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. $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; // Make sure we dont over allocate diff --git a/modules/payment/classes/Model/Payment/Item.php b/modules/payment/classes/Model/Payment/Item.php index 781786b0..31071c42 100644 --- a/modules/payment/classes/Model/Payment/Item.php +++ b/modules/payment/classes/Model/Payment/Item.php @@ -11,6 +11,13 @@ * @license http://dev.osbill.net/license.html */ 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(), + ); } ?>