re-work coupons to allow multiple coupon types
[bse.git] / site / cgi-bin / modules / BSE / TB / Order.pm
CommitLineData
0ec4ac8a
TC
1package BSE::TB::Order;
2use strict;
3# represents an order from the database
4use Squirrel::Row;
5use vars qw/@ISA/;
6@ISA = qw/Squirrel::Row/;
5d88571c 7use Carp 'confess';
b62cae00 8use BSE::Shop::PaymentTypes;
0ec4ac8a 9
b55d4af1 10our $VERSION = "1.029";
cb7fd78d 11
0ec4ac8a
TC
12sub columns {
13 return qw/id
14 delivFirstName delivLastName delivStreet delivSuburb delivState
15 delivPostCode delivCountry
16 billFirstName billLastName billStreet billSuburb billState
17 billPostCode billCountry
18 telephone facsimile emailAddress
19 total wholesaleTotal gst orderDate
20 ccNumberHash ccName ccExpiryHash ccType
21 filled whenFilled whoFilled paidFor paymentReceipt
22 randomId cancelled userId paymentType
23 customInt1 customInt2 customInt3 customInt4 customInt5
24 customStr1 customStr2 customStr3 customStr4 customStr5
25 instructions billTelephone billFacsimile billEmail
e3d242f7 26 siteuser_id affiliate_code shipping_cost
41e7c841
TC
27 delivMobile billMobile
28 ccOnline ccSuccess ccReceipt ccStatus ccStatusText
37dd20ad 29 ccStatus2 ccTranId complete delivOrganization billOrganization
d9803c26 30 delivStreet2 billStreet2 purchase_order shipping_method
13a986ee 31 shipping_name shipping_trace
b62cae00 32 paypal_token paypal_tran_id freight_tracking stage ccPAN
b55d4af1
TC
33 paid_manually coupon_id coupon_code_discount_pc delivery_in
34 product_cost_discount coupon_cart_wide coupon_description/;
f0722dd2
TC
35}
36
37sub table {
38 return "orders";
0ec4ac8a
TC
39}
40
14604ada
TC
41sub defaults {
42 require BSE::Util::SQL;
43 require Digest::MD5;
44 return
45 (
f0722dd2
TC
46 billFirstName => "",
47 billLastName => "",
48 billStreet => "",
49 billSuburb => "",
50 billState => "",
51 billPostCode => "",
52 billCountry => "",
14604ada
TC
53 total => 0,
54 wholesaleTotal => 0,
55 gst => 0,
56 orderDate => BSE::Util::SQL::now_datetime(),
57 filled => 0,
58 whenFilled => undef,
59 whoFilled => '',
60 paidFor => 0,
61 paymentReceipt => '',
62 randomId => Digest::MD5::md5_hex(time().rand().{}.$$),
63 ccNumberHash => '',
64 ccName => '',
65 ccExpiryHash => '',
66 ccType => '',
67 randomId => '',
68 cancelled => 0,
69 userId => '',
70 paymentType => 0,
71 customInt1 => undef,
72 customInt2 => undef,
73 customInt3 => undef,
74 customInt4 => undef,
75 customInt5 => undef,
76 customStr1 => undef,
77 customStr2 => undef,
78 customStr3 => undef,
79 customStr4 => undef,
80 customStr5 => undef,
81 instructions => '',
82 siteuser_id => undef,
83 affiliate_code => '',
84 shipping_cost => 0,
85 ccOnline => 0,
86 ccSuccess => 0,
87 ccReceipt => '',
88 ccStatus => 0,
89 ccStatusText => '',
90 ccStatus2 => '',
91 ccTranId => '',
92 complete => 0,
93 purchase_order => '',
94 shipping_method => '',
95 shipping_name => '',
96 shipping_trace => undef,
f0722dd2
TC
97 paypal_token => "",
98 paypal_tran_id => "",
080fc207 99 freight_tracking => "",
f0722dd2 100 stage => "incomplete",
6abd8ce8 101 ccPAN => "",
b62cae00 102 paid_manually => 0,
c6369510 103 delivery_in => undef,
14604ada
TC
104 );
105}
106
107sub address_columns {
108 return qw/
109 delivFirstName delivLastName delivStreet delivSuburb delivState
110 delivPostCode delivCountry
111 billFirstName billLastName billStreet billSuburb billState
112 billPostCode billCountry
113 telephone facsimile emailAddress
114 instructions billTelephone billFacsimile billEmail
115 delivMobile billMobile
116 delivOrganization billOrganization
117 delivStreet2 billStreet2/;
118}
119
120sub user_columns {
121 return qw/userId siteuser_id/;
122}
123
124sub payment_columns {
125 return qw/ccNumberHash ccName ccExpiryHash ccType
126 paidFor paymentReceipt paymentType
127 ccOnline ccSuccess ccReceipt ccStatus ccStatusText
b62cae00 128 ccStatus2 ccTranId ccPAN paid_manually/;
14604ada
TC
129}
130
c4f18087
TC
131=item billing_to_delivery_map
132
133Return a hashref where the key is a billing field and the value is the
134corresponding delivery field.
135
136=cut
137
138{
139 my %billing_to_delivery =
140 (
141 billEmail => "emailAddress",
142 billFirstName => "delivFirstName",
143 billLastName => "delivLastName",
144 billStreet => "delivStreet",
145 billStreet2 => "delivStreet2",
146 billSuburb => "delivSuburb",
147 billState => "delivState",
148 billPostCode => "delivPostCode",
149 billCountry => "delivCountry",
150 billTelephone => "telephone",
a964e89d 151 billMobile => "delivMobile",
c4f18087
TC
152 billFacsimile => "facsimile",
153 billOrganization => "delivOrganization",
154 );
155
156 sub billing_to_delivery_map {
157 return \%billing_to_delivery;
158 }
159}
160
0ec4ac8a
TC
161=item siteuser
162
163returns the SiteUser object of the user who made this order.
164
165=cut
166
167sub siteuser {
168 my ($self) = @_;
169
f0722dd2 170 if ($self->siteuser_id) {
b7cadc84
AO
171 require BSE::TB::SiteUsers;
172 my $user = BSE::TB::SiteUsers->getByPkey($self->siteuser_id);
f0722dd2
TC
173 $user and return $user;
174 }
175
0ec4ac8a
TC
176 $self->{userId} or return;
177
b7cadc84 178 require BSE::TB::SiteUsers;
0ec4ac8a 179
b7cadc84 180 return ( BSE::TB::SiteUsers->getBy(userId=>$self->{userId}) )[0];
0ec4ac8a
TC
181}
182
183sub items {
184 my ($self) = @_;
185
186 require BSE::TB::OrderItems;
187 return BSE::TB::OrderItems->getBy(orderId => $self->{id});
188}
189
ab2cd916
TC
190sub files {
191 my ($self) = @_;
192
7c6f563b
TC
193 require BSE::TB::ArticleFiles;
194 return BSE::TB::ArticleFiles->getSpecial(orderFiles=>$self->{id});
ab2cd916
TC
195}
196
eb9d306d
TC
197sub paid_files {
198 my ($self) = @_;
199
200 $self->paidFor
201 or return;
202
203 require BSE::TB::ArticleFiles;
204 return BSE::TB::ArticleFiles->getSpecial(orderPaidFor => $self->id);
205}
206
ab2cd916
TC
207sub products {
208 my ($self) = @_;
209
10dd37f9
AO
210 require BSE::TB::Products;
211 BSE::TB::Products->getSpecial(orderProducts=>$self->{id});
ab2cd916
TC
212}
213
41e7c841
TC
214sub valid_fields {
215 my ($class, $cfg) = @_;
216
217 my %fields =
218 (
b27af108 219 delivFirstName => { description=>'Delivery First Name',
37dd20ad 220 rules=>'dh_one_line' },
b27af108 221 delivLastName => { description => 'Delivery Last Name',
37dd20ad 222 rules=>'dh_one_line' },
b27af108 223 delivOrganization => { description => 'Delivery Organization',
37dd20ad 224 rules=>'dh_one_line' },
b27af108 225 delivStreet => { description => 'Delivery Street',
37dd20ad 226 rules=>'dh_one_line' },
b27af108 227 delivStreet2 => { description => 'Delivery Street 2',
37dd20ad 228 rules=>'dh_one_line' },
b27af108 229 delivState => { description => 'Delivery State',
37dd20ad 230 rules=>'dh_one_line' },
b27af108 231 delivSuburb => { description => 'Delivery Suburb',
37dd20ad 232 rules=>'dh_one_line' },
b27af108 233 delivPostCode => { description => 'Delivery Post Code',
9074efa2 234 rules=>'dh_one_line;dh_int_postcode' },
b27af108 235 delivCountry => { description => 'Delivery Country',
37dd20ad 236 rules=>'dh_one_line' },
b27af108 237 billFirstName => { description => 'Billing First Name',
37dd20ad 238 rules=>'dh_one_line' },
b27af108 239 billLastName => { description => 'Billing Last Name',
37dd20ad 240 rules=>'dh_one_line' },
b27af108 241 billOrganization => { description => 'Billing Organization',
37dd20ad 242 rules=>'dh_one_line' },
b27af108 243 billStreet => { description => 'Billing Street',
37dd20ad 244 rules=>'dh_one_line' },
b27af108 245 billStreet2 => { description => 'Billing Street 2',
37dd20ad 246 rules=>'dh_one_line' },
b27af108 247 billSuburb => { description => 'Billing Suburb',
37dd20ad 248 rules=>'dh_one_line' },
b27af108 249 billState => { description => 'Billing State',
37dd20ad 250 rules=>'dh_one_line' },
b27af108 251 billPostCode => { description => 'Billing Post Code',
9074efa2 252 rules=>'dh_one_line;dh_int_postcode' },
b27af108 253 billCountry => { description => 'Billing First Name',
37dd20ad 254 rules=>'dh_one_line' },
41e7c841
TC
255 telephone => { description => 'Telephone Number',
256 rules => "phone" },
257 facsimile => { description => 'Facsimile Number',
258 rules => 'phone' },
259 emailAddress => { description => 'Email Address',
c4f18087 260 rules=>'email' },
41e7c841 261 instructions => { description => 'Instructions' },
b27af108 262 billTelephone => { description => 'Billing Telephone Number',
41e7c841
TC
263 rules=>'phone' },
264 billFacsimile => { description => 'Billing Facsimile Number',
265 rules=>'phone' },
266 billEmail => { description => 'Billing Email Address',
c4f18087 267 rules => 'email;required' },
41e7c841
TC
268 delivMobile => { description => 'Delivery Mobile Number',
269 rules => 'phone' },
270 billMobile => { description => 'Billing Mobile Number',
271 rules=>'phone' },
272 instructions => { description => 'Instructions' },
74b21f6d 273 purchase_order => { description => 'Purchase Order No' },
d8674b8b
AMS
274 shipping_cost => { description => 'Shipping charges' },
275 shipping_method => { description => 'Shipping method' },
41e7c841
TC
276 );
277
278 for my $field (keys %fields) {
279 my $display = $cfg->entry('shop', "display_$field");
280 $display and $fields{$field}{description} = $display;
281 }
282
283 return %fields;
284}
285
286sub valid_rules {
287 my ($class, $cfg) = @_;
288
289 return;
290}
291
292sub valid_payment_fields {
293 my ($class, $cfg) = @_;
294
295 my %fields =
296 (
b27af108
TC
297 cardNumber =>
298 {
41e7c841
TC
299 description => "Credit Card Number",
300 rules=>"creditcardnumber",
301 },
b27af108 302 cardExpiry =>
41e7c841
TC
303 {
304 description => "Credit Card Expiry Date",
305 rules => 'creditcardexpirysingle',
306 },
6abd8ce8 307 ccName => { description => "Credit Card Holder" },
1546e1f0 308 ccType => { description => "Credit Card Type" },
b27af108
TC
309 cardVerify =>
310 {
41e7c841
TC
311 description => 'Card Verification Value',
312 rules => 'creditcardcvv',
313 },
314 );
315
316 for my $field (keys %fields) {
317 my $display = $cfg->entry('shop', "display_$field");
318 $display and $fields{$field}{description} = $display;
319 }
320
321 return %fields;
322}
323
324sub valid_payment_rules {
325 return;
326}
327
5d88571c
TC
328sub clear_items {
329 my ($self) = @_;
330
331 confess "Attempt to clear items on completed order $self->{id}"
332 if $self->{complete};
b27af108 333
5d88571c
TC
334 BSE::DB->run(deleteOrdersItems => $self->{id});
335}
336
14604ada
TC
337sub add_item {
338 my ($self, %opts) = @_;
339
340 my $prod = delete $opts{product}
341 or confess "Missing product option";
342 my $units = delete $opts{units} || 1;
343
344 my $options = '';
345 my @dboptions;
346 if ($opts{options}) {
347 if (ref $opts{options}) {
348 @dboptions = @{delete $opts{options}};
349 }
350 else {
351 $options = delete $opts{options};
352 }
353 }
b27af108 354
14604ada
TC
355 require BSE::TB::OrderItems;
356 my %item =
357 (
358 productId => $prod->id,
359 orderId => $self->id,
360 units => $units,
361 price => $prod->retailPrice,
362 options => $options,
363 max_lapsed => 0,
364 session_id => 0,
365 ( map { $_ => $prod->{$_} }
366 qw/wholesalePrice gst customInt1 customInt2 customInt3 customStr1 customStr2 customStr3 title description subscription_id subscription_period product_code/
367 ),
368 );
369
370 $self->set_total($self->total + $prod->retailPrice * $units);
371
372 return BSE::TB::OrderItems->make(%item);
373}
374
13a986ee
TC
375sub deliv_country_code {
376 my ($self) = @_;
377
378 my $use_codes = BSE::Cfg->single->entry("shop", "country_code", 0);
379 if ($use_codes) {
380 return $self->delivCountry;
381 }
382 else {
383 require BSE::Countries;
384 return BSE::Countries::bse_country_code($self->delivCountry);
385 }
386}
387
f0722dd2
TC
388=item stage
389
390Return the order stage.
391
392If the stage is empty, guess from the order flags.
393
394=cut
395
396sub stage {
397 my ($self) = @_;
398
399 if ($self->{stage} ne "") {
400 return $self->{stage};
401 }
402
403 if (!$self->complete) {
404 return "incomplete";
405 }
406 elsif ($self->filled) {
407 return "shipped";
408 }
409 else {
410 return "unprocessed";
411 }
412}
413
414sub stage_description {
415 my ($self, $lang) = @_;
416
417 return BSE::TB::Orders->stage_label($self->stage, $lang);
418}
419
f55be9df
TC
420sub stage_description_id {
421 my ($self) = @_;
422
423 return BSE::TB::Orders->stage_label_id($self->stage);
424}
425
c4f18087 426=item delivery_mail_recipient
f0722dd2 427
c4f18087
TC
428Return a value suitable for BSE::ComposeMail's to parameter for the
429shipping email address.
f0722dd2
TC
430
431=cut
432
c4f18087 433sub delivery_mail_recipient {
f0722dd2
TC
434 my ($self) = @_;
435
436 my $user = $self->siteuser;
c4f18087 437 my $email = $self->emailAddress || $self->billEmail;
f0722dd2 438
c4f18087 439 if ($user && $user->email eq $email) {
f0722dd2
TC
440 return $user;
441 }
442
c4f18087 443 return $email;
f0722dd2
TC
444}
445
768dccf0 446=item _tags
8d8895b4 447
768dccf0 448Internal method with the common code between tags() and mail_tags().
8d8895b4
TC
449
450=cut
451
768dccf0
TC
452sub _tags {
453 my ($self, $escape) = @_;
8d8895b4
TC
454
455 require BSE::Util::Tags;
8d8895b4 456 require BSE::TB::OrderItems;
768dccf0
TC
457 require BSE::Util::Iterate;
458 my $it;
459 my $art;
460 my $esc;
461 my $obj;
462 if ($escape) {
463 require BSE::Util::HTML;
464 $it = BSE::Util::Iterate::Objects->new;
465 $art = \&BSE::Util::Tags::tag_article;
466 $obj = \&BSE::Util::Tags::tag_object;
467 $esc = \&BSE::Util::HTML::escape_html;
468 }
469 else {
470 $it = BSE::Util::Iterate::Objects::Text->new;
471 $art = \&BSE::Util::Tags::tag_article_plain;
472 $obj = \&BSE::Util::Tags::tag_object_plain;
473 $esc = sub { return $_[0] };
474 }
475
476 my $cfg = BSE::Cfg->single;
477 my $must_be_paid = $cfg->entryBool('downloads', 'must_be_paid', 0);
478 my $must_be_filled = $cfg->entryBool('downloads', 'must_be_filled', 0);
479
8d8895b4
TC
480 my %item_cols = map { $_ => 1 } BSE::TB::OrderItem->columns;
481 my %products;
482 my $current_item;
768dccf0 483 my $current_file;
8d8895b4
TC
484 return
485 (
768dccf0 486 order => [ $obj, $self ],
8d8895b4
TC
487 $it->make
488 (
489 single => "item",
490 plural => "items",
491 code => [ items => $self ],
492 store => \$current_item,
493 ),
494 extended => sub {
495 my ($args) = @_;
496
497 $current_item
498 or return '* only usable in items iterator *';
499
500 $item_cols{$args}
501 or return "* unknown item column $args *";
502
503 return $current_item->$args() * $current_item->units;
504 },
505 $it->make
506 (
507 single => "option",
508 plural => "options",
509 code => sub {
510 $current_item
511 or return;
512 return $current_item->option_hashes
513 },
514 nocache => 1,
515 ),
516 options => sub {
517 $current_item
518 or return '* only in the items iterator *';
768dccf0 519 return $esc->($current_item->nice_options);
8d8895b4
TC
520 },
521 product => sub {
522 $current_item
523 or return '* only usable in items *';
524
10dd37f9 525 require BSE::TB::Products;
8d8895b4 526 my $id = $current_item->productId;
10dd37f9 527 $products{$id} ||= BSE::TB::Products->getByPkey($id);
8d8895b4
TC
528
529 my $product = $products{$id}
530 or return '';
531
768dccf0
TC
532 return $art->($product, $cfg, $_[0]);
533 },
7c6f563b 534 $it->make
768dccf0
TC
535 (
536 single => 'orderfile',
537 plural => 'orderfiles',
538 code => [ files => $self ],
539 store => \$current_file,
540 ),
7c6f563b 541 $it->make
768dccf0
TC
542 (
543 single => "prodfile",
544 plural => "prodfiles",
545 code => sub {
546 $current_item
547 or return '* only usable in items *';
548
10dd37f9 549 require BSE::TB::Products;
768dccf0 550 my $id = $current_item->productId;
10dd37f9 551 $products{$id} ||= BSE::TB::Products->getByPkey($id);
768dccf0
TC
552
553 my $product = $products{$id}
554 or return '';
555
556 return $product->files;
557 },
558 store => \$current_file,
559 ),
560 ifFileAvail => sub {
561 $current_file or return 0;
562 $current_file->{forSale} or return 1;
563
564 return 0 if $must_be_paid && !$self->{paidFor};
565 return 0 if $must_be_filled && !$self->{filled};
566
567 return 1;
8d8895b4
TC
568 },
569 );
570}
571
25a17f89
TC
572sub cfg_must_be_paid {
573 BSE::Cfg->single->entryBool("download", "must_be_paid", 0);
574}
575
576sub cfg_must_be_filled {
577 BSE::Cfg->single->entryBool("download", "must_be_filled", 0);
578}
579
580=item file_available
581
582Given an order file, return true if available for download.
583
584This will return nonsensical results for files not associated with the
585order.
586
587=cut
588
589sub file_available {
590 my ($self, $file) = @_;
591
592 $file->forSale or return 1;
593
594 return 0 if $self->cfg_must_be_paid && !$self->paidFor;
595 return 0 if $self->cfg_must_be_filled && !$self->filled;
596
597 return 1;
598}
599
768dccf0
TC
600=item mail_tags
601
602=cut
603
604sub mail_tags {
605 my ($self) = @_;
606
607 return $self->_tags(0);
608}
609
610=item tags
611
612Return template tags suitable for an order (non-mail)
613
614=cut
615
616sub tags {
617 my ($self) = @_;
618
619 return $self->_tags(1);
620}
621
f0722dd2
TC
622sub send_shipped_email {
623 my ($self) = @_;
624
c4f18087 625 my $to = $self->delivery_mail_recipient;
f0722dd2
TC
626 require BSE::ComposeMail;
627 my $mailer = BSE::ComposeMail->new(cfg => BSE::Cfg->single);
628 require BSE::Util::Tags;
f0722dd2
TC
629 my %acts =
630 (
631 BSE::Util::Tags->mail_tags(),
8d8895b4 632 $self->mail_tags,
f0722dd2 633 );
c4f18087 634 my %opts =
f0722dd2
TC
635 (
636 to => $to,
637 subject => "Your order has shipped",
638 template => "email/ordershipped",
639 acts => \%acts,
68d44fe0 640 log_msg => "Notify customer that Order No. " . $self->id . " has shipped",
f0722dd2
TC
641 log_object => $self,
642 log_component => "shopadmin:orders:saveorder",
44af429d 643 vars => { order => $self },
f0722dd2 644 );
c4f18087
TC
645 if ($self->emailAddress && $self->billEmail
646 && lc $self->emailAddress ne $self->billEmail) {
647 $opts{cc} = $self->billEmail;
648 }
649
650 $mailer->send(%opts);
f0722dd2
TC
651}
652
653sub new_stage {
654 my ($self, $who, $stage, $stage_note) = @_;
655
656 unless ($stage ne $self->stage
657 || defined $stage_note && $stage_note =~ /\S/) {
658 return;
659 }
660
661 my $old_stage = $self->stage;
68d44fe0 662 my $msg = "Set Order No. ". $self->id . " stage to '$stage'";
f0722dd2
TC
663 if (defined $stage_note && $stage_note =~ /\S/) {
664 $msg .= ": $stage_note";
665 }
666 require BSE::TB::AuditLog;
667 BSE::TB::AuditLog->log
668 (
669 component => "shopadmin:orders:saveorder",
670 object => $self,
671 msg => $msg,
d9c45dcc 672 level => "notice",
f0722dd2
TC
673 actor => $who || "U"
674 );
675
676 if ($stage ne $old_stage) {
677 $self->set_stage($stage);
678 if ($stage eq "shipped") {
4b9daeca
TC
679 if (!$self->filled) {
680 require BSE::Util::SQL;
681
682 $self->set_whoFilled($who ? $who->logon : "-unknown-");
683 $self->set_whenFilled(BSE::Util::SQL::now_datetime());
684 }
f0722dd2
TC
685 $self->send_shipped_email();
686 $self->set_filled(1);
687 }
688 else {
689 $self->set_filled(0);
690 }
691 }
692}
693
6abd8ce8
TC
694sub set_ccPANTruncate {
695 my ($self, $pan) = @_;
696
697 if (length $pan > 4) {
698 $pan = substr($pan, -4);
699 }
700
701 $self->set_ccPAN($pan);
702}
703
b62cae00
TC
704=item is_manually_paid
705
706Returns true if the order is marked as manually paid, either through
707the older PAYMENT_MANUAL paymentType value or via the newer flag.
708
709=cut
710
711sub is_manually_paid {
712 my ($self) = @_;
713
714 return $self->paidFor &&
715 ($self->paid_manually || $self->paymentType == PAYMENT_MANUAL);
716}
717
91a02f51
TC
718=item coupon_valid
719
720For compatibility with cart objects, returns true if the currently
721stored coupon is valid.
722
723Since only an active coupon is stored, if we have a coupon code, then
724it's valid.
725
726=cut
727
728sub coupon_valid {
729 my ($self) = @_;
730
2ced88e0 731 return defined($self->coupon_id);
91a02f51
TC
732}
733
734=item coupon_active
735
736For compatibility with cart objects, returns true if the currently
737stored coupon is active.
738
739Since only an active coupon is stored, if we have a coupon code, then
740it's valid.
741
742=cut
743
744*coupon_active = \&coupon_valid;
745
746=item total_cost
747
748Return the total cost of products without the coupon discount applied.
749
750=cut
751
752sub total_cost {
753 my ($self) = @_;
754
755 my $total = 0;
756 for my $item ($self->items) {
757 $total += $item->extended("price");
758 }
759
760 return $total;
761}
762
763=item discounted_product_cost
764
765Return the total cost of products less the discount from the coupon
766code.
767
768=cut
769
770sub discounted_product_cost {
771 my ($self) = @_;
772
773 my $cost = $self->total_cost;
774
b55d4af1
TC
775 if ($self->product_cost_discount) {
776 return $cost - $self->product_cost_discount;
777 }
91a02f51 778
b55d4af1 779 $cost -= int($cost * $self->coupon_code_discount_pc / 100);
91a02f51 780
b55d4af1 781 return $cost;
91a02f51
TC
782}
783
2ced88e0
TC
784=item coupon
785
786Return the coupon used for this order, if any.
787
788=cut
789
790sub coupon {
791 my ($self) = @_;
792
793 $self->coupon_id
794 or return;
795
796 require BSE::TB::Coupons;
797 return BSE::TB::Coupons->getByPkey($self->coupon_id);
798}
799
9a3d402d
TC
800=item coupon_code
801
802Emulate the cart's coupon-code method.
803
804=cut
805
806sub coupon_code {
807 my ($self) = @_;
808
809 my $coupon = $self->coupon
810 or return;
811
812 return $coupon->code;
813}
814
0ec4ac8a 8151;