coupon admin, cart, checkout, payment form
authorTony Cook <tony@develop-help.com>
Tue, 11 Jun 2013 00:40:57 +0000 (10:40 +1000)
committerTony Cook <tony@develop-help.com>
Sun, 21 Jul 2013 23:34:15 +0000 (09:34 +1000)
24 files changed:
MANIFEST
schema/bse.sql
site/cgi-bin/modules/BSE/Cart.pm
site/cgi-bin/modules/BSE/Request/Base.pm
site/cgi-bin/modules/BSE/Shop/Util.pm
site/cgi-bin/modules/BSE/TB/Coupon.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/CouponTier.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/CouponTiers.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/Coupons.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/UI/AdminShop.pm
site/cgi-bin/modules/BSE/UI/Shop.pm
site/data/db/bse_msg_base.data
site/data/db/bse_msg_defaults.data
site/data/db/sql_statements.data
site/templates/admin/coupons/add.tmpl [new file with mode: 0644]
site/templates/admin/coupons/delete.tmpl [new file with mode: 0644]
site/templates/admin/coupons/edit.tmpl [new file with mode: 0644]
site/templates/admin/coupons/list.tmpl [new file with mode: 0644]
site/templates/admin/menu.tmpl
site/templates/cart_base.tmpl
site/templates/checkoutnew_base.tmpl
site/templates/checkoutpay_base.tmpl
site/util/mysql.str
t/data/known_pod_issues.txt

index 309f9c4110b6984b7c663c721022e2cbf6a54474..b85c4c867a199976873175f9ead9dd961342dad1 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -185,6 +185,10 @@ site/cgi-bin/modules/BSE/TB/AuditEntry.pm
 site/cgi-bin/modules/BSE/TB/AuditLog.pm
 site/cgi-bin/modules/BSE/TB/BackgroundTask.pm
 site/cgi-bin/modules/BSE/TB/BackgroundTasks.pm
+site/cgi-bin/modules/BSE/TB/Coupon.pm
+site/cgi-bin/modules/BSE/TB/Coupons.pm
+site/cgi-bin/modules/BSE/TB/CouponTier.pm
+site/cgi-bin/modules/BSE/TB/CouponTiers.pm
 site/cgi-bin/modules/BSE/TB/File.pm
 site/cgi-bin/modules/BSE/TB/FileAccessLog.pm
 site/cgi-bin/modules/BSE/TB/FileAccessLogEntry.pm
@@ -560,6 +564,10 @@ site/templates/admin/base.tmpl
 site/templates/admin/catalog.tmpl      # embedded in the shopadmin catalog/product display
 site/templates/admin/catalog_custom.tmpl
 site/templates/admin/changepw.tmpl
+site/templates/admin/coupons/add.tmpl
+site/templates/admin/coupons/delete.tmpl
+site/templates/admin/coupons/edit.tmpl
+site/templates/admin/coupons/list.tmpl
 site/templates/admin/edit_0.tmpl
 site/templates/admin/edit_1.tmpl
 site/templates/admin/edit_catalog.tmpl
index eceb3c1351d2dc6b71226add3a0c831e3f18bd9e..988843e5060471af517306ff370501ab9a250ac0 100644 (file)
@@ -2,6 +2,8 @@ drop table if exists bse_tag_category_deps;
 drop table if exists bse_tag_categories;
 drop table if exists bse_tag_members;
 drop table if exists bse_tags;
+drop table if exists bse_coupon_tiers;
+drop table if exists bse_coupons;
 
 -- represents sections, articles
 DROP TABLE IF EXISTS article;
@@ -347,6 +349,9 @@ create table orders (
   -- true if the order was paid manually
   paid_manually integer not null default 0,
 
+  coupon_code varchar(40) not null default '',
+  coupon_discount real not null default 0,
+
   primary key (id),
   index order_cchash(ccNumberHash),
   index order_userId(userId, orderDate)
@@ -908,7 +913,7 @@ create table bse_product_options (
   enabled integer not null default 0,
   default_value integer,
   index product_order(product_id, display_order)
-) type=innodb;
+) engine=innodb;
 
 drop table if exists bse_product_option_values;
 create table bse_product_option_values (
@@ -917,7 +922,7 @@ create table bse_product_option_values (
   value varchar(255) not null,
   display_order integer not null,
   index option_order(product_option_id, display_order)
-) type=innodb;
+) engine=innodb;
 
 drop table if exists bse_order_item_options;
 create table bse_order_item_options (
@@ -929,7 +934,7 @@ create table bse_order_item_options (
   display varchar(80) not null,
   display_order integer not null,
   index item_order(order_item_id, display_order)
-) type=innodb;
+) engine=innodb;
 
 drop table if exists bse_owned_files;
 create table bse_owned_files (
@@ -1236,7 +1241,7 @@ create table bse_files (
   ftype varchar(20) not null default 'img',
 
   index owner(file_type, owner_id)
-) type = InnoDB;
+) engine = InnoDB;
 
 -- a generic selection of files from a pool
 create table bse_selected_files (
@@ -1252,7 +1257,7 @@ create table bse_selected_files (
   display_order integer not null default -1,
 
   unique only_one(owner_id, owner_type, file_id)
-) type = InnoDB;
+) engine = InnoDB;
 
 drop table if exists bse_price_tiers;
 create table bse_price_tiers (
@@ -1266,7 +1271,7 @@ create table bse_price_tiers (
   to_date date null,
 
   display_order integer null null
-);
+) engine=innodb;
 
 drop table if exists bse_price_tier_prices;
 
@@ -1336,4 +1341,42 @@ create table bse_ip_lockouts (
   expires datetime not null,
 
   unique ip_address(ip_address, type)
-) type=innodb;
\ No newline at end of file
+) engine=innodb;
+
+create table bse_coupons (
+  id integer not null auto_increment primary key,
+
+  code varchar(40) not null,
+
+  description text not null,
+
+  `release` date not null,
+
+  expiry date not null,
+
+  discount_percent real not null,
+
+  campaign varchar(20) not null,
+
+  last_modified datetime not null,
+
+  untiered integer not null default 0,
+
+  unique codes(code)
+) engine=InnoDB;
+
+create table bse_coupon_tiers (
+  id integer not null auto_increment primary key,
+
+  coupon_id integer not null,
+
+  tier_id integer not null,
+
+  unique (coupon_id, tier_id),
+
+  foreign key (coupon_id) references bse_coupons(id)
+    on delete cascade on update restrict,
+
+  foreign key (tier_id) references bse_price_tiers(id)
+    on delete cascade on update restrict
+) engine=InnoDB;
\ No newline at end of file
index fa698726c48fb012c653db826d0ea8cc22164349..b1a704490a4fcb843e01b7e0b53bb22953c872ac 100644 (file)
@@ -2,7 +2,7 @@ package BSE::Cart;
 use strict;
 use Scalar::Util;
 
-our $VERSION = "1.003";
+our $VERSION = "1.004";
 
 =head1 NAME
 
@@ -54,6 +54,9 @@ sub new {
     $self->_enter_cart;
   }
 
+  $self->{coupon_code} = $self->{req}->session->{cart_coupon_code};
+  defined $self->{coupon_code} or $self->{coupon_code} = "";
+
   return $self;
 }
 
@@ -102,6 +105,8 @@ sub products {
 
 Return the total cost of the items in the cart.
 
+This does not include shipping costs and is not discounted.
+
 =cut
 
 sub total_cost {
@@ -115,6 +120,38 @@ sub total_cost {
   return $total_cost;
 }
 
+=item discounted_product_cost
+
+Cost of products with an product discount taken into account.
+
+Note: this rounds thr total B<down>.
+
+=cut
+
+sub discounted_product_cost {
+  my ($self) = @_;
+
+  my $cost = $self->total_cost;
+
+  if ($self->coupon_active) {
+    $cost -= $cost * $self->coupon_code_discount_pc / 100;
+  }
+
+  return int($cost);
+}
+
+=item product_cost_discount
+
+Return any amount taken off the product cost.
+
+=cut
+
+sub product_cost_discount {
+  my ($self) = @_;
+
+  return $self->total_cost - $self->discounted_product_cost;
+}
+
 =item set_shipping_cost()
 
 Set the cost of shipping.
@@ -169,7 +206,167 @@ This doesn't handle custom costs yet.
 sub total {
   my ($self) = @_;
 
-  return $self->total_cost() + $self->shipping_cost();
+  my $cost = 0;
+
+  $cost += $self->discounted_product_cost;
+
+  $cost += $self->shipping_cost;
+
+  $cost += $self->custom_cost;
+
+  return $cost;
+}
+
+=item coupon_code
+
+The current coupon code.
+
+=cut
+
+sub coupon_code {
+  my ($self) = @_;
+
+  return $self->{coupon_code};
+}
+
+=item set_coupon_code()
+
+Used by the shop to set the coupon code.
+
+=cut
+
+sub set_coupon_code {
+  my ($self, $code) = @_;
+
+  $code =~ s/\A\s+//;
+  $code =~ s/\s+\z//;
+  $self->{coupon_code} = $code;
+  delete $self->{coupon_valid};
+  $self->{req}->session->{cart_coupon_code} = $code;
+}
+
+=item coupon_code_discount_pc
+
+The percentage discount for the current coupon code, if that code is
+valid and the contents of the cart are valid for that coupon code.
+
+=cut
+
+sub coupon_code_discount_pc {
+  my ($self) = @_;
+
+  $self->coupon_valid
+    or return 0;
+
+  return $self->{coupon_check}{coupon}->discount_percent;
+}
+
+=item coupon_valid
+
+Return true if the current coupon code is valid
+
+=cut
+
+sub coupon_valid {
+  my ($self) = @_;
+
+  unless ($self->{coupon_check}) {
+    if (length $self->{coupon_code}) {
+      require BSE::TB::Coupons;
+      my ($coupon) = BSE::TB::Coupons->getBy(code => $self->{coupon_code});
+      print STDERR "Searching for coupon '$self->{coupon_code}'\n";
+      my %check =
+       (
+        coupon => $coupon,
+        valid => 0,
+       );
+      #print STDERR " coupon $coupon\n";
+      #print STDERR "released ", 0+ $coupon->is_released, " expired ",
+      #        0+$coupon->is_expired, " valid ", 0+$coupon->is_valid, "\n" if $coupon;
+      if ($coupon && $coupon->is_valid) {
+       $check{valid} = 1;
+       $check{active} = 1;
+       my %tiers = map { $_ => 1 } $coupon->tiers;
+      ITEM:
+       for my $item ($self->items) {
+         my $applies = 1;
+         if ($item->tier_id) {
+           #print STDERR "tier ", $item->tier_id, " tiers ", join(",", keys %tiers), "\n";
+           if (!$tiers{$item->tier_id}) {
+             $applies = 0;
+           }
+         }
+         else {
+           if (!$coupon->untiered) {
+             $applies = 0;
+           }
+         }
+         $item->{coupon_applies} = $applies;
+         $applies or $check{active} = 0;
+       }
+      }
+      $self->{coupon_check} = \%check;
+    }
+    else {
+      $self->{coupon_check} =
+       {
+        valid => 0,
+        active => 0,
+       };
+    }
+  }
+
+  return $self->{coupon_check}{valid};
+}
+
+=item coupon_active
+
+Return true if the current coupon is active, ie. both valid and the
+cart has products of all the right tiers.
+
+=cut
+
+sub coupon_active {
+  my ($self) = @_;
+
+  $self->coupon_valid
+    or return 0;
+
+  return $self->{coupon_check}{active};
+}
+
+=item coupon
+
+The current coupon object, if and only if the coupon code is valid.
+
+=cut
+
+sub coupon {
+  my ($self) = @_;
+
+  $self->coupon_valid
+    or return;
+
+  $self->{coupon_check}{coupon};
+}
+
+=item custom_cost
+
+Return any custom cost specified by a custom class.
+
+=cut
+
+sub custom_cost {
+  my ($self) = @_;
+
+  unless (exists $self->{custom_cost}) {
+    my $obj = BSE::CfgInfo::custom_class($self->{req}->cfg);
+    $self->{custom_cost} =
+      $obj->total_extras(scalar $self->items, scalar $self->products,
+                        $self->{custom_state}, $self->{req}->cfg, $self->{stage});
+  }
+
+  return $self->{custom_cost};
 }
 
 =item have_sales_files
@@ -219,7 +416,7 @@ sub need_logon {
   return 1;
 }
 
-=head1 need_logon_message
+=item need_logon_message
 
 Returns a list with the error message and message id of the reason the
 user needs to logon for this cart.
@@ -591,6 +788,21 @@ sub option_text {
   return join(", ", map "$_->{desc}: $_->{display}", @$options);
 }
 
+=item coupon_applies
+
+Returns true if the current coupon code applies to the item.
+
+=cut
+
+sub coupon_applies {
+  my ($self) = @_;
+
+  $self->{cart}->coupon_valid
+    or return 0;
+
+  return $self->{coupon_applies};
+}
+
 =item session
 
 The session object of the seminar session
index 92812bb9fba2fa6ad214020a87f134b22910c76a..11020ced085208386586a53e5a0181d1eceb7514 100644 (file)
@@ -5,7 +5,7 @@ use BSE::Cfg;
 use BSE::Util::HTML;
 use Carp qw(cluck confess);
 
-our $VERSION = "1.023";
+our $VERSION = "1.024";
 
 =head1 NAME
 
@@ -1779,6 +1779,9 @@ sub cgi_fields {
        $value = join("", $cgi->param($name));
       }
     }
+    elsif ($field->{htmltype} eq "multicheck") {
+      $value = [ $cgi->param($name) ];
+    }
     elsif ($field->{type} && $field->{type} eq "date" && !$opts{api}) {
       ($value) = $cgi->param($name);
       require DevHelp::Date;
index e90bb3db1704d2fab5bc17d88ff080a07ab0386d..9d3851308135016219e11774de4136f0b2b724ea 100644 (file)
@@ -7,7 +7,7 @@ use vars qw(@ISA @EXPORT_OK);
                 payment_types order_item_opts
  PAYMENT_CC PAYMENT_CHEQUE PAYMENT_CALLME PAYMENT_MANUAL PAYMENT_PAYPAL/;
 
-our $VERSION = "1.008";
+our $VERSION = "1.009";
 
 our %EXPORT_TAGS =
   (
@@ -71,7 +71,7 @@ sub shop_cart_tags {
        my $what = $_[0] || 'retailPrice';
        $current_item->extended($what);
      },
-     total => sub { $cart->total_cost },
+     total => sub { $cart->total },
      $ito->make
      (
       plural => "options",
@@ -82,7 +82,7 @@ sub shop_cart_tags {
      location => [ \&tag_location, \$current_item, \$location ],
      ifHaveSaleFiles => [ have_sales_files => $cart ],
      custom_class($cfg)
-     ->checkout_actions($acts, $cart->items, $cart->products, $req->session->{custom}, $q, $cfg),
+     ->checkout_actions($acts, scalar $cart->items, scalar $cart->products, $req->session->{custom}, $q, $cfg),
     );  
 }
 
diff --git a/site/cgi-bin/modules/BSE/TB/Coupon.pm b/site/cgi-bin/modules/BSE/TB/Coupon.pm
new file mode 100644 (file)
index 0000000..b5710ca
--- /dev/null
@@ -0,0 +1,282 @@
+package BSE::TB::Coupon;
+use strict;
+use Squirrel::Row;
+our @ISA = qw/Squirrel::Row/;
+use BSE::TB::CouponTiers;
+
+=head1 NAME
+
+our $VERSION = "1.000";
+
+BSE::TB::Coupon - shop coupon objects
+
+=head1 SYNOPSIS
+
+  use BSE::TB::Coupons;
+
+  my $coupon = BSE::TB::Coupons->make(...);
+
+=head1 DESCRIPTION
+
+Represents shop coupons.
+
+=head1 METHODS
+
+=over
+
+=cut
+
+sub columns {
+  return qw/id code description release expiry discount_percent campaign last_modified untiered/;
+}
+
+sub table {
+  "bse_coupons";
+}
+
+sub defaults {
+  require BSE::Util::SQL;
+  return
+    (
+     last_modified => BSE::Util::SQL::now_sqldatetime(),
+     untiered => 1,
+    );
+}
+
+=item tiers
+
+Return the tier ids for a coupon.
+
+=cut
+
+sub tiers {
+  my ($self) = @_;
+
+  return BSE::TB::CouponTiers->getColumnBy
+    (
+     tier_id =>
+     [
+      coupon_id => $self->id
+     ]
+    );
+}
+
+=item tier_objects
+
+Return tier objects for each of the tiers this coupon is valid for.
+
+=cut
+
+sub tier_objects {
+  my ($self) = @_;
+
+  require BSE::TB::PriceTiers;
+  return BSE::TB::PriceTiers->getSpecial(forCoupon => $self->id);
+}
+
+=item set_tiers(\@tiers)
+
+Set the tiers for a coupon.
+
+=cut
+
+sub set_tiers {
+  my ($self, $tiers) = @_;
+
+  my %current = map { $_->tier_id => $_ }
+    BSE::TB::CouponTiers->getBy2
+       (
+        [
+         coupon_id => $self->id
+        ]
+       );
+
+  my %keep = map { $_->tier_id => $_ } grep $_, delete @current{@$tiers};
+
+  $_->remove for keys %current;
+
+  for my $tier_id (grep !$keep{$_}, @$tiers) {
+    BSE::TB::CouponTiers->make
+       (
+        coupon_id => $self->id,
+        tier_id => $tier_id
+       );
+  }
+
+  1;
+}
+
+sub remove {
+  my ($self) = @_;
+
+  my @tiers = BSE::TB::CouponTiers->getBy2
+    (
+     [
+      coupon_id => $self->id
+     ]
+    );
+  $_->remove for @tiers;
+
+  $self->SUPER::remove();
+}
+
+sub json_data {
+  my ($self) = @_;
+
+  my $data = $self->data_only;
+  $data->{tiers} = [ $self->tiers ];
+
+  return $data;
+}
+
+=item is_expired
+
+Returns true if the coupon has expired.
+
+=cut
+
+sub is_expired {
+  my ($self) = @_;
+
+  require BSE::Util::SQL;
+  return BSE::Util::SQL::now_sqldate() gt $self->expiry;
+}
+
+=item is_released
+
+Returns true if the coupon has been released.
+
+=cut
+
+sub is_released {
+  my ($self) = @_;
+
+  require BSE::Util::SQL;
+  return $self->release le BSE::Util::SQL::now_sqldate();
+}
+
+=item is_valid
+
+Returns true if the coupon is both released and unexpired.
+
+=cut
+
+sub is_valid {
+  my ($self) = @_;
+
+  return $self->is_released && !$self->is_expired;
+}
+
+sub fields {
+  my ($class) = @_;
+
+  my %fields =
+    (
+     code =>
+     {
+      description => "Coupon Code",
+      required => 1,
+      width => 20,
+      maxlength => 40,
+      htmltype => "text",
+      rules => "dh_one_line;coupon_code",
+     },
+     description =>
+     {
+      description => "Description",
+      required => 1,
+      width => 80,
+      htmltype => "text",
+      rules => "dh_one_line",
+     },
+     release =>
+     {
+      description => "Release Date",
+      required => 1,
+      width => 10,
+      htmltype => "text",
+      type => "date",
+      rules => "date",
+     },
+     expiry =>
+     {
+      description => "Expiry Date",
+      required => 1,
+      width => 10,
+      htmltype => "text",
+      type => "date",
+      rules => "date",
+     },
+     discount_percent =>
+     {
+      description => "Discount %",
+      required => 1,
+      width => 5,
+      htmltype => "text",
+      rules => "coupon_percent",
+      units => "%",
+     },
+     campaign =>
+     {
+      description => "Campaign",
+      width => 20,
+      maxlength => 20,
+      htmltype => "text",
+      rules => "dh_one_line",
+     },
+     tiers =>
+     {
+      description => "Price Tiers",
+      htmltype => "multicheck",
+      select =>
+      {
+       values => sub {
+        require BSE::TB::PriceTiers;
+        BSE::TB::PriceTiers->getColumnsBy
+            (
+             [ qw(id description) ],
+             [ ],
+             { order => "display_order asc" },
+            );
+       },
+       id => "id",
+       label => "description",
+      },
+     },
+     untiered =>
+     {
+      description => "Untiered",
+      htmltype => "checkbox",
+      units => "Applies to untiered products too",
+      default => 1,
+     },
+    );
+
+  require BSE::Validate;
+  return BSE::Validate::bse_configure_fields(\%fields, BSE::Cfg->single, "bse coupon validation");
+}
+
+sub rules {
+  return
+    {
+     coupon_code =>
+     {
+      match => qr/\A[a-zA-Z0-9]+\z/,
+      error => '$n can only contain letters and digits',
+     },
+     coupon_percent =>
+     {
+      real => '0 - 100',
+     },
+    };
+}
+
+1;
+
+=back
+
+=head1 AUTHOR
+
+Tony Cook <tony@develop-help.com>
+
+=cut
diff --git a/site/cgi-bin/modules/BSE/TB/CouponTier.pm b/site/cgi-bin/modules/BSE/TB/CouponTier.pm
new file mode 100644 (file)
index 0000000..fda7646
--- /dev/null
@@ -0,0 +1,16 @@
+package BSE::TB::CouponTier;
+use strict;
+use Squirrel::Row;
+our @ISA = qw/Squirrel::Row/;
+
+our $VERSION = "1.000";
+
+sub columns {
+  return qw/id coupon_id tier_id/;
+}
+
+sub table {
+  "bse_coupon_tiers";
+}
+
+1;
diff --git a/site/cgi-bin/modules/BSE/TB/CouponTiers.pm b/site/cgi-bin/modules/BSE/TB/CouponTiers.pm
new file mode 100644 (file)
index 0000000..a932d85
--- /dev/null
@@ -0,0 +1,13 @@
+package BSE::TB::CouponTiers;
+use strict;
+use Squirrel::Table;
+our @ISA = qw(Squirrel::Table);
+use BSE::TB::CouponTier;
+
+our $VERSION = "1.000";
+
+sub rowClass {
+  return 'BSE::TB::CouponTier';
+}
+
+1;
diff --git a/site/cgi-bin/modules/BSE/TB/Coupons.pm b/site/cgi-bin/modules/BSE/TB/Coupons.pm
new file mode 100644 (file)
index 0000000..d551477
--- /dev/null
@@ -0,0 +1,27 @@
+package BSE::TB::Coupons;
+use strict;
+use Squirrel::Table;
+our @ISA = qw(Squirrel::Table);
+use BSE::TB::Coupon;
+
+our $VERSION = "1.000";
+
+sub rowClass {
+  return 'BSE::TB::Coupon';
+}
+
+sub make {
+  my ($self, %opts) = @_;
+
+  my $tiers = delete $opts{tiers};
+
+  my $coupon = $self->SUPER::make(%opts);
+
+  if ($tiers) {
+    $coupon->set_tiers($tiers);
+  }
+
+  return $coupon;
+}
+
+1;
index bf00f4c51c9df34103a8e29291996fdc72d0b1a7..59f44937cffc0393238b905ffc394b4f979486e0 100644 (file)
@@ -18,8 +18,10 @@ use BSE::Util::HTML qw(:default popup_menu);
 use BSE::Arrows;
 use BSE::Shop::Util qw(:payment order_item_opts nice_options payment_types);
 use BSE::CfgInfo qw(cfg_dist_image_uri);
+use BSE::Util::SQL qw/now_sqldate sql_to_date date_to_sql sql_date sql_datetime/;
+use BSE::Util::Valid qw/valid_date/;
 
-our $VERSION = "1.019";
+our $VERSION = "1.020";
 
 my %actions =
   (
@@ -36,6 +38,13 @@ my %actions =
    product_detail => '',
    product_list => '',
    paypal_refund => 'bse_shop_order_refund_paypal',
+   coupon_list => 'bse_shop_coupon_list',
+   coupon_addform => 'bse_shop_coupon_add',
+   coupon_add => 'bse_shop_coupon_add',
+   coupon_edit => 'bse_shop_coupon_edit',
+   coupon_save => 'bse_shop_coupon_edit',
+   coupon_deleteform => 'bse_shop_coupon_delete',
+   coupon_delete => 'bse_shop_coupon_delete',
   );
 
 sub actions {
@@ -54,6 +63,17 @@ sub action_prefix {
   ''
 }
 
+my %csrfp =
+  (
+   coupon_add => { token => "admin_bse_coupon_add", target => "coupon_addform" },
+   coupon_save => { token => "admin_bse_coupon_edit", target => "coupon_edit" },
+   coupon_delete => { token => "admin_bse_coupon_delete", target => "coupon_delete" },
+  );
+
+sub csrfp_tokens {
+  \%csrfp;
+}
+
 #####################
 # product management
 
@@ -190,7 +210,7 @@ sub req_product_list {
   my @catalogs = sort { $b->{displayOrder} <=> $a->{displayOrder} }
     grep $_->{generator} eq 'Generate::Catalog', Articles->children($shopid);
   my $catalog_index = -1;
-  $message ||= $cgi->param('m') || $cgi->param('message') || '';
+  $message = $req->message($message);
   if (defined $cgi->param('showstepkids')) {
     $session->{showstepkids} = $cgi->param('showstepkids');
   }
@@ -464,8 +484,6 @@ sub order_list_low {
 
   my $from = $cgi->param('from');
   my $to = $cgi->param('to');
-  use BSE::Util::SQL qw/now_sqldate sql_to_date date_to_sql sql_date/;
-  use BSE::Util::Valid qw/valid_date/;
   my $today = now_sqldate();
   for my $what ($from, $to) {
     if (defined $what) {
@@ -1125,18 +1143,408 @@ sub req_order_save {
   return $req->get_refresh($url);
 }
 
+my %coupon_sorts =
+  (
+   expiry => "expiry desc",
+   release => "release desc",
+   code => "code asc",
+  );
+
+=item coupon_list
+
+Display a list of coupons.
+
+Accepts two optional parameters:
+
+=over
+
+=item *
+
+C<sort> which can be any of:
+
+=over
+
+=item *
+
+C<expiry> - sort by expiry date descending
+
+=item *
+
+C<release> - sort by release date descending
+
+=item *
+
+C<code> - sort by code ascending
+
+=back
+
+The default and fallback for unknown values is C<expiry>.
+
+=item *
+
+C<all> - if a true value, returns all coupons, otherwise only coupons
+modified in the last 60 days, or with a release or expiry date in the
+last 60 days are returned.
+
+=back
+
+Allows standard admin tags and variables with the following additional
+variable:
+
+=over
+
+=item *
+
+C<coupons> - an array of coupons
+
+=item *
+
+C<coupons_all> - true if all coupons were requested
+
+=item *
+
+C<coupons_sort> - the 
+
+=back
+
+In ajax context returns:
+
+  {
+    success => 1,
+    coupons => [ coupon, ... ]
+  }
+
+where each coupon is a hash containing the coupon data, and the key
+tiers is a list of tier ids.
+
+Template: F<admin/coupons/list>
+
+=cut
+
+sub req_coupon_list {
+  my ($self, $req) = @_;
+
+  my $sort = $req->cgi->param('sort') || 'expiry';
+  $sort =~ /^(expiry|code|release)/ or $sort = 'expiry';
+  my $all = $req->cgi->param('all')  || 0;
+  my @cond;
+  unless ($all) {
+    my $past_60_days = sql_datetime(time() - 60 * 86_400);
+    @cond = 
+      (
+       [ or =>
+        [ '>', last_modified => $past_60_days ],
+        [ '>', expiry => $past_60_days ],
+        [ '>', release => $past_60_days ],
+       ]
+      );
+  }
+  my $scode = $req->cgi->param('scode');
+  if ($scode) {
+    if ($scode =~ /^=(.*)/) {
+      push @cond, [ '=', code => $1 ];
+    }
+    else {
+      push @cond, [ 'like', code => $scode . '%' ];
+    }
+  }
+  require BSE::TB::Coupons;
+  my @coupons = BSE::TB::Coupons->getBy2
+    (
+     \@cond,
+     { order => $coupon_sorts{$sort} }
+    );
+
+  if ($req->is_ajax) {
+    return $req->json_content
+      (
+       success => 1,
+       coupons => [ map $_->json_data, @coupons ],
+      );
+  }
+
+  $req->set_variable(coupons => \@coupons);
+  $req->set_variable(coupons_all => $all);
+  $req->set_variable(coupons_sort => $sort);
+
+  my %acts = $req->admin_tags;
+
+  return $req->dyn_response('admin/coupons/list', \%acts);
+}
+
+=item coupon_addform
+
+Display a form for adding new coupons.
+
+Template: F<admin/coupons/add>
+
+=cut
+
+sub req_coupon_addform {
+  my ($self, $req, $errors) = @_;
+
+  my %acts = $req->admin_tags;
+
+  $req->message($errors);
+
+  require BSE::TB::Coupons;
+  $req->set_variable(fields => BSE::TB::Coupon->fields);
+  $req->set_variable(coupon => undef);
+  $req->set_variable(errors => $errors || {});
+
+  return $req->dyn_response("admin/coupons/add", \%acts);
+}
+
+=item coupon_add
+
+Add a new coupon.
+
+Accepts coupon fields.
+
+Tiers are accepted as separate values for the tiers field.
+
+CSRF token: C<admin_bse_coupon_add>
+
+=cut
+
+sub req_coupon_add {
+  my ($self, $req) = @_;
+
+  require BSE::TB::Coupons;
+  my $fields = BSE::TB::Coupon->fields;
+  my %errors;
+  $req->validate(fields => $fields, errors => \%errors,
+                rules => BSE::TB::Coupon->rules);
+
+  my $values = $req->cgi_fields(fields => $fields);
+
+  unless ($errors{code}) {
+    my ($other) = BSE::TB::Coupons->getBy(code => $values->{code});
+    $other
+      and $errors{code} = "msg:bse/admin/shop/coupons/adddup:$values->{code}";
+  }
+
+  if (keys %errors) {
+    $req->is_ajax
+      and return $req->field_error(\%errors);
+    return $self->req_coupon_addform($req, \%errors);
+  }
+
+  my $coupon = BSE::TB::Coupons->make(%$values);
+
+  if ($req->is_ajax) {
+    return $req->json_content
+      (
+       success => 1,
+       coupon => $coupon->json_data,
+      );
+  }
+  else {
+    $req->flash_notice("msg:bse/admin/shop/coupons/add", [ $coupon ]);
+
+    return $req->get_def_refresh($req->cfg->admin_url2("shopadmin", "coupon_list"));
+  }
+}
+
+sub _get_coupon {
+  my ($self, $req, $rresult) = @_;
+
+  my $cgi = $req->cgi;
+  my $id = $cgi->param("id");
+  require BSE::TB::Coupons;
+  my $coupon;
+  if ($id) {
+    $coupon = BSE::TB::Coupons->getByPkey($id);
+  }
+  else {
+    my $code = $cgi->param("code");
+    if ($code) {
+      ($coupon) = BSE::TB::Coupons->getBy(code => $code);
+    }
+  }
+  unless ($coupon) {
+    $$rresult = $self->req_coupon_list($req, { id => "Missing id or code" });
+    return;
+  }
+
+  return $coupon;
+}
+
+sub _get_coupon_id {
+  my ($self, $req, $rresult) = @_;
+
+  my $cgi = $req->cgi;
+  my $id = $cgi->param("id");
+  require BSE::TB::Coupons;
+  my $coupon;
+  if ($id) {
+    $coupon = BSE::TB::Coupons->getByPkey($id);
+  }
+  unless ($coupon) {
+    $$rresult = $self->req_coupon_list($req, { id => "Missing id or code" });
+    return;
+  }
+
+  return $coupon;
+}
+
+=item coupon_edit
+
+Edit a coupon.
+
+Requires C<id> as a coupon id to edit.
+
+Template: F<admin/coupons/edit>
+
+=cut
+
+sub req_coupon_edit {
+  my ($self, $req, $errors) = @_;
+
+  my $result;
+  my $coupon = $self->_get_coupon_id($req, \$result)
+    or return $result;
+
+  my %acts = $req->admin_tags;
+
+  $req->message($errors);
+
+  require BSE::TB::Coupons;
+  $req->set_variable(fields => BSE::TB::Coupon->fields);
+  $req->set_variable(coupon => $coupon);
+  $req->set_variable(errors => $errors || {});
+
+  return $req->dyn_response("admin/coupons/edit", \%acts);
+}
+
+=item coupon_save
+
+Save changes to a coupon, accepts:
+
+=over
+
+=item *
+
+C<id> - id of the coupon to save.
+
+=item *
+
+other coupon fields.
+
+=back
+
+CSRF token: C<admin_bse_coupon_save>
+
+=cut
+
+sub req_coupon_save {
+  my ($self, $req) = @_;
+
+  my $result;
+  my $coupon = $self->_get_coupon_id($req, \$result)
+    or return $result;
+
+  require BSE::TB::Coupons;
+  my $fields = BSE::TB::Coupon->fields;
+  my %errors;
+  $req->validate(fields => $fields, errors => \%errors,
+                rules => BSE::TB::Coupon->rules);
+
+  my $values = $req->cgi_fields(fields => $fields);
+
+  unless ($errors{code}) {
+    my ($other) = BSE::TB::Coupons->getBy(code => $values->{code});
+    $other && $other->id != $coupon->id
+      and $errors{code} = "msg:bse/admin/shop/coupons/editdup:$values->{code}";
+  }
+
+  if (keys %errors) {
+    $req->is_ajax
+      and return $req->field_error(\%errors);
+    return $self->req_coupon_edit($req, \%errors);
+  }
+
+  my $tiers = delete $values->{tiers};
+  for my $key (keys %$values) {
+    $coupon->set($key => $values->{$key});
+  }
+  $coupon->set_tiers($tiers);
+  $coupon->save;
+
+  if ($req->is_ajax) {
+    return $req->json_content
+      (
+       success => 1,
+       coupon => $coupon->json_data,
+      );
+  }
+  else {
+    $req->flash_notice("msg:bse/admin/shop/coupons/save", [ $coupon ]);
+
+    return $req->get_def_refresh($req->cfg->admin_url2("shopadmin", "coupon_list"));
+  }
+}
+
+=item coupon_deleteform
+
+Prompt for deletion of a coupon
+
+Requires C<id> as a coupon id to elete.
+
+Template: F<admin/coupons/delete>
+
+=cut
+
+sub req_coupon_deleteform {
+  my ($self, $req) = @_;
+
+  my $result;
+  my $coupon = $self->_get_coupon_id($req, \$result)
+    or return $result;
+
+  my %acts = $req->admin_tags;
+
+  require BSE::TB::Coupons;
+  $req->set_variable(fields => BSE::TB::Coupon->fields);
+  $req->set_variable(coupon => $coupon);
+
+  return $req->dyn_response("admin/coupons/delete", \%acts);
+}
+
+=item coupon_delete
+
+Delete a coupon
+
+Requires C<id> as a coupon id to delete.
+
+CSRF token: C<admin_bse_coupon_delete>
+
+=cut
+
+sub req_coupon_delete {
+  my ($self, $req) = @_;
+
+  my $result;
+  my $coupon = $self->_get_coupon_id($req, \$result)
+    or return $result;
+
+  my $code = $coupon->code;
+  $coupon->remove;
+
+  if ($req->is_ajax) {
+    return $req->json_content(success => 1);
+  }
+  else {
+    $req->flash_notice("msg:bse/admin/shop/coupons/delete", [ $code ]);
+
+    return $req->get_def_refresh($req->cfg->admin_url2("shopadmin", "coupon_list"));
+  }
+}
+
 #####################
 # utilities
 # perhaps some of these belong in a class...
 
-# format an ANSI SQL date for display
-sub money_to_cents {
-  my $money = shift;
-
-  $$money =~ /^\s*(\d+(\.\d*)|\.\d+)/
-    or return undef;
-  return $$money = sprintf("%.0f ", $$money * 100);
-}
 
 # convert an epoch time to sql format
 sub epoch_to_sql {
index 92e2710643eb8fc7e3507bcccede8c4c4a06d36a..d9688556483e5da36f245ea780c47089180c3006 100644 (file)
@@ -18,7 +18,21 @@ use BSE::Countries qw(bse_country_code);
 use BSE::Util::Secure qw(make_secret);
 use BSE::Template;
 
-our $VERSION = "1.038";
+our $VERSION = "1.039";
+
+=head1 NAME
+
+BSE::UI::Shop - implements the shop for BSE
+
+=head1 DESCRIPTION
+
+BSE::UI::Shop implements the shop for BSE.
+
+=head1 TARGETS
+
+=over
+
+=cut
 
 use constant MSG_SHOP_CART_FULL => 'Your shopping cart is full, please remove an item and try adding an item again';
 
@@ -90,19 +104,13 @@ sub req_cart {
 
   $class->_refresh_cart($req);
 
-  my @cart = @{$req->session->{cart} || []};
-  my @cart_prods;
-  my @items = $class->_build_items($req, \@cart_prods);
-  my $item_index = -1;
-  my @options;
-  my $option_index;
-  
   my $cart = $req->cart("cart");
-  $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); 
+  # $req->session->{custom} ||= {};
+  # my %custom_state = %{$req->session->{custom}};
+
+  # $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $req->cfg);
   $msg = '' unless defined $msg;
   $msg = escape_html($msg);
 
@@ -111,7 +119,7 @@ sub req_cart {
   my %acts;
   %acts =
     (
-     $cust_class->cart_actions(\%acts, $cart->items, $cart->products,
+     $cust_class->cart_actions(\%acts, scalar $cart->items, scalar $cart->products,
                               $cart->custom_state, $req->cfg),
      shop_cart_tags(\%acts, $cart, $req, 'cart'),
      basic_tags(\%acts),
@@ -145,6 +153,7 @@ sub req_emptycart {
 
   my $old = $req->session->{cart};;
   $req->session->{cart} = [];
+  delete $req->session->{cart_coupon_code};
 
   my $refresh = $req->cgi->param('r');
   unless ($refresh) {
@@ -686,13 +695,6 @@ sub req_checkout {
      ifNeedDelivery => $need_delivery,
     );
   $req->session->{custom} = $cart->custom_state;
-  my $tmp = $acts{total};
-  $acts{total} =
-    sub {
-        my $total = &$tmp();
-        $total += $shipping_cost if $total and $shipping_cost;
-        return $total;
-    };
 
   return $req->response('checkoutnew', \%acts);
 }
@@ -910,6 +912,8 @@ sub req_show_payment {
   $errors and $payment = $cgi->param('paymentType');
   defined $payment or $payment = $payment_types[0];
 
+  $cart->set_shipping_cost($order->{shipping_cost});
+
   my %acts;
   %acts =
     (
@@ -938,6 +942,7 @@ sub req_show_payment {
   }
   $req->set_variable(ordercart => $cart);
   $req->set_variable(order => $order);
+  $req->set_variable(is_order => !!$order_id);
 
   return $req->response('checkoutpay', \%acts);
 }
@@ -1378,16 +1383,6 @@ sub req_orderdone {
        $items[$item_index]{units} * $items[$item_index]{$what};
      },
      order => sub { escape_html($order->{$_[0]}) },
-     _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]}) },
@@ -1503,7 +1498,13 @@ sub req_recalc {
 
   $class->update_quantities($req);
   $req->session->{order_info_confirmed} = 0;
-  return $class->req_cart($req);
+
+  my $refresh = $req->cgi->param('r');
+  unless ($refresh) {
+    $refresh = $req->user_url(shop => 'cart');
+  }
+
+  return $req->get_refresh($refresh);
 }
 
 sub req_recalculate {
@@ -1701,16 +1702,18 @@ sub update_quantities {
       }
     }
   }
-  my ($coupon) = $cgi->param("coupon");
-  if (defined $coupon) {
-    $session->{cart_coupon} = $coupon;
-  }
   @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;
+
+  my ($coupon) = $cgi->param("coupon");
+  if (defined $coupon) {
+    my $cart = $req->cart;
+    $cart->set_coupon_code($coupon);
+  }
 }
 
 sub _build_items {
@@ -2251,15 +2254,7 @@ sub _refresh_cart {
 
 1;
 
-__END__
-
-=head1 NAME
-
-shop.pl - implements the shop for BSE
-
-=head1 DESCRIPTION
-
-shop.pl implements the shop for BSE.
+=back
 
 =head1 TAGS
 
@@ -2352,22 +2347,6 @@ subscription.
 
 =back
 
-You can also use "|format" at the end of a field to perform some
-simple formatting.  Eg. <:order total |m6:> or <:order id |%06d:>.
-
-=over 4
-
-=item m<number>
-
-Formats the value as a <number> wide money value.
-
-=item %<format>
-
-Performs sprintf() formatting on the value.  Eg. %06d will format 25
-as 000025.
-
-=back
-
 =head2 Mailed order tags
 
 These tags are used in the emails sent to the user to confirm an order
@@ -2375,27 +2354,39 @@ and in the encrypted copy sent to the site administrator:
 
 =over 4
 
-=item iterate ... items
+=item *
+
+C<iterate> ... C<items>
 
 Iterates over the items in the order.
 
-=item item I<field>
+=item *
+
+C<item> I<field>
 
 Access to the given field in the order item.
 
-=item product I<field>
+=item *
+
+C<product> I<field>
 
 Access to the product field for the current order item.
 
-=item order I<field>
+=item *
+
+C<order> I<field>
 
 Access to fields of the order.
 
-=item extended I<field>
+=item *
+
+C<extended> I<field>
 
 The product of the I<field> in the current item and it's quantity.
 
-=item money I<tag> I<parameters>
+=item *
+
+C<money> I<tag> I<parameters>
 
 Formats the given field as a money value.
 
@@ -2406,15 +2397,21 @@ The mail generation template can use extra formatting specified with
 
 =over 4
 
-=item m<number>
+=item *
+
+m<number>
 
 Format the value as a I<number> wide money value.
 
-=item %<format>
+=item *
+
+%<format>
 
 Performs sprintf formatting on the value.
 
-=item <number>
+=item *
+
+<number>
 
 Left justifies the value in a I<number> wide field.
 
@@ -2423,13 +2420,17 @@ Left justifies the value in a I<number> wide field.
 The order email sent to the site administrator has a couple of extra
 fields:
 
-=over 4
+=over
 
-=item cardNumber
+=item *
+
+cardNumber
 
 The credit card number of the user's credit card.
 
-=item cardExpiry
+=item *
+
+cardExpiry
 
 The entered expiry date for the user's credit card.
 
@@ -2441,138 +2442,118 @@ These names can be used with the <: order ... :> tag.
 
 Monetary values should typically be used with <:money order ...:>
 
-=over 4
-
-=item id
-
-The order id or order number.
-
-=item delivFirstName
-
-=item delivLastName
-
-=item delivStreet
-
-=item delivSuburb
-
-=item delivState
-
-=item delivPostCode
-
-=item delivCountry
-
-Delivery information for the order.
-
-=item billFirstName
-
-=item billLastName
-
-=item billStreet
+=over
 
-=item billSuburb
+=item *
 
-=item billState
+id
 
-=item billPostCode
+The order id or order number.
 
-=item billCountry
+=item *
 
-Billing information for the order.
+delivFirstName, delivLastName, delivStreet, delivSuburb, delivState,
+delivPostCode, delivCountry - Delivery information for the order.
 
-=item telephone
+=item *
 
-=item facsimile
+billFirstName, billLastName, billStreet, billSuburb, billState,
+billPostCode, billCountry - Billing information for the order.
 
-=item emailAddress
+=item *
 
-Contact information for the order.
+telephone, facsimile, emailAddress - Contact information for the
+order.
 
-=item total
+=item *
 
-Total price of the order.
+total - Total price of the order.
 
-=item wholesaleTotal
+=item *
 
-Wholesale cost of the total.  Your costs, if you entered wholesale
-prices for the products.
+wholesaleTotal - Wholesale cost of the total.  Your costs, if you
+entered wholesale prices for the products.
 
-=item gst
+=item *
 
-GST (in Australia) payable on the order, if you entered GST for the products.
+gst - GST (in Australia) payable on the order, if you entered GST for
+the products.
 
-=item orderDate
+=item *
 
-When the order was made.
+orderDate - When the order was made.
 
-=item filled
+=item *
 
-Whether or not the order has been filled.  This can be used with the
-order_filled target in shopadmin.pl for tracking filled orders.
+filled - Whether or not the order has been filled.  This can be used
+with the order_filled target in shopadmin.pl for tracking filled
+orders.
 
-=item whenFilled
+=item *
 
-The time and date when the order was filled.
+whenFilled - The time and date when the order was filled.
 
-=item whoFilled
+=item *
 
-The user who marked the order as filled.
+whoFilled - The user who marked the order as filled.
 
-=item paidFor
+=item *
 
-Whether or not the order has been paid for.  This can be used with a
-custom purchasing handler to mark the product as paid for.  You can
-then filter the order list to only display paid for orders.
+paidFor - Whether or not the order has been paid for.  This can be
+used with a custom purchasing handler to mark the product as paid for.
+You can then filter the order list to only display paid for orders.
 
-=item paymentReceipt
+=item *
 
-A custom payment handler can fill this with receipt information.
+paymentReceipt - A custom payment handler can fill this with receipt
+information.
 
-=item randomId
+=item *
 
-Generated by the prePurchase target, this can be used as a difficult
-to guess identifier for orders, when working with custom payment
-handlers.
+randomId - Generated by the prePurchase target, this can be used as a
+difficult to guess identifier for orders, when working with custom
+payment handlers.
 
-=item cancelled
+=item *
 
-This can be used by a custom payment handler to mark an order as
-cancelled if the user starts processing an order without completing
-payment.
+cancelled - This can be used by a custom payment handler to mark an
+order as cancelled if the user starts processing an order without
+completing payment.
 
 =back
 
 =head2 Order item fields
 
-=over 4
+=over
 
-=item productId
+=item *
 
-The product id of this item.
+productId - The product id of this item.
 
-=item orderId 
+=item *
 
-The order Id.
+orderId - The order Id.
 
-=item units
+=item *
 
-The number of units for this item.
+units - The number of units for this item.
 
-=item price
+=item *
 
-The price paid for the product.
+price - The price paid for the product.
 
-=item wholesalePrice
+=item *
 
-The wholesale price for the product.
+wholesalePrice - The wholesale price for the product.
 
-=item gst
+=item *
 
-The gst for the product.
+gst - The gst for the product.
 
-=item options
+=item *
 
-A comma separated list of options specified for this item.  These
-correspond to the option names in the product.
+options - A comma separated list of options specified for this item.
+These correspond to the option names in the product.
 
 =back
 
@@ -2585,46 +2566,51 @@ tags:
 
 =over
 
-=item iterator ... options
+=item *
+
+C<iterator> ... <options>
 
 within an item, iterates over the options for this item in the cart.
 Sets the item tag.
 
-=item option field
+=item *
+
+C<option> I<field>
 
 Retrieves the given field from the option, possible field names are:
 
 =over
 
-=item id
+=item *
 
-The type/identifier for this option.  eg. msize for a male clothing
-size field.
+id - The type/identifier for this option.  eg. msize for a male
+clothing size field.
 
-=item value
+=item *
 
-The underlying value of the option, eg. XL.
+value - The underlying value of the option, eg. XL.
 
-=item desc
+=item *
 
-The description of the field from the product options hash.  If the
-description isn't defined this is the same as the id. eg. Size.
+desc - The description of the field from the product options hash.  If
+the description isn't defined this is the same as the id. eg. Size.
 
-=item label
+=item *
 
-The description of the value from the product options hash.
+label - The description of the value from the product options hash.
 eg. "Extra large".
 
 =back
 
-=item ifOptions
+=item *
 
-A conditional tag, true if the current cart item has any options.
+ifOptions - A conditional tag, true if the current cart item has any
+options.
 
-=item options
+=item *
 
-A simple rendering of the options as a parenthesized comma-separated
-list.
+options - A simple rendering of the options as a parenthesized
+comma-separated list.
 
 =back
 
index 1873020d9ae7f3d81aabb2a9e5a14628e53cd9e2..ab7877afc793008b8ade56ef76795fdf19d6213a 100644 (file)
@@ -218,6 +218,24 @@ description: Invalid shipping method, $1 is the supplied method id
 id: bse/admin/shop/saveorder/badstage
 description: Invalid order stage, $1 is the supplied stage id
 
+id: bse/admin/shop/coupons/
+description: Message for coupon management
+
+id: bse/admin/shop/coupons/adddup
+description: Displayed on a duplicate code when adding a coupon (%1 - coupon code)
+
+id: bse/admin/shop/coupons/add
+description: Flashed when a coupon is added successfully (%1 coupon)
+
+id: bse/admin/shop/coupons/editdup
+description: Displayed on a duplicate code when editing a coupon (%1 - coupon code)
+
+id: bse/admin/shop/coupons/save
+description: Flashed when a coupon is saved (%1 coupon)
+
+id: bse/admin/shop/coupons/delete
+description: Flashed when a coupon is deleted (%1 coupon code)
+
 id: bse/admin/logon/
 description: Logon tool messages
 
index d4ae6b61fc1e2d11fa2996eec177dcfb5a5b0760..194b4a27ee923238d6a11f6ce1c1848ee5c45ef4 100644 (file)
@@ -157,6 +157,21 @@ message: Unknown shipping method '%1:s'
 id: bse/admin/shop/saveorder/badstage
 message: Unknown order stage '%1:s'
 
+id: bse/admin/shop/coupons/adddup
+message: Duplicate code '%1:s'
+
+id: bse/admin/shop/coupons/add
+message: Added new coupon '%1:{code}s'
+
+id: bse/admin/shop/coupons/editdup
+message: Duplicate code '%1:s'
+
+id: bse/admin/shop/coupons/save
+message: Saved coupon '%1:{code}s'
+
+id: bse/admin/shop/coupons/delete
+message: Deleted coupon '%1:s'
+
 id: bse/admin/logon/logoff
 message: Administrative user '%1:s' logged off
 
index dc4b8bb1bcba8b147bfad8882fe1ff4af1f6bac0..727bcb61f9d08264feb8cc7d249359994b847830 100644 (file)
@@ -672,3 +672,11 @@ sql_statement: <<SQL
 select bs.*, us.* from admin_base bs, admin_users us
   where bs.id = us.base_id and us.password_type = ?
 SQL
+
+name: PriceTiers.forCoupon
+sql_statement: <<SQL
+select t.*
+from bse_price_tiers t, bse_coupon_tiers c
+where c.coupon_id = ?
+  and c.tier_id = t.id
+SQL
diff --git a/site/templates/admin/coupons/add.tmpl b/site/templates/admin/coupons/add.tmpl
new file mode 100644 (file)
index 0000000..f0b76ca
--- /dev/null
@@ -0,0 +1,26 @@
+<:wrap admin/base.tmpl title => "Shop: Add Coupon Code", bodyid => "coupon_add":>
+<h1>Shop: Add Coupon Code</h1>
+<p>| <a href="<:= cfg.admin_url("menu") :>">Admin Menu</a> |
+<:.if request.user_can("bse_shop_coupon_list") -:>
+<a href="<:= cfg.admin_url2("shopadmin", "coupon_list") :>">Return to coupon list</a> |
+<:.end if-:>
+</p>
+<:.call "messages"-:>
+<:.set object = coupon -:>
+<form action="<:= cfg.admin_url("shopadmin") :>" method="post">
+  <:csrfp admin_bse_coupon_add hidden:>
+  <fieldset>
+    <legend>Coupon Details</legend>
+    <:.call "field", "name":"code", "autofocus":1 :>
+    <:.call "field", "name":"description" :>
+    <:.call "field", "name":"release" :>
+    <:.call "field", "name":"expiry" :>
+    <:.call "field", "name":"discount_percent" :>
+    <:.call "field", "name":"untiered" :>
+    <:.call "field", "name":"campaign" :>
+  </fieldset>
+  <:.call "fieldset", "name":"tiers" :>
+  <p class="buttons">
+    <input type="submit" name="a_coupon_add" value="Add Coupon">
+  </p>
+</form>
diff --git a/site/templates/admin/coupons/delete.tmpl b/site/templates/admin/coupons/delete.tmpl
new file mode 100644 (file)
index 0000000..8978066
--- /dev/null
@@ -0,0 +1,27 @@
+<:wrap admin/base.tmpl title => "Shop: Delete Coupon Code", bodyid => "coupon_delete":>
+<h1>Shop: Delete Coupon <span><:= coupon.code -:></span></h1>
+<p>| <a href="<:= cfg.admin_url("menu") :>">Admin Menu</a> |
+<:.if request.user_can("bse_shop_coupon_list") -:>
+<a href="<:= cfg.admin_url2("shopadmin", "coupon_list") :>">Return to coupon list</a> |
+<:.end if-:>
+</p>
+<:.call "messages"-:>
+<:.set object = coupon -:>
+<form action="<:= cfg.admin_url("shopadmin") :>" method="post">
+  <:csrfp admin_bse_coupon_delete hidden:>
+  <input type="hidden" name="id" value="<:= coupon.id :>">
+  <p class="warning">Deleting a coupon cannot be reversed.</p>
+  <fieldset>
+    <legend>Coupon Details</legend>
+    <:.call "fieldro", "name":"code", "autofocus":1 :>
+    <:.call "fieldro", "name":"description" :>
+    <:.call "fieldro", "name":"release" :>
+    <:.call "fieldro", "name":"expiry" :>
+    <:.call "fieldro", "name":"discount_percent" :>
+    <:.call "fieldro", "name":"campaign" :>
+  </fieldset>
+  <:.call "fieldsetro", "name":"tiers" :>
+  <p class="buttons">
+    <input type="submit" name="a_coupon_delete" value="Delete Coupon">
+  </p>
+</form>
diff --git a/site/templates/admin/coupons/edit.tmpl b/site/templates/admin/coupons/edit.tmpl
new file mode 100644 (file)
index 0000000..d91ed0b
--- /dev/null
@@ -0,0 +1,27 @@
+<:wrap admin/base.tmpl title => "Shop: Edit Coupon Code", bodyid => "coupon_edit":>
+<h1>Shop: Edit Coupon <span><:= coupon.code -:></span></h1>
+<p>| <a href="<:= cfg.admin_url("menu") :>">Admin Menu</a> |
+<:.if request.user_can("bse_shop_coupon_list") -:>
+<a href="<:= cfg.admin_url2("shopadmin", "coupon_list") :>">Return to coupon list</a> |
+<:.end if-:>
+</p>
+<:.call "messages"-:>
+<:.set object = coupon -:>
+<form action="<:= cfg.admin_url("shopadmin") :>" method="post">
+  <:csrfp admin_bse_coupon_edit hidden:>
+  <input type="hidden" name="id" value="<:= coupon.id :>">
+  <fieldset>
+    <legend>Coupon Details</legend>
+    <:.call "field", "name":"code", "autofocus":1 :>
+    <:.call "field", "name":"description" :>
+    <:.call "field", "name":"release" :>
+    <:.call "field", "name":"expiry" :>
+    <:.call "field", "name":"discount_percent" :>
+    <:.call "field", "name":"untiered" :>
+    <:.call "field", "name":"campaign" :>
+  </fieldset>
+  <:.call "fieldset", "name":"tiers" :>
+  <p class="buttons">
+    <input type="submit" name="a_coupon_save" value="Save Coupon">
+  </p>
+</form>
diff --git a/site/templates/admin/coupons/list.tmpl b/site/templates/admin/coupons/list.tmpl
new file mode 100644 (file)
index 0000000..54a0adf
--- /dev/null
@@ -0,0 +1,101 @@
+<:wrap admin/base.tmpl title => "Shop: Coupon List", bodyid => "coupon_list":>
+<h1>Shop: Coupon List</h1>
+<p>| <a href="<:= cfg.admin_url("menu") :>">Admin Menu</a> |
+<:.if request.user_can("bse_shop_coupon_add") -:>
+<a href="<:= cfg.admin_url2("shopadmin", "coupon_addform") :>">Add a coupon</a> |
+<:.end if-:>
+</p>
+
+<:.call "messages"-:>
+<form action="<:= cfg.admin_url("shopadmin") :>">
+<:.set object = 0 -:>
+<:.set errors = {} -:>
+<fieldset>
+  <legend>Filter/sort</legend>
+  <:.call "inlinefield",
+    "name":"all",
+    "field":{
+              "description": "Show all",
+             "htmltype": "checkbox",
+             "default": coupons_all
+            } -:>
+  <:.call "inlinefield",
+     "name":"sort",
+     "field":{
+               "description": "Sort",
+              "htmltype": "select",
+              "default": coupons_sort,
+              "select":
+                 {
+                  "values":
+                    [
+                      { "id": "expiry", "desc":"By Expiry date" },
+                      { "id": "release", "desc":"By Release date" },
+                      { "id": "code", "desc":"By code" }
+                    ],
+                  "id":"id",
+                  "label":"desc"
+                }
+             } -:>
+  <:.call "inlinefield",
+     "name": "scode",
+     "field": {
+                "description": "Search code",
+                "units": "(=code to search for exact code, otherwise prefix)",
+               "maxlength": 40,
+               "size": 20
+              } -:>
+</fieldset>
+<p class="buttons"><input type="submit" name="a_coupon_list" value="Sort/Filtter"></p>
+</form>
+
+<table>
+  <tr>
+    <th class="col_id">Id</th>
+    <th class="col_code">Code</th>
+    <th class="col_description">Description</th>
+    <th class="col_release">Release</th>
+    <th class="col_expiry">Expires</th>
+    <th class="col_discount">Discount</th>
+    <th class="col_tiers">Tiers</th>
+    <th class="col_campaign">Campaign</th>
+    <th class="col_actions"></th>
+  </tr>
+
+<:.if coupons.size -:>
+  <:.for coupon in coupons -:>
+    <:.set classes = [ loop.even ? "even" : "odd"  ] -:>
+    <:.if coupon.is_expired -:>
+       <:% classes.push("expired") -:>
+    <:.elsif coupon.is_released -:>
+       <:% classes.push("released") -:>
+    <:.end if -:>
+    <:.set tier_names = [] -:>
+    <:.for tier in [ coupon.tier_objects ] -:>
+      <:% tier_names.push(tier.description) -:>
+    <:.end for -:>
+  <tr class="<:= classes.join(" ") :>">
+    <td class="col_id"><a href="<:= cfg.admin_url2("shopadmin", "details", { "id": coupon.id }) :>"><:= coupon.id :></a></td>
+    <td class="col_code"><:= coupon.code :></td>
+    <td class="col_description"><:= coupon.description :></td>
+    <td class="col_release"><:= bse.date("%d/%m/%Y", coupon.release) :></td>
+    <td class="col_expiry"><:= bse.date("%d/%m/%Y", coupon.expiry) :></td>
+    <td class="col_discount"><:= coupon.discount_percent :>%</td>
+    <td class="col_tiers"><:= tier_names.size ? tier_names.join(", ") : "(none)" :></td>
+    <td class="col_campaign"><:= coupon.campaign :></td>
+    <td class="col_actions">
+      <:.if request.user_can("bse_shop_coupon_edit") -:>
+        <a href="<:= cfg.admin_url2("shopadmin", "coupon_edit", { "id": coupon.id }) :>">Edit</a>
+      <:.end if -:>
+      <:.if request.user_can("bse_shop_coupon_delete") -:>
+        <a href="<:= cfg.admin_url2("shopadmin", "coupon_deleteform", { "id": coupon.id }) :>">Delete</a>
+      <:.end if -:>
+    </td>
+  </tr>
+  <:.end for -:>
+<:.else -:>
+  <tr class="nothing">
+    <td colspan="8">No coupons are currently defined</td>
+  </tr>
+<:.end if -:>
+</table>
index a04e8cfdf27be531194d377b85264bda2da75bec..b0ed790f4d28476fa3ffd54dee6a9461a69d9f0f 100644 (file)
@@ -94,6 +94,10 @@ href="<:= cfg.admin_url2("shopadmin", "order_list_filled", { "template":"order_l
 
 <li><a href="<:= cfg.admin_url("add", { "parentid":3 }) | html :>">Add catalog</a></li>
 
+<:.if request.user_can("bse_shop_coupon_list") -:>
+<li><a href="<:= cfg.admin_url2("shopadmin", "coupon_list") :>">Coupons</a></li>
+<:.end if -:>
+
 </ul>
 
 <:.if request.user_can("bse_admin_import") :>
index 01a6d99956424d5228aa80a5687f51a17a7a633b..97ddd263b0b55f487b9852e512cbbe05223e404d 100644 (file)
   </table>
   <table border="0" cellspacing="0" cellpadding="1" width="100%" bgcolor="#666666">
     <tr valign="middle" align="center"> 
-      <td width="100%"> 
+      <td width="100%">
         <table width="100%" border="0" cellspacing="1" cellpadding="1" bgcolor="#EEEEEE">
           <tr valign="middle" align="center" bgcolor="#666666"> 
+<:.set cart = request.cart -:>
+<:.if cart.coupon_valid and !cart.coupon_active -:>
+            <td></td>
+<:.end if -:>
             <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>
              <:.set options = item.option_list -:>
              <:.set session = item.session -:>
           <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
+<:.if cart.coupon_valid and !cart.coupon_active -:>
+            <td>
+<:= item.coupon_applies ? "Y" : "N" -:>
+           </td>
+<:.end if -:>
             <td width="100%" align="left"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><a href="<:= item.link | html:>"><:= item.product.description | html :> <:.if options.size:>(<:.for option in options:><:= loop.index ? ", " : "" :><:= option.desc | html:>: 
               <:= option.display |html :><:.end for:>)<:.end if -:></a><:.if item.session_id:>(session at <:= session.location.description | html:> <:= bse.date("%H:%M %d/%m/%Y", session.when_at) -:>)<:.end if:></font></td>
             <td nowrap align="center"> 
     </tr>
   </table>
   <table width="100%" border="0" cellspacing="0" cellpadding="0">
+    <tr>
+      <td>Coupon code: <input type="text" name="coupon" value="<:= cart.coupon_code -:>">
+<:.if cart.coupon_active -:>
+Coupon active
+<:.elsif cart.coupon_valid -:>
+<:.if request.siteuser -:>
+Your cart contains items the code isn't valid for
+<:.else -:>
+You need to logon
+<:.end if -:>
+<:.elsif cart.coupon_code ne "" -:>
+Unknown coupon code
+<:.end if -:>
+</td>
+<:.if cart.coupon_active -:>
+      <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>DISCOUNT</b></font></td>
+      <td height="20" bgcolor="#666666">&nbsp;</td>
+<:.else -:>
+      <td colspan="6"></td>
+<:.end if -:>
+    </tr>
+<:.if cart.coupon_active -:>
+    <tr>
+      <td colspan="2">&nbsp;</td>
+      <td height="20" style="border-left: 1px solid #666666">&nbsp;</td>
+      <td align="CENTER">$<:= bse.number("money", cart.product_cost_discount) -:></td>
+      <td height="20" style="border-right: 1px solid #666666">&nbsp;</td>
+    </tr>
+<:.end if -:>
     <tr> 
       <td>&nbsp;</td>
       <td height="20">&nbsp;</td>
index 9f1f055bdfba90e93202fa56fc44ef8142ce6bea..40c3055ff74290307ca8f845414e3c6adcc17844 100644 (file)
@@ -78,21 +78,39 @@ function BSE_validateForm {
     <td width="100%"> 
       <table width="100%" border="0" cellspacing="1" cellpadding="2" bgcolor="#EEEEEE">
         <tr valign="middle" align="center" bgcolor="#666666"> 
+<:.set cart = request.cart -:>
+<:.if cart.coupon_valid and !cart.coupon_active -:>
+            <td></td>
+<:.end if -:>
           <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;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><a href="<:item link:>"><:item 
-            description:>  <:options:></a><:ifItem session_id:>(session at <:location description:> <:date "%H:%M %d/%m/%Y" session when_at:>)<:or:><:eif:></font></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 price :></b></font></td>
-        </tr>
-        <:iterator end items:> 
+         <:-.set items = request.cart.items -:>
+          <:.if items.size -:>
+           <:.for item in items -:>
+             <:.set options = item.option_list -:>
+             <:.set session = item.session -:>
+          <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
+<:.if cart.coupon_valid and !cart.coupon_active -:>
+            <td>
+<:= item.coupon_applies ? "Y" : "N" -:>
+           </td>
+<:.end if -:>
+            <td width="100%" align="left"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><a href="<:= item.link | html:>"><:= item.product.description | html :> <:.if options.size:>(<:.for option in options:><:= loop.index ? ", " : "" :><:= option.desc | html:>: 
+              <:= option.display |html :><:.end for:>)<:.end if -:></a><:.if item.session_id:>(session at <:= session.location.description | html:> <:= bse.date("%H:%M %d/%m/%Y", session.when_at) -:>)<:.end if:></font></td>
+            <td nowrap align="center"> 
+              <input type="text" name="quantity_<:= loop.index :>" size="2" value="<:= item.units :>">
+            </td>
+            <td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:= bse.number("money", item.price) | html :></b></font></td>
+          </tr>
+           <:.end for -:>
+          <:.else -:>
+          <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
+            <td width="100%" height="20" align="center" colspan="4"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">You have no items in your shopping cart!</font></td>
+           </tr>
+          <:.end if -:>
         <:if Shipping_cost:>
         <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
           <td colspan=2 width="100%" align="left">&nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2">Shipping charges (for <:shipping_method:><:if Delivery_in:>, delivery in <:delivery_in:> days<:or Delivery_in:><:eif Delivery_in:>)</font></td>
@@ -110,6 +128,38 @@ function BSE_validateForm {
   </tr>
 </table>
 <table width="100%" border="0" cellspacing="0" cellpadding="0">   
+    <tr>
+      <td>Coupon code: <input type="text" name="coupon" value="<:= cart.coupon_code -:>">
+<:.if cart.coupon_active -:>
+Coupon active
+<:.elsif cart.coupon_valid -:>
+<:.if request.siteuser -:>
+Your cart contains items the code isn't valid for
+<:.else -:>
+You need to logon
+<:.end if -:>
+<:.elsif cart.coupon_code ne "" -:>
+Unknown coupon code
+<:.end if -:>
+</td>
+<:.if cart.coupon_active -:>
+      <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>DISCOUNT</b></font></td>
+      <td height="20" bgcolor="#666666">&nbsp;</td>
+<:.else -:>
+      <td colspan="6"></td>
+<:.end if -:>
+    </tr>
+<:.if cart.coupon_active -:>
+    <tr>
+      <td colspan="2">&nbsp;</td>
+      <td height="20" style="border-left: 1px solid #666666">&nbsp;</td>
+      <td align="CENTER">$<:= bse.number("money", cart.product_cost_discount) -:></td>
+      <td height="20" style="border-right: 1px solid #666666">&nbsp;</td>
+    </tr>
+<:.end if -:>
   <tr> 
     <td>&nbsp;</td>
     <td height="20">&nbsp;</td>
index 2c8878e21d5c25e5d09d13999373f7fbe0c7256e..26c076292aece551d67a7d34606a2df00ee195cb 100644 (file)
           <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 
-            description:>  <: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 price :></b></font></td>
-        </tr>
-        <:iterator end items:> 
+         <:-.set items = [ ordercart.items ] -:>
+          <:.if items.size -:>
+           <:.for item in items -:>
+             <:.set options = item.option_list -:>
+             <:.set session = item.session -:>
+          <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
+            <td width="100%" align="left"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><a href="<:= item.link | html:>"><:= item.product.description | html :> <:.if options.size:>(<:.for option in options:><:= loop.index ? ", " : "" :><:= option.desc | html:>: 
+              <:= option.display |html :><:.end for:>)<:.end if -:></a><:.if item.session_id:>(session at <:= session.location.description | html:> <:= bse.date("%H:%M %d/%m/%Y", session.when_at) -:>)<:.end if:></font></td>
+            <td nowrap align="center"> 
+              <input type="text" name="quantity_<:= loop.index :>" size="2" value="<:= item.units :>">
+            </td>
+            <td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:= bse.number("money", item.price) | html :></b></font></td>
+          </tr>
+           <:.end for -:>
+          <:.else -:>
+          <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
+            <td width="100%" height="20" align="center" colspan="4"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">You have no items in your shopping cart!</font></td>
+           </tr>
+          <:.end if -:>
         <:if Shipping_cost:>
         <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
           <td colspan=2 width="100%" align="left">&nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2">Shipping charges (for <:shipping_method:><:if Delivery_in:>, delivery in <:delivery_in:> days<:or Delivery_in:><:eif Delivery_in:>)</font></td>
     </td>
   </tr>
 </table>
-<table width="100%" border="0" cellspacing="0" cellpadding="0">   
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+<:.if is_order ? order.coupon_code ne "" : ordercart.coupon_active -:>
+    <tr>
+      <td>Coupon code: <:= ordercart.coupon_code -:>
+</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>DISCOUNT</b></font></td>
+      <td height="20" bgcolor="#666666">&nbsp;</td>
+    </tr>
+    <tr>
+      <td colspan="2">&nbsp;</td>
+      <td height="20" style="border-left: 1px solid #666666">&nbsp;</td>
+      <td align="CENTER">$<:= bse.number("money", ordercart.product_cost_discount) -:></td>
+      <td height="20" style="border-right: 1px solid #666666">&nbsp;</td>
+    </tr>
+<:.end if -:>
   <tr> 
     <td>&nbsp;</td>
     <td height="20">&nbsp;</td>
index cd71037d6f5310066be4036dd5ad9fa82ab5dc19..ad6680c32f1705bb42d1775fbfc98a5cc58a031d 100644 (file)
@@ -1,10 +1,10 @@
 Table admin_base
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column type;char(1);NO;NULL;
 Index PRIMARY;1;[id]
 Table admin_groups
-Engine MyISAM
+Engine InnoDB
 Column base_id;int(11);NO;NULL;
 Column name;varchar(80);NO;NULL;
 Column description;varchar(255);NO;NULL;
@@ -13,18 +13,18 @@ Column template_set;varchar(80);NO;;
 Index PRIMARY;1;[base_id]
 Index name;1;[name]
 Table admin_membership
-Engine MyISAM
+Engine InnoDB
 Column user_id;int(11);NO;NULL;
 Column group_id;int(11);NO;NULL;
 Index PRIMARY;1;[user_id;group_id]
 Table admin_perms
-Engine MyISAM
+Engine InnoDB
 Column object_id;int(11);NO;NULL;
 Column admin_id;int(11);NO;NULL;
 Column perm_map;varchar(255);YES;NULL;
 Index PRIMARY;1;[object_id;admin_id]
 Table admin_users
-Engine MyISAM
+Engine InnoDB
 Column base_id;int(11);NO;NULL;
 Column logon;varchar(60);NO;NULL;
 Column name;varchar(255);NO;NULL;
@@ -35,7 +35,7 @@ Column lockout_end;datetime;YES;NULL;
 Index PRIMARY;1;[base_id]
 Index logon;1;[logon]
 Table article
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column parentid;int(11);NO;0;
 Column displayOrder;int(11);NO;0;
@@ -89,7 +89,7 @@ Index article_displayOrder_index;0;[displayOrder]
 Index article_level_index;0;[level;id]
 Index article_parentId_index;0;[parentid]
 Table article_files
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column articleId;int(11);NO;NULL;
 Column displayName;varchar(255);NO;;
@@ -111,14 +111,14 @@ Column category;varchar(20);NO;;
 Column file_handler;varchar(20);NO;;
 Index PRIMARY;1;[id]
 Table bse_admin_ui_state
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column user_id;int(11);NO;NULL;
 Column name;varchar(80);NO;NULL;
 Column val;text;NO;NULL;
 Index PRIMARY;1;[id]
 Table bse_article_file_meta
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column file_id;int(11);NO;NULL;
 Column name;varchar(20);NO;NULL;
@@ -128,12 +128,12 @@ Column appdata;int(11);NO;0;
 Index PRIMARY;1;[id]
 Index file_name;1;[file_id;name]
 Table bse_article_groups
-Engine MyISAM
+Engine InnoDB
 Column article_id;int(11);NO;NULL;
 Column group_id;int(11);NO;NULL;
 Index PRIMARY;1;[article_id;group_id]
 Table bse_audit_log
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column when_at;datetime;NO;NULL;
 Column facility;varchar(20);NO;bse;
@@ -152,7 +152,7 @@ Index PRIMARY;1;[id]
 Index ba_what;0;[facility;component;module;function]
 Index ba_when;0;[when_at]
 Table bse_background_tasks
-Engine MyISAM
+Engine InnoDB
 Column id;varchar(20);NO;NULL;
 Column description;varchar(80);NO;NULL;
 Column modname;varchar(80);NO;;
@@ -167,8 +167,29 @@ Column last_started;datetime;YES;NULL;
 Column last_completion;datetime;YES;NULL;
 Column long_desc;text;YES;NULL;
 Index PRIMARY;1;[id]
+Table bse_coupon_tiers
+Engine InnoDB
+Column id;int(11);NO;NULL;auto_increment
+Column coupon_id;int(11);NO;NULL;
+Column tier_id;int(11);NO;NULL;
+Index PRIMARY;1;[id]
+Index coupon_id;1;[coupon_id;tier_id]
+Index tier_id;0;[tier_id]
+Table bse_coupons
+Engine InnoDB
+Column id;int(11);NO;NULL;auto_increment
+Column code;varchar(40);NO;NULL;
+Column description;text;NO;NULL;
+Column release;date;NO;NULL;
+Column expiry;date;NO;NULL;
+Column discount_percent;double;NO;NULL;
+Column campaign;varchar(20);NO;NULL;
+Column last_modified;datetime;NO;NULL;
+Column untiered;int(11);NO;0;
+Index PRIMARY;1;[id]
+Index codes;1;[code]
 Table bse_file_access_log
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column when_at;datetime;NO;NULL;
 Column siteuser_id;int(11);NO;NULL;
@@ -189,7 +210,7 @@ Index by_file;0;[file_id]
 Index by_user;0;[siteuser_id;when_at]
 Index by_when_at;0;[when_at]
 Table bse_file_notifies
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column owner_type;char(1);NO;NULL;
 Column owner_id;int(11);NO;NULL;
@@ -199,7 +220,7 @@ Index PRIMARY;1;[id]
 Index by_owner;0;[owner_type;owner_id]
 Index by_time;0;[owner_type;when_at]
 Table bse_file_subscriptions
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;
 Column siteuser_id;int(11);NO;NULL;
 Column category;varchar(20);NO;NULL;
@@ -237,7 +258,7 @@ Column expires;datetime;NO;NULL;
 Index PRIMARY;1;[id]
 Index ip_address;1;[ip_address;type]
 Table bse_locations
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column description;varchar(255);NO;NULL;
 Column room;varchar(40);NO;NULL;
@@ -258,7 +279,7 @@ Column admin_notes;text;NO;NULL;
 Column disabled;int(11);NO;0;
 Index PRIMARY;1;[id]
 Table bse_msg_base
-Engine MyISAM
+Engine InnoDB
 Column id;varchar(80);NO;NULL;
 Column description;text;NO;NULL;
 Column formatting;varchar(5);NO;none;
@@ -266,14 +287,14 @@ Column params;varchar(40);NO;;
 Column multiline;int(11);NO;0;
 Index PRIMARY;1;[id]
 Table bse_msg_defaults
-Engine MyISAM
+Engine InnoDB
 Column id;varchar(80);NO;NULL;
 Column language_code;varchar(10);NO;;
 Column priority;int(11);NO;0;
 Column message;text;NO;NULL;
 Index PRIMARY;1;[id;language_code;priority]
 Table bse_msg_managed
-Engine MyISAM
+Engine InnoDB
 Column id;varchar(80);NO;NULL;
 Column language_code;varchar(10);NO;;
 Column message;text;NO;NULL;
@@ -290,7 +311,7 @@ Column display_order;int(11);NO;NULL;
 Index PRIMARY;1;[id]
 Index item_order;0;[order_item_id;display_order]
 Table bse_owned_files
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column owner_type;char(1);NO;NULL;
 Column owner_id;int(11);NO;NULL;
@@ -307,7 +328,7 @@ Column filekey;varchar(80);NO;;
 Index PRIMARY;1;[id]
 Index by_owner_category;0;[owner_type;owner_id;category]
 Table bse_price_tier_prices
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column tier_id;int(11);NO;NULL;
 Column product_id;int(11);NO;NULL;
@@ -315,7 +336,7 @@ Column retailPrice;int(11);NO;NULL;
 Index PRIMARY;1;[id]
 Index tier_product;1;[tier_id;product_id]
 Table bse_price_tiers
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column description;text;NO;NULL;
 Column group_id;int(11);YES;NULL;
@@ -353,7 +374,7 @@ Column display_order;int(11);NO;-1;
 Index PRIMARY;1;[id]
 Index only_one;1;[owner_id;owner_type;file_id]
 Table bse_seminar_bookings
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column session_id;int(11);NO;NULL;
 Column siteuser_id;int(11);NO;NULL;
@@ -365,7 +386,7 @@ Index PRIMARY;1;[id]
 Index session_id;1;[session_id;siteuser_id]
 Index siteuser_id;0;[siteuser_id]
 Table bse_seminar_sessions
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column seminar_id;int(11);NO;NULL;
 Column location_id;int(11);NO;NULL;
@@ -376,17 +397,17 @@ Index location_id;0;[location_id]
 Index seminar_id;1;[seminar_id;location_id;when_at]
 Index seminar_id_2;0;[seminar_id]
 Table bse_seminars
-Engine MyISAM
+Engine InnoDB
 Column seminar_id;int(11);NO;NULL;
 Column duration;int(11);NO;NULL;
 Index PRIMARY;1;[seminar_id]
 Table bse_siteuser_groups
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column name;varchar(80);NO;NULL;
 Index PRIMARY;1;[id]
 Table bse_siteuser_images
-Engine MyISAM
+Engine InnoDB
 Column siteuser_id;int(11);NO;NULL;
 Column image_id;varchar(20);NO;NULL;
 Column filename;varchar(80);NO;NULL;
@@ -397,13 +418,13 @@ Column content_type;varchar(80);NO;NULL;
 Column alt;varchar(255);NO;NULL;
 Index PRIMARY;1;[siteuser_id;image_id]
 Table bse_siteuser_membership
-Engine MyISAM
+Engine InnoDB
 Column group_id;int(11);NO;NULL;
 Column siteuser_id;int(11);NO;NULL;
 Index PRIMARY;1;[group_id;siteuser_id]
 Index siteuser_id;0;[siteuser_id]
 Table bse_siteusers
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column idUUID;varchar(40);NO;NULL;
 Column userId;varchar(40);NO;NULL;
@@ -467,7 +488,7 @@ Index affiliate_name;0;[affiliate_name]
 Index idUUID;1;[idUUID]
 Index userId;1;[userId]
 Table bse_subscriptions
-Engine MyISAM
+Engine InnoDB
 Column subscription_id;int(11);NO;NULL;auto_increment
 Column text_id;varchar(20);NO;NULL;
 Column title;varchar(255);NO;NULL;
@@ -476,21 +497,21 @@ Column max_lapsed;int(11);NO;NULL;
 Index PRIMARY;1;[subscription_id]
 Index text_id;1;[text_id]
 Table bse_tag_categories
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column cat;varchar(80);NO;NULL;
 Column owner_type;char(2);NO;NULL;
 Index PRIMARY;1;[id]
 Index cat;1;[cat;owner_type]
 Table bse_tag_category_deps
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column cat_id;int(11);NO;NULL;
 Column depname;varchar(160);NO;NULL;
 Index PRIMARY;1;[id]
 Index cat_dep;1;[cat_id;depname]
 Table bse_tag_members
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column owner_type;char(2);NO;NULL;
 Column owner_id;int(11);NO;NULL;
@@ -499,7 +520,7 @@ Index PRIMARY;1;[id]
 Index art_tag;1;[owner_id;tag_id]
 Index by_tag;0;[tag_id]
 Table bse_tags
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column owner_type;char(2);NO;NULL;
 Column cat;varchar(80);NO;NULL;
@@ -507,7 +528,7 @@ Column val;varchar(80);NO;NULL;
 Index PRIMARY;1;[id]
 Index cat_val;1;[owner_type;cat;val]
 Table bse_user_subscribed
-Engine MyISAM
+Engine InnoDB
 Column subscription_id;int(11);NO;NULL;
 Column siteuser_id;int(11);NO;NULL;
 Column started_at;date;NO;NULL;
@@ -515,20 +536,20 @@ Column ends_at;date;NO;NULL;
 Column max_lapsed;int(11);NO;NULL;
 Index PRIMARY;1;[subscription_id;siteuser_id]
 Table bse_wishlist
-Engine MyISAM
+Engine InnoDB
 Column user_id;int(11);NO;NULL;
 Column product_id;int(11);NO;NULL;
 Column display_order;int(11);NO;NULL;
 Index PRIMARY;1;[user_id;product_id]
 Table email_blacklist
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column email;varchar(127);NO;NULL;
 Column why;varchar(80);NO;NULL;
 Index PRIMARY;1;[id]
 Index email;1;[email]
 Table email_requests
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column email;varchar(127);NO;NULL;
 Column genEmail;varchar(127);NO;NULL;
@@ -538,7 +559,7 @@ Index PRIMARY;1;[id]
 Index email;1;[email]
 Index genEmail;1;[genEmail]
 Table image
-Engine MyISAM
+Engine InnoDB
 Column id;mediumint(8) unsigned;NO;NULL;auto_increment
 Column articleId;int(11);NO;NULL;
 Column image;varchar(255);NO;;
@@ -553,7 +574,7 @@ Column src;varchar(255);NO;;
 Column ftype;varchar(20);NO;img;
 Index PRIMARY;1;[id]
 Table order_item
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column productId;int(11);NO;NULL;
 Column orderId;int(11);NO;NULL;
@@ -578,7 +599,7 @@ Column product_code;varchar(80);NO;;
 Index PRIMARY;1;[id]
 Index order_item_order;0;[orderId;id]
 Table orders
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column delivFirstName;varchar(127);NO;;
 Column delivLastName;varchar(127);NO;;
@@ -655,11 +676,13 @@ Column freight_tracking;varchar(255);NO;;
 Column stage;varchar(20);NO;;
 Column ccPAN;varchar(4);NO;;
 Column paid_manually;int(11);NO;0;
+Column coupon_code;varchar(40);NO;;
+Column coupon_discount;double;NO;0;
 Index PRIMARY;1;[id]
 Index order_cchash;0;[ccNumberHash]
 Index order_userId;0;[userId;orderDate]
 Table other_parents
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column parentId;int(11);NO;NULL;
 Column childId;int(11);NO;NULL;
@@ -671,7 +694,7 @@ Index PRIMARY;1;[id]
 Index childId;0;[childId;childDisplayOrder]
 Index parentId;1;[parentId;childId]
 Table product
-Engine MyISAM
+Engine InnoDB
 Column articleId;int(11);NO;NULL;
 Column summary;varchar(255);NO;NULL;
 Column leadTime;int(11);NO;0;
@@ -690,32 +713,32 @@ Column width;int(11);NO;0;
 Column height;int(11);NO;0;
 Index PRIMARY;1;[articleId]
 Table searchindex
-Engine MyISAM
+Engine InnoDB
 Column id;varbinary(200);NO;;
 Column articleIds;varchar(255);NO;;
 Column sectionIds;varchar(255);NO;;
 Column scores;varchar(255);NO;;
 Index PRIMARY;1;[id]
 Table sessions
-Engine MyISAM
+Engine InnoDB
 Column id;char(32);NO;NULL;
 Column a_session;blob;YES;NULL;
 Column whenChanged;timestamp;NO;CURRENT_TIMESTAMP;on update CURRENT_TIMESTAMP
 Index PRIMARY;1;[id]
 Table sql_statements
-Engine MyISAM
+Engine InnoDB
 Column name;varchar(80);NO;NULL;
 Column sql_statement;text;NO;NULL;
 Index PRIMARY;1;[name]
 Table subscribed_users
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column subId;int(11);NO;NULL;
 Column userId;int(11);NO;NULL;
 Index PRIMARY;1;[id]
 Index subId;1;[subId;userId]
 Table subscription_types
-Engine MyISAM
+Engine InnoDB
 Column id;int(11);NO;NULL;auto_increment
 Column name;varchar(80);NO;NULL;
 Column title;varchar(64);NO;NULL;
index dd44733f3b6a6783ba692685f7cb65dc1b534c60..c7ac2ee81064f60f8f82c626025a0dc683a9f223 100644 (file)
@@ -44,16 +44,7 @@ site/cgi-bin/modules/BSE/UI/AdminShop.pm     multiple occurrence of link target 'pro
 site/cgi-bin/modules/BSE/UI/AdminShop.pm       multiple occurrence of link target 'script'     1
 site/cgi-bin/modules/BSE/UI/Background.pm      =item without previous =over    1
 site/cgi-bin/modules/BSE/UI/Formmail.pm        =item without previous =over    1
-site/cgi-bin/modules/BSE/UI/Shop.pm    =item without previous =over    1
-site/cgi-bin/modules/BSE/UI/Shop.pm    multiple occurrence of link target '%<format>'  1
-site/cgi-bin/modules/BSE/UI/Shop.pm    multiple occurrence of link target 'gst'        1
-site/cgi-bin/modules/BSE/UI/Shop.pm    multiple occurrence of link target 'id' 1
 site/cgi-bin/modules/BSE/UI/Shop.pm    multiple occurrence of link target 'item field' 1
-site/cgi-bin/modules/BSE/UI/Shop.pm    multiple occurrence of link target 'm<number>'  1
-site/cgi-bin/modules/BSE/UI/Shop.pm    multiple occurrence of link target 'options'    1
-site/cgi-bin/modules/BSE/UI/Shop.pm    multiple occurrence of link target 'order field'        1
-site/cgi-bin/modules/BSE/UI/Shop.pm    multiple occurrence of link target 'product field'      1
-site/cgi-bin/modules/BSE/UI/Shop.pm    multiple occurrence of link target 'total'      1
 site/cgi-bin/modules/BSE/Util/SQL.pm   =over on line 37 without closing =back  1
 site/cgi-bin/modules/BSE/Util/SQL.pm   Verbatim paragraph in NAME section      1
 site/cgi-bin/modules/BSE/Util/Secure.pm        =over on line 25 without closing =back (at head1)       1