0.15_05 commit
authorTony Cook <tony@develop-help.com>
Fri, 21 Jan 2005 00:01:42 +0000 (00:01 +0000)
committertony <tony@45cb6cf1-00bc-42d2-bb5a-07f51df49f94>
Fri, 21 Jan 2005 00:01:42 +0000 (00:01 +0000)
23 files changed:
MANIFEST
Makefile
schema/bse.sql
site/cgi-bin/modules/BSE/CfgInfo.pm
site/cgi-bin/modules/BSE/DB/Mysql.pm
site/cgi-bin/modules/BSE/Shop/Util.pm
site/cgi-bin/modules/BSE/TB/Order.pm
site/cgi-bin/modules/BSE/UI/Dispatch.pm
site/cgi-bin/modules/BSE/UI/Shop.pm [new file with mode: 0644]
site/cgi-bin/modules/DevHelp/Payments/Inpho.pm [new file with mode: 0644]
site/cgi-bin/modules/DevHelp/Payments/Test.pm [new file with mode: 0644]
site/cgi-bin/modules/DevHelp/Validate.pm
site/cgi-bin/modules/Util.pm
site/cgi-bin/shop.pl
site/docs/bse.pod
site/docs/config.pod
site/templates/checkout_base.tmpl
site/templates/checkoutfinal_base.tmpl
site/templates/checkoutnew_base.tmpl [new file with mode: 0644]
site/templates/checkoutpay_base.tmpl [new file with mode: 0644]
site/templates/mailconfirm.tmpl
site/templates/mailorder.tmpl
test.cfg

index de713a853ec199ce9c80f262e1778bf5218b9525..7f00c2df822f02a028c34aeb8ab22e11a341f260 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -102,6 +102,7 @@ site/cgi-bin/modules/BSE/UI/AdminDispatch.pm
 site/cgi-bin/modules/BSE/UI/Affiliate.pm
 site/cgi-bin/modules/BSE/UI/Dispatch.pm
 site/cgi-bin/modules/BSE/UI/Formmail.pm
+site/cgi-bin/modules/BSE/UI/Shop.pm
 site/cgi-bin/modules/BSE/UI/SiteuserCommon.pm
 site/cgi-bin/modules/BSE/UI/SubAdmin.pm
 site/cgi-bin/modules/BSE/UserReg.pm
@@ -118,6 +119,8 @@ site/cgi-bin/modules/DevHelp/DynSort.pm
 site/cgi-bin/modules/DevHelp/FileUpload.pm
 site/cgi-bin/modules/DevHelp/Formatter.pm
 site/cgi-bin/modules/DevHelp/HTML.pm
+site/cgi-bin/modules/DevHelp/Payments/Inpho.pm
+site/cgi-bin/modules/DevHelp/Payments/Test.pm
 site/cgi-bin/modules/DevHelp/Report.pm
 site/cgi-bin/modules/DevHelp/Tags.pm
 site/cgi-bin/modules/DevHelp/Tags/Iterate.pm
@@ -302,10 +305,11 @@ site/templates/cart_base.tmpl
 site/templates/catalog.tmpl
 site/templates/catalog/multi.tmpl
 site/templates/catalog/shop_subcat.tmpl
-site/templates/checkout_base.tmpl
 site/templates/checkoutcard_base.tmpl
 site/templates/checkoutconfirm_base.tmpl
 site/templates/checkoutfinal_base.tmpl
+site/templates/checkoutnew_base.tmpl
+site/templates/checkoutpay_base.tmpl
 site/templates/common/default.tmpl
 site/templates/common/defsteps.tmpl
 site/templates/common/downloads.tmpl
index 59f7380518a9446216efaef00437cbfd125da229..018dd28fe3880743a97f243c22ae4161768faf52 100755 (executable)
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.15_04
+VERSION=0.15_05
 DISTNAME=bse-$(VERSION)
 DISTBUILD=$(DISTNAME)
 DISTTAR=../$(DISTNAME).tar
index 65e071af1571a57acc0549968f8122bc3263f105..beafa94973a2a0f5f243857ee1f39b4c015349f4 100644 (file)
@@ -245,6 +245,22 @@ create table orders (
   delivMobile varchar(80) not null default '',
   billMobile varchar(80) not null default '',
 
+  -- information from online credit card processing
+  -- non-zero if we did online CC processing
+  ccOnline integer not null default 0,
+  -- non-zero if processing was successful
+  ccSuccess integer not null default 0,
+  -- receipt number
+  ccReceipt varchar(80) not null default '',
+  -- main status code (value depends on driver)
+  ccStatus integer not null default 0,
+  ccStatusText varchar(80) not null default '',
+  -- secondary status code (if any)
+  ccStatus2 integer not null default 0,
+  -- card processor transaction identifier
+  -- the ORDER_NUMBER for Inpho
+  ccTranId varchar(40) not null default '',
+
   primary key (id),
   index order_cchash(ccNumberHash),
   index order_userId(userId, orderDate)
index 095208691e10a631da2e3cae2a9a7ded173ea0e0..9a3dfb8f4b796c6e6be660d3fe8c60d70a133e33 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 use vars qw(@ISA @EXPORT_OK);
 require Exporter;
 @ISA = qw(Exporter);
-@EXPORT_OK = qw(custom_class admin_base_url cfg_image_dir);
+@EXPORT_OK = qw(custom_class admin_base_url cfg_image_dir credit_card_class);
 
 =head1 NAME
 
@@ -19,6 +19,9 @@ BSE::CfgInfo - functions that return information derived from configuration
   use BSE::CfgInfo 'custom_class';
   my $class = custom_class($cfg);
 
+  use BSE::CfgInfo 'credit_card_class';
+  my $class = credit_card_class($cfg);
+
 =head1 DESCRIPTION
 
 This module contains functions which examine the BSE configuration and
@@ -69,6 +72,23 @@ sub cfg_image_dir {
   $cfg->entry('paths', 'images', $Constants::IMAGEDIR);
 }
 
+sub credit_card_class {
+  my ($cfg) = @_;
+
+  local @INC = @INC;
+
+  my $class = $cfg->entry('shop', 'cardprocessor')
+    or return;
+  (my $file = $class . ".pm") =~ s!::!/!g;
+
+  my $local_inc = $cfg->entry('paths', 'libraries');
+  unshift @INC, $local_inc if $local_inc;
+
+  require $file;
+
+  return $class->new($cfg);
+}
+
 1;
 
 __END__
index fdea2adb816df117a4ec9f27a0aa04a2bfa23201..e6f1f1aeca2d29748b9e5838f5290e19be7c3fe9 100644 (file)
@@ -84,8 +84,8 @@ SQL
    Orders => 'select * from orders',
    getOrderByPkey => 'select * from orders where id = ?',
    getOrderItemByOrderId => 'select * from order_item where orderId = ?',
-   addOrder => 'insert orders values(null,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
-   replaceOrder => 'replace orders values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
+   addOrder => 'insert orders values(null,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
+   replaceOrder => 'replace orders values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
    addOrderItem => 'insert order_item values(null,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
    getOrderByUserId => 'select * from orders where userId = ?',
 
index 5b4bc8d80c800d55f5eee27e13b52a9451cd63d0..f433efab390c91d6a938cdbe04842bf4d2407e23 100644 (file)
@@ -411,8 +411,16 @@ sub payment_types {
     $types{$type}{enabled} = 1;
   }
 
-  #use Data::Dumper;
-  #print STDERR Dumper \%types;
+  # credit card payments require either encrypted emails enabled or
+  # an online CC processing module
+  if ($types{0}) {
+    my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
+    my $ccprocessor = $cfg->entry('shop', 'cardprocessor');
+
+    if ($noencrypt && !$ccprocessor) {
+      $types{0}{enabled} = 0;
+    }
+  }
 
   return values %types;
 }
index 0bdfcb8f40e3a6f9aca8b110c3eb7ec49403e828..da7915a943f22e71516c4db903165265cba643f1 100644 (file)
@@ -20,7 +20,9 @@ sub columns {
            customStr1 customStr2 customStr3 customStr4 customStr5
            instructions billTelephone billFacsimile billEmail
            siteuser_id affiliate_code shipping_cost
-           delivMobile billMobile/;
+           delivMobile billMobile
+           ccOnline ccSuccess ccReceipt ccStatus ccStatusText
+           ccStatus2 ccTranId/;
 }
 
 =item siteuser
@@ -59,4 +61,93 @@ sub products {
   Products->getSpecial(orderProducts=>$self->{id});
 }
 
+sub valid_fields {
+  my ($class, $cfg) = @_;
+
+  my %fields =
+    (
+     delivFirstName => { description=>'Delivery First Name', },
+     delivLastName => { description => 'Delivery Last Name' },
+     delivStreet => { description => 'Delivery Street' },
+     delivState => { description => 'Delivery State' },
+     delivSuburb => { description => 'Delivery Suburb' },
+     delivPostCode => { description => 'Delivery Post Code' },
+     delivCountry => { description => 'Delivery Country' },
+     billFirstName => { description => 'Billing First Name' },
+     billLastName => { description => 'Billing Last Name' },
+     billStreet => { description => 'Billing First Name' },
+     billSuburb => { description => 'Billing First Name' },
+     billState => { description => 'Billing First Name' },
+     billPostCode => { description => 'Billing First Name' },
+     billCountry => { description => 'Billing First Name' },
+     telephone => { description => 'Telephone Number',
+                   rules => "phone" },
+     facsimile => { description => 'Facsimile Number',
+                   rules => 'phone' },
+     emailAddress => { description => 'Email Address',
+                      rules=>'email;required' },
+     instructions => { description => 'Instructions' },
+     billTelephone => { description => 'Billing Telephone Number', 
+                       rules=>'phone' },
+     billFacsimile => { description => 'Billing Facsimile Number',
+                       rules=>'phone' },
+     billEmail => { description => 'Billing Email Address',
+                   rules => 'email' },
+     delivMobile => { description => 'Delivery Mobile Number',
+                     rules => 'phone' },
+     billMobile => { description => 'Billing Mobile Number',
+                    rules=>'phone' },
+     instructions => { description => 'Instructions' },
+    );
+
+  for my $field (keys %fields) {
+    my $display = $cfg->entry('shop', "display_$field");
+    $display and $fields{$field}{description} = $display;
+  }
+
+  return %fields;
+}
+
+sub valid_rules {
+  my ($class, $cfg) = @_;
+
+  return;
+}
+
+sub valid_payment_fields {
+  my ($class, $cfg) = @_;
+
+  my %fields =
+    (
+     cardNumber => 
+     { 
+      description => "Credit Card Number",
+      rules=>"creditcardnumber",
+     },
+     cardExpiry => 
+     {
+      description => "Credit Card Expiry Date",
+      rules => 'creditcardexpirysingle',
+     },
+     cardHolder => { description => "Credit Card Holder" },
+     cardType => { description => "Credit Card Type" },
+     cardVerify => 
+     { 
+      description => 'Card Verification Value',
+      rules => 'creditcardcvv',
+     },
+    );
+
+  for my $field (keys %fields) {
+    my $display = $cfg->entry('shop', "display_$field");
+    $display and $fields{$field}{description} = $display;
+  }
+
+  return %fields;
+}
+
+sub valid_payment_rules {
+  return;
+}
+
 1;
index 23339b3645dce173134a15197ad5781ba3e6892d..7b617725be6bceb5e093782abd5f7388ea25869c 100644 (file)
@@ -11,21 +11,34 @@ sub dispatch {
 
   my $actions = $class->actions;
 
+  my $prefix = $class->action_prefix;
   my $cgi = $req->cgi;
   my $action;
   for my $check (keys %$actions) {
-    if ($cgi->param("a_$check")) {
+    if ($cgi->param("$prefix$check") || $cgi->param("$prefix$check.x")) {
       $action = $check;
       last;
     }
   }
+  if (!$action && $prefix ne 'a_') {
+    for my $check (keys %$actions) {
+      if ($cgi->param("a_$check") || $cgi->param("a_$check.x")) {
+       $action = $check;
+      last;
+      }
+    }
+  }
+  my @extras;
+  unless ($action) {
+    ($action, @extras) = $class->other_action($cgi);
+  }
   $action ||= $class->default_action;
 
   $class->check_action($req, $action, \$result)
     or return $result;
 
   my $method = "req_$action";
-  $class->$method($req);
+  $class->$method($req, @extras);
 }
 
 sub check_secure {
@@ -40,4 +53,12 @@ sub check_action {
   return 1;
 }
 
+sub other_action {
+  return;
+}
+
+sub action_prefix {
+  'a_';
+}
+
 1;
diff --git a/site/cgi-bin/modules/BSE/UI/Shop.pm b/site/cgi-bin/modules/BSE/UI/Shop.pm
new file mode 100644 (file)
index 0000000..b42821b
--- /dev/null
@@ -0,0 +1,1356 @@
+package BSE::UI::Shop;
+use strict;
+use base 'BSE::UI::Dispatch';
+use DevHelp::HTML;
+use BSE::Util::SQL qw(now_sqldate now_sqldatetime);
+use BSE::Shop::Util qw(need_logon shop_cart_tags payment_types nice_options cart_item_opts basic_tags);
+use BSE::CfgInfo qw(custom_class credit_card_class);
+use BSE::TB::Orders;
+use BSE::TB::OrderItems;
+use BSE::Mail;
+use BSE::Util::Tags qw(tag_error_img);
+use Products;
+use DevHelp::Validate qw(dh_validate dh_validate_hash);
+
+use constant PAYMENT_CC => 0;
+use constant PAYMENT_CHEQUE => 1;
+use constant PAYMENT_CALLME => 2;
+
+my %actions =
+  (
+   add => 1,
+   cart => 1,
+   checkout => 1,
+   checkupdate => 1,
+   recheckout => 1,
+   confirm => 1,
+   recalc=>1,
+   recalculate => 1,
+   #purchase => 1,
+   order => 1,
+   show_payment => 1,
+   payment => 1,
+   orderdone => 1,
+  );
+
+sub actions { \%actions }
+
+sub default_action { 'cart' }
+
+sub other_action {
+  my ($class, $cgi) = @_;
+
+  for my $key ($cgi->param()) {
+    if ($key =~ /^delete_(\d+)$/) {
+      return ( remove_item => $1 );
+    }
+  }
+
+  return;
+}
+
+sub req_cart {
+  my ($class, $req, $msg) = @_;
+
+  my @cart = @{$req->session->{cart} || []};
+  my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
+  my $item_index = -1;
+  my @options;
+  my $option_index;
+  
+  $req->session->{custom} ||= {};
+  my %custom_state = %{$req->session->{custom}};
+
+  my $cust_class = custom_class($req->cfg);
+  $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $req->cfg); 
+  $msg = '' unless defined $msg;
+  $msg = escape_html($msg);
+
+  my %acts;
+  %acts =
+    (
+     $cust_class->cart_actions(\%acts, \@cart, \@cart_prods, \%custom_state, 
+                              $req->cfg),
+     shop_cart_tags(\%acts, \@cart, \@cart_prods, $req->session, $req->cgi, 
+                   $req->cfg, 'cart'),
+     basic_tags(\%acts),
+     msg => $msg,
+    );
+  $req->session->{custom} = \%custom_state;
+  $req->session->{order_info_confirmed} = 0;
+
+  return $req->response('cart', \%acts);
+}
+
+sub req_add {
+  my ($class, $req) = @_;
+
+  my $cgi = $req->cgi;
+
+  my $addid = $cgi->param('id');
+  $addid ||= '';
+  my $quantity = $cgi->param('quantity');
+  $quantity ||= 1;
+  my $product;
+  $product = Products->getByPkey($addid) if $addid;
+  $product or 
+    return $class->req_cart($req, "Cannot find product $addid"); # oops
+
+  # collect the product options
+  my @options;
+  my @opt_names = split /,/, $product->{options};
+  my @not_def;
+  for my $name (@opt_names) {
+    my $value = $cgi->param($name);
+    push @options, $value;
+    unless (defined $value) {
+      push @not_def, $name;
+    }
+  }
+  @not_def
+    and return $class->req_cart($req, "Some product options (@not_def) not supplied");
+  my $options = join(",", @options);
+  
+  # the product must be non-expired and listed
+  (my $comp_release = $product->{release}) =~ s/ .*//;
+  (my $comp_expire = $product->{expire}) =~ s/ .*//;
+  my $today = now_sqldate();
+  $comp_release le $today
+    or return $class->req_cart($req, "Product has not been released yet");
+  $today le $comp_expire
+    or return $class->req_cart($req, "Product has expired");
+  $product->{listed} or return $class->req_cart($req, "Product not available");
+  
+  # used to refresh if a logon is needed
+  my $securlbase = $req->cfg->entryVar('site', 'secureurl');
+  my $r = $securlbase . $ENV{SCRIPT_NAME} . "?add=1&id=$addid";
+  for my $opt_index (0..$#opt_names) {
+    $r .= "&$opt_names[$opt_index]=".escape_uri($options[$opt_index]);
+  }
+  
+  my $user = $req->siteuser;
+  # need to be logged on if it has any subs
+  if ($product->{subscription_id} != -1) {
+    if ($user) {
+      my $sub = $product->subscription;
+      if ($product->is_renew_sub_only) {
+       unless ($user->subscribed_to_grace($sub)) {
+         return show_cart("This product can only be used to renew your subscription to $sub->{title} and you are not subscribed nor within the renewal grace period");
+       }
+      }
+      elsif ($product->is_start_sub_only) {
+       if ($user->subscribed_to_grace($sub)) {
+         return show_cart("This product can only be used to start your subscription to $sub->{title} and you are already subscribed or within the grace period");
+       }
+      }
+    }
+    else {
+      return $class->_refresh_logon
+       ($req, "You must be logged on to add this product to your cart", 
+        'prodlogon', $r);
+    }
+  }
+  if ($product->{subscription_required} != -1) {
+    my $sub = $product->subscription_required;
+    if ($user) {
+      unless ($user->subscribed_to($sub)) {
+       return $class->req_cart($req, "You must be subscribed to $sub->{title} to purchase this product");
+       return;
+      }
+    }
+    else {
+      # we want to refresh back to adding the item to the cart if possible
+      return $class->_refresh_logon
+       ($req, "You must be logged on and subscribed to $sub->{title} to add this product to your cart",
+        'prodlogonsub', $r);
+    }
+  }
+
+  # we need a natural integer quantity
+  $quantity =~ /^\d+$/
+    or return $class->req_cart($req, "Invalid quantity");
+
+  $req->session->{cart} ||= [];
+  my @cart = @{$req->session->{cart}};
+  # if this is is already present, replace it
+  @cart = grep { $_->{productId} ne $addid || $_->{options} ne $options } 
+    @cart;
+  push @cart, 
+    { 
+     productId => $addid, 
+     units => $quantity, 
+     price=>$product->{retailPrice},
+     options=>$options 
+    };
+
+  $req->session->{cart} = \@cart;
+  $req->session->{order_info_confirmed} = 0;
+  
+  return $class->req_cart($req);
+}
+
+sub req_checkout {
+  my ($class, $req, $message, $olddata) = @_;
+
+  my $errors = {};
+  if (defined $message) {
+    if (ref $message) {
+      $errors = $message;
+      $message = $req->message($errors);
+    }
+  }
+  else {
+    $message = '';
+  }
+  my $cfg = $req->cfg;
+  my $cgi = $req->cgi;
+
+  $class->update_quantities($req);
+  my @cart = @{$req->session->{cart}};
+
+  @cart or return $class->req_cart($req);
+
+  my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
+
+  if (my ($msg, $id) = $class->_need_logon($req, \@cart, \@cart_prods)) {
+    return $class->_refresh_logon($req, $msg, $id);
+    return;
+  }
+
+  my $user = $req->siteuser;
+
+  $req->session->{custom} ||= {};
+  my %custom_state = %{$req->session->{custom}};
+
+  my $cust_class = custom_class($cfg);
+  $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $cfg);
+
+  my $affiliate_code = $req->session->{affiliate_code};
+  defined $affiliate_code or $affiliate_code = '';
+
+  my $item_index = -1;
+  my @options;
+  my $option_index;
+  my %acts;
+  %acts =
+    (
+     shop_cart_tags(\%acts, \@cart, \@cart_prods, $req->session, $req->cgi, 
+                   $cfg, 'checkout'),
+     basic_tags(\%acts),
+     message => $message,
+     msg => $message,
+     old => 
+     sub { 
+       my $value;
+
+       if ($olddata) {
+        $value = $cgi->param($_[0]);
+        unless (defined $value) {
+          $value = $user->{$_[0]}
+            if $user;
+        }
+       }
+       else {
+        $value = $user && defined $user->{$_[0]} ? $user->{$_[0]} : '';
+       }
+       
+       defined $value or $value = '';
+       escape_html($value);
+     },
+     $cust_class->checkout_actions(\%acts, \@cart, \@cart_prods, 
+                                  \%custom_state, $req->cgi, $cfg),
+     ifUser => defined $user,
+     user => $user ? [ \&tag_hash, $user ] : '',
+     affiliate_code => escape_html($affiliate_code),
+     error_img => [ \&tag_error_img, $cfg, $errors ],
+    );
+  $req->session->{custom} = \%custom_state;
+
+  return $req->response('checkoutnew', \%acts);
+}
+
+sub req_checkupdate {
+  my ($class, $req) = @_;
+
+  my @cart = @{$req->session->{cart}};
+  my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
+  $req->session->{custom} ||= {};
+  my %custom_state = %{$req->session->{custom}};
+  custom_class($req->cfg)
+      ->checkout_update($req->cgi, \@cart, \@cart_prods, \%custom_state, $req->cfg);
+  $req->session->{custom} = \%custom_state;
+  $req->session->{order_info_confirmed} = 0;
+  
+  return $class->req_checkout($req, "", 1);
+}
+
+sub req_remove_item {
+  my ($class, $req, $index) = @_;
+  my @cart = @{$req->session->{cart}};
+  if ($index >= 0 && $index < @cart) {
+    splice(@cart, $index, 1);
+  }
+  $req->session->{cart} = \@cart;
+  $req->session->{order_info_confirmed} = 0;
+
+  return BSE::Template->get_refresh($ENV{SCRIPT_NAME}, $req->cfg);
+}
+
+my %field_map = 
+  (
+   name1 => 'delivFirstName',
+   name2 => 'delivLastName',
+   address => 'delivStreet',
+   city => 'delivSuburb',
+   postcode => 'delivPostCode',
+   state => 'delivState',
+   country => 'delivCountry',
+   email => 'emailAddress',
+   cardHolder => 'ccName',
+   cardType => 'ccType',
+  );
+
+
+# saves order and refresh to payment page
+sub req_order {
+  my ($class, $req) = @_;
+
+  my $cfg = $req->cfg;
+  my $cgi = $req->cgi;
+
+  $req->session->{cart} && @{$req->session->{cart}}
+    or return $class->req_cart($req, "Your cart is empty");
+
+  my $msg;
+  $class->_validate_cfg($req, \$msg)
+    or return $class->req_cart($req, $msg);
+
+  my @products;
+  my @items = $class->_build_items($req, \@products);
+
+  my $id;
+  if (($msg, $id) = $class->_need_logon($req, \@items, \@products)) {
+    return $class->_refresh_logon($req, $msg, $id);
+  }
+
+  # some basic validation, in case the user switched off javascript
+  my $cust_class = custom_class($cfg);
+
+  my %fields = BSE::TB::Order->valid_fields($cfg);
+  my %rules = BSE::TB::Order->valid_rules($cfg);
+  
+  my %errors;
+  my %values;
+  for my $name (keys %fields) {
+    ($values{$name}) = $cgi->param($name);
+  }
+
+  my @required = 
+    $cust_class->required_fields($cgi, $req->session->{custom}, $cfg);
+
+  for my $name (@required) {
+    $field_map{$name} and $name = $field_map{$name};
+
+    $fields{$name}{required} = 1;
+  }
+
+  dh_validate_hash(\%values, \%errors, { rules=>\%rules, fields=>\%fields },
+                  $cfg, 'Shop Order Validation');
+  keys %errors
+    and return $class->req_checkout($req, \%errors, 1);
+
+  $class->_fillout_order($req, \%values, \@items, \$msg, 'payment')
+    or return $class->req_checkout($req, $msg, 1);
+
+  $req->session->{order_info} = \%values;
+  $req->session->{order_info_confirmed} = 1;
+
+  return BSE::Template->get_refresh("$ENV{SCRIPT_NAME}?a_show_payment=1", $req->cfg);
+}
+
+sub req_show_payment {
+  my ($class, $req, $errors) = @_;
+
+  $req->session->{order_info_confirmed}
+    or return $class->req_checkout($req, 'Please proceed via the checkout page');
+
+  my $cfg = $req->cfg;
+  my $cgi = $req->cgi;
+
+  $errors ||= {};
+  my $msg = $req->message($errors);
+
+  my $order_values = $req->session->{order_info}
+    or return $class->req_checkout($req, "You need to enter order information first");
+
+  my @pay_types = payment_types($cfg);
+  my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
+  my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
+  @payment_types or @payment_types = ( PAYMENT_CALLME );
+  @payment_types = sort { $a <=> $b } @payment_types;
+  my %payment_types = map { $_=> 1 } @payment_types;
+  my $payment;
+  $errors and $payment = $cgi->param('paymentType');
+  defined $payment or $payment = $payment_types[0];
+
+  my @products;
+  my @items = $class->_build_items($req, \@products);
+
+  my %acts;
+  %acts =
+    (
+     basic_tags(\%acts),
+     message => $msg,
+     msg => $msg,
+     order => [ \&tag_hash, $order_values ],
+     shop_cart_tags(\%acts, \@items, \@products, $req->session, $req->cgi,
+                   $req->cfg, 'payment'),
+     ifMultPaymentTypes => @payment_types > 1,
+     checkedPayment => [ \&tag_checkedPayment, $payment, \%types_by_name ],
+     ifPayments => [ \&tag_ifPayments, \@payment_types, \%types_by_name ],
+     error_img => [ \&tag_error_img, $cfg, $errors ],
+    );
+  for my $type (@pay_types) {
+    my $id = $type->{id};
+    my $name = $type->{name};
+    $acts{"if${name}Payments"} = exists $payment_types{$id};
+    $acts{"if${name}FirstPayment"} = $payment_types[0] == $id;
+    $acts{"checkedIfFirst$name"} = $payment_types[0] == $id ? "checked " : "";
+    $acts{"checkedPayment$name"} = $payment == $id ? 'checked="checked" ' : "";
+  }
+
+  return $req->response('checkoutpay', \%acts);
+}
+
+my %nostore =
+  (
+   cardNumber => 1,
+   cardExpiry => 1,
+  );
+
+sub req_payment {
+  my ($class, $req, $errors) = @_;
+
+  $req->session->{order_info_confirmed}
+    or return $class->req_checkout($req, 'Please proceed via the checkout page');
+
+  my $order_values = $req->session->{order_info}
+    or return $class->req_checkout($req, "You need to enter order information first");
+
+
+  my $cgi = $req->cgi;
+  my $cfg = $req->cfg;
+  my $session = $req->session;
+
+  my @pay_types = payment_types($cfg);
+  my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
+  my %pay_types = map { $_->{id} => $_ } @pay_types;
+  my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
+  @payment_types or @payment_types = ( PAYMENT_CALLME );
+  @payment_types = sort { $a <=> $b } @payment_types;
+  my %payment_types = map { $_=> 1 } @payment_types;
+
+  my $paymentType = $cgi->param('paymentType');
+  defined $paymentType or $paymentType = $payment_types[0];
+  $payment_types{$paymentType}
+    or return $class->req_show_payment($req, { paymentType => "Invalid payment type" } , 1);
+
+  my @required;
+  push @required, @{$pay_types{$paymentType}{require}};
+
+  my %fields = BSE::TB::Order->valid_payment_fields($cfg);
+  my %rules = BSE::TB::Order->valid_payment_rules($cfg);
+  for my $field (@required) {
+    if (exists $fields{$field}) {
+      $fields{$field}{required} = 1;
+    }
+    else {
+      $fields{$field} = { description => $field, required=> 1 };
+    }
+  }
+
+  my %errors;
+  dh_validate($cgi, \%errors, { rules => \%rules, fields=>\%fields },
+             $cfg, 'Shop Order Validation');
+  keys %errors
+    and return $class->req_show_payment($req, \%errors);
+
+  my @products;
+  my @items = $class->_build_items($req, \@products);
+  
+  for my $field (@required) {
+    unless ($nostore{$field}) {
+      ($order_values->{$field}) = $cgi->param($field);
+    }
+  }
+
+  $order_values->{filled} = 0;
+  $order_values->{paidFor} = 0;
+
+  my $cust_class = custom_class($req->cfg);
+  eval {
+    my %custom = %{$session->{custom}};
+    $cust_class->order_save($cgi, $order_values, \@items, \@products, 
+                           \%custom, $cfg);
+    $session->{custom} = \%custom;
+  };
+  if ($@) {
+    return $class->req_checkout($req, $@, 1);
+  }
+
+  my @columns = BSE::TB::Order->columns;
+  my %columns; 
+  @columns{@columns} = @columns;
+
+  for my $col (@columns) {
+    defined $order_values->{$col} or $order_values->{$col} = '';
+  }
+
+  $order_values->{paymentType} = $paymentType;
+
+  my @data = @{$order_values}{@columns};
+  shift @data;
+  my $order = BSE::TB::Orders->add(@data)
+    or die "Cannot add order";
+
+  my @dbitems;
+  my %subscribing_to;
+  my @item_cols = BSE::TB::OrderItem->columns;
+  for my $row_num (0..$#items) {
+    my $item = $items[$row_num];
+    my $product = $products[$row_num];
+    $item->{orderId} = $order->{id};
+    $item->{max_lapsed} = 0;
+    if ($product->{subscription_id} != -1) {
+      my $sub = $product->subscription;
+      $item->{max_lapsed} = $sub->{max_lapsed} if $sub;
+    }
+    my @data = @{$item}{@item_cols};
+    
+    shift @data;
+    push(@dbitems, BSE::TB::OrderItems->add(@data));
+
+    my $sub = $product->subscription;
+    if ($sub) {
+      $subscribing_to{$sub->{text_id}} = $sub;
+    }
+  }
+  
+  my $ccprocessor = $cfg->entry('shop', 'cardprocessor');
+  if ($paymentType == PAYMENT_CC && $ccprocessor) {
+    my $cc_class = credit_card_class($cfg);
+
+    $order->{ccOnline} = 1;
+
+    my $ccNumber = $cgi->param('cardNumber');
+    my $ccExpiry = $cgi->param('cardExpiry');
+    $ccExpiry =~ m!^(\d+)\D(\d+)$! or die;
+    my ($month, $year) = ($1, $2);
+    $year > 2000 or $year += 2000;
+    my $expiry = sprintf("%04d%02d", $year, $month);
+    my $verify = $cgi->param('cardVerify');
+    defined $verify or $verify = '';
+    my $result = $cc_class->payment(orderno=>$order->{id},
+                                   amount => $order->{total},
+                                   cardnumber => $ccNumber,
+                                   expirydate => $expiry,
+                                   cvv => $verify,
+                                   ipaddress => $ENV{REMOTE_ADDR});
+    unless ($result->{success}) {
+      use Data::Dumper;
+      print STDERR Dumper($result);
+      # failed, back to payments
+      $order->{ccSuccess}     = 0;
+      $order->{ccStatus}      = $result->{statuscode};
+      $order->{ccStatus2}     = 0;
+      $order->{ccStatusText}  = $result->{error};
+      $order->{ccTranId}      = '';
+      $order->save;
+      $errors{cardNumber} = $result->{error};
+      $session->{order_work} = $order->{id};
+      return $class->req_show_payment($req, \%errors);
+    }
+
+    $order->{ccSuccess}            = 1;
+    $order->{ccReceipt}            = $result->{receipt};
+    $order->{ccStatus}     = 0;
+    $order->{ccStatus2}            = 0;
+    $order->{ccStatusText}  = '';
+    $order->{ccTranId}     = $result->{transactionid};
+    $order->{paidFor}      = 1;
+    $order->save;
+  }
+
+  # set the order displayed by orderdone
+  $session->{order_completed} = $order->{id};
+  $session->{order_completed_at} = time;
+
+  my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
+  $class->_send_order($req, $order, \@dbitems, \@products, $noencrypt,
+                     \%subscribing_to);
+
+  # empty the cart ready for the next order
+  delete @{$session}{qw/order_info order_info_confirmed cart order_work/};
+
+  return BSE::Template->get_refresh("$ENV{SCRIPT_NAME}?a_orderdone=1", $req->cfg);
+}
+
+sub req_orderdone {
+  my ($class, $req) = @_;
+
+  my $session = $req->session;
+  my $cfg = $req->cfg;
+
+  my $id = $session->{order_completed};
+  my $when = $session->{order_completed_at};
+  $id && defined $when && time < $when + 500
+    or return $class->req_cart($req);
+    
+  my $order = BSE::TB::Orders->getByPkey($id)
+    or return $class->req_cart($req);
+  my @items = $order->items;
+
+  my @products = map { Products->getByPkey($_->{productId}) } @items;
+
+  my $cust_class = custom_class($req->cfg);
+
+  my @pay_types = payment_types($cfg);
+  my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
+  my %pay_types = map { $_->{id} => $_ } @pay_types;
+  my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
+
+  my $item_index = -1;
+  my @options;
+  my $option_index;
+  my %acts;
+  %acts =
+    (
+     $cust_class->purchase_actions(\%acts, \@items, \@products, 
+                                  $session->{custom}, $cfg),
+     BSE::Util::Tags->static(\%acts, $cfg),
+     iterate_items_reset => sub { $item_index = -1; },
+     iterate_items => 
+     sub { 
+       if (++$item_index < @items) {
+        $option_index = -1;
+        @options = cart_item_opts($items[$item_index], 
+                                  $products[$item_index]);
+        return 1;
+       }
+       return 0;
+     },
+     item=> sub { escape_html($items[$item_index]{$_[0]}); },
+     product => 
+     sub { 
+       my $value = $products[$item_index]{$_[0]};
+       defined $value or $value = '';
+
+       escape_html($value);
+     },
+     extended =>
+     sub { 
+       my $what = $_[0] || 'retailPrice';
+       $items[$item_index]{units} * $items[$item_index]{$what};
+     },
+     order => sub { escape_html($order->{$_[0]}) },
+     money =>
+     sub {
+       my ($func, $args) = split ' ', $_[0], 2;
+       $acts{$func} || return "<: money $_[0] :>";
+       return sprintf("%.02f", $acts{$func}->($args)/100);
+     },
+     _format =>
+     sub {
+       my ($value, $fmt) = @_;
+       if ($fmt =~ /^m(\d+)/) {
+        return sprintf("%$1s", sprintf("%.2f", $value/100));
+       }
+       elsif ($fmt =~ /%/) {
+        return sprintf($fmt, $value);
+       }
+     },
+     iterate_options_reset => sub { $option_index = -1 },
+     iterate_options => sub { ++$option_index < @options },
+     option => sub { escape_html($options[$option_index]{$_[0]}) },
+     ifOptions => sub { @options },
+     options => sub { nice_options(@options) },
+     ifPayment => [ \&tag_ifPayment, $order->{paymentType}, \%types_by_name ],
+     #ifSubscribingTo => [ \&tag_ifSubscribingTo, \%subscribing_to ],
+    );
+  for my $type (@pay_types) {
+    my $id = $type->{id};
+    my $name = $type->{name};
+    $acts{"if${name}Payment"} = $order->{paymentType} == $id;
+  }
+
+  return $req->response('checkoutfinal', \%acts);
+}
+
+sub tag_ifPayment {
+  my ($payment, $types_by_name, $args) = @_;
+
+  my $type = $args;
+  if ($type !~ /^\d+$/) {
+    return '' unless exists $types_by_name->{$type};
+    $type = $types_by_name->{$type};
+  }
+
+  return $payment == $type;
+}
+
+
+sub _validate_cfg {
+  my ($class, $req, $rmsg) = @_;
+
+  my $cfg = $req->cfg;
+  my $from = $cfg->entry('shop', 'from', $Constants::SHOP_FROM);
+  unless ($from && $from =~ /.\@./) {
+    $$rmsg = "Configuration error: shop from address not set";
+    return;
+  }
+  my $toEmail = $cfg->entry('shop', 'to_email', $Constants::SHOP_TO_EMAIL);
+  unless ($toEmail && $toEmail =~ /.\@./) {
+    $$rmsg = "Configuration error: shop to_email address not set";
+    return;
+  }
+
+  return 1;
+}
+
+sub req_purchase {
+  my ($class, $req) = @_;
+
+  my $cfg = $req->cfg;
+  my $cgi = $req->cgi;
+  my $session = $req->session;
+
+  my $msg;
+  $class->_validate_cfg($req, \$msg)
+    or return $class->req_cart($req, $msg);
+    
+  # some basic validation, in case the user switched off javascript
+  my $cust_class = custom_class($cfg);
+  my @required = 
+    $cust_class->required_fields($cgi, $session->{custom}, $cfg);
+
+  my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
+
+  my @pay_types = payment_types($cfg);
+  my %pay_types = map { $_->{id} => $_ } @pay_types;
+  my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
+  #use Data::Dumper;
+  #print STDERR Dumper \%pay_types;
+  my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
+  if ($noencrypt) {
+    @payment_types = grep $_ ne PAYMENT_CC, @payment_types;
+    @payment_types or @payment_types = ( PAYMENT_CALLME );
+  }
+  else {
+    @payment_types or @payment_types = ( PAYMENT_CC );
+  }
+  @payment_types = sort { $a <=> $b } @payment_types;
+  my %payment_types = map { $_=> 1 } @payment_types;
+
+  my $paymentType = $cgi->param('paymentType');
+  defined $paymentType or $paymentType = $payment_types[0];
+  $payment_types{$paymentType}
+    or return $class->req_checkout($req, "Invalid payment type", 1);
+
+  push @required, @{$pay_types{$paymentType}{require}};
+
+  for my $field (@required) {
+    my $display = $cfg->entry('shop', "display_$field", $field);
+    defined($cgi->param($field)) && length($cgi->param($field))
+      or return $class->req_checkout($req, "Field $display is required", 1);
+  }
+  defined($cgi->param('email')) && $cgi->param('email') =~ /.\@./
+    or return $class->req_checkout($req, "Please enter a valid email address", 1);
+  if ($paymentType == PAYMENT_CC) {
+    defined($cgi->param('cardNumber')) && $cgi->param('cardNumber') =~ /^\d+$/
+      or return $class->req_checkout($req, "Please enter a credit card number", 1);
+  }
+
+  # map some form fields to order field names
+  my %field_map = 
+    (
+     name1 => 'delivFirstName',
+     name2 => 'delivLastName',
+     address => 'delivStreet',
+     city => 'delivSuburb',
+     postcode => 'delivPostCode',
+     state => 'delivState',
+     country => 'delivCountry',
+     email => 'emailAddress',
+     cardHolder => 'ccName',
+     cardType => 'ccType',
+    );
+  # paranoia, don't store these
+  my %nostore =
+    (
+     cardNumber => 1,
+     cardExpiry => 1,
+    );
+  my %order;
+  my @cart = @{$session->{cart}};
+  @cart or return $class->req_cart($req, 'You have no items in your shopping cart');
+
+  # so we can quickly check for columns
+  my @columns = BSE::TB::Order->columns;
+  my %columns; 
+  @columns{@columns} = @columns;
+
+  for my $field ($req->param()) {
+    $order{$field_map{$field} || $field} = $req->param($field)
+      unless $nostore{$field};
+  }
+
+  my $ccNumber = $req->param('cardNumber');
+  defined $ccNumber or $ccNumber = '';
+  my $ccExpiry = $req->param('cardExpiry');
+  defined $ccExpiry or $ccExpiry = '';
+  my $affiliate_code = $session->{affiliate_code};
+  defined $affiliate_code && length $affiliate_code
+    or $affiliate_code = $cgi->param('affiliate_code');
+  defined $affiliate_code or $affiliate_code = '';
+  $order{affiliate_code} = $affiliate_code;
+
+  use Digest::MD5 'md5_hex';
+  $ccNumber =~ tr/0-9//cd;
+  $order{ccNumberHash} = md5_hex($ccNumber);
+  $order{ccExpiryHash} = md5_hex($ccExpiry);
+
+  # work out totals
+  $order{total} = 0;
+  $order{gst} = 0;
+  $order{wholesale} = 0;
+  $order{shipping_cost} = 0;
+  my @products;
+  my $today = now_sqldate();
+  for my $item (@cart) {
+    my $product = Products->getByPkey($item->{productId});
+    # double check that it's still a valid product
+    if (!$product) {
+      return $class->req_cart($req, "Product $item->{productId} not found");
+    }
+    else {
+      (my $comp_release = $product->{release}) =~ s/ .*//;
+      (my $comp_expire = $product->{expire}) =~ s/ .*//;
+      $comp_release le $today
+       or return $class->req_cart($req, "'$product->{title}' has not been released yet");
+      $today le $comp_expire
+       or return $class->req_cart("'$product->{title}' has expired");
+      $product->{listed} 
+       or return $class->req_cart("'$product->{title}' not available");
+    }
+    push(@products, $product); # used in page rendering
+    @$item{qw/price wholesalePrice gst/} = 
+      @$product{qw/retailPrice wholesalePrice gst/};
+    $order{total} += $item->{price} * $item->{units};
+    $order{wholesale} += $item->{wholesalePrice} * $item->{units};
+    $order{gst} += $item->{gst} * $item->{units};
+  }
+
+  if (my ($msg, $id) = $class->_need_logon($req, \@cart, \@products)) {
+    return $class->_refresh_logon($req, $msg, $id);
+  }
+
+  $order{orderDate} = now_sqldatetime;
+  $order{paymentType} = $paymentType;
+  ++$session->{changed};
+
+  # blank anything else
+  for my $column (@columns) {
+    defined $order{$column} or $order{$column} = '';
+  }
+  # make sure the user can't set these behind our backs
+  $order{filled} = 0;
+  $order{paidFor} = 0;
+
+  my $user = $req->siteuser;
+  if ($user) {
+    $order{userId} = $user->{userId};
+    $order{siteuser_id} = $user->{id};
+  }
+  else {
+    $order{userId} = '';
+    $order{siteuser_id} = -1;
+  }
+
+  # this should be hard to guess
+  $order{randomId} = md5_hex(time().rand().{}.$$);
+
+  # check if a customizer has anything to do
+  # if it sets shipping cost it must also update the total
+  eval {
+    my %custom = %{$session->{custom}};
+    $cust_class->order_save($cgi, \%order, \@cart, \@products, 
+                           \%custom, $cfg);
+    $session->{custom} = \%custom;
+  };
+  if ($@) {
+    return $class->req_checkout($req, $@, 1);
+  }
+
+  $order{total} += $cust_class->total_extras(\@cart, \@products, 
+                                            $session->{custom}, $cfg, 'final');
+
+  my %subscribing_to;
+
+  # load up the database
+  my @data = @order{@columns};
+  shift @data; # lose the dummy id
+  my $order = BSE::TB::Orders->add(@data)
+    or die "Cannot add order";
+  my @items;
+  my @item_cols = BSE::TB::OrderItem->columns;
+  my @prod_xfer = qw/title summary subscription_id subscription_period/;
+  for my $row_num (0..$#cart) {
+    my $row = $cart[$row_num];
+    my $product = $products[$row_num];
+    $row->{orderId} = $order->{id};
+
+    # store product data too
+    @$row{@prod_xfer} = @{$product}{@prod_xfer};
+
+    # store the lapsed value, this prevents future changes causing
+    # variation of the expiry date
+    $row->{max_lapsed} = 0;
+    if ($product->{subscription_id} != -1) {
+      my $sub = $product->subscription;
+      $row->{max_lapsed} = $sub->{max_lapsed} if $sub;
+    }
+
+    my @data = @$row{@item_cols};
+    
+    shift @data;
+    push(@items, BSE::TB::OrderItems->add(@data));
+
+    my $sub = $product->subscription;
+    if ($sub) {
+      $subscribing_to{$sub->{text_id}} = $sub;
+    }
+  }
+
+  if ($user) {
+    $user->recalculate_subscriptions($cfg);
+  }
+
+  my $item_index = -1;
+  my @options;
+  my $option_index;
+  my %acts;
+  %acts =
+    (
+     $cust_class->purchase_actions(\%acts, \@items, \@products, 
+                                  $session->{custom}, $cfg),
+     BSE::Util::Tags->static(\%acts, $cfg),
+     iterate_items_reset => sub { $item_index = -1; },
+     iterate_items => 
+     sub { 
+       if (++$item_index < @items) {
+        $option_index = -1;
+        @options = cart_item_opts($items[$item_index], 
+                                  $products[$item_index]);
+        return 1;
+       }
+       return 0;
+     },
+     item=> sub { escape_html($items[$item_index]{$_[0]}); },
+     product => 
+     sub { 
+       my $value = $products[$item_index]{$_[0]};
+       defined $value or $value = '';
+
+       escape_html($value);
+     },
+     extended =>
+     sub { 
+       my $what = $_[0] || 'retailPrice';
+       $items[$item_index]{units} * $items[$item_index]{$what};
+     },
+     order => sub { escape_html($order->{$_[0]}) },
+     money =>
+     sub {
+       my ($func, $args) = split ' ', $_[0], 2;
+       $acts{$func} || return "<: money $_[0] :>";
+       return sprintf("%.02f", $acts{$func}->($args)/100);
+     },
+     _format =>
+     sub {
+       my ($value, $fmt) = @_;
+       if ($fmt =~ /^m(\d+)/) {
+        return sprintf("%$1s", sprintf("%.2f", $value/100));
+       }
+       elsif ($fmt =~ /%/) {
+        return sprintf($fmt, $value);
+       }
+     },
+     iterate_options_reset => sub { $option_index = -1 },
+     iterate_options => sub { ++$option_index < @options },
+     option => sub { escape_html($options[$option_index]{$_[0]}) },
+     ifOptions => sub { @options },
+     options => sub { nice_options(@options) },
+     ifPayment => [ \&tag_ifPayments, $order->{paymentType}, \%types_by_name ],
+     #ifSubscribingTo => [ \&tag_ifSubscribingTo, \%subscribing_to ],
+    );
+  for my $type (@pay_types) {
+    my $id = $type->{id};
+    my $name = $type->{name};
+    $acts{"if${name}Payment"} = $order->{paymentType} == $id;
+  }
+  send_order($order, \@items, \@products, $noencrypt, \%subscribing_to);
+  $session->{cart} = []; # empty the cart
+
+  return req->response('checkoutfinal', \%acts);
+}
+
+sub req_recalc {
+  my ($class, $req) = @_;
+  $class->update_quantities($req);
+  $req->session->{order_info_confirmed} = 0;
+  return $class->req_cart($req);
+}
+
+sub req_recalculate {
+  my ($class, $req) = @_;
+
+  return $class->req_recalc($req);
+}
+
+sub _send_order {
+  my ($class, $req, $order, $items, $products, $noencrypt, 
+      $subscribing_to) = @_;
+
+  my $cfg = $req->cfg;
+  my $cgi = $req->cgi;
+
+  my $crypto_class = $Constants::SHOP_CRYPTO;
+  my $signing_id = $Constants::SHOP_SIGNING_ID;
+  my $pgp = $Constants::SHOP_PGP;
+  my $pgpe = $Constants::SHOP_PGPE;
+  my $gpg = $Constants::SHOP_GPG;
+  my $passphrase = $Constants::SHOP_PASSPHRASE;
+  my $from = $cfg->entry('shop', 'from', $Constants::SHOP_FROM);
+  my $toName = $cfg->entry('shop', 'to_name', $Constants::SHOP_TO_NAME);
+  my $toEmail = $cfg->entry('shop', 'to_email', $Constants::SHOP_TO_EMAIL);
+  my $subject = $cfg->entry('shop', 'subject', $Constants::SHOP_MAIL_SUBJECT);
+
+  my $session = $req->session;
+  my %extras = $cfg->entriesCS('extra tags');
+  for my $key (keys %extras) {
+    # follow any links
+    my $data = $cfg->entryVar('extra tags', $key);
+    $extras{$key} = sub { $data };
+  }
+
+  my $item_index = -1;
+  my @options;
+  my $option_index;
+  my %acts;
+  %acts =
+    (
+     %extras,
+     custom_class($cfg)
+     ->order_mail_actions(\%acts, $order, $items, $products, 
+                         $session->{custom}, $cfg),
+     BSE::Util::Tags->static(\%acts, $cfg),
+     iterate_items_reset => sub { $item_index = -1; },
+     iterate_items => 
+     sub { 
+       if (++$item_index < @$items) {
+        $option_index = -1;
+        @options = cart_item_opts($items->[$item_index], 
+                                  $products->[$item_index]);
+        return 1;
+       }
+       return 0;
+     },
+     item=> sub { $items->[$item_index]{$_[0]}; },
+     product => 
+     sub { 
+       my $value = $products->[$item_index]{$_[0]};
+       defined($value) or $value = '';
+       $value;
+     },
+     order => sub { $order->{$_[0]} },
+     extended => 
+     sub {
+       $items->[$item_index]{units} * $items->[$item_index]{$_[0]};
+     },
+     _format =>
+     sub {
+       my ($value, $fmt) = @_;
+       if ($fmt =~ /^m(\d+)/) {
+        return sprintf("%$1s", sprintf("%.2f", $value/100));
+       }
+       elsif ($fmt =~ /%/) {
+        return sprintf($fmt, $value);
+       }
+       elsif ($fmt =~ /^\d+$/) {
+        return substr($value . (" " x $fmt), 0, $fmt);
+       }
+       else {
+        return $value;
+       }
+     },
+     iterate_options_reset => sub { $option_index = -1 },
+     iterate_options => sub { ++$option_index < @options },
+     option => sub { escape_html($options[$option_index]{$_[0]}) },
+     ifOptions => sub { @options },
+     options => sub { nice_options(@options) },
+     with_wrap => \&tag_with_wrap,
+     ifSubscribingTo => [ \&tag_ifSubscribingTo, $subscribing_to ],
+    );
+
+  my $mailer = BSE::Mail->new(cfg=>$cfg);
+  # ok, send some email
+  my $confirm = BSE::Template->get_page('mailconfirm', $cfg, \%acts);
+  my $email_order = $cfg->entryBool('shop', 'email_order', $Constants::SHOP_EMAIL_ORDER);
+  if ($email_order) {
+    unless ($noencrypt) {
+      $acts{cardNumber} = $cgi->param('cardNumber');
+      $acts{cardExpiry} = $cgi->param('cardExpiry');
+    }
+    my $ordertext = BSE::Template->get_page('mailorder', $cfg, \%acts);
+    
+    my $send_text;
+    if ($noencrypt) {
+      $send_text = $ordertext;
+    }
+    else {
+      eval "use $crypto_class";
+      !$@ or die $@;
+      my $encrypter = $crypto_class->new;
+      
+      my $debug = $cfg->entryBool('debug', 'mail_encryption', 0);
+      my $sign = $cfg->entryBool('basic', 'sign', 1);
+      
+      # encrypt and sign
+      my %opts = 
+       (
+        sign=> $sign,
+        passphrase=> $passphrase,
+        stripwarn=>1,
+        debug=>$debug,
+       );
+      
+      $opts{secretkeyid} = $signing_id if $signing_id;
+      $opts{pgp} = $pgp if $pgp;
+      $opts{gpg} = $gpg if $gpg;
+      $opts{pgpe} = $pgpe if $pgpe;
+      my $recip = "$toName $toEmail";
+
+      $send_text = $encrypter->encrypt($recip, $ordertext, %opts )
+       or die "Cannot encrypt ", $encrypter->error;
+    }
+    $mailer->send(to=>$toEmail, from=>$from, subject=>'New Order '.$order->{id},
+                 body=>$send_text)
+      or print STDERR "Error sending order to admin: ",$mailer->errstr,"\n";
+  }
+  $mailer->send(to=>$order->{emailAddress}, from=>$from,
+               subject=>$subject . " " . localtime,
+               body=>$confirm)
+    or print STDERR "Error sending order to customer: ",$mailer->errstr,"\n";
+}
+
+sub tag_with_wrap {
+  my ($args, $text) = @_;
+
+  my $margin = $args =~ /^\d+$/ && $args > 30 ? $args : 70;
+
+  require Text::Wrap;
+  # do it twice to prevent a warning
+  $Text::Wrap::columns = $margin;
+  $Text::Wrap::columns = $margin;
+
+  return Text::Wrap::fill('', '', split /\n/, $text);
+}
+
+sub _refresh_logon {
+  my ($class, $req, $msg, $msgid, $r) = @_;
+
+  my $securlbase = $req->cfg->entryVar('site', 'secureurl');
+  my $url = $securlbase."/cgi-bin/user.pl";
+
+  $r ||= $securlbase."/cgi-bin/shop.pl?checkout=1";
+  
+  my %parms;
+  $parms{r} = $r;
+  $parms{message} = $msg if $msg;
+  $parms{mid} = $msgid if $msgid;
+  $url .= "?" . join("&", map "$_=".escape_uri($parms{$_}), keys %parms);
+  
+  return BSE::Template->get_refresh($url, $req->cfg);
+}
+
+sub _need_logon {
+  my ($class, $req, $cart, $cart_prods) = @_;
+
+  return need_logon($req->cfg, $cart, $cart_prods, $req->session, $req->cgi);
+}
+
+sub tag_checkedPayment {
+  my ($payment, $types_by_name, $args) = @_;
+
+  my $type = $args;
+  if ($type !~ /^\d+$/) {
+    return '' unless exists $types_by_name->{$type};
+    $type = $types_by_name->{$type};
+  }
+
+  return $payment == $type  ? 'checked="checked"' : '';
+}
+
+sub tag_ifPayments {
+  my ($enabled, $types_by_name, $args) = @_;
+
+  my $type = $args;
+  if ($type !~ /^\d+$/) {
+    return '' unless exists $types_by_name->{$type};
+    $type = $types_by_name->{$type};
+  }
+
+  my @found = grep $_ == $type, @$enabled;
+
+  return scalar @found;
+}
+
+sub update_quantities {
+  my ($class, $req) = @_;
+
+  my $session = $req->session;
+  my $cgi = $req->cgi;
+  my $cfg = $req->cfg;
+  my @cart = @{$session->{cart} || []};
+  for my $index (0..$#cart) {
+    my $new_quantity = $cgi->param("quantity_$index");
+    if (defined $new_quantity) {
+      if ($new_quantity =~ /^\s*(\d+)/) {
+       $cart[$index]{units} = $1;
+      }
+      elsif ($new_quantity =~ /^\s*$/) {
+       $cart[$index]{units} = 0;
+      }
+    }
+  }
+  @cart = grep { $_->{units} != 0 } @cart;
+  $session->{cart} = \@cart;
+  $session->{custom} ||= {};
+  my %custom_state = %{$session->{custom}};
+  custom_class($cfg)->recalc($cgi, \@cart, [], \%custom_state, $cfg);
+  $session->{custom} = \%custom_state;
+}
+
+sub _build_items {
+  my ($class, $req, $products) = @_;
+
+  my $session = $req->session;
+  $session->{cart}
+    or return;
+  my @msgs;
+  my @cart = @{$req->session->{cart}}
+    or return;
+  my @items;
+  my @prodcols = Product->columns;
+  my @newcart;
+  my $today = now_sqldate();
+  for my $item (@cart) {
+    my %work = %$item;
+    my $product = Products->getByPkey($item->{productId});
+    if ($product) {
+      (my $comp_release = $product->{release}) =~ s/ .*//;
+      (my $comp_expire = $product->{expire}) =~ s/ .*//;
+      $comp_release le $today
+       or do { push @msgs, "'$product->{title}' has not been released yet";
+               next; };
+      $today le $comp_expire
+       or do { push @msgs, "'$product->{title}' has expired"; next; };
+      $product->{listed} 
+       or do { push @msgs, "'$product->{title}' not available"; next; };
+
+      for my $col (@prodcols) {
+       $work{$col} = $product->{$col} unless exists $work{$col};
+      }
+      $work{extended_retailPrice} = $work{units} * $work{retailPrice};
+      $work{extended_gst} = $work{units} * $work{gst};
+      $work{extended_wholesale} = $work{units} * $work{wholesalePrice};
+      
+      push @newcart, \%work;
+      push @$products, $product;
+    }
+  }
+
+  # we don't use these for anything for now
+  #if (@msgs) {
+  #  @$rmsg = @msgs;
+  #}
+
+  return @newcart;
+}
+
+sub _fillout_order {
+  my ($class, $req, $values, $items, $rmsg, $how) = @_;
+
+  my $session = $req->session;
+  my $cfg = $req->cfg;
+  my $cgi = $req->cgi;
+
+  my $total = 0;
+  my $total_gst = 0;
+  my $total_wholesale = 0;
+  for my $item (@$items) {
+    $total += $item->{extended_retailPrice};
+    $total_gst += $item->{extended_gst};
+    $total_wholesale += $item->{extended_wholesale};
+  }
+  $values->{total} = $total;
+  $values->{gst} = $total_gst;
+  $values->{wholesale} = $total_wholesale;
+  $values->{shipping_cost} = 0;
+
+  my $cust_class = custom_class($cfg);
+
+  # if it sets shipping cost it must also update the total
+  eval {
+    my %custom = %{$session->{custom}};
+    $cust_class->order_save($cgi, $values, $items, $items, 
+                           \%custom, $cfg);
+    $session->{custom} = \%custom;
+  };
+  if ($@) {
+    $$rmsg = $@;
+    return;
+  }
+
+  $values->{total} += 
+    $cust_class->total_extras($items, $items, 
+                             $session->{custom}, $cfg, $how);
+
+  my $affiliate_code = $session->{affiliate_code};
+  defined $affiliate_code && length $affiliate_code
+    or $affiliate_code = $cgi->param('affiliate_code');
+  defined $affiliate_code or $affiliate_code = '';
+  $values->{affiliate_code} = $affiliate_code;
+
+  my $user = $req->siteuser;
+  if ($user) {
+    $values->{userId} = $user->{userId};
+    $values->{siteuser_id} = $user->{id};
+  }
+  else {
+    $values->{userId} = '';
+    $values->{siteuser_id} = -1;
+  }
+
+  $values->{orderDate} = now_sqldatetime;
+
+  # this should be hard to guess
+  $values->{randomId} ||= md5_hex(time().rand().{}.$$);
+
+  return 1;
+}
+
+sub action_prefix { '' }
+
+1;
diff --git a/site/cgi-bin/modules/DevHelp/Payments/Inpho.pm b/site/cgi-bin/modules/DevHelp/Payments/Inpho.pm
new file mode 100644 (file)
index 0000000..5e3e93b
--- /dev/null
@@ -0,0 +1,134 @@
+package DevHelp::Payments::Inpho;
+use strict;
+use Carp 'confess';
+use LWP::UserAgent;
+use DevHelp::HTML;
+
+sub new {
+  my ($class, $cfg) = @_;
+
+  return bless { cfg => $cfg }, $class;
+}
+
+sub payment {
+  my ($self, %args) = @_;
+
+  my $cfg = $self->{cfg};
+
+  my $conf = $self->_conf;
+
+  for my $name (qw/orderno amount cardnumber expirydate ipaddress cvv/) {
+    defined $args{$name}
+      or confess "Missing $name argument";
+  }
+
+  my ($expyear, $expmonth) = $args{expirydate} =~ /^(\d\d\d\d)(\d\d)$/
+    or return
+      {
+       success => 0,
+       statuscode => 1,
+       error => 'Invalid expiry date',
+      };
+
+  my $currency = $args{currency};
+  if (defined $currency && $currency ne 'AUD') {
+    return
+      {
+       success => 0,
+       error => 'Unknown currency',
+       statuscode => 5,
+      };
+  }
+  $currency = 'AUD';
+
+  my $url = $conf->{url};
+  my %url_args =
+    (
+     PAN => $args{cardnumber},
+     expiry_month => $expmonth,
+     expiry_year => sprintf("%02d", $expyear % 100),
+     cardVerificationData => $args{cvv},
+     currency_type => $currency,
+     currency_amount => $args{amount},
+     cardholderIP => $args{ipaddress},
+     user_name => $conf->{user},
+     user_password => $conf->{password},
+    );
+  $url .= '?' . join("&", map "$_=" . escape_uri($url_args{$_}), keys %url_args);
+  
+  my $ua = LWP::UserAgent->new;
+  my $response = $ua->get($url);
+  unless ($response->is_success) {
+    return
+      {
+       success => 0,
+       statuscode => 99,
+       error => 'Error making request: '.$response->status_line,
+      };
+  }
+  my $content = $response->content;
+  if ($content eq '0') {
+    return
+      {
+       success => 0,
+       statuscode => 98,
+       error => "Invalid request or invalid merchant user/password",
+      };
+  }
+
+  # extract the fields from it
+  my %result;
+  for my $line (split /,/, $content) {
+    if ($line =~ /^(\w+)=(.*)$/) {
+      $result{$1} = $2;
+    }
+  }
+
+  if ($result{SUMMARY_RESPONSE_CODE} != 0) {
+    return
+      {
+       success => 0,
+       statuscode => $result{RESPONSE_CODE},
+       error => $result{RESPONSE_TEXT},
+      };
+  }
+
+  # should be success
+  return
+    {
+     success => 1,
+     statuscode => $result{RESPONSE_CODE},
+     error => $result{RESPONSE_TEXT},
+     receipt => $result{RECEIPT_NUMBER},
+     transactionid => $result{ORDER_NUMBER},
+    };
+}
+
+my %norm_defs =
+  (
+   url => 'https://extranet.inpho.com.au/cc_ssl/process',
+  );
+
+my %test_defs =
+  (
+  );
+
+sub _conf {
+  my ($self) = @_;
+
+  my $cfg = $self->{cfg};
+  my $test = $cfg->entryBool('inpho', 'test');
+  my $prefix = $test ? 'test_' : '';
+  my $defs = $test ? \%test_defs : \%norm_defs;
+  my %conf;
+  for my $key (qw(url user password)) {
+    my $value = $cfg->entry('inpho', $prefix.$key, $defs->{$key});
+    defined $value
+      or confess "Key $prefix$key not defined in [inpho] for credit card processing";
+    $conf{$key} = $value;
+  }
+
+  \%conf;
+}
+
+1;
diff --git a/site/cgi-bin/modules/DevHelp/Payments/Test.pm b/site/cgi-bin/modules/DevHelp/Payments/Test.pm
new file mode 100644 (file)
index 0000000..7744461
--- /dev/null
@@ -0,0 +1,138 @@
+package DevHelp::Payments::Test;
+use strict;
+use Carp 'confess';
+
+sub new {
+  my ($class, $cfg) = @_;
+
+  return bless { cfg => $cfg }, $class;
+}
+
+sub payment {
+  my ($self, %args) = @_;
+
+  my $cfg = $self->{cfg};
+
+  for my $name (qw/orderno amount cardnumber expirydate ipaddress cvv/) {
+    defined $args{$name}
+      or confess "Missing $name argument";
+  }
+
+  my $currency = $args{currency};
+  if (defined $currency && $currency ne 'AUD') {
+    return
+      {
+       success => 0,
+       error => 'Unknown currency',
+       statuscode => 5,
+      };
+  }
+
+  if (my $result = $cfg->entry('test payments', 'result')) {
+    my ($code, $error) = split /;/, $result, 2;
+    return 
+      {
+       success => 0,
+       statuscode => $code,
+       error => $error,
+      };
+  }
+
+  my ($expyear, $expmonth) = $args{expirydate} =~ /^(\d\d\d\d)(\d\d)$/
+    or return
+      {
+       success => 0,
+       statuscode => 1,
+       error => 'Invalid expiry date',
+      };
+
+  my ($nowyear, $nowmonth) = (localtime)[5, 4];
+  $nowyear += 1900;
+  ++$nowmonth;
+
+  if ($expyear < $nowyear || $expyear == $nowyear && $expmonth < $nowmonth) {
+    return
+      {
+       success => 0,
+       statuscode => 1,
+       error => 'Card expired',
+      };
+  }
+
+  unless ($args{amount} =~ /^\d+$/ && $args{amount} > 0) {
+    return
+      {
+       success => 0,
+       statuscode => 2,
+       error => 'Invalid amount',
+      };
+  }
+
+  $args{cardnumber} =~ tr/0-9//cd;
+  if ($args{cardnumber} eq '4111111111111100') {
+    return
+      {
+       success => 1,
+       statuscode => 0,
+       error => '',
+       receipt => "R$args{orderno}",
+       transactionid => '',
+      };
+  }
+  elsif ($args{cardnumber} =~ /(\d\d)$/) {
+    return
+      {
+       success => 0,
+       statuscode => 0+$1,
+       error => "Synthetic error $1",
+      };
+  }
+}
+
+1;
+
+=head1 NAME
+
+DevHelp::Payments::Test - test payments driver
+
+=head1 SYNOPSIS
+
+  my $obj = DevHelp::Payments::Test->new($cfg);
+
+  my $result = $obj->payment(orderno=>$order_number,
+                             amount => $amount_in_cents,
+                             cardnumber => $cc_number,
+                             expirydate => $yyyymm,
+                             ipaddress => $user_ip,
+                             cvv => $cvv);
+  if ($result->{success}) {
+    print "Receipt: $result->{receipt}\n";
+  }
+  else {
+    print "Error: $result->{error}\n";
+  }
+
+=head1 TESTING
+
+This module provides mechanisms for testing credit card transactions.
+
+To use it add 
+
+  cardprocessor=DevHelp::Payments::Test
+
+to the [shop] section of bse.cfg (or an includes config file.)
+
+The amount, currency type, and card expiry date are all validated.
+
+If the credit card number supplied is: 4111111111111100 the
+transaction will succeed.
+
+Otherwise the last 2 digits of the credit card number are used to
+synthesize an error.
+
+=head1 AUTHOR
+
+Tony Cook <tony@develop-help.com>
+
+=back
+
index f0f0a5878d9b638b72899d6372080f68bf371daa..3cfef3f8b56b7a5bec98c619fcd8ab9d31954389 100644 (file)
@@ -69,6 +69,15 @@ my %built_ins =
    {
     ccexpiry => 1,
    },
+   creditcardexpirysingle =>
+   {
+    ccexpirysingle => 1,
+   },
+   creditcardcvv =>
+   {
+    match => qr/^(\d){3,4}$/,
+    error => '$n is the 3 or 4 digit code on the back of your card',
+   },
    miaa =>
    {
     match => qr/^\s*\d{1,6}\s*$/,
@@ -369,6 +378,28 @@ sub validate_field {
          last RULE;
        }
       }
+      if ($rule->{ccexpirysingle}) {
+       unless ($data =~ m!^\s*(\d+)\s*/\s*(\d+)+\s*$!) {
+         $errors->{$field} = _make_error($field, $info, $rule,
+                                         q!$n must be in MM/YY format!);
+         last RULE;
+       }
+       my ($month, $year) = ($1, $2);
+       $year += 2000;
+       if ($month < 1 || $month > 12) {
+         $errors->{$field} = _make_error($field, $info, $rule,
+                                         q!$n month must be between 1 and 12!);
+         last RULE;
+       }
+       my ($now_year, $now_month) = (localtime)[5, 4];
+       $now_year += 1900;
+       ++$now_month;
+       if ($year < $now_year || $year == $now_year && $month < $now_month) {
+         $errors->{$field} = _make_error($field, $info, $rule,
+                                         q!$n is in the past, your card has expired!);
+         last RULE;
+       }
+      }
     }
   }
 }
index 3c05266350a36d4769254429167ac2cc6543b25d..c672dd369d41c7735e20e00f86089d135f20390d 100644 (file)
@@ -105,7 +105,8 @@ sub generate_shop {
   my ($articles) = @_;
   my @pages =
     (
-     'cart', 'checkout', 'checkoutfinal', 'checkoutcard', 'checkoutconfirm',
+     'cart', 'checkoutnew', 'checkoutfinal', 'checkoutcard', 'checkoutconfirm',
+     'checkoutpay',
     );
   require 'Generate/Article.pm';
   my $shop = $articles->getByPkey($SHOPID);
index d969c98108e2660f9d44ff53b945c2396c584377..9d91849af1165dd85bd2413e76e075ef23adb503 100755 (executable)
-#!/usr/bin/perl -w
-# -d:ptkdb
+#!/usr/bin/perl -w -d:ptkdb
 BEGIN { $ENV{DISPLAY} = '192.168.32.15:0.0' }
 use strict;
 use FindBin;
 use lib "$FindBin::Bin/modules";
 use CGI ':standard';
-use Products;
-use Product;
-use Constants qw(:shop $CGI_URI);
+use BSE::Request;
+use BSE::UI::Shop;
 use BSE::Template;
-use CGI::Cookie;
-use BSE::WebUtil qw(refresh_to);
-use BSE::CfgInfo qw(custom_class);
-use BSE::Mail;
-use BSE::Shop::Util qw/shop_cart_tags cart_item_opts nice_options total 
-                       basic_tags load_order_fields need_logon get_siteuser
-                       payment_types/;
-use BSE::Session;
-use BSE::Cfg;
-use BSE::Util::Tags qw(tag_hash);
-use DevHelp::HTML;
-
-my $cfg = BSE::Cfg->new();
-
-my $subject = $cfg->entry('shop', 'subject', $SHOP_MAIL_SUBJECT);
-
-# our PGP passphrase
-my $passphrase = $SHOP_PASSPHRASE;
-
-# the class we use to perform encryption
-# we can change this to switch between GnuPG and PGP
-my $crypto_class = $SHOP_CRYPTO;
-
-# id of the private key to use for signing
-# leave as undef to use your default key
-my $signing_id = $SHOP_SIGNING_ID;
-
-# location of PGP
-my $pgpe = $SHOP_PGPE;
-my $pgp = $SHOP_PGP;
-my $gpg = $SHOP_GPG;
-
-my $from = $cfg->entry('shop', 'from', $SHOP_FROM);
-
-my $toName = $cfg->entry('shop', 'to_name', $SHOP_TO_NAME);
-my $toEmail= $cfg->entry('shop', 'to_email', $SHOP_TO_EMAIL);
-
-use constant PAYMENT_CC => 0;
-use constant PAYMENT_CHEQUE => 1;
-use constant PAYMENT_CALLME => 2;
-
-my $urlbase = $cfg->entryVar('site', 'url');
-my $securlbase = $cfg->entryVar('site', 'secureurl');
-my %session;
-BSE::Session->tie_it(\%session, $cfg);
-
-# this shouldn't be necessary, but it stopped working elsewhere and this
-# fixed it
-END {
-  untie %session;
-}
-
-if (!exists $session{cart}) {
-  $session{cart} = [];
-}
-
-# the keys here are the names of the buttons on the various forms
-# we also have 'delete_<number>' buttons.
-my %steps =
-  (
-   add=>\&add_item,
-   cart=>\&show_cart,
-   checkout=>\&checkout,
-   checkupdate => \&checkupdate,
-   recheckout => sub { checkout('', 1); },
-   confirm => \&checkout_confirm,
-   recalc=>\&recalc,
-   recalculate=>\&recalc,
-   purchase=>\&purchase,
-   #prePurchase=>\&prePurchase,
-  );
-
-for my $key (keys %steps) {
-  if (param($key) or param("$key.x")) {
-    $steps{$key}->();
-    exit;
-  }
-}
-
-for my $key (param()) {
-  if ($key =~ /^delete_(\d+)/) {
-    remove_item($1);
-    exit;
-  }
-}
-
-show_cart();
-
-sub add_item {
-  my $addid = param('id');
-  $addid ||= '';
-  my $quantity = param('quantity');
-  $quantity ||= 1;
-  my $product;
-  $product = Products->getByPkey($addid) if $addid;
-  $product or return show_cart("Cannot find product $addid"); # oops
-
-  # collect the product options
-  my @options;
-  my @opt_names = split /,/, $product->{options};
-  my @not_def;
-  for my $name (@opt_names) {
-    my $value = param($name);
-    push @options, $value;
-    unless (defined $value) {
-      push @not_def, $name;
-    }
-  }
-  @not_def
-    and return show_cart("Some product options (@not_def) not supplied");
-  my $options = join(",", @options);
+use Carp 'confess';
+
+$SIG{__DIE__} = sub { confess $@ };
+
+my $req = BSE::Request->new;
+my $result = BSE::UI::Shop->dispatch($req);
+BSE::Template->output_result($req, $result);
+
+
+
+# use Products;
+# use Product;
+# use Constants qw(:shop $CGI_URI);
+# use BSE::Template;
+# use CGI::Cookie;
+# use BSE::WebUtil qw(refresh_to);
+# use BSE::CfgInfo qw(custom_class);
+# use BSE::Mail;
+# use BSE::Shop::Util qw/shop_cart_tags cart_item_opts nice_options total 
+#                        basic_tags load_order_fields need_logon get_siteuser
+#                        payment_types/;
+# use BSE::Session;
+# use BSE::Cfg;
+# use BSE::Util::Tags qw(tag_hash);
+# use DevHelp::HTML;
+
+# my $cfg = BSE::Cfg->new();
+
+# my $subject = $cfg->entry('shop', 'subject', $SHOP_MAIL_SUBJECT);
+
+# # our PGP passphrase
+# my $passphrase = $SHOP_PASSPHRASE;
+
+# # the class we use to perform encryption
+# # we can change this to switch between GnuPG and PGP
+# my $crypto_class = $SHOP_CRYPTO;
+
+# # id of the private key to use for signing
+# # leave as undef to use your default key
+# my $signing_id = $SHOP_SIGNING_ID;
+
+# # location of PGP
+# my $pgpe = $SHOP_PGPE;
+# my $pgp = $SHOP_PGP;
+# my $gpg = $SHOP_GPG;
+
+# my $from = $cfg->entry('shop', 'from', $SHOP_FROM);
+
+# my $toName = $cfg->entry('shop', 'to_name', $SHOP_TO_NAME);
+# my $toEmail= $cfg->entry('shop', 'to_email', $SHOP_TO_EMAIL);
+
+# use constant PAYMENT_CC => 0;
+# use constant PAYMENT_CHEQUE => 1;
+# use constant PAYMENT_CALLME => 2;
+
+# my $urlbase = $cfg->entryVar('site', 'url');
+# my $securlbase = $cfg->entryVar('site', 'secureurl');
+# my %session;
+# BSE::Session->tie_it(\%session, $cfg);
+
+# # this shouldn't be necessary, but it stopped working elsewhere and this
+# # fixed it
+# END {
+#   untie %session;
+# }
+
+# if (!exists $session{cart}) {
+#   $session{cart} = [];
+# }
+
+# # the keys here are the names of the buttons on the various forms
+# # we also have 'delete_<number>' buttons.
+# my %steps =
+#   (
+#    add=>\&add_item,
+#    cart=>\&show_cart,
+#    checkout=>\&checkout,
+#    checkupdate => \&checkupdate,
+#    recheckout => sub { checkout('', 1); },
+#    confirm => \&checkout_confirm,
+#    recalc=>\&recalc,
+#    recalculate=>\&recalc,
+#    purchase=>\&purchase,
+#    #prePurchase=>\&prePurchase,
+#   );
+
+# for my $key (keys %steps) {
+#   if (param($key) or param("$key.x")) {
+#     $steps{$key}->();
+#     exit;
+#   }
+# }
+
+# for my $key (param()) {
+#   if ($key =~ /^delete_(\d+)/) {
+#     remove_item($1);
+#     exit;
+#   }
+# }
+
+# show_cart();
+
+# sub add_item {
+#   my $addid = param('id');
+#   $addid ||= '';
+#   my $quantity = param('quantity');
+#   $quantity ||= 1;
+#   my $product;
+#   $product = Products->getByPkey($addid) if $addid;
+#   $product or return show_cart("Cannot find product $addid"); # oops
+
+#   # collect the product options
+#   my @options;
+#   my @opt_names = split /,/, $product->{options};
+#   my @not_def;
+#   for my $name (@opt_names) {
+#     my $value = param($name);
+#     push @options, $value;
+#     unless (defined $value) {
+#       push @not_def, $name;
+#     }
+#   }
+#   @not_def
+#     and return show_cart("Some product options (@not_def) not supplied");
+#   my $options = join(",", @options);
   
-  # the product must be non-expired and listed
-  use BSE::Util::SQL qw(now_sqldate);
-  (my $comp_release = $product->{release}) =~ s/ .*//;
-  (my $comp_expire = $product->{expire}) =~ s/ .*//;
-  my $today = now_sqldate();
-  $comp_release le $today
-    or return show_cart("Product has not been released yet");
-  $today le $comp_expire
-    or return show_cart("Product has expired");
-  $product->{listed} or return show_cart("Product not available");
+  # the product must be non-expired and listed
+  use BSE::Util::SQL qw(now_sqldate);
+  (my $comp_release = $product->{release}) =~ s/ .*//;
+  (my $comp_expire = $product->{expire}) =~ s/ .*//;
+  my $today = now_sqldate();
+  $comp_release le $today
+    or return show_cart("Product has not been released yet");
+  $today le $comp_expire
+    or return show_cart("Product has expired");
+  $product->{listed} or return show_cart("Product not available");
   
-  # used to refresh if a logon is needed
-  my $r = $securlbase . $ENV{SCRIPT_NAME} . "?add=1&id=$addid";
-  for my $opt_index (0..$#opt_names) {
-    $r .= "&$opt_names[$opt_index]=".escape_uri($options[$opt_index]);
-  }
+  # used to refresh if a logon is needed
+  my $r = $securlbase . $ENV{SCRIPT_NAME} . "?add=1&id=$addid";
+  for my $opt_index (0..$#opt_names) {
+    $r .= "&$opt_names[$opt_index]=".escape_uri($options[$opt_index]);
+  }
   
-  my $user = get_siteuser(\%session, $cfg, $CGI::Q);
-  # need to be logged on if it has any subs
-  if ($product->{subscription_id} != -1) {
-    if ($user) {
-      my $sub = $product->subscription;
-      if ($product->is_renew_sub_only) {
-       unless ($user->subscribed_to_grace($sub)) {
-         return show_cart("This product can only be used to renew your subscription to $sub->{title} and you are not subscribed nor within the renewal grace period");
-       }
-      }
-      elsif ($product->is_start_sub_only) {
-       if ($user->subscribed_to_grace($sub)) {
-         return show_cart("This product can only be used to start your subscription to $sub->{title} and you are already subscribed or within the grace period");
-       }
-      }
-    }
-    else {
-      refresh_logon("You must be logged on to add this product to your cart",
-                   'prodlogon', $r);
-      return;
-    }
-  }
-  if ($product->{subscription_required} != -1) {
-    my $sub = $product->subscription_required;
-    if ($user) {
-      unless ($user->subscribed_to($sub)) {
-       show_cart("You must be subscribed to $sub->{title} to purchase this product");
-       return;
-      }
-    }
-    else {
-      # we want to refresh back to adding the item to the cart if possible
-      refresh_logon("You must be logged on and subscribed to $sub->{title} to add this product to your cart",
-                   'prodlogonsub', $r);
-      return;
-    }
-  }
-
-  # we need a natural integer quantity
-  $quantity =~ /^\d+$/
-    or return show_cart("Invalid quantity");
-
-  my @cart = @{$session{cart}};
+  my $user = get_siteuser(\%session, $cfg, $CGI::Q);
+  # need to be logged on if it has any subs
+  if ($product->{subscription_id} != -1) {
+    if ($user) {
+      my $sub = $product->subscription;
+      if ($product->is_renew_sub_only) {
+#      unless ($user->subscribed_to_grace($sub)) {
+#        return show_cart("This product can only be used to renew your subscription to $sub->{title} and you are not subscribed nor within the renewal grace period");
+#      }
+      }
+      elsif ($product->is_start_sub_only) {
+#      if ($user->subscribed_to_grace($sub)) {
+#        return show_cart("This product can only be used to start your subscription to $sub->{title} and you are already subscribed or within the grace period");
+#      }
+      }
+    }
+    else {
+      refresh_logon("You must be logged on to add this product to your cart",
+#                  'prodlogon', $r);
+      return;
+    }
+  }
+  if ($product->{subscription_required} != -1) {
+    my $sub = $product->subscription_required;
+    if ($user) {
+      unless ($user->subscribed_to($sub)) {
+#      show_cart("You must be subscribed to $sub->{title} to purchase this product");
+#      return;
+      }
+    }
+    else {
+      # we want to refresh back to adding the item to the cart if possible
+      refresh_logon("You must be logged on and subscribed to $sub->{title} to add this product to your cart",
+#                  'prodlogonsub', $r);
+      return;
+    }
+  }
+
+  # we need a natural integer quantity
+  $quantity =~ /^\d+$/
+    or return show_cart("Invalid quantity");
+
+  my @cart = @{$session{cart}};
  
-  # if this is is already present, replace it
-  @cart = grep { $_->{productId} ne $addid || $_->{options} ne $options } 
-    @cart;
-  push(@cart, { productId => $addid, units => $quantity, 
-               price=>$product->{retailPrice},
-               options=>$options });
-
-  $session{cart} = \@cart;
-  show_cart();
-}
-
-sub show_cart {
-  my ($msg) = @_;
-  my @cart = @{$session{cart}};
-  my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
-  my $item_index = -1;
-  my @options;
-  my $option_index;
+  # if this is is already present, replace it
+  @cart = grep { $_->{productId} ne $addid || $_->{options} ne $options } 
+    @cart;
+  push(@cart, { productId => $addid, units => $quantity, 
+#              price=>$product->{retailPrice},
+#              options=>$options });
+
+  $session{cart} = \@cart;
+  show_cart();
+}
+
+sub show_cart {
+  my ($msg) = @_;
+  my @cart = @{$session{cart}};
+  my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
+  my $item_index = -1;
+  my @options;
+  my $option_index;
   
-  $session{custom} ||= {};
-  my %custom_state = %{$session{custom}};
-
-  my $cust_class = custom_class($cfg);
-  $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $cfg); 
-  $msg = '' unless defined $msg;
-  $msg = CGI::escapeHTML($msg);
-
-  my %acts;
-  %acts =
-    (
-     $cust_class->cart_actions(\%acts, \@cart, \@cart_prods, \%custom_state, 
-                              $cfg),
-     shop_cart_tags(\%acts, \@cart, \@cart_prods, \%session, $CGI::Q, $cfg,
-                   'cart'),
-     basic_tags(\%acts),
-     msg => $msg,
-    );
-  $session{custom} = \%custom_state;
-
-  page('cart.tmpl', \%acts);
-}
-
-sub update_quantities {
-  my @cart = @{$session{cart}};
-  for my $index (0..$#cart) {
-    my $new_quantity = param("quantity_$index");
-    if (defined $new_quantity) {
-      if ($new_quantity =~ /^\s*(\d+)/) {
-       $cart[$index]{units} = $1;
-      }
-      elsif ($new_quantity =~ /^\s*$/) {
-       $cart[$index]{units} = 0;
-      }
-    }
-  }
-  @cart = grep { $_->{units} != 0 } @cart;
-  $session{cart} = \@cart;
-  $session{custom} ||= {};
-  my %custom_state = %{$session{custom}};
-  custom_class($cfg)->recalc($CGI::Q, \@cart, [], \%custom_state, $cfg);
-  $session{custom} = \%custom_state;
-}
-
-sub recalc {
-  update_quantities();
-  show_cart();
-}
-
-sub remove_item {
-  my ($index) = @_;
-  my @cart = @{$session{cart}};
-  if ($index >= 0 && $index < @cart) {
-    splice(@cart, $index, 1);
-  }
-  $session{cart} = \@cart;
-
-  refresh_to($ENV{SCRIPT_NAME});
-}
-
-sub checkupdate {
-  my @cart = @{$session{cart}};
-  my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
-  $session{custom} ||= {};
-  my %custom_state = %{$session{custom}};
-  custom_class($cfg)
-      ->checkout_update($CGI::Q, \@cart, \@cart_prods, \%custom_state, $cfg);
-  $session{custom} = \%custom_state;
+  $session{custom} ||= {};
+  my %custom_state = %{$session{custom}};
+
+  my $cust_class = custom_class($cfg);
+  $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $cfg); 
+  $msg = '' unless defined $msg;
+  $msg = CGI::escapeHTML($msg);
+
+  my %acts;
+  %acts =
+    (
+     $cust_class->cart_actions(\%acts, \@cart, \@cart_prods, \%custom_state, 
+#                             $cfg),
+     shop_cart_tags(\%acts, \@cart, \@cart_prods, \%session, $CGI::Q, $cfg,
+#                  'cart'),
+     basic_tags(\%acts),
+     msg => $msg,
+    );
+  $session{custom} = \%custom_state;
+
+  page('cart.tmpl', \%acts);
+}
+
+sub update_quantities {
+  my @cart = @{$session{cart}};
+  for my $index (0..$#cart) {
+    my $new_quantity = param("quantity_$index");
+    if (defined $new_quantity) {
+      if ($new_quantity =~ /^\s*(\d+)/) {
+#      $cart[$index]{units} = $1;
+      }
+      elsif ($new_quantity =~ /^\s*$/) {
+#      $cart[$index]{units} = 0;
+      }
+    }
+  }
+  @cart = grep { $_->{units} != 0 } @cart;
+  $session{cart} = \@cart;
+  $session{custom} ||= {};
+  my %custom_state = %{$session{custom}};
+  custom_class($cfg)->recalc($CGI::Q, \@cart, [], \%custom_state, $cfg);
+  $session{custom} = \%custom_state;
+}
+
+sub recalc {
+  update_quantities();
+  show_cart();
+}
+
+sub remove_item {
+  my ($index) = @_;
+  my @cart = @{$session{cart}};
+  if ($index >= 0 && $index < @cart) {
+    splice(@cart, $index, 1);
+  }
+  $session{cart} = \@cart;
+
+  refresh_to($ENV{SCRIPT_NAME});
+}
+
+sub checkupdate {
+  my @cart = @{$session{cart}};
+  my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
+  $session{custom} ||= {};
+  my %custom_state = %{$session{custom}};
+  custom_class($cfg)
+      ->checkout_update($CGI::Q, \@cart, \@cart_prods, \%custom_state, $cfg);
+  $session{custom} = \%custom_state;
   
-  checkout("", 1);
-}
-
-sub tag_checkedPayment {
-  my ($payment, $types_by_name, $args) = @_;
-
-  my $type = $args;
-  if ($type !~ /^\d+$/) {
-    return '' unless $types_by_name->{$type};
-    $type = $types_by_name->{$type};
-  }
-
-  return $payment == $type  ? 'checked="checked"' : '';
-}
-
-sub tag_ifPayments {
-  my ($enabled, $types_by_name, $args) = @_;
-
-  my $type = $args;
-  if ($type !~ /^\d+$/) {
-    return '' unless $types_by_name->{$type};
-    $type = $types_by_name->{$type};
-  }
-
-  my @found = grep $_ == $type, @$enabled;
-
-  return scalar @found;
-}
-
-# display the checkout form
-# can also be called with an error message and a flag to fillin the old
-# values for the form elements
-sub checkout {
-  my ($message, $olddata) = @_;
-
-  $message = '' unless defined $message;
-
-  update_quantities();
-  my @cart = @{$session{cart}};
-
-  @cart or return show_cart();
-
-  my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
-
-  if (my ($msg, $id) = need_logon($cfg, \@cart, \@cart_prods, \%session, $CGI::Q)) {
-    refresh_logon($msg, $id);
-    return;
-  }
-
-  my $user = get_siteuser(\%session, $cfg, $CGI::Q);
-
-  $session{custom} ||= {};
-  my %custom_state = %{$session{custom}};
-
-  my $cust_class = custom_class($cfg);
-  $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $cfg);
-
-  my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
-
-  my @pay_types = payment_types($cfg);
-  my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
-  my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
-  if ($noencrypt) {
-    @payment_types = grep $_ ne PAYMENT_CC, @payment_types;
-    @payment_types or @payment_types = ( PAYMENT_CALLME );
-  }
-  else {
-    @payment_types or @payment_types = ( PAYMENT_CC );
-  }
-  @payment_types = sort { $a <=> $b } @payment_types;
-  my %payment_types = map { $_=> 1 } @payment_types;
-  my $payment;
-  $olddata and $payment = param('paymentType');
-  defined $payment or $payment = $payment_types[0];
-
-  my $affiliate_code = $session{affiliate_code};
-  defined $affiliate_code or $affiliate_code = '';
-
-  my $item_index = -1;
-  my @options;
-  my $option_index;
-  my %acts;
-  %acts =
-    (
-     shop_cart_tags(\%acts, \@cart, \@cart_prods, \%session, $CGI::Q, $cfg,
-                  'checkout'),
-     basic_tags(\%acts),
-     message => sub { $message },
-     old => 
-     sub { 
-       my $value;
-
-       if ($olddata) {
-        $value = param($_[0]);
-        unless (defined $value) {
-          $value = $user->{$_[0]}
-            if $user;
-        }
-       }
-       else {
-        $value = $user && defined $user->{$_[0]} ? $user->{$_[0]} : '';
-       }
+  checkout("", 1);
+}
+
+sub tag_checkedPayment {
+  my ($payment, $types_by_name, $args) = @_;
+
+  my $type = $args;
+  if ($type !~ /^\d+$/) {
+    return '' unless $types_by_name->{$type};
+    $type = $types_by_name->{$type};
+  }
+
+  return $payment == $type  ? 'checked="checked"' : '';
+}
+
+sub tag_ifPayments {
+  my ($enabled, $types_by_name, $args) = @_;
+
+  my $type = $args;
+  if ($type !~ /^\d+$/) {
+    return '' unless $types_by_name->{$type};
+    $type = $types_by_name->{$type};
+  }
+
+  my @found = grep $_ == $type, @$enabled;
+
+  return scalar @found;
+}
+
+# display the checkout form
+# can also be called with an error message and a flag to fillin the old
+# values for the form elements
+sub checkout {
+  my ($message, $olddata) = @_;
+
+  $message = '' unless defined $message;
+
+  update_quantities();
+  my @cart = @{$session{cart}};
+
+  @cart or return show_cart();
+
+  my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
+
+  if (my ($msg, $id) = need_logon($cfg, \@cart, \@cart_prods, \%session, $CGI::Q)) {
+    refresh_logon($msg, $id);
+    return;
+  }
+
+  my $user = get_siteuser(\%session, $cfg, $CGI::Q);
+
+  $session{custom} ||= {};
+  my %custom_state = %{$session{custom}};
+
+  my $cust_class = custom_class($cfg);
+  $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $cfg);
+
+  my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
+
+  my @pay_types = payment_types($cfg);
+  my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
+  my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
+  if ($noencrypt) {
+    @payment_types = grep $_ ne PAYMENT_CC, @payment_types;
+    @payment_types or @payment_types = ( PAYMENT_CALLME );
+  }
+  else {
+    @payment_types or @payment_types = ( PAYMENT_CC );
+  }
+  @payment_types = sort { $a <=> $b } @payment_types;
+  my %payment_types = map { $_=> 1 } @payment_types;
+  my $payment;
+  $olddata and $payment = param('paymentType');
+  defined $payment or $payment = $payment_types[0];
+
+  my $affiliate_code = $session{affiliate_code};
+  defined $affiliate_code or $affiliate_code = '';
+
+  my $item_index = -1;
+  my @options;
+  my $option_index;
+  my %acts;
+  %acts =
+    (
+     shop_cart_tags(\%acts, \@cart, \@cart_prods, \%session, $CGI::Q, $cfg,
+#                 'checkout'),
+     basic_tags(\%acts),
+     message => sub { $message },
+     old => 
+     sub { 
+       my $value;
+
+       if ($olddata) {
+#       $value = param($_[0]);
+#       unless (defined $value) {
+#         $value = $user->{$_[0]}
+#           if $user;
+#       }
+       }
+       else {
+#       $value = $user && defined $user->{$_[0]} ? $user->{$_[0]} : '';
+       }
        
-       defined $value or $value = '';
-       CGI::escapeHTML($value);
-     },
-     $cust_class->checkout_actions(\%acts, \@cart, \@cart_prods, 
-                                  \%custom_state, $CGI::Q, $cfg),
-     ifMultPaymentTypes => @payment_types > 1,
-     ifUser => defined $user,
-     user => $user ? [ \&tag_hash, $user ] : '',
-     checkedPayment => [ \&tag_checkedPayment, $payment, \%types_by_name ],
-     ifPayments => [ \&tag_ifPayments, \@payment_types, \%types_by_name ],
-     affiliate_code => escape_html($affiliate_code),
-    );
-  for my $type (@pay_types) {
-    my $id = $type->{id};
-    my $name = $type->{name};
-    $acts{"if${name}Payments"} = exists $payment_types{$id};
-    $acts{"if${name}FirstPayment"} = $payment_types[0] == $id;
-    $acts{"checkedIfFirst$name"} = $payment_types[0] == $id ? "checked " : "";
-    $acts{"checkedPayment$name"} = $payment == $id ? 'checked="checked" ' : "";
-  }
-  $session{custom} = \%custom_state;
-
-  page('checkout.tmpl', \%acts);
-}
-
-# displays the data entered by the user so they can either confirm the
-# details or redisplay the checkout page
-sub checkout_confirm {
-  my %order;
-  my $error;
-
-  my @cart_prods;
-  unless (load_order_fields(0, $CGI::Q, \%order, \%session, \@cart_prods,
-                            \$error)) {
-    return checkout($error, 1);
-  }
-  ++$session{changed};
-  my @cart = @{$session{cart}};
-  # display the confirmation page
-  my %acts;
-  %acts =
-    (
-     order => sub { CGI::escapeHTML($order{$_[0]}) },
-     shop_cart_tags(\%acts, \@cart, \@cart_prods, \%session, $CGI::Q, $cfg,
-                   'confirm'),
-     basic_tags(\%acts),
-     old => 
-     sub { 
-       my $value = param($_[0]);
-       defined $value or $value = '';
-       CGI::escapeHTML($value);
-     },
-    );
-  page('checkoutconfirm.tmpl', \%acts);
-}
-
-sub tag_ifPayment {
-  my ($payment, $types_by_name, $args) = @_;
-
-  my $type = $args;
-  if ($type !~ /^\d+$/) {
-    return '' unless $types_by_name->{$type};
-    $type = $types_by_name->{$type};
-  }
-
-  return $payment == $type;
-}
-
-# the real work
-sub purchase {
-  $from && $from =~ /.\@./
-    or return checkout("Configuration error: shop from address not set", 1);
-  $toEmail && $toEmail =~ /.\@./
-    or return checkout("Configuration error: shop to_email address not set", 1);
-
-  # some basic validation, in case the user switched off javascript
-  my $cust_class = custom_class($cfg);
-  my @required = 
-    $cust_class->required_fields($CGI::Q, $session{custom}, $cfg);
-
-  my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
-
-  my @pay_types = payment_types($cfg);
-  my %pay_types = map { $_->{id} => $_ } @pay_types;
-  my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
-  #use Data::Dumper;
-  #print STDERR Dumper \%pay_types;
-  my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
-  if ($noencrypt) {
-    @payment_types = grep $_ ne PAYMENT_CC, @payment_types;
-    @payment_types or @payment_types = ( PAYMENT_CALLME );
-  }
-  else {
-    @payment_types or @payment_types = ( PAYMENT_CC );
-  }
-  @payment_types = sort { $a <=> $b } @payment_types;
-  my %payment_types = map { $_=> 1 } @payment_types;
-
-  my $paymentType = param('paymentType');
-  defined $paymentType or $paymentType = $payment_types[0];
-  $payment_types{$paymentType}
-    or return checkout("Invalid payment type");
-
-  push @required, @{$pay_types{$paymentType}{require}};
-
-  for my $field (@required) {
-    my $display = $cfg->entry('shop', "display_$field", $field);
-    defined(param($field)) && length(param($field))
-      or return checkout("Field $display is required", 1);
-  }
-  defined(param('email')) && param('email') =~ /.\@./
-    or return checkout("Please enter a valid email address", 1);
-  if ($paymentType == PAYMENT_CC) {
-    defined(param('cardNumber')) && param('cardNumber') =~ /^\d+$/
-      or return checkout("Please enter a credit card number", 1);
-  }
-
-  use BSE::TB::Orders;
-  use BSE::TB::OrderItems;
-
-  # map some form fields to order field names
-  my %field_map = 
-    (
-     name1 => 'delivFirstName',
-     name2 => 'delivLastName',
-     address => 'delivStreet',
-     city => 'delivSuburb',
-     postcode => 'delivPostCode',
-     state => 'delivState',
-     country => 'delivCountry',
-     email => 'emailAddress',
-     cardHolder => 'ccName',
-     cardType => 'ccType',
-    );
-  # paranoia, don't store these
-  my %nostore =
-    (
-     cardNumber => 1,
-     cardExpiry => 1,
-    );
-  my %order;
-  my @cart = @{$session{cart}};
-  @cart or return show_cart('You have no items in your shopping cart');
-
-  # so we can quickly check for columns
-  my @columns = BSE::TB::Order->columns;
-  my %columns; 
-  @columns{@columns} = @columns;
-
-  for my $field (param()) {
-    $order{$field_map{$field} || $field} = param($field)
-      unless $nostore{$field};
-  }
-
-  my $ccNumber = param('cardNumber');
-  defined $ccNumber or $ccNumber = '';
-  my $ccExpiry = param('cardExpiry');
-  defined $ccExpiry or $ccExpiry = '';
-  my $affiliate_code = $session{affiliate_code};
-  defined $affiliate_code && length $affiliate_code
-    or $affiliate_code = param('affiliate_code');
-  defined $affiliate_code or $affiliate_code = '';
-  $order{affiliate_code} = $affiliate_code;
-
-  use Digest::MD5 'md5_hex';
-  $ccNumber =~ tr/0-9//cd;
-  $order{ccNumberHash} = md5_hex($ccNumber);
-  $order{ccExpiryHash} = md5_hex($ccExpiry);
-
-  # work out totals
-  $order{total} = 0;
-  $order{gst} = 0;
-  $order{wholesale} = 0;
-  $order{shipping_cost} = 0;
-  my @products;
-  my $today = now_sqldate();
-  for my $item (@cart) {
-    my $product = Products->getByPkey($item->{productId});
-    # double check that it's still a valid product
-    if (!$product) {
-      return show_cart("Product $item->{productId} not found");
-    }
-    else {
-      (my $comp_release = $product->{release}) =~ s/ .*//;
-      (my $comp_expire = $product->{expire}) =~ s/ .*//;
-      $comp_release le $today
-       or return show_cart("'$product->{title}' has not been released yet");
-      $today le $comp_expire
-       or return show_cart("'$product->{title}' has expired");
-      $product->{listed} 
-       or return show_cart("'$product->{title}' not available");
-    }
-    push(@products, $product); # used in page rendering
-    @$item{qw/price wholesalePrice gst/} = 
-      @$product{qw/retailPrice wholesalePrice gst/};
-    $order{total} += $item->{price} * $item->{units};
-    $order{wholesale} += $item->{wholesalePrice} * $item->{units};
-    $order{gst} += $item->{gst} * $item->{units};
-  }
-
-  if (my ($msg, $id) = need_logon($cfg, \@cart, \@products, \%session, $CGI::Q)) {
-    refresh_logon($msg, $id);
-    return;
-  }
-
-  use BSE::Util::SQL qw(now_sqldatetime);
-  $order{orderDate} = now_sqldatetime;
-  $order{paymentType} = $paymentType;
-  ++$session{changed};
-
-  # blank anything else
-  for my $column (@columns) {
-    defined $order{$column} or $order{$column} = '';
-  }
-  # make sure the user can't set these behind our backs
-  $order{filled} = 0;
-  $order{paidFor} = 0;
-
-  my $user = get_siteuser(\%session, $cfg, $CGI::Q);
-  if ($user) {
-    $order{userId} = $user->{userId};
-    $order{siteuser_id} = $user->{id};
-  }
-  else {
-    $order{userId} = '';
-    $order{siteuser_id} = -1;
-  }
-
-  # this should be hard to guess
-  $order{randomId} = md5_hex(time().rand().{}.$$);
-
-  # check if a customizer has anything to do
-  # if it sets shipping cost it must also update the total
-  eval {
-    my %custom = %{$session{custom}};
-    $cust_class->order_save($CGI::Q, \%order, \@cart, \@products, 
-                           \%custom, $cfg);
-    $session{custom} = \%custom;
-  };
-  if ($@) {
-    return checkout($@, 1);
-  }
-
-  $order{total} += $cust_class->total_extras(\@cart, \@products, 
-                                            $session{custom}, $cfg, 'final');
-
-  my %subscribing_to;
-
-  # load up the database
-  my @data = @order{@columns};
-  shift @data; # lose the dummy id
-  my $order = BSE::TB::Orders->add(@data)
-    or die "Cannot add order";
-  my @items;
-  my @item_cols = BSE::TB::OrderItem->columns;
-  my @prod_xfer = qw/title summary subscription_id subscription_period/;
-  for my $row_num (0..$#cart) {
-    my $row = $cart[$row_num];
-    my $product = $products[$row_num];
-    $row->{orderId} = $order->{id};
-
-    # store product data too
-    @$row{@prod_xfer} = @{$product}{@prod_xfer};
-
-    # store the lapsed value, this prevents future changes causing
-    # variation of the expiry date
-    $row->{max_lapsed} = 0;
-    if ($product->{subscription_id} != -1) {
-      my $sub = $product->subscription;
-      $row->{max_lapsed} = $sub->{max_lapsed} if $sub;
-    }
-
-    my @data = @$row{@item_cols};
+       defined $value or $value = '';
+       CGI::escapeHTML($value);
+     },
+     $cust_class->checkout_actions(\%acts, \@cart, \@cart_prods, 
+#                                 \%custom_state, $CGI::Q, $cfg),
+     ifMultPaymentTypes => @payment_types > 1,
+     ifUser => defined $user,
+     user => $user ? [ \&tag_hash, $user ] : '',
+     checkedPayment => [ \&tag_checkedPayment, $payment, \%types_by_name ],
+     ifPayments => [ \&tag_ifPayments, \@payment_types, \%types_by_name ],
+     affiliate_code => escape_html($affiliate_code),
+    );
+  for my $type (@pay_types) {
+    my $id = $type->{id};
+    my $name = $type->{name};
+    $acts{"if${name}Payments"} = exists $payment_types{$id};
+    $acts{"if${name}FirstPayment"} = $payment_types[0] == $id;
+    $acts{"checkedIfFirst$name"} = $payment_types[0] == $id ? "checked " : "";
+    $acts{"checkedPayment$name"} = $payment == $id ? 'checked="checked" ' : "";
+  }
+  $session{custom} = \%custom_state;
+
+  page('checkout.tmpl', \%acts);
+}
+
+# displays the data entered by the user so they can either confirm the
+# details or redisplay the checkout page
+sub checkout_confirm {
+  my %order;
+  my $error;
+
+  my @cart_prods;
+  unless (load_order_fields(0, $CGI::Q, \%order, \%session, \@cart_prods,
+                            \$error)) {
+    return checkout($error, 1);
+  }
+  ++$session{changed};
+  my @cart = @{$session{cart}};
+  # display the confirmation page
+  my %acts;
+  %acts =
+    (
+     order => sub { CGI::escapeHTML($order{$_[0]}) },
+     shop_cart_tags(\%acts, \@cart, \@cart_prods, \%session, $CGI::Q, $cfg,
+#                  'confirm'),
+     basic_tags(\%acts),
+     old => 
+     sub { 
+       my $value = param($_[0]);
+       defined $value or $value = '';
+       CGI::escapeHTML($value);
+     },
+    );
+  page('checkoutconfirm.tmpl', \%acts);
+}
+
+sub tag_ifPayment {
+  my ($payment, $types_by_name, $args) = @_;
+
+  my $type = $args;
+  if ($type !~ /^\d+$/) {
+    return '' unless $types_by_name->{$type};
+    $type = $types_by_name->{$type};
+  }
+
+  return $payment == $type;
+}
+
+# the real work
+sub purchase {
+  $from && $from =~ /.\@./
+    or return checkout("Configuration error: shop from address not set", 1);
+  $toEmail && $toEmail =~ /.\@./
+    or return checkout("Configuration error: shop to_email address not set", 1);
+
+  # some basic validation, in case the user switched off javascript
+  my $cust_class = custom_class($cfg);
+  my @required = 
+    $cust_class->required_fields($CGI::Q, $session{custom}, $cfg);
+
+  my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
+
+  my @pay_types = payment_types($cfg);
+  my %pay_types = map { $_->{id} => $_ } @pay_types;
+  my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
+  #use Data::Dumper;
+  #print STDERR Dumper \%pay_types;
+  my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
+  if ($noencrypt) {
+    @payment_types = grep $_ ne PAYMENT_CC, @payment_types;
+    @payment_types or @payment_types = ( PAYMENT_CALLME );
+  }
+  else {
+    @payment_types or @payment_types = ( PAYMENT_CC );
+  }
+  @payment_types = sort { $a <=> $b } @payment_types;
+  my %payment_types = map { $_=> 1 } @payment_types;
+
+  my $paymentType = param('paymentType');
+  defined $paymentType or $paymentType = $payment_types[0];
+  $payment_types{$paymentType}
+    or return checkout("Invalid payment type");
+
+  push @required, @{$pay_types{$paymentType}{require}};
+
+  for my $field (@required) {
+    my $display = $cfg->entry('shop', "display_$field", $field);
+    defined(param($field)) && length(param($field))
+      or return checkout("Field $display is required", 1);
+  }
+  defined(param('email')) && param('email') =~ /.\@./
+    or return checkout("Please enter a valid email address", 1);
+  if ($paymentType == PAYMENT_CC) {
+    defined(param('cardNumber')) && param('cardNumber') =~ /^\d+$/
+      or return checkout("Please enter a credit card number", 1);
+  }
+
+  use BSE::TB::Orders;
+  use BSE::TB::OrderItems;
+
+  # map some form fields to order field names
+  my %field_map = 
+    (
+     name1 => 'delivFirstName',
+     name2 => 'delivLastName',
+     address => 'delivStreet',
+     city => 'delivSuburb',
+     postcode => 'delivPostCode',
+     state => 'delivState',
+     country => 'delivCountry',
+     email => 'emailAddress',
+     cardHolder => 'ccName',
+     cardType => 'ccType',
+    );
+  # paranoia, don't store these
+  my %nostore =
+    (
+     cardNumber => 1,
+     cardExpiry => 1,
+    );
+  my %order;
+  my @cart = @{$session{cart}};
+  @cart or return show_cart('You have no items in your shopping cart');
+
+  # so we can quickly check for columns
+  my @columns = BSE::TB::Order->columns;
+  my %columns; 
+  @columns{@columns} = @columns;
+
+  for my $field (param()) {
+    $order{$field_map{$field} || $field} = param($field)
+      unless $nostore{$field};
+  }
+
+  my $ccNumber = param('cardNumber');
+  defined $ccNumber or $ccNumber = '';
+  my $ccExpiry = param('cardExpiry');
+  defined $ccExpiry or $ccExpiry = '';
+  my $affiliate_code = $session{affiliate_code};
+  defined $affiliate_code && length $affiliate_code
+    or $affiliate_code = param('affiliate_code');
+  defined $affiliate_code or $affiliate_code = '';
+  $order{affiliate_code} = $affiliate_code;
+
+  use Digest::MD5 'md5_hex';
+  $ccNumber =~ tr/0-9//cd;
+  $order{ccNumberHash} = md5_hex($ccNumber);
+  $order{ccExpiryHash} = md5_hex($ccExpiry);
+
+  # work out totals
+  $order{total} = 0;
+  $order{gst} = 0;
+  $order{wholesale} = 0;
+  $order{shipping_cost} = 0;
+  my @products;
+  my $today = now_sqldate();
+  for my $item (@cart) {
+    my $product = Products->getByPkey($item->{productId});
+    # double check that it's still a valid product
+    if (!$product) {
+      return show_cart("Product $item->{productId} not found");
+    }
+    else {
+      (my $comp_release = $product->{release}) =~ s/ .*//;
+      (my $comp_expire = $product->{expire}) =~ s/ .*//;
+      $comp_release le $today
+#      or return show_cart("'$product->{title}' has not been released yet");
+      $today le $comp_expire
+#      or return show_cart("'$product->{title}' has expired");
+      $product->{listed} 
+#      or return show_cart("'$product->{title}' not available");
+    }
+    push(@products, $product); # used in page rendering
+    @$item{qw/price wholesalePrice gst/} = 
+      @$product{qw/retailPrice wholesalePrice gst/};
+    $order{total} += $item->{price} * $item->{units};
+    $order{wholesale} += $item->{wholesalePrice} * $item->{units};
+    $order{gst} += $item->{gst} * $item->{units};
+  }
+
+  if (my ($msg, $id) = need_logon($cfg, \@cart, \@products, \%session, $CGI::Q)) {
+    refresh_logon($msg, $id);
+    return;
+  }
+
+  use BSE::Util::SQL qw(now_sqldatetime);
+  $order{orderDate} = now_sqldatetime;
+  $order{paymentType} = $paymentType;
+  ++$session{changed};
+
+  # blank anything else
+  for my $column (@columns) {
+    defined $order{$column} or $order{$column} = '';
+  }
+  # make sure the user can't set these behind our backs
+  $order{filled} = 0;
+  $order{paidFor} = 0;
+
+  my $user = get_siteuser(\%session, $cfg, $CGI::Q);
+  if ($user) {
+    $order{userId} = $user->{userId};
+    $order{siteuser_id} = $user->{id};
+  }
+  else {
+    $order{userId} = '';
+    $order{siteuser_id} = -1;
+  }
+
+  # this should be hard to guess
+  $order{randomId} = md5_hex(time().rand().{}.$$);
+
+  # check if a customizer has anything to do
+  # if it sets shipping cost it must also update the total
+  eval {
+    my %custom = %{$session{custom}};
+    $cust_class->order_save($CGI::Q, \%order, \@cart, \@products, 
+#                          \%custom, $cfg);
+    $session{custom} = \%custom;
+  };
+  if ($@) {
+    return checkout($@, 1);
+  }
+
+  $order{total} += $cust_class->total_extras(\@cart, \@products, 
+#                                           $session{custom}, $cfg, 'final');
+
+  my %subscribing_to;
+
+  # load up the database
+  my @data = @order{@columns};
+  shift @data; # lose the dummy id
+  my $order = BSE::TB::Orders->add(@data)
+    or die "Cannot add order";
+  my @items;
+  my @item_cols = BSE::TB::OrderItem->columns;
+  my @prod_xfer = qw/title summary subscription_id subscription_period/;
+  for my $row_num (0..$#cart) {
+    my $row = $cart[$row_num];
+    my $product = $products[$row_num];
+    $row->{orderId} = $order->{id};
+
+    # store product data too
+    @$row{@prod_xfer} = @{$product}{@prod_xfer};
+
+    # store the lapsed value, this prevents future changes causing
+    # variation of the expiry date
+    $row->{max_lapsed} = 0;
+    if ($product->{subscription_id} != -1) {
+      my $sub = $product->subscription;
+      $row->{max_lapsed} = $sub->{max_lapsed} if $sub;
+    }
+
+    my @data = @$row{@item_cols};
     
-    shift @data;
-    push(@items, BSE::TB::OrderItems->add(@data));
-
-    my $sub = $product->subscription;
-    if ($sub) {
-      $subscribing_to{$sub->{text_id}} = $sub;
-    }
-  }
-
-  if ($user) {
-    $user->recalculate_subscriptions($cfg);
-  }
-
-  my $item_index = -1;
-  my @options;
-  my $option_index;
-  my %acts;
-  %acts =
-    (
-     $cust_class->purchase_actions(\%acts, \@items, \@products, 
-                                  $session{custom}, $cfg),
-     BSE::Util::Tags->static(\%acts, $cfg),
-     iterate_items_reset => sub { $item_index = -1; },
-     iterate_items => 
-     sub { 
-       if (++$item_index < @items) {
-        $option_index = -1;
-        @options = cart_item_opts($items[$item_index], 
-                                  $products[$item_index]);
-        return 1;
-       }
-       return 0;
-     },
-     item=> sub { CGI::escapeHTML($items[$item_index]{$_[0]}); },
-     product => 
-     sub { 
-       my $value = $products[$item_index]{$_[0]};
-       defined $value or $value = '';
-
-       escape_html($value);
-     },
-     extended =>
-     sub { 
-       my $what = $_[0] || 'retailPrice';
-       $items[$item_index]{units} * $items[$item_index]{$what};
-     },
-     order => sub { CGI::escapeHTML($order->{$_[0]}) },
-     money =>
-     sub {
-       my ($func, $args) = split ' ', $_[0], 2;
-       $acts{$func} || return "<: money $_[0] :>";
-       return sprintf("%.02f", $acts{$func}->($args)/100);
-     },
-     _format =>
-     sub {
-       my ($value, $fmt) = @_;
-       if ($fmt =~ /^m(\d+)/) {
-        return sprintf("%$1s", sprintf("%.2f", $value/100));
-       }
-       elsif ($fmt =~ /%/) {
-        return sprintf($fmt, $value);
-       }
-     },
-     iterate_options_reset => sub { $option_index = -1 },
-     iterate_options => sub { ++$option_index < @options },
-     option => sub { CGI::escapeHTML($options[$option_index]{$_[0]}) },
-     ifOptions => sub { @options },
-     options => sub { nice_options(@options) },
-     ifPayment => [ \&tag_ifPayment, $order->{paymentType}, \%types_by_name ],
-     #ifSubscribingTo => [ \&tag_ifSubscribingTo, \%subscribing_to ],
-    );
-  for my $type (@pay_types) {
-    my $id = $type->{id};
-    my $name = $type->{name};
-    $acts{"if${name}Payment"} = $order->{paymentType} == $id;
-  }
-  send_order($order, \@items, \@products, $noencrypt, \%subscribing_to);
-  $session{cart} = []; # empty the cart
-  page('checkoutfinal.tmpl', \%acts);
-}
-
-sub tag_ifSubscribingTo {
-  my ($subscribing_to, $args) = @_;
-
-  exists $subscribing_to->{$args};
-}
-
-sub tag_with_wrap {
-  my ($args, $text) = @_;
-
-  my $margin = $args =~ /^\d+$/ && $args > 30 ? $args : 70;
-
-  require Text::Wrap;
-  # do it twice to prevent a warning
-  $Text::Wrap::columns = $margin;
-  $Text::Wrap::columns = $margin;
-
-  return Text::Wrap::fill('', '', split /\n/, $text);
-}
-
-# sends the email order confirmation and the PGP encrypted
-# email to the site owner
-sub send_order {
-  my ($order, $items, $products, $noencrypt, $subscribing_to) = @_;
-
-  my %extras = $cfg->entriesCS('extra tags');
-  for my $key (keys %extras) {
-    # follow any links
-    my $data = $cfg->entryVar('extra tags', $key);
-    $extras{$key} = sub { $data };
-  }
-
-  my $item_index = -1;
-  my @options;
-  my $option_index;
-  my %acts;
-  %acts =
-    (
-     %extras,
-     custom_class($cfg)
-     ->order_mail_actions(\%acts, $order, $items, $products, 
-                         $session{custom}, $cfg),
-     BSE::Util::Tags->static(\%acts, $cfg),
-     iterate_items_reset => sub { $item_index = -1; },
-     iterate_items => 
-     sub { 
-       if (++$item_index < @$items) {
-        $option_index = -1;
-        @options = cart_item_opts($items->[$item_index], 
-                                  $products->[$item_index]);
-        return 1;
-       }
-       return 0;
-     },
-     item=> sub { $items->[$item_index]{$_[0]}; },
-     product => 
-     sub { 
-       my $value = $products->[$item_index]{$_[0]};
-       defined($value) or $value = '';
-       $value;
-     },
-     order => sub { $order->{$_[0]} },
-     extended => 
-     sub {
-       $items->[$item_index]{units} * $items->[$item_index]{$_[0]};
-     },
-     _format =>
-     sub {
-       my ($value, $fmt) = @_;
-       if ($fmt =~ /^m(\d+)/) {
-        return sprintf("%$1s", sprintf("%.2f", $value/100));
-       }
-       elsif ($fmt =~ /%/) {
-        return sprintf($fmt, $value);
-       }
-       elsif ($fmt =~ /^\d+$/) {
-        return substr($value . (" " x $fmt), 0, $fmt);
-       }
-       else {
-        return $value;
-       }
-     },
-     iterate_options_reset => sub { $option_index = -1 },
-     iterate_options => sub { ++$option_index < @options },
-     option => sub { CGI::escapeHTML($options[$option_index]{$_[0]}) },
-     ifOptions => sub { @options },
-     options => sub { nice_options(@options) },
-     with_wrap => \&tag_with_wrap,
-     ifSubscribingTo => [ \&tag_ifSubscribingTo, $subscribing_to ],
-    );
-
-  my $mailer = BSE::Mail->new(cfg=>$cfg);
-  # ok, send some email
-  my $confirm = BSE::Template->get_page('mailconfirm', $cfg, \%acts);
-  my $email_order = $cfg->entryBool('shop', 'email_order', $SHOP_EMAIL_ORDER);
-  if ($email_order) {
-    unless ($noencrypt) {
-      $acts{cardNumber} = sub { param('cardNumber') };
-      $acts{cardExpiry} = sub { param('cardExpiry') };
-    }
-    my $ordertext = BSE::Template->get_page('mailorder', $cfg, \%acts);
+    shift @data;
+    push(@items, BSE::TB::OrderItems->add(@data));
+
+    my $sub = $product->subscription;
+    if ($sub) {
+      $subscribing_to{$sub->{text_id}} = $sub;
+    }
+  }
+
+  if ($user) {
+    $user->recalculate_subscriptions($cfg);
+  }
+
+  my $item_index = -1;
+  my @options;
+  my $option_index;
+  my %acts;
+  %acts =
+    (
+     $cust_class->purchase_actions(\%acts, \@items, \@products, 
+#                                 $session{custom}, $cfg),
+     BSE::Util::Tags->static(\%acts, $cfg),
+     iterate_items_reset => sub { $item_index = -1; },
+     iterate_items => 
+     sub { 
+       if (++$item_index < @items) {
+#       $option_index = -1;
+#       @options = cart_item_opts($items[$item_index], 
+#                                 $products[$item_index]);
+#       return 1;
+       }
+       return 0;
+     },
+     item=> sub { CGI::escapeHTML($items[$item_index]{$_[0]}); },
+     product => 
+     sub { 
+       my $value = $products[$item_index]{$_[0]};
+       defined $value or $value = '';
+
+       escape_html($value);
+     },
+     extended =>
+     sub { 
+       my $what = $_[0] || 'retailPrice';
+       $items[$item_index]{units} * $items[$item_index]{$what};
+     },
+     order => sub { CGI::escapeHTML($order->{$_[0]}) },
+     money =>
+     sub {
+       my ($func, $args) = split ' ', $_[0], 2;
+       $acts{$func} || return "<: money $_[0] :>";
+       return sprintf("%.02f", $acts{$func}->($args)/100);
+     },
+     _format =>
+     sub {
+       my ($value, $fmt) = @_;
+       if ($fmt =~ /^m(\d+)/) {
+#       return sprintf("%$1s", sprintf("%.2f", $value/100));
+       }
+       elsif ($fmt =~ /%/) {
+#       return sprintf($fmt, $value);
+       }
+     },
+     iterate_options_reset => sub { $option_index = -1 },
+     iterate_options => sub { ++$option_index < @options },
+     option => sub { CGI::escapeHTML($options[$option_index]{$_[0]}) },
+     ifOptions => sub { @options },
+     options => sub { nice_options(@options) },
+     ifPayment => [ \&tag_ifPayment, $order->{paymentType}, \%types_by_name ],
+     #ifSubscribingTo => [ \&tag_ifSubscribingTo, \%subscribing_to ],
+    );
+  for my $type (@pay_types) {
+    my $id = $type->{id};
+    my $name = $type->{name};
+    $acts{"if${name}Payment"} = $order->{paymentType} == $id;
+  }
+  send_order($order, \@items, \@products, $noencrypt, \%subscribing_to);
+  $session{cart} = []; # empty the cart
+  page('checkoutfinal.tmpl', \%acts);
+}
+
+sub tag_ifSubscribingTo {
+  my ($subscribing_to, $args) = @_;
+
+  exists $subscribing_to->{$args};
+}
+
+sub tag_with_wrap {
+  my ($args, $text) = @_;
+
+  my $margin = $args =~ /^\d+$/ && $args > 30 ? $args : 70;
+
+  require Text::Wrap;
+  # do it twice to prevent a warning
+  $Text::Wrap::columns = $margin;
+  $Text::Wrap::columns = $margin;
+
+  return Text::Wrap::fill('', '', split /\n/, $text);
+}
+
+# sends the email order confirmation and the PGP encrypted
+# email to the site owner
+sub send_order {
+  my ($order, $items, $products, $noencrypt, $subscribing_to) = @_;
+
+  my %extras = $cfg->entriesCS('extra tags');
+  for my $key (keys %extras) {
+    # follow any links
+    my $data = $cfg->entryVar('extra tags', $key);
+    $extras{$key} = sub { $data };
+  }
+
+  my $item_index = -1;
+  my @options;
+  my $option_index;
+  my %acts;
+  %acts =
+    (
+     %extras,
+     custom_class($cfg)
+     ->order_mail_actions(\%acts, $order, $items, $products, 
+#                        $session{custom}, $cfg),
+     BSE::Util::Tags->static(\%acts, $cfg),
+     iterate_items_reset => sub { $item_index = -1; },
+     iterate_items => 
+     sub { 
+       if (++$item_index < @$items) {
+#       $option_index = -1;
+#       @options = cart_item_opts($items->[$item_index], 
+#                                 $products->[$item_index]);
+#       return 1;
+       }
+       return 0;
+     },
+     item=> sub { $items->[$item_index]{$_[0]}; },
+     product => 
+     sub { 
+       my $value = $products->[$item_index]{$_[0]};
+       defined($value) or $value = '';
+       $value;
+     },
+     order => sub { $order->{$_[0]} },
+     extended => 
+     sub {
+       $items->[$item_index]{units} * $items->[$item_index]{$_[0]};
+     },
+     _format =>
+     sub {
+       my ($value, $fmt) = @_;
+       if ($fmt =~ /^m(\d+)/) {
+#       return sprintf("%$1s", sprintf("%.2f", $value/100));
+       }
+       elsif ($fmt =~ /%/) {
+#       return sprintf($fmt, $value);
+       }
+       elsif ($fmt =~ /^\d+$/) {
+#       return substr($value . (" " x $fmt), 0, $fmt);
+       }
+       else {
+#       return $value;
+       }
+     },
+     iterate_options_reset => sub { $option_index = -1 },
+     iterate_options => sub { ++$option_index < @options },
+     option => sub { CGI::escapeHTML($options[$option_index]{$_[0]}) },
+     ifOptions => sub { @options },
+     options => sub { nice_options(@options) },
+     with_wrap => \&tag_with_wrap,
+     ifSubscribingTo => [ \&tag_ifSubscribingTo, $subscribing_to ],
+    );
+
+  my $mailer = BSE::Mail->new(cfg=>$cfg);
+  # ok, send some email
+  my $confirm = BSE::Template->get_page('mailconfirm', $cfg, \%acts);
+  my $email_order = $cfg->entryBool('shop', 'email_order', $SHOP_EMAIL_ORDER);
+  if ($email_order) {
+    unless ($noencrypt) {
+      $acts{cardNumber} = sub { param('cardNumber') };
+      $acts{cardExpiry} = sub { param('cardExpiry') };
+    }
+    my $ordertext = BSE::Template->get_page('mailorder', $cfg, \%acts);
     
-    my $send_text;
-    if ($noencrypt) {
-      $send_text = $ordertext;
-    }
-    else {
-      eval "use $crypto_class";
-      !$@ or die $@;
-      my $encrypter = $crypto_class->new;
+    my $send_text;
+    if ($noencrypt) {
+      $send_text = $ordertext;
+    }
+    else {
+      eval "use $crypto_class";
+      !$@ or die $@;
+      my $encrypter = $crypto_class->new;
       
-      my $debug = $cfg->entryBool('debug', 'mail_encryption', 0);
-      my $sign = $cfg->entryBool('basic', 'sign', 1);
+      my $debug = $cfg->entryBool('debug', 'mail_encryption', 0);
+      my $sign = $cfg->entryBool('basic', 'sign', 1);
       
-      # encrypt and sign
-      my %opts = 
-       (
-        sign=> $sign,
-        passphrase=> $passphrase,
-        stripwarn=>1,
-        debug=>$debug,
-       );
+      # encrypt and sign
+      my %opts = 
+#      (
+#       sign=> $sign,
+#       passphrase=> $passphrase,
+#       stripwarn=>1,
+#       debug=>$debug,
+#      );
       
-      $opts{secretkeyid} = $signing_id if $signing_id;
-      $opts{pgp} = $pgp if $pgp;
-      $opts{gpg} = $gpg if $gpg;
-      $opts{pgpe} = $pgpe if $pgpe;
-      my $recip = "$toName $toEmail";
-
-      $send_text = $encrypter->encrypt($recip, $ordertext, %opts )
-       or die "Cannot encrypt ", $encrypter->error;
-    }
-    $mailer->send(to=>$toEmail, from=>$from, subject=>'New Order '.$order->{id},
-                 body=>$send_text)
-      or print STDERR "Error sending order to admin: ",$mailer->errstr,"\n";
-  }
-  $mailer->send(to=>$order->{emailAddress}, from=>$from,
-               subject=>$subject . " " . localtime,
-               body=>$confirm)
-    or print STDERR "Error sending order to customer: ",$mailer->errstr,"\n";
-}
-
-sub page {
-  my ($template, $acts) = @_;
-
-  BSE::Template->show_page($template, $cfg, $acts);
-}
-
-# convert an epoch time to sql format
-sub epoch_to_sql {
-  use POSIX 'strftime';
-  my ($time) = @_;
-
-  return strftime('%Y-%m-%d', localtime $time);
-}
-
-sub refresh_logon {
-  my ($msg, $msgid, $r) = @_;
-  my $url = $securlbase."/cgi-bin/user.pl";
-
-  $r ||= $securlbase."/cgi-bin/shop.pl?checkout=1";
+      $opts{secretkeyid} = $signing_id if $signing_id;
+      $opts{pgp} = $pgp if $pgp;
+      $opts{gpg} = $gpg if $gpg;
+      $opts{pgpe} = $pgpe if $pgpe;
+      my $recip = "$toName $toEmail";
+
+      $send_text = $encrypter->encrypt($recip, $ordertext, %opts )
+#      or die "Cannot encrypt ", $encrypter->error;
+    }
+    $mailer->send(to=>$toEmail, from=>$from, subject=>'New Order '.$order->{id},
+#                body=>$send_text)
+      or print STDERR "Error sending order to admin: ",$mailer->errstr,"\n";
+  }
+  $mailer->send(to=>$order->{emailAddress}, from=>$from,
+#              subject=>$subject . " " . localtime,
+#              body=>$confirm)
+    or print STDERR "Error sending order to customer: ",$mailer->errstr,"\n";
+}
+
+sub page {
+  my ($template, $acts) = @_;
+
+  BSE::Template->show_page($template, $cfg, $acts);
+}
+
+# convert an epoch time to sql format
+sub epoch_to_sql {
+  use POSIX 'strftime';
+  my ($time) = @_;
+
+  return strftime('%Y-%m-%d', localtime $time);
+}
+
+sub refresh_logon {
+  my ($msg, $msgid, $r) = @_;
+  my $url = $securlbase."/cgi-bin/user.pl";
+
+  $r ||= $securlbase."/cgi-bin/shop.pl?checkout=1";
   
-  my %parms;
-  $parms{r} = $r;
-  $parms{message} = $msg if $msg;
-  $parms{mid} = $msgid if $msgid;
-  $url .= "?" . join("&", map "$_=".CGI::escape($parms{$_}), keys %parms);
+  my %parms;
+  $parms{r} = $r;
+  $parms{message} = $msg if $msg;
+  $parms{mid} = $msgid if $msgid;
+  $url .= "?" . join("&", map "$_=".CGI::escape($parms{$_}), keys %parms);
   
-  refresh_to($url);
-}
+  refresh_to($url);
+}
 
 __END__
 
index 143a87efd7b8001b04fe809ab4facb1ca5fee17a..a0780a2204ade13e9e58d86f40eb62501058b746 100644 (file)
@@ -10,6 +10,109 @@ Maybe I'll add some other bits here.
 
 =head1 CHANGES
 
+=head2 0.15_05
+
+WARNING: this release makes major changes to the way the shop works.
+Make sure you test B<BEFORE> you deploy.
+
+This release may introduce incompatibilities with older BSE::Custom
+modules, if you come across any of these, please let me know.
+
+To deploy this with a custom template set the following templates will
+need to be updated:
+
+  - checkoutnew_base.tmpl - new checkout page
+  - checkoutpay_base.tmpl - new payment page
+  - mailconfirm.tmpl - handle CC processing
+  - mailorder.tmpl - handle CC processing
+  - checkoutfinal_base.tmpl - display credit card receipt number
+
+This is primarily intended as a test release, currently it has three
+known problems:
+
+=over
+
+=item *
+
+the checkout page won't load the saved order values if you go back to
+it after having gone through it without finishing the order.
+
+=item *
+
+each failed online credit card transaction results in a new order in
+the database (marked as failed)
+
+=item *
+
+some admin side templates still need updating
+
+=back
+
+The changes:
+
+=over
+
+=item *
+
+major changes to the structure of the shop, There is no longer a
+purchase action, this has been split into an order action, which saves
+user information, and a payment action, which attempts to process a
+payment.
+
+The initial checkout page now uses the checkoutnew template rather
+than the checkout template.  The payment page uses the checkoutpay
+template.
+
+=item *
+
+the final order display is now a separate request from the
+purchase/payment action, you can now make changes to the template and
+test them without having to create a new order each time.  The final
+order display page will only display the last successful order for 5
+minutes.
+
+=item *
+
+the shop can now process credit card transactions online through the
+Inpho credit card gateway.  There is also a also a "test" gateway
+module that allows for offline testing.  See [shop].cardprocessor in
+config.pod
+
+=item *
+
+the shop (and other scripts that use the general dispatcher, like
+affiliate.pl and fmail.pl) should now work with image buttons
+submitting the form.
+
+=item *
+
+conditions to check for payments types on the shop pages that accepted
+a payment type name (like <:ifPayments Name:>) would always return
+value for the CC type, even when it was enabled.
+
+=item *
+
+fields that were marked as required previously are now actually
+required, this most likely a problem for the cardType field.  I may
+end up marking this as not required.
+
+=item *
+
+a new credit card field value is accepted and passed to the online
+credit card processor, the cardVerify field.
+
+=item *
+
+fields on the checkout page now must use the name defined in the order
+record.  The older names are no longer usable, except in the required
+method of BSE::Custom.
+
+=item *
+
+the error_img tag is now available on the checkout and payment pages
+
+=back
+
 =head2 0.15_04
 
 =over
index 3555708336068a3d6142b18f8c0c24100f58696d..0b5f87b49cb646a22a0191726fc0a6bec6453524 100644 (file)
@@ -595,8 +595,20 @@ card information included.  Default: $SHOP_EMAIL_ORDER.
 Used to translate the stored order field name into a presentation name
 suitable for error messages.
 
+=item cardprocessor
+
+The name of a class to load to process credit card transactions online.
+
+Currently this can be either DevHelp::Payments::Test or
+DevHelp::Payments::Inpho.
+
 =back
 
+=head2 [Shop Order Validation]
+
+This section can contain extra order validation information, including
+specifying required fields, display names and extra validation rules.
+
 =head2 [fields]
 
 =over
@@ -1159,6 +1171,45 @@ C<[level >I<level>C<]> sections.
 These defaults are used when creating an article where no value is
 supplied, they can also be accessed via the <:default I<name>:> tag.
 
+=head2 [inpho]
+
+This is used to configure the DevHelp::Payments::Inpho module.
+
+=over
+
+=item test
+
+If this is set then the test parameters are used instead of the
+product values.
+
+=item url
+
+The URL to process requests through.  
+
+Default: https://extranet.inpho.com.au/cc_ssl/process
+
+=item user
+
+Inpho supplied user name.
+
+=item password
+
+Inpho supplied password.
+
+=item test_url
+
+The URL to process test requests through.
+
+=item test_user
+
+The user to supply to test requests.
+
+=item test_password
+
+The password to supply to test requests.
+
+=back
+
 =head1 AUTHOR
 
 Tony Cook <tony@develop-help.com>
index cecf8848515a1f3170cb1fbb904bb29e844228f6..1153de5087e4582b327465a2a1dbc2a65b5280e9 100644 (file)
@@ -29,10 +29,10 @@ function BSE_validateForm {
   var typeEl = MM_findObj('paymentType');
   var type = typeEl.value;
   if (type == 0) {
-    MM_validateForm('name1','','R','name2','','R','address','','R','city','','R','postcode','','R','state','','R','country','','R','email','','RisEmail','cardHolder','','R','cardNumber','','R','cardExpiry','','R');
+    MM_validateForm('delivFirstName','','R','delivLastName','','R','delivStreet','','R','delivSuburb','','R','delivPostCode','','R','delivState','','R','delivCountry','','R','email','','RisEmail','cardHolder','','R','cardNumber','','R','cardExpiry','','R');
   }
   else {
-    MM_validateForm('name1','','R','name2','','R','address','','R','city','','R','postcode','','R','state','','R','country','','R','email','','RisEmail');
+    MM_validateForm('delivFirstName','','R','delivLastName','','R','delivStreet','','R','delivSuburb','','R','delivPostCode','','R','delivState','','R','delivCountry','','R','email','','RisEmail');
   }
 }
 
@@ -145,7 +145,6 @@ function BSE_validateForm {
     <tr> 
       <td colspan=2> 
         <p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:message:></font></p>
-        <br>
       </td>
     </tr>
     <:or Message:><:eif Message:> 
@@ -153,67 +152,67 @@ function BSE_validateForm {
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> First 
         Name:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="name1" size=34 value="<:old name1:>">
+        <input type="Text" name="delivFirstName" size=34 value="<:old delivFirstName:>"><:error_img delivFirstName:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Last Name:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="name2" size=34 value="<:old name2:>">
+        <input type="Text" name="delivLastName" size=34 value="<:old delivLastName:>"><:error_img delivLastName:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Address:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="address" size=34 value="<:old address:>">
+        <input type="Text" name="delivStreet" size=34 value="<:old delivStreet:>" /><:error_img delivStreet:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> City:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="city" size=34 value="<:old city:>">
+        <input type="Text" name="delivSuburb" size=34 value="<:old delivSuburb:>" /><:error_img delivSuburb:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Postcode:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="postcode" size=10 value="<:old postcode:>">
+        <input type="Text" name="delivPostCode" size=10 value="<:old delivPostCode:>" /><:error_img delivPostCode:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> State:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="state" size=10 value="<:old state:>">
+        <input type="Text" name="delivState" size=10 value="<:old delivState:>" /><:error_img delivState:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Country:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="country" size=20 value="<:old country:>">
+        <input type="Text" name="delivCountry" size=20 value="<:old delivCountry:>" /><:error_img delivCountry:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Telephone:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="telephone" size=20 value="<:old telephone:>">
+        <input type="Text" name="telephone" size=20 value="<:old telephone:>" /><:error_img telephone:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Mobile:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="delivMobile" size=20 value="<:old delivMobile:>">
+        <input type="Text" name="delivMobile" size=20 value="<:old delivMobile:>" /><:error_img delivMobile:>
         </font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Facsimile:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="facsimile" size=20 value="<:old facsimile:>">
+        <input type="Text" name="facsimile" size=20 value="<:old facsimile:>" /><:error_img facsimile:>
         </font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> E-mail:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="email" size=34 value="<:old email:>">
+        <input type="Text" name="email" size=34 value="<:old email:>"><:error_img email:>
         *</font></td>
     </tr>
     <tr> 
@@ -236,67 +235,67 @@ function BSE_validateForm {
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> First 
         Name:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billFirstName" size=34 value="<:old billFirstName:>">
+        <input type="Text" name="billFirstName" size=34 value="<:old billFirstName:>"><:error_img billFirstName:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Last Name:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billLastName" size=34 value="<:old billLastName:>">
+        <input type="Text" name="billLastName" size=34 value="<:old billLastName:>"><:error_img billLastName:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Address:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billStreet" size=34 value="<:old billStreet:>">
+        <input type="Text" name="billStreet" size=34 value="<:old billStreet:>"><:error_img billStreet:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> City:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billSuburb" size=34 value="<:old billSuburb:>">
+        <input type="Text" name="billSuburb" size=34 value="<:old billSuburb:>"><:error_img billSuburb:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Postcode:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billPostCode" size=10 value="<:old billPostCode:>">
+        <input type="Text" name="billPostCode" size=10 value="<:old billPostCode:>"><:error_img billPostCode:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> State:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billState" size=10 value="<:old billState:>">
+        <input type="Text" name="billState" size=10 value="<:old billState:>"><:error_img billState:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Country:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billCountry" size=20 value="<:old billCountry:>">
+        <input type="Text" name="billCountry" size=20 value="<:old billCountry:>"><:error_img billCountry:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Email:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billEmail" size=20 value="<:old billEmail:>">
+        <input type="Text" name="billEmail" size=20 value="<:old billEmail:>"><:error_img billEmail:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Telephone:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billTelephone" size=20 value="<:old billTelephone:>">
+        <input type="Text" name="billTelephone" size=20 value="<:old billTelephone:>"><:error_img billTelephone:>
         *</font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Mobile:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billMobile" size=20 value="<:old billMobile:>">
+        <input type="Text" name="billMobile" size=20 value="<:old billMobile:>"><:error_img billMobile:>
         </font></td>
     </tr>
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Facsimile:</font></td>
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
-        <input type="Text" name="billFacsimile" size=20 value="<:old billFacsimile:>">
+        <input type="Text" name="billFacsimile" size=20 value="<:old billFacsimile:>"><:error_img billFacsimile:>
         *</font></td>
     </tr>
   </table>
@@ -374,7 +373,7 @@ function BSE_validateForm {
   </table>
   <p>
     <input type="submit" value="Update" name="checkupdate" />
-    <input type="submit" value="Purchase Now" name="purchase">
+    <input type="submit" value="Purchase Now" name="a_order">
     <input type="reset" value="Reset Form" name="reset">
   </p>
   </form>
index 6fec7741a58ecfd2f82b05367829fed2b198db2e..af5737b24fc1f84bd45b83d4a042fc395b304b2d 100644 (file)
@@ -28,7 +28,7 @@
 <p> The <:siteName:> store is run on a secure encrypted server, your details are 
   safe with us.<br>
 </p>
-<:if Payment CC:><:or Payment:><:eif Payment:>
+<:if Payment CC:><p>Paid by credit card.</p><:if Order ccOnline:><p>Credit Card Receipt Number: <:order ccReceipt:></p><:or Order:><:eif Order:><:or Payment:><:eif Payment:>
 <:if Payment Cheque:>
 <p>Please send your cheque to:</p>
 <ul><:cfg shop address1 |h:><br>
diff --git a/site/templates/checkoutnew_base.tmpl b/site/templates/checkoutnew_base.tmpl
new file mode 100644 (file)
index 0000000..7df047b
--- /dev/null
@@ -0,0 +1,312 @@
+<:wrap base.tmpl:> 
+<script language="JavaScript">
+<!--
+function MM_findObj(n, d) { //v4.01
+  var p,i,x;  if(!d) d=document; if((p=n.indexOf("?"))>0&&parent.frames.length) {
+    d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
+  if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
+  for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=MM_findObj(n,d.layers[i].document);
+  if(!x && d.getElementById) x=d.getElementById(n); return x;
+}
+
+function MM_validateForm() { //v4.0
+  var i,p,q,nm,test,num,min,max,errors='',args=MM_validateForm.arguments;
+  for (i=0; i<(args.length-2); i+=3) { test=args[i+2]; val=MM_findObj(args[i]);
+    if (val) { nm=val.name; if ((val=val.value)!="") {
+      if (test.indexOf('isEmail')!=-1) { p=val.indexOf('@');
+        if (p<1 || p==(val.length-1)) errors+='- '+nm+' must contain an e-mail address.\n';
+      } else if (test!='R') {
+        if (isNaN(val)) errors+='- '+nm+' must contain a number.\n';
+        if (test.indexOf('inRange') != -1) { p=test.indexOf(':');
+          min=test.substring(8,p); max=test.substring(p+1);
+          if (val<min || max<val) errors+='- '+nm+' must contain a number between '+min+' and '+max+'.\n';
+    } } } else if (test.charAt(0) == 'R') errors += '- '+nm+' is required.\n'; }
+  } if (errors) alert('The following error(s) occurred:\n'+errors);
+  document.MM_returnValue = (errors == '');
+}
+
+function BSE_validateForm {
+  var typeEl = MM_findObj('paymentType');
+  var type = typeEl.value;
+  if (type == 0) {
+    MM_validateForm('delivFirstName','','R','delivLastName','','R','delivStreet','','R','delivSuburb','','R','delivPostCode','','R','delivState','','R','delivCountry','','R','emailAddress','','RisEmail','cardHolder','','R','cardNumber','','R','cardExpiry','','R');
+  }
+  else {
+    MM_validateForm('delivFirstName','','R','delivLastName','','R','delivStreet','','R','delivSuburb','','R','delivPostCode','','R','delivState','','R','delivCountry','','R','emailAddress','','RisEmail');
+  }
+}
+
+//-->
+</script>
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+  <tr>
+    <td width="80%" height="24">&nbsp;&nbsp;<font face="Arial, Helvetica, sans-serif" size="4" color="#FF7F00"><b><:title:></b></font></td>
+    <td height="24">&nbsp;</td>
+  </tr>
+  <tr> 
+    <td bgcolor="#999999" colspan="2" height="1"><img src="/images/trans_pixel.gif" width="24" height="1" border="0"></td>
+  </tr>
+  <tr> 
+    <td colspan="2"> 
+      <table width="100%" border="0" cellspacing="0" cellpadding="0">
+        <tr> 
+          <td width="100"><img src="/images/trans_pixel.gif" width="100" height="10" border="0"></td>
+          <td bgcolor="#999999" width="100%">&nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2">/ 
+            <a href="<:ifAdmin:>/cgi-bin/admin/admin.pl?id=1<:or:>/<:eif:>"><font color="#FFFFFF">Home</font></a> 
+            / <a href="/shop/index.html"><font color="#FFFFFF"><:article title:></font></a> 
+            /</font></td>
+        </tr>
+      </table>
+    </td>
+  </tr>
+</table>
+<p> <b><font face="Verdana, Arial, Helvetica, sans-serif" size="3"> Thank you 
+  for shopping at <:siteName:></font></b></p>
+<font class="article_body_text" face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+<p> The <:siteName:> store is run on a secure encrypted server, your details are 
+  safe with us.<br>
+</p>
+</font> 
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+  <tr> 
+    <td align="center" bgcolor="#CCCCCC" width="100%" height="18"> <font size="2" face="Verdana, Arial, Helvetica, sans-serif"> 
+      <b>Shopping Cart Items</b></font></td>
+  </tr>
+</table>
+<table border="0" cellspacing="0" cellpadding="1" width="100%" bgcolor="#666666">
+  <tr valign="middle" align="center"> 
+    <td width="100%"> 
+      <table width="100%" border="0" cellspacing="1" cellpadding="2" bgcolor="#EEEEEE">
+        <tr valign="middle" align="center" bgcolor="#666666"> 
+          <td width="100%" align="left" height="18"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF"><b>Item:</b></font>&nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF">(All 
+            prices in AUD &#150; includes GST and shipping costs where applicable)</font></td>
+          <td nowrap height="18"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF"><b>Qty:</b></font>&nbsp;</td>
+          <td height="18"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF"><b>Price:</b></font>&nbsp;</td>
+        </tr>
+        <:iterator begin items:> 
+        <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
+          <td width="100%" align="left"> &nbsp;<a href="<:item link:>"><font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><:item 
+            summary:>  <:options:></font></a></td>
+          <td nowrap align="center"><font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><:item 
+            units:></font></td>
+          <td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<: 
+            money item retailPrice :></b></font></td>
+        </tr>
+        <:iterator end items:> 
+      </table>
+    </td>
+  </tr>
+</table>
+<table width="100%" border="0" cellspacing="0" cellpadding="0">   
+  <tr> 
+    <td>&nbsp;</td>
+    <td height="20">&nbsp;</td>
+    <td height="20" bgcolor="#666666">&nbsp;</td>
+    <td align="CENTER" height="20" bgcolor="#666666" NOWRAP><font size="2" face="Verdana, Arial, Helvetica, sans-serif" color="#FFFFFF"> 
+      <b>GRAND TOTAL</b></font></td>
+    <td height="20" bgcolor="#666666">&nbsp;</td>
+  </tr>
+  <tr> 
+    <td width="50%" valign="MIDDLE"><a href="/shop/index.html"><img src="/images/store/browse_more.gif" width="133" height="21" border="0" alt="Browse More"></a></td>
+    <td NOWRAP width="50%"> 
+      <table border="0" cellspacing="0" cellpadding="0">
+        <tr></tr>
+      </table>
+    </td>
+    <td><img src="/images/store/left_bottom_corner_line.gif" width="26" height="31"></td>
+    <td align="center" bgcolor="#FFFFFF" height="100%" NOWRAP> <font size="3" face="Verdana, Arial, Helvetica, sans-serif"> 
+      <b>$<:money total:></b></font></td>
+    <td><img src="/images/store/right_bottom_corner_line.gif" width="26" height="31"></td>
+  </tr>
+  <tr> 
+    <td width="50%"></td>
+    <td width="50%"></td>
+    <td></td>
+    <td bgcolor="#666666"><img src="/images/trans_pixel.gif" width="1" height="1"></td>
+    <td></td>
+  </tr>
+</table>
+<:if User:>
+<p>&nbsp;</p>
+<:or User:>
+    <br>
+    <table bgcolor="#EEEEEE" border="0" cellspacing="0" cellpadding="10" width="100%">
+    <tr>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2">If you wish to track the status of your order you must either <a href="/cgi-bin/user.pl?show_register=1&r=/cgi-bin/shop.pl?checkout=1"><b>Register</b></a> or <a href="/cgi-bin/user.pl?show_logon=1&r=/cgi-bin/shop.pl?checkout=1"><b>Logon</b></a> before you continue with this purchase.</font></td>
+    </tr>
+    </table>
+    <br>
+<:eif User:>
+<form action="/cgi-bin/shop.pl" method="POST" onSubmit="BSE_validateForm();return document.MM_returnValue">
+  <font face="Verdana, Arial, Helvetica, sans-serif" size="3"> <b>Shipping Details:</b></font> 
+  <hr noshade size="1">
+  <table border="0" cellspacing="0" cellpadding="0">
+    <:if Message:> 
+    <tr> 
+      <td colspan=2> 
+        <p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:message:></font></p>
+      </td>
+    </tr>
+    <:or Message:><:eif Message:> 
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> First 
+        Name:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="delivFirstName" size=34 value="<:old delivFirstName:>"><:error_img delivFirstName:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Last Name:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="delivLastName" size=34 value="<:old delivLastName:>"><:error_img delivLastName:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Address:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="delivStreet" size=34 value="<:old delivStreet:>" /><:error_img delivStreet:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> City:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="delivSuburb" size=34 value="<:old delivSuburb:>" /><:error_img delivSuburb:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Postcode:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="delivPostCode" size=10 value="<:old delivPostCode:>" /><:error_img delivPostCode:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> State:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="delivState" size=10 value="<:old delivState:>" /><:error_img delivState:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Country:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="delivCountry" size=20 value="<:old delivCountry:>" /><:error_img delivCountry:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Telephone:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="telephone" size=20 value="<:old telephone:>" /><:error_img telephone:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Mobile:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="delivMobile" size=20 value="<:old delivMobile:>" /><:error_img delivMobile:>
+        </font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Facsimile:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="facsimile" size=20 value="<:old facsimile:>" /><:error_img facsimile:>
+        </font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> E-mail:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="emailAddress" size=34 value="<:old emailAddress:>"><:error_img emailAddress:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td valign="top"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Special<br />Instructions:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <textarea name="instructions" rows="5" cols="40" wrap="virtual"><:old instructions:></textarea></font><:error_img instructions:></td>
+    </tr>
+    <tr> 
+      <td colspan="2"> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        * Required information for order to be shipped</font></td>
+    </tr>
+  </table>
+  <p>&nbsp; </p>
+ <:if Cgi need_billing:>
+  <font face="Verdana, Arial, Helvetica, sans-serif" size="3"><input type="checkbox" name="need_billing" checked="checked" onClick="this.form.checkupdate.click()" /> <b>Billing Details:</b></font> 
+   
+  <hr size="1" noshade>
+  <table border="0" cellspacing="0" cellpadding="0">
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> First 
+        Name:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billFirstName" size=34 value="<:old billFirstName:>"><:error_img billFirstName:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Last Name:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billLastName" size=34 value="<:old billLastName:>"><:error_img billLastName:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Address:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billStreet" size=34 value="<:old billStreet:>"><:error_img billStreet:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> City:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billSuburb" size=34 value="<:old billSuburb:>"><:error_img billSuburb:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Postcode:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billPostCode" size=10 value="<:old billPostCode:>"><:error_img billPostCode:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> State:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billState" size=10 value="<:old billState:>"><:error_img billState:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Country:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billCountry" size=20 value="<:old billCountry:>"><:error_img billCountry:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Email:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billEmail" size=20 value="<:old billEmail:>"><:error_img billEmail:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Telephone:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billTelephone" size=20 value="<:old billTelephone:>"><:error_img billTelephone:>
+        *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Mobile:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billMobile" size=20 value="<:old billMobile:>"><:error_img billMobile:>
+        </font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Facsimile:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="billFacsimile" size=20 value="<:old billFacsimile:>" /><:error_img billFacsimile:>
+        *</font></td>
+    </tr>
+  </table>
+  <p>&nbsp; </p>
+ <:or Cgi:>
+   <p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"> <input type="checkbox" name="need_billing" onClick="this.form.checkupdate.click()" /> Billing details different to shipping</font></p>
+ <:eif Cgi:>
+<:include custom/checkout.include optional:>
+  <p>
+    <input type="submit" value="Update" name="checkupdate" />
+    <input type="submit" value="Purchase Now" name="a_order">
+    <input type="reset" value="Reset Form" name="reset">
+  </p>
+  </form>
diff --git a/site/templates/checkoutpay_base.tmpl b/site/templates/checkoutpay_base.tmpl
new file mode 100644 (file)
index 0000000..419910f
--- /dev/null
@@ -0,0 +1,162 @@
+<:wrap base.tmpl:> 
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+  <tr>
+    <td width="80%" height="24">&nbsp;&nbsp;<font face="Arial, Helvetica, sans-serif" size="4" color="#FF7F00"><b><:title:></b></font></td>
+    <td height="24">&nbsp;</td>
+  </tr>
+  <tr> 
+    <td bgcolor="#999999" colspan="2" height="1"><img src="/images/trans_pixel.gif" width="24" height="1" border="0"></td>
+  </tr>
+  <tr> 
+    <td colspan="2"> 
+      <table width="100%" border="0" cellspacing="0" cellpadding="0">
+        <tr> 
+          <td width="100"><img src="/images/trans_pixel.gif" width="100" height="10" border="0"></td>
+          <td bgcolor="#999999" width="100%">&nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2">/ 
+            <a href="<:ifAdmin:>/cgi-bin/admin/admin.pl?id=1<:or:>/<:eif:>"><font color="#FFFFFF">Home</font></a> 
+            / <a href="/shop/index.html"><font color="#FFFFFF"><:article title:></font></a> 
+            /</font></td>
+        </tr>
+      </table>
+    </td>
+  </tr>
+</table>
+<p> <b><font face="Verdana, Arial, Helvetica, sans-serif" size="3"> Thank you 
+  for shopping at <:siteName:></font></b></p>
+<font class="article_body_text" face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+<p> The <:siteName:> store is run on a secure encrypted server, your details are 
+  safe with us.</p></font> 
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+  <tr> 
+    <td align="center" bgcolor="#CCCCCC" width="100%" height="18"> <font size="2" face="Verdana, Arial, Helvetica, sans-serif"> 
+      <b>Shopping Cart Items</b></font></td>
+  </tr>
+</table>
+<table border="0" cellspacing="0" cellpadding="1" width="100%" bgcolor="#666666">
+  <tr valign="middle" align="center"> 
+    <td width="100%"> 
+      <table width="100%" border="0" cellspacing="1" cellpadding="2" bgcolor="#EEEEEE">
+        <tr valign="middle" align="center" bgcolor="#666666"> 
+          <td width="100%" align="left" height="18"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF"><b>Item:</b></font>&nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF">(All 
+            prices in AUD &#150; includes GST and shipping costs where applicable)</font></td>
+          <td nowrap height="18"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF"><b>Qty:</b></font>&nbsp;</td>
+          <td height="18"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF"><b>Price:</b></font>&nbsp;</td>
+        </tr>
+        <:iterator begin items:> 
+        <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
+          <td width="100%" align="left"> &nbsp;<a href="<:item link:>"><font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><:item 
+            summary:>  <:options:></font></a></td>
+          <td nowrap align="center"><font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><:item 
+            units:></font></td>
+          <td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<: 
+            money item retailPrice :></b></font></td>
+        </tr>
+        <:iterator end items:> 
+      </table>
+    </td>
+  </tr>
+</table>
+<table width="100%" border="0" cellspacing="0" cellpadding="0">   
+  <tr> 
+    <td>&nbsp;</td>
+    <td height="20">&nbsp;</td>
+    <td height="20" bgcolor="#666666">&nbsp;</td>
+    <td align="CENTER" height="20" bgcolor="#666666" NOWRAP><font size="2" face="Verdana, Arial, Helvetica, sans-serif" color="#FFFFFF"> 
+      <b>GRAND TOTAL</b></font></td>
+    <td height="20" bgcolor="#666666">&nbsp;</td>
+  </tr>
+  <tr> 
+    <td width="50%" valign="MIDDLE"><a href="/shop/index.html"><img src="/images/store/browse_more.gif" width="133" height="21" border="0" alt="Browse More"></a></td>
+    <td NOWRAP width="50%"> 
+      <table border="0" cellspacing="0" cellpadding="0">
+        <tr></tr>
+      </table>
+    </td>
+    <td><img src="/images/store/left_bottom_corner_line.gif" width="26" height="31"></td>
+    <td align="center" bgcolor="#FFFFFF" height="100%" NOWRAP> <font size="3" face="Verdana, Arial, Helvetica, sans-serif"> 
+      <b>$<:money total:></b></font></td>
+    <td><img src="/images/store/right_bottom_corner_line.gif" width="26" height="31"></td>
+  </tr>
+  <tr> 
+    <td width="50%"></td>
+    <td width="50%"></td>
+    <td></td>
+    <td bgcolor="#666666"><img src="/images/trans_pixel.gif" width="1" height="1"></td>
+    <td></td>
+  </tr>
+</table>
+<form action="/cgi-bin/shop.pl" method="post">
+  <font face="Verdana, Arial, Helvetica, sans-serif" size="3"> <b>Payment Details:</b></font> 
+  <hr size="1" noshade>
+<:ifMsg:><p><b><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:msg:></font></b></p><:or:><:eif:>
+  <:if Payments CC :>
+  <:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><input type="radio" name="paymentType" value="0" <:checkedPayment CC:>> Credit Card</font></p><:or MultPaymentTypes:><input type=hidden name=paymentType value=0 <:checkedPayment CC:>><:eif MultPaymentTypes:>
+  <table border="0" cellspacing="0" cellpadding="0">
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Name on 
+        Card: </font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="cardHolder" size=30 value="<:old cardHolder:>"><:error_img cardHolder:>
+        (As per card) *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Card Number:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="cardNumber" size=16 maxlength="16" value="<:old cardNumber:>"><:error_img cardNumber:>*
+         CVV: <input type="text" name="cardVerify" size="4" maxlength="4" value="<:old cardVerify:>" /><:error_img cardVerify:>  (no spaces)</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Expiry 
+        Date:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <input type="Text" name="cardExpiry" size=5 maxlength="5" value="<:old cardExpiry:>"><:error_img cardExpiry:>
+        (eg: 09/01) *</font></td>
+    </tr>
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Card Type:</font></td>
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> 
+        <select name="cardType">
+          <option value="">Choose type</option>
+          <option value="Visa" <:ifEq [cgi cardType] "Visa":>selected="selected"<:or:><:eif:>>Visa</option>
+          <option value="Mastercard" <:ifEq [cgi cardType] "Mastercard":>selected="selected"<:or:><:eif:>>Mastercard</option>
+          <option value="Bankcard" <:ifEq [cgi cardType] "Bankcard":>selected="selected"<:or:><:eif:>>Bankcard</option>
+        </select><:error_img cardType:>
+        &nbsp;*</font></td>
+    </tr>
+  </table>
+  <:or Payments:><:eif Payments:>
+  <:if Payments Cheque:>
+  <:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"> <input type=radio name=paymentType value=1 <:checkedPayment Cheque:>/>
+    Cheque</font></p><:or MultPaymentTypes:><input type=hidden name=paymentType value=1><:eif MultPaymentTypes:>
+  <p> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Please send your cheque to:</font></p>
+  <ul> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> <:cfg shop address1:><br />
+     <:cfg shop address2:><br />
+     <:cfg shop address3:></font></ul>
+  <:or Payments:><:eif Payments:>
+  <:if Payments CallMe:>
+   <:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><input type=radio name=paymentType value=2 <:checkedPayment CallMe:>/> Contact me for billing details</font></p>
+   <:or MultPaymentTypes:>
+     <input type=hidden name=paymentType value=2>
+     <p><font face="Verdana, Arial, Helvetica, sans-serif" size="2">We will call you to arrange for payment.</font></p>
+   <:eif MultPaymentTypes:>
+  <:or Payments:>
+
+  <:eif Payments:>
+  <:include custom/payment_type.include optional:>
+  <p>&nbsp; </p>
+  <font face="Verdana, Arial, Helvetica, sans-serif" size="3"> <b>Tax Invoice 
+  / Receipt &amp; Delivery Costs:</b></font> 
+  <hr size="1" noshade>
+  <table border="0" cellspacing="0" cellpadding="0" width="375">
+    <tr> 
+      <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2">We will 
+        include a tax invoice / receipt with your order, clearly showing the GST 
+        and delivery components of the purchase price.</font></td>
+    </tr>
+  </table>
+  <p>
+    <input type="submit" value="Update" name="checkupdate" />
+    <input type="submit" value="Purchase Now" name="payment">
+    <input type="reset" value="Reset Form" name="reset">
+  </p>
+  </form>
index 12e0e66989aaafb5d92c8faedafe6e2f1580bbef..ffdc40ef840170b0ded535a27c66ce5b07e3abc4 100644 (file)
@@ -21,7 +21,11 @@ Product                                   Units  Price   Extended
                                                 Total: <:order total |m10:>
                                                  GST: <:order gst   |m10:>
 
-<:ifEq [order paymentType] "0" :>Paid by credit card.<:or:><:eif
+<:ifEq [order paymentType] "0" :>Paid by credit card.<:if Order ccOnline
+:>Processed online.
+Receipt No.  : <:order ccReceipt:>
+<:or Order
+:><:eif Order:><:or:><:eif
 :><:ifEq [order paymentType] "1" :>Will be paid by cheque<:or
 :><:eif:><:ifEq [order paymentType] "2"
 :>We will call you to arrange for payment<:or:><:eif:><:
index f9c9a725daa71cd2f4d2129ea0f6579168fd13da..73f347a597df5caaed1ba71d97d9469cea9f1c3b 100644 (file)
@@ -22,10 +22,14 @@ Product                                   Units  Price   Extended
                                                  GST: <:order gst   |m10:>
 
 <:ifEq [order paymentType] "0":>Paid by credit card:
-Card No.     : <:cardNumber:>
+<:if Order ccOnline
+:>Processed online.
+Receipt No.  : <:order ccReceipt:>
+<:or Order
+:>Card No.     : <:cardNumber:>
 Expires      : <:cardExpiry:>
 Name on Card : <:order ccName:>
-Card Type    : <:order ccType :><:or:><:eif
+Card Type    : <:order ccType :><:eif Order:><:or:><:eif
 :><:ifEq [order paymentType] "1" :>Will be paid by cheque<:or
 :><:eif:><:ifEq [order paymentType] "2"
 :>Please call the customer to arrange for payment<:or:><:eif:><:
index 59bae7c0ba223a399e8a37766f18342c98d3702f..d3f528a4f69fa8f9c84871fba0d8cc86c2793ed4 100644 (file)
--- a/test.cfg
+++ b/test.cfg
@@ -52,7 +52,7 @@ site users.billing_on_main_opts=0
 #custom.user_auth=1
 # product fields.retailPrice=Dealer Price Inc GST
 #paths.local_templates=/home/tony/dev/bse/tandb_dealer/cvs/templates
-shop.payment_types=1,2,10,11,12
+#shop.payment_types=1,2,10,11,12
 payment type names.10=DirectDeposit
 payment type names.11=FaxProForma
 payment type names.12=EmailProForma
@@ -107,3 +107,12 @@ article defaults.title=<set the article title>
 article defaults.body=<set the body>
 product defaults.title=<set the product title>
 catalog defaults.title=<set the catalog title>
+
+#shop.cardprocessor=DevHelp::Payments::Test
+shop.cardprocessor=DevHelp::Payments::Inpho
+inpho.user=theowww
+inpho.password=s1tz^^mD1
+inpho.test=1
+inpho.test_url=http://www.develop-help.com/cgi-bin/inphotest.pl
+inpho.test_user=test
+inpho.test_password=test