we no longer accept site user field names for required_fields or checkout
[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';
0ec4ac8a 8
1546e1f0 9our $VERSION = "1.012";
cb7fd78d 10
0ec4ac8a
TC
11sub columns {
12 return qw/id
13 delivFirstName delivLastName delivStreet delivSuburb delivState
14 delivPostCode delivCountry
15 billFirstName billLastName billStreet billSuburb billState
16 billPostCode billCountry
17 telephone facsimile emailAddress
18 total wholesaleTotal gst orderDate
19 ccNumberHash ccName ccExpiryHash ccType
20 filled whenFilled whoFilled paidFor paymentReceipt
21 randomId cancelled userId paymentType
22 customInt1 customInt2 customInt3 customInt4 customInt5
23 customStr1 customStr2 customStr3 customStr4 customStr5
24 instructions billTelephone billFacsimile billEmail
e3d242f7 25 siteuser_id affiliate_code shipping_cost
41e7c841
TC
26 delivMobile billMobile
27 ccOnline ccSuccess ccReceipt ccStatus ccStatusText
37dd20ad 28 ccStatus2 ccTranId complete delivOrganization billOrganization
d9803c26 29 delivStreet2 billStreet2 purchase_order shipping_method
13a986ee 30 shipping_name shipping_trace
f0722dd2
TC
31 paypal_token paypal_tran_id freight_tracking stage/;
32}
33
34sub table {
35 return "orders";
0ec4ac8a
TC
36}
37
14604ada
TC
38sub defaults {
39 require BSE::Util::SQL;
40 require Digest::MD5;
41 return
42 (
f0722dd2
TC
43 billFirstName => "",
44 billLastName => "",
45 billStreet => "",
46 billSuburb => "",
47 billState => "",
48 billPostCode => "",
49 billCountry => "",
14604ada
TC
50 total => 0,
51 wholesaleTotal => 0,
52 gst => 0,
53 orderDate => BSE::Util::SQL::now_datetime(),
54 filled => 0,
55 whenFilled => undef,
56 whoFilled => '',
57 paidFor => 0,
58 paymentReceipt => '',
59 randomId => Digest::MD5::md5_hex(time().rand().{}.$$),
60 ccNumberHash => '',
61 ccName => '',
62 ccExpiryHash => '',
63 ccType => '',
64 randomId => '',
65 cancelled => 0,
66 userId => '',
67 paymentType => 0,
68 customInt1 => undef,
69 customInt2 => undef,
70 customInt3 => undef,
71 customInt4 => undef,
72 customInt5 => undef,
73 customStr1 => undef,
74 customStr2 => undef,
75 customStr3 => undef,
76 customStr4 => undef,
77 customStr5 => undef,
78 instructions => '',
79 siteuser_id => undef,
80 affiliate_code => '',
81 shipping_cost => 0,
82 ccOnline => 0,
83 ccSuccess => 0,
84 ccReceipt => '',
85 ccStatus => 0,
86 ccStatusText => '',
87 ccStatus2 => '',
88 ccTranId => '',
89 complete => 0,
90 purchase_order => '',
91 shipping_method => '',
92 shipping_name => '',
93 shipping_trace => undef,
f0722dd2
TC
94 paypal_token => "",
95 paypal_tran_id => "",
080fc207 96 freight_tracking => "",
f0722dd2 97 stage => "incomplete",
14604ada
TC
98 );
99}
100
101sub address_columns {
102 return qw/
103 delivFirstName delivLastName delivStreet delivSuburb delivState
104 delivPostCode delivCountry
105 billFirstName billLastName billStreet billSuburb billState
106 billPostCode billCountry
107 telephone facsimile emailAddress
108 instructions billTelephone billFacsimile billEmail
109 delivMobile billMobile
110 delivOrganization billOrganization
111 delivStreet2 billStreet2/;
112}
113
114sub user_columns {
115 return qw/userId siteuser_id/;
116}
117
118sub payment_columns {
119 return qw/ccNumberHash ccName ccExpiryHash ccType
120 paidFor paymentReceipt paymentType
121 ccOnline ccSuccess ccReceipt ccStatus ccStatusText
122 ccStatus2 ccTranId/;
123}
124
c4f18087
TC
125=item billing_to_delivery_map
126
127Return a hashref where the key is a billing field and the value is the
128corresponding delivery field.
129
130=cut
131
132{
133 my %billing_to_delivery =
134 (
135 billEmail => "emailAddress",
136 billFirstName => "delivFirstName",
137 billLastName => "delivLastName",
138 billStreet => "delivStreet",
139 billStreet2 => "delivStreet2",
140 billSuburb => "delivSuburb",
141 billState => "delivState",
142 billPostCode => "delivPostCode",
143 billCountry => "delivCountry",
144 billTelephone => "telephone",
a964e89d 145 billMobile => "delivMobile",
c4f18087
TC
146 billFacsimile => "facsimile",
147 billOrganization => "delivOrganization",
148 );
149
150 sub billing_to_delivery_map {
151 return \%billing_to_delivery;
152 }
153}
154
0ec4ac8a
TC
155=item siteuser
156
157returns the SiteUser object of the user who made this order.
158
159=cut
160
161sub siteuser {
162 my ($self) = @_;
163
f0722dd2
TC
164 if ($self->siteuser_id) {
165 require SiteUsers;
166 my $user = SiteUsers->getByPkey($self->siteuser_id);
167 $user and return $user;
168 }
169
0ec4ac8a
TC
170 $self->{userId} or return;
171
172 require SiteUsers;
173
174 return ( SiteUsers->getBy(userId=>$self->{userId}) )[0];
175}
176
177sub items {
178 my ($self) = @_;
179
180 require BSE::TB::OrderItems;
181 return BSE::TB::OrderItems->getBy(orderId => $self->{id});
182}
183
ab2cd916
TC
184sub files {
185 my ($self) = @_;
186
7c6f563b
TC
187 require BSE::TB::ArticleFiles;
188 return BSE::TB::ArticleFiles->getSpecial(orderFiles=>$self->{id});
ab2cd916
TC
189}
190
eb9d306d
TC
191sub paid_files {
192 my ($self) = @_;
193
194 $self->paidFor
195 or return;
196
197 require BSE::TB::ArticleFiles;
198 return BSE::TB::ArticleFiles->getSpecial(orderPaidFor => $self->id);
199}
200
ab2cd916
TC
201sub products {
202 my ($self) = @_;
203
204 require Products;
205 Products->getSpecial(orderProducts=>$self->{id});
206}
207
41e7c841
TC
208sub valid_fields {
209 my ($class, $cfg) = @_;
210
211 my %fields =
212 (
37dd20ad
TC
213 delivFirstName => { description=>'Delivery First Name',
214 rules=>'dh_one_line' },
215 delivLastName => { description => 'Delivery Last Name',
216 rules=>'dh_one_line' },
217 delivOrganization => { description => 'Delivery Organization',
218 rules=>'dh_one_line' },
219 delivStreet => { description => 'Delivery Street',
220 rules=>'dh_one_line' },
221 delivStreet2 => { description => 'Delivery Street 2',
222 rules=>'dh_one_line' },
223 delivState => { description => 'Delivery State',
224 rules=>'dh_one_line' },
225 delivSuburb => { description => 'Delivery Suburb',
226 rules=>'dh_one_line' },
227 delivPostCode => { description => 'Delivery Post Code',
9074efa2 228 rules=>'dh_one_line;dh_int_postcode' },
37dd20ad
TC
229 delivCountry => { description => 'Delivery Country',
230 rules=>'dh_one_line' },
231 billFirstName => { description => 'Billing First Name',
232 rules=>'dh_one_line' },
233 billLastName => { description => 'Billing Last Name',
234 rules=>'dh_one_line' },
235 billOrganization => { description => 'Billing Organization',
236 rules=>'dh_one_line' },
237 billStreet => { description => 'Billing Street',
238 rules=>'dh_one_line' },
239 billStreet2 => { description => 'Billing Street 2',
240 rules=>'dh_one_line' },
241 billSuburb => { description => 'Billing Suburb',
242 rules=>'dh_one_line' },
243 billState => { description => 'Billing State',
244 rules=>'dh_one_line' },
245 billPostCode => { description => 'Billing Post Code',
9074efa2 246 rules=>'dh_one_line;dh_int_postcode' },
37dd20ad
TC
247 billCountry => { description => 'Billing First Name',
248 rules=>'dh_one_line' },
41e7c841
TC
249 telephone => { description => 'Telephone Number',
250 rules => "phone" },
251 facsimile => { description => 'Facsimile Number',
252 rules => 'phone' },
253 emailAddress => { description => 'Email Address',
c4f18087 254 rules=>'email' },
41e7c841
TC
255 instructions => { description => 'Instructions' },
256 billTelephone => { description => 'Billing Telephone Number',
257 rules=>'phone' },
258 billFacsimile => { description => 'Billing Facsimile Number',
259 rules=>'phone' },
260 billEmail => { description => 'Billing Email Address',
c4f18087 261 rules => 'email;required' },
41e7c841
TC
262 delivMobile => { description => 'Delivery Mobile Number',
263 rules => 'phone' },
264 billMobile => { description => 'Billing Mobile Number',
265 rules=>'phone' },
266 instructions => { description => 'Instructions' },
74b21f6d 267 purchase_order => { description => 'Purchase Order No' },
d8674b8b
AMS
268 shipping_cost => { description => 'Shipping charges' },
269 shipping_method => { description => 'Shipping method' },
41e7c841
TC
270 );
271
272 for my $field (keys %fields) {
273 my $display = $cfg->entry('shop', "display_$field");
274 $display and $fields{$field}{description} = $display;
275 }
276
277 return %fields;
278}
279
280sub valid_rules {
281 my ($class, $cfg) = @_;
282
283 return;
284}
285
286sub valid_payment_fields {
287 my ($class, $cfg) = @_;
288
289 my %fields =
290 (
291 cardNumber =>
292 {
293 description => "Credit Card Number",
294 rules=>"creditcardnumber",
295 },
296 cardExpiry =>
297 {
298 description => "Credit Card Expiry Date",
299 rules => 'creditcardexpirysingle',
300 },
301 cardHolder => { description => "Credit Card Holder" },
1546e1f0 302 ccType => { description => "Credit Card Type" },
41e7c841
TC
303 cardVerify =>
304 {
305 description => 'Card Verification Value',
306 rules => 'creditcardcvv',
307 },
308 );
309
310 for my $field (keys %fields) {
311 my $display = $cfg->entry('shop', "display_$field");
312 $display and $fields{$field}{description} = $display;
313 }
314
315 return %fields;
316}
317
318sub valid_payment_rules {
319 return;
320}
321
5d88571c
TC
322sub clear_items {
323 my ($self) = @_;
324
325 confess "Attempt to clear items on completed order $self->{id}"
326 if $self->{complete};
327
328 BSE::DB->run(deleteOrdersItems => $self->{id});
329}
330
14604ada
TC
331sub add_item {
332 my ($self, %opts) = @_;
333
334 my $prod = delete $opts{product}
335 or confess "Missing product option";
336 my $units = delete $opts{units} || 1;
337
338 my $options = '';
339 my @dboptions;
340 if ($opts{options}) {
341 if (ref $opts{options}) {
342 @dboptions = @{delete $opts{options}};
343 }
344 else {
345 $options = delete $opts{options};
346 }
347 }
348
349 require BSE::TB::OrderItems;
350 my %item =
351 (
352 productId => $prod->id,
353 orderId => $self->id,
354 units => $units,
355 price => $prod->retailPrice,
356 options => $options,
357 max_lapsed => 0,
358 session_id => 0,
359 ( map { $_ => $prod->{$_} }
360 qw/wholesalePrice gst customInt1 customInt2 customInt3 customStr1 customStr2 customStr3 title description subscription_id subscription_period product_code/
361 ),
362 );
363
364 $self->set_total($self->total + $prod->retailPrice * $units);
365
366 return BSE::TB::OrderItems->make(%item);
367}
368
13a986ee
TC
369sub deliv_country_code {
370 my ($self) = @_;
371
372 my $use_codes = BSE::Cfg->single->entry("shop", "country_code", 0);
373 if ($use_codes) {
374 return $self->delivCountry;
375 }
376 else {
377 require BSE::Countries;
378 return BSE::Countries::bse_country_code($self->delivCountry);
379 }
380}
381
f0722dd2
TC
382=item stage
383
384Return the order stage.
385
386If the stage is empty, guess from the order flags.
387
388=cut
389
390sub stage {
391 my ($self) = @_;
392
393 if ($self->{stage} ne "") {
394 return $self->{stage};
395 }
396
397 if (!$self->complete) {
398 return "incomplete";
399 }
400 elsif ($self->filled) {
401 return "shipped";
402 }
403 else {
404 return "unprocessed";
405 }
406}
407
408sub stage_description {
409 my ($self, $lang) = @_;
410
411 return BSE::TB::Orders->stage_label($self->stage, $lang);
412}
413
f55be9df
TC
414sub stage_description_id {
415 my ($self) = @_;
416
417 return BSE::TB::Orders->stage_label_id($self->stage);
418}
419
c4f18087 420=item delivery_mail_recipient
f0722dd2 421
c4f18087
TC
422Return a value suitable for BSE::ComposeMail's to parameter for the
423shipping email address.
f0722dd2
TC
424
425=cut
426
c4f18087 427sub delivery_mail_recipient {
f0722dd2
TC
428 my ($self) = @_;
429
430 my $user = $self->siteuser;
c4f18087 431 my $email = $self->emailAddress || $self->billEmail;
f0722dd2 432
c4f18087 433 if ($user && $user->email eq $email) {
f0722dd2
TC
434 return $user;
435 }
436
c4f18087 437 return $email;
f0722dd2
TC
438}
439
768dccf0 440=item _tags
8d8895b4 441
768dccf0 442Internal method with the common code between tags() and mail_tags().
8d8895b4
TC
443
444=cut
445
768dccf0
TC
446sub _tags {
447 my ($self, $escape) = @_;
8d8895b4
TC
448
449 require BSE::Util::Tags;
8d8895b4 450 require BSE::TB::OrderItems;
768dccf0
TC
451 require BSE::Util::Iterate;
452 my $it;
453 my $art;
454 my $esc;
455 my $obj;
456 if ($escape) {
457 require BSE::Util::HTML;
458 $it = BSE::Util::Iterate::Objects->new;
459 $art = \&BSE::Util::Tags::tag_article;
460 $obj = \&BSE::Util::Tags::tag_object;
461 $esc = \&BSE::Util::HTML::escape_html;
462 }
463 else {
464 $it = BSE::Util::Iterate::Objects::Text->new;
465 $art = \&BSE::Util::Tags::tag_article_plain;
466 $obj = \&BSE::Util::Tags::tag_object_plain;
467 $esc = sub { return $_[0] };
468 }
469
470 my $cfg = BSE::Cfg->single;
471 my $must_be_paid = $cfg->entryBool('downloads', 'must_be_paid', 0);
472 my $must_be_filled = $cfg->entryBool('downloads', 'must_be_filled', 0);
473
8d8895b4
TC
474 my %item_cols = map { $_ => 1 } BSE::TB::OrderItem->columns;
475 my %products;
476 my $current_item;
768dccf0 477 my $current_file;
8d8895b4
TC
478 return
479 (
768dccf0 480 order => [ $obj, $self ],
8d8895b4
TC
481 $it->make
482 (
483 single => "item",
484 plural => "items",
485 code => [ items => $self ],
486 store => \$current_item,
487 ),
488 extended => sub {
489 my ($args) = @_;
490
491 $current_item
492 or return '* only usable in items iterator *';
493
494 $item_cols{$args}
495 or return "* unknown item column $args *";
496
497 return $current_item->$args() * $current_item->units;
498 },
499 $it->make
500 (
501 single => "option",
502 plural => "options",
503 code => sub {
504 $current_item
505 or return;
506 return $current_item->option_hashes
507 },
508 nocache => 1,
509 ),
510 options => sub {
511 $current_item
512 or return '* only in the items iterator *';
768dccf0 513 return $esc->($current_item->nice_options);
8d8895b4
TC
514 },
515 product => sub {
516 $current_item
517 or return '* only usable in items *';
518
519 require Products;
520 my $id = $current_item->productId;
521 $products{$id} ||= Products->getByPkey($id);
522
523 my $product = $products{$id}
524 or return '';
525
768dccf0
TC
526 return $art->($product, $cfg, $_[0]);
527 },
7c6f563b 528 $it->make
768dccf0
TC
529 (
530 single => 'orderfile',
531 plural => 'orderfiles',
532 code => [ files => $self ],
533 store => \$current_file,
534 ),
7c6f563b 535 $it->make
768dccf0
TC
536 (
537 single => "prodfile",
538 plural => "prodfiles",
539 code => sub {
540 $current_item
541 or return '* only usable in items *';
542
543 require Products;
544 my $id = $current_item->productId;
545 $products{$id} ||= Products->getByPkey($id);
546
547 my $product = $products{$id}
548 or return '';
549
550 return $product->files;
551 },
552 store => \$current_file,
553 ),
554 ifFileAvail => sub {
555 $current_file or return 0;
556 $current_file->{forSale} or return 1;
557
558 return 0 if $must_be_paid && !$self->{paidFor};
559 return 0 if $must_be_filled && !$self->{filled};
560
561 return 1;
8d8895b4
TC
562 },
563 );
564}
565
768dccf0
TC
566=item mail_tags
567
568=cut
569
570sub mail_tags {
571 my ($self) = @_;
572
573 return $self->_tags(0);
574}
575
576=item tags
577
578Return template tags suitable for an order (non-mail)
579
580=cut
581
582sub tags {
583 my ($self) = @_;
584
585 return $self->_tags(1);
586}
587
f0722dd2
TC
588sub send_shipped_email {
589 my ($self) = @_;
590
c4f18087 591 my $to = $self->delivery_mail_recipient;
f0722dd2
TC
592 require BSE::ComposeMail;
593 my $mailer = BSE::ComposeMail->new(cfg => BSE::Cfg->single);
594 require BSE::Util::Tags;
f0722dd2
TC
595 my %acts =
596 (
597 BSE::Util::Tags->mail_tags(),
8d8895b4 598 $self->mail_tags,
f0722dd2 599 );
c4f18087 600 my %opts =
f0722dd2
TC
601 (
602 to => $to,
603 subject => "Your order has shipped",
604 template => "email/ordershipped",
605 acts => \%acts,
606 log_msg => "Notify customer order has shipped",
607 log_object => $self,
608 log_component => "shopadmin:orders:saveorder",
609 );
c4f18087
TC
610 if ($self->emailAddress && $self->billEmail
611 && lc $self->emailAddress ne $self->billEmail) {
612 $opts{cc} = $self->billEmail;
613 }
614
615 $mailer->send(%opts);
f0722dd2
TC
616}
617
618sub new_stage {
619 my ($self, $who, $stage, $stage_note) = @_;
620
621 unless ($stage ne $self->stage
622 || defined $stage_note && $stage_note =~ /\S/) {
623 return;
624 }
625
626 my $old_stage = $self->stage;
627 my $msg = "Set to stage '$stage'";
628 if (defined $stage_note && $stage_note =~ /\S/) {
629 $msg .= ": $stage_note";
630 }
631 require BSE::TB::AuditLog;
632 BSE::TB::AuditLog->log
633 (
634 component => "shopadmin:orders:saveorder",
635 object => $self,
636 msg => $msg,
637 level => "info",
638 actor => $who || "U"
639 );
640
641 if ($stage ne $old_stage) {
642 $self->set_stage($stage);
643 if ($stage eq "shipped") {
644 $self->send_shipped_email();
645 $self->set_filled(1);
646 }
647 else {
648 $self->set_filled(0);
649 }
650 }
651}
652
0ec4ac8a 6531;