]> git.imager.perl.org - bse.git/blame - site/cgi-bin/modules/BSE/UI/Shop.pm
0.15_15 commit
[bse.git] / site / cgi-bin / modules / BSE / UI / Shop.pm
CommitLineData
41e7c841
TC
1package BSE::UI::Shop;
2use strict;
3use base 'BSE::UI::Dispatch';
4use DevHelp::HTML;
5use BSE::Util::SQL qw(now_sqldate now_sqldatetime);
5d88571c
TC
6use BSE::Shop::Util qw(need_logon shop_cart_tags payment_types nice_options
7 cart_item_opts basic_tags);
41e7c841
TC
8use BSE::CfgInfo qw(custom_class credit_card_class);
9use BSE::TB::Orders;
10use BSE::TB::OrderItems;
11use BSE::Mail;
718a070d 12use BSE::Util::Tags qw(tag_error_img tag_hash);
41e7c841 13use Products;
718a070d 14use BSE::TB::Seminars;
41e7c841 15use DevHelp::Validate qw(dh_validate dh_validate_hash);
2c9b9618 16use Digest::MD5 'md5_hex';
41e7c841
TC
17
18use constant PAYMENT_CC => 0;
19use constant PAYMENT_CHEQUE => 1;
20use constant PAYMENT_CALLME => 2;
21
22my %actions =
23 (
24 add => 1,
25 cart => 1,
26 checkout => 1,
27 checkupdate => 1,
28 recheckout => 1,
29 confirm => 1,
30 recalc=>1,
31 recalculate => 1,
32 #purchase => 1,
33 order => 1,
34 show_payment => 1,
35 payment => 1,
36 orderdone => 1,
718a070d 37 location => 1,
41e7c841
TC
38 );
39
a392c69e
TC
40my %field_map =
41 (
42 name1 => 'delivFirstName',
43 name2 => 'delivLastName',
44 address => 'delivStreet',
37dd20ad 45 organization => 'delivOrganization',
a392c69e
TC
46 city => 'delivSuburb',
47 postcode => 'delivPostCode',
48 state => 'delivState',
49 country => 'delivCountry',
50 email => 'emailAddress',
51 cardHolder => 'ccName',
52 cardType => 'ccType',
53 );
54
55my %rev_field_map = reverse %field_map;
56
41e7c841
TC
57sub actions { \%actions }
58
59sub default_action { 'cart' }
60
61sub other_action {
62 my ($class, $cgi) = @_;
63
64 for my $key ($cgi->param()) {
2c9b9618 65 if ($key =~ /^delete_(\d+)(?:\.x)?$/) {
41e7c841
TC
66 return ( remove_item => $1 );
67 }
68 }
69
70 return;
71}
72
73sub req_cart {
74 my ($class, $req, $msg) = @_;
75
76 my @cart = @{$req->session->{cart} || []};
a392c69e
TC
77 my @cart_prods;
78 my @items = $class->_build_items($req, \@cart_prods);
41e7c841
TC
79 my $item_index = -1;
80 my @options;
81 my $option_index;
82
83 $req->session->{custom} ||= {};
84 my %custom_state = %{$req->session->{custom}};
85
86 my $cust_class = custom_class($req->cfg);
87 $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $req->cfg);
88 $msg = '' unless defined $msg;
89 $msg = escape_html($msg);
90
91 my %acts;
92 %acts =
93 (
94 $cust_class->cart_actions(\%acts, \@cart, \@cart_prods, \%custom_state,
95 $req->cfg),
a392c69e 96 shop_cart_tags(\%acts, \@items, \@cart_prods, $req->session, $req->cgi,
41e7c841
TC
97 $req->cfg, 'cart'),
98 basic_tags(\%acts),
99 msg => $msg,
100 );
101 $req->session->{custom} = \%custom_state;
102 $req->session->{order_info_confirmed} = 0;
103
104 return $req->response('cart', \%acts);
105}
106
107sub req_add {
108 my ($class, $req) = @_;
109
110 my $cgi = $req->cgi;
111
112 my $addid = $cgi->param('id');
113 $addid ||= '';
114 my $quantity = $cgi->param('quantity');
115 $quantity ||= 1;
116 my $product;
718a070d
TC
117 if ($addid) {
118 $product = BSE::TB::Seminars->getByPkey($addid);
119 $product ||= Products->getByPkey($addid);
120 }
41e7c841
TC
121 $product or
122 return $class->req_cart($req, "Cannot find product $addid"); # oops
123
124 # collect the product options
125 my @options;
126 my @opt_names = split /,/, $product->{options};
127 my @not_def;
128 for my $name (@opt_names) {
129 my $value = $cgi->param($name);
130 push @options, $value;
131 unless (defined $value) {
132 push @not_def, $name;
133 }
134 }
135 @not_def
136 and return $class->req_cart($req, "Some product options (@not_def) not supplied");
137 my $options = join(",", @options);
138
139 # the product must be non-expired and listed
140 (my $comp_release = $product->{release}) =~ s/ .*//;
141 (my $comp_expire = $product->{expire}) =~ s/ .*//;
142 my $today = now_sqldate();
143 $comp_release le $today
144 or return $class->req_cart($req, "Product has not been released yet");
145 $today le $comp_expire
146 or return $class->req_cart($req, "Product has expired");
147 $product->{listed} or return $class->req_cart($req, "Product not available");
148
149 # used to refresh if a logon is needed
150 my $securlbase = $req->cfg->entryVar('site', 'secureurl');
151 my $r = $securlbase . $ENV{SCRIPT_NAME} . "?add=1&id=$addid";
152 for my $opt_index (0..$#opt_names) {
153 $r .= "&$opt_names[$opt_index]=".escape_uri($options[$opt_index]);
154 }
155
156 my $user = $req->siteuser;
157 # need to be logged on if it has any subs
158 if ($product->{subscription_id} != -1) {
159 if ($user) {
160 my $sub = $product->subscription;
161 if ($product->is_renew_sub_only) {
162 unless ($user->subscribed_to_grace($sub)) {
37dd20ad 163 return $class->req_cart($req, "This product can only be used to renew your subscription to $sub->{title} and you are not subscribed nor within the renewal grace period");
41e7c841
TC
164 }
165 }
166 elsif ($product->is_start_sub_only) {
167 if ($user->subscribed_to_grace($sub)) {
37dd20ad 168 return $class->req_cart($req, "This product can only be used to start your subscription to $sub->{title} and you are already subscribed or within the grace period");
41e7c841
TC
169 }
170 }
171 }
172 else {
173 return $class->_refresh_logon
174 ($req, "You must be logged on to add this product to your cart",
175 'prodlogon', $r);
176 }
177 }
178 if ($product->{subscription_required} != -1) {
179 my $sub = $product->subscription_required;
180 if ($user) {
181 unless ($user->subscribed_to($sub)) {
182 return $class->req_cart($req, "You must be subscribed to $sub->{title} to purchase this product");
183 return;
184 }
185 }
186 else {
187 # we want to refresh back to adding the item to the cart if possible
188 return $class->_refresh_logon
189 ($req, "You must be logged on and subscribed to $sub->{title} to add this product to your cart",
190 'prodlogonsub', $r);
191 }
192 }
193
194 # we need a natural integer quantity
195 $quantity =~ /^\d+$/
196 or return $class->req_cart($req, "Invalid quantity");
197
718a070d
TC
198 my %extras;
199 if ($product->isa('BSE::TB::Seminar')) {
200 # you must be logged on to add a seminar
201 unless ($user) {
202 return $class->_refresh_logon
203 ($req, "You must be logged on to add seminars to your cart",
204 'seminarlogon', $r);
205 }
206
207 # get and validate the session
208 my $session_id = $cgi->param('session_id');
209 defined $session_id
210 or return $class->req_cart($req, "Please select a session when adding a seminar");
211 $session_id =~ /^\d+$/
212 or return $class->req_cart($req, "Invalid session_id supplied");
213 require BSE::TB::SeminarSessions;
214 my $session = BSE::TB::SeminarSessions->getByPkey($session_id)
215 or return $class->req_cart($req, "Unknown session id supplied");
216 $session->{seminar_id} == $addid
217 or return $class->req_cart($req, "Session not for this seminar");
218
219 # check if the user is already booked for this session
220 grep($_ == $session_id, $user->seminar_sessions_booked($addid))
221 and return $class->req_cart($req, "You are already booked for this session");
222
223 $extras{session_id} = $session_id;
224 }
225
41e7c841
TC
226 $req->session->{cart} ||= [];
227 my @cart = @{$req->session->{cart}};
228
229 # if this is is already present, replace it
230 @cart = grep { $_->{productId} ne $addid || $_->{options} ne $options }
231 @cart;
232 push @cart,
233 {
234 productId => $addid,
235 units => $quantity,
236 price=>$product->{retailPrice},
718a070d
TC
237 options=>$options,
238 %extras,
41e7c841
TC
239 };
240
241 $req->session->{cart} = \@cart;
242 $req->session->{order_info_confirmed} = 0;
243
244 return $class->req_cart($req);
245}
246
247sub req_checkout {
248 my ($class, $req, $message, $olddata) = @_;
249
250 my $errors = {};
251 if (defined $message) {
252 if (ref $message) {
253 $errors = $message;
254 $message = $req->message($errors);
255 }
256 }
257 else {
258 $message = '';
259 }
260 my $cfg = $req->cfg;
261 my $cgi = $req->cgi;
262
263 $class->update_quantities($req);
264 my @cart = @{$req->session->{cart}};
265
266 @cart or return $class->req_cart($req);
267
a392c69e
TC
268 my @cart_prods;
269 my @items = $class->_build_items($req, \@cart_prods);
41e7c841
TC
270
271 if (my ($msg, $id) = $class->_need_logon($req, \@cart, \@cart_prods)) {
272 return $class->_refresh_logon($req, $msg, $id);
273 return;
274 }
275
276 my $user = $req->siteuser;
277
278 $req->session->{custom} ||= {};
279 my %custom_state = %{$req->session->{custom}};
280
281 my $cust_class = custom_class($cfg);
282 $cust_class->enter_cart(\@cart, \@cart_prods, \%custom_state, $cfg);
283
284 my $affiliate_code = $req->session->{affiliate_code};
285 defined $affiliate_code or $affiliate_code = '';
286
a392c69e
TC
287 my $order_info = $req->session->{order_info};
288
41e7c841
TC
289 my $item_index = -1;
290 my @options;
291 my $option_index;
292 my %acts;
293 %acts =
294 (
a392c69e 295 shop_cart_tags(\%acts, \@items, \@cart_prods, $req->session, $req->cgi,
41e7c841
TC
296 $cfg, 'checkout'),
297 basic_tags(\%acts),
298 message => $message,
299 msg => $message,
300 old =>
301 sub {
302 my $value;
303
304 if ($olddata) {
305 $value = $cgi->param($_[0]);
306 unless (defined $value) {
307 $value = $user->{$_[0]}
308 if $user;
309 }
310 }
a392c69e
TC
311 elsif ($order_info && defined $order_info->{$_[0]}) {
312 $value = $order_info->{$_[0]};
313 }
41e7c841 314 else {
a392c69e
TC
315 my $field = $_[0];
316 $rev_field_map{$field} and $field = $rev_field_map{$field};
317 $value = $user && defined $user->{$field} ? $user->{$field} : '';
41e7c841
TC
318 }
319
320 defined $value or $value = '';
321 escape_html($value);
322 },
323 $cust_class->checkout_actions(\%acts, \@cart, \@cart_prods,
324 \%custom_state, $req->cgi, $cfg),
325 ifUser => defined $user,
326 user => $user ? [ \&tag_hash, $user ] : '',
327 affiliate_code => escape_html($affiliate_code),
328 error_img => [ \&tag_error_img, $cfg, $errors ],
329 );
330 $req->session->{custom} = \%custom_state;
331
332 return $req->response('checkoutnew', \%acts);
333}
334
335sub req_checkupdate {
336 my ($class, $req) = @_;
337
2c9b9618 338 $req->session->{cart} ||= [];
41e7c841
TC
339 my @cart = @{$req->session->{cart}};
340 my @cart_prods = map { Products->getByPkey($_->{productId}) } @cart;
341 $req->session->{custom} ||= {};
342 my %custom_state = %{$req->session->{custom}};
343 custom_class($req->cfg)
344 ->checkout_update($req->cgi, \@cart, \@cart_prods, \%custom_state, $req->cfg);
345 $req->session->{custom} = \%custom_state;
346 $req->session->{order_info_confirmed} = 0;
347
348 return $class->req_checkout($req, "", 1);
349}
350
351sub req_remove_item {
352 my ($class, $req, $index) = @_;
2c9b9618
TC
353
354 $req->session->{cart} ||= [];
41e7c841
TC
355 my @cart = @{$req->session->{cart}};
356 if ($index >= 0 && $index < @cart) {
357 splice(@cart, $index, 1);
358 }
359 $req->session->{cart} = \@cart;
360 $req->session->{order_info_confirmed} = 0;
361
362 return BSE::Template->get_refresh($ENV{SCRIPT_NAME}, $req->cfg);
363}
364
41e7c841
TC
365
366# saves order and refresh to payment page
367sub req_order {
368 my ($class, $req) = @_;
369
370 my $cfg = $req->cfg;
371 my $cgi = $req->cgi;
372
373 $req->session->{cart} && @{$req->session->{cart}}
374 or return $class->req_cart($req, "Your cart is empty");
375
376 my $msg;
377 $class->_validate_cfg($req, \$msg)
378 or return $class->req_cart($req, $msg);
379
380 my @products;
381 my @items = $class->_build_items($req, \@products);
382
383 my $id;
384 if (($msg, $id) = $class->_need_logon($req, \@items, \@products)) {
385 return $class->_refresh_logon($req, $msg, $id);
386 }
387
388 # some basic validation, in case the user switched off javascript
389 my $cust_class = custom_class($cfg);
390
391 my %fields = BSE::TB::Order->valid_fields($cfg);
392 my %rules = BSE::TB::Order->valid_rules($cfg);
393
394 my %errors;
395 my %values;
396 for my $name (keys %fields) {
397 ($values{$name}) = $cgi->param($name);
398 }
399
400 my @required =
401 $cust_class->required_fields($cgi, $req->session->{custom}, $cfg);
402
403 for my $name (@required) {
404 $field_map{$name} and $name = $field_map{$name};
405
406 $fields{$name}{required} = 1;
407 }
408
409 dh_validate_hash(\%values, \%errors, { rules=>\%rules, fields=>\%fields },
410 $cfg, 'Shop Order Validation');
411 keys %errors
412 and return $class->req_checkout($req, \%errors, 1);
413
414 $class->_fillout_order($req, \%values, \@items, \$msg, 'payment')
415 or return $class->req_checkout($req, $msg, 1);
416
417 $req->session->{order_info} = \%values;
418 $req->session->{order_info_confirmed} = 1;
419
420 return BSE::Template->get_refresh("$ENV{SCRIPT_NAME}?a_show_payment=1", $req->cfg);
421}
422
423sub req_show_payment {
424 my ($class, $req, $errors) = @_;
425
426 $req->session->{order_info_confirmed}
427 or return $class->req_checkout($req, 'Please proceed via the checkout page');
428
2c9b9618
TC
429 $req->session->{cart} && @{$req->session->{cart}}
430 or return $class->req_cart($req, "Your cart is empty");
431
41e7c841
TC
432 my $cfg = $req->cfg;
433 my $cgi = $req->cgi;
434
435 $errors ||= {};
436 my $msg = $req->message($errors);
437
438 my $order_values = $req->session->{order_info}
439 or return $class->req_checkout($req, "You need to enter order information first");
440
441 my @pay_types = payment_types($cfg);
442 my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
443 my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
444 @payment_types or @payment_types = ( PAYMENT_CALLME );
445 @payment_types = sort { $a <=> $b } @payment_types;
446 my %payment_types = map { $_=> 1 } @payment_types;
447 my $payment;
448 $errors and $payment = $cgi->param('paymentType');
449 defined $payment or $payment = $payment_types[0];
450
451 my @products;
452 my @items = $class->_build_items($req, \@products);
453
454 my %acts;
455 %acts =
456 (
457 basic_tags(\%acts),
458 message => $msg,
459 msg => $msg,
460 order => [ \&tag_hash, $order_values ],
461 shop_cart_tags(\%acts, \@items, \@products, $req->session, $req->cgi,
462 $req->cfg, 'payment'),
463 ifMultPaymentTypes => @payment_types > 1,
464 checkedPayment => [ \&tag_checkedPayment, $payment, \%types_by_name ],
465 ifPayments => [ \&tag_ifPayments, \@payment_types, \%types_by_name ],
466 error_img => [ \&tag_error_img, $cfg, $errors ],
467 );
468 for my $type (@pay_types) {
469 my $id = $type->{id};
470 my $name = $type->{name};
471 $acts{"if${name}Payments"} = exists $payment_types{$id};
472 $acts{"if${name}FirstPayment"} = $payment_types[0] == $id;
473 $acts{"checkedIfFirst$name"} = $payment_types[0] == $id ? "checked " : "";
474 $acts{"checkedPayment$name"} = $payment == $id ? 'checked="checked" ' : "";
475 }
476
477 return $req->response('checkoutpay', \%acts);
478}
479
480my %nostore =
481 (
482 cardNumber => 1,
483 cardExpiry => 1,
484 );
485
486sub req_payment {
487 my ($class, $req, $errors) = @_;
488
489 $req->session->{order_info_confirmed}
490 or return $class->req_checkout($req, 'Please proceed via the checkout page');
491
492 my $order_values = $req->session->{order_info}
493 or return $class->req_checkout($req, "You need to enter order information first");
494
495
496 my $cgi = $req->cgi;
497 my $cfg = $req->cfg;
498 my $session = $req->session;
499
500 my @pay_types = payment_types($cfg);
501 my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
502 my %pay_types = map { $_->{id} => $_ } @pay_types;
503 my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
504 @payment_types or @payment_types = ( PAYMENT_CALLME );
505 @payment_types = sort { $a <=> $b } @payment_types;
506 my %payment_types = map { $_=> 1 } @payment_types;
507
508 my $paymentType = $cgi->param('paymentType');
509 defined $paymentType or $paymentType = $payment_types[0];
510 $payment_types{$paymentType}
511 or return $class->req_show_payment($req, { paymentType => "Invalid payment type" } , 1);
512
513 my @required;
514 push @required, @{$pay_types{$paymentType}{require}};
515
516 my %fields = BSE::TB::Order->valid_payment_fields($cfg);
517 my %rules = BSE::TB::Order->valid_payment_rules($cfg);
518 for my $field (@required) {
519 if (exists $fields{$field}) {
520 $fields{$field}{required} = 1;
521 }
522 else {
523 $fields{$field} = { description => $field, required=> 1 };
524 }
525 }
526
527 my %errors;
528 dh_validate($cgi, \%errors, { rules => \%rules, fields=>\%fields },
529 $cfg, 'Shop Order Validation');
530 keys %errors
531 and return $class->req_show_payment($req, \%errors);
532
533 my @products;
534 my @items = $class->_build_items($req, \@products);
535
536 for my $field (@required) {
537 unless ($nostore{$field}) {
538 ($order_values->{$field}) = $cgi->param($field);
539 }
540 }
541
542 $order_values->{filled} = 0;
543 $order_values->{paidFor} = 0;
544
545 my $cust_class = custom_class($req->cfg);
546 eval {
547 my %custom = %{$session->{custom}};
548 $cust_class->order_save($cgi, $order_values, \@items, \@products,
549 \%custom, $cfg);
550 $session->{custom} = \%custom;
551 };
552 if ($@) {
553 return $class->req_checkout($req, $@, 1);
554 }
555
556 my @columns = BSE::TB::Order->columns;
557 my %columns;
558 @columns{@columns} = @columns;
559
560 for my $col (@columns) {
561 defined $order_values->{$col} or $order_values->{$col} = '';
562 }
563
564 $order_values->{paymentType} = $paymentType;
565
566 my @data = @{$order_values}{@columns};
567 shift @data;
5d88571c
TC
568
569 my $order;
570 if ($session->{order_work}) {
571 $order = BSE::TB::Orders->getByPkey($session->{order_work});
572 }
573 if ($order) {
574 print STDERR "Recycling order $order->{id}\n";
575
576 my @allbutid = @columns;
577 shift @allbutid;
578 @{$order}{@allbutid} = @data;
579
580 $order->clear_items;
581 }
582 else {
583 $order = BSE::TB::Orders->add(@data)
584 or die "Cannot add order";
585 }
41e7c841
TC
586
587 my @dbitems;
588 my %subscribing_to;
589 my @item_cols = BSE::TB::OrderItem->columns;
590 for my $row_num (0..$#items) {
591 my $item = $items[$row_num];
592 my $product = $products[$row_num];
593 $item->{orderId} = $order->{id};
594 $item->{max_lapsed} = 0;
595 if ($product->{subscription_id} != -1) {
596 my $sub = $product->subscription;
597 $item->{max_lapsed} = $sub->{max_lapsed} if $sub;
598 }
599 my @data = @{$item}{@item_cols};
600
601 shift @data;
602 push(@dbitems, BSE::TB::OrderItems->add(@data));
603
604 my $sub = $product->subscription;
605 if ($sub) {
606 $subscribing_to{$sub->{text_id}} = $sub;
607 }
718a070d
TC
608
609 if ($item->{session_id}) {
610 my $user = $req->siteuser;
611 require BSE::TB::SeminarSessions;
612 my $session = BSE::TB::SeminarSessions->getByPkey($item->{session_id});
613 eval {
614 $session->add_attendee($user, 0);
615 };
616 }
41e7c841 617 }
5d88571c
TC
618
619 $order->{ccOnline} = 0;
41e7c841
TC
620
621 my $ccprocessor = $cfg->entry('shop', 'cardprocessor');
622 if ($paymentType == PAYMENT_CC && $ccprocessor) {
623 my $cc_class = credit_card_class($cfg);
624
625 $order->{ccOnline} = 1;
626
627 my $ccNumber = $cgi->param('cardNumber');
628 my $ccExpiry = $cgi->param('cardExpiry');
629 $ccExpiry =~ m!^(\d+)\D(\d+)$! or die;
630 my ($month, $year) = ($1, $2);
631 $year > 2000 or $year += 2000;
632 my $expiry = sprintf("%04d%02d", $year, $month);
633 my $verify = $cgi->param('cardVerify');
634 defined $verify or $verify = '';
635 my $result = $cc_class->payment(orderno=>$order->{id},
636 amount => $order->{total},
637 cardnumber => $ccNumber,
638 expirydate => $expiry,
639 cvv => $verify,
640 ipaddress => $ENV{REMOTE_ADDR});
641 unless ($result->{success}) {
642 use Data::Dumper;
643 print STDERR Dumper($result);
644 # failed, back to payments
645 $order->{ccSuccess} = 0;
646 $order->{ccStatus} = $result->{statuscode};
647 $order->{ccStatus2} = 0;
648 $order->{ccStatusText} = $result->{error};
649 $order->{ccTranId} = '';
650 $order->save;
651 $errors{cardNumber} = $result->{error};
652 $session->{order_work} = $order->{id};
653 return $class->req_show_payment($req, \%errors);
654 }
655
656 $order->{ccSuccess} = 1;
657 $order->{ccReceipt} = $result->{receipt};
658 $order->{ccStatus} = 0;
659 $order->{ccStatus2} = 0;
660 $order->{ccStatusText} = '';
661 $order->{ccTranId} = $result->{transactionid};
662 $order->{paidFor} = 1;
41e7c841
TC
663 }
664
5d88571c
TC
665 # order complete
666 $order->{complete} = 1;
667 $order->save;
668
41e7c841
TC
669 # set the order displayed by orderdone
670 $session->{order_completed} = $order->{id};
671 $session->{order_completed_at} = time;
672
673 my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
674 $class->_send_order($req, $order, \@dbitems, \@products, $noencrypt,
675 \%subscribing_to);
676
677 # empty the cart ready for the next order
678 delete @{$session}{qw/order_info order_info_confirmed cart order_work/};
679
680 return BSE::Template->get_refresh("$ENV{SCRIPT_NAME}?a_orderdone=1", $req->cfg);
681}
682
683sub req_orderdone {
684 my ($class, $req) = @_;
685
686 my $session = $req->session;
687 my $cfg = $req->cfg;
688
689 my $id = $session->{order_completed};
690 my $when = $session->{order_completed_at};
691 $id && defined $when && time < $when + 500
692 or return $class->req_cart($req);
693
694 my $order = BSE::TB::Orders->getByPkey($id)
695 or return $class->req_cart($req);
696 my @items = $order->items;
41e7c841
TC
697 my @products = map { Products->getByPkey($_->{productId}) } @items;
698
2c9b9618
TC
699 my @item_cols = BSE::TB::OrderItem->columns;
700 my %copy_cols = map { $_ => 1 } Product->columns;
701 delete @copy_cols{@item_cols};
702 my @copy_cols = keys %copy_cols;
703 my @showitems;
704 for my $item_index (0..$#items) {
705 my $item = $items[$item_index];
706 my $product = $products[$item_index];
707 my %entry;
708 @entry{@item_cols} = @{$item}{@item_cols};
709 @entry{@copy_cols} = @{$product}{@copy_cols};
710
711 push @showitems, \%entry;
712 }
713
41e7c841
TC
714 my $cust_class = custom_class($req->cfg);
715
716 my @pay_types = payment_types($cfg);
717 my @payment_types = map $_->{id}, grep $_->{enabled}, @pay_types;
718 my %pay_types = map { $_->{id} => $_ } @pay_types;
719 my %types_by_name = map { $_->{name} => $_->{id} } @pay_types;
720
721 my $item_index = -1;
722 my @options;
723 my $option_index;
718a070d
TC
724 my $item;
725 my $product;
726 my $sem_session;
727 my $location;
41e7c841
TC
728 my %acts;
729 %acts =
730 (
731 $cust_class->purchase_actions(\%acts, \@items, \@products,
732 $session->{custom}, $cfg),
733 BSE::Util::Tags->static(\%acts, $cfg),
734 iterate_items_reset => sub { $item_index = -1; },
735 iterate_items =>
736 sub {
737 if (++$item_index < @items) {
738 $option_index = -1;
739 @options = cart_item_opts($items[$item_index],
740 $products[$item_index]);
718a070d
TC
741 undef $sem_session;
742 undef $location;
743 $item = $items[$item_index];
744 $product = $products[$item_index];
41e7c841
TC
745 return 1;
746 }
718a070d
TC
747 undef $item;
748 undef $sem_session;
749 undef $product;
750 undef $location;
41e7c841
TC
751 return 0;
752 },
2c9b9618 753 item=> sub { escape_html($showitems[$item_index]{$_[0]}); },
41e7c841
TC
754 product =>
755 sub {
756 my $value = $products[$item_index]{$_[0]};
757 defined $value or $value = '';
758
759 escape_html($value);
760 },
761 extended =>
762 sub {
763 my $what = $_[0] || 'retailPrice';
764 $items[$item_index]{units} * $items[$item_index]{$what};
765 },
766 order => sub { escape_html($order->{$_[0]}) },
767 money =>
768 sub {
769 my ($func, $args) = split ' ', $_[0], 2;
770 $acts{$func} || return "<: money $_[0] :>";
771 return sprintf("%.02f", $acts{$func}->($args)/100);
772 },
773 _format =>
774 sub {
775 my ($value, $fmt) = @_;
776 if ($fmt =~ /^m(\d+)/) {
777 return sprintf("%$1s", sprintf("%.2f", $value/100));
778 }
779 elsif ($fmt =~ /%/) {
780 return sprintf($fmt, $value);
781 }
782 },
783 iterate_options_reset => sub { $option_index = -1 },
784 iterate_options => sub { ++$option_index < @options },
785 option => sub { escape_html($options[$option_index]{$_[0]}) },
786 ifOptions => sub { @options },
787 options => sub { nice_options(@options) },
788 ifPayment => [ \&tag_ifPayment, $order->{paymentType}, \%types_by_name ],
789 #ifSubscribingTo => [ \&tag_ifSubscribingTo, \%subscribing_to ],
718a070d
TC
790 session => [ \&tag_session, \$item, \$sem_session ],
791 location => [ \&tag_location, \$item, \$location ],
41e7c841
TC
792 );
793 for my $type (@pay_types) {
794 my $id = $type->{id};
795 my $name = $type->{name};
796 $acts{"if${name}Payment"} = $order->{paymentType} == $id;
797 }
798
799 return $req->response('checkoutfinal', \%acts);
800}
801
718a070d
TC
802sub tag_session {
803 my ($ritem, $rsession, $arg) = @_;
804
805 $$ritem or return '';
806
807 $$ritem->{session_id} or return '';
808
809 unless ($$rsession) {
810 require BSE::TB::SeminarSessions;
811 $$rsession = BSE::TB::SeminarSessions->getByPkey($$ritem->{session_id})
812 or return '';
813 }
814
815 my $value = $$rsession->{$arg};
816 defined $value or return '';
817
818 escape_html($value);
819}
820
821sub tag_location {
822 my ($ritem, $rlocation, $arg) = @_;
823
824 $$ritem or return '';
825
826 $$ritem->{session_id} or return '';
827
828 unless ($$rlocation) {
829 require BSE::TB::Locations;
830 ($$rlocation) = BSE::TB::Locations->getSpecial(session_id => $$ritem->{session_id})
831 or return '';
832 }
833
834 my $value = $$rlocation->{$arg};
835 defined $value or return '';
836
837 escape_html($value);
838}
839
41e7c841
TC
840sub tag_ifPayment {
841 my ($payment, $types_by_name, $args) = @_;
842
843 my $type = $args;
844 if ($type !~ /^\d+$/) {
845 return '' unless exists $types_by_name->{$type};
846 $type = $types_by_name->{$type};
847 }
848
849 return $payment == $type;
850}
851
852
853sub _validate_cfg {
854 my ($class, $req, $rmsg) = @_;
855
856 my $cfg = $req->cfg;
857 my $from = $cfg->entry('shop', 'from', $Constants::SHOP_FROM);
858 unless ($from && $from =~ /.\@./) {
859 $$rmsg = "Configuration error: shop from address not set";
860 return;
861 }
862 my $toEmail = $cfg->entry('shop', 'to_email', $Constants::SHOP_TO_EMAIL);
863 unless ($toEmail && $toEmail =~ /.\@./) {
864 $$rmsg = "Configuration error: shop to_email address not set";
865 return;
866 }
867
868 return 1;
869}
870
41e7c841
TC
871sub req_recalc {
872 my ($class, $req) = @_;
2c9b9618 873
41e7c841
TC
874 $class->update_quantities($req);
875 $req->session->{order_info_confirmed} = 0;
876 return $class->req_cart($req);
877}
878
879sub req_recalculate {
880 my ($class, $req) = @_;
881
882 return $class->req_recalc($req);
883}
884
885sub _send_order {
886 my ($class, $req, $order, $items, $products, $noencrypt,
887 $subscribing_to) = @_;
888
889 my $cfg = $req->cfg;
890 my $cgi = $req->cgi;
891
892 my $crypto_class = $Constants::SHOP_CRYPTO;
893 my $signing_id = $Constants::SHOP_SIGNING_ID;
894 my $pgp = $Constants::SHOP_PGP;
895 my $pgpe = $Constants::SHOP_PGPE;
896 my $gpg = $Constants::SHOP_GPG;
897 my $passphrase = $Constants::SHOP_PASSPHRASE;
898 my $from = $cfg->entry('shop', 'from', $Constants::SHOP_FROM);
899 my $toName = $cfg->entry('shop', 'to_name', $Constants::SHOP_TO_NAME);
900 my $toEmail = $cfg->entry('shop', 'to_email', $Constants::SHOP_TO_EMAIL);
901 my $subject = $cfg->entry('shop', 'subject', $Constants::SHOP_MAIL_SUBJECT);
902
903 my $session = $req->session;
904 my %extras = $cfg->entriesCS('extra tags');
905 for my $key (keys %extras) {
906 # follow any links
907 my $data = $cfg->entryVar('extra tags', $key);
908 $extras{$key} = sub { $data };
909 }
910
911 my $item_index = -1;
912 my @options;
913 my $option_index;
914 my %acts;
915 %acts =
916 (
917 %extras,
918 custom_class($cfg)
919 ->order_mail_actions(\%acts, $order, $items, $products,
920 $session->{custom}, $cfg),
921 BSE::Util::Tags->static(\%acts, $cfg),
922 iterate_items_reset => sub { $item_index = -1; },
923 iterate_items =>
924 sub {
925 if (++$item_index < @$items) {
926 $option_index = -1;
927 @options = cart_item_opts($items->[$item_index],
928 $products->[$item_index]);
929 return 1;
930 }
931 return 0;
932 },
933 item=> sub { $items->[$item_index]{$_[0]}; },
934 product =>
935 sub {
936 my $value = $products->[$item_index]{$_[0]};
937 defined($value) or $value = '';
938 $value;
939 },
940 order => sub { $order->{$_[0]} },
941 extended =>
942 sub {
943 $items->[$item_index]{units} * $items->[$item_index]{$_[0]};
944 },
945 _format =>
946 sub {
947 my ($value, $fmt) = @_;
948 if ($fmt =~ /^m(\d+)/) {
949 return sprintf("%$1s", sprintf("%.2f", $value/100));
950 }
951 elsif ($fmt =~ /%/) {
952 return sprintf($fmt, $value);
953 }
954 elsif ($fmt =~ /^\d+$/) {
955 return substr($value . (" " x $fmt), 0, $fmt);
956 }
957 else {
958 return $value;
959 }
960 },
961 iterate_options_reset => sub { $option_index = -1 },
962 iterate_options => sub { ++$option_index < @options },
963 option => sub { escape_html($options[$option_index]{$_[0]}) },
964 ifOptions => sub { @options },
965 options => sub { nice_options(@options) },
966 with_wrap => \&tag_with_wrap,
967 ifSubscribingTo => [ \&tag_ifSubscribingTo, $subscribing_to ],
968 );
969
970 my $mailer = BSE::Mail->new(cfg=>$cfg);
971 # ok, send some email
972 my $confirm = BSE::Template->get_page('mailconfirm', $cfg, \%acts);
973 my $email_order = $cfg->entryBool('shop', 'email_order', $Constants::SHOP_EMAIL_ORDER);
974 if ($email_order) {
975 unless ($noencrypt) {
976 $acts{cardNumber} = $cgi->param('cardNumber');
977 $acts{cardExpiry} = $cgi->param('cardExpiry');
978 }
979 my $ordertext = BSE::Template->get_page('mailorder', $cfg, \%acts);
980
981 my $send_text;
982 if ($noencrypt) {
983 $send_text = $ordertext;
984 }
985 else {
986 eval "use $crypto_class";
987 !$@ or die $@;
988 my $encrypter = $crypto_class->new;
989
990 my $debug = $cfg->entryBool('debug', 'mail_encryption', 0);
991 my $sign = $cfg->entryBool('basic', 'sign', 1);
992
993 # encrypt and sign
994 my %opts =
995 (
996 sign=> $sign,
997 passphrase=> $passphrase,
998 stripwarn=>1,
999 debug=>$debug,
1000 );
1001
1002 $opts{secretkeyid} = $signing_id if $signing_id;
1003 $opts{pgp} = $pgp if $pgp;
1004 $opts{gpg} = $gpg if $gpg;
1005 $opts{pgpe} = $pgpe if $pgpe;
1006 my $recip = "$toName $toEmail";
1007
1008 $send_text = $encrypter->encrypt($recip, $ordertext, %opts )
1009 or die "Cannot encrypt ", $encrypter->error;
1010 }
1011 $mailer->send(to=>$toEmail, from=>$from, subject=>'New Order '.$order->{id},
1012 body=>$send_text)
1013 or print STDERR "Error sending order to admin: ",$mailer->errstr,"\n";
1014 }
1015 $mailer->send(to=>$order->{emailAddress}, from=>$from,
1016 subject=>$subject . " " . localtime,
1017 body=>$confirm)
1018 or print STDERR "Error sending order to customer: ",$mailer->errstr,"\n";
1019}
1020
1021sub tag_with_wrap {
1022 my ($args, $text) = @_;
1023
1024 my $margin = $args =~ /^\d+$/ && $args > 30 ? $args : 70;
1025
1026 require Text::Wrap;
1027 # do it twice to prevent a warning
1028 $Text::Wrap::columns = $margin;
1029 $Text::Wrap::columns = $margin;
1030
1031 return Text::Wrap::fill('', '', split /\n/, $text);
1032}
1033
1034sub _refresh_logon {
1035 my ($class, $req, $msg, $msgid, $r) = @_;
1036
1037 my $securlbase = $req->cfg->entryVar('site', 'secureurl');
1038 my $url = $securlbase."/cgi-bin/user.pl";
1039
1040 $r ||= $securlbase."/cgi-bin/shop.pl?checkout=1";
1041
1042 my %parms;
1043 $parms{r} = $r;
1044 $parms{message} = $msg if $msg;
1045 $parms{mid} = $msgid if $msgid;
1046 $url .= "?" . join("&", map "$_=".escape_uri($parms{$_}), keys %parms);
1047
1048 return BSE::Template->get_refresh($url, $req->cfg);
1049}
1050
1051sub _need_logon {
1052 my ($class, $req, $cart, $cart_prods) = @_;
1053
1054 return need_logon($req->cfg, $cart, $cart_prods, $req->session, $req->cgi);
1055}
1056
1057sub tag_checkedPayment {
1058 my ($payment, $types_by_name, $args) = @_;
1059
1060 my $type = $args;
1061 if ($type !~ /^\d+$/) {
1062 return '' unless exists $types_by_name->{$type};
1063 $type = $types_by_name->{$type};
1064 }
1065
1066 return $payment == $type ? 'checked="checked"' : '';
1067}
1068
1069sub tag_ifPayments {
1070 my ($enabled, $types_by_name, $args) = @_;
1071
1072 my $type = $args;
1073 if ($type !~ /^\d+$/) {
1074 return '' unless exists $types_by_name->{$type};
1075 $type = $types_by_name->{$type};
1076 }
1077
1078 my @found = grep $_ == $type, @$enabled;
1079
1080 return scalar @found;
1081}
1082
1083sub update_quantities {
1084 my ($class, $req) = @_;
1085
1086 my $session = $req->session;
1087 my $cgi = $req->cgi;
1088 my $cfg = $req->cfg;
1089 my @cart = @{$session->{cart} || []};
1090 for my $index (0..$#cart) {
1091 my $new_quantity = $cgi->param("quantity_$index");
1092 if (defined $new_quantity) {
1093 if ($new_quantity =~ /^\s*(\d+)/) {
1094 $cart[$index]{units} = $1;
1095 }
1096 elsif ($new_quantity =~ /^\s*$/) {
1097 $cart[$index]{units} = 0;
1098 }
1099 }
1100 }
1101 @cart = grep { $_->{units} != 0 } @cart;
1102 $session->{cart} = \@cart;
1103 $session->{custom} ||= {};
1104 my %custom_state = %{$session->{custom}};
1105 custom_class($cfg)->recalc($cgi, \@cart, [], \%custom_state, $cfg);
1106 $session->{custom} = \%custom_state;
1107}
1108
1109sub _build_items {
1110 my ($class, $req, $products) = @_;
1111
1112 my $session = $req->session;
1113 $session->{cart}
1114 or return;
1115 my @msgs;
1116 my @cart = @{$req->session->{cart}}
1117 or return;
1118 my @items;
1119 my @prodcols = Product->columns;
1120 my @newcart;
1121 my $today = now_sqldate();
1122 for my $item (@cart) {
1123 my %work = %$item;
1124 my $product = Products->getByPkey($item->{productId});
1125 if ($product) {
1126 (my $comp_release = $product->{release}) =~ s/ .*//;
1127 (my $comp_expire = $product->{expire}) =~ s/ .*//;
1128 $comp_release le $today
1129 or do { push @msgs, "'$product->{title}' has not been released yet";
1130 next; };
1131 $today le $comp_expire
1132 or do { push @msgs, "'$product->{title}' has expired"; next; };
1133 $product->{listed}
1134 or do { push @msgs, "'$product->{title}' not available"; next; };
1135
1136 for my $col (@prodcols) {
1137 $work{$col} = $product->{$col} unless exists $work{$col};
1138 }
1139 $work{extended_retailPrice} = $work{units} * $work{retailPrice};
1140 $work{extended_gst} = $work{units} * $work{gst};
1141 $work{extended_wholesale} = $work{units} * $work{wholesalePrice};
1142
1143 push @newcart, \%work;
1144 push @$products, $product;
1145 }
1146 }
1147
1148 # we don't use these for anything for now
1149 #if (@msgs) {
1150 # @$rmsg = @msgs;
1151 #}
1152
1153 return @newcart;
1154}
1155
1156sub _fillout_order {
1157 my ($class, $req, $values, $items, $rmsg, $how) = @_;
1158
1159 my $session = $req->session;
1160 my $cfg = $req->cfg;
1161 my $cgi = $req->cgi;
1162
1163 my $total = 0;
1164 my $total_gst = 0;
1165 my $total_wholesale = 0;
1166 for my $item (@$items) {
1167 $total += $item->{extended_retailPrice};
1168 $total_gst += $item->{extended_gst};
1169 $total_wholesale += $item->{extended_wholesale};
1170 }
1171 $values->{total} = $total;
1172 $values->{gst} = $total_gst;
1173 $values->{wholesale} = $total_wholesale;
1174 $values->{shipping_cost} = 0;
1175
1176 my $cust_class = custom_class($cfg);
1177
1178 # if it sets shipping cost it must also update the total
1179 eval {
1180 my %custom = %{$session->{custom}};
1181 $cust_class->order_save($cgi, $values, $items, $items,
1182 \%custom, $cfg);
1183 $session->{custom} = \%custom;
1184 };
1185 if ($@) {
1186 $$rmsg = $@;
1187 return;
1188 }
1189
1190 $values->{total} +=
1191 $cust_class->total_extras($items, $items,
1192 $session->{custom}, $cfg, $how);
1193
1194 my $affiliate_code = $session->{affiliate_code};
1195 defined $affiliate_code && length $affiliate_code
1196 or $affiliate_code = $cgi->param('affiliate_code');
1197 defined $affiliate_code or $affiliate_code = '';
1198 $values->{affiliate_code} = $affiliate_code;
1199
1200 my $user = $req->siteuser;
1201 if ($user) {
1202 $values->{userId} = $user->{userId};
1203 $values->{siteuser_id} = $user->{id};
1204 }
1205 else {
1206 $values->{userId} = '';
1207 $values->{siteuser_id} = -1;
1208 }
1209
1210 $values->{orderDate} = now_sqldatetime;
1211
1212 # this should be hard to guess
1213 $values->{randomId} ||= md5_hex(time().rand().{}.$$);
1214
1215 return 1;
1216}
1217
1218sub action_prefix { '' }
1219
718a070d
TC
1220sub req_location {
1221 my ($class, $req) = @_;
1222
1223 require BSE::TB::Locations;
1224 my $cgi = $req->cgi;
1225 my $location_id = $cgi->param('location_id');
1226 my $location;
1227 if (defined $location_id && $location_id =~ /^\d$/) {
1228 $location = BSE::TB::Locations->getByPkey($location_id);
1229 my %acts;
1230 %acts =
1231 (
1232 BSE::Util::Tags->static(\%acts, $req->cfg),
1233 location => [ \&tag_hash, $location ],
1234 );
1235
1236 return $req->response('location', \%acts);
1237 }
1238 else {
1239 return
1240 {
1241 type=>BSE::Template->get_type($req->cfg, 'error'),
1242 content=>"Missing or invalid location_id",
1243 };
1244 }
1245}
1246
41e7c841 12471;