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