]> git.imager.perl.org - bse.git/blame - site/cgi-bin/shop.pl
0.11_02 commit
[bse.git] / site / cgi-bin / shop.pl
CommitLineData
41b9d8ec
TC
1#!/usr/bin/perl -w
2use strict;
691cde19
TC
3use FindBin;
4use lib "$FindBin::Bin/modules";
41b9d8ec 5use CGI ':standard';
41b9d8ec
TC
6use Products;
7use Product;
edc5d096 8use Constants qw(:shop $TMPLDIR %EXTRA_TAGS $CGI_URI $URLBASE);
41b9d8ec 9use Squirrel::Template;
41b9d8ec
TC
10use Squirrel::ImageEditor;
11use CGI::Cookie;
c7adeba6 12use BSE::Custom;
a82753e6 13use BSE::Mail;
2aea9be6 14use BSE::Shop::Util qw/shop_cart_tags cart_item_opts nice_options total
edc5d096
TC
15 basic_tags load_order_fields need_logon/;
16use BSE::Session;
17use BSE::Cfg;
18use Util qw/refresh_to/;
41b9d8ec
TC
19
20my $subject = $SHOP_MAIL_SUBJECT;
21
22# our PGP passphrase
23my $passphrase = $SHOP_PASSPHRASE;
24
25# the class we use to perform encryption
26# we can change this to switch between GnuPG and PGP
27my $crypto_class = $SHOP_CRYPTO;
28
29# id of the private key to use for signing
30# leave as undef to use your default key
31my $signing_id = $SHOP_SIGNING_ID;
32
41b9d8ec
TC
33# location of PGP
34my $pgpe = $SHOP_PGPE;
35my $pgp = $SHOP_PGP;
36my $gpg = $SHOP_GPG;
37
38my $from = $SHOP_FROM;
39
40my $toName = $SHOP_TO_NAME;
41my $toEmail= $SHOP_TO_EMAIL;
42
edc5d096 43my $cfg = BSE::Cfg->new();
41b9d8ec 44my %session;
edc5d096 45BSE::Session->tie_it(\%session, $cfg);
41b9d8ec
TC
46
47# this shouldn't be necessary, but it stopped working elsewhere and this
48# fixed it
49END {
50 untie %session;
51}
52
53if (!exists $session{cart}) {
54 $session{cart} = [];
55}
56
57# the keys here are the names of the buttons on the various forms
58# we also have 'delete_<number>' buttons.
59my %steps =
60 (
61 add=>\&add_item,
62 cart=>\&show_cart,
63 checkout=>\&checkout,
cd6734fc 64 recheckout => sub { checkout('', 1); },
ca849431 65 confirm => \&checkout_confirm,
41b9d8ec 66 recalc=>\&recalc,
ad80cda7 67 recalculate=>\&recalc,
41b9d8ec 68 purchase=>\&purchase,
691cde19 69 prePurchase=>\&prePurchase,
41b9d8ec
TC
70 );
71
72for my $key (keys %steps) {
c7adeba6 73 if (param($key) or param("$key.x")) {
41b9d8ec
TC
74 $steps{$key}->();
75 exit;
76 }
77}
78
79for my $key (param()) {
80 if ($key =~ /^delete_(\d+)/) {
81 remove_item($1);
82 exit;
83 }
84}
85
86show_cart();
87
88sub add_item {
89 my $addid = param('id');
90 my $quantity = param('quantity');
91 my $product;
92 $product = Products->getByPkey($addid) if $addid;
93 $product or return show_cart(); # oops
d7c3b6f9
TC
94
95 # collect the product options
96 my @options = map scalar param($_), split /,/, $product->{options};
97 grep(!defined, @options)
98 and return show_cart(); # invalid parameter
99 my $options = join(",", @options);
41b9d8ec
TC
100
101 # the product must be non-expired and listed
102 my $today = epoch_to_sql(time);
103 $product->{release} le $today and $today le $product->{expire}
104 or return show_cart();
105 $product->{listed} or return show_cart();
106
107 # we need a natural integer quantity
108 $quantity =~ /^\d+$/
109 or return show_cart();
110
111 my @cart = @{$session{cart}};
112
113 # if this is is already present, replace it
d7c3b6f9
TC
114 @cart = grep { $_->{productId} ne $addid || $_->{options} ne $options }
115 @cart;
41b9d8ec 116 push(@cart, { productId => $addid, units => $quantity,
d7c3b6f9
TC
117 price=>$product->{retailPrice},
118 options=>$options });
119
41b9d8ec
TC
120 $session{cart} = \@cart;
121 show_cart();
122}
123
41b9d8ec
TC
124sub show_cart {
125 my @cart = @{$session{cart}};
126 my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
127 my $item_index = -1;
d7c3b6f9
TC
128 my @options;
129 my $option_index;
c7adeba6
TC
130
131 $session{custom} ||= {};
132 my %custom_state = %{$session{custom}};
133
134 BSE::Custom->enter_cart(\@cart, \@cart_prods, \%custom_state);
41b9d8ec
TC
135
136 my %acts;
137 %acts =
138 (
c7adeba6 139 BSE::Custom->cart_actions(\%acts, \@cart, \@cart_prods, \%custom_state),
d7c3b6f9
TC
140 iterate_items_reset => sub { $item_index = -1; },
141 iterate_items =>
142 sub {
143 if (++$item_index < @cart) {
144 $option_index = -1;
145 @options = cart_item_opts($cart[$item_index],
146 $cart_prods[$item_index]);
147 return 1;
148 }
149 return 0;
150 },
41b9d8ec
TC
151 item =>
152 sub { $cart[$item_index]{$_[0]} || $cart_prods[$item_index]{$_[0]} },
691cde19
TC
153 extended =>
154 sub {
155 my $what = $_[0] || 'retailPrice';
156 $cart[$item_index]{units} * $cart_prods[$item_index]{$what};
157 },
41b9d8ec 158 index => sub { $item_index },
c7adeba6 159 total => sub { total(\@cart, \@cart_prods, \%custom_state) },
41b9d8ec
TC
160 money =>
161 sub {
162 my ($func, $args) = split ' ', $_[0], 2;
163 $acts{$func} || return "<: money $_[0] :>";
164 return sprintf("%.02f", $acts{$func}->($args)/100);
165 },
166 count => sub { scalar @cart },
d7c3b6f9
TC
167 iterate_options_reset => sub { $option_index = -1 },
168 iterate_options => sub { ++$option_index < @options },
169 option => sub { CGI::escapeHTML($options[$option_index]{$_[0]}) },
170 ifOptions => sub { @options },
171 options => sub { nice_options(@options) },
41b9d8ec 172 );
c7adeba6
TC
173 $session{custom} = \%custom_state;
174
41b9d8ec
TC
175 page('cart.tmpl', \%acts);
176}
177
178sub update_quantities {
179 my @cart = @{$session{cart}};
180 for my $index (0..$#cart) {
181 my $new_quantity = param("quantity_$index");
182 if (defined $new_quantity) {
183 if ($new_quantity =~ /^\s*(\d+)/) {
184 $cart[$index]{units} = $1;
185 }
186 elsif ($new_quantity =~ /^\s*$/) {
187 $cart[$index]{units} = 0;
188 }
189 }
41b9d8ec
TC
190 }
191 @cart = grep { $_->{units} != 0 } @cart;
192 $session{cart} = \@cart;
c7adeba6
TC
193 $session{custom} ||= {};
194 my %custom_state = %{$session{custom}};
195 BSE::Custom->recalc($CGI::Q, \@cart, [], \%custom_state);
196 $session{custom} = \%custom_state;
41b9d8ec
TC
197}
198
199sub recalc {
200 update_quantities();
201 show_cart();
202}
203
204sub remove_item {
205 my ($index) = @_;
206 my @cart = @{$session{cart}};
207 if ($index >= 0 && $index < @cart) {
208 splice(@cart, $index, 1);
209 }
210 $session{cart} = \@cart;
211
212 print "Refresh: 0; url=\"$ENV{SCRIPT_NAME}\"\n";
213 print "Content-Type: text/html\n\n<html> </html>\n";
214}
215
216# display the checkout form
217# can also be called with an error message and a flag to fillin the old
218# values for the form elements
219sub checkout {
220 my ($message, $olddata) = @_;
221
222 $message = '' unless defined $message;
223
224 update_quantities();
225 my @cart = @{$session{cart}};
ad80cda7
TC
226
227 @cart or return show_cart();
228
41b9d8ec 229 my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
edc5d096
TC
230
231 if (need_logon($cfg, \@cart, \@cart_prods, \%session)) {
232 refresh_to("$URLBASE/cgi-bin/user.pl?message=".
233 CGI::escape("Some of the products in your cart include downloadable files. Please logon or register before checkout."));
234 return;
235 }
236
237 my $user;
238 if ($session{userid}) {
239 require 'SiteUsers.pm';
240 $user = SiteUsers->getBy(userId=>$session{userid});
241 }
242
c7adeba6
TC
243 $session{custom} ||= {};
244 my %custom_state = %{$session{custom}};
245
246 BSE::Custom->enter_cart(\@cart, \@cart_prods, \%custom_state);
247
41b9d8ec 248 my $item_index = -1;
d7c3b6f9
TC
249 my @options;
250 my $option_index;
41b9d8ec
TC
251 my %acts;
252 %acts =
253 (
d7c3b6f9
TC
254 iterate_items_reset => sub { $item_index = -1 },
255 iterate_items =>
256 sub {
257 if (++$item_index < @cart) {
258 $option_index = -1;
259 @options = cart_item_opts($cart[$item_index],
260 $cart_prods[$item_index]);
261 return 1;
262 }
263 return 0;
264 },
41b9d8ec
TC
265 item =>
266 sub { $cart[$item_index]{$_[0]} || $cart_prods[$item_index]{$_[0]} },
691cde19
TC
267 extended =>
268 sub {
269 my $what = $_[0] || 'retailPrice';
270 $cart[$item_index]{units} * $cart_prods[$item_index]{$what};
271 },
41b9d8ec 272 index => sub { $item_index },
c7adeba6 273 total => sub { total(\@cart, \@cart_prods, \%custom_state) },
41b9d8ec
TC
274 money =>
275 sub {
276 my ($func, $args) = split ' ', $_[0], 2;
277 $acts{$func} || return "<: money $_[0] :>";
278 return sprintf("%.02f", $acts{$func}->($args)/100);
279 },
280 count => sub { scalar @cart },
281 message => sub { $message },
edc5d096
TC
282 old => sub { CGI::escapeHTML($olddata ? param($_[0]) :
283 $user && defined $user->{$_[0]} ? $user->{$_[0]} : '') },
d7c3b6f9
TC
284 iterate_options_reset => sub { $option_index = -1 },
285 iterate_options => sub { ++$option_index < @options },
286 option => sub { CGI::escapeHTML($options[$option_index]{$_[0]}) },
287 ifOptions => sub { @options },
288 options => sub { nice_options(@options) },
a82753e6 289 BSE::Custom->checkout_actions(\%acts, \@cart, \@cart_prods, \%custom_state, $CGI::Q),
41b9d8ec 290 );
c7adeba6 291 $session{custom} = \%custom_state;
41b9d8ec
TC
292
293 page('checkout.tmpl', \%acts);
294}
295
ca849431
TC
296# displays the data entered by the user so they can either confirm the
297# details or redisplay the checkout page
298sub checkout_confirm {
299 my %order;
300 my $error;
301
302 my @cart_prods;
303 unless (load_order_fields(0, $CGI::Q, \%order, \%session, \@cart_prods,
304 \$error)) {
305 return checkout($error, 1);
306 }
dec5de5e 307 ++$session{changed};
ca849431
TC
308 my @cart = @{$session{cart}};
309 # display the confirmation page
310 my %acts;
311 %acts =
312 (
313 order => sub { CGI::escapeHTML($order{$_[0]}) },
314 shop_cart_tags(\%acts, \@cart, \@cart_prods, \%session, $CGI::Q),
315 basic_tags(\%acts),
dec5de5e
TC
316 old =>
317 sub {
318 my $value = param($_[0]);
319 defined $value or $value = '';
320 CGI::escapeHTML($value);
321 },
ca849431 322 );
cd6734fc 323 page('checkoutconfirm.tmpl', \%acts);
ca849431
TC
324}
325
691cde19
TC
326# this can be used instead of the purchase page to work in 2 steps:
327# - collect shipping details
328# - collect CC details
329# the collection of the CC details should go to another script that
330# processes the CC information and then displays the order complete
331# information
332# BUG!!: this duplicates the code in purchase() a great deal
333sub prePurchase {
c7adeba6 334 my @required = BSE::Custom->required_fields($CGI::Q, $session{custom});
691cde19
TC
335 for my $field (@required) {
336 defined(param($field)) && length(param($field))
337 or return checkout("Field $field is required", 1);
338 }
dec5de5e
TC
339 if (grep /email/, @required) {
340 defined(param('email')) && param('email') =~ /.\@./
341 or return checkout("Please enter a valid email address", 1);
342 }
343
691cde19
TC
344 use Orders;
345 use Order;
346 use OrderItems;
347 use OrderItem;
348
349 # map some form fields to order field names
350 my %field_map =
351 (
352 name1 => 'delivFirstName',
353 name2 => 'delivLastName',
354 address => 'delivStreet',
355 city => 'delivSuburb',
356 postcode => 'delivPostCode',
357 state => 'delivState',
358 country => 'delivCountry',
359 email => 'emailAddress',
360 cardHolder => 'ccName',
361 cardType => 'ccType',
362 );
363 # paranoia, don't store these
364 my %nostore =
365 (
366 cardNumber => 1,
367 cardExpiry => 1,
368 );
369 my %order;
370 my @cart = @{$session{cart}};
371 @cart or return show_cart('You have no items in your shopping cart');
372
373 # so we can quickly check for columns
374 my @columns = Order->columns;
375 my %columns;
376 @columns{@columns} = @columns;
377
378 for my $field (param()) {
379 $order{$field_map{$field} || $field} = param($field)
380 unless $nostore{$field};
381 }
382
383 my $ccNumber = param('cardNumber');
384 my $ccExpiry = param('cardExpiry');
385
386 use Digest::MD5 'md5_hex';
387 $ccNumber =~ tr/0-9//cd;
388 $order{ccNumberHash} = md5_hex($ccNumber);
389 $order{ccExpiryHash} = md5_hex($ccExpiry);
390
391 # work out totals
392 $order{total} = 0;
393 $order{gst} = 0;
394 $order{wholesale} = 0;
395 my @products;
396 my $today = epoch_to_sql(time);
397 for my $item (@cart) {
398 my $product = Products->getByPkey($item->{productId});
399 # double check that it's still a valid product
400 if (!$product) {
401 return show_cart("Product $item->{productId} not found");
402 }
403 elsif ($product->{release} gt $today || $product->{expire} lt $today
404 || !$product->{listed}) {
405 return show_cart("Sorry, '$product->{title}' is no longer available");
406 }
407 push(@products, $product); # used in page rendering
408 @$item{qw/price wholesalePrice gst/} =
409 @$product{qw/retailPrice wholesalePrice gst/};
410 $order{total} += $item->{price} * $item->{units};
411 $order{wholesale} += $item->{wholesalePrice} * $item->{units};
412 $order{gst} += $item->{gst} * $item->{units};
413 }
414 $order{orderDate} = $today;
415
edc5d096
TC
416 if (need_logon($cfg, \@cart, \@products, \%session)) {
417 refresh_to("$URLBASE/cgi-bin/user.pl?message=".
418 CGI::escape("Some of the products in your cart include downloadable files. Please logon or register before checkout."));
419 return;
420 }
421
c7adeba6
TC
422 $order{total} += BSE::Custom->total_extras(\@cart, \@products,
423 $session{custom});
424 ++$session{changed};
691cde19
TC
425 # blank anything else
426 for my $column (@columns) {
427 defined $order{$column} or $order{$column} = '';
428 }
429 # make sure the user can't set these behind our backs
430 $order{filled} = 0;
431 $order{paidFor} = 0;
432
433 # this should be hard to guess
434 $order{randomId} = md5_hex(time().rand().{}.$$);
435
c7adeba6
TC
436 # check if a customizer has anything to do
437 eval {
438 BSE::Custom->order_save($CGI::Q, \%order, \@cart, \@products, $session{custom});
439 ++$session{changed};
440 };
441 if ($@) {
442 return checkout($@, 1);
443 }
444
691cde19
TC
445 # load up the database
446 my @data = @order{@columns};
447 shift @data; # lose the dummy id
448 my $order = Orders->add(@data)
449 or die "Cannot add order";
450 my @items;
451 my @item_cols = OrderItem->columns;
452 for my $row (@cart) {
453 $row->{orderId} = $order->{id};
454 my @data = @$row{@item_cols};
455 shift @data;
456 push(@items, OrderItems->add(@data));
457 }
458
459 my $item_index = -1;
460 my @options;
461 my $option_index;
462 my %acts;
463 %acts =
464 (
465 iterate_items_reset => sub { $item_index = -1; },
466 iterate_items =>
467 sub {
468 if (++$item_index < @items) {
469 $option_index = -1;
470 @options = cart_item_opts($items[$item_index],
471 $products[$item_index]);
472 return 1;
473 }
474 return 0;
475 },
476 item=> sub { CGI::escapeHTML($items[$item_index]{$_[0]}); },
477 product => sub { CGI::escapeHTML($products[$item_index]{$_[0]}) },
478 extended =>
479 sub {
480 my $what = $_[0] || 'retailPrice';
481 $items[$item_index]{units} * $items[$item_index]{$what};
482 },
483 order => sub { CGI::escapeHTML($order->{$_[0]}) },
484 money =>
485 sub {
486 my ($func, $args) = split ' ', $_[0], 2;
487 $acts{$func} || return "<: money $_[0] :>";
488 return sprintf("%.02f", $acts{$func}->($args)/100);
489 },
490 old => sub { '' },
491 _format =>
492 sub {
493 my ($value, $fmt) = @_;
494 if ($fmt =~ /^m(\d+)/) {
495 return sprintf("%$1s", sprintf("%.2f", $value/100));
496 }
497 elsif ($fmt =~ /%/) {
498 return sprintf($fmt, $value);
499 }
500 },
501 iterate_options_reset => sub { $option_index = -1 },
502 iterate_options => sub { ++$option_index < @options },
503 option => sub { CGI::escapeHTML($options[$option_index]{$_[0]}) },
504 ifOptions => sub { @options },
505 options => sub { nice_options(@options) },
506 );
507 # this should be reset once the order has been paid
508 $session{orderPayment} = $order->{id};
509
510 page('checkoutcard.tmpl', \%acts);
511}
512
41b9d8ec
TC
513# the real work
514sub purchase {
515 # some basic validation, in case the user switched off javascript
516 my @required =
c7adeba6
TC
517 (BSE::Custom->required_fields($CGI::Q, $session{custom}),
518 qw(cardHolder cardExpiry) );
41b9d8ec
TC
519 for my $field (@required) {
520 defined(param($field)) && length(param($field))
521 or return checkout("Field $field is required", 1);
522 }
523 defined(param('email')) && param('email') =~ /.\@./
524 or return checkout("Please enter a valid email address", 1);
525 defined(param('cardNumber')) && param('cardNumber') =~ /^\d+$/
526 or return checkout("Please enter a credit card number", 1);
527
528 use Orders;
529 use Order;
530 use OrderItems;
531 use OrderItem;
532
533 # map some form fields to order field names
534 my %field_map =
535 (
536 name1 => 'delivFirstName',
537 name2 => 'delivLastName',
538 address => 'delivStreet',
539 city => 'delivSuburb',
540 postcode => 'delivPostCode',
541 state => 'delivState',
542 country => 'delivCountry',
543 email => 'emailAddress',
544 cardHolder => 'ccName',
545 cardType => 'ccType',
546 );
547 # paranoia, don't store these
548 my %nostore =
549 (
550 cardNumber => 1,
551 cardExpiry => 1,
552 );
553 my %order;
554 my @cart = @{$session{cart}};
555 @cart or return show_cart('You have no items in your shopping cart');
556
557 # so we can quickly check for columns
558 my @columns = Order->columns;
559 my %columns;
560 @columns{@columns} = @columns;
561
562 for my $field (param()) {
563 $order{$field_map{$field} || $field} = param($field)
564 unless $nostore{$field};
565 }
566
567 my $ccNumber = param('cardNumber');
568 my $ccExpiry = param('cardExpiry');
569
570 use Digest::MD5 'md5_hex';
571 $ccNumber =~ tr/0-9//cd;
572 $order{ccNumberHash} = md5_hex($ccNumber);
573 $order{ccExpiryHash} = md5_hex($ccExpiry);
574
575 # work out totals
576 $order{total} = 0;
577 $order{gst} = 0;
578 $order{wholesale} = 0;
579 my @products;
580 my $today = epoch_to_sql(time);
581 for my $item (@cart) {
582 my $product = Products->getByPkey($item->{productId});
583 # double check that it's still a valid product
584 if (!$product) {
585 return show_cart("Product $item->{productId} not found");
586 }
587 elsif ($product->{release} gt $today || $product->{expire} lt $today
588 || !$product->{listed}) {
589 return show_cart("Sorry, '$product->{title}' is no longer available");
590 }
591 push(@products, $product); # used in page rendering
592 @$item{qw/price wholesalePrice gst/} =
593 @$product{qw/retailPrice wholesalePrice gst/};
594 $order{total} += $item->{price} * $item->{units};
595 $order{wholesale} += $item->{wholesalePrice} * $item->{units};
596 $order{gst} += $item->{gst} * $item->{units};
597 }
edc5d096
TC
598
599 if (need_logon($cfg, \@cart, \@products, \%session)) {
600 refresh_to("$URLBASE/cgi-bin/user.pl?message=".
601 CGI::escape("Some of the products in your cart include downloadable files. Please logon or register before checkout."));
602 return;
603 }
604
41b9d8ec 605 $order{orderDate} = $today;
c7adeba6
TC
606 $order{total} += BSE::Custom->total_extras(\@cart, \@products,
607 $session{custom});
608 ++$session{changed};
41b9d8ec
TC
609
610 # blank anything else
611 for my $column (@columns) {
612 defined $order{$column} or $order{$column} = '';
613 }
691cde19
TC
614 # make sure the user can't set these behind our backs
615 $order{filled} = 0;
616 $order{paidFor} = 0;
617
edc5d096
TC
618 if ($session{userid}) {
619 $order{userId} = $session{userid};
620 }
621 else {
622 $order{userId} = '';
623 }
624
691cde19
TC
625 # this should be hard to guess
626 $order{randomId} = md5_hex(time().rand().{}.$$);
41b9d8ec 627
c7adeba6
TC
628 # check if a customizer has anything to do
629 eval {
630 BSE::Custom->order_save($CGI::Q, \%order, \@cart, \@products);
631 };
632 if ($@) {
633 return checkout($@, 1);
634 }
635
41b9d8ec
TC
636 # load up the database
637 my @data = @order{@columns};
638 shift @data; # lose the dummy id
639 my $order = Orders->add(@data)
640 or die "Cannot add order";
641 my @items;
642 my @item_cols = OrderItem->columns;
643 for my $row (@cart) {
644 $row->{orderId} = $order->{id};
645 my @data = @$row{@item_cols};
646 shift @data;
647 push(@items, OrderItems->add(@data));
648 }
649
650 my $item_index = -1;
d7c3b6f9
TC
651 my @options;
652 my $option_index;
41b9d8ec
TC
653 my %acts;
654 %acts =
655 (
c7adeba6
TC
656 BSE::Custom->purchase_actions(\%acts, \@items, \@products,
657 $session{custom}),
41b9d8ec 658 iterate_items_reset => sub { $item_index = -1; },
d7c3b6f9
TC
659 iterate_items =>
660 sub {
661 if (++$item_index < @items) {
662 $option_index = -1;
663 @options = cart_item_opts($items[$item_index],
664 $products[$item_index]);
665 return 1;
666 }
667 return 0;
668 },
41b9d8ec
TC
669 item=> sub { CGI::escapeHTML($items[$item_index]{$_[0]}); },
670 product => sub { CGI::escapeHTML($products[$item_index]{$_[0]}) },
691cde19
TC
671 extended =>
672 sub {
673 my $what = $_[0] || 'retailPrice';
674 $items[$item_index]{units} * $items[$item_index]{$what};
675 },
41b9d8ec
TC
676 order => sub { CGI::escapeHTML($order->{$_[0]}) },
677 money =>
678 sub {
679 my ($func, $args) = split ' ', $_[0], 2;
680 $acts{$func} || return "<: money $_[0] :>";
681 return sprintf("%.02f", $acts{$func}->($args)/100);
682 },
683 _format =>
684 sub {
685 my ($value, $fmt) = @_;
686 if ($fmt =~ /^m(\d+)/) {
687 return sprintf("%$1s", sprintf("%.2f", $value/100));
688 }
689 elsif ($fmt =~ /%/) {
690 return sprintf($fmt, $value);
691 }
692 },
d7c3b6f9
TC
693 iterate_options_reset => sub { $option_index = -1 },
694 iterate_options => sub { ++$option_index < @options },
695 option => sub { CGI::escapeHTML($options[$option_index]{$_[0]}) },
696 ifOptions => sub { @options },
697 options => sub { nice_options(@options) },
41b9d8ec
TC
698 );
699 send_order($order, \@items, \@products);
700 $session{cart} = []; # empty the cart
701 page('checkoutfinal.tmpl', \%acts);
702}
703
704# sends the email order confirmation and the PGP encrypted
705# email to the site owner
706sub send_order {
707 my ($order, $items, $products) = @_;
708
709 my %extras = %EXTRA_TAGS;
710 for my $key (keys %extras) {
711 unless (ref $extras{$key}) {
712 my $data = $extras{$key};
713 $extras{$key} = sub { $data };
714 }
715 }
716
717 my $item_index = -1;
d7c3b6f9
TC
718 my @options;
719 my $option_index;
41b9d8ec
TC
720 my %acts;
721 %acts =
722 (
723 %extras,
724
725 iterate_items_reset => sub { $item_index = -1; },
d7c3b6f9
TC
726 iterate_items =>
727 sub {
728 if (++$item_index < @$items) {
729 $option_index = -1;
730 @options = cart_item_opts($items->[$item_index],
731 $products->[$item_index]);
732 return 1;
733 }
734 return 0;
735 },
41b9d8ec
TC
736 item=> sub { $items->[$item_index]{$_[0]}; },
737 product => sub { $products->[$item_index]{$_[0]} },
738 order => sub { $order->{$_[0]} },
739 extended =>
740 sub {
741 $items->[$item_index]{units} * $items->[$item_index]{$_[0]};
742 },
743 money =>
744 sub {
745 my ($func, $args) = split ' ', $_[0], 2;
746 $acts{$func} || return "<: money $_[0] :>";
747 return sprintf("%.02f", $acts{$func}->($args)/100);
748 },
749 _format =>
750 sub {
751 my ($value, $fmt) = @_;
752 if ($fmt =~ /^m(\d+)/) {
753 return sprintf("%$1s", sprintf("%.2f", $value/100));
754 }
755 elsif ($fmt =~ /%/) {
756 return sprintf($fmt, $value);
757 }
758 elsif ($fmt =~ /^\d+$/) {
759 return substr($value . (" " x $fmt), 0, $fmt);
760 }
761 else {
762 return $value;
763 }
764 },
d7c3b6f9
TC
765 iterate_options_reset => sub { $option_index = -1 },
766 iterate_options => sub { ++$option_index < @options },
767 option => sub { CGI::escapeHTML($options[$option_index]{$_[0]}) },
768 ifOptions => sub { @options },
769 options => sub { nice_options(@options) },
41b9d8ec
TC
770 );
771 my $templ = Squirrel::Template->new;
772
a82753e6 773 my $mailer = BSE::Mail->new;
41b9d8ec
TC
774 # ok, send some email
775 my $confirm = $templ->show_page($TMPLDIR, 'mailconfirm.tmpl', \%acts);
776 if ($SHOP_EMAIL_ORDER) {
777 $acts{cardNumber} = sub { param('cardNumber') };
778 $acts{cardExpiry} = sub { param('cardExpiry') };
779 my $ordertext = $templ->show_page($TMPLDIR, 'mailorder.tmpl', \%acts);
780
781 eval "use $crypto_class";
782 !$@ or die $@;
783 my $encrypter = $crypto_class->new;
784
785 # encrypt and sign
786 my %opts =
787 (
788 sign=> 1,
789 passphrase=> $passphrase,
790 stripwarn=>1,
791 #debug=>1,
792 );
793 $opts{secretkeyid} = $signing_id if $signing_id;
794 $opts{pgp} = $pgp if $pgp;
795 $opts{gpg} = $gpg if $gpg;
796 $opts{pgpe} = $pgpe if $pgpe;
797 #$opts{home} = '/home/bodyscoop';
798 my $recip = "$toName $toEmail";
799
800 my $crypted = $encrypter->encrypt($recip, $ordertext, %opts )
801 or die "Cannot encrypt ", $encrypter->error;
802
dec5de5e 803 $mailer->send(to=>$toEmail, from=>$from, subject=>'New Order '.$order->{id},
a82753e6 804 body=>$crypted)
33bccea7 805 or print STDERR "Error sending order to admin: ",$mailer->errstr,"\n";
41b9d8ec 806 }
a82753e6
TC
807 $mailer->send(to=>$order->{emailAddress}, from=>$from,
808 subject=>$subject . " " . localtime,
809 body=>$confirm)
33bccea7 810 or print STDERR "Error sending order to customer: ",$mailer->errstr,"\n";
41b9d8ec
TC
811}
812
813sub page {
814 my ($template, $acts) = @_;
815 print "Content-Type: text/html\n\n";
816 print Squirrel::Template->new->show_page($TMPLDIR, $template, $acts);
817}
818
819# convert an epoch time to sql format
820sub epoch_to_sql {
821 use POSIX 'strftime';
822 my ($time) = @_;
823
824 return strftime('%Y-%m-%d', localtime $time);
825}
826
827__END__
828
829=head1 NAME
830
831shop.pl - implements the shop for BSE
832
833=head1 DESCRIPTION
834
835shop.pl implements the shop for BSE.
836
837=head1 TAGS
838
839=head2 Cart page
840
841=over 4
842
843=item iterator ... items
844
845Iterates over the items in the shopping cart, setting the C<item> tag
846for each one.
847
848=item item I<field>
849
850Retreives the given field from the item. This can include product
851fields for this item.
852
853=item index
854
855The numeric index of the current item.
856
691cde19
TC
857=item extended [<field>]
858
859The "extended price", the product of the unit cost and the number of
860units for the current item in the cart. I<field> defaults to the
861price of the product.
862
41b9d8ec
TC
863=item money I<which> <field>
864
865Formats the given field as a money value (without a currency symbol.)
866
867=item count
868
869The number of items in the cart.
870
871=back
872
873=head2 Checkout tags
874
875This has the same tags as the L<Cart page>, and some extras:
876
877=over 4
878
879=item total
880
881The total cost of all items in the cart.
882
883This will need to be formatted as a money value with the C<money> tag.
884
885=item message
886
887An error message, if a validation error occurred.
888
889=item old I<field>
890
891The previously entered value for I<field>. This should be used as the
892value for the various checkout fields, so that if a validation error
893occurs the user won't need to re-enter values.
894
895=back
896
897=head2 Completed order
898
899These tags are used in the F<checkoutfinal_base.tmpl>.
900
901=over 4
902
903=item item I<field>
904
905=item product I<field>
906
907This is split out for these forms.
908
909=item order I<field>
910
911Order fields.
912
913=back
914
915You can also use "|format" at the end of a field to perform some
916simple formatting. Eg. <:order total |m6:> or <:order id |%06d:>.
917
918=over 4
919
920=item m<number>
921
922Formats the value as a <number> wide money value.
923
924=item %<format>
925
926Performs sprintf() formatting on the value. Eg. %06d will format 25
927as 000025.
928
929=back
930
931=head2 Mailed order tags
932
933These tags are used in the emails sent to the user to confirm an order
934and in the encrypted copy sent to the site administrator:
935
936=over 4
937
938=item iterate ... items
939
940Iterates over the items in the order.
941
942=item item I<field>
943
944Access to the given field in the order item.
945
946=item product I<field>
947
948Access to the product field for the current order item.
949
950=item order I<field>
951
952Access to fields of the order.
953
954=item extended I<field>
955
956The product of the I<field> in the current item and it's quantity.
957
958=item money I<tag> I<parameters>
959
960Formats the given field as a money value.
961
962=back
963
964The mail generation template can use extra formatting specified with
965'|format':
966
967=over 4
968
969=item m<number>
970
971Format the value as a I<number> wide money value.
972
973=item %<format>
974
975Performs sprintf formatting on the value.
976
977=item <number>
978
979Left justifies the value in a I<number> wide field.
980
981=back
982
983The order email sent to the site administrator has a couple of extra
984fields:
985
986=over 4
987
988=item cardNumber
989
990The credit card number of the user's credit card.
991
992=item cardExpiry
993
994The entered expiry date for the user's credit card.
995
996=back
997
998=head2 Order fields
999
691cde19
TC
1000These names can be used with the <: order ... :> tag.
1001
1002Monetary values should typically be used with <:money order ...:>
1003
41b9d8ec
TC
1004=over 4
1005
1006=item id
1007
1008The order id or order number.
1009
1010=item delivFirstName
1011
1012=item delivLastName
1013
1014=item delivStreet
1015
1016=item delivSuburb
1017
1018=item delivState
1019
1020=item delivPostCode
1021
1022=item delivCountry
1023
1024Delivery information for the order.
1025
1026=item billFirstName
1027
1028=item billLastName
1029
1030=item billStreet
1031
1032=item billSuburb
1033
1034=item billState
1035
1036=item billPostCode
1037
1038=item billCountry
1039
1040Billing information for the order.
1041
1042=item telephone
1043
1044=item facsimile
1045
1046=item emailAddress
1047
1048Contact information for the order.
1049
1050=item total
1051
1052Total price of the order.
1053
1054=item wholesaleTotal
1055
1056Wholesale cost of the total. Your costs, if you entered wholesale
1057prices for the products.
1058
1059=item gst
1060
1061GST (in Australia) payable on the order, if you entered GST for the products.
1062
1063=item orderDate
1064
1065When the order was made.
1066
691cde19
TC
1067=item filled
1068
1069Whether or not the order has been filled. This can be used with the
1070order_filled target in shopadmin.pl for tracking filled orders.
1071
1072=item whenFilled
1073
1074The time and date when the order was filled.
1075
1076=item whoFilled
1077
1078The user who marked the order as filled.
1079
1080=item paidFor
1081
1082Whether or not the order has been paid for. This can be used with a
1083custom purchasing handler to mark the product as paid for. You can
1084then filter the order list to only display paid for orders.
1085
1086=item paymentReceipt
1087
1088A custom payment handler can fill this with receipt information.
1089
1090=item randomId
1091
1092Generated by the prePurchase target, this can be used as a difficult
1093to guess identifier for orders, when working with custom payment
1094handlers.
1095
1096=item cancelled
1097
1098This can be used by a custom payment handler to mark an order as
1099cancelled if the user starts processing an order without completing
1100payment.
1101
41b9d8ec
TC
1102=back
1103
1104=head2 Order item fields
1105
1106=over 4
1107
1108=item productId
1109
1110The product id of this item.
1111
1112=item orderId
1113
1114The order Id.
1115
1116=item units
1117
1118The number of units for this item.
1119
1120=item price
1121
1122The price paid for the product.
1123
1124=item wholesalePrice
1125
1126The wholesale price for the product.
1127
1128=item gst
1129
1130The gst for the product.
1131
691cde19
TC
1132=item options
1133
1134A comma separated list of options specified for this item. These
1135correspond to the option names in the product.
1136
41b9d8ec
TC
1137=back
1138
d7c3b6f9
TC
1139=head2 Options
1140
1141New with 0.10_04 is the facility to set options for each product.
1142
1143The cart, checkout and checkoutfinal pages now include the following
1144tags:
1145
1146=over
1147
1148=item iterator ... options
1149
1150within an item, iterates over the options for this item in the cart.
1151Sets the item tag.
1152
1153=item option field
1154
1155Retrieves the given field from the option, possible field names are:
1156
1157=over
1158
1159=item id
1160
1161The type/identifier for this option. eg. msize for a male clothing
1162size field.
1163
1164=item value
1165
1166The underlying value of the option, eg. XL.
1167
1168=item desc
1169
1170The description of the field from the product options hash. If the
1171description isn't defined this is the same as the id. eg. Size.
1172
1173=item label
1174
1175The description of the value from the product options hash.
1176eg. "Extra large".
1177
1178=back
1179
1180=item ifOptions
1181
1182A conditional tag, true if the current cart item has any options.
1183
1184=item options
1185
1186A simple rendering of the options as a parenthesized comma-separated
1187list.
1188
1189=back
1190
41b9d8ec 1191=cut