1 # Vend::Order - Interchange order routing routines
3 # Copyright (C) 2002-2009 Interchange Development Group
4 # Copyright (C) 1996-2002 Red Hat, Inc.
6 # This program was originally based on Vend 0.2 and 0.3
7 # Copyright 1995 by Andrew M. Wilcox <amw@wilcoxsolutions.com>
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public
20 # License along with this program; if not, write to the Free
21 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
50 use Vend::Interpolate;
56 no warnings qw(uninitialized numeric);
58 use autouse 'Vend::Error' => qw/do_lockout/;
76 sub reset_order_vars {
89 # copy global order check routines
90 $OrderCheck = { %{$Global::OrderCheck || {} }};
92 # overlay any catalog order check routines
94 if ($r = $Vend::Cfg->{CodeDef}{OrderCheck} and $r = $r->{Routine}) {
96 $OrderCheck->{$_} = $r->{$_};
105 '&charge' => \&_charge,
106 '&credit_card' => \&_credit_card,
107 '&return' => \&_return,
108 '&update' => \&_update,
109 '&fatal' => \&_fatal,
110 '&and' => \&_and_check,
111 '&or' => \&_or_check,
112 '&format' => \&_format,
113 '&tables' => sub { $Tables = $_[1]; return 1; },
114 '&noerror' => sub { $No_error = $_[1] },
115 '&success' => sub { $Success_page = $_[1] },
116 '&fail' => sub { $Fail_page = $_[1] },
117 '&final' => \&_final,
118 '&calc' => sub { Vend::Interpolate::tag_calc($_[1]) },
119 '&perl' => sub { Vend::Interpolate::tag_perl($Tables, {}, $_[1]) },
121 my($ref,$params) = @_;
126 my($ref,$params) = @_;
127 my ($var, $value) = split /\s+/, $params, 2;
128 $::Values->{$var} = $value;
132 my($ref,$params) = @_;
133 my ($var, $value) = split /\s+/, $params, 2;
134 $::Values->{$var} = $value;
135 my $msg = errmsg("%s set failed.", $var);
136 return ($value, $var, $msg);
141 $Update = is_yes($_[1]);
146 $Fatal = ( defined($_[1]) && ($_[1] =~ /^[yYtT1]/) ) ? 1 : 0;
151 $Final = ( defined($_[1]) && ($_[1] =~ /^[yYtT1]/) ) ? 1 : 0;
156 $Success = ( defined($_[1]) && ($_[1] =~ /^[yYtT1]/) ) ? 1 : 0;
160 my($ref, $params, $message) = @_;
162 my ($routine, $var, $val) = split /\s+/, $params, 3;
166 #::logDebug("OrderCheck = $OrderCheck routine=$routine");
169 if( $sub = $Parse{$routine}) {
170 @args = ($var, $val, $message);
173 elsif ($OrderCheck and $sub = $OrderCheck->{$routine}) {
174 #::logDebug("Using coderef OrderCheck = $sub");
175 @args = ($ref,$var,$val,$message);
178 elsif (defined &{"_$routine"}) {
179 $sub = \&{"_$routine"};
180 @args = ($ref,$var,$val,$message);
183 return (undef, $var, errmsg("No format check routine for '%s'", $routine));
186 @return = $sub->(@args);
188 if(! $return[0] and $message) {
189 $return[2] = $message;
195 my ($or, $ref, $checks, $err, $vref) = @_;
196 my ($var, $val, $mess, $message);
198 $mess = "$checks $err";
199 while($mess =~ s/(\S+=\w+)[\s,]*//) {
201 ($val, $var, $message) = do_check($check, $vref);
202 return undef if ! defined $var;
204 1 while $mess =~ s/(\S+=\w+)[\s,]*//;
205 return ($val, $var, $message)
213 1 while $mess =~ s/(\S+=\w+)[\s,]*//;
214 return($val, $var, $mess);
217 return ($val, $var, $mess);
221 if(! length($_[1]) ) {
225 return chain_checks(0, @_);
229 if(! length($_[1]) ) {
233 return chain_checks(1, @_);
237 my ($ref, $params, $message) = @_;
240 if ($params =~ /^custom\s+/) {
244 $params =~ s/(\w+)\s*(.*)/$1/s;
245 $opt = get_option_hash($2);
249 $result = Vend::Payment::charge($params, $opt);
255 my $msg = errmsg("Fatal error on charge operation '%s': %s", $params, $@);
259 elsif( $Vend::Session->{payment_error} ) {
260 # do nothing, no extended messages
262 "Charge failed, reason: %s",
263 $Vend::Session->{payment_error},
269 "Charge operation '%s' failed.",
270 ($ref->{mv_cyber_mode} || $params),
274 #::logDebug("charge result: result=$result params=$params message=$message");
275 return ($result, $params, $message);
279 my($ref, $params) = @_;
287 # Make a copy if we need to keep the credit card number in memory for
290 # New or Compatibility to get options
292 if($params =~ /=/) { # New
293 $params =~ s/^\s*(\w+)(\s+|$)//
295 $subname = 'standard' if ! $subname;
296 $opt = get_option_hash($params);
300 $opt->{keep} = 1 if $params =~ s/\s+keep//i;
302 if($params =~ s/\s+(.*)//) {
303 $opt->{accepted} = $1;
308 $sub = $subname eq 'standard'
309 ? \&encrypt_standard_cc
310 : $Global::GlobalSub->{$subname};
313 ::logError("bad credit card check GlobalSub: '%s'", $subname);
326 mv_credit_card_exp_month
327 mv_credit_card_exp_year
328 mv_credit_card_exp_all
330 mv_credit_card_reference
333 = $sub->($ref, undef, $opt );
337 ::logError("credit card check (%s) error: %s", $subname, $@);
340 elsif(! $::Values->{mv_credit_card_valid}) {
341 return (0, 'mv_credit_card_valid', $::Values->{mv_credit_card_error});
344 return (1, 'mv_credit_card_valid');
353 $expire =~ /(\d\d?)(.*)/;
359 $month = $CGI::values{mv_credit_card_exp_month};
360 $year = $CGI::values{mv_credit_card_exp_year};
362 return '' if $month !~ /^\d+$/ || $year !~ /^\d+$/;
363 return '' if $month <1 || $month > 12;
364 $year += ($year < 70) ? 2000 : 1900 if $year < 1900;
365 my (@now) = localtime();
367 return '' if ($year < $now[5]) || ($year == $now[5] && $month <= $now[4]);
371 sub validate_whole_cc {
372 my($mess) = join " ", @_;
373 $mess =~ s:[^\sA-Za-z0-9/]::g ;
374 my (@tok) = split /\s+/, $mess;
375 my($num,$expire) = ('', '', '');
378 $num .= $_ if /^\d+$/;
379 $expire = $_ if m:/: ;
381 return 0 unless valid_exp_date($expire);
387 # Validate credit card routine
388 # by Jon Orwant, from Business::CreditCard and well-known algorithms
391 my ($number,$min_digits) = @_;
392 my ($i, $sum, $weight);
395 $min_digits = 2 if $min_digits < 2;
399 return 0 unless length($number) >= $min_digits && 0+$number;
401 for ($i = 0; $i < length($number) - 1; $i++) {
402 $weight = substr($number, -1 * ($i + 2), 1) * (2 - ($i % 2));
403 $sum += (($weight < 10) ? $weight : ($weight - 9));
406 return 1 if substr($number, -1) == (10 - $sum % 10) % 10;
412 my ($cardinfo, $template) = @_;
414 if (ref $cardinfo eq 'SCALAR') {
415 $cardinfo = { MV_CREDIT_CARD_NUMBER => $$cardinfo };
416 } elsif (! ref $cardinfo) {
417 $cardinfo = { MV_CREDIT_CARD_NUMBER => $cardinfo };
418 } elsif (ref $cardinfo eq 'ARRAY') {
420 my %c = map { $_ => $cardinfo->[$i++] } qw(
421 MV_CREDIT_CARD_NUMBER
422 MV_CREDIT_CARD_EXP_MONTH
423 MV_CREDIT_CARD_EXP_YEAR
428 } elsif (ref $cardinfo ne 'HASH') {
432 if(my $num = $cardinfo->{MV_CREDIT_CARD_NUMBER}) {
435 @quads = $num =~ m{(\d\d\d\d)(\d\d\d\d)(\d\d\d\d)(\d+)};
436 $cardinfo->{MV_CREDIT_CARD_QUADS} = join "-", @quads;
439 $template = $template ||
440 $::Variable->{MV_CREDIT_CARD_INFO_TEMPLATE} ||
442 {MV_CREDIT_CARD_TYPE}
443 {MV_CREDIT_CARD_NUMBER}
444 {MV_CREDIT_CARD_EXP_MONTH}/{MV_CREDIT_CARD_EXP_YEAR}
447 $cardinfo->{MV_CREDIT_CARD_TYPE} ||=
448 guess_cc_type($cardinfo->{MV_CREDIT_CARD_NUMBER});
450 return Vend::Interpolate::tag_attr_list($template, $cardinfo);
458 my $country = uc($::Values->{$::Variable->{MV_COUNTRY_FIELD} || 'country'} || '');
460 if(my $subname = $Vend::Cfg->{SpecialSub}{guess_cc_type}) {
461 my $sub = $Vend::Cfg->{Sub}{$subname} || $Global::GlobalSub->{$subname};
463 if( $sub and $guess = $sub->($ccnum) ) {
468 # based on logic from Business::CreditCard
472 elsif ($ccnum =~ /^4(?:\d{12}|\d{15})$/)
475 elsif ($ccnum =~ /^5[1-5]\d{14}$/)
479 $ccnum =~ /^30[0-5]\d{11}(?:\d{2})?$/ # Diners Club: 300-305
480 or $ccnum =~ /^3095\d{10}(?:\d{2})?$/ # Diners Club: 3095
481 or $ccnum =~ /^3[68]\d{12}(?:\d{2})?$/ # Diners Club: 36
482 or $ccnum =~ /^6011\d{12}$/
483 or $ccnum =~ /^64[4-9]\d{13}$/
484 or $ccnum =~ /^65\d{14}$/
485 or ( $ccnum =~ /^62[24-68]\d{13}$/ and $country ne 'CN' ) # China Unionpay
486 or ( $ccnum =~ /^35(?:2[89]|[3-8]\d)\d{10}$/ and $country eq 'US' ) # JCB
488 { return 'discover' }
490 elsif ($ccnum =~ /^3[47]\d{13}$/)
493 elsif ($ccnum =~ /^3(?:6\d{12}|0[0-5]\d{11})$/)
494 { return 'dinersclub' }
496 elsif ($ccnum =~ /^38\d{12}$/)
497 { return 'carteblanche' }
499 elsif ($ccnum =~ /^2(?:014|149)\d{11}$/)
502 elsif ($ccnum =~ /^(?:3\d{15}|2131\d{11}|1800\d{11})$/)
506 $ccnum =~ /^49(?:03(?:0[2-9]|3[5-9])|11(?:0[1-2]|7[4-9]|8[1-2])|36[0-9]{2})\d{10}(?:\d{2,3})?$/
507 or $ccnum =~ /^564182\d{10}(?:\d{2,3})?$/
508 or $ccnum =~ /^6(?:3(?:33[0-4][0-9])|759[0-9]{2})\d{10}(?:\d{2,3})?$/
512 elsif ($ccnum =~ /^56(?:10\d\d|022[1-5])\d{10}$/)
513 { return 'bankcard' }
515 elsif ($ccnum =~ /^6(?:3(?:34[5-9][0-9])|767[0-9]{2})\d{10}(?:\d{2,3})?$/)
518 elsif ($ccnum =~ /^62[24-68]\d{13}$/)
519 { return 'chinaunionpay' }
521 elsif ($ccnum =~ /^6(?:304|7(?:06|09|71))\d{12,15}$/)
525 { return $::Variable->{MV_PAYMENT_OTHER_CARD} || 'other' }
529 # Takes a reference to a hash (usually %CGI::values) that contains
532 # mv_credit_card_number The actual credit card number
533 # mv_credit_card_exp_all A combined expiration MM/YY
534 # mv_credit_card_exp_month Month only, used if _all not present
535 # mv_credit_card_exp_year Year only, used if _all not present
536 # mv_credit_card_cvv2 CVV2 verification number from back of card
537 # mv_credit_card_type A = Amex, D = Discover, etc. Attempts
538 # to guess from number if not there
539 # mv_credit_card_separate Causes mv_credit_card_info to contain only number, must
540 # then develop expiration from the above
542 sub encrypt_standard_cc {
543 my($ref, $nodelete, $opt) = @_;
546 $opt = {} unless ref $opt;
548 mv_credit_card_type mv_credit_card_number
549 mv_credit_card_exp_year mv_credit_card_exp_month
550 mv_credit_card_force mv_credit_card_exp_reference
551 mv_credit_card_exp_all mv_credit_card_exp_separate
555 my $month = $ref->{mv_credit_card_exp_month} || '';
556 my $type = $ref->{mv_credit_card_type} || '';
557 my $num = $ref->{mv_credit_card_number} || '';
558 my $year = $ref->{mv_credit_card_exp_year} || '';
559 my $all = $ref->{mv_credit_card_exp_all} || '';
560 my $cvv2 = $ref->{mv_credit_card_cvv2} || '';
561 my $force = $ref->{mv_credit_card_force} || '';
562 my $separate = $ref->{mv_credit_card_separate} || $opt->{separate} || '';
564 delete @$ref{@deletes} unless ($opt->{nodelete} or $nodelete);
566 # remove unwanted chars from card number
569 # error will be pushed on this if present
571 '', # 0- Whether it is valid
572 '', # 1- Encrypted credit card information
577 '', # 6- Reference number in form 41**1111
581 if ($all =~ m!(\d\d?)[-/](\d\d)(\d\d)?! ){
585 elsif ($month >= 1 and $month <= 12 and $year) {
586 $all = "$month/$year";
598 my $msg = errmsg("Can't figure out credit card expiration.");
599 $Vend::Session->{errors}{mv_credit_card_valid} = $msg;
604 if(! valid_exp_date($all) ) {
605 my $msg = errmsg("Card is expired.");
606 $Vend::Session->{errors}{mv_credit_card_valid} = $msg;
612 my $msg = errmsg("Missing credit card number");
613 $Vend::Session->{errors}{mv_credit_card_valid} = $msg;
618 $type = guess_cc_type($num) unless $type;
620 if ($type and $opt->{accepted} and $opt->{accepted} !~ /\b$type\b/i) {
621 my $msg = errmsg("Sorry, we don't accept credit card type '%s'.", $type);
622 $Vend::Session->{errors}{mv_credit_card_valid} = $msg;
629 elsif(! $opt->{any}) {
630 my $msg = errmsg("Can't figure out credit card type from number.");
631 $Vend::Session->{errors}{mv_credit_card_valid} = $msg;
636 unless ($valid = luhn($num) || $force ) {
637 my $msg = errmsg("Credit card number fails LUHN-10 check.");
638 $Vend::Session->{errors}{mv_credit_card_valid} = $msg;
645 my $check_string = $num;
646 $check_string =~ s/(\d\d).*(\d\d\d\d)$/$1**$2/;
648 my $encrypt_string = $separate ? $num :
649 build_cc_info( [$num, $month, $year, $cvv2, $type] );
650 $info = pgp_encrypt ($encrypt_string);
652 unless (defined $info) {
653 my $msg = errmsg("Credit card encryption failed: %s", $! );
654 $Vend::Session->{errors}{mv_credit_card_valid} = $msg;
660 $return[6] = $check_string;
667 my ($code, $qty, $opt) = @_;
670 $item_text = $opt->{text} || '';
677 # return create_onfly() if $opt->{create};
679 my $joiner = $::Variable->{MV_ONFLY_JOINER} || '|';
680 my $split_fields= $::Variable->{MV_ONFLY_FIELDS} || undef;
682 $item_text =~ s/\s+$//;
683 $item_text =~ s/^\s+//;
686 $joiner = quotemeta $joiner;
687 @parms = split /$joiner|\0/, $item_text;
690 if(defined $split_fields) {
691 @fields = split /[,\s]+/, $split_fields;
692 @{$item}{@fields} = @parms;
696 ($k, $v) = split /=/, $_;
700 $item->{mv_price} = $item->{price}
701 if ! $item->{mv_price};
702 $item->{code} = $code if ! $item->{code};
703 $item->{quantity} = $qty if ! $item->{quantity};
707 # Email the processed order. This is a legacy routine, not normally used
708 # any more. Order email is normally sent via Route.
710 my ($email, $order_no) = @_;
711 $email = $Vend::Cfg->{MailOrderTo} unless $email;
715 if ($::Values->{mv_order_report}) {
716 unless( allowed_file($::Values->{mv_order_report}) ) {
717 log_file_violation ($::Values->{mv_order_report}, 'mail_order');
720 $body = readin($::Values->{mv_order_report})
723 $body = readfile($Vend::Cfg->{OrderReport})
725 unless (defined $body) {
727 q{Cannot find order report in:
732 trying one more time. Fix this.},
733 $Vend::Cfg->{OrderReport},
734 $::Values->{mv_order_report},
736 unless( allowed_file($Vend::Cfg->{OrderReport}) ) {
737 log_file_violation($Vend::Cfg->{OrderReport}, 'mail_order');
740 $body = readin($Vend::Cfg->{OrderReport});
741 return undef if ! $body;
743 return undef unless defined $body;
745 $order_no = update_order_number() unless $order_no;
747 $body = interpolate_html($body);
749 $body = pgp_encrypt($body) if $Vend::Cfg->{PGP};
751 track_order($order_no, $body);
753 $subject = $::Values->{mv_order_subject} || "ORDER %n";
755 if(defined $order_no) {
756 $subject =~ s/%n/$order_no/;
758 else { $subject =~ s/\s*%n\s*//g; }
760 $ok = send_mail($email, $subject, $body);
765 my($body, $key, $cmd) = @_;
766 #::logDebug("called pgp_encrypt key=$key cmd=$cmd");
767 $cmd = $Vend::Cfg->{EncryptProgram} unless $cmd;
768 $key = $Vend::Cfg->{EncryptKey} unless $key;
769 #::logDebug("pgp_encrypt using key=$key cmd=$cmd");
771 $key =~ s/,/ /g; # turn commas to spaces
772 $key =~ s/^\s+//; # strip leading spaces
773 $key =~ s/\s+$//; # strip trailing spaces
774 $key =~ s/\s+/ /g; # convert multiple spaces to single spaces
776 my @keys = split /\s/, $key;
780 if("\L$cmd" eq 'none') {
781 return ::errmsg("NEED ENCRYPTION ENABLED.");
784 return ::errmsg("NEED ENCRYPTION KEY POINTER.");
786 elsif($cmd =~ m{^(?:/\S+/)?\bgpg$}) {
787 $cmd .= " --batch --always-trust -e -a ";
790 elsif($cmd =~ m{^(?:/\S+/)?pgpe$}) {
794 elsif($cmd =~ m{^(?:/\S+/)?\bpgp$}) {
800 die ::errmsg("Illegal character in encryption command: %s", $cmd);
804 $cmd =~ s/%%/:~PERCENT~:/g;
806 foreach my $thiskey (@keys) {
807 $thiskey =~ s/'/\\'/g;
808 $cmd .= "$keyparam '$thiskey' ";
810 $cmd =~ s/:~PERCENT~:/%/g;
812 #::logDebug("after pgp_encrypt key=$key cmd=$cmd");
814 my $fpre = $Vend::Cfg->{ScratchDir} . "/pgp.$Vend::Session->{id}.$$";
815 $cmd .= " >$fpre.out";
816 $cmd .= " 2>$fpre.err" unless $cmd =~ /2>/;
818 or die "Couldn't fork: $!";
826 $status = $status >> 8;
829 logError("PGP failed with error level %s, status %s: $!", $?, $status);
831 logError("PGP hard failure, command that failed: %s", $cmd);
835 $body = readfile("$fpre.out");
843 my $ref = \%CGI::values;
844 my $vref = shift || $::Values;
846 my $conditional_update;
848 my($var, $val, $m, $message);
850 ($var,$val) = split /[\s=]+/, $parameter, 2;
852 elsif ($parameter =~ /(\w+)[\s=]+(.*)/) {
855 $conditional_update = $Update;
856 $m = $v =~ s/\s+(.*)// ? $1 : undef;
859 $v . ' ' . $k . ' ' . $vref->{$k}
863 logError("Unknown order check '%s' in profile %s", $parameter, $Profile);
866 $val =~ s/&#(\d+);/chr($1)/ge;
869 ## $vref added for chained checks only
870 ($val, $var, $message) = $Parse{$var}->($ref, $val, $m, $vref);
873 logError( "Unknown order check parameter in profile %s: %s=%s",
880 #::logDebug("&Vend::Order::do_check returning val=$val, var=$var, message=$message");
881 if($conditional_update and $val) {
882 ::update_values($var);
884 return ($val, $var, $message);
888 my ($profiles, $vref, $individual) = @_;
891 $Vend::Session->{errors} = {}
892 unless ref $Vend::Session->{errors} eq 'HASH';
894 ## Must have some security on mv_individual_profile because data
895 ## lookups can be done via filter and/or unique
896 if($individual and ! delete($::Scratch->{mv_individual_profile})) {
897 ::logError("Individual profile supplied without scratch authorization");
901 #::logDebug("nextpage=$CGI::values{mv_nextpage}");
902 for my $profile (split /\0+/, $profiles) {
904 $status = check_order_each($profile, $vref, $individual);
906 # only do the individual checks once
909 my $np = $CGI::values{mv_nextpage};
912 $np = $CGI::values{mv_nextpage} = $Success_page;
914 elsif ($CGI::values{mv_success_href}) {
915 $np = $CGI::values{mv_nextpage} = $CGI::values{mv_success_href};
918 my $f = $CGI::values{mv_success_form};
920 if($CGI::values{mv_success_zero}) {
922 $CGI::values{mv_nextpage} ||= $np;
926 my $r = Vend::Util::scalar_to_hash($f);
927 while (my ($k, $v) = each %$r) {
928 $CGI::values{$k} = $v;
933 #::logDebug("Got to status=$status on profile=$profile");
935 $np = $CGI::values{mv_nextpage} = $Fail_page;
937 elsif ($CGI::values{mv_fail_href}) {
938 $np = $CGI::values{mv_nextpage} = $CGI::values{mv_fail_href};
941 my $f = $CGI::values{mv_fail_form};
943 if($CGI::values{mv_fail_zero}) {
945 $CGI::values{mv_nextpage} ||= $np;
949 my $r = Vend::Util::scalar_to_hash($f);
950 while (my ($k, $v) = each %$r) {
951 $CGI::values{$k} = $v;
956 if ($Final and ! scalar @{$Vend::Items}) {
958 $::Values->{"mv_error_items"} =
959 $Vend::Session->{errors}{items} =
961 "You might want to order something! No items in cart.",
964 #::logDebug("FINISH checking profile $profile: Fatal=$Fatal Final=$Final Status=$status");
966 # first profile to fail prevents all other profiles from running
971 my $errors = join "\n", @Errors;
972 #::logDebug("Errors after checking profile(s):\n$errors") if $errors;
973 $errors = '' unless defined $errors and ! $Success;
974 #::logDebug("status=$status nextpage=$CGI::values{mv_nextpage}");
975 return ($status, $Final, $errors);
978 sub check_order_each {
979 my ($profile, $vref, $individual) = @_;
982 if(defined $Vend::Cfg->{OrderProfileName}->{$profile}) {
983 $profile = $Vend::Cfg->{OrderProfileName}->{$profile};
984 $params = $Vend::Cfg->{OrderProfile}->[$profile];
986 elsif($profile =~ /^\d+$/) {
987 $params = $Vend::Cfg->{OrderProfile}->[$profile];
989 elsif(defined $::Scratch->{$profile}) {
990 $params = $::Scratch->{$profile};
993 ::logError("Order profile %s not found", $profile);
996 return undef unless $params;
998 $params = interpolate_html($params);
999 $params =~ s/\\\n//g;
1002 $Fatal = $Final = 0;
1004 my($var,$val,$message);
1006 my(@param) = split /[\r\n]+/, $params;
1008 ## Find marker for individual insertion
1014 next unless /^\s*\&fatal\s*=\s*(.*)/i and is_yes($1);
1018 if(! defined $mark) {
1022 next unless /^\s*\&update\s*=\s*(.*)/i and is_yes($1);
1027 $mark = 0 unless defined $mark;
1028 my @newparams = split /\0/, $individual;
1029 splice(@param, $mark, 0, @newparams);
1032 #::logDebug("Total profile:\n" . join ("\n", @param));
1044 ($join .= "$_\n", next) if $here;
1049 if(s/<<(\w+);?\s*$//) {
1062 ($val, $var, $message) = do_check($_, $vref);
1064 # no actual check on this line, skip to next
1065 next if /^&(?:and|or)\s*$/i;
1069 $val = ($last_one && $val);
1072 $val = ($last_one || $val);
1077 $status = 0 unless $val;
1080 $::Values->{"mv_status_$var"} = $message
1081 if defined $message and $message;
1082 delete $Vend::Session->{errors}{$var};
1083 delete $::Values->{"mv_error_$var"};
1087 $::Values->{"mv_error_$var"} = $message;
1092 elsif( $Vend::Session->{errors}{$var} ) {
1093 if ($message and $Vend::Session->{errors}{$var} !~ /\Q$message/) {
1094 $Vend::Session->{errors}{$var} = errmsg(
1096 $Vend::Session->{errors}{$var},
1102 $Vend::Session->{errors}{$var} = $message ||
1103 errmsg('%s: failed check', $var);
1105 push @Errors, "$var: $message";
1108 if (defined $Success) {
1112 last if $Fatal && ! $status;
1117 use vars qw/ %state_template %state_error %zip_routine %zip_error /;
1118 $state_error{US} = "'%s' not a two-letter state code";
1119 $state_error{CA} = "'%s' not a two-letter province code";
1120 $state_template{US} = <<EOF;
1121 | AL AK AZ AR CA CO CT DE FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS MO |
1122 | MT NE NV NH NJ NM NY NC ND OH OK OR PA RI SC SD TN TX UT VT VA WA WV WI WY |
1123 | PR DC AA AE GU VI AS MP FM MH PW AP FP FPO APO |
1126 # NF = Newfoundland is deprecated and will be removed at some point;
1128 $state_template{CA} = <<EOF;
1129 | AB BC MB NB NF NL NS NT NU ON PE QC SK YT YK |
1132 $zip_error{US} = "'%s' not a US zip code";
1133 $zip_routine{US} = sub { $_[0] =~ /^\s*\d\d\d\d\d(?:-?\d\d\d\d)?\s*$/ };
1135 $zip_error{CA} = "'%s' not a Canadian postal code";
1136 $zip_routine{CA} = sub {
1138 return undef unless defined $val;
1139 $val =~ s/[_\W]+//g;
1140 $val =~ /^[ABCEGHJKLMNPRSTVXYabceghjklmnprstvxy]\d[A-Za-z]\d[A-Za-z]\d$/;
1143 sub _state_province {
1144 my($ref,$var,$val) = @_;
1146 if(length($val) != 2) {
1150 my $pval = $::Variable->{MV_VALID_PROVINCE}
1151 ? " $::Variable->{MV_VALID_PROVINCE} "
1152 : $state_template{CA};
1153 my $sval = $::Variable->{MV_VALID_STATE}
1154 ? " $::Variable->{MV_VALID_STATE} "
1155 : $state_template{US};
1157 unless $sval =~ /\s$val\s/i or $pval =~ /\s$val\s/i ;
1160 return (undef, $var,
1161 errmsg( "'%s' not a two-letter state or province code", $val )
1164 return (1, $var, '');
1168 my($ref,$var,$val) = @_;
1169 my $sval = $::Variable->{MV_VALID_STATE}
1170 ? " $::Variable->{MV_VALID_STATE} "
1171 : $state_template{US};
1173 if( $val =~ /\S/ and $sval =~ /\s$val\s/i ) {
1174 return (1, $var, '');
1177 return (undef, $var,
1178 errmsg( $state_error{US}, $val )
1184 my($ref,$var,$val) = @_;
1185 my $pval = $::Variable->{MV_VALID_PROVINCE}
1186 ? " $::Variable->{MV_VALID_PROVINCE} "
1187 : $state_template{CA};
1188 if( $val =~ /\S/ and $pval =~ /\s$val\s/i) {
1189 return (1, $var, '');
1192 return (undef, $var,
1193 errmsg( $state_error{CA}, $val )
1199 my ($ref, $var) = @_;
1200 my $cfield = $::Variable->{MV_COUNTRY_FIELD} || 'country';
1201 my $cval = $ref->{$cfield} || $::Values->{$cfield};
1203 if($var =~ /^b_/ and $ref->{"b_$cfield"} || $::Values->{"b_$cfield"}) {
1204 $cval = $ref->{"b_$cfield"} || $::Values->{"b_$cfield"};
1210 my($ref,$var,$val) = @_;
1214 my $cval = _get_cval($ref, $var);
1216 if (my $sub = $zip_routine{$cval}) {
1217 $sub->($val) or $error = 1;
1219 elsif($::Variable->{MV_ZIP_REQUIRED}) {
1220 " $::Variable->{MV_ZIP_REQUIRED} " =~ /\s$cval\s/
1222 length($val) < 4 and $error = 1;
1226 my $tpl = $zip_error{$cval} || "'%s' not a valid post code for country '%s'";
1227 my $msg = errmsg( $tpl, $val, $cval );
1228 return (undef, $var, $msg );
1230 return (1, $var, '');
1234 my($ref,$var,$val) = @_;
1237 my $cval = _get_cval($ref, $var);
1239 if(my $sval = $state_template{$cval}) {
1240 $error = 1 unless $sval =~ /\s$val\s/;
1242 elsif($::Variable->{MV_STATE_REQUIRED}) {
1243 " $::Variable->{MV_STATE_REQUIRED} " =~ /\s$cval\s/
1245 length($val) < 2 and $error = 1;
1249 my $tpl = $state_error{$cval} || "'%s' not a valid state for country '%s'";
1250 my $msg = errmsg( $tpl, $val, $cval );
1251 return (undef, $var, $msg );
1253 return (1, $var, '');
1257 return undef unless defined $_[1];
1258 [split /\s*[,\0]\s*/, $_[1]]
1262 return( defined($_[2]) && ($_[2] =~ /^[yYtT1]/));
1266 my($ref,$var,$val) = @_;
1267 ((_zip(@_))[0] or (_ca_postcode(@_))[0])
1268 and return (1, $var, '');
1269 return (undef, $var, errmsg("'%s' not a US zip or Canadian postal code", $val));
1273 my($ref,$var,$val) = @_;
1274 $val =~ s/[_\W]+//g;
1277 $val =~ /^[ABCEGHJKLMNPRSTVXYabceghjklmnprstvxy]\d[A-Za-z]\d[A-Za-z]\d$/
1278 and return (1, $var, '');
1279 return (undef, $var, errmsg("'%s' not a Canadian postal code", $val));
1283 my($ref,$var,$val) = @_;
1284 defined $val and $val =~ /^\s*\d{5}(?:-?\d{4})?\s*$/
1285 and return (1, $var, '');
1286 return (undef, $var, errmsg("'%s' not a US zip code", $val));
1289 *_us_postcode = \&_zip;
1292 my($ref,$var,$val) = @_;
1293 defined $val and $val =~ /\d{3}.*\d{3}/
1294 and return (1, $var, '');
1295 return (undef, $var, errmsg("'%s' not a phone number", $val));
1299 my($ref, $var,$val) = @_;
1300 if($val and $val =~ /\d{3}.*?\d{4}/) {
1301 return (1, $var, '');
1304 return (undef, $var, errmsg("'%s' not a US phone number", $val));
1308 sub _phone_us_with_area {
1309 my($ref, $var,$val) = @_;
1310 if($val and $val =~ /\d{3}\D*\d{3}\D*\d{4}/) {
1311 return (1, $var, '');
1314 return (undef, $var, errmsg("'%s' not a US phone number with area code", $val));
1318 sub _phone_us_with_area_strict {
1319 my($ref, $var,$val) = @_;
1320 if($val and $val =~ /^\d{3}-\d{3}-\d{4}$/) {
1321 return (1, $var, '');
1324 return (undef, $var,
1325 errmsg("'%s' not a US phone number with area code (strict formatting)", $val)
1331 my($ref, $var, $val) = @_;
1332 if($val and $val =~ /[\040-\077\101-\176]+\@[-A-Za-z0-9.]+\.[A-Za-z]+/) {
1333 return (1, $var, '');
1336 return (undef, $var,
1337 errmsg( "'%s' not an email address", $val )
1343 my($ref,$var,$val) = @_;
1344 return (1, $var, '')
1345 if (defined $ref->{$var} and $ref->{$var} =~ /\S/);
1346 return (undef, $var, errmsg("blank"));
1350 my($ref,$var,$val) = @_;
1351 return (1, $var, '') if is_yes($val);
1352 return (undef, $var, errmsg("false"));
1356 my($ref,$var,$val) = @_;
1357 return (1, $var, '') if is_no($val);
1358 return (undef, $var, errmsg("true"));
1362 my($ref,$var,$val) = @_;
1363 return (1, $var, '')
1364 if defined $::Values->{$var};
1365 return (undef, $var, errmsg("undefined"));
1369 my($ref,$var,$val) = @_;
1370 return (1, $var, '')
1371 if (defined $val and $val =~ /\S/);
1372 return (1, $var, '')
1373 if (defined $ref->{$var} and $ref->{$var} =~ /\S/);
1374 return (undef, $var, errmsg("blank"));
1378 my($ref, $var, $val) = @_;
1380 return (1, $var, '') if luhn($val,2);
1381 return (undef, $var, errmsg('failed the LUHN-10 check'));
1384 sub counter_number {
1385 my $file = shift || $Vend::Cfg->{OrderCounter};
1387 my $start = shift || '000000';
1389 return Vend::Interpolate::tag_counter(
1399 sub update_order_number {
1403 if($Vend::Cfg->{OrderCounter}) {
1404 $order_no = counter_number();
1407 $order_no = $Vend::SessionID . '.' . time;
1410 $::Values->{mv_order_number} = $order_no;
1414 # Places the order report in the AsciiTrack file
1416 my ($order_no,$order_report) = @_;
1418 if ($Vend::Cfg->{AsciiTrack}) {
1419 logData ($Vend::Cfg->{AsciiTrack}, <<EndOOrder);
1420 ##### BEGIN ORDER $order_no #####
1422 ##### END ORDER $order_no #####
1428 sub route_profile_check {
1432 my ($status, $final, $missing);
1433 my $value_save = { %{$::Values} };
1435 undef $SIG{__DIE__};
1436 foreach my $c (@routes) {
1437 $Vend::Interpolate::Values = $::Values = { %$value_save };
1439 my $route = $Vend::Cfg->{Route_repository}{$c}
1441 # Change to ::logDebug because of dynamic routes
1442 ::logDebug("Non-existent order route %s, skipping.", $c);
1445 if($route->{profile}) {
1446 ($status, $final, $missing) = check_order($route->{profile});
1449 "Route %s failed order profile %s. Final=%s. Errors:\n\n%s\n\n",
1464 #::logDebug("check_only -- profile=$c status=$status final=$final failed=$failed errors=$errors missing=$missing");
1465 $Vend::Interpolate::Values = $::Values = { %$value_save };
1466 return (! $failed, $final, $errors);
1470 my ($route, $save_cart, $check_only) = @_;
1471 my $main = $Vend::Cfg->{Route};
1472 return unless $main;
1473 $route = 'default' unless $route;
1475 my $cart = [ @$save_cart ];
1477 my $save_mime = $::Instance->{MIME} || undef;
1479 my $encrypt_program = $main->{encrypt_program};
1484 foreach $item (@$cart) {
1485 $shelf = { } unless $shelf;
1486 next unless $item->{mv_order_route};
1487 my(@r) = split /[\s\0,]+/, $item->{mv_order_route};
1490 $shelf->{$_} = [] unless defined $shelf->{$_};
1492 push @{$shelf->{$_}}, $item;
1497 @routes = grep !$seen{$_}++, @routes;
1498 my (@main) = grep /\S/, split /[\s\0,]+/, $route;
1501 $shelf->{$_} = [ @$cart ];
1504 # We empty @main so that we can push more routes on with cascade option
1505 push @routes, splice @main;
1515 $Vend::Session->{routes_run} = [];
1517 # Careful! If you set it on one order and not on another,
1518 # you must delete in between.
1520 my $no_increment = $check_only
1521 || $main->{no_increment}
1522 || $main->{counter_tid}
1523 || $Vend::Session->{mv_order_number};
1525 unless($no_increment) {
1526 $::Values->{mv_order_number} = counter_number(
1528 $main->{sql_counter},
1529 $main->{first_order_number},
1530 $main->{date_counter},
1534 my $value_save = { %{$::Values} };
1536 # We aren't going to allow encrypt_program setting from database as
1537 # that is a security problem
1538 my %override_key = qw/
1542 # Settable by user to indicate failure
1543 delete $::Scratch->{mv_route_failed};
1545 ## Allow setting of a master transaction route. This allows
1546 ## setting tables in transaction mode, then only committing
1547 ## once all routes have completed.
1548 my $master_transactions;
1552 foreach $c (@routes) {
1553 my $route = $Vend::Cfg->{Route_repository}{$c} || {};
1554 $main = $route if $route->{master};
1557 ## Record the routes run
1558 push @{$Vend::Session->{routes_run}}, $c;
1560 #::logDebug("route $c is: " . ::uneval($route));
1561 ##### OK, can put variables in DB all the time. It can be dynamic
1562 ##### from the database if $main->{dynamic_routes} is set. ITL only if
1563 ##### $main->{expandable}.
1565 ##### The encrypt_program key cannot be dynamic. You can set the
1566 ##### key substition value instead.
1568 if($Vend::Cfg->{RouteDatabase} and $main->{dynamic_routes}) {
1569 my $ref = tag_data( $Vend::Cfg->{RouteDatabase},
1574 #::logDebug("Read dynamic route %s from database, got: %s", $c, $ref );
1578 for(keys %override_key) {
1579 $route->{$_} = $old->{$_};
1585 ::logError("Non-existent order routing %s, skipping.", $c);
1590 if($route->{extended}) {
1591 my $ref = get_option_hash($route->{extended});
1594 #::logDebug("setting extended $_ = $ref->{$_}");
1595 $route->{$_} = $ref->{$_}
1596 unless $override_key{$_};
1602 $route->{$_} =~ s/^\s*__([A-Z]\w+)__\s*$/$::Variable->{$1}/;
1603 next unless $main->{expandable};
1604 next if $override_key{$_};
1605 next unless $route->{$_} =~ /\[/;
1606 $route->{$_} = ::interpolate_html($route->{$_});
1612 ## Make route available to subsidiary files
1613 $Vend::Session->{current_route} = $route;
1616 if($route->{cascade}) {
1617 my @extra = grep /\S/, split /[\s,\0]+/, $route->{cascade};
1619 $shelf->{$_} = [ @$cart ];
1624 if($Vend::Session->{mv_order_number}) {
1625 $value_save->{mv_order_number} =
1626 $::Values->{mv_order_number} =
1627 $Vend::Session->{mv_order_number};
1630 $Vend::Interpolate::Values = $::Values = { %$value_save };
1631 $::Values->{mv_current_route} = $c;
1633 my $credit_card_info;
1635 $Vend::Items = $shelf->{$c};
1637 Vend::Interpolate::flag( 'write', {}, $route->{write_tables})
1638 if $route->{write_tables};
1640 Vend::Interpolate::flag( 'transactions', {}, $route->{transactions})
1641 if $route->{transactions};
1646 if(! $check_only and $route->{inline_profile}) {
1649 ($status, undef, $err) = check_order($route->{inline_profile});
1650 #::logDebug("inline profile returned status=$status errors=$err");
1651 die "$err\n" unless $status;
1654 if ($CGI::values{mv_credit_card_number}) {
1655 $CGI::values{mv_credit_card_type} ||=
1656 guess_cc_type($CGI::values{mv_credit_card_number});
1657 my %attrlist = map { uc($_) => $CGI::values{$_} } keys %CGI::values;
1658 $::Values->{mv_credit_card_info} = build_cc_info(\%attrlist);
1660 elsif ($::Values->{mv_credit_card_info}) {
1661 $::Values->{mv_credit_card_info} =~ /BEGIN\s+[PG]+\s+MESSAGE/
1662 and $pre_encrypted = 1;
1665 if ($check_only and $route->{profile}) {
1667 my ($status, $final, $missing) = check_order($route->{profile});
1670 "Route %s failed order profile %s. Final=%s. Errors:\n\n%s\n\n",
1679 last PROCESS if $check_only;
1681 if($route->{payment_mode}) {
1683 $ok = Vend::Payment::charge($route->{payment_mode});
1685 die errmsg("Failed online charge for routing %s: %s",
1687 $Vend::Session->{mv_payment_error}
1691 $Vend::Session->{route_payment_id} ||= {};
1692 $Vend::Session->{route_payment_id}{$c} = $Vend::Session->{payment_id};
1695 if( $route->{credit_card}
1696 and ! $pre_encrypted
1697 and $::Values->{mv_credit_card_info}
1700 $::Values->{mv_credit_card_info} = pgp_encrypt(
1701 $::Values->{mv_credit_card_info},
1702 ($route->{pgp_cc_key} || $route->{pgp_key}),
1703 ($route->{encrypt_program} || $main->{encrypt_program} || $encrypt_program),
1707 if($route->{counter_tid}) {
1708 ## This is designed to allow order number setting in
1709 ## the report code file
1710 $Vend::Session->{mv_transaction_id} = counter_number(
1711 $route->{counter_tid},
1712 $route->{sql_counter},
1713 $route->{first_order_number},
1714 $route->{date_counter},
1717 elsif($Vend::Session->{mv_order_number}) {
1718 $::Values->{mv_order_number} = $Vend::Session->{mv_order_number};
1720 elsif(defined $route->{increment}) {
1721 $::Values->{mv_order_number} = counter_number(
1723 $main->{sql_counter},
1724 $main->{first_order_number},
1725 $main->{date_counter},
1727 if $route->{increment};
1729 elsif($route->{counter}) {
1730 $::Values->{mv_order_number} = counter_number(
1732 $route->{sql_counter},
1733 $route->{first_order_number},
1734 $route->{date_counter},
1738 # Pick up transaction ID if already set
1739 if($Vend::Session->{mv_transaction_id}) {
1740 $::Values->{mv_transaction_id} = $Vend::Session->{mv_transaction_id};
1745 if($route->{empty} and ! $route->{report}) {
1749 $pagefile = $route->{'report'} || $main->{'report'};
1750 $page = readfile($pagefile);
1752 unless (defined $page) {
1753 my $msg = errmsg("No order report %s or %s found.",
1756 ::logError("$msg\n");
1761 undef $::Instance->{MIME};
1762 if(not ($pre_encrypted || $route->{credit_card} || $route->{encrypt}) ) {
1763 unless ($::Values->{mv_credit_card_info}
1764 =~ s/^(\s*\w+\s+)(\d\d)[\d ]+(\d\d\d\d.*?)(?:\s+\d{3,4})?$/$1$2 NEED ENCRYPTION $3/) {
1765 $::Values->{mv_credit_card_info} = 'NEED ENCRYPTION';
1769 $page = interpolate_html($page) if $page;
1772 die "Error while interpolating page $pagefile:\n $@";
1774 $use_mime = $::Instance->{MIME} || undef;
1775 $::Instance->{MIME} = $save_mime || undef;
1777 if($route->{encrypt}) {
1778 $page = pgp_encrypt($page,
1780 ($route->{encrypt_program} || $main->{encrypt_program} || $encrypt_program),
1783 my ($address, $reply, $to, $subject, $template);
1784 if($route->{attach}) {
1785 $Vend::Items->[0]{mv_order_report} = $page;
1787 elsif ($route->{empty}) {
1790 elsif ($address = $route->{email}) {
1791 $address = $::Values->{$address} if $address =~ /^\w+$/;
1792 $subject = $route->{subject} || $::Values->{mv_order_subject} || 'ORDER %s';
1793 $subject =~ s/%n/%s/;
1794 $subject = sprintf "$subject", $::Values->{mv_order_number};
1795 $reply = $route->{reply} || $main->{reply};
1796 $reply = $::Values->{$reply} if $reply =~ /^\w+$/;
1797 $to = $route->{email};
1798 my $ary = [$to, $subject, $page, $reply, $use_mime];
1799 for (qw/from bcc cc/) {
1801 push @$ary, ucfirst($_) . ": $route->{$_}";
1807 die "Empty order routing $c (and not explicitly empty).\nEither attach or email are required in the route setting.\n";
1809 if ($route->{supplant}) {
1810 track_order($::Values->{mv_order_number}, $page);
1812 if ($route->{track}) {
1813 my $fn = escape_chars($route->{track});
1814 Vend::Util::writefile($fn, $page)
1815 or ::logError("route tracking error writing %s: %s", $fn, $!);
1816 my $mode = $route->{track_mode} || '';
1817 if ($mode =~ s/^0+//) {
1818 chmod oct($mode), $fn;
1824 if ($route->{individual_track}) {
1825 my $fn = Vend::Util::catfile(
1826 $route->{individual_track},
1827 $::Values->{mv_order_number} .
1828 $route->{individual_track_ext},
1830 Vend::Util::writefile( $fn, $page, )
1831 or ::logError("route tracking error writing $fn: $!");
1832 my $mode = $route->{track_mode} || '';
1833 if ($mode =~ s/^0+//) {
1834 chmod oct($mode), $fn;
1840 if($::Scratch->{mv_route_failed}) {
1841 my $msg = delete $::Scratch->{mv_route_error}
1842 || ::errmsg('Route %s failed.', $c);
1849 #::logDebug("route failed: $c");
1852 "Error during creation of order routing %s:\n%s",
1856 if ($route->{error_ok}) {
1857 push @route_complete, $c;
1860 next BUILD if $route->{continue};
1861 push @route_failed, $c;
1863 @route_done = @route_complete;
1864 @route_complete = ();
1868 push @route_complete, $c;
1872 if(@main and ! @route_failed) {
1873 @routes = splice @main;
1882 $Vend::Interpolate::Values = $::Values = $value_save;
1883 $Vend::Items = $save_cart;
1885 return (0, 0, $errors);
1887 elsif($route_checked) {
1891 return (1, undef, '');
1895 foreach $msg (@out) {
1902 "Error sending mail to %s:\n%s",
1914 $::Instance->{MIME} = $save_mime || undef;
1915 $Vend::Interpolate::Values = $::Values = $value_save;
1916 $Vend::Items = $save_cart;
1918 for(@route_failed) {
1919 my $route = $Vend::Cfg->{Route_repository}{$_};
1921 #::logDebug("checking route $_ for transactions");
1922 ## We only want to roll back the master at the end
1923 next if $route->{master};
1926 if($route->{transactions}) {
1927 #::logDebug("rolling back route $_");
1928 Vend::Interpolate::flag( 'rollback', {}, $route->{transactions})
1930 next unless $route->{rollback};
1931 Vend::Interpolate::tag_perl(
1932 $route->{rollback_tables},
1938 for(@route_complete) {
1939 my $route = $Vend::Cfg->{Route_repository}{$_};
1940 #::logDebug("checking route $_ for transactions");
1941 ## We only want to commit the master if nothing failed
1942 next if $route->{master};
1944 if($route->{transactions}) {
1945 #::logDebug("committing route $_");
1946 Vend::Interpolate::flag( 'commit', {}, $route->{transactions})
1948 next unless $route->{commit};
1949 Vend::Interpolate::tag_perl(
1950 $route->{commit_tables},
1957 delete $Vend::Session->{order_error};
1958 #::logDebug("no errors, commiting main route");
1959 if($main->{transactions}) {
1960 Vend::Interpolate::flag( 'commit', {}, $main->{transactions})
1962 if($main->{commit}) {
1963 Vend::Interpolate::tag_perl(
1964 $main->{commit_tables},
1971 if($main->{transactions}) {
1972 #::logDebug("errors, rolling back main route");
1973 Vend::Interpolate::flag( 'rollback', {}, $main->{transactions})
1975 if($main->{rollback}) {
1976 Vend::Interpolate::tag_perl(
1977 $main->{rollback_tables},
1982 $Vend::Session->{order_error} = $errors;
1983 ::logError("ERRORS on ORDER %s:\n%s", $::Values->{mv_order_number}, $errors);
1985 if ($main->{errors_to}) {
1988 errmsg("ERRORS on ORDER %s", $::Values->{mv_order_number}),
1994 # Get rid of this puppy
1995 $::Values->{mv_credit_card_info}
1996 =~ s/^(\s*\w+\s+)(\d\d)[\d ]+(\d\d\d\d)/$1$2 NEED ENCRYPTION $3/;
1998 # Clear these, we are done with them
1999 delete $Vend::Session->{mv_transaction_id};
2000 delete $Vend::Session->{current_route};
2002 # If we give a defined value, the regular mail_order routine will not
2004 #::logDebug("route errors=$errors supplant=$main->{supplant}");
2005 if($main->{supplant}) {
2006 return ($status, $::Values->{mv_order_number}, $main);
2008 return (undef, $::Values->{mv_order_number}, $main);
2016 my $code = $CGI::values{mv_arg};
2017 #::logDebug("do_order: path=$path");
2021 if($path =~ s:/(.*)::) {
2023 if($cart =~ s:/(.*)::) {
2028 if(defined $CGI::values{mv_pc} and $CGI::values{mv_pc} =~ /_(\d+)/) {
2029 $CGI::values{mv_order_quantity} = $1;
2031 $CGI::values{mv_cartname} = $cart if $cart;
2032 $CGI::values{mv_nextpage} = $page if $page;
2034 $CGI::values{mv_nextpage} = $CGI::values{mv_orderpage}
2035 || find_special_page('order')
2036 if ! $CGI::values{mv_nextpage};
2042 my @Scan_modifiers = qw/
2049 # Returns undef if interaction error
2050 sub update_quantity {
2051 return 1 unless defined $CGI::values{"quantity0"}
2052 || $CGI::values{mv_quantity_update};
2053 my ($h, $i, $quantity, $modifier, $cart, $cartname, %altered_items, %old_items);
2055 if ($CGI::values{mv_cartname}) {
2056 $cart = $::Carts->{$cartname = $CGI::values{mv_cartname}} ||= [];
2059 $cart = $Vend::Items;
2060 $cartname = $Vend::CurrentCart;
2063 my ($raise_event, $quantity_raise_event)
2064 = @{$Vend::Cfg}{qw/CartTrigger CartTriggerQuantity/};
2065 $quantity_raise_event = $raise_event && $quantity_raise_event;
2068 @mods = @{$Vend::Cfg->{UseModifier}} if $Vend::Cfg->{UseModifier};
2070 #::logDebug("adding modifiers");
2071 push(@mods, (grep $_ !~ /^mv_/, split /\0/, $CGI::values{mv_item_option}))
2072 if defined $CGI::values{mv_item_option};
2075 push @mods, grep defined $CGI::values{"${_}0"}, @Scan_modifiers;
2076 @mods = grep ! $seen{$_}++, @mods;
2078 foreach $h (@mods) {
2079 delete @{$::Values}{grep /^$h\d+$/, keys %$::Values};
2080 foreach $i (0 .. $#$cart) {
2081 #::logDebug("updating line $i modifiers: " . ::uneval($cart->[$i]));
2082 #::logDebug(qq{CGI value=$CGI::values{"$h$i"}});
2084 ! defined $CGI::values{"$h$i"}
2085 and defined $cart->[$i]{$h};
2086 $old_items{$i} ||= { %{$cart->[$i]} } if $raise_event;
2087 $modifier = $CGI::values{"$h$i"}
2088 || (defined $cart->[$i]{$h} ? '' : undef);
2089 #::logDebug("line $i modifier $h now $modifier");
2090 if (defined($modifier)) {
2091 $modifier =~ s/\0+/\0/g;
2092 $modifier =~ s/\0$//;
2093 $modifier =~ s/^\0//;
2094 $modifier =~ s/\0/, /g;
2095 $altered_items{$i} = 1
2097 and $cart->[$i]->{$h} ne $modifier;
2098 $cart->[$i]->{$h} = $modifier;
2099 $::Values->{"$h$i"} = $modifier;
2100 delete $CGI::values{"$h$i"};
2105 foreach $i (0 .. $#$cart) {
2106 #::logDebug("updating line $i quantity: " . ::uneval($cart->[$i]));
2107 my $line = $cart->[$i];
2108 $line->{mv_ip} = $i;
2109 $quantity = $CGI::values{"quantity$i"};
2110 next unless defined $quantity;
2112 my $old_item = $old_items{$i} ||= { %$line } if $raise_event;
2113 if ($quantity =~ m/^\d*$/) {
2114 $line->{'quantity'} = $quantity || 0;
2116 $altered_items{$i} = 1
2117 if $quantity_raise_event
2118 and $line->{quantity} != $old_item->{quantity};
2120 elsif ($quantity =~ m/^[\d.]+$/
2121 and $Vend::Cfg->{FractionalItems} ) {
2122 $line->{'quantity'} = $quantity;
2124 $altered_items{$i} = 1
2125 if $quantity_raise_event
2126 and $line->{quantity} != $old_item->{quantity};
2128 # This allows a last-positioned input of item quantity to
2130 elsif ($quantity =~ s/.*\00$/0/) {
2131 $CGI::values{"quantity$i"} = $quantity;
2134 # This allows a multiple input of item quantity to
2135 # pass -- FIRST ONE CONTROLS
2136 elsif ($quantity =~ s/\0.*//) {
2137 $CGI::values{"quantity$i"} = $quantity;
2141 my $item = $line->{'code'};
2142 $line->{quantity} = int $line->{quantity};
2143 $Vend::Session->{errors}{mv_order_quantity} =
2144 errmsg("'%s' for item %s is not numeric/integer", $quantity, $item);
2147 if($do_update and my $oe = $Vend::Cfg->{OptionsAttribute}) {
2149 my $loc = $Vend::Cfg->{Options_repository}{$line->{$oe}};
2150 if($loc and $loc->{item_update_routine}) {
2152 my $sub = \&{"$loc->{item_update_routine}"};
2154 $sub->($line, $loc);
2160 "error during %s (option type %s) item_update_routine: %s",
2168 $::Values->{"quantity$i"} = delete $CGI::values{"quantity$i"};
2172 last SKUSET unless $sku = delete $CGI::values{"mv_sku$i"};
2173 my @sku = split /\0/, $sku, -1;
2174 for(@sku[1..$#sku]) {
2175 if (not length $_) {
2176 $_ = $::Variable->{MV_VARIANT_JOINER} || '0';
2182 if(@sku > 1 and ! $found_option) {
2186 $sku = join "-", @sku;
2189 unless($ib = ::product_code_exists_tag($sku)) {
2190 push @{$Vend::Session->{warnings} ||= []},
2191 errmsg("Not a valid option combination: %s", $sku);
2195 $line->{mv_ib} = $ib;
2197 if($sku ne $line->{code}) {
2198 if($line->{mv_mp}) {
2199 $line->{mv_sku} = $line->{code} = $sku;
2201 elsif (! $line->{mv_sku}) {
2202 $line->{mv_sku} = $line->{code};
2203 $line->{code} = $sku;
2206 $line->{code} = $sku;
2208 $altered_items{$i} = 1 if $raise_event;
2213 Vend::Cart::trigger_update(
2215 $cart->[$_], # new item version
2216 $old_items{$_}, # old item version
2218 ) for sort { $a <=> $b } keys %altered_items;
2219 #::logDebug("after update, cart is: " . ::uneval($cart));
2221 # If the user has put in "0" for any quantity, delete that item
2222 # from the order list. Handles sub-items.
2223 Vend::Cart::toss_cart($cart, $CGI::values{mv_cartname});
2225 #::logDebug("after toss, cart is: " . ::uneval($cart));
2231 ## This routine loads AutoModifier values
2232 ## The $recalc parameter indicates it is a recalc load and not
2233 ## an initial load, so that you don't reload all parameters only ones
2234 ## that should change based on an option setting (different SKU)
2237 my ($item, $recalc) = @_;
2238 my $code = $item->{code};
2239 for my $mod (@{$Vend::Cfg->{AutoModifier}}) {
2241 my ($table,$key,$foreign) = split /:+/, $mod, 3;
2243 if($table =~ s/^!\s*//) {
2244 # This is an auto-recalculating attribute
2247 # Don't want to reload non-auto-recalculating attributes
2252 ($attr, $table) = split /\s*=\s*/, $table, 2;
2255 if(! $key and ! $foreign) {
2257 $item->{$attr} = item_common($item, $table);
2263 $table = $item->{mv_ib};
2269 my $select = $foreign ? $item->{$foreign} : $code;
2272 $item->{$attr} = ::tag_data($table, $key, $select);
2277 my($items,$quantities) = @_;
2279 $items = delete $CGI::values{mv_order_item} if ! defined $items;
2280 return unless $items;
2282 my($code,$found,$item,$base,$quantity,$i,$j,$q);
2292 if ($value = delete $Vend::Session->{scratch}{mv_UseModifier}) {
2293 $Vend::Cfg->{UseModifier} = [split /[\s\0,]+/, $value];
2296 ::update_quantity() if ! defined $CGI::values{mv_orderline};
2298 my ($cart, $cartname);
2299 if ($cartname = $CGI::values{mv_cartname}) {
2300 $cart = $::Carts->{$cartname} ||= [];
2303 $cart = $Vend::Items;
2304 $cartname = $Vend::CurrentCart;
2307 my ($raise_event,$track_quantity)
2308 = @{$Vend::Cfg}{qw/CartTrigger CartTriggerQuantity/};
2309 $raise_event = @$raise_event if ref $raise_event eq 'ARRAY';
2311 @items = split /\0/, ($items);
2312 @quantities = split /\0/, ($quantities || delete $CGI::values{mv_order_quantity} || '');
2313 @bases = split /\0/, delete $CGI::values{mv_order_mv_ib}
2314 if defined $CGI::values{mv_order_mv_ib};
2315 @lines = split /\0/, delete $CGI::values{mv_orderline}
2316 if defined $CGI::values{mv_orderline};
2318 if($CGI::values{mv_order_fly} and $Vend::Cfg->{OnFly}) {
2319 if(scalar @items == 1) {
2320 @fly = $CGI::values{mv_order_fly};
2323 @fly = split /\0/, $CGI::values{mv_order_fly};
2327 if(defined $CGI::values{mv_item_option}) {
2328 $Vend::Cfg->{UseModifier} = [] if ! $Vend::Cfg->{UseModifier};
2330 my @mods = (grep $_ !~ /^mv_/, split /\0/, $CGI::values{mv_item_option});
2331 @mods = grep ! $seen{$_}++, @mods;
2332 push @{$Vend::Cfg->{UseModifier}}, @mods;
2335 if($CGI::values{mv_sku}) {
2336 my @sku = split /\0/, $CGI::values{mv_sku};
2338 $_ = $::Variable->{MV_VARIANT_JOINER} || '0' if ! length($_);
2340 $items[0] = join '-', @sku;
2341 my $sku_field = $Vend::Cfg->{Options_repository}{Matrix}->{sku} || 'sku';
2342 $skus[0] = Vend::Data::product_field($sku_field, $items[0]);
2345 if ($Vend::Cfg->{UseModifier}) {
2346 foreach $attr (@{$Vend::Cfg->{UseModifier} || []}) {
2348 next unless defined $CGI::values{"mv_order_$attr"};
2349 @{$attr{$attr}} = split /\0/, $CGI::values{"mv_order_$attr"};
2353 my ($group, $found_master, $mv_mi, $mv_si, $mv_mp, @group, @modular);
2356 if( $CGI::values{mv_order_modular} ) {
2357 @modular = split /\0/, delete $CGI::values{mv_order_modular};
2358 for( my $i = 0; $i < @modular; $i++ ) {
2359 $attr{mv_mp}->[$i] = $modular[$i] if $modular[$i];
2364 $separate = defined $CGI::values{mv_separate_items}
2365 ? is_yes($CGI::values{mv_separate_items})
2367 $Vend::Cfg->{SeparateItems} ||
2369 defined $Vend::Session->{scratch}->{mv_separate_items}
2370 && is_yes( $Vend::Session->{scratch}->{mv_separate_items} )
2375 @group = split /\0/, (delete $CGI::values{mv_order_group} || '');
2378 for( my $i = 0; $i < @group; $i++ ) {
2379 #::logDebug("processing order group=$group[$i]");
2382 my $add = sprintf('%06d', ++$Vend::Session->{pageCount});
2383 $attr{mv_mi}->[$i] = $inc . $add;
2386 $attr{mv_mi}->[$i] = 0;
2394 foreach $code (@items) {
2396 $quantity = defined $quantities[$j] ? $quantities[$j] : 1;
2397 $set = $quantity =~ s/^=//;
2398 $quantity =~ s/^(-?)\D+/$1/;
2399 $quantity =~ s/^(-?\d*)\D.*/$1/
2400 unless $Vend::Cfg->{FractionalItems};
2401 ($j++,next) unless $quantity;
2403 $base = product_code_exists_tag($code, $bases[$j] || undef);
2408 #::logError("onfly call=$Vend::Cfg->{OnFly} ($code, $quantity, $fly[$j])");
2410 $item = Vend::Parse::do_tag($Vend::Cfg->{OnFly},
2418 "failed on-the-fly item add with error %s for: tag=%s sku=%s, qty=%s, passed=%s",
2420 $Vend::Cfg->{OnFly},
2429 my ($subname, $sub, $ret);
2431 if ($subname = $Vend::Cfg->{SpecialSub}{order_missing}) {
2432 $sub = $Vend::Cfg->{Sub}{$subname} || $Global::GlobalSub->{$subname};
2434 $ret = $sub->($code, $quantity);
2438 ::logError("Error running %s subroutine %s: %s", 'order_missing', $subname, $@);
2443 logError( "Attempt to order missing product code: %s", $code);
2450 # Check that the item has not been already ordered.
2451 # But let us order separates if so configured
2453 last INCREMENT if $separate;
2454 last INCREMENT if defined $lines[$j] and length($lines[$j]);
2456 foreach $i (0 .. $#$cart) {
2457 if ($cart->[$i]->{'code'} eq $code) {
2458 next unless $base eq $cart->[$i]->{mv_ib};
2459 next if $cart->[$i]->{mv_free_item};
2461 # Increment quantity. This is different than
2462 # the standard handling because we are ordering
2463 # accessories, and may want more than 1 of each
2464 my %old_item = %{$cart->[$i]} if $raise_event and $track_quantity;
2465 $cart->[$i]{quantity} = $set ? $quantity : $cart->[$i]{quantity} + $quantity;
2466 Vend::Cart::trigger_update(
2468 $cart->[$i], # new row
2469 \%old_item, # old row
2471 ) if $raise_event and $track_quantity;
2476 # And if not, start with a whole new line.
2477 # If mv_orderline is set, will replace a line.
2479 $item = {'code' => $code, 'quantity' => $quantity, mv_ib => $base}
2482 # Add the master item/sub item ids if appropriate
2484 #::logDebug("processing order_group");
2485 if(! $group_seen{ $group[$j] }++ ) {
2486 $item->{mv_mi} = $mv_mi = $attr{mv_mi}->[$j];
2487 #::logDebug("processing new master item=$mv_mi");
2488 $item->{mv_mp} = $mv_mp = $attr{mv_mp}->[$j];
2489 $item->{mv_si} = $mv_si = 0;
2492 $item->{mv_mi} = $mv_mi;
2493 $item->{mv_si} = ++$mv_si;
2494 #::logDebug("processing sub item=$mv_si");
2495 $item->{mv_mp} = $attr{mv_mp}->[$j] || $mv_mp;
2499 $item->{mv_sku} = $skus[$i] if defined $skus[$i];
2501 if($Vend::Cfg->{UseModifier}) {
2502 foreach $i (@{$Vend::Cfg->{UseModifier}}) {
2503 $item->{$i} = $attr{$i}->[$j];
2507 auto_modifier($item) if $Vend::Cfg->{AutoModifier};
2509 if(my $oe = $Vend::Cfg->{OptionsAttribute}) {
2511 my $loc = $Vend::Cfg->{Options_repository}{$item->{$oe}};
2512 if($loc and $loc->{item_add_routine}) {
2514 my $sub = \&{"$loc->{item_add_routine}"};
2516 $sub->($item, $loc);
2522 "error during %s (option type %s) item_add_routine: %s",
2530 if($lines[$j] =~ /^\d+$/ and defined $cart->[$lines[$j]] ) {
2531 my %old = %{$cart->[$lines[$j]]} if $raise_event;
2532 $cart->[$lines[$j]] = $item;
2533 Vend::Cart::trigger_update(
2542 $Vend::Track->add_item($cart,$item) if $Vend::Track;
2545 Vend::Cart::trigger_add(
2555 if($Vend::Cfg->{OrderLineLimit} and $#$cart >= $Vend::Cfg->{OrderLineLimit}) {
2559 "Possible bad robot. Cart limit of %s exceeded. Cart emptied.\n",
2560 $Vend::Cfg->{OrderLineLimit}
2564 Vend::Cart::toss_cart($cart, $CGI::values{mv_cartname});
2568 # Compatibility with old globalsub payment
2569 *send_mail = \&Vend::Util::send_mail;
2571 # Compatibility with old globalsub payment
2572 *map_actual = \&Vend::Payment::map_actual;