use base 'BSE::UI::Dispatch';
use BSE::Util::HTML qw(:default popup_menu);
use BSE::Util::SQL qw(now_sqldate now_sqldatetime);
-use BSE::Shop::Util qw(:payment need_logon shop_cart_tags payment_types nice_options
+use BSE::Shop::Util qw(:payment shop_cart_tags payment_types nice_options
cart_item_opts basic_tags order_item_opts);
use BSE::CfgInfo qw(custom_class credit_card_class bse_default_country);
use BSE::TB::Orders;
use BSE::TB::OrderItems;
use BSE::Util::Tags qw(tag_error_img tag_hash tag_article);
-use Products;
+use BSE::TB::Products;
use BSE::TB::Seminars;
use DevHelp::Validate qw(dh_validate dh_validate_hash);
use Digest::MD5 'md5_hex';
use BSE::Shipping;
use BSE::Countries qw(bse_country_code);
use BSE::Util::Secure qw(make_secret);
+use BSE::Template;
-our $VERSION = "1.029";
+our $VERSION = "1.051";
+
+=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';
sub req_cart {
my ($class, $req, $msg) = @_;
- 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;
-
- $req->session->{custom} ||= {};
- my %custom_state = %{$req->session->{custom}};
+ $class->_refresh_cart($req);
+
+ my $cart = $req->cart("cart");
my $cust_class = custom_class($req->cfg);
- $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $req->cfg);
- $msg = '' unless defined $msg;
- $msg = escape_html($msg);
+ # $req->session->{custom} ||= {};
+ # my %custom_state = %{$req->session->{custom}};
- $msg ||= $req->message;
+ # $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $req->cfg);
+ $msg = $req->message($msg);
my %acts;
%acts =
(
- $cust_class->cart_actions(\%acts, \@cart, \@cart_prods, \%custom_state,
- $req->cfg),
- shop_cart_tags(\%acts, \@items, \@cart_prods, $req, 'cart'),
+ $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),
msg => $msg,
);
- $req->session->{custom} = \%custom_state;
+
+ $req->session->{custom} = { %{$cart->custom_state} };
$req->session->{order_info_confirmed} = 0;
# intended to ajax enable the shop cart with partial templates
sub req_emptycart {
my ($self, $req) = @_;
- my $old = $req->session->{cart};;
- $req->session->{cart} = [];
+ my $cart = $req->cart;
+ my $item_count = @{$cart->items};
+ $cart->empty;
my $refresh = $req->cgi->param('r');
unless ($refresh) {
$refresh = $req->user_url(shop => 'cart');
- $req->flash("msg:bse/shop/cart/empty");
+ $req->flash_notice("msg:bse/shop/cart/empty");
}
- return _add_refresh($refresh, $req, !$old);
+ return _add_refresh($refresh, $req, !$item_count);
}
sub req_add {
++$found;
$item->{units} += $quantity;
+ $req->flash_notice("msg:bse/shop/cart/addquant", [ $product, $quantity ]);
last;
}
unless ($found) {
if (defined $cart_limit && @cart >= $cart_limit) {
return $class->req_cart($req, $req->text('shop/cartfull', MSG_SHOP_CART_FULL));
}
+ my $user = $req->siteuser;
+ my ($price, $tier) = $product->price(user => $user);
push @cart,
{
productId => $product->{id},
units => $quantity,
- price=> scalar $product->price(user => scalar $req->siteuser),
+ price=> $price,
options=>$options,
+ tier => $tier ? $tier->id : "",
+ user => $user ? $user->id : 0,
%$extras,
};
+ $req->flash_notice("msg:bse/shop/cart/add", [ $product, $quantity ]);
}
$req->session->{cart} = \@cart;
++$found;
$item->{units} += $quantity;
+ $req->flash_notice("msg:bse/shop/cart/addquant", [ $product, $quantity ]);
last;
}
unless ($found) {
if (defined $cart_limit && @cart >= $cart_limit) {
return $class->req_cart($req, $req->text('shop/cartfull', MSG_SHOP_CART_FULL));
}
+ my $user = $req->siteuser;
+ my ($price, $tier) = $product->price(user => $user);
push @cart,
{
productId => $addid,
units => $quantity,
- price=> scalar $product->price(user => scalar $req->siteuser),
+ price=> $price,
options=>$options,
+ tier => $tier ? $tier->id : "",
+ user => $user ? $user->id : 0,
%$extras,
};
+ $req->flash_notice("msg:bse/shop/cart/add", [ $product, $quantity ]);
}
$req->session->{cart} = \@cart;
or next;
$item->{units} += $addition->{quantity};
+ $req->flash_notice("msg:bse/shop/cart/addquant",
+ [ $addition->{product}, $addition->{quantity} ]);
}
my $cart_limit = $req->cfg->entry('shop', 'cart_entry_limit');
my @additions = grep $_->{quantity} > 0, values %additions;
+ my $user = $req->siteuser;
my $error;
for my $addition (@additions) {
my $product = $addition->{product};
last;
}
+ my ($price, $tier) = $product->price(user => $user);
push @cart,
{
productId => $product->{id},
units => $addition->{quantity},
- price=> scalar $product->price(user => scalar $req->siteuser),
+ price=> $price,
+ tier => $tier ? $tier->id : "",
+ user => $user ? $user->id : 0,
options=>[],
%{$addition->{extras}},
};
+ $req->flash_notice("msg:bse/shop/cart/add",
+ [ $addition->{product}, $addition->{quantity} ]);
}
$req->session->{cart} = \@cart;
$refresh = $req->user_url(shop => 'cart');
}
if (@messages) {
- my $sep = $refresh =~ /\?/ ? '&' : '?';
-
- for my $message (@messages) {
- $refresh .= $sep . "m=" . escape_uri($message);
- $sep = '&';
- }
+ $req->flash_error($_) for @messages;
}
# speed for ajax
}
}
+sub _any_physical_products {
+ my $prods = shift;
+
+ for my $prod (@$prods) {
+ if ($prod->weight) {
+ return 1;
+ last;
+ }
+ }
+
+ return 0;
+}
+
+=item checkout
+
+Display the checkout form.
+
+Variables:
+
+=over
+
+=item *
+
+old - a function returning the old value for most fields.
+
+=item *
+
+errors - any errors from attempting to progress to payment
+
+=item *
+
+need_delivery - true if the user indicates they want separate delivery
+and billing details.
+
+=back
+
+Template C<checkoutnew>
+
+=cut
+
sub req_checkout {
my ($class, $req, $message, $olddata) = @_;
+ $class->_refresh_cart($req);
+
my $errors = {};
if (defined $message) {
if (ref $message) {
my $need_delivery = ( $olddata ? $cgi->param("need_delivery") : $req->session->{order_need_delivery} ) || 0;
$class->update_quantities($req);
- my @cart = @{$req->session->{cart}};
+ my $cart = $req->cart("checkout");
+ my @cart = @{$cart->items};
@cart or return $class->req_cart($req);
- my @cart_prods;
- my @items = $class->_build_items($req, \@cart_prods);
+ my @cart_prods = @{$cart->products};
+ my @items = @{$cart->items};
- if (my ($msg, $id) = $class->_need_logon($req, \@cart, \@cart_prods)) {
+ if ($cart->need_logon) {
+ my ($msg, $id) = $cart->need_logon_message;
return $class->_refresh_logon($req, $msg, $id);
- return;
}
my $user = $req->siteuser;
- $req->session->{custom} ||= {};
- my %custom_state = %{$req->session->{custom}};
-
- my $cust_class = custom_class($cfg);
- $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $cfg);
-
- my $affiliate_code = $req->session->{affiliate_code};
- defined $affiliate_code or $affiliate_code = '';
-
my $order_info = $req->session->{order_info};
my $billing_map = BSE::TB::Order->billing_to_delivery_map;
my ($delivery_in, $shipping_cost, $shipping_method);
my $shipping_error = '';
my $shipping_name = '';
- my $prompt_ship = $cfg->entry("shop", "shipping", 0);
- if ($prompt_ship) {
- my $work_order;
- $work_order = $order_info unless $olddata;
- unless ($work_order) {
- my %fake_order;
- my %fields = $class->_order_fields($req);
- $class->_order_hash($req, \%fake_order, \%fields, user => 1);
- $work_order = \%fake_order;
- }
+ my $prompt_ship = $cart->cfg_shipping;
- # Get a list of couriers
- my $sel_cn = $old->("shipping_name") || "";
- my $country_field = $need_delivery ? "delivCountry" : "billCountry";
- my $country = $old->($country_field)
- || bse_default_country($cfg);
- my $country_code = bse_country_code($country);
- my $suburb = $old->($need_delivery ? "delivSuburb" : "billSuburb");
- my $postcode = $old->($need_delivery ? "delivPostCode" : "billPostCode");
+ my $physical = $cart->any_physical_products;
- $country_code
- or $errors->{$country_field} = "Unknown country name $country";
-
- my @couriers = BSE::Shipping->get_couriers($cfg);
-
- if ($country_code and $postcode) {
- @couriers = grep $_->can_deliver(country => $country_code,
- suburb => $suburb,
- postcode => $postcode), @couriers;
- }
+ if ($prompt_ship) {
+ my $sel_cn = $old->("shipping_name") || "";
+ if ($physical) {
+ my $work_order;
+ $work_order = $order_info unless $olddata;
+ unless ($work_order) {
+ my %fake_order;
+ my %fields = $class->_order_fields($req);
+ $class->_order_hash($req, \%fake_order, \%fields, user => 1);
+ $work_order = \%fake_order;
+ }
+
+ # Get a list of couriers
+ my $country_field = $need_delivery ? "delivCountry" : "billCountry";
+ my $country = $old->($country_field)
+ || bse_default_country($cfg);
+ my $country_code = bse_country_code($country);
+ my $suburb = $old->($need_delivery ? "delivSuburb" : "billSuburb");
+ my $postcode = $old->($need_delivery ? "delivPostCode" : "billPostCode");
+
+ $country_code
+ or $errors->{$country_field} = "Unknown country name $country";
+
+ my @couriers = BSE::Shipping->get_couriers($cfg);
+
+ if ($country_code and $postcode) {
+ @couriers = grep $_->can_deliver(country => $country_code,
+ suburb => $suburb,
+ postcode => $postcode), @couriers;
+ }
+
+ my ($sel_cour) = grep $_->name eq $sel_cn, @couriers;
+ # if we don't match against the list (perhaps because of a country
+ # change) the first item in the list will be selected by the
+ # browser anyway, so select it ourselves and display an
+ # appropriate shipping cost for the item
+ unless ($sel_cour) {
+ $sel_cour = $couriers[0];
+ $sel_cn = $sel_cour->name;
+ }
+ if ($sel_cour and $postcode and $suburb and $country_code) {
+ my @parcels = BSE::Shipping->package_order($cfg, $order_info, \@items);
+ $shipping_cost = $sel_cour->calculate_shipping
+ (
+ parcels => \@parcels,
+ suburb => $suburb,
+ postcode => $postcode,
+ country => $country_code,
+ products => \@cart_prods,
+ items => \@items,
+ );
+ $delivery_in = $sel_cour->delivery_in();
+ $shipping_method = $sel_cour->description();
+ $shipping_name = $sel_cour->name;
+ unless (defined $shipping_cost) {
+ $shipping_error = "$shipping_method: " . $sel_cour->error_message;
+ $errors->{shipping_name} = $shipping_error;
+
+ # use the last one, which should be the Null shipper
+ $sel_cour = $couriers[-1];
+ $sel_cn = $sel_cour->name;
+ $shipping_method = $sel_cour->description;
+ }
+ }
- my ($sel_cour) = grep $_->name eq $sel_cn, @couriers;
- # if we don't match against the list (perhaps because of a country
- # change) the first item in the list will be selected by the
- # browser anyway, so select it ourselves and display an
- # appropriate shipping cost for the item
- unless ($sel_cour) {
- $sel_cour = $couriers[0];
- $sel_cn = $sel_cour->name;
+ $shipping_select = popup_menu
+ (
+ -name => "shipping_name",
+ -id => "shipping_name",
+ -values => [ map $_->name, @couriers ],
+ -labels => { map { $_->name => $_->description } @couriers },
+ -default => $sel_cn,
+ );
}
- if ($sel_cour and $postcode and $suburb and $country_code) {
- my @parcels = BSE::Shipping->package_order($cfg, $order_info, \@items);
- $shipping_cost = $sel_cour->calculate_shipping
+ else {
+ $sel_cn = $shipping_name = "none";
+ $shipping_method = "Nothing to ship!";
+ $shipping_select = popup_menu
(
- parcels => \@parcels,
- suburb => $suburb,
- postcode => $postcode,
- country => $country_code,
- products => \@cart_prods,
- items => \@items,
+ -name => "shipping_name",
+ -id => "shipping_name",
+ -values => [ "none" ],
+ -labels => { none => $shipping_method },
+ -default => $sel_cn,
);
- $delivery_in = $sel_cour->delivery_in();
- $shipping_method = $sel_cour->description();
- $shipping_name = $sel_cour->name;
- unless (defined $shipping_cost) {
- $shipping_error = "$shipping_method: " . $sel_cour->error_message;
- $errors->{shipping_name} = $shipping_error;
-
- # use the last one, which should be the Null shipper
- $sel_cour = $couriers[-1];
- $sel_cn = $sel_cour->name;
- $shipping_method = $sel_cour->description;
- }
}
-
- $shipping_select = popup_menu
- (
- -name => "shipping_name",
- -id => "shipping_name",
- -values => [ map $_->name, @couriers ],
- -labels => { map { $_->name => $_->description } @couriers },
- -default => $sel_cn,
- );
}
+ my $cust_class = custom_class($cfg);
+
if (!$message && keys %$errors) {
$message = $req->message($errors);
}
+ $cart->set_shipping_cost($shipping_cost);
+ $cart->set_shipping_method($shipping_method);
+ $cart->set_shipping_name($shipping_name);
+ $cart->set_delivery_in($delivery_in);
+ $req->set_variable(old => $old);
+ $req->set_variable(errors => $errors);
+ $req->set_variable(need_delivery => $need_delivery);
my $item_index = -1;
my @options;
my %acts;
%acts =
(
- shop_cart_tags(\%acts, \@items, \@cart_prods, $req, 'checkout'),
+ shop_cart_tags(\%acts, $cart, $req, 'checkout'),
basic_tags(\%acts),
message => $message,
msg => $message,
old => sub { escape_html($old->($_[0])); },
$cust_class->checkout_actions(\%acts, \@cart, \@cart_prods,
- \%custom_state, $req->cgi, $cfg),
+ $cart->custom_state, $req->cgi, $cfg),
ifUser => [ \&tag_ifUser, $user ],
user => $user ? [ \&tag_hash, $user ] : '',
- affiliate_code => escape_html($affiliate_code),
+ affiliate_code => escape_html($cart->affiliate_code),
error_img => [ \&tag_error_img, $cfg, $errors ],
ifShipping => $prompt_ship,
shipping_select => $shipping_select,
- delivery_in => escape_html($delivery_in),
+ delivery_in => escape_html(defined $delivery_in ? $delivery_in : ""),
shipping_cost => $shipping_cost,
shipping_method => escape_html($shipping_method),
shipping_error => escape_html($shipping_error),
shipping_name => $shipping_name,
+ ifPhysical => $physical,
ifNeedDelivery => $need_delivery,
);
- $req->session->{custom} = \%custom_state;
- my $tmp = $acts{total};
- $acts{total} =
- sub {
- my $total = &$tmp();
- $total += $shipping_cost if $total and $shipping_cost;
- return $total;
- };
+ $req->session->{custom} = $cart->custom_state;
return $req->response('checkoutnew', \%acts);
}
sub req_checkupdate {
- my ($class, $req) = @_;
+ my ($self, $req) = @_;
- $req->session->{cart} ||= [];
- my @cart = @{$req->session->{cart}};
- my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
- $req->session->{custom} ||= {};
- my %custom_state = %{$req->session->{custom}};
- custom_class($req->cfg)
- ->checkout_update($req->cgi, \@cart, \@cart_prods, \%custom_state, $req->cfg);
- $req->session->{custom} = \%custom_state;
+ my $cart = $req->cart("checkupdate");
+
+ $self->update_quantities($req);
+
+ $req->session->{custom} = $cart->custom_state;
$req->session->{order_info_confirmed} = 0;
-
- return $class->req_checkout($req, "", 1);
+
+ my %fields = $self->_order_fields($req);
+ my %values;
+ $self->_order_hash($req, \%values, \%fields);
+ $req->session->{order_info} = \%values;
+ $req->session->{order_need_delivery} = $req->cgi->param("need_delivery");
+
+ return $req->get_refresh($req->user_url(shop => "checkout"));
}
sub req_remove_item {
$req->session->{cart} ||= [];
my @cart = @{$req->session->{cart}};
if ($index >= 0 && $index < @cart) {
- splice(@cart, $index, 1);
+ my ($item) = splice(@cart, $index, 1);
+ my $product = BSE::TB::Products->getByPkey($item->{productId});
+ $req->flash_notice("msg:bse/shop/cart/remove", [ $product ]);
}
$req->session->{cart} = \@cart;
$req->session->{order_info_confirmed} = 0;
$values->{$delivery} = $values->{$billing};
}
}
+ my $cart = $req->cart;
+ if ($cart->cfg_shipping && $cart->any_physical_products) {
+ my $shipping_name = $cgi->param("shipping_name");
+ defined $shipping_name and $values->{shipping_name} = $shipping_name;
+ }
}
# saves order and refresh to payment page
$class->_validate_cfg($req, \$msg)
or return $class->req_cart($req, $msg);
- my @products;
- my @items = $class->_build_items($req, \@products);
+ my $cart = $req->cart("order");
+
+ my @products = @{$cart->products};
+ my @items = @{$cart->items};
my $id;
- if (($msg, $id) = $class->_need_logon($req, \@items, \@products)) {
+ if ($cart->need_logon) {
+ my ($msg, $id) = $cart->need_logon_message;
return $class->_refresh_logon($req, $msg, $id);
}
dh_validate_hash(\%values, \%errors, { rules=>\%rules, fields=>\%fields },
$cfg, 'Shop Order Validation');
- my $prompt_ship = $cfg->entry("shop", "shipping", 0);
+ my $prompt_ship = $cart->cfg_shipping;
if ($prompt_ship) {
my $country = $values{delivCountry} || bse_default_country($cfg);
my $country_code = bse_country_code($country);
keys %errors
and return $class->req_checkout($req, \%errors, 1);
- $class->_fillout_order($req, \%values, \@items, \@products, \$msg, 'payment')
+ $class->_fillout_order($req, \%values, \$msg, 'payment')
or return $class->req_checkout($req, $msg, 1);
$req->session->{order_info} = \%values;
# ideally supply order_id to be consistent with a_payment.
my $order_id = $cgi->param('orderid') || $cgi->param("order_id");
+ my $cart;
if ($order_id) {
$order_id =~ /^\d+$/
or return $class->req_cart($req, "No or invalid order id supplied");
-
+
my $user = $req->siteuser
or return $class->_refresh_logon
($req, "Please logon before paying your existing order", "logonpayorder",
undef, { a_show_payment => 1, orderid => $order_id });
-
+
require BSE::TB::Orders;
$order = BSE::TB::Orders->getByPkey($order_id)
or return $class->req_cart($req, "Unknown order id");
-
+
$order->siteuser_id == $user->id
or return $class->req_cart($req, "You can only pay for your own orders");
-
+
$order->paidFor
and return $class->req_cart($req, "Order $order->{id} has been paid");
-
- @items = $order->items;
- @products = $order->products;
+
+ $cart = $order;
}
else {
$req->session->{order_info_confirmed}
or return $class->req_checkout($req, 'Please proceed via the checkout page');
-
+
$req->session->{cart} && @{$req->session->{cart}}
or return $class->req_cart($req, "Your cart is empty");
-
+
$order = $req->session->{order_info}
or return $class->req_checkout($req, "You need to enter order information first");
- @items = $class->_build_items($req, \@products);
+ $cart = $req->cart("payment");
}
$errors ||= {};
$errors and $payment = $cgi->param('paymentType');
defined $payment or $payment = $payment_types[0];
+ $cart->set_shipping_cost($order->{shipping_cost});
+ $cart->set_shipping_method($order->{shipping_method});
+ $cart->set_shipping_name($order->{shipping_name});
+ $req->set_variable(errors => $errors);
+
my %acts;
%acts =
(
message => $msg,
msg => $msg,
order => [ \&tag_hash, $order ],
- shop_cart_tags(\%acts, \@items, \@products, $req, 'payment'),
+ shop_cart_tags(\%acts, $cart, $req, 'payment'),
ifMultPaymentTypes => @payment_types > 1,
checkedPayment => [ \&tag_checkedPayment, $payment, \%types_by_name ],
ifPayments => [ \&tag_ifPayments, \@payment_types, \%types_by_name ],
paymentTypeId => [ \&tag_paymentTypeId, \%types_by_name ],
error_img => [ \&tag_error_img, $cfg, $errors ],
- total => $order->{total},
+ total => $cart->total,
delivery_in => $order->{delivery_in},
shipping_cost => $order->{shipping_cost},
shipping_method => $order->{shipping_method},
$acts{"checkedIfFirst$name"} = $payment_types[0] == $id ? "checked " : "";
$acts{"checkedPayment$name"} = $payment == $id ? 'checked="checked" ' : "";
}
+ $req->set_variable(ordercart => $cart);
+ $req->set_variable(order => $order);
+ $req->set_variable(is_order => !!$order_id);
return $req->response('checkoutpay', \%acts);
}
for my $field (keys %fields) {
unless ($nostore{$field}) {
- ($order_values->{$field}) = $cgi->param($field);
+ if (my ($value) = $cgi->param($field)) {
+ $order_values->{$field} = $value;
+ }
}
}
}
}
else {
+ my $cart = $req->cart("payment");
+
$order_values->{filled} = 0;
$order_values->{paidFor} = 0;
- my @items = $class->_build_items($req, \@products);
+ my @items = $class->_build_items($req);
+ @products = $cart->products;
if ($session->{order_work}) {
$order = BSE::TB::Orders->getByPkey($session->{order_work});
my @allbutid = @columns;
shift @allbutid;
@{$order}{@allbutid} = @data;
-
+
$order->clear_items;
delete $session->{order_work};
eval {
defined $item{session_id} or $item{session_id} = 0;
$item{options} = ""; # not used for new orders
my @data = @item{@item_cols};
- shift @data;
+ shift @data;
my $dbitem = BSE::TB::OrderItems->add(@data);
push @dbitems, $dbitem;
$self->_send_order($req, $order);
- # empty the cart ready for the next order
- delete @{$req->session}{qw/order_info order_info_confirmed order_need_delivery cart order_work/};
+ my $cart = $req->cart;
+ $cart->empty;
}
+=item orderdone
+
+Display the order after the order is complete.
+
+Sets variables:
+
+=over
+
+=item *
+
+C<order> - the new L<BSE::TB::Order> object.
+
+=back
+
+=cut
+
sub req_orderdone {
my ($class, $req) = @_;
my $order = BSE::TB::Orders->getByPkey($id)
or return $class->req_cart($req);
my @items = $order->items;
- my @products = map { Products->getByPkey($_->{productId}) } @items;
+ my @products = map { BSE::TB::Products->getByPkey($_->{productId}) } @items;
my @item_cols = BSE::TB::OrderItem->columns;
- my %copy_cols = map { $_ => 1 } Product->columns;
+ my %copy_cols = map { $_ => 1 } BSE::TB::Product->columns;
delete @copy_cols{@item_cols};
my @copy_cols = keys %copy_cols;
my @showitems;
$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]}) },
$acts{"if${name}Payment"} = $order->{paymentType} == $id;
}
+ $req->set_variable(order => $order);
+ $req->set_variable(payment_types => \@pay_types);
+
return $req->response('checkoutfinal', \%acts);
}
$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 {
ifSubscribingTo => [ \&tag_ifSubscribingTo, \%subscribing_to ],
);
+ my %vars =
+ (
+ order => $order,
+ );
+
my $email_order = $cfg->entryBool('shop', 'email_order', $Constants::SHOP_EMAIL_ORDER);
require BSE::ComposeMail;
if ($email_order) {
$acts{cardNumber} = $cgi->param('cardNumber');
$acts{cardExpiry} = $cgi->param('cardExpiry');
$acts{cardVerify} = $cgi->param('cardVerify');
+ @vars{qw(cardNumber cardExpiry cardVerify)} =
+ @acts{qw(cardNumber cardExpiry cardVerify)};
}
my $mailer = BSE::ComposeMail->new(cfg => $cfg);
template => "mailorder",
log_component => "shop:sendorder:mailowner",
log_object => $order,
- log_msg => "Order $order->{id} sent to site owner",
+ log_msg => "Send Order No. $order->{id} to admin",
+ vars => \%vars,
);
unless ($noencrypt) {
}
delete @acts{qw/cardNumber cardExpiry cardVerify/};
+ delete @vars{qw/cardNumber cardExpiry cardVerify/};
}
my $to_email = $order->billEmail;
my $user = $req->siteuser;
acts => \%acts,
log_component => "shop:sendorder:mailbuyer",
log_object => $order,
- log_msg => "Order $order->{id} sent to purchaser $to_email",
+ log_msg => "Send Order No. $order->{id} to customer ($to_email)",
+ vars => \%vars,
);
my $bcc_order = $cfg->entry("shop", "bcc_email");
if ($bcc_order) {
if ($msgid) {
$msg = $req->cfg->entry('messages', $msgid, $msg);
}
- $parms{message} = $msg if $msg;
+ $parms{m} = $msg if $msg;
$parms{mid} = $msgid if $msgid;
$url .= "?" . join("&", map "$_=".escape_uri($parms{$_}), keys %parms);
return BSE::Template->get_refresh($url, $req->cfg);
}
-sub _need_logon {
- my ($class, $req, $cart, $cart_prods) = @_;
-
- return need_logon($req, $cart, $cart_prods);
-}
-
sub tag_checkedPayment {
my ($payment, $types_by_name, $args) = @_;
sub update_quantities {
my ($class, $req) = @_;
+ # FIXME: should use the cart class to update quantities
my $session = $req->session;
my $cgi = $req->cgi;
my $cfg = $req->cfg;
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 {
- my ($class, $req, $products) = @_;
+ my ($class, $req) = @_;
my $session = $req->session;
+ my $cart = $req->cart;
$session->{cart}
or return;
my @msgs;
my @cart = @{$req->session->{cart}}
or return;
my @items;
- my @prodcols = Product->columns;
+ my @prodcols = BSE::TB::Product->columns;
my @newcart;
my $today = now_sqldate();
- for my $item (@cart) {
+ for my $item ($cart->items) {
my %work = %$item;
- my $product = Products->getByPkey($item->{productId});
+ my $product = $item->product;
if ($product) {
- (my $comp_release = $product->{release}) =~ s/ .*//;
- (my $comp_expire = $product->{expire}) =~ s/ .*//;
- $comp_release le $today
+ $product->is_released
or do { push @msgs, "'$product->{title}' has not been released yet";
next; };
- $today le $comp_expire
- or do { push @msgs, "'$product->{title}' has expired"; next; };
- $product->{listed}
+ $product->is_expired
+ and do { push @msgs, "'$product->{title}' has expired"; next; };
+ $product->listed
or do { push @msgs, "'$product->{title}' not available"; next; };
for my $col (@prodcols) {
$work{$col} = $product->$col() unless exists $work{$col};
}
- $work{price} = $product->price(user => scalar $req->siteuser);
+ my ($price, $tier) = $product->price(user => scalar $req->siteuser);
+ $work{price} = $price;
+ $work{tier_id} = $tier ? $tier->id : undef;
$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;
- push @$products, $product;
}
}
}
sub _fillout_order {
- my ($class, $req, $values, $items, $products, $rmsg, $how) = @_;
+ my ($class, $req, $values, $rmsg, $how) = @_;
my $session = $req->session;
my $cfg = $req->cfg;
my $cgi = $req->cgi;
- my $total = 0;
- my $total_gst = 0;
- my $total_wholesale = 0;
- for my $item (@$items) {
- $total += $item->{extended_retailPrice};
- $total_gst += $item->{extended_gst};
- $total_wholesale += $item->{extended_wholesale};
+ my $cart = $req->cart($how);
+
+ if ($cart->is_empty) {
+ $$rmsg = "Your cart is empty";
+ return;
}
- $values->{total} = $total;
- $values->{gst} = $total_gst;
- $values->{wholesaleTotal} = $total_wholesale;
- my $prompt_ship = $cfg->entry("shop", "shipping", 0);
+ # FIXME? this doesn't take discounting into effect
+ $values->{gst} = $cart->gst;
+ $values->{wholesaleTotal} = $cart->wholesaleTotal;
+
+ my $items = $cart->items;
+ my $products = $cart->products;
+ my $prompt_ship = $cart->cfg_shipping;
if ($prompt_ship) {
- my ($courier) = BSE::Shipping->get_couriers($cfg, $cgi->param("shipping_name"));
- my $country_code = bse_country_code($values->{delivCountry});
- if ($courier) {
- unless ($courier->can_deliver(country => $country_code,
- suburb => $values->{delivSuburb},
- postcode => $values->{delivPostCode})) {
- $cgi->param("courier", undef);
- $$rmsg =
- "Can't use the selected courier ".
- "(". $courier->description(). ") for this order.";
- return;
+ if (_any_physical_products($products)) {
+ my ($courier) = BSE::Shipping->get_couriers($cfg, $cgi->param("shipping_name"));
+ my $country_code = bse_country_code($values->{delivCountry});
+ if ($courier) {
+ unless ($courier->can_deliver(country => $country_code,
+ suburb => $values->{delivSuburb},
+ postcode => $values->{delivPostCode})) {
+ $cgi->param("courier", undef);
+ $$rmsg =
+ "Can't use the selected courier ".
+ "(". $courier->description(). ") for this order.";
+ return;
+ }
+ my @parcels = BSE::Shipping->package_order($cfg, $values, $items);
+ my $cost = $courier->calculate_shipping
+ (
+ parcels => \@parcels,
+ country => $country_code,
+ suburb => $values->{delivSuburb},
+ postcode => $values->{delivPostCode},
+ products => $products,
+ items => $items,
+ );
+ if (!defined $cost and $courier->name() ne 'contact') {
+ my $err = $courier->error_message();
+ $$rmsg = "Error calculating shipping cost";
+ $$rmsg .= ": $err" if $err;
+ return;
+ }
+ $values->{shipping_method} = $courier->description();
+ $values->{shipping_name} = $courier->name;
+ $values->{shipping_cost} = $cost;
+ $values->{shipping_trace} = $courier->trace;
+ $values->{delivery_in} = $courier->delivery_in();
}
- my @parcels = BSE::Shipping->package_order($cfg, $values, $items);
- my $cost = $courier->calculate_shipping
- (
- parcels => \@parcels,
- country => $country_code,
- suburb => $values->{delivSuburb},
- postcode => $values->{delivPostCode},
- products => $products,
- items => $items,
- );
- if (!$cost and $courier->name() ne 'contact') {
- my $err = $courier->error_message();
- $$rmsg = "Error calculating shipping cost";
- $$rmsg .= ": $err" if $err;
+ else {
+ # XXX: What to do?
+ $$rmsg = "Error: no usable courier found.";
return;
}
- $values->{shipping_method} = $courier->description();
- $values->{shipping_name} = $courier->name;
- $values->{shipping_cost} = $cost;
- $values->{shipping_trace} = $courier->trace;
- #$values->{delivery_in} = $courier->delivery_in();
- $values->{total} += $values->{shipping_cost};
}
else {
- # XXX: What to do?
- $$rmsg = "Error: no usable courier found.";
- return;
+ $values->{shipping_method} = "Nothing to ship!";
+ $values->{shipping_name} = "none";
+ $values->{shipping_cost} = 0;
+ $values->{shipping_trace} = "All products have zero weight.";
}
}
+ 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_shipping_name($values->{shipping_name});
+ $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);
eval {
local $SIG{__DIE__};
+ $session->{custom} = $cart->custom_state || {};
my %custom = %{$session->{custom}};
$cust_class->order_save($cgi, $values, $items, $items,
\%custom, $cfg);
my $product;
if ($addid) {
$product = BSE::TB::Seminars->getByPkey($addid);
- $product ||= Products->getByPkey($addid);
+ $product ||= BSE::TB::Products->getByPkey($addid);
}
unless ($product) {
$$error = "Cannot find product $addid";
my $product;
if (defined $code) {
$product = BSE::TB::Seminars->getBy(product_code => $code);
- $product ||= Products->getBy(product_code => $code);
+ $product ||= BSE::TB::Products->getBy(product_code => $code);
}
unless ($product) {
$$error = "Cannot find product code $code";
print STDERR "not on base host ('$ENV{SERVER_NAME}' cmp '$basehost' '$protocol cmp '$baseprot' $baseport cmp $port\n" if $debug;
$onbase = 0;
}
- my $url = $onbase ? $secure_url : $base_url;
+ my $base = $onbase ? $secure_url : $base_url;
my $finalbase = $onbase ? $base_url : $secure_url;
$refresh = $finalbase . $refresh unless $refresh =~ /^\w+:/;
+ my $sessionid = $req->session->{_session_id};
+ require BSE::SessionSign;
+ my $sig = BSE::SessionSign->make($sessionid);
+ my $url = $cfg->user_url("user", undef,
+ -base => $base,
+ setcookie => $sessionid,
+ s => $sig,
+ r => $refresh);
print STDERR "Heading to $url to setcookie\n" if $debug;
- $url .= "/cgi-bin/user.pl?setcookie=".$req->session->{_session_id};
- $url .= "&r=".CGI::escape($refresh);
- return BSE::Template->get_refresh($url, $cfg);
+ return $req->get_refresh($url);
}
}
- return BSE::Template->get_refresh($refresh, $cfg);
+ return $req->get_refresh($refresh);
}
sub _same_options {
my $order = $self->_paypal_order($req, \$msg)
or return $self->req_show_payment($req, { _ => $msg });
- $req->flash("msg:bse/shop/paypal/cancelled");
+ $req->flash_notice("msg:bse/shop/paypal/cancelled");
my $url = $req->user_url(shop => "show_payment");
return $req->get_refresh($url);
}
+sub _refresh_cart {
+ my ($self, $req) = @_;
+
+ my $user = $req->siteuser
+ or return;
+
+ my $cart = $req->session->{cart}
+ or return;
+
+ for my $item (@$cart) {
+ if (!$item->{user} || $item->{user} != $user->id) {
+ my $product = BSE::TB::Products->getByPkey($item->{productId})
+ or next;
+ my ($price, $tier) = $product->price(user => $user);
+ $item->{price} = $price;
+ $item->{tier} = $tier ? $tier->id : "";
+ }
+ }
+
+ $req->session->{cart} = $cart;
+}
+
1;
+
+=back
+
+=head1 TAGS
+
+=head2 Cart page
+
+=over 4
+
+=item iterator ... items
+
+Iterates over the items in the shopping cart, setting the C<item> tag
+for each one.
+
+=item item I<field>
+
+Retreives the given field from the item. This can include product
+fields for this item.
+
+=item index
+
+The numeric index of the current item.
+
+=item extended [<field>]
+
+The "extended price", the product of the unit cost and the number of
+units for the current item in the cart. I<field> defaults to the
+price of the product.
+
+=item money I<which> <field>
+
+Formats the given field as a money value (without a currency symbol.)
+
+=item count
+
+The number of items in the cart.
+
+=item ifUser
+
+Conditional tag, true if a registered user is logged in.
+
+=item user I<field>
+
+Retrieved the given field from the currently logged in user, if any.
+
+=back
+
+=head2 Checkout tags
+
+This has the same tags as the L<Cart page>, and some extras:
+
+=over 4
+
+=item total
+
+The total cost of all items in the cart.
+
+This will need to be formatted as a money value with the C<money> tag.
+
+=item message
+
+An error message, if a validation error occurred.
+
+=item old I<field>
+
+The previously entered value for I<field>. This should be used as the
+value for the various checkout fields, so that if a validation error
+occurs the user won't need to re-enter values.
+
+=back
+
+=head2 Completed order
+
+These tags are used in the F<checkoutfinal_base.tmpl>.
+
+=over 4
+
+=item item I<field>
+
+=item product I<field>
+
+This is split out for these forms.
+
+=item order I<field>
+
+Order fields.
+
+=item ifSubscribingTo I<subid>
+
+Can be used to check if this order is intended to be subscribing to a
+subscription.
+
+=back
+
+=head2 Mailed order tags
+
+These tags are used in the emails sent to the user to confirm an order
+and in the encrypted copy sent to the site administrator:
+
+=over 4
+
+=item *
+
+C<iterate> ... C<items>
+
+Iterates over the items in the order.
+
+=item *
+
+C<item> I<field>
+
+Access to the given field in the order item.
+
+=item *
+
+C<product> I<field>
+
+Access to the product field for the current order item.
+
+=item *
+
+C<order> I<field>
+
+Access to fields of the order.
+
+=item *
+
+C<extended> I<field>
+
+The product of the I<field> in the current item and it's quantity.
+
+=item *
+
+C<money> I<tag> I<parameters>
+
+Formats the given field as a money value.
+
+=back
+
+The mail generation template can use extra formatting specified with
+'|format':
+
+=over 4
+
+=item *
+
+m<number>
+
+Format the value as a I<number> wide money value.
+
+=item *
+
+%<format>
+
+Performs sprintf formatting on the value.
+
+=item *
+
+<number>
+
+Left justifies the value in a I<number> wide field.
+
+=back
+
+The order email sent to the site administrator has a couple of extra
+fields:
+
+=over
+
+=item *
+
+cardNumber
+
+The credit card number of the user's credit card.
+
+=item *
+
+cardExpiry
+
+The entered expiry date for the user's credit card.
+
+=back
+
+=head2 Order fields
+
+These names can be used with the <: order ... :> tag.
+
+Monetary values should typically be used with <:money order ...:>
+
+=over
+
+=item *
+
+id
+
+The order id or order number.
+
+=item *
+
+delivFirstName, delivLastName, delivStreet, delivSuburb, delivState,
+delivPostCode, delivCountry - Delivery information for the order.
+
+=item *
+
+billFirstName, billLastName, billStreet, billSuburb, billState,
+billPostCode, billCountry - Billing information for the order.
+
+=item *
+
+telephone, facsimile, emailAddress - Contact information for the
+order.
+
+=item *
+
+total - Total price of the order.
+
+=item *
+
+wholesaleTotal - Wholesale cost of the total. Your costs, if you
+entered wholesale prices for the products.
+
+=item *
+
+gst - GST (in Australia) payable on the order, if you entered GST for
+the products.
+
+=item *
+
+orderDate - When the order was made.
+
+=item *
+
+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 - The time and date when the order was filled.
+
+=item *
+
+whoFilled - The user who marked the order as filled.
+
+=item *
+
+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 - A custom payment handler can fill this with receipt
+information.
+
+=item *
+
+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 - 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
+
+=item *
+
+productId - The product id of this item.
+
+=item *
+
+orderId - The order Id.
+
+=item *
+
+units - The number of units for this item.
+
+=item *
+
+price - The price paid for the product.
+
+=item *
+
+wholesalePrice - The wholesale price for the product.
+
+=item *
+
+gst - The gst for the product.
+
+=item *
+
+options - A comma separated list of options specified for this item.
+These correspond to the option names in the product.
+
+=back
+
+=head2 Options
+
+New with 0.10_04 is the facility to set options for each product.
+
+The cart, checkout and checkoutfinal pages now include the following
+tags:
+
+=over
+
+=item *
+
+C<iterator> ... <options>
+
+within an item, iterates over the options for this item in the cart.
+Sets the item tag.
+
+=item *
+
+C<option> I<field>
+
+Retrieves the given field from the option, possible field names are:
+
+=over
+
+=item *
+
+id - The type/identifier for this option. eg. msize for a male
+clothing size field.
+
+=item *
+
+value - The underlying value of the option, eg. XL.
+
+=item *
+
+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 - The description of the value from the product options hash.
+eg. "Extra large".
+
+=back
+
+=item *
+
+ifOptions - A conditional tag, true if the current cart item has any
+options.
+
+=item *
+
+options - A simple rendering of the options as a parenthesized
+comma-separated list.
+
+=back
+
+=cut