add some support for customizing product options
[bse.git] / site / cgi-bin / modules / BSE / Cart.pm
CommitLineData
11af7272
TC
1package BSE::Cart;
2use strict;
3use Scalar::Util;
84e29d01 4use BSE::PubSub;
11af7272 5
84e29d01 6our $VERSION = "1.016";
11af7272
TC
7
8=head1 NAME
9
10BSE::Cart - abstraction for the BSE cart.
11
12=head1 SYNOPSIS
13
14 use BSE::Cart;
8757d2fb 15 my $cart = BSE::Cart->new($req, $stage);
11af7272
TC
16
17 my $items = $cart->items;
18 my $products = $cart->products;
19
20=head1 DESCRIPTION
21
22This class provides a simple abstraction for access to the BSE
23shopping cart.
24
25This is intended for use in templates, but may be expanded further.
26
27=head1 METHODS
28
29=over
30
31=item new()
32
33Create a new cart object based on the session.
34
35=cut
36
37sub new {
8757d2fb 38 my ($class, $req, $stage) = @_;
11af7272 39
2ace323a
TC
40 $stage ||= "";
41
11af7272
TC
42 my $self = bless
43 {
44 products => {},
45 req => $req,
8757d2fb 46 stage => $stage,
676f5398 47 shipping => 0,
11af7272
TC
48 }, $class;
49 Scalar::Util::weaken($self->{req});
50 my $items = $req->session->{cart} || [];
51 my $myself = $self;
52 Scalar::Util::weaken($myself);
53 my $index = 0;
8757d2fb 54 $self->{items} = [ map BSE::Cart::Item->new($_, $index++, $self), @$items ];
11af7272 55
676f5398
TC
56 if ($stage eq 'cart' || $stage eq 'checkout') {
57 $self->_enter_cart;
58 }
2ace323a
TC
59 elsif ($stage eq 'checkupdate') {
60 $self->_checkout_update;
61 }
676f5398 62
023761bd
TC
63 $self->{coupon_code} = $self->{req}->session->{cart_coupon_code};
64 defined $self->{coupon_code} or $self->{coupon_code} = "";
65
11af7272
TC
66 return $self;
67}
68
676f5398
TC
69sub _enter_cart {
70 my ($self) = @_;
71
72 my $req = $self->{req};
73 require BSE::CfgInfo;
74
75 $req->session->{custom} ||= {};
76 my %custom_state = %{$req->session->{custom}};
77
78 $self->{custom_state} = \%custom_state;
79
80 my $cust_class = BSE::CfgInfo::custom_class($self->{req}->cfg);
2ace323a 81 $cust_class->enter_cart(scalar $self->items, scalar $self->products,
676f5398
TC
82 \%custom_state, $req->cfg);
83}
84
2ace323a
TC
85sub _checkout_update {
86 my ($self) = @_;
87
88 my $req = $self->{req};
89 require BSE::CfgInfo;
90
91 $req->session->{custom} ||= {};
92 my %custom_state = %{$req->session->{custom}};
93
94 $self->{custom_state} = \%custom_state;
95
96 my $cust_class = BSE::CfgInfo::custom_class($self->{req}->cfg);
97 $cust_class->checkout_update
98 ($req->cgi, scalar $self->items, scalar $self->products,
99 \%custom_state, $req->cfg);
100}
101
ad3a054c
TC
102=item request
103
104Return the request object.
105
106=cut
107
108sub request {
109 $_[0]{req};
110}
111
2ace323a
TC
112=item is_empty()
113
114Return true if the cart has no items in it.
115
116=cut
117
118sub is_empty {
119 my ($self) = @_;
120
121 return @{$self->{items}} == 0;
122}
123
11af7272
TC
124=item items()
125
126Return an array reference of cart items.
127
128=cut
129
130sub items {
676f5398 131 return wantarray ? @{$_[0]{items}} : $_[0]{items};
11af7272
TC
132}
133
134=item products().
135
136Return an array reference of products in the cart, corresponding to
137the array reference returned by items().
138
139=cut
140
141sub products {
142 my $self = shift;
676f5398
TC
143
144 my @products = map $self->_product($_->{productId}), @{$self->items};
145
146 return wantarray ? @products : \@products;
11af7272
TC
147}
148
149=item total_cost
150
151Return the total cost of the items in the cart.
152
023761bd
TC
153This does not include shipping costs and is not discounted.
154
11af7272
TC
155=cut
156
157sub total_cost {
158 my ($self) = @_;
159
160 my $total_cost = 0;
161 for my $item (@{$self->items}) {
676f5398 162 $total_cost += $item->extended_retailPrice;
11af7272
TC
163 }
164
165 return $total_cost;
166}
167
2ace323a
TC
168=item gst
169
170Return the total GST paid for the items in the cart.
171
172This currently depends on the gst values of the products.
173
174This ignores the coupon code discount.
175
176=cut
177
178sub gst {
179 my ($self) = @_;
180
181 my $total_gst = 0;
182 for my $item (@{$self->items}) {
183 $total_gst += $item->extended_gst;
184 }
185
186 return $total_gst;
187}
188
189=item wholesaleTotal
190
191Return the wholesale cost for the items in the cart.
192
193This depends on the wholesale values of the products.
194
195=cut
196
197sub wholesaleTotal {
198 my ($self) = @_;
199
200 my $total_wholesale = 0;
201 for my $item (@{$self->items}) {
202 $total_wholesale += $item->extended_wholesale;
203 }
204
205 return $total_wholesale;
206}
207
023761bd
TC
208=item discounted_product_cost
209
210Cost of products with an product discount taken into account.
211
2ace323a 212Note: this rounds the total B<down>.
023761bd
TC
213
214=cut
215
216sub discounted_product_cost {
217 my ($self) = @_;
218
b55d4af1 219 return $self->total_cost - $self->product_cost_discount;
023761bd
TC
220}
221
222=item product_cost_discount
223
224Return any amount taken off the product cost.
225
226=cut
227
228sub product_cost_discount {
229 my ($self) = @_;
230
b55d4af1
TC
231 $self->coupon_active
232 or return 0;
233
234 return $self->{coupon_check}{coupon}->discount($self);
023761bd
TC
235}
236
c6369510
TC
237=item cfg_shipping
238
239Return true if the system is configured to prompt for shipper
240information.
241
242=cut
243
244sub cfg_shipping {
245 my $self = shift;
246
247 return $self->{req}->cfg->entry("shop", "shipping", 0);
248}
249
676f5398
TC
250=item set_shipping_cost()
251
252Set the cost of shipping.
253
254Called by the shop.
255
256=cut
257
258sub set_shipping_cost {
259 my ($self, $cost) = @_;
260
261 $self->{shipping} = $cost;
262}
263
264=item shipping_cost()
265
266Fetch the cost of shipping.
267
268=cut
269
270sub shipping_cost {
271 my ($self) = @_;
272
273 return $self->{shipping};
274}
275
c6369510
TC
276=item set_shipping_method
277
278Set the stored shipping method. For internal use.
279
280=cut
281
282sub set_shipping_method {
283 my ($self, $method) = @_;
284
285 $self->{shipping_method} = $method;
286}
287
288=item shipping_method
289
290The description of the selected shipping method.
291
292=cut
293
294sub shipping_method {
295 my ($self) = @_;
296
297 return $self->{shipping_method};
298}
299
300=item set_shipping_name
301
302Set the stored shipping name. For internal use.
303
304=cut
305
306sub set_shipping_name {
307 my ($self, $name) = @_;
308
309 $self->{shipping_name} = $name;
310}
311
312=item shipping_name
313
314The name of the selected shipping method.
315
316=cut
317
318sub shipping_name {
319 my ($self) = @_;
320
321 return $self->{shipping_name};
322}
323
324=item set_delivery_in
325
326Set the stored delivery time in days.
327
328=cut
329
330sub set_delivery_in {
331 my ($self, $days) = @_;
332
333 $self->{delivery_in} = $days;
334}
335
336=item delivery_in
337
338The expected delivery time in days. Some shippers may not supply
339this, in which case this will be an undefined value.
340
341=cut
342
343sub delivery_in {
344 my ($self) = @_;
345
346 return $self->{delivery_in};
347}
348
11af7272
TC
349=item total_units
350
351Return the total number of units in the cart.
352
353=cut
354
355sub total_units {
356 my ($self) = @_;
357
358 my $total_units = 0;
359 for my $item (@{$self->items}) {
2ace323a 360 $total_units += $item->units;
11af7272
TC
361 }
362
363 return $total_units;
364}
365
366=item total
367
2ace323a 368Total of items in the cart, any custom costs and shipping costs.
11af7272
TC
369
370=cut
371
8757d2fb
TC
372sub total {
373 my ($self) = @_;
11af7272 374
023761bd
TC
375 my $cost = 0;
376
377 $cost += $self->discounted_product_cost;
378
379 $cost += $self->shipping_cost;
380
381 $cost += $self->custom_cost;
382
383 return $cost;
384}
385
386=item coupon_code
387
388The current coupon code.
389
390=cut
391
392sub coupon_code {
393 my ($self) = @_;
394
395 return $self->{coupon_code};
396}
397
398=item set_coupon_code()
399
400Used by the shop to set the coupon code.
401
402=cut
403
404sub set_coupon_code {
405 my ($self, $code) = @_;
406
407 $code =~ s/\A\s+//;
408 $code =~ s/\s+\z//;
409 $self->{coupon_code} = $code;
410 delete $self->{coupon_valid};
411 $self->{req}->session->{cart_coupon_code} = $code;
412}
413
414=item coupon_code_discount_pc
415
416The percentage discount for the current coupon code, if that code is
417valid and the contents of the cart are valid for that coupon code.
418
b55d4af1
TC
419This method is historical and no longer useful.
420
023761bd
TC
421=cut
422
423sub coupon_code_discount_pc {
424 my ($self) = @_;
425
426 $self->coupon_valid
427 or return 0;
428
429 return $self->{coupon_check}{coupon}->discount_percent;
430}
431
432=item coupon_valid
433
434Return true if the current coupon code is valid
435
436=cut
437
438sub coupon_valid {
439 my ($self) = @_;
440
441 unless ($self->{coupon_check}) {
442 if (length $self->{coupon_code}) {
443 require BSE::TB::Coupons;
444 my ($coupon) = BSE::TB::Coupons->getBy(code => $self->{coupon_code});
023761bd
TC
445 my %check =
446 (
447 coupon => $coupon,
448 valid => 0,
b55d4af1
TC
449 active => 0,
450 msg => "",
023761bd
TC
451 );
452 #print STDERR " coupon $coupon\n";
453 #print STDERR "released ", 0+ $coupon->is_released, " expired ",
454 # 0+$coupon->is_expired, " valid ", 0+$coupon->is_valid, "\n" if $coupon;
455 if ($coupon && $coupon->is_valid) {
456 $check{valid} = 1;
b55d4af1
TC
457 my ($active, $msg) = $coupon->is_active($self);
458 $check{active} = $active;
459 $check{msg} = $msg || "";
023761bd
TC
460 }
461 $self->{coupon_check} = \%check;
462 }
463 else {
464 $self->{coupon_check} =
465 {
466 valid => 0,
467 active => 0,
b55d4af1 468 msg => "",
023761bd
TC
469 };
470 }
471 }
472
473 return $self->{coupon_check}{valid};
474}
475
476=item coupon_active
477
478Return true if the current coupon is active, ie. both valid and the
479cart has products of all the right tiers.
480
481=cut
482
483sub coupon_active {
484 my ($self) = @_;
485
486 $self->coupon_valid
487 or return 0;
488
489 return $self->{coupon_check}{active};
490}
491
b55d4af1
TC
492=item coupon_inactive_message
493
494Returns why the coupon is inactive.
495
496=cut
497
498sub coupon_inactive_message {
499 my ($self) = @_;
500
501 $self->coupon_valid
502 or return "";
503
504 return $self->{coupon_check}{msg};
505}
506
023761bd
TC
507=item coupon
508
509The current coupon object, if and only if the coupon code is valid.
510
511=cut
512
513sub coupon {
514 my ($self) = @_;
515
516 $self->coupon_valid
517 or return;
518
519 $self->{coupon_check}{coupon};
520}
521
b55d4af1
TC
522=item coupon_cart_wide
523
524Returns true if the coupon discount applies to the cart as a whole.
525
526Always returns false if the coupon is not active.
527
528If this is true the item discount methods are useful.
529
530=cut
531
532sub coupon_cart_wide {
533 my ($self) = @_;
534
535 $self->coupon_active
536 or return;
537
538 return $self->coupon->cart_wide($self);
539}
540
541=item coupon_description
542
543Describe the coupon.
544
545Compatible with order objects.
546
547=cut
548
549sub coupon_description {
550 my ($self) = @_;
551
552 $self->coupon_valid
553 or return;
554
555 return $self->coupon->describe;
556}
557
023761bd
TC
558=item custom_cost
559
560Return any custom cost specified by a custom class.
561
562=cut
563
564sub custom_cost {
565 my ($self) = @_;
566
567 unless (exists $self->{custom_cost}) {
568 my $obj = BSE::CfgInfo::custom_class($self->{req}->cfg);
569 $self->{custom_cost} =
570 $obj->total_extras(scalar $self->items, scalar $self->products,
571 $self->{custom_state}, $self->{req}->cfg, $self->{stage});
572 }
573
574 return $self->{custom_cost};
676f5398
TC
575}
576
577=item have_sales_files
578
579Return true if the cart contains products with files that are for
580sale.
581
582=cut
583
584sub have_sales_files {
585 my ($self) = @_;
586
587 unless (defined $self->{have_sales_files}) {
588 $self->{have_sales_files} = 0;
589 PRODUCTS:
590 for my $prod (@{$self->products}) {
591 if ($prod->has_sales_files) {
592 $self->{have_sales_files} = 1;
593 last PRODUCTS;
594 }
595 }
596 }
597
598 return $self->{have_sales_files};
599}
600
601=item need_logon
602
603Return true if the cart contains items that the user needs to be
604logged on to purchase, or if the current user isn't qualified to
605purchase the item.
606
607Call need_logon_message() to get the reason for this method returning
608false.
609
610=cut
611
612sub need_logon {
613 my ($self) = @_;
614
615 unless (exists $self->{need_logon}) {
616 $self->{need_logon} = $self->_need_logon;
617 }
618
619 $self->{need_logon} or return;
620
621 return 1;
622}
623
023761bd 624=item need_logon_message
676f5398
TC
625
626Returns a list with the error message and message id of the reason the
627user needs to logon for this cart.
628
629=cut
630
631sub need_logon_message {
632 my ($self) = @_;
633
634 unless (exists $self->{need_logon}) {
635 $self->{need_logon} = $self->_need_logon;
636 }
637
638 return @{$self->{logon_reason}};
639}
640
641=item custom_state
642
643State managed by a custom class.
644
645=cut
646
647sub custom_state {
648 my ($self) = @_;
649
650 $self->{custom_state};
651}
652
653=item affiliate_code
654
655Return the stored affiliate code.
656
657=cut
658
659sub affiliate_code {
660 my ($self) = @_;
661
662 my $code = $self->{req}->session->{affiliate_code};
663 defined $code or $code = '';
664
665 return $code;
666}
667
b55d4af1 668=item any_physical_products
676f5398
TC
669
670Returns true if the cart contains any physical products, ie. needs
671shipping.
672
673=cut
674
675sub any_physical_products {
676 my ($self) = @_;
677
678 for my $prod (@{$self->products}) {
679 if ($prod->weight) {
680 return 1;
681 last;
682 }
683 }
684
685 return 0;
686}
687
688
689=item _need_logon
690
691Internal implementation of need_logon.
692
693=cut
694
695sub _need_logon {
696 my ($self) = @_;
697
698 my $cfg = $self->{req}->cfg;
699
700 $self->{logon_reason} = [];
701
702 my $reg_if_files = $cfg->entryBool('shop', 'register_if_files', 1);
703
704 my $user = $self->{req}->siteuser;
705
706 if (!$user && $reg_if_files) {
707 require BSE::TB::ArticleFiles;
708 # scan to see if any of the products have files
709 # requires a subscription or subscribes
710 for my $prod (@{$self->products}) {
711 my @files = $prod->files;
712 if (grep $_->forSale, @files) {
713 $self->{logon_reason} =
714 [ "register before checkout", "shop/fileitems" ];
7606de27 715 return 1;
676f5398
TC
716 }
717 if ($prod->{subscription_id} != -1) {
718 $self->{logon_reason} =
719 [ "you must be logged in to purchase a subscription", "shop/buysub" ];
7606de27 720 return 1;
676f5398
TC
721 }
722 if ($prod->{subscription_required} != -1) {
723 $self->{logon_reason} =
724 [ "must be logged in to purchase a product requiring a subscription", "shop/subrequired" ];
7606de27 725 return 1;
676f5398
TC
726 }
727 }
728 }
729
730 my $require_logon = $cfg->entryBool('shop', 'require_logon', 0);
731 if (!$user && $require_logon) {
732 $self->{logon_reason} =
733 [ "register before checkout", "shop/logonrequired" ];
7606de27 734 return 1;
676f5398
TC
735 }
736
737 # check the user has the right required subs
738 # and that they qualify to subscribe for limited subscription products
739 if ($user) {
740 for my $prod (@{$self->products}) {
741 my $sub = $prod->subscription_required;
742 if ($sub && !$user->subscribed_to($sub)) {
743 $self->{logon_reason} =
744 [ "you must be subscribed to $sub->{title} to purchase one of these products", "shop/subrequired" ];
7606de27 745 return 1;
676f5398
TC
746 }
747
748 $sub = $prod->subscription;
749 if ($sub && $prod->is_renew_sub_only) {
750 unless ($user->subscribed_to_grace($sub)) {
751 $self->{logon_reason} =
752 [ "you must be subscribed to $sub->{title} to use this renew only product", "sub/renewsubonly" ];
753 return;
754 }
755 }
756 if ($sub && $prod->is_start_sub_only) {
757 if ($user->subscribed_to_grace($sub)) {
758 $self->{logon_reason} =
759 [ "you must not be subscribed to $sub->{title} already to use this new subscription only product", "sub/newsubonly" ];
7606de27 760 return 1;
676f5398
TC
761 }
762 }
763 }
764 }
765
7606de27 766 return 0;
8757d2fb 767}
11af7272
TC
768
769sub _product {
770 my ($self, $id) = @_;
771
772 my $product = $self->{products}{$id};
773 unless ($product) {
10dd37f9
AO
774 require BSE::TB::Products;
775 $product = BSE::TB::Products->getByPkey($id)
11af7272
TC
776 or die "No product $id\n";
777 # FIXME
f09e8b4f 778 if ($product->generator ne "BSE::Generate::Product") {
11af7272
TC
779 require BSE::TB::Seminars;
780 $product = BSE::TB::Seminars->getByPkey($id)
781 or die "Not a product, not a seminar $id\n";
782 }
783
784 $self->{products}{$id} = $product;
785 }
786 return $product;
8757d2fb
TC
787}
788
789sub _session {
790 my ($self, $id) = @_;
791 my $session = $self->{sessions}{$id};
792 unless ($session) {
793 require BSE::TB::SeminarSessions;
794 $session = BSE::TB::SeminarSessions->getByPkey($id);
795 $self->{sessions}{$id} = $session;
796 }
11af7272 797
8757d2fb
TC
798 return $session;
799}
800
801=item cleanup()
802
803Clean up the cart, removing any items that are unreleased, expired or
804unlisted.
805
806For BSE use.
807
808=cut
809
810sub cleanup {
811 my ($self) = @_;
812
813 my @newitems;
814 for my $item ($self->items) {
815 my $product = $item->product;
816
817 if ($product->is_released && !$product->is_expired && $product->listed) {
818 push @newitems, $item;
819 }
820 }
821
822 $self->{items} = \@newitems;
823}
824
2ace323a
TC
825=item empty
826
827Empty the cart.
828
b55d4af1
TC
829For BSE use.
830
2ace323a
TC
831=cut
832
833sub empty {
834 my ($self) = @_;
835
836 my $req = $self->{req};
837
838 # empty the cart ready for the next order
839 delete @{$req->session}{qw/order_info order_info_confirmed order_need_delivery cart order_work cart_coupon_code/};
840}
841
8757d2fb
TC
842=back
843
844=cut
845
846package BSE::Cart::Item;
847
848sub new {
849 my ($class, $raw_item, $index, $cart) = @_;
850
851 my $item = bless
852 {
853 %$raw_item,
854 index => $index,
855 cart => $cart,
856 }, $class;
857
858 Scalar::Util::weaken($item->{cart});
859
860 return $item;
861}
862
863=head2 Item Members
864
865=over
866
867=item product
868
869Returns the product for that line item.
870
871=cut
872
873sub product {
874 my $self = shift;
875
876 return $self->{cart}->_product($self->{productId});
877}
878
b55d4af1
TC
879=item product_id
880
881Id of the product in this row.
882
883=cut
884
885sub product_id {
886 $_[0]{productId};
887}
888
8757d2fb
TC
889=item price
890
891=cut
892
893sub price {
894 my ($self) = @_;
895
896 unless (defined $self->{calc_price}) {
88a03daa 897 $self->{calc_price} = $self->product->price(user => $self->{cart}{req}->siteuser);
84e29d01
TC
898
899 BSE::PubSub->customize(
900 cart_price => {
901 cartitem => $self,
902 cart => $self->{cart},
903 price => \($self->{calc_price}),
904 request => $self->{cart}{req},
905 });
8757d2fb
TC
906 }
907
908 return $self->{calc_price};
11af7272
TC
909}
910
911=item extended
912
913The extended price for the item.
914
8757d2fb
TC
915=cut
916
917sub extended {
918 my ($self, $base) = @_;
919
920 $base =~ /^(price|retailPrice|gst|wholesalePrice)$/
921 or return 0;
922
676f5398 923 return $self->$base() * $self->{units};
8757d2fb
TC
924}
925
926sub extended_retailPrice {
927 $_[0]->extended("price");
928}
929
930sub extended_wholesalePrice {
931 $_[0]->extended("wholesalePrice");
932}
933
934sub extended_gst {
935 $_[0]->extended("gst");
936}
937
938=item units
939
940The number of units.
941
942=cut
943
944sub units {
945 $_[0]{units};
946}
947
948=item session_id
949
950The seminar session id, if any.
951
952=cut
953
954sub session_id {
955 $_[0]{session_id};
956}
957
958=item tier_id
959
960The pricing tier id.
961
962=cut
963
964sub tier_id {
965 $_[0]{tier};
966}
967
11af7272
TC
968=item link
969
970A link to the product.
971
972=cut
973
8757d2fb 974sub link {
11af7272
TC
975 my ($self, $id) = @_;
976
8757d2fb 977 my $product = $self->product;
11af7272
TC
978 my $link = $product->link;
979 unless ($link =~ /^\w+:/) {
980 $link = BSE::Cfg->single->entryErr("site", "url") . $link;
981 }
982
983 return $link;
984}
985
986=item option_list
987
988Return a list of options for the item, each with:
989
990=over
991
992=item *
993
994id, name - the identifier for the option
995
996=item *
997
998value - the value of the option.
999
1000=item *
1001
1002desc - the description of the option
1003
1004=item *
1005
1006display - display of the option value
1007
1008=back
1009
1010=cut
1011
8757d2fb 1012sub option_list {
90d46483 1013 my ($self) = @_;
11af7272 1014
9a3d402d
TC
1015 my @options = $self->product->option_descs(BSE::Cfg->single, $self->{options});
1016
1017 return wantarray ? @options : \@options;
11af7272
TC
1018}
1019
1020=item option_text
1021
1022Display text for options for the item.
1023
1024=cut
1025
8757d2fb 1026sub option_text {
11af7272
TC
1027 my ($self, $index) = @_;
1028
8757d2fb 1029 my $options = $self->option_list;
11af7272
TC
1030 return join(", ", map "$_->{desc}: $_->{display}", @$options);
1031}
1032
023761bd
TC
1033=item coupon_applies
1034
b55d4af1
TC
1035Returns true for a cart-wide coupon if this item allows the coupon to
1036apply.
023761bd
TC
1037
1038=cut
1039
1040sub coupon_applies {
1041 my ($self) = @_;
1042
b55d4af1 1043 $self->{cart}->coupon_active
023761bd
TC
1044 or return 0;
1045
b55d4af1 1046 return $self->{cart}{coupon_check}{coupon}->product_valid($self->{cart}, $self->{index});
023761bd
TC
1047}
1048
b55d4af1
TC
1049=item product_discount
1050
1051Returns the number of cents of discount this product receives per unit
1052
1053=cut
1054
1055sub product_discount {
1056 my ($self) = @_;
1057
1058 $self->{cart}->coupon_active
1059 or return 0;
1060
1061 return $self->{cart}{coupon_check}{coupon}->product_discount($self->{cart}, $self->{index});
1062}
1063
1064=item product_discount_units
1065
1066Returns the number of units in the current row that the product
1067discount applies to.
1068
1069=cut
1070
1071sub product_discount_units {
1072 my ($self) = @_;
1073
1074 $self->{cart}->coupon_active
1075 or return 0;
1076
1077 return $self->{cart}{coupon_check}{coupon}->product_discount_units($self->{cart}, $self->{index});
1078}
1079
1080
11af7272
TC
1081=item session
1082
1083The session object of the seminar session
1084
1085=cut
1086
8757d2fb
TC
1087sub session {
1088 my ($self) = @_;
11af7272 1089
8757d2fb
TC
1090 $self->{session_id} or return;
1091 return $self->{cart}->_session($self->{session_id});
1092}
1093
1094
1095my %product_keys;
1096
1097sub AUTOLOAD {
1098 our $AUTOLOAD;
1099 (my $name = $AUTOLOAD) =~ s/^.*:://;
1100 unless (%product_keys) {
10dd37f9
AO
1101 require BSE::TB::Products;
1102 %product_keys = map { $_ => 1 } BSE::TB::Product->columns;
11af7272
TC
1103 }
1104
8757d2fb
TC
1105 if ($product_keys{$name}) {
1106 return $_[0]->product->$name();
1107 }
1108 else {
1109 return "* unknown method $name *";
1110 }
11af7272
TC
1111}
1112
676f5398
TC
1113=item description
1114
1115=item title
1116
1117=cut
1118
1119sub description {
1120 my ($self) = @_;
1121
1122 $self->product->description;
1123}
1124
1125sub title {
1126 my ($self) = @_;
1127
1128 $self->product->title;
1129}
1130
11af7272
TC
11311;
1132
1133=back
1134
1135=head1 AUTHOR
1136
1137Tony Cook <tony@develop-help.com>
1138
1139=cut