site/cgi-bin/modules/BSE/ComposeMail.pm
site/cgi-bin/modules/BSE/Console.pm
site/cgi-bin/modules/BSE/Countries.pm
+site/cgi-bin/modules/BSE/Coupon/Base.pm
+site/cgi-bin/modules/BSE/Coupon/Dollar.pm
+site/cgi-bin/modules/BSE/Coupon/Percent.pm
+site/cgi-bin/modules/BSE/Coupon/ProductPercent.pm
site/cgi-bin/modules/BSE/Custom.pm
site/cgi-bin/modules/BSE/CustomBase.pm
site/cgi-bin/modules/BSE/DB.pm
site/htdocs/js/admin-ui/debug.js
site/htdocs/js/admin-ui/menu.js
site/htdocs/js/admin.js
+site/htdocs/js/admin_coupons.js
site/htdocs/js/admin_edit.js
site/htdocs/js/admin_editprodopt.js
site/htdocs/js/admin_jedit.js
$(PERL) -MExtUtils::Command -e rm_rf $(DISTBUILD)
cd $(UTILDIR) ; $(PERL) loaddata.pl $(DATADIR)/db
-testup: checkver distdir
+testupx: checkver distdir
+ $(PERL) localinst.perl $(DISTBUILD) leavedb
+ $(PERL) -MExtUtils::Command -e rm_rf $(DISTBUILD)
+ cd $(UTILDIR) ; $(PERL) upgrade_mysql.pl -b ; $(PERL) loaddata.pl $(DATADIR)/db
+
+testload :
+ $(PERL) '-MTest::Harness=runtests,$$verbose' -Isite/cgi-bin/modules -It -e '$$verbose=$(TEST_VERBOSE); runtests @ARGV' t/t000load.t
+
+testup : testload checkver distdir
$(PERL) localinst.perl $(DISTBUILD) leavedb
$(PERL) -MExtUtils::Command -e rm_rf $(DISTBUILD)
cd $(UTILDIR) ; $(PERL) upgrade_mysql.pl -b ; $(PERL) loaddata.pl $(DATADIR)/db
TEST_FILES=t/*.t t/*/*.t
TEST_VERBOSE=0
-test: testup
+test: testupx
$(PERL) '-MTest::Harness=runtests,$$verbose' -Isite/cgi-bin/modules -It -e '$$verbose=$(TEST_VERBOSE); runtests @ARGV' $(TEST_FILES)
test_load: testup
paid_manually integer not null default 0,
coupon_id integer null,
- coupon_code_discount_pc real not null default 0,
+ -- obsolete
+ coupon_code_discount_pc real null default 0,
delivery_in integer null,
+ product_cost_discount integer not null default 0,
+
+ coupon_cart_wide integer not null default 1,
+
+ coupon_description varchar(255) not null default '',
+
primary key (id),
index order_cchash(ccNumberHash),
index order_userId(userId, orderDate),
tier_id integer null default null,
+ product_discount integer not null default 0,
+ product_discount_units integer not null default 0,
+
primary key (id),
index order_item_order(orderId, id)
) engine=InnoDB;
expiry date not null,
- discount_percent real not null,
+ discount_percent real null,
campaign varchar(20) not null,
untiered integer not null default 0,
+ classid varchar(20) not null default 'bse_simple',
+
+ config blob not null,
+
unique codes(code)
) engine=InnoDB;
[number money]
divisor=100
places=2
+
+[coupon classes]
+bse_simple=BSE::Coupon::Percent
+bse_dollar=BSE::Coupon::Dollar
+bse_prodpercent=BSE::Coupon::ProductPercent
use strict;
use Scalar::Util;
-our $VERSION = "1.011";
+our $VERSION = "1.015";
=head1 NAME
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);
+ return $self->total_cost - $self->product_cost_discount;
}
=item product_cost_discount
sub product_cost_discount {
my ($self) = @_;
- return $self->total_cost - $self->discounted_product_cost;
+ $self->coupon_active
+ or return 0;
+
+ return $self->{coupon_check}{coupon}->discount($self);
}
=item cfg_shipping
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.
+This method is historical and no longer useful.
+
=cut
sub coupon_code_discount_pc {
(
coupon => $coupon,
valid => 0,
+ active => 0,
+ msg => "",
);
#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;
- }
+ my ($active, $msg) = $coupon->is_active($self);
+ $check{active} = $active;
+ $check{msg} = $msg || "";
}
$self->{coupon_check} = \%check;
}
{
valid => 0,
active => 0,
+ msg => "",
};
}
}
return $self->{coupon_check}{active};
}
+=item coupon_inactive_message
+
+Returns why the coupon is inactive.
+
+=cut
+
+sub coupon_inactive_message {
+ my ($self) = @_;
+
+ $self->coupon_valid
+ or return "";
+
+ return $self->{coupon_check}{msg};
+}
+
=item coupon
The current coupon object, if and only if the coupon code is valid.
$self->{coupon_check}{coupon};
}
+=item coupon_cart_wide
+
+Returns true if the coupon discount applies to the cart as a whole.
+
+Always returns false if the coupon is not active.
+
+If this is true the item discount methods are useful.
+
+=cut
+
+sub coupon_cart_wide {
+ my ($self) = @_;
+
+ $self->coupon_active
+ or return;
+
+ return $self->coupon->cart_wide($self);
+}
+
+=item coupon_description
+
+Describe the coupon.
+
+Compatible with order objects.
+
+=cut
+
+sub coupon_description {
+ my ($self) = @_;
+
+ $self->coupon_valid
+ or return;
+
+ return $self->coupon->describe;
+}
+
=item custom_cost
Return any custom cost specified by a custom class.
return $code;
}
-=item any_phyiscal_products
+=item any_physical_products
Returns true if the cart contains any physical products, ie. needs
shipping.
Empty the cart.
+For BSE use.
+
=cut
sub empty {
return $self->{cart}->_product($self->{productId});
}
+=item product_id
+
+Id of the product in this row.
+
+=cut
+
+sub product_id {
+ $_[0]{productId};
+}
+
=item price
=cut
=item coupon_applies
-Returns true if the current coupon code applies to the item.
+Returns true for a cart-wide coupon if this item allows the coupon to
+apply.
=cut
sub coupon_applies {
my ($self) = @_;
- $self->{cart}->coupon_valid
+ $self->{cart}->coupon_active
or return 0;
- return $self->{coupon_applies};
+ return $self->{cart}{coupon_check}{coupon}->product_valid($self->{cart}, $self->{index});
}
+=item product_discount
+
+Returns the number of cents of discount this product receives per unit
+
+=cut
+
+sub product_discount {
+ my ($self) = @_;
+
+ $self->{cart}->coupon_active
+ or return 0;
+
+ return $self->{cart}{coupon_check}{coupon}->product_discount($self->{cart}, $self->{index});
+}
+
+=item product_discount_units
+
+Returns the number of units in the current row that the product
+discount applies to.
+
+=cut
+
+sub product_discount_units {
+ my ($self) = @_;
+
+ $self->{cart}->coupon_active
+ or return 0;
+
+ return $self->{cart}{coupon_check}{coupon}->product_discount_units($self->{cart}, $self->{index});
+}
+
+
=item session
The session object of the seminar session
--- /dev/null
+package BSE::Coupon::Base;
+use strict;
+
+our $VERSION = "1.001";
+
+sub new {
+ my ($class, $config) = @_;
+
+ return bless { config => $config }, $class;
+}
+
+sub config_rules {
+ return {};
+}
+
+sub used_in {
+ # do nothing by default
+}
+
+sub cart_wide {
+ 1;
+}
+
+sub product_valid {
+ 0;
+}
+
+sub product_discount {
+ 0;
+}
+
+sub product_discount_units {
+ 0;
+}
+
+sub test_all_tiers_match {
+ my ($self, $coupon, $cart) = @_;
+
+ my %tiers = map { $_ => 1 } $coupon->tiers;
+
+ my $bad_tier_count = 0;
+ for my $item ($cart->items) {
+ if ($item->tier_id) {
+ if (!$tiers{$item->tier_id}) {
+ return 0;
+ }
+ }
+ else {
+ if (!$coupon->untiered) {
+ return 0;
+ }
+ }
+ }
+
+ return 1;
+}
+
+sub test_tier_matches {
+ my ($self, $coupon, $cart, $index) = @_;
+
+ my @items = $cart->items;
+ $index >= 0 && $index < @items
+ or return 0;
+
+ my $item = $items[$index];
+
+ if ($item->tier_id) {
+ my %tiers = map { $_ => 1 } $coupon->tiers;
+
+ if (!$tiers{$item->tier_id}) {
+ return 0;
+ }
+ }
+ else {
+ if (!$coupon->untiered) {
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+BSE::Coupon::Base - base class for coupon behaviour classes.
+
+=head1 SYNOPSIS
+
+ package BSE::Coupon::YourCoupon;
+ use parent 'BSE::Coupon::Base';
+
+ sub new {
+ my ($class, $config) = @_;
+ ...
+ }
+
+ # ... and more
+
+=head1 DESCRIPTION
+
+This class provides base behaviour and documentation on the
+requirements of BSE coupon behaviour classes.
+
+A coupon behaviour class must use BSE::Coupon::Base as a base class,
+to provide default behaviour that might be added later to this
+specification.
+
+A coupon behaviour class may override the default constructor:
+
+=over
+
+=item *
+
+new($config) - create a new coupon behaviour object. This is passed a
+single parameter of the config hash for that behaviour, as specified
+by config_fields().
+
+=back
+
+The class must implement the following class methods:
+
+=over
+
+=item *
+
+config_fields() - returns a hash reference of customization
+fields for this coupon class. For example it might return a field
+definition for the dollar amount to be discounted.
+
+If a field called C<discount_percent> is included it will be stored in
+the C<discount_percent> field of the coupon entry.
+
+ my $fields = $class->config_fields();
+
+=item *
+
+config_rules() - any extra rule definitions required for the
+validation rules in config_fields().
+
+ my $rules = $class->config_rules();
+
+A default implementation is provided that returns an empty hash.
+
+=item *
+
+config_valid() - validate whether a supplied configuration is valid.
+
+ my $valid = $class->config_valid($config, \%errors);
+
+This occurs in addition to any validation rules in the fields returned
+by config_fields().
+
+=item *
+
+class_description() - a brief description of this coupon behaviour,
+for use in drop-down lists when creating a coupon.
+
+=back
+
+The class must implement the following object methods:
+
+=over
+
+=item *
+
+is_active() - return true if the coupon is usable for the cart. If
+the coupon is not usable, returns $msg as the reson why:
+
+ my ($active, $msg) = $cb->is_active($coupon, $cart);
+
+This is in addition to the date and tier checks already done for the coupon.
+
+=item *
+
+discount() - return the discount in cents provided by the coupon on
+the supplied cart.
+
+ my ($cents) = $cb->discount($coupon, $cart);
+
+Must only be called if is_active() returns true for the cart.
+
+=item *
+
+product_valid($coupon, $cart, $index) - returns true if the given line
+item in the cart is valid for the coupon.
+
+Only meaningful for cart-wide coupons.
+
+=item *
+
+product_discount($coupon, $cart, $index) - returns the per-unit
+discount in cents provided by the coupon on the specified entry in the
+given cart.
+
+ my ($cents) = $cb->product_discount($coupon, $cart, $index);
+
+Must only be called if is_active() returns true for the cart.
+
+Returns 0 if the discount is against the entire cart.
+
+A default implementation returns 0.
+
+=item *
+
+product_discount_units($coupon, $cart, $index) - returns the number of
+units the product specific discount applies to on specified entry in
+the given cart.
+
+ my ($cents) = $cb->product_discount_units($coupon, $cart, $index);
+
+Must only be called if is_active() returns true for the cart.
+
+Returns 0 if the discount is against the entire cart.
+
+A default implementation returns 0.
+
+=item *
+
+cart_wide($coupon, $cart) - return true if the coupon behaviour
+provides a cart-wide discount. Generally this is class-wide, but must
+be called as an instance method in-case that's untrue for some
+particular behaviour class.
+
+The default implementation returns false.
+
+=item *
+
+used_in() - called when a new order if finalized with the given coupon
+code.
+
+ $cb->used_in($coupon_code, $order);
+
+A default implementation is provided that does nothing.
+
+=item *
+
+describe() - return a text description of the coupon based on its
+configuration data.
+
+ my ($text) = $cb->describe();
+
+=back
+
--- /dev/null
+package BSE::Coupon::Dollar;
+use parent 'BSE::Coupon::Base';
+use strict;
+
+our $VERSION = "1.003";
+
+sub config_fields {
+ my ($class) = @_;
+
+ return
+ {
+ min_cart =>
+ {
+ description => "Min Cart Value",
+ required => 1,
+ width => 5,
+ htmltype => "text",
+ rules => "money",
+ type => "money",
+ order => 1,
+ },
+ discount_dollars =>
+ {
+ description => "Discount \$",
+ required => 1,
+ width => 5,
+ htmltype => "text",
+ rules => "money",
+ type => "money",
+ order => 2,
+ },
+ };
+}
+
+sub config_valid {
+ 1;
+}
+
+sub class_description {
+ "Simple dollar cart discount";
+}
+
+sub is_active {
+ my ($self, $coupon, $cart) = @_;
+
+ $self->test_all_tiers_match($coupon, $cart)
+ or return ( 0, "One or more products are already discounted" );
+ unless ($cart->total_cost >= $self->{config}{min_cart}) {
+ require BSE::Util::Format;
+ return ( 0, sprintf("You need \$%s of items in the cart for the discount",
+ BSE::Util::Format::bse_number("money", $self->{config}{min_cart})) );
+ }
+
+ 1;
+}
+
+sub product_valid {
+ my ($self, $coupon, $cart, $index) = @_;
+
+ return $self->test_tier_matches($coupon, $cart, $index);
+}
+
+sub discount {
+ my ($self, $coupon, $cart) = @_;
+
+ return 0
+ if $cart->total_cost < $self->{config}{min_cart};
+
+ return $self->{config}{discount_dollars};
+}
+
+sub describe {
+ my ($self) = @_;
+
+ require BSE::Util::Format;
+
+ sprintf("\$%s discount on cart over \$%s",
+ BSE::Util::Format::bse_number("money", $self->{config}{discount_dollars}),
+ BSE::Util::Format::bse_number("money", $self->{config}{min_cart}));
+}
+
+1;
--- /dev/null
+package BSE::Coupon::Percent;
+use parent 'BSE::Coupon::Base';
+use strict;
+
+our $VERSION = "1.002";
+
+sub config_fields {
+ my ($class) = @_;
+
+ return
+ {
+ discount_percent =>
+ {
+ description => "Discount",
+ required => 1,
+ width => 5,
+ htmltype => "text",
+ rules => "coupon_percent",
+ units => "%",
+ order => 1,
+ },
+ };
+}
+
+sub config_rules {
+ return
+ (
+ coupon_percent =>
+ {
+ real => '0 - 100',
+ },
+ );
+}
+
+sub config_valid {
+ 1;
+}
+
+sub class_description {
+ "Simple percentage cart discount";
+}
+
+sub is_active {
+ my ($self, $coupon, $cart) = @_;
+
+ $self->test_all_tiers_match($coupon, $cart)
+ or return ( 0, "One or more products are already discounted" );
+
+ 1;
+}
+
+sub product_valid {
+ my ($self, $coupon, $cart, $index) = @_;
+
+ return $self->test_tier_matches($coupon, $cart, $index);
+}
+
+sub discount {
+ my ($self, $coupon, $cart) = @_;
+
+ return int($cart->total_cost * $self->{config}{discount_percent} / 100);
+}
+
+sub describe {
+ my ($self) = @_;
+
+ sprintf("%.1f%% cart discount", $self->{config}{discount_percent});
+}
+
+1;
--- /dev/null
+package BSE::Coupon::ProductPercent;
+use parent 'BSE::Coupon::Base';
+use strict;
+
+our $VERSION = "1.000";
+
+sub config_fields {
+ my ($class) = @_;
+
+ require BSE::TB::Products;
+
+ return
+ {
+ discount_percent =>
+ {
+ description => "Discount",
+ required => 1,
+ width => 5,
+ htmltype => "text",
+ rules => "coupon_percent",
+ units => "%",
+ order => 1,
+ },
+ product_id =>
+ {
+ description => "Product",
+ required => 1,
+ htmltype => "select",
+ order => 2,
+ select =>
+ {
+ id => "id",
+ label => "label",
+ values =>
+ [
+ sort { lc $a->{label} cmp lc $b->{label} }
+ map
+ +{
+ id => $_->id,
+ label => $_->title,
+ }, BSE::TB::Products->all
+ ],
+ },
+ },
+ max_units =>
+ {
+ description => "Max Units",
+ required => 1,
+ width => 5,
+ htmltype => "text",
+ rules => "natural",
+ units => "units",
+ order => 3,
+ },
+ };
+}
+
+sub config_rules {
+ return
+ (
+ );
+}
+
+sub config_valid {
+ my ($self, $config, $errors) = @_;
+
+ require BSE::TB::Products;
+ my ($prod) = BSE::TB::Products->getByPkey($config->{product_id});
+ unless ($prod) {
+ $errors->{product_id} = "Unknown product id";
+ }
+ !keys %$errors;
+}
+
+sub class_description {
+ "Simple product specific discount";
+}
+
+sub _product {
+ my ($self) = @_;
+
+ unless ($self->{_product}) {
+ require BSE::TB::Products;
+ $self->{_product} = BSE::TB::Products->getByPkey($self->{config}{product_id});
+ }
+
+ $self->{_product};
+}
+
+sub is_active {
+ my ($self, $coupon, $cart) = @_;
+
+ for my $item ($cart->items) {
+ return 1
+ if $item->product->id == $self->{config}{product_id};
+ }
+
+ require BSE::TB::Products;
+ my ($prod) = $self->_product;
+
+ return ( 0, "This coupon only applies to ".$prod->title );
+}
+
+sub cart_wide {
+ 0;
+}
+
+sub _row_discounts {
+ my ($self, $coupon, $cart) = @_;
+
+ my $max_units = $self->{config}{max_units};
+ my $product_id = $self->{config}{product_id};
+ my $discount = $self->{config}{discount_percent};
+ my $units_seen = 0;
+ my @row_discounts;
+ for my $item ($cart->items) {
+ my $discount_units = 0;
+ my $row_discount = 0;
+ if ($item->product_id == $product_id) {
+ my $prod_discount = int($item->price * $discount / 100);
+ if ($max_units) {
+ if ($units_seen < $max_units) {
+ $row_discount = $prod_discount;
+ my $units_left = $max_units - $units_seen;
+ $discount_units = $units_left > $item->units ? $item->units : $units_left;
+ }
+ }
+ else {
+ $row_discount = $prod_discount;
+ $discount_units = $item->units;
+ }
+ $units_seen += $item->units;
+ }
+ push @row_discounts, [ $row_discount, $discount_units ];
+ }
+
+ @row_discounts;
+}
+
+sub discount {
+ my ($self, $coupon, $cart) = @_;
+
+ my @discounts = $self->_row_discounts($coupon, $cart);
+
+ my $total_discount = 0;
+ for my $row (@discounts) {
+ $total_discount += $row->[0] * $row->[1];
+ }
+
+ return $total_discount;
+}
+
+sub product_discount {
+ my ($self, $coupon, $cart, $index) = @_;
+
+ my @discounts = $self->_row_discounts($coupon, $cart);
+
+ return $discounts[$index][0];
+}
+
+sub product_discount_units {
+ my ($self, $coupon, $cart, $index) = @_;
+
+ my @discounts = $self->_row_discounts($coupon, $cart);
+
+ return $discounts[$index][1];
+}
+
+sub describe {
+ my ($self) = @_;
+
+ my $desc = sprintf("%.1f%% discount on ", $self->{config}{discount_percent});
+ if ($self->{config}{max_units}) {
+ $desc .= "the first $self->{config}{max_units} units of ";
+ }
+ $desc .= $self->_product->title;
+
+ $desc;
+}
+
+1;
use BSE::Util::HTML;
use Carp qw(cluck confess);
-our $VERSION = "1.033";
+our $VERSION = "1.034";
=head1 NAME
my ($hour, $minute, $sec) = DevHelp::Date::dh_parse_time($value, \$msg);
$value = sprintf("%02d:%02d:%02d", $hour, $minute, $sec);
}
+ elsif ($field->{type} && $field->{type} eq "money") {
+ ($value) = $cgi->param($name);
+ $value *= 100;
+ }
else {
($value) = $cgi->param($name);
defined $name or $value = "";
our @ISA = qw/Squirrel::Row/;
use BSE::TB::CouponTiers;
-our $VERSION = "1.003";
+our $VERSION = "1.007";
=head1 NAME
=cut
sub columns {
- return qw/id code description release expiry discount_percent campaign last_modified untiered/;
+ return qw/id code description release expiry discount_percent campaign last_modified untiered
+ classid config/;
}
sub table {
(
last_modified => BSE::Util::SQL::now_sqldatetime(),
untiered => 1,
+ discount_percent => undef,
);
}
my $data = $self->data_only;
$data->{tiers} = [ $self->tiers ];
+ $data->{config_obj} = $self->config_obj;
+ delete @$data{qw/config discount_percent/};
return $data;
}
return $self->is_removable;
}
+=item is_active
+
+Returns a list of (is active, message) for the given cart.
+
+Wrapper around is_active() for the behaviour.
+
+ my ($active, $msg) = $coupon->is_active($cart);
+
+=cut
+
+sub is_active {
+ my ($self, $cart) = @_;
+
+ return $self->behaviour->is_active($self, $cart);
+}
+
+=item discount
+
+Return the discount in cents for the given cart.
+
+Must only be called if is_active() returned the coupon as active.
+
+Wrapper around discount() for the behaviour.
+
+ my ($cents) = $coupon->discount($cart);
+
+=cut
+
+sub discount {
+ my ($self, $cart) = @_;
+
+ return $self->behaviour->discount($self, $cart);
+}
+
+=item product_valid
+
+Return true if the given cart item is valid for the coupon.
+
+Only relevant for cart-wide coupons.
+
+=cut
+
+sub product_valid {
+ my ($self, $cart, $index) = @_;
+
+ return $self->behaviour->product_valid($self, $cart, $index);
+}
+
+=item product_discount
+
+Return the product specific discount per unit for the given row
+(counting from zero) in the cart.
+
+Must only be called if is_active() returned the coupon as active.
+
+Returns zero if the coupon discount is for the cart as a whole.
+
+Wrapper around product_discount() for the behaviour.
+
+ my $cents = $coupon->product_discount($cart, $index);
+
+=cut
+
+sub product_discount {
+ my ($self, $cart, $index) = @_;
+
+ return $self->behaviour->product_discount($self, $cart, $index);
+}
+
+=item product_discount_units
+
+Return the number of units a product specific discount applies to the given row
+(counting from zero) in the cart.
+
+Must only be called if is_active() returned the coupon as active.
+
+Returns zero if the coupon discount is for the cart as a whole.
+
+Wrapper around product_discount_units() for the behaviour.
+
+ my $cents = $coupon->product_discount_units($cart, $index);
+
+=cut
+
+sub product_discount_units {
+ my ($self, $cart, $index) = @_;
+
+ return $self->behaviour->product_discount_units($self, $cart, $index);
+}
+
+=item describe
+
+Describe the behaviour of the coupon briefly.
+
+=cut
+
+sub describe {
+ my ($self) = @_;
+
+ $self->behaviour->describe;
+}
+
+=item cart_wide($cart)
+
+Returns true if the discount provided by the behaviour applies to the
+cart as a whole.
+
+=cut
+
+sub cart_wide {
+ my ($self, $cart) = @_;
+
+ return $self->behaviour->cart_wide($cart);
+}
+
=item set_code($code)
Set the coupon code. Requires that is_renamable() be true.
sub fields {
my ($self) = @_;
+ my $bclasses = BSE::TB::Coupons->behaviour_classes;
+
my %fields =
(
code =>
type => "date",
rules => "date",
},
- discount_percent =>
- {
- description => "Discount %",
- required => 1,
- width => 5,
- htmltype => "text",
- rules => "coupon_percent",
- units => "%",
- },
campaign =>
{
description => "Campaign",
label => "description",
},
},
+ classid =>
+ {
+ description => "Coupon Class",
+ htmltype => "select",
+ select =>
+ {
+ id => "id",
+ label => "label",
+ values =>
+ [
+ sort { lc $a->{label} cmp lc $b->{label} }
+ map
+ +{
+ id => $_,
+ label => $bclasses->{$_}->class_description,
+ },
+ sort { lc $bclasses->{$a}->class_description cmp lc $bclasses->{$b}->class_description}
+ keys %$bclasses
+ ],
+ },
+ },
);
if (ref $self && !$self->is_renamable) {
match => qr/\A[a-zA-Z0-9]+\z/,
error => '$n can only contain letters and digits',
},
- coupon_percent =>
- {
- real => '0 - 100',
- },
};
}
+sub config_obj {
+ my ($self) = @_;
+
+ my $config = $self->config;
+ $config = "{}" if $config eq "";
+
+ require JSON;
+ my $obj = JSON->new->decode($config);
+ $obj->{discount_percent} = $self->discount_percent;
+
+ return $obj;
+}
+
+sub set_config_obj {
+ my ($self, $obj) = @_;
+
+ $self->set_discount_percent(delete $obj->{discount_percent});
+
+ require JSON;
+ $self->set_config(JSON->new->encode($obj));
+}
+
+sub behaviour {
+ my ($self) = @_;
+
+ delete $self->{_behaviour}
+ if $self->{_classid} ne $self->classid
+ || $self->{_config} ne $self->config;
+
+ $self->{_behaviour} ||=
+ BSE::TB::Coupons->behaviour_class($self->classid)->new($self->config_obj);
+ $self->{_classid} = $self->classid;
+ $self->{_config} = $self->config;
+
+ $self->{_behaviour};
+}
+
1;
=back
our @ISA = qw(Squirrel::Table);
use BSE::TB::Coupon;
-our $VERSION = "1.001";
+our $VERSION = "1.002";
sub rowClass {
return 'BSE::TB::Coupon';
my $tiers = delete $opts{tiers};
+ my $config_obj = delete $opts{config_obj};
+ if ($config_obj) {
+ $opts{discount_percent} = delete $config_obj->{discount_percent};
+ require JSON;
+ $opts{config} = JSON->new->encode($config_obj);
+ }
+
my $coupon = $self->SUPER::make(%opts);
if ($tiers) {
return $coupon;
}
+=item behaviour_class
+
+Return the class name of a behaviour class given a class id.
+
+Loads the class.
+
+Throws an exception if no class if configured for the class id or if
+the class cannot be loaded.
+
+=cut
+
+sub behaviour_class {
+ my ($class, $classid) = @_;
+
+ my $bclass = BSE::Cfg->single->entryErr("coupon classes", $classid);
+ (my $bfile = $bclass . ".pm") =~ s(::)(/)g;
+
+ require $bfile;
+
+ return $bclass;
+}
+
+=item behaviour_classes
+
+Returns a hash of all behaviour classes with the keys being the
+classid and the value the class name.
+
+=cut
+
+sub behaviour_classes {
+ my ($class) = @_;
+
+ my %entries = BSE::Cfg->single->entries("coupon classes");
+ my %bclasses;
+ for my $classid (keys %entries) {
+ $bclasses{$classid} = $class->behaviour_class($classid);
+ }
+
+ \%bclasses;
+}
+
1;
use Carp 'confess';
use BSE::Shop::PaymentTypes;
-our $VERSION = "1.026";
+our $VERSION = "1.029";
sub columns {
return qw/id
delivStreet2 billStreet2 purchase_order shipping_method
shipping_name shipping_trace
paypal_token paypal_tran_id freight_tracking stage ccPAN
- paid_manually coupon_id coupon_code_discount_pc delivery_in/;
+ paid_manually coupon_id coupon_code_discount_pc delivery_in
+ product_cost_discount coupon_cart_wide coupon_description/;
}
sub table {
my $cost = $self->total_cost;
- $cost -= $cost * $self->coupon_code_discount_pc / 100;
-
- return int($cost);
-}
-
-=item product_cost_discount
-
-Return any amount taken off the product cost.
-
-=cut
+ if ($self->product_cost_discount) {
+ return $cost - $self->product_cost_discount;
+ }
-sub product_cost_discount {
- my ($self) = @_;
+ $cost -= int($cost * $self->coupon_code_discount_pc / 100);
- return $self->total_cost - $self->discounted_product_cost;
+ return $cost;
}
=item coupon
use vars qw/@ISA/;
@ISA = qw/Squirrel::Row/;
-our $VERSION = "1.005";
+our $VERSION = "1.006";
sub table { "order_item" }
return qw/id productId orderId units price wholesalePrice gst options
customInt1 customInt2 customInt3 customStr1 customStr2 customStr3
title description subscription_id subscription_period max_lapsed
- session_id product_code tier_id/;
+ session_id product_code tier_id product_discount product_discount_units/;
}
sub db_columns {
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.028";
+our $VERSION = "1.030";
my %actions =
(
return $req->dyn_response('admin/coupons/list', \%acts);
}
+# coupon behaviour classes wrapped for use in templates
+
+sub _coupon_behaviours {
+ my ($self) = @_;
+
+ require BSE::TB::Coupons;
+ my $bclasses = BSE::TB::Coupons->behaviour_classes();
+ return
+ [
+ map
+ +{
+ id => $_,
+ behaviour => Squirrel::Template::Expr::WrapClass->new($bclasses->{$_})
+ },
+ sort { lc $bclasses->{$a}->class_description cmp lc $bclasses->{$b}->class_description}
+ keys %$bclasses
+ ]
+}
+
=item coupon_addform
Display a form for adding new coupons.
$req->set_variable(errors => $errors || {});
require BSE::TB::PriceTiers;
$req->set_variable(tiers => [ BSE::TB::PriceTiers->all ]);
+ $req->set_variable(behaviours => $self->_coupon_behaviours);
return $req->dyn_response("admin/coupons/add", \%acts);
}
my %errors;
$req->validate(fields => $fields, errors => \%errors,
rules => BSE::TB::Coupon->rules);
-
my $values = $req->cgi_fields(fields => $fields);
+ unless ($errors{classid}) {
+ my $bh = BSE::TB::Coupons->behaviour_class($values->{classid});
+ my $bfields = $bh->config_fields();
+ my $brules = $bh->config_rules();
+ $req->validate(fields => $bfields, rules => $brules,
+ errors => \%errors);
+ unless (keys %errors) {
+ $values->{config_obj} = $req->cgi_fields(fields => $bfields);
+ $bh->config_valid($values->{config_obj}, \%errors);
+ }
+ }
+
unless ($errors{code}) {
my ($other) = BSE::TB::Coupons->getBy(code => $values->{code});
$other
$req->set_variable(errors => $errors || {});
require BSE::TB::PriceTiers;
$req->set_variable(tiers => [ BSE::TB::PriceTiers->all ]);
+ $req->set_variable(behaviours => $self->_coupon_behaviours);
return $req->dyn_response("admin/coupons/edit", \%acts);
}
my $values = $req->cgi_fields(fields => $fields);
+ unless ($errors{classid}) {
+ my $bh = BSE::TB::Coupons->behaviour_class($values->{classid});
+ my $bfields = $bh->config_fields();
+ my $brules = $bh->config_rules();
+ $req->validate(fields => $bfields, rules => $brules,
+ errors => \%errors);
+ unless (keys %errors) {
+ $values->{config_obj} = $req->cgi_fields(fields => $bfields);
+ $bh->config_valid($values->{config_obj}, \%errors);
+ }
+ }
+
unless ($errors{code}) {
my ($other) = BSE::TB::Coupons->getBy(code => $values->{code});
$other && $other->id != $coupon->id
my $old = $coupon->json_data;
my $tiers = delete $values->{tiers};
+ my $config_obj = delete $values->{config_obj};
for my $key (keys %$values) {
$coupon->set($key => $values->{$key});
}
$coupon->set_tiers($tiers);
+ $coupon->set_config_obj($config_obj);
$coupon->save;
$req->audit
use BSE::Util::Secure qw(make_secret);
use BSE::Template;
-our $VERSION = "1.049";
+our $VERSION = "1.051";
=head1 NAME
$work{extended_retailPrice} = $work{units} * $work{price};
$work{extended_gst} = $work{units} * $work{gst};
$work{extended_wholesale} = $work{units} * $work{wholesalePrice};
+ if ($cart->coupon_active) {
+ $work{product_discount} = $item->product_discount;
+ $work{product_discount_units} = $item->product_discount_units;
+ }
+ else {
+ $work{product_discount} = 0;
+ $work{product_discount_units} = 0;
+ }
push @newcart, \%work;
}
}
if ($cart->coupon_active) {
$values->{coupon_id} = $cart->coupon->id;
+ $values->{coupon_description} = $cart->coupon_description;
+ $values->{coupon_cart_wide} = $cart->coupon_cart_wide;
}
else {
$values->{coupon_id} = undef;
+ $values->{coupon_description} = "";
+ $values->{coupon_cart_wide} = 0;
}
$cart->set_shipping_cost($values->{shipping_cost});
$cart->set_shipping_method($values->{shipping_method});
$cart->set_delivery_in($values->{delivery_in});
$values->{coupon_code_discount_pc} = $cart->coupon_code_discount_pc;
+ $values->{product_cost_discount} = $cart->product_cost_discount;
$values->{total} = $cart->total;
my $cust_class = custom_class($cfg);
use strict;
use base qw(Squirrel::Template::Expr::WrapBase);
-our $VERSION = "1.010";
+our $VERSION = "1.011";
sub _do_length {
my ($self, $args) = @_;
my ($self, $args) = @_;
@$args == 1
- or die [ error => "scalar.escape requires one parameter" ];
+ or die [ error => "scalar.match requires one parameter" ];
$self->[0] =~ $args->[0]
or return undef;
font-weight: bold;
text-align: center;
padding: 0.5em;
+}
+
+.price_tier, .productdiscount {
+ font-size: 90%;
}
\ No newline at end of file
background-color: #FCC;
border: 1px dashed #F00;
padding: 2px 5px;
+}
+
+.itemdiscount {
+ color: green;
+ font-size: 80%;
+ padding-left: 1em;
}
\ No newline at end of file
/* stuff for every admin page */
/* mark accesskeys */
+jQuery.noConflict();
document.observe("dom:loaded", function() {
$$("label").each(function(label) {
if (!label.htmlFor)
}
}.bindAsEventListener(ele));
});
-});
\ No newline at end of file
+});
--- /dev/null
+(function($) {
+ var $form = $("#coupon_form");
+ var $classid = $("select[name=classid]", $form);
+ $classid.on("change", function(ev) {
+ var val = ev.target.value;
+ var val_sel = "[data-behaviour=" + val + "]";
+ $("[data-behaviour]", $form).hide();
+ $("[data-behaviour] input, [data-behaviour] select", $form).prop("disabled", true);
+ $(val_sel + " input, " + val_sel + " select", $form).prop("disabled", false);
+ $(val_sel, $form).show();
+
+ });
+})(jQuery);
<link rel="stylesheet" href="/css/admin.css" type="text/css" />
<:ifParam css:><link rel="stylesheet" href="/css/<:param css:>" type="text/css" /><:or:><:eif:>
<:ajax includes:>
+<:ajax jquery:>
<script type="text/javascript" src="/js/bse.js"></script>
<script type="text/javascript" src="/js/admin.js"></script>
<script type="text/javascript" src="/js/swfobject.js"></script>
</p>
<:.call "messages"-:>
<:.set object = coupon -:>
-<form action="<:= cfg.admin_url("shopadmin") :>" method="post">
+<form action="<:= cfg.admin_url("shopadmin") :>" method="post" id="coupon_form">
<:csrfp admin_bse_coupon_add hidden:>
<fieldset>
<legend>Coupon Details</legend>
<:.call "field", "name":"description" :>
<:.call "field", "name":"release" :>
<:.call "field", "name":"expiry" :>
- <:.call "field", "name":"discount_percent" :>
+ <:.call "field", "name":"classid" :>
+ <:.set classid = cgi.param("classid") ? [ cgi.param("classid") ][0] : behaviours[0].id -:>
+ <:.for bh in behaviours -:>
+ <:.set fs = bh.behaviour.config_fields -:>
+ <:.set ordered_f = fs.keys.sort(@{a,b: fs[a].order <=> fs[b].order }) -:>
+ <:.set attr = { "data-behaviour": bh.id } -:>
+ <:.set inputattr = { } -:>
+ <:.if classid ne bh.id -:>
+ <:% attr.set("style", "display: none") -:>
+ <:% inputattr.set("disabled", "disabled") -:>
+ <:.end if -:>
+ <:.for f in ordered_f -:>
+ <:.call "field", name:f, fields: fs, options: { htmlattr: attr, inputattr: inputattr } -:>
+ <:.end for -:>
+ <:.end for -:>
<:.call "field", "name":"campaign" :>
</fieldset>
<:.call "fieldset", "name":"tiers" :>
<input type="submit" name="a_coupon_add" value="Add Coupon">
</p>
</form>
+<script type="text/javascript" src="/js/admin_coupons.js"></script>
+
</p>
<:.call "messages"-:>
<:.set object = coupon -:>
-<form action="<:= cfg.admin_url("shopadmin") :>" method="post">
+<form action="<:= cfg.admin_url("shopadmin") :>" method="post" id="coupon_form">
<:csrfp admin_bse_coupon_edit hidden:>
<input type="hidden" name="id" value="<:= coupon.id :>">
<fieldset>
<:.call "field", "name":"description" :>
<:.call "field", "name":"release" :>
<:.call "field", "name":"expiry" :>
- <:.call "field", "name":"discount_percent" :>
+ <:.call "field", "name":"classid" :>
+ <:.set classid = cgi.param("classid") ? [ cgi.param("classid") ][0] : coupon.classid -:>
+ <:.set config = coupon.config_obj -:>
+ <:.for bh in behaviours -:>
+ <:.set fs = bh.behaviour.config_fields -:>
+ <:.set ordered_f = fs.keys.sort(@{a,b: fs[a].order <=> fs[b].order }) -:>
+ <:.set attr = { "data-behaviour": bh.id } -:>
+ <:.set inputattr = { } -:>
+ <:.if classid ne bh.id -:>
+ <:% attr.set("style", "display: none") -:>
+ <:% inputattr.set("disabled", "disabled") -:>
+ <:.end if -:>
+ <:.for f in ordered_f -:>
+ <:.call "field", name:f, fields: fs, options: { htmlattr: attr, inputattr: inputattr }, object: config -:>
+ <:.end for -:>
+ <:.end for -:>
<:.call "field", "name":"campaign" :>
</fieldset>
<:.call "fieldset", "name":"tiers" :>
<input type="submit" name="a_coupon_save" value="Save Coupon">
</p>
</form>
+<script type="text/javascript" src="/js/admin_coupons.js"></script>
<:% classes.push("released") -:>
<:.end if -:>
<:.set tier_names = [] -:>
+ <:.if coupon.untiered -:>
+ <:% tier_names.push("(untiered)") -:>
+ <:.end if :>
<:.for tier in [ coupon.tier_objects ] -:>
<:% tier_names.push(tier.description) -:>
<:.end for -:>
<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_discount"><:= coupon.describe :></td>
<td class="col_tiers"><:= tier_names.size ? tier_names.join(", ") : "(none)" :></td>
<td class="col_campaign"><:= coupon.campaign :></td>
<td class="col_actions">
<:.end if -:>
<tr>
<td class="col_description"><:.if product :><a href="<:= product.admin :>"><:= product.title :></a><:.else:><:= item.title :> (product deleted)<:.end if:> <:= item.nice_options:>
- <:.if item.tier_id:><br><span class="price_tier"><:= item.tier.description :></span><:.end if:></td>
+ <:.if item.tier_id:><br><span class="price_tier"><:= item.tier.description :></span><:.end if:>
+ <:.if item.product_discount_units -:>
+ <br><span class="productdiscount">
+ <:.if item.product_discount_units < item.units -:>
+ (Saved $<:= bse.number("money", item.product_discount) :> on the first <:= item.product_discount_units :> units)
+ <:-.else -:>
+Saved $<:= bse.number("money", item.product_discount) :> on each unit
+ <:.end if -:>
+ (total $<:= bse.number("money", item.product_discount * item.product_discount_units) :>)
+ </span>
+ <:-.end if -:>
+ </td>
<td class="col_units"><:= item.units:></td>
<td class="col_unit_wsale"><:= bse.number("money", item.wholesalePrice) :></td>
<td class="col_ext_wsale"><:= bse.number("money", item.extended("wholesalePrice")) :></td>
</tr>
<:.if order.coupon -:>
<tr>
- <td>Coupon code <b><:= order.coupon_code -:></b></td>
+ <td>Coupon code <b><:= order.coupon_code -:></b> (<:= order.coupon_description :>)</td>
<td colspan="6" class="col_label_right">Discount:</td>
<td class="col_extension">(<:= bse.number("money", order.product_cost_discount) -:>)</td>
</tr>
<:- .if article.metaDescription :>
<meta name="description" content="<:= article.metaDescription:>" />
<:- .end if:>
-<:- .if article.linkAlias or article.id == 1:>
<link rel="stylesheet" type="text/css" href="/css/style-main.css">
+<:- .if article.linkAlias or article.id == 1:>
<link rel="canonical" href="<:= url(article, 1) :>" />
<:- .end if:>
<:ajax includes:>
<:= item.coupon_applies ? "Y" : "N" -:>
</td>
<:.end if -:>
- <td width="100%" align="left"> <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 width="100%" align="left"> <span class="cartproducttitle"><a href="<:= item.link | html:>"><:= item.product.description | html :></a></span> <:.if options.size:>(<:.for option in options:><:= loop.index ? ", " : "" :><:= option.desc | html:>:
+ <:= option.display |html :><:.end for:>)<:.end if -:><:.if item.session_id:>(session at <:= session.location.description | html:> <:= bse.date("%H:%M %d/%m/%Y", session.when_at) -:>)<:.end if:>
+<:-.if cart.coupon_active and !cart.coupon_cart_wide and item.product_discount_units > 0 :>
+<br><span class="itemdiscount">
+ <:-.if item.product_discount_units < item.units -:>
+Saved $<:= bse.number("money", item.product_discount) :> on the first <:= item.product_discount_units :> units
+ <:-.else -:>
+Saved $<:= bse.number("money", item.product_discount) :> on each unit
+ <:-.end if -:>
+ (total $<:= bse.number("money", item.product_discount * item.product_discount_units) :>)
+</span>
+<:-.end if -:>
+ </td>
<td nowrap align="center">
<input type="text" name="quantity_<:= loop.index :>" size="2" value="<:= item.units :>">
</td>
<:.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, or your cart contains items the code isn't valid for.
-<:.end if -:>
+<:= cart.coupon_inactive_message :>
<:.elsif cart.coupon_code ne "" -:>
Unknown coupon code
<:.end if -:>
<:.set items = [ order.items ] -:>
<:.for item in items -:>
<tr valign="middle" align="center" bgcolor="#FFFFFF">
- <td width="100%" align="left"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><a href="<:= item.product.link | html :>"><:= item.product.description | html:></a> <:= item.nice_options | html :><:.if item.session_id:>(session at <:= item.session.location.description | html:> <:= bse.date("%H:%M %d/%m/%Y", item.session.when_at) | html:>)<:.end if:></font></td>
+ <td width="100%" align="left"> <span class="cartproducttitle"><a href="<:= item.product.link | html :>"><:= item.product.description | html:></a></span> <:= item.nice_options | html :><:.if item.session_id:>(session at <:= item.session.location.description | html:> <:= bse.date("%H:%M %d/%m/%Y", item.session.when_at) | html:>)<:.end if:>
+<:-.if order.coupon_active and !order.coupon_cart_wide and item.product_discount_units > 0 :>
+<br><span class="itemdiscount">
+ <:-.if item.product_discount_units < item.units -:>
+Saved $<:= bse.number("money", item.product_discount) :> on the first <:= item.product_discount_units :> units
+ <:-.else -:>
+Saved $<:= bse.number("money", item.product_discount) :> on each unit
+ <:-.end if -:>
+ (total $<:= bse.number("money", item.product_discount * item.product_discount_units) :>)
+</span>
+<:-.end if -:>
+</td>
<td nowrap align="center"><font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><:= item.units | html :></font></td>
<td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:= bse.number("money", item.price) | html :></b></font></td>
</tr>
<:= item.coupon_applies ? "Y" : "N" -:>
</td>
<:.end if -:>
- <td width="100%" align="left"> <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 width="100%" align="left"> <span class="cartproducttitle"><a href="<:= item.link | html:>"><:= item.product.description | html :></a></span> <:.if options.size:>(<:.for option in options:><:= loop.index ? ", " : "" :><:= option.desc | html:>:
+ <:= option.display |html :><:.end for:>)<:.end if -:><:.if item.session_id:>(session at <:= session.location.description | html:> <:= bse.date("%H:%M %d/%m/%Y", session.when_at) -:>)<:.end if:>
+<:-.if cart.coupon_active and !cart.coupon_cart_wide and item.product_discount_units > 0 :>
+<br><span class="itemdiscount">
+ <:-.if item.product_discount_units < item.units -:>
+Saved $<:= bse.number("money", item.product_discount) :> on the first <:= item.product_discount_units :> units
+ <:-.else -:>
+Saved $<:= bse.number("money", item.product_discount) :> on each unit
+ <:-.end if -:>
+ (total $<:= bse.number("money", item.product_discount * item.product_discount_units) :>)
+</span>
+<:-.end if -:>
+</td>
<td nowrap align="center">
<input type="text" name="quantity_<:= loop.index :>" size="2" value="<:= item.units :>">
</td>
<:.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 -:>
+<:= cart.coupon_inactive_message :>
<:.elsif cart.coupon_code ne "" -:>
Unknown coupon code
<:.end if -:>
<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>
-<:.if cart.cfg_shipping and cfg.any_physical_products:>
+<:.if cart.cfg_shipping and cart.any_physical_products:>
<tr>
<td valign="top"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Shipping<br /> method:</font></td>
<td><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:shipping_select:></font><:error_img shipping_name:> *
<:.set options = [ item.option_list ] -:>
<:.set session = item.session -:>
<tr valign="middle" align="center" bgcolor="#FFFFFF">
- <td width="100%" align="left"> <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 width="100%" align="left"> <span class="cartproducttitle"><a href="<:= item.link | html:>"><:= item.product.description | html :></a></span> <:.if options.size:>(<:.for option in options:><:= loop.index ? ", " : "" :><:= option.desc | html:>:
+ <:= option.display |html :><:.end for:>)<:.end if -:><:.if item.session_id:>(session at <:= session.location.description | html:> <:= bse.date("%H:%M %d/%m/%Y", session.when_at) -:>)<:.end if:>
+<:-.if ordercart.coupon_active and !ordercart.coupon_cart_wide and item.product_discount_units > 0 :>
+<br><span class="itemdiscount">
+ <:-.if item.product_discount_units < item.units -:>
+Saved $<:= bse.number("money", item.product_discount) :> on the first <:= item.product_discount_units :> units
+ <:-.else -:>
+Saved $<:= bse.number("money", item.product_discount) :> on each unit
+ <:-.end if -:>
+ (total $<:= bse.number("money", item.product_discount * item.product_discount_units) :>)
+</span>
+<:-.end if -:>
+</td>
+</td>
<td nowrap align="center">
<input type="text" name="quantity_<:= loop.index :>" size="2" value="<:= item.units :>">
</td>
<:.set dist_image_uri = cfg.entryIfVar("uri", "dist_images", "/images") -:>
<:# utility definitions :>
<:-.define make_select; groups: 0, grouplabel: "label", groupid: "id",
- itemgroupid: "groupid" -:>
+ itemgroupid: "groupid", attr: {} -:>
<:-.if !default.defined -:>
<:-.set default = "" -:>
<:.end if:>
- <select name="<:= name | html :>">
+ <select name="<:= name | html :>"<:.call "elementextra", extra:attr:>>
<:- .if groups -:>
<:-.for i in list -:>
<:.if i.$itemgroupid eq "" -:>
desc - the name of the description field.
name - the name of the input elements
readonly - true to make it readonly
+ attr - extra attributes to set on the generated inputs
-:>
-<:-.define make_multicheck; readonly: 0-:>
+<:-.define make_multicheck; readonly: 0, attr: {}-:>
<:.if !readonly -:>
<input type="hidden" name="_save_<:= name -:>" value="1">
<:.end if -:>
id="<:= element_id -:>" value="<:= i.$id :>"
<:-# readonly attribute isn't valid for checkboxes -:>
<:-= readonly ? " disabled" : "" -:>
+<:-.call "elementextra", extra: attr -:>
>
<label for="<:= element_id :>"><:= i.$desc -:></label>
</li>
<:.call "error_img_n", index:0 -:>
<:.end define -:>
+<:.define elementextra -:>
+<: .set extratext = "" -:>
+<: .if extra.defined -:>
+<: .for n in extra.keys -:>
+<: .set extratext = extratext _ " " _ n _ '="' _ extra[n].escape("html") _ '"' -:>
+<: .end for -:>
+<: .end if -:>
+<:= extratext |raw -:>
+<:.end define -:>
+
<:.define input; options: {} -:>
<:# parameters:
name - field name
<: .set default = default.replace(/(\d+)\D+(\d+)\D+(\d+)/, "$3/$2/$1") -:>
<: .elsif field.type and field.type eq "time" and default ne "" -:>
<: .set default = bse.date(default =~ /:00$/ ? "%I:%M%p" : "%I:%M:%S%p", default).replace(/^0/, "").lower() -:>
+<: .elsif field.type and field.type eq "money" and default ne "" -:>
+<: .set default = bse.number("money", default) -:>
<: .end if -:>
<: .if cgi.param(name).defined -:>
<: .set default = cgi.param(name) -:>
<: .end if -:>
<: .if field.htmltype eq "textarea" -:>
-<textarea id="<:= name | html :>" name="<:= name | html :>" rows="<:= field.height ? field.height : cfg.entry("forms", "textarea_rows", 10) :>" cols=<:= field.width ? field.width : cfg.entry("forms", "textarea_cols", 60) | html :>>
+<textarea id="<:= name | html :>" name="<:= name | html :>" rows="<:= field.height ? field.height : cfg.entry("forms", "textarea_rows", 10) :>" cols=<:= field.width ? field.width : cfg.entry("forms", "textarea_cols", 60) | html :><:.call "elementextra", extra: options.inputattr :>>
<:-= default | html -:>
</textarea>
<: .elsif field.htmltype eq "checkbox" -:>
<:.set is_checked = cgi.param("_save_" _ name) ? cgi.param(name).defined : default -:>
<input type="hidden" name="_save_<:= name -:>" value="1">
-<input id="<:= name | html :>" type="checkbox" name="<:= name | html :>"<:= is_checked ? ' checked="checked"' : '' :> value="<:= field.value ? field.value : 1 | html :>" />
+<input id="<:= name | html :>" type="checkbox" name="<:= name | html :>"<:= is_checked ? ' checked="checked"' : '' :> value="<:= field.value ? field.value : 1 | html :>"<:.call "elementextra", extra: options.inputattr :> />
<: .elsif field.htmltype eq "multicheck" -:>
<:# we expect default to be a list of selected checks -:>
<:.set values = field.select["values"] -:>
<:.set default = cgi.param("_save_" _ name) ? [ cgi.param(name) ] : default -:>
<:.call "make_multicheck",
id:field.select.id,
- desc:field.select.label -:>
+ desc:field.select.label,
+ attr: options.inputattr -:>
<: .elsif field.htmltype eq "select" -:>
<:.set values = field.select["values"] -:>
<:.set values = values.is_code ? values() : values -:>
groupid : (field.select.groupid or "id"),
itemgroupid: (field.select.itemgroupid or "groupid"),
groups: field.select.groups ? (field.select.groups.is_code ? (field.select.groups)() : field.select.groups ) : 0,
- grouplabel: (field.select.grouplabel or "label")
+ grouplabel: (field.select.grouplabel or "label"),
+ attr: options.inputattr
-:>
<: .elsif field.htmltype eq 'file' -:>
<: .if default.length -:>
<span class="filename"><:= default :></span>
<: .end if -:>
-<input id="<:= name :>" type="file" name="<:= name :>" />
+<input id="<:= name :>" type="file" name="<:= name :>"<:.call "elementextra", extra: options.inputattr :> />
<:- .else -:>
<input id="<:= name | html :>" type="text" name="<:= name | html :>" value="<:= default | html :>"
<:-= field.maxlength ? ' maxlength="' _ field.maxlength _ '"' : '' |raw:>
-<:-= field.width ? ' size="' _ field.width _ '"' : '' | raw :> />
+<:-= field.width ? ' size="' _ field.width _ '"' : '' | raw :><:.call "elementextra", extra: options.inputattr :> />
<: .end if -:>
<:.end define -:>
note - display this text as a note below the field
delete - add a delete checkbox
default - a custom default value, overrides object
+ htmlattr - attributes for the wrapper div
+ inputattr - attributes for the input/select generated
-:>
<:.if field.is_hash -:>
-<div>
+ <:.set divextra = "" -:>
+ <:.if options.htmlattr :>
+ <:.for n in options.htmlattr.keys -:>
+ <:.set divextra = divextra _ " " _ n _ '="' _ options.htmlattr[n].escape("html") _ '"' -:>
+ <:.end for -:>
+ <:.end if -:>
+<div<:= divextra |raw:>>
<label for="<:= name :>"><:= field.nolabel ? "" : field.description | html :>:</label>
<span>
<:-.if field.readonly -:>
Column description;text;NO;NULL;
Column release;date;NO;NULL;
Column expiry;date;NO;NULL;
-Column discount_percent;double;NO;NULL;
+Column discount_percent;double;YES;NULL;
Column campaign;varchar(20);NO;NULL;
Column last_modified;datetime;NO;NULL;
Column untiered;int(11);NO;0;
+Column classid;varchar(20);NO;bse_simple;
+Column config;blob;NO;NULL;
Index PRIMARY;1;[id]
Index codes;1;[code]
Table bse_file_access_log
Column session_id;int(11);NO;-1;
Column product_code;varchar(80);NO;;
Column tier_id;int(11);YES;NULL;
+Column product_discount;int(11);NO;0;
+Column product_discount_units;int(11);NO;0;
Index PRIMARY;1;[id]
Index order_item_order;0;[orderId;id]
Index tier_id;0;[tier_id]
Column ccPAN;varchar(4);NO;;
Column paid_manually;int(11);NO;0;
Column coupon_id;int(11);YES;NULL;
-Column coupon_code_discount_pc;double;NO;0;
+Column coupon_code_discount_pc;double;YES;0;
Column delivery_in;int(11);YES;NULL;
+Column product_cost_discount;int(11);NO;0;
+Column coupon_cart_wide;int(11);NO;1;
+Column coupon_description;varchar(255);NO;;
Index PRIMARY;1;[id]
Index order_cchash;0;[ccNumberHash]
Index order_coupon;0;[coupon_id]
#!perl -w
use strict;
-use Test::More tests => 38;
+use Test::More tests => 45;
use_ok("BSE::Cfg");
use_ok("Squirrel::Template");
use_ok("BSE::Template");
use_ok("BSE::TB::TagOwners");
use_ok("BSE::TB::Article");
use_ok("BSE::TB::Articles");
+use_ok("BSE::TB::Coupons");
+use_ok("BSE::TB::Coupon");
use_ok('BSE::Generate');
use_ok('BSE::Generate::Article');
use_ok('BSE::Generate::Product');
use_ok("BSE::Request::Base");
use_ok("BSE::Request");
use_ok("BSE::Request::Test");
+use_ok("BSE::Coupon::Base");
+use_ok("BSE::Coupon::Percent");
+use_ok("BSE::Coupon::Dollar");
+use_ok("BSE::Coupon::ProductPercent");
+use_ok("BSE::UI::AdminShop");
my $builder = Test::Builder->new;
$builder->is_passing or $builder->BAIL_OUT;