3 use base 'BSE::UI::Dispatch';
4 use BSE::Util::HTML qw(:default popup_menu);
5 use BSE::Util::SQL qw(now_sqldate now_sqldatetime);
6 use BSE::Shop::Util qw(:payment need_logon shop_cart_tags payment_types nice_options
7 cart_item_opts basic_tags order_item_opts);
8 use BSE::CfgInfo qw(custom_class credit_card_class bse_default_country);
10 use BSE::TB::OrderItems;
11 use BSE::Util::Tags qw(tag_error_img tag_hash tag_article);
13 use BSE::TB::Seminars;
14 use DevHelp::Validate qw(dh_validate dh_validate_hash);
15 use Digest::MD5 'md5_hex';
17 use BSE::Countries qw(bse_country_code);
18 use BSE::Util::Secure qw(make_secret);
20 our $VERSION = "1.034";
22 use constant MSG_SHOP_CART_FULL => 'Your shopping cart is full, please remove an item and try adding an item again';
45 # map of SiteUser field names to order field names - mostly
48 name1 => 'billFirstName',
49 name2 => 'billLastName',
50 street => 'billStreet',
51 street2 => 'billStreet2',
52 suburb => 'billSuburb',
53 postcode => 'billPostCode',
55 country => 'billCountry',
56 telephone => 'billTelephone',
57 facsimile => 'billFacsimile',
58 mobile => 'billMobile',
59 organization => 'billOrganization',
61 delivFacsimile => 'facsimile',
62 delivTelephone => 'telephone',
63 delivEmail => 'emailAddress',
66 my %rev_field_map = reverse %field_map;
68 sub actions { \%actions }
70 sub default_action { 'cart' }
73 my ($class, $cgi) = @_;
75 for my $key ($cgi->param()) {
76 if ($key =~ /^delete_(\d+)(?:\.x)?$/) {
77 return ( remove_item => $1 );
79 elsif ($key =~ /^(?:a_)?addsingle(\d+)(?:\.x)?$/) {
80 return ( addsingle => $1 );
88 my ($class, $req, $msg) = @_;
90 $class->_refresh_cart($req);
92 my @cart = @{$req->session->{cart} || []};
94 my @items = $class->_build_items($req, \@cart_prods);
99 $req->session->{custom} ||= {};
100 my %custom_state = %{$req->session->{custom}};
102 my $cust_class = custom_class($req->cfg);
103 $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $req->cfg);
104 $msg = '' unless defined $msg;
105 $msg = escape_html($msg);
107 $msg ||= $req->message;
112 $cust_class->cart_actions(\%acts, \@cart, \@cart_prods, \%custom_state,
114 shop_cart_tags(\%acts, \@items, \@cart_prods, $req, 'cart'),
118 $req->session->{custom} = \%custom_state;
119 $req->session->{order_info_confirmed} = 0;
121 # intended to ajax enable the shop cart with partial templates
122 my $template = 'cart';
123 my $embed = $req->cgi->param('embed');
124 if (defined $embed and $embed =~ /^\w+$/) {
125 $template = "include/cart_$embed";
127 return $req->response($template, \%acts);
132 Empty the shopping cart.
134 Refreshes to the URL in C<r> or the cart otherwise.
136 Flashes msg:bse/shop/cart/empty unless C<r> is supplied.
141 my ($self, $req) = @_;
143 my $old = $req->session->{cart};;
144 $req->session->{cart} = [];
146 my $refresh = $req->cgi->param('r');
148 $refresh = $req->user_url(shop => 'cart');
149 $req->flash("msg:bse/shop/cart/empty");
152 return _add_refresh($refresh, $req, !$old);
156 my ($class, $req) = @_;
160 my $quantity = $cgi->param('quantity');
165 my ($product, $options, $extras);
166 my $addid = $cgi->param('id');
167 if (defined $addid) {
168 ($product, $options, $extras)
169 = $class->_validate_add_by_id($req, $addid, $quantity, \$error, \$refresh_logon);
172 my $code = $cgi->param('code');
174 ($product, $options, $extras)
175 = $class->_validate_add_by_code($req, $code, $quantity, \$error, \$refresh_logon);
178 return $class->req_cart($req, "No product id or code supplied");
181 if ($refresh_logon) {
182 return $class->_refresh_logon($req, @$refresh_logon);
185 return $class->req_cart($req, $error);
188 if ($cgi->param('empty')) {
189 $req->session->{cart} = [];
192 $req->session->{cart} ||= [];
193 my @cart = @{$req->session->{cart}};
194 my $started_empty = @cart == 0;
197 for my $item (@cart) {
198 $item->{productId} eq $product->{id} && _same_options($item->{options}, $options)
202 $item->{units} += $quantity;
206 my $cart_limit = $req->cfg->entry('shop', 'cart_entry_limit');
207 if (defined $cart_limit && @cart >= $cart_limit) {
208 return $class->req_cart($req, $req->text('shop/cartfull', MSG_SHOP_CART_FULL));
210 my $user = $req->siteuser;
211 my ($price, $tier) = $product->price(user => $user);
214 productId => $product->{id},
218 tier => $tier ? $tier->id : "",
219 user => $user ? $user->id : 0,
224 $req->session->{cart} = \@cart;
225 $req->session->{order_info_confirmed} = 0;
227 my $refresh = $cgi->param('r');
229 $refresh = $req->user_url(shop => 'cart');
233 if ($refresh eq 'ajaxcart') {
234 return $class->req_cart($req);
237 return _add_refresh($refresh, $req, $started_empty);
241 my ($class, $req, $addid) = @_;
246 my $quantity = $cgi->param("qty$addid");
247 defined $quantity && $quantity =~ /\S/
252 my ($product, $options, $extras)
253 = $class->_validate_add_by_id($req, $addid, $quantity, \$error, \$refresh_logon);
254 if ($refresh_logon) {
255 return $class->_refresh_logon($req, @$refresh_logon);
258 return $class->req_cart($req, $error);
261 if ($cgi->param('empty')) {
262 $req->session->{cart} = [];
265 $req->session->{cart} ||= [];
266 my @cart = @{$req->session->{cart}};
267 my $started_empty = @cart == 0;
270 for my $item (@cart) {
271 $item->{productId} eq $addid && _same_options($item->{options}, $options)
275 $item->{units} += $quantity;
279 my $cart_limit = $req->cfg->entry('shop', 'cart_entry_limit');
280 if (defined $cart_limit && @cart >= $cart_limit) {
281 return $class->req_cart($req, $req->text('shop/cartfull', MSG_SHOP_CART_FULL));
283 my $user = $req->siteuser;
284 my ($price, $tier) = $product->price(user => $user);
291 tier => $tier ? $tier->id : "",
292 user => $user ? $user->id : 0,
297 $req->session->{cart} = \@cart;
298 $req->session->{order_info_confirmed} = 0;
300 my $refresh = $cgi->param('r');
302 $refresh = $req->user_url(shop => 'cart');
306 if ($refresh eq 'ajaxcart') {
307 return $class->req_cart($req);
310 return _add_refresh($refresh, $req, $started_empty);
313 sub req_addmultiple {
314 my ($class, $req) = @_;
317 my @qty_keys = map /^qty(\d+)/, $cgi->param;
321 for my $addid (@qty_keys) {
322 my $quantity = $cgi->param("qty$addid");
323 defined $quantity && $quantity =~ /^\s*\d+\s*$/
328 my ($product, $options, $extras) =
329 $class->_validate_add_by_id($req, $addid, $quantity, \$error, \$refresh_logon);
330 if ($refresh_logon) {
331 return $class->_refresh_logon($req, @$refresh_logon);
334 return $class->req_cart($req, $error);
336 if ($product->{options}) {
337 push @messages, "$product->{title} has options, you need to use the product page to add this product";
342 id => $product->{id},
345 quantity => $quantity,
349 my @qtys = $cgi->param("qty");
350 my @ids = $cgi->param("id");
351 for my $addid (@ids) {
352 my $quantity = shift @qtys;
357 defined $quantity or $quantity = 1;
362 my ($error, $refresh_logon);
364 my ($product, $options, $extras) =
365 $class->_validate_add_by_id($req, $addid, $quantity, \$error, \$refresh_logon);
366 if ($refresh_logon) {
367 return $class->_refresh_logon($req, @$refresh_logon);
370 return $class->req_cart($req, $error);
372 if ($product->{options}) {
373 push @messages, "$product->{title} has options, you need to use the product page to add this product";
378 id => $product->{id},
381 quantity => $quantity,
385 my $started_empty = 0;
386 if (keys %additions) {
387 if ($cgi->param('empty')) {
388 $req->session->{cart} = [];
390 $req->session->{cart} ||= [];
391 my @cart = @{$req->session->{cart}};
392 $started_empty = @cart == 0;
393 for my $item (@cart) {
394 @{$item->{options}} == 0 or next;
396 my $addition = delete $additions{$item->{productId}}
399 $item->{units} += $addition->{quantity};
402 my $cart_limit = $req->cfg->entry('shop', 'cart_entry_limit');
404 my @additions = grep $_->{quantity} > 0, values %additions;
406 my $user = $req->siteuser;
408 for my $addition (@additions) {
409 my $product = $addition->{product};
411 if (defined $cart_limit && @cart >= $cart_limit) {
412 $error = $req->text('shop/cartfull', MSG_SHOP_CART_FULL);
416 my ($price, $tier) = $product->price(user => $user);
419 productId => $product->{id},
420 units => $addition->{quantity},
422 tier => $tier ? $tier->id : "",
423 user => $user ? $user->id : 0,
425 %{$addition->{extras}},
429 $req->session->{cart} = \@cart;
430 $req->session->{order_info_confirmed} = 0;
432 and return $class->req_cart($req, $error);
435 my $refresh = $cgi->param('r');
437 $refresh = $req->user_url(shop => 'cart');
440 my $sep = $refresh =~ /\?/ ? '&' : '?';
442 for my $message (@messages) {
443 $refresh .= $sep . "m=" . escape_uri($message);
449 if ($refresh eq 'ajaxcart') {
450 return $class->req_cart($req);
453 return _add_refresh($refresh, $req, $started_empty);
457 my ($user, $args) = @_;
461 return defined $user->{$args} && $user->{$args};
468 return defined $user;
472 sub _any_physical_products {
475 for my $prod (@$prods) {
486 my ($class, $req, $message, $olddata) = @_;
488 $class->_refresh_cart($req);
491 if (defined $message) {
494 $message = $req->message($errors);
503 my $need_delivery = ( $olddata ? $cgi->param("need_delivery") : $req->session->{order_need_delivery} ) || 0;
505 $class->update_quantities($req);
506 my @cart = @{$req->session->{cart}};
508 @cart or return $class->req_cart($req);
511 my @items = $class->_build_items($req, \@cart_prods);
513 if (my ($msg, $id) = $class->_need_logon($req, \@cart, \@cart_prods)) {
514 return $class->_refresh_logon($req, $msg, $id);
518 my $user = $req->siteuser;
520 $req->session->{custom} ||= {};
521 my %custom_state = %{$req->session->{custom}};
523 my $cust_class = custom_class($cfg);
524 $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $cfg);
526 my $affiliate_code = $req->session->{affiliate_code};
527 defined $affiliate_code or $affiliate_code = '';
529 my $order_info = $req->session->{order_info};
531 my $billing_map = BSE::TB::Order->billing_to_delivery_map;
532 my %delivery_map = reverse %$billing_map;
534 if ($order_info && !$need_delivery) {
535 # if need delivery is off, remove any delivery fields
536 my $map = BSE::TB::Order->billing_to_delivery_map;
537 delete @{$order_info}{keys %delivery_map};
544 if ($olddata && defined($cgi->param($field))) {
545 $value = $cgi->param($field);
547 elsif ($order_info && defined $order_info->{$field}) {
548 $value = $order_info->{$field};
551 $rev_field_map{$field} and $field = $rev_field_map{$field};
552 $value = $user && defined $user->{$field} ? $user->{$field} : '';
555 defined $value or $value = '';
559 # shipping handling, if enabled
560 my $shipping_select = ''; # select of shipping types
561 my ($delivery_in, $shipping_cost, $shipping_method);
562 my $shipping_error = '';
563 my $shipping_name = '';
564 my $prompt_ship = $cfg->entry("shop", "shipping", 0);
566 my $physical = _any_physical_products(\@cart_prods);
569 my $sel_cn = $old->("shipping_name") || "";
572 $work_order = $order_info unless $olddata;
573 unless ($work_order) {
575 my %fields = $class->_order_fields($req);
576 $class->_order_hash($req, \%fake_order, \%fields, user => 1);
577 $work_order = \%fake_order;
580 # Get a list of couriers
581 my $country_field = $need_delivery ? "delivCountry" : "billCountry";
582 my $country = $old->($country_field)
583 || bse_default_country($cfg);
584 my $country_code = bse_country_code($country);
585 my $suburb = $old->($need_delivery ? "delivSuburb" : "billSuburb");
586 my $postcode = $old->($need_delivery ? "delivPostCode" : "billPostCode");
589 or $errors->{$country_field} = "Unknown country name $country";
591 my @couriers = BSE::Shipping->get_couriers($cfg);
593 if ($country_code and $postcode) {
594 @couriers = grep $_->can_deliver(country => $country_code,
596 postcode => $postcode), @couriers;
599 my ($sel_cour) = grep $_->name eq $sel_cn, @couriers;
600 # if we don't match against the list (perhaps because of a country
601 # change) the first item in the list will be selected by the
602 # browser anyway, so select it ourselves and display an
603 # appropriate shipping cost for the item
605 $sel_cour = $couriers[0];
606 $sel_cn = $sel_cour->name;
608 if ($sel_cour and $postcode and $suburb and $country_code) {
609 my @parcels = BSE::Shipping->package_order($cfg, $order_info, \@items);
610 $shipping_cost = $sel_cour->calculate_shipping
612 parcels => \@parcels,
614 postcode => $postcode,
615 country => $country_code,
616 products => \@cart_prods,
619 $delivery_in = $sel_cour->delivery_in();
620 $shipping_method = $sel_cour->description();
621 $shipping_name = $sel_cour->name;
622 unless (defined $shipping_cost) {
623 $shipping_error = "$shipping_method: " . $sel_cour->error_message;
624 $errors->{shipping_name} = $shipping_error;
626 # use the last one, which should be the Null shipper
627 $sel_cour = $couriers[-1];
628 $sel_cn = $sel_cour->name;
629 $shipping_method = $sel_cour->description;
633 $shipping_select = popup_menu
635 -name => "shipping_name",
636 -id => "shipping_name",
637 -values => [ map $_->name, @couriers ],
638 -labels => { map { $_->name => $_->description } @couriers },
643 $sel_cn = $shipping_name = "none";
644 $shipping_method = "Nothing to ship!";
645 $shipping_select = popup_menu
647 -name => "shipping_name",
648 -id => "shipping_name",
649 -values => [ "none" ],
650 -labels => { none => $shipping_method },
656 if (!$message && keys %$errors) {
657 $message = $req->message($errors);
666 shop_cart_tags(\%acts, \@items, \@cart_prods, $req, 'checkout'),
670 old => sub { escape_html($old->($_[0])); },
671 $cust_class->checkout_actions(\%acts, \@cart, \@cart_prods,
672 \%custom_state, $req->cgi, $cfg),
673 ifUser => [ \&tag_ifUser, $user ],
674 user => $user ? [ \&tag_hash, $user ] : '',
675 affiliate_code => escape_html($affiliate_code),
676 error_img => [ \&tag_error_img, $cfg, $errors ],
677 ifShipping => $prompt_ship,
678 shipping_select => $shipping_select,
679 delivery_in => escape_html($delivery_in),
680 shipping_cost => $shipping_cost,
681 shipping_method => escape_html($shipping_method),
682 shipping_error => escape_html($shipping_error),
683 shipping_name => $shipping_name,
684 ifPhysical => $physical,
685 ifNeedDelivery => $need_delivery,
687 $req->session->{custom} = \%custom_state;
688 my $tmp = $acts{total};
692 $total += $shipping_cost if $total and $shipping_cost;
696 return $req->response('checkoutnew', \%acts);
699 sub req_checkupdate {
700 my ($class, $req) = @_;
702 $req->session->{cart} ||= [];
703 my @cart = @{$req->session->{cart}};
704 my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
705 $req->session->{custom} ||= {};
706 my %custom_state = %{$req->session->{custom}};
707 custom_class($req->cfg)
708 ->checkout_update($req->cgi, \@cart, \@cart_prods, \%custom_state, $req->cfg);
709 $req->session->{custom} = \%custom_state;
710 $req->session->{order_info_confirmed} = 0;
712 return $class->req_checkout($req, "", 1);
715 sub req_remove_item {
716 my ($class, $req, $index) = @_;
718 $req->session->{cart} ||= [];
719 my @cart = @{$req->session->{cart}};
720 if ($index >= 0 && $index < @cart) {
721 splice(@cart, $index, 1);
723 $req->session->{cart} = \@cart;
724 $req->session->{order_info_confirmed} = 0;
726 return BSE::Template->get_refresh($req->user_url(shop => 'cart'), $req->cfg);
730 my ($self, $req) = @_;
732 my %fields = BSE::TB::Order->valid_fields($req->cfg);
733 my $cust_class = custom_class($req->cfg);
735 $cust_class->required_fields($req->cgi, $req->session->{custom}, $req->cfg);
737 for my $name (@required) {
738 $fields{$name}{required} = 1;
745 my ($self, $req, $values, $fields, %opts) = @_;
748 my $user = $req->siteuser;
749 for my $name (keys %$fields) {
750 my ($value) = $cgi->param($name);
751 if (!defined $value && $opts{user} && $user) {
752 my $field = $rev_field_map{$name} || $name;
753 if ($user->can($field)) {
754 $value = $user->$field();
757 defined $value or $value = "";
758 $values->{$name} = $value;
761 unless ($cgi->param("need_delivery")) {
762 my $map = BSE::TB::Order->billing_to_delivery_map;
763 keys %$map; # reset iterator
764 while (my ($billing, $delivery) = each %$map) {
765 $values->{$delivery} = $values->{$billing};
770 # saves order and refresh to payment page
772 my ($class, $req) = @_;
777 $req->session->{cart} && @{$req->session->{cart}}
778 or return $class->req_cart($req, "Your cart is empty");
781 $class->_validate_cfg($req, \$msg)
782 or return $class->req_cart($req, $msg);
785 my @items = $class->_build_items($req, \@products);
788 if (($msg, $id) = $class->_need_logon($req, \@items, \@products)) {
789 return $class->_refresh_logon($req, $msg, $id);
792 my %fields = $class->_order_fields($req);
793 my %rules = BSE::TB::Order->valid_rules($cfg);
797 $class->_order_hash($req, \%values, \%fields);
799 dh_validate_hash(\%values, \%errors, { rules=>\%rules, fields=>\%fields },
800 $cfg, 'Shop Order Validation');
801 my $prompt_ship = $cfg->entry("shop", "shipping", 0);
803 my $country = $values{delivCountry} || bse_default_country($cfg);
804 my $country_code = bse_country_code($country);
806 or $errors{delivCountry} = "Unknown country name $country";
809 and return $class->req_checkout($req, \%errors, 1);
811 $class->_fillout_order($req, \%values, \@items, \@products, \$msg, 'payment')
812 or return $class->req_checkout($req, $msg, 1);
814 $req->session->{order_info} = \%values;
815 $req->session->{order_need_delivery} = $cgi->param("need_delivery");
816 $req->session->{order_info_confirmed} = 1;
818 # skip payment page if nothing to pay
819 if ($values{total} == 0) {
820 return $class->req_payment($req);
823 return BSE::Template->get_refresh($req->user_url(shop => 'show_payment'), $req->cfg);
829 Allows the customer to pay for an existing order.
837 orderid - the order id to be paid (Optional, otherwise displays the
842 Template: checkoutpay
847 sub req_show_payment {
848 my ($class, $req, $errors) = @_;
857 # ideally supply order_id to be consistent with a_payment.
858 my $order_id = $cgi->param('orderid') || $cgi->param("order_id");
861 or return $class->req_cart($req, "No or invalid order id supplied");
863 my $user = $req->siteuser
864 or return $class->_refresh_logon
865 ($req, "Please logon before paying your existing order", "logonpayorder",
866 undef, { a_show_payment => 1, orderid => $order_id });
868 require BSE::TB::Orders;
869 $order = BSE::TB::Orders->getByPkey($order_id)
870 or return $class->req_cart($req, "Unknown order id");
872 $order->siteuser_id == $user->id
873 or return $class->req_cart($req, "You can only pay for your own orders");
876 and return $class->req_cart($req, "Order $order->{id} has been paid");
878 @items = $order->items;
879 @products = $order->products;
882 $req->session->{order_info_confirmed}
883 or return $class->req_checkout($req, 'Please proceed via the checkout page');
885 $req->session->{cart} && @{$req->session->{cart}}
886 or return $class->req_cart($req, "Your cart is empty");
888 $order = $req->session->{order_info}
889 or return $class->req_checkout($req, "You need to enter order information first");
891 @items = $class->_build_items($req, \@products);
895 my $msg = $req->message($errors);
897 my @pay_types = payment_types($cfg);
898 my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
899 my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
900 @payment_types or @payment_types = ( PAYMENT_CALLME );
901 @payment_types = sort { $a <=> $b } @payment_types;
902 my %payment_types = map { $_=> 1 } @payment_types;
904 $errors and $payment = $cgi->param('paymentType');
905 defined $payment or $payment = $payment_types[0];
913 order => [ \&tag_hash, $order ],
914 shop_cart_tags(\%acts, \@items, \@products, $req, 'payment'),
915 ifMultPaymentTypes => @payment_types > 1,
916 checkedPayment => [ \&tag_checkedPayment, $payment, \%types_by_name ],
917 ifPayments => [ \&tag_ifPayments, \@payment_types, \%types_by_name ],
918 paymentTypeId => [ \&tag_paymentTypeId, \%types_by_name ],
919 error_img => [ \&tag_error_img, $cfg, $errors ],
920 total => $order->{total},
921 delivery_in => $order->{delivery_in},
922 shipping_cost => $order->{shipping_cost},
923 shipping_method => $order->{shipping_method},
925 for my $type (@pay_types) {
926 my $id = $type->{id};
927 my $name = $type->{name};
928 $acts{"if${name}Payments"} = exists $payment_types{$id};
929 $acts{"if${name}FirstPayment"} = $payment_types[0] == $id;
930 $acts{"checkedIfFirst$name"} = $payment_types[0] == $id ? "checked " : "";
931 $acts{"checkedPayment$name"} = $payment == $id ? 'checked="checked" ' : "";
934 return $req->response('checkoutpay', \%acts);
948 # hash of CC payment parameter names to arrays of billing address fields
949 firstname => "billFirstName",
950 lastname => "billLastName",
951 address1 => "billStreet",
952 address2 => "billStreet2",
953 postcode => "billPostCode",
954 state => "billState",
955 suburb => "billSuburb",
956 email => "billEmail",
960 my ($class, $req, $errors) = @_;
962 require BSE::TB::Orders;
964 my $order_id = $cgi->param("order_id");
965 my $user = $req->siteuser;
968 my $old_order; # true if we're paying an old order
971 return $class->_refresh_logon
974 "Please logon before paying your existing order",
977 { a_show_payment => 1, orderid => $order_id }
981 or return $class->req_cart($req, "Invalid order id");
982 $order = BSE::TB::Orders->getByPkey($order_id)
983 or return $class->req_cart($req, "Unknown order id");
984 $order->siteuser_id == $user->id
985 or return $class->req_cart($req, "You can only pay for your own orders");
988 and return $class->req_cart($req, "Order $order->{id} has been paid");
990 $order_values = $order;
994 $req->session->{order_info_confirmed}
995 or return $class->req_checkout($req, 'Please proceed via the checkout page');
997 $order_values = $req->session->{order_info}
998 or return $class->req_checkout($req, "You need to enter order information first");
1002 my $cfg = $req->cfg;
1003 my $session = $req->session;
1006 if ($order_values->{total} != 0) {
1007 my @pay_types = payment_types($cfg);
1008 my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
1009 my %pay_types = map { $_->{id} => $_ } @pay_types;
1010 my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
1011 @payment_types or @payment_types = ( PAYMENT_CALLME );
1012 @payment_types = sort { $a <=> $b } @payment_types;
1013 my %payment_types = map { $_=> 1 } @payment_types;
1015 $paymentType = $cgi->param('paymentType');
1016 defined $paymentType or $paymentType = $payment_types[0];
1017 $payment_types{$paymentType}
1018 or return $class->req_show_payment($req, { paymentType => "Invalid payment type" } , 1);
1021 push @required, @{$pay_types{$paymentType}{require}};
1023 my %fields = BSE::TB::Order->valid_payment_fields($cfg);
1024 my %rules = BSE::TB::Order->valid_payment_rules($cfg);
1025 for my $field (@required) {
1026 if (exists $fields{$field}) {
1027 $fields{$field}{required} = 1;
1030 $fields{$field} = { description => $field, required=> 1 };
1035 dh_validate($cgi, \%errors, { rules => \%rules, fields=>\%fields },
1036 $cfg, 'Shop Order Validation');
1038 and return $class->req_show_payment($req, \%errors);
1040 for my $field (keys %fields) {
1041 unless ($nostore{$field}) {
1042 if (my ($value) = $cgi->param($field)) {
1043 $order_values->{$field} = $value;
1053 $order_values->{paymentType} = $paymentType;
1058 @dbitems = $order->items;
1059 @products = $order->products;
1060 for my $product (@products) {
1061 my $sub = $product->subscription;
1063 $subscribing_to{$sub->{text_id}} = $sub;
1068 $order_values->{filled} = 0;
1069 $order_values->{paidFor} = 0;
1071 my @items = $class->_build_items($req, \@products);
1073 if ($session->{order_work}) {
1074 $order = BSE::TB::Orders->getByPkey($session->{order_work});
1076 if ($order && !$order->{complete}) {
1077 my @columns = BSE::TB::Order->columns;
1078 shift @columns; # don't set id
1080 @columns{@columns} = @columns;
1082 for my $col (@columns) {
1083 defined $order_values->{$col} or $order_values->{$col} = '';
1086 my @data = @{$order_values}{@columns};
1089 print STDERR "Recycling order $order->{id}\n";
1091 my @allbutid = @columns;
1093 @{$order}{@allbutid} = @data;
1095 $order->clear_items;
1096 delete $session->{order_work};
1098 tied(%$session)->save;
1102 $order = BSE::TB::Orders->make(%$order_values)
1103 or die "Cannot add order";
1106 my @item_cols = BSE::TB::OrderItem->columns;
1107 for my $row_num (0..$#items) {
1108 my $item = $items[$row_num];
1109 my $product = $products[$row_num];
1111 $item{orderId} = $order->{id};
1112 $item{max_lapsed} = 0;
1113 if ($product->{subscription_id} != -1) {
1114 my $sub = $product->subscription;
1115 $item{max_lapsed} = $sub->{max_lapsed} if $sub;
1117 defined $item{session_id} or $item{session_id} = 0;
1118 $item{options} = ""; # not used for new orders
1119 my @data = @item{@item_cols};
1121 my $dbitem = BSE::TB::OrderItems->add(@data);
1122 push @dbitems, $dbitem;
1124 if ($item->{options} and @{$item->{options}}) {
1125 require BSE::TB::OrderItemOptions;
1126 my @option_descs = $product->option_descs($cfg, $item->{options});
1127 my $display_order = 1;
1128 for my $option (@option_descs) {
1129 BSE::TB::OrderItemOptions->make
1131 order_item_id => $dbitem->{id},
1132 original_id => $option->{id},
1133 name => $option->{desc},
1134 value => $option->{value},
1135 display => $option->{display},
1136 display_order => $display_order++,
1141 my $sub = $product->subscription;
1143 $subscribing_to{$sub->{text_id}} = $sub;
1146 if ($item->{session_id}) {
1147 require BSE::TB::SeminarSessions;
1148 my $session = BSE::TB::SeminarSessions->getByPkey($item->{session_id});
1149 my $options = join(",", @{$item->{options}});
1150 $session->add_attendee($user,
1151 customer_instructions => $order->{instructions},
1152 options => $options);
1157 $order->set_randomId(make_secret($cfg));
1158 $order->{ccOnline} = 0;
1160 my $ccprocessor = $cfg->entry('shop', 'cardprocessor');
1161 if ($paymentType == PAYMENT_CC) {
1162 my $ccNumber = $cgi->param('cardNumber');
1163 my $ccExpiry = $cgi->param('cardExpiry');
1164 my $ccName = $cgi->param('ccName');
1167 my $cc_class = credit_card_class($cfg);
1169 $order->{ccOnline} = 1;
1171 $ccExpiry =~ m!^(\d+)\D(\d+)$! or die;
1172 my ($month, $year) = ($1, $2);
1173 $year > 2000 or $year += 2000;
1174 my $expiry = sprintf("%04d%02d", $year, $month);
1175 my $verify = $cgi->param('cardVerify');
1176 defined $verify or $verify = '';
1178 while (my ($cc_field, $order_field) = each %bill_ccmap) {
1179 if ($order->$order_field()) {
1180 $more{$cc_field} = $order->$order_field();
1183 my $result = $cc_class->payment
1185 orderno => $order->{id},
1186 amount => $order->{total},
1187 cardnumber => $ccNumber,
1188 nameoncard => $ccName,
1189 expirydate => $expiry,
1191 ipaddress => $ENV{REMOTE_ADDR},
1194 unless ($result->{success}) {
1196 print STDERR Dumper($result);
1197 # failed, back to payments
1198 $order->{ccSuccess} = 0;
1199 $order->{ccStatus} = $result->{statuscode};
1200 $order->{ccStatus2} = 0;
1201 $order->{ccStatusText} = $result->{error};
1202 $order->{ccTranId} = '';
1205 $errors{cardNumber} = $result->{error};
1206 $session->{order_work} = $order->{id};
1207 return $class->req_show_payment($req, \%errors);
1210 $order->{ccSuccess} = 1;
1211 $order->{ccReceipt} = $result->{receipt};
1212 $order->{ccStatus} = 0;
1213 $order->{ccStatus2} = 0;
1214 $order->{ccStatusText} = '';
1215 $order->{ccTranId} = $result->{transactionid};
1216 $order->set_ccPANTruncate($ccNumber);
1217 defined $order->{ccTranId} or $order->{ccTranId} = '';
1218 $order->{paidFor} = 1;
1221 $ccNumber =~ tr/0-9//cd;
1222 $order->{ccExpiryHash} = md5_hex($ccExpiry);
1223 $order->set_ccPANTruncate($ccNumber);
1225 $order->set_ccName($ccName);
1227 elsif ($paymentType == PAYMENT_PAYPAL) {
1228 require BSE::PayPal;
1230 my $url = BSE::PayPal->payment_url(order => $order,
1234 $session->{order_work} = $order->{id};
1236 $errors{_} = "PayPal error: $msg" if $msg;
1237 return $class->req_show_payment($req, \%errors);
1240 # have to mark it complete so it doesn't get used by something else
1241 return BSE::Template->get_refresh($url, $req->cfg);
1245 $order->set_complete(1);
1246 $order->set_stage("unprocessed");
1249 $class->_finish_order($req, $order);
1251 return BSE::Template->get_refresh($req->user_url(shop => 'orderdone'), $req->cfg);
1254 # do final processing of an order after payment
1256 my ($self, $req, $order) = @_;
1259 my $custom = custom_class($req->cfg);
1260 $custom->can("order_complete")
1261 and $custom->order_complete($req->cfg, $order);
1263 # set the order displayed by orderdone
1264 $req->session->{order_completed} = $order->{id};
1265 $req->session->{order_completed_at} = time;
1267 $self->_send_order($req, $order);
1269 # empty the cart ready for the next order
1270 delete @{$req->session}{qw/order_info order_info_confirmed order_need_delivery cart order_work/};
1274 my ($class, $req) = @_;
1276 my $session = $req->session;
1277 my $cfg = $req->cfg;
1279 my $id = $session->{order_completed};
1280 my $when = $session->{order_completed_at};
1281 $id && defined $when && time < $when + 500
1282 or return $class->req_cart($req);
1284 my $order = BSE::TB::Orders->getByPkey($id)
1285 or return $class->req_cart($req);
1286 my @items = $order->items;
1287 my @products = map { Products->getByPkey($_->{productId}) } @items;
1289 my @item_cols = BSE::TB::OrderItem->columns;
1290 my %copy_cols = map { $_ => 1 } Product->columns;
1291 delete @copy_cols{@item_cols};
1292 my @copy_cols = keys %copy_cols;
1294 for my $item_index (0..$#items) {
1295 my $item = $items[$item_index];
1296 my $product = $products[$item_index];
1298 @entry{@item_cols} = @{$item}{@item_cols};
1299 @entry{@copy_cols} = @{$product}{@copy_cols};
1301 push @showitems, \%entry;
1304 my $cust_class = custom_class($req->cfg);
1306 my @pay_types = payment_types($cfg);
1307 my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
1308 my %pay_types = map { $_->{id} => $_ } @pay_types;
1309 my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
1311 my $item_index = -1;
1318 require BSE::Util::Iterate;
1319 my $it = BSE::Util::Iterate::Objects->new(cfg => $req->cfg);
1320 my $message = $req->message();
1324 $req->dyn_user_tags(),
1325 $cust_class->purchase_actions(\%acts, \@items, \@products,
1326 $session->{custom}, $cfg),
1327 BSE::Util::Tags->static(\%acts, $cfg),
1328 iterate_items_reset => sub { $item_index = -1; },
1331 if (++$item_index < @items) {
1333 @options = order_item_opts($req, $items[$item_index]);
1336 $item = $items[$item_index];
1337 $product = $products[$item_index];
1346 item=> sub { escape_html($showitems[$item_index]{$_[0]}); },
1349 return tag_article($product, $cfg, $_[0]);
1353 my $what = $_[0] || 'retailPrice';
1354 $items[$item_index]{units} * $items[$item_index]{$what};
1356 order => sub { escape_html($order->{$_[0]}) },
1359 my ($value, $fmt) = @_;
1360 if ($fmt =~ /^m(\d+)/) {
1361 return sprintf("%$1s", sprintf("%.2f", $value/100));
1363 elsif ($fmt =~ /%/) {
1364 return sprintf($fmt, $value);
1367 iterate_options_reset => sub { $option_index = -1 },
1368 iterate_options => sub { ++$option_index < @options },
1369 option => sub { escape_html($options[$option_index]{$_[0]}) },
1370 ifOptions => sub { @options },
1371 options => sub { nice_options(@options) },
1372 ifPayment => [ \&tag_ifPayment, $order->{paymentType}, \%types_by_name ],
1373 #ifSubscribingTo => [ \&tag_ifSubscribingTo, \%subscribing_to ],
1374 session => [ \&tag_session, \$item, \$sem_session ],
1375 location => [ \&tag_location, \$item, \$location ],
1377 delivery_in => $order->{delivery_in},
1378 shipping_cost => $order->{shipping_cost},
1379 shipping_method => $order->{shipping_method},
1382 single => "orderpaidfile",
1383 plural => "orderpaidfiles",
1384 code => [ paid_files => $order ],
1387 for my $type (@pay_types) {
1388 my $id = $type->{id};
1389 my $name = $type->{name};
1390 $acts{"if${name}Payment"} = $order->{paymentType} == $id;
1393 return $req->response('checkoutfinal', \%acts);
1397 my ($ritem, $rsession, $arg) = @_;
1399 $$ritem or return '';
1401 $$ritem->{session_id} or return '';
1403 unless ($$rsession) {
1404 require BSE::TB::SeminarSessions;
1405 $$rsession = BSE::TB::SeminarSessions->getByPkey($$ritem->{session_id})
1409 my $value = $$rsession->{$arg};
1410 defined $value or return '';
1412 escape_html($value);
1416 my ($ritem, $rlocation, $arg) = @_;
1418 $$ritem or return '';
1420 $$ritem->{session_id} or return '';
1422 unless ($$rlocation) {
1423 require BSE::TB::Locations;
1424 ($$rlocation) = BSE::TB::Locations->getSpecial(session_id => $$ritem->{session_id})
1428 my $value = $$rlocation->{$arg};
1429 defined $value or return '';
1431 escape_html($value);
1435 my ($payment, $types_by_name, $args) = @_;
1438 if ($type !~ /^\d+$/) {
1439 return '' unless exists $types_by_name->{$type};
1440 $type = $types_by_name->{$type};
1443 return $payment == $type;
1446 sub tag_paymentTypeId {
1447 my ($types_by_name, $args) = @_;
1449 if (exists $types_by_name->{$args}) {
1450 return $types_by_name->{$args};
1458 my ($class, $req, $rmsg) = @_;
1460 my $cfg = $req->cfg;
1461 my $from = $cfg->entry('shop', 'from', $Constants::SHOP_FROM);
1462 unless ($from && $from =~ /.\@./) {
1463 $$rmsg = "Configuration error: shop from address not set";
1466 my $toEmail = $cfg->entry('shop', 'to_email', $Constants::SHOP_TO_EMAIL);
1467 unless ($toEmail && $toEmail =~ /.\@./) {
1468 $$rmsg = "Configuration error: shop to_email address not set";
1476 my ($class, $req) = @_;
1478 $class->update_quantities($req);
1479 $req->session->{order_info_confirmed} = 0;
1480 return $class->req_cart($req);
1483 sub req_recalculate {
1484 my ($class, $req) = @_;
1486 return $class->req_recalc($req);
1490 my ($class, $req, $order) = @_;
1492 my $cfg = $req->cfg;
1493 my $cgi = $req->cgi;
1495 my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
1496 my $crypto_class = $cfg->entry('shop', 'crypt_module',
1497 $Constants::SHOP_CRYPTO);
1498 my $signing_id = $cfg->entry('shop', 'crypt_signing_id',
1499 $Constants::SHOP_SIGNING_ID);
1500 my $pgp = $cfg->entry('shop', 'crypt_pgp', $Constants::SHOP_PGP);
1501 my $pgpe = $cfg->entry('shop', 'crypt_pgpe', $Constants::SHOP_PGPE);
1502 my $gpg = $cfg->entry('shop', 'crypt_gpg', $Constants::SHOP_GPG);
1503 my $passphrase = $cfg->entry('shop', 'crypt_passphrase',
1504 $Constants::SHOP_PASSPHRASE);
1505 my $from = $cfg->entry('shop', 'from', $Constants::SHOP_FROM);
1506 my $toName = $cfg->entry('shop', 'to_name', $Constants::SHOP_TO_NAME);
1507 my $toEmail = $cfg->entry('shop', 'to_email', $Constants::SHOP_TO_EMAIL);
1508 my $subject = $cfg->entry('shop', 'subject', $Constants::SHOP_MAIL_SUBJECT);
1510 my $session = $req->session;
1511 my %extras = $cfg->entriesCS('extra tags');
1512 for my $key (keys %extras) {
1514 my $data = $cfg->entryVar('extra tags', $key);
1515 $extras{$key} = sub { $data };
1518 my @items = $order->items;
1519 my @products = map $_->product, @items;
1521 for my $product (@products) {
1522 my $sub = $product->subscription;
1524 $subscribing_to{$sub->{text_id}} = $sub;
1528 my $item_index = -1;
1536 ->order_mail_actions(\%acts, $order, \@items, \@products,
1537 $session->{custom}, $cfg),
1538 BSE::Util::Tags->mail_tags(),
1539 $order->mail_tags(),
1540 ifSubscribingTo => [ \&tag_ifSubscribingTo, \%subscribing_to ],
1543 my $email_order = $cfg->entryBool('shop', 'email_order', $Constants::SHOP_EMAIL_ORDER);
1544 require BSE::ComposeMail;
1546 unless ($noencrypt) {
1547 $acts{cardNumber} = $cgi->param('cardNumber');
1548 $acts{cardExpiry} = $cgi->param('cardExpiry');
1549 $acts{cardVerify} = $cgi->param('cardVerify');
1552 my $mailer = BSE::ComposeMail->new(cfg => $cfg);
1557 subject=>'New Order '.$order->{id},
1559 template => "mailorder",
1560 log_component => "shop:sendorder:mailowner",
1561 log_object => $order,
1562 log_msg => "Order $order->{id} sent to site owner",
1565 unless ($noencrypt) {
1567 my $sign = $cfg->entryBool('basic', 'sign', 1);
1568 $sign or $crypt_opts{signing_id} = "";
1569 $crypt_opts{recipient} =
1570 $cfg->entry("shop", "crypt_recipient", "$toName $toEmail");
1571 $mailer->encrypt_body(%crypt_opts);
1574 unless ($mailer->done) {
1575 $req->flash_error("Could not mail order to admin: " . $mailer->errstr);
1578 delete @acts{qw/cardNumber cardExpiry cardVerify/};
1580 my $to_email = $order->billEmail;
1581 my $user = $req->siteuser;
1583 if ($user && $user->email eq $to_email) {
1586 my $mailer = BSE::ComposeMail->new(cfg => $cfg);
1591 subject => $subject . " " . localtime,
1592 template => "mailconfirm",
1594 log_component => "shop:sendorder:mailbuyer",
1595 log_object => $order,
1596 log_msg => "Order $order->{id} sent to purchaser $to_email",
1598 my $bcc_order = $cfg->entry("shop", "bcc_email");
1600 $opts{bcc} = $bcc_order;
1602 $mailer->send(%opts)
1603 or print STDERR "Error sending order to customer: ",$mailer->errstr,"\n";
1606 sub _refresh_logon {
1607 my ($class, $req, $msg, $msgid, $r, $parms) = @_;
1609 my $securlbase = $req->cfg->entryVar('site', 'secureurl');
1610 my $url = $securlbase."/cgi-bin/user.pl";
1611 $parms ||= { checkout => 1 };
1614 $r = $securlbase."/cgi-bin/shop.pl?"
1615 . join("&", map "$_=" . escape_uri($parms->{$_}), keys %$parms);
1619 if ($req->cfg->entry('shop registration', 'all')
1620 || $req->cfg->entry('shop registration', $msgid)) {
1621 $parms{show_register} = 1;
1625 $msg = $req->cfg->entry('messages', $msgid, $msg);
1627 $parms{message} = $msg if $msg;
1628 $parms{mid} = $msgid if $msgid;
1629 $url .= "?" . join("&", map "$_=".escape_uri($parms{$_}), keys %parms);
1631 return BSE::Template->get_refresh($url, $req->cfg);
1635 my ($class, $req, $cart, $cart_prods) = @_;
1637 return need_logon($req, $cart, $cart_prods);
1640 sub tag_checkedPayment {
1641 my ($payment, $types_by_name, $args) = @_;
1644 if ($type !~ /^\d+$/) {
1645 return '' unless exists $types_by_name->{$type};
1646 $type = $types_by_name->{$type};
1649 return $payment == $type ? 'checked="checked"' : '';
1652 sub tag_ifPayments {
1653 my ($enabled, $types_by_name, $args) = @_;
1656 if ($type !~ /^\d+$/) {
1657 return '' unless exists $types_by_name->{$type};
1658 $type = $types_by_name->{$type};
1661 my @found = grep $_ == $type, @$enabled;
1663 return scalar @found;
1666 sub update_quantities {
1667 my ($class, $req) = @_;
1669 my $session = $req->session;
1670 my $cgi = $req->cgi;
1671 my $cfg = $req->cfg;
1672 my @cart = @{$session->{cart} || []};
1673 for my $index (0..$#cart) {
1674 my $new_quantity = $cgi->param("quantity_$index");
1675 if (defined $new_quantity) {
1676 if ($new_quantity =~ /^\s*(\d+)/) {
1677 $cart[$index]{units} = $1;
1679 elsif ($new_quantity =~ /^\s*$/) {
1680 $cart[$index]{units} = 0;
1684 @cart = grep { $_->{units} != 0 } @cart;
1685 $session->{cart} = \@cart;
1686 $session->{custom} ||= {};
1687 my %custom_state = %{$session->{custom}};
1688 custom_class($cfg)->recalc($cgi, \@cart, [], \%custom_state, $cfg);
1689 $session->{custom} = \%custom_state;
1693 my ($class, $req, $products) = @_;
1695 my $session = $req->session;
1699 my @cart = @{$req->session->{cart}}
1702 my @prodcols = Product->columns;
1704 my $today = now_sqldate();
1705 for my $item (@cart) {
1707 my $product = Products->getByPkey($item->{productId});
1709 (my $comp_release = $product->{release}) =~ s/ .*//;
1710 (my $comp_expire = $product->{expire}) =~ s/ .*//;
1711 $comp_release le $today
1712 or do { push @msgs, "'$product->{title}' has not been released yet";
1714 $today le $comp_expire
1715 or do { push @msgs, "'$product->{title}' has expired"; next; };
1717 or do { push @msgs, "'$product->{title}' not available"; next; };
1719 for my $col (@prodcols) {
1720 $work{$col} = $product->$col() unless exists $work{$col};
1722 $work{price} = $product->price(user => scalar $req->siteuser);
1723 $work{extended_retailPrice} = $work{units} * $work{price};
1724 $work{extended_gst} = $work{units} * $work{gst};
1725 $work{extended_wholesale} = $work{units} * $work{wholesalePrice};
1727 push @newcart, \%work;
1728 push @$products, $product;
1732 # we don't use these for anything for now
1740 sub _fillout_order {
1741 my ($class, $req, $values, $items, $products, $rmsg, $how) = @_;
1743 my $session = $req->session;
1744 my $cfg = $req->cfg;
1745 my $cgi = $req->cgi;
1749 my $total_wholesale = 0;
1750 for my $item (@$items) {
1751 $total += $item->{extended_retailPrice};
1752 $total_gst += $item->{extended_gst};
1753 $total_wholesale += $item->{extended_wholesale};
1755 $values->{total} = $total;
1756 $values->{gst} = $total_gst;
1757 $values->{wholesaleTotal} = $total_wholesale;
1759 my $prompt_ship = $cfg->entry("shop", "shipping", 0);
1761 if (_any_physical_products($products)) {
1762 my ($courier) = BSE::Shipping->get_couriers($cfg, $cgi->param("shipping_name"));
1763 my $country_code = bse_country_code($values->{delivCountry});
1765 unless ($courier->can_deliver(country => $country_code,
1766 suburb => $values->{delivSuburb},
1767 postcode => $values->{delivPostCode})) {
1768 $cgi->param("courier", undef);
1770 "Can't use the selected courier ".
1771 "(". $courier->description(). ") for this order.";
1774 my @parcels = BSE::Shipping->package_order($cfg, $values, $items);
1775 my $cost = $courier->calculate_shipping
1777 parcels => \@parcels,
1778 country => $country_code,
1779 suburb => $values->{delivSuburb},
1780 postcode => $values->{delivPostCode},
1781 products => $products,
1784 if (!defined $cost and $courier->name() ne 'contact') {
1785 my $err = $courier->error_message();
1786 $$rmsg = "Error calculating shipping cost";
1787 $$rmsg .= ": $err" if $err;
1790 $values->{shipping_method} = $courier->description();
1791 $values->{shipping_name} = $courier->name;
1792 $values->{shipping_cost} = $cost;
1793 $values->{shipping_trace} = $courier->trace;
1794 #$values->{delivery_in} = $courier->delivery_in();
1795 $values->{total} += $values->{shipping_cost};
1799 $$rmsg = "Error: no usable courier found.";
1804 $values->{shipping_method} = "Nothing to ship!";
1805 $values->{shipping_name} = "none";
1806 $values->{shipping_cost} = 0;
1807 $values->{shipping_trace} = "All products have zero weight.";
1811 my $cust_class = custom_class($cfg);
1814 local $SIG{__DIE__};
1815 my %custom = %{$session->{custom}};
1816 $cust_class->order_save($cgi, $values, $items, $items,
1818 $session->{custom} = \%custom;
1826 $cust_class->total_extras($items, $items,
1827 $session->{custom}, $cfg, $how);
1829 my $affiliate_code = $session->{affiliate_code};
1830 defined $affiliate_code && length $affiliate_code
1831 or $affiliate_code = $cgi->param('affiliate_code');
1832 defined $affiliate_code or $affiliate_code = '';
1833 $values->{affiliate_code} = $affiliate_code;
1835 my $user = $req->siteuser;
1837 $values->{userId} = $user->{userId};
1838 $values->{siteuser_id} = $user->{id};
1841 $values->{userId} = '';
1842 $values->{siteuser_id} = -1;
1845 $values->{orderDate} = now_sqldatetime;
1847 # this should be hard to guess
1848 $values->{randomId} = md5_hex(time().rand().{}.$$);
1853 sub action_prefix { '' }
1856 my ($class, $req) = @_;
1858 require BSE::TB::Locations;
1859 my $cgi = $req->cgi;
1860 my $location_id = $cgi->param('location_id');
1862 if (defined $location_id && $location_id =~ /^\d+$/) {
1863 $location = BSE::TB::Locations->getByPkey($location_id);
1867 BSE::Util::Tags->static(\%acts, $req->cfg),
1868 location => [ \&tag_hash, $location ],
1871 return $req->response('location', \%acts);
1876 type=>BSE::Template->get_type($req->cfg, 'error'),
1877 content=>"Missing or invalid location_id",
1882 sub _validate_add_by_id {
1883 my ($class, $req, $addid, $quantity, $error, $refresh_logon) = @_;
1887 $product = BSE::TB::Seminars->getByPkey($addid);
1888 $product ||= Products->getByPkey($addid);
1891 $$error = "Cannot find product $addid";
1895 return $class->_validate_add($req, $product, $quantity, $error, $refresh_logon);
1898 sub _validate_add_by_code {
1899 my ($class, $req, $code, $quantity, $error, $refresh_logon) = @_;
1902 if (defined $code) {
1903 $product = BSE::TB::Seminars->getBy(product_code => $code);
1904 $product ||= Products->getBy(product_code => $code);
1907 $$error = "Cannot find product code $code";
1911 return $class->_validate_add($req, $product, $quantity, $error, $refresh_logon);
1915 my ($class, $req, $product, $quantity, $error, $refresh_logon) = @_;
1917 # collect the product options
1919 my @option_descs = $product->option_descs($req->cfg);
1920 my @option_names = map $_->{name}, @option_descs;
1922 my $cgi = $req->cgi;
1923 for my $name (@option_names) {
1924 my $value = $cgi->param($name);
1925 push @options, $value;
1926 unless (defined $value) {
1927 push @not_def, $name;
1931 $$error = "Some product options (@not_def) not supplied";
1935 # the product must be non-expired and listed
1936 (my $comp_release = $product->{release}) =~ s/ .*//;
1937 (my $comp_expire = $product->{expire}) =~ s/ .*//;
1938 my $today = now_sqldate();
1939 unless ($comp_release le $today) {
1940 $$error = "Product $product->{title} has not been released yet";
1943 unless ($today le $comp_expire) {
1944 $$error = "Product $product->{title} has expired";
1947 unless ($product->{listed}) {
1948 $$error = "Product $product->{title} not available";
1952 # used to refresh if a logon is needed
1953 my $securlbase = $req->cfg->entryVar('site', 'secureurl');
1954 my $r = $securlbase . $ENV{SCRIPT_NAME} . "?add=1&id=$product->{id}";
1955 for my $opt_index (0..$#option_names) {
1956 $r .= "&$option_names[$opt_index]=".escape_uri($options[$opt_index]);
1959 my $user = $req->siteuser;
1960 # need to be logged on if it has any subs
1961 if ($product->{subscription_id} != -1) {
1963 my $sub = $product->subscription;
1964 if ($product->is_renew_sub_only) {
1965 unless ($user->subscribed_to_grace($sub)) {
1966 $$error = "The product $product->{title} can only be used to renew your subscription to $sub->{title} and you are not subscribed nor within the renewal grace period";
1970 elsif ($product->is_start_sub_only) {
1971 if ($user->subscribed_to_grace($sub)) {
1972 $$error = "The product $product->{title} can only be used to start your subscription to $sub->{title} and you are already subscribed or within the grace period";
1979 [ "You must be logged on to add this product to your cart",
1984 if ($product->{subscription_required} != -1) {
1985 my $sub = $product->subscription_required;
1987 unless ($user->subscribed_to($sub)) {
1988 $$error = "You must be subscribed to $sub->{title} to purchase this product";
1993 # we want to refresh back to adding the item to the cart if possible
1995 [ "You must be logged on and subscribed to $sub->{title} to add this product to your cart",
1996 'prodlogonsub', $r ];
2001 # we need a natural integer quantity
2002 unless ($quantity =~ /^\d+$/ && $quantity > 0) {
2003 $$error = "Invalid quantity";
2008 if ($product->isa('BSE::TB::Seminar')) {
2009 # you must be logged on to add a seminar
2012 [ "You must be logged on to add seminars to your cart",
2013 'seminarlogon', $r ];
2017 # get and validate the session
2018 my $session_id = $cgi->param('session_id');
2019 unless (defined $session_id) {
2020 $$error = "Please select a session when adding a seminar";
2024 unless ($session_id =~ /^\d+$/) {
2025 $$error = "Invalid session_id supplied";
2029 require BSE::TB::SeminarSessions;
2030 my $session = BSE::TB::SeminarSessions->getByPkey($session_id);
2032 $$error = "Unknown session id supplied";
2035 unless ($session->{seminar_id} == $product->{id}) {
2036 $$error = "Session not for this seminar";
2040 # check if the user is already booked for this session
2041 if (grep($_ == $session_id, $user->seminar_sessions_booked($product->{id}))) {
2042 $$error = "You are already booked for this session";
2046 $extras{session_id} = $session_id;
2049 return ( $product, \@options, \%extras );
2053 my ($refresh, $req, $started_empty) = @_;
2055 my $cfg = $req->cfg;
2056 my $cookie_domain = $cfg->entry('basic', 'cookie_domain');
2057 if ($started_empty && !$cookie_domain) {
2058 my $base_url = $cfg->entryVar('site', 'url');
2059 my $secure_url = $cfg->entryVar('site', 'secureurl');
2060 if ($base_url ne $secure_url) {
2061 my $debug = $cfg->entryBool('debug', 'logon_cookies', 0);
2063 # magical refresh time
2064 # which host are we on?
2065 # first get info about the 2 possible hosts
2066 my ($baseprot, $basehost, $baseport) =
2067 $base_url =~ m!^(\w+)://([\w.-]+)(?::(\d+))?!;
2068 $baseport ||= $baseprot eq 'http' ? 80 : 443;
2069 print STDERR "Base: prot: $baseprot Host: $basehost Port: $baseport\n"
2072 #my ($secprot, $sechost, $secport) =
2073 # $securl =~ m!^(\w+)://([\w.-]+)(?::(\d+))?!;
2076 # get info about the current host
2077 my $port = $ENV{SERVER_PORT} || 80;
2078 my $ishttps = exists $ENV{HTTPS} || exists $ENV{SSL_CIPHER};
2079 print STDERR "\$ishttps: $ishttps\n" if $debug;
2080 my $protocol = $ishttps ? 'https' : 'http';
2082 if (lc $ENV{SERVER_NAME} ne lc $basehost
2083 || lc $protocol ne $baseprot
2084 || $baseport != $port) {
2085 print STDERR "not on base host ('$ENV{SERVER_NAME}' cmp '$basehost' '$protocol cmp '$baseprot' $baseport cmp $port\n" if $debug;
2088 my $base = $onbase ? $secure_url : $base_url;
2089 my $finalbase = $onbase ? $base_url : $secure_url;
2090 $refresh = $finalbase . $refresh unless $refresh =~ /^\w+:/;
2091 my $sessionid = $req->session->{_session_id};
2092 require BSE::SessionSign;
2093 my $sig = BSE::SessionSign->make($sessionid);
2094 my $url = $cfg->user_url("user", undef,
2096 setcookie => $sessionid,
2099 print STDERR "Heading to $url to setcookie\n" if $debug;
2100 return $req->get_refresh($url);
2104 return $req->get_refresh($refresh);
2108 my ($left, $right) = @_;
2110 for my $index (0 .. $#$left) {
2111 my $left_value = $left->[$index];
2112 my $right_value = $right->[$index];
2113 defined $right_value
2115 $left_value eq $right_value
2123 my ($self, $req, $rmsg) = @_;
2125 my $id = $req->cgi->param("order");
2127 $$rmsg = $req->catmsg("msg:bse/shop/paypal/noorderid");
2130 my ($order) = BSE::TB::Orders->getBy(randomId => $id);
2132 $$rmsg = $req->catmsg("msg:bse/shop/paypal/unknownorderid");
2141 Handles PayPal returning control.
2149 order - the randomId of the order
2153 token - paypal token we originally supplied to paypal. Supplied by
2158 PayerID - the paypal user who paid the order. Supplied by PayPal.
2165 my ($self, $req) = @_;
2167 require BSE::PayPal;
2168 BSE::PayPal->configured
2169 or return $self->req_cart($req, { _ => "msg:bse/shop/paypal/unconfigured" });
2172 my $order = $self->_paypal_order($req, \$msg)
2173 or return $self->req_show_payment($req, { _ => $msg });
2176 and return $self->req_cart($req, { _ => "msg:bse/shop/paypal/alreadypaid" });
2178 unless (BSE::PayPal->pay_order(req => $req,
2181 return $self->req_show_payment($req, { _ => $msg });
2184 $self->_finish_order($req, $order);
2186 return $req->get_refresh($req->user_url(shop => "orderdone"));
2190 my ($self, $req) = @_;
2192 require BSE::PayPal;
2193 BSE::PayPal->configured
2194 or return $self->req_cart($req, { _ => "msg:bse/shop/paypal/unconfigured" });
2197 my $order = $self->_paypal_order($req, \$msg)
2198 or return $self->req_show_payment($req, { _ => $msg });
2200 $req->flash("msg:bse/shop/paypal/cancelled");
2202 my $url = $req->user_url(shop => "show_payment");
2203 return $req->get_refresh($url);
2207 my ($self, $req) = @_;
2209 my $user = $req->siteuser
2212 my $cart = $req->session->{cart}
2215 for my $item (@$cart) {
2216 if (!$item->{user} || $item->{user} != $user->id) {
2217 my $product = Products->getByPkey($item->{productId})
2219 my ($price, $tier) = $product->price(user => $user);
2220 $item->{price} = $price;
2221 $item->{tier} = $tier ? $tier->id : "";
2225 $req->session->{cart} = $cart;