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