1 # Vend::Payment - Interchange payment processing routines
3 # $Id: Payment.pm,v 2.23 2009-03-20 22:15:56 markj Exp $
5 # Copyright (C) 2002-2009 Interchange Development Group
6 # Copyright (C) 1996-2002 Red Hat, Inc.
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public
19 # License along with this program; if not, write to the Free
20 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
23 package Vend::Payment;
26 $VERSION = substr(q$Revision: 2.23 $, 10);
40 use Vend::Interpolate;
45 use vars qw/$Have_LWP $Have_Net_SSLeay/;
51 configfile CYBER_CONFIGFILE
57 currency CYBER_CURRENCY
58 precision CYBER_PRECISION
62 my %ignore_mv_payment = (
69 my ($name, $value, $mode) = @_;
73 $opt = $Vend::Cfg->{Route_repository}{$mode} ||= {};
76 $opt = $pay_opt ||= {};
79 if($name =~ s/^mv_payment_//i) {
84 return $pay_opt->{$name} = $value;
87 # Find if set in route or options
88 return $opt->{$name} if defined $opt->{$name};
90 # "gateway" and possibly other future options
91 return undef if $ignore_mv_payment{$name};
93 # Now check Variable space as last resort
94 my $uname = "MV_PAYMENT_\U$name";
96 return $::Variable->{$uname} if defined $::Variable->{$uname};
97 return $::Variable->{$cyber_remap{$name}}
98 if defined $::Variable->{$cyber_remap{$name}};
102 # Do remapping of payment variables submitted by user
103 # Can be changed/extended with remap/MV_PAYMENT_REMAP
105 my ($vref, $cref) = (@_);
106 $vref = $::Values unless $vref;
107 $cref = \%CGI::values unless $cref;
148 mv_credit_card_exp_month
149 mv_credit_card_exp_year
150 mv_credit_card_number
169 cyber_mode mv_cyber_mode
174 # Allow remapping of the variable names
176 if( $remap = charge_param('remap') ) {
179 my (%remap) = split /[\s=]+/, $remap;
181 $map{$_} = $remap{$_};
189 my @billing_set = qw/
199 my @billing_ind = qw/
204 if(my $str = $::Variable->{MV_PAYMENT_BILLING_SET}) {
205 @billing_set = grep $_ !~ /\W/, split /[\s,\0]+/, $str;
207 if(my $str = $::Variable->{MV_PAYMENT_BILLING_INDICATOR}) {
208 @billing_ind = grep $_ !~ /\W/, split /[\s,\0]+/, $str;
211 @billing_set{@billing_set} = @billing_set;
213 my $no_billing_xfer = 1;
216 $no_billing_xfer = 0 unless length($vref->{$_});
219 # pick out the right values, need alternate billing address
221 foreach $key (keys %map) {
222 $actual{$key} = $vref->{$map{$key}} || $cref->{$key};
223 my $secondary = $key;
224 next unless $secondary =~ s/^b_//;
225 if ($billing_set{$key}) {
226 next if $no_billing_xfer;
227 $actual{$key} = $vref->{$secondary};
230 next if $actual{$key};
231 $actual{$key} = $vref->{$map{$secondary}} || $cref->{$map{$secondary}};
234 $actual{name} = "$actual{fname} $actual{lname}"
236 $actual{b_name} = "$actual{b_fname} $actual{b_lname}"
238 if($actual{b_address1}) {
239 $actual{b_address} = "$actual{b_address1}";
240 $actual{b_address} .= ", $actual{b_address2}"
241 if $actual{b_address2};
243 if($actual{address1}) {
244 $actual{address} = "$actual{address1}";
245 $actual{address} .= ", $actual{address2}"
246 if $actual{address2};
249 # Do some standard processing of credit card expirations
250 $actual{mv_credit_card_exp_month} =~ s/\D//g;
251 $actual{mv_credit_card_exp_month} =~ s/^0+//;
252 $actual{mv_credit_card_exp_year} =~ s/\D//g;
253 $actual{mv_credit_card_exp_year} =~ s/\d\d(\d\d)/$1/;
255 $actual{mv_credit_card_reference} = $actual{mv_credit_card_number} =~ s/\D//g;
256 $actual{mv_credit_card_reference} =~ s/^(\d\d).*(\d\d\d\d)$/$1**$2/;
258 $actual{mv_credit_card_exp_all} = sprintf(
260 $actual{mv_credit_card_exp_month},
261 $actual{mv_credit_card_exp_year},
264 $actual{cyber_mode} = charge_param('transaction')
265 || $actual{cyber_mode}
272 my $opt = shift || {};
273 if( $opt->{order_id}) {
274 # do nothing, already set
276 elsif($opt->{counter}) {
277 $opt->{order_id} = Vend::Interpolate::tag_counter(
279 { start => $opt->{counter_start} || 100000,
280 sql => $opt->{sql_counter},
285 my(@t) = gmtime(time());
286 my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = @t;
287 $opt->{order_id} = POSIX::strftime("%y%m%d%H%M%S$$", @t);
291 return $opt->{order_id};
295 my ($charge_type, $opt) = @_;
299 ### We get the payment base information from a route with the
300 ### same name as $charge_type if it is there
301 if($Vend::Cfg->{Route}) {
302 $pay_route = $Vend::Cfg->{Route_repository}{$charge_type} || {};
308 ### Then we take any payment options set in &charge, [charge ...],
311 # $pay_opt is package-scoped but lexical
312 $pay_opt = { %$pay_route };
314 $pay_opt->{$_} = $opt->{$_};
317 # We relocate these to subroutines to standardize
319 ### Maps the form variable names to the names needed by the routine
320 ### Standard names are defined ala Interchange or MV4.0x, b_name, lname,
321 ### etc. with b_varname taking precedence for these. Falls back to lname
322 ### if the b_lname is not set
323 my (%actual) = map_actual();
324 $pay_opt->{actual} = \%actual;
326 # We relocate this to a subroutine to standardize. Uses the payment
328 my $orderID = gen_order_id($pay_opt);
330 ### Set up the amounts. The {amount} key will have the currency prepended,
331 ### e.g. "usd 19.95". {total_cost} has just the cost.
333 # Uses the {currency} -> MV_PAYMENT_CURRENCY options if set
334 my $currency = charge_param('currency')
335 || ($Vend::Cfg->{Locale} && $Vend::Cfg->{Locale}{currency_code})
338 # Uses the {precision} -> MV_PAYMENT_PRECISION options if set
339 my $precision = charge_param('precision') || 2;
340 my $penny = charge_param('penny_pricing') || 0;
342 my $amount = $pay_opt->{amount} || Vend::Interpolate::total_cost();
343 $amount = round_to_frac_digits($amount, $precision);
344 $amount = sprintf "%.${precision}f", $amount;
345 $amount *= 100 if $penny;
347 $pay_opt->{total_cost} = $amount;
348 $pay_opt->{amount} = "$currency $amount";
351 ### Finish setting amounts and currency
353 # If we have a previous payment amount, delete it but push it on a stack
355 my $stack = $Vend::Session->{payment_stack} || [];
356 delete $Vend::Session->{payment_result};
357 delete $Vend::Session->{cybercash_result}; ### Deprecated
359 #::logDebug("Called charge at " . scalar(localtime));
360 #::logDebug("Charge caller is " . join(':', caller));
362 #::logDebug("mode=$pay_opt->{gateway}");
363 #::logDebug("pay_opt=" . ::uneval($pay_opt));
364 # Default to the gateway same as charge type if no gateway specified,
365 # and set the gateway in the session for logging on completion
366 if(! $opt->{gateway}) {
367 $pay_opt->{gateway} = charge_param('gateway') || $charge_type;
369 #$charge_type ||= $pay_opt->{gateway};
370 $Vend::Session->{payment_mode} = $pay_opt->{gateway};
372 # See if we are in test mode
373 $pay_opt->{test} = charge_param('test');
376 my $gw = $pay_opt->{gateway};
378 # See if we are calling a defined GlobalSub payment mode
379 my $sub = $Global::GlobalSub->{$gw};
381 # Try our predefined modes
382 if (! $sub and defined &{"Vend::Payment::$gw"} ) {
383 $sub = \&{"Vend::Payment::$gw"};
386 # This is the return from all routines
390 #::logDebug("Charge sub");
391 # Calling a defined GlobalSub payment mode
392 # Arguments are the passed option hash (if any) and the route hash
395 my $timeout = $pay_opt->{global_timeout} || charge_param('global_timeout');
400 my $pipe = IO::Pipe->new;
402 unless ($pid = fork) {
403 Vend::Server::child_process_dbi_prep();
405 my %rv = $sub->($pay_opt);
406 $pipe->print( ::uneval(\%rv) );
412 my $to_msg = $pay_opt->{global_timeout_msg}
413 || charge_param('global_timeout_msg')
414 || 'Due to technical difficulties, your order could not be processed.';
415 local $SIG{ALRM} = sub { die "$to_msg\n" };
423 my $rv = eval join ('', $pipe->getlines);
428 return $sub->($pay_opt);
433 "payment routine '%s' returned error: %s",
438 if $pid && kill (0 => $pid);
440 $result{MStatus} = 'died';
441 $result{MErrMsg} = $msg;
444 elsif($charge_type =~ /^\s*custom\s+(\w+)(?:\s+(.*))?/si) {
445 #::logDebug("Charge custom");
446 # MV4 and IC4.6.x methods
448 @args = Text::ParseWords::shellwords($2) if $2;
449 if(! defined ($sub = $Global::GlobalSub->{$1}) ) {
450 ::logError("bad custom payment GlobalSub: %s", $1);
454 %result = $sub->(@args);
458 "payment routine '%s' returned error: %s",
463 $result{MStatus} = $msg;
467 $actual{cyber_mode} =~ /^minivend_test(?:_(.*))?/
469 $charge_type =~ /^internal_test(?:[ _]+(.*))?/
472 #::logDebug("Internal test");
476 my $status = $1 || charge_param('result') || undef;
477 # Interchange test mode
478 my %payment = ( %$pay_opt );
479 &testSetServer ( %payment );
480 %result = testsendmserver(
482 'Order-ID' => $orderID,
484 'Card-Number' => $actual{mv_credit_card_number},
485 'Card-Name' => $actual{b_name},
486 'Card-Address' => $actual{b_address},
487 'Card-City' => $actual{b_city},
488 'Card-State' => $actual{b_state},
489 'Card-Zip' => $actual{b_zip},
490 'Card-Country' => $actual{b_country},
491 'Card-Exp' => $actual{mv_credit_card_exp_all},
493 $result{MStatus} = $status if defined $status;
496 #::logDebug("Unknown charge type");
497 my $msg = errmsg("Unknown charge type: %s", $charge_type);
499 $result{MStatus} = $msg;
502 push @$stack, \%result;
503 $Vend::Session->{payment_result} = \%result;
504 $Vend::Session->{payment_stack} = $stack;
506 my $svar = charge_param('success_variable') || 'MStatus';
507 my $evar = charge_param('error_variable') || 'MErrMsg';
509 if($result{$svar} !~ /^success/) {
510 $Vend::Session->{payment_error} = $result{$evar};
511 if ($result{$evar} =~ /\S/) {
512 $Vend::Session->{errors}{mv_credit_card_valid} = $result{$evar};
514 $result{'invalid-order-id'} = delete $result{'order-id'}
515 if $result{'order-id'};
517 elsif($result{$svar} =~ /success-duplicate/) {
518 $Vend::Session->{payment_error} = $result{$evar};
519 $result{'invalid-order-id'} = delete $result{'order-id'}
520 if $result{'order-id'};
523 delete $Vend::Session->{payment_error};
526 $Vend::Session->{payment_id} = $result{'order-id'};
528 my $encrypt = charge_param('encrypt');
530 if($encrypt and $CGI::values{mv_credit_card_number} and $Vend::Cfg->{EncryptKey}) {
531 my $prog = charge_param('encrypt_program') || $Vend::Cfg->{EncryptProgram};
532 if($prog =~ /pgp|gpg/) {
533 $CGI::values{mv_credit_card_force} = 1;
536 $::Values->{mv_credit_card_info},
537 $::Values->{mv_credit_card_exp_month},
538 $::Values->{mv_credit_card_exp_year},
539 $::Values->{mv_credit_card_exp_all},
540 $::Values->{mv_credit_card_type},
541 $::Values->{mv_credit_card_error}
542 ) = encrypt_standard_cc(\%CGI::values);
546 "Order id for charge type %s: %s",
548 $Vend::Session->{cybercash_id},
550 if $pay_opt->{log_to_error};
553 for(qw/ id error result /) {
554 $Vend::Session->{"cybercash_$_"} = $Vend::Session->{"payment_$_"};
557 return \%result if $pay_opt->{hash};
558 return $result{'order-id'};
564 for(sort keys %options) {
565 $out .= "$_=$options{$_}\n";
567 logError("Test CyberCash SetServer:\n%s\n" , $out);
571 sub testsendmserver {
572 my ($type, %options) = @_;
573 my $out ="type=$type\n";
574 for(sort keys %options) {
575 $out .= "$_=$options{$_}\n";
577 logError("Test CyberCash sendmserver:\n$out\n");
580 $oid = Vend::Interpolate::tag_counter(
581 "$Vend::Cfg->{ScratchDir}/internal_test.payment.number"
584 return ('MStatus', 'success', 'order-id', $oid || 'COUNTER_FAILED');
588 my ($opt, $query) = @_;
590 unless ($opt->{use_wget} or $Have_Net_SSLeay or $Have_LWP) {
591 die "No Net::SSLeay or Crypt::SSLeay found.\n";
594 my $submit_url = $opt->{submit_url};
596 my $port = $opt->{port} || 443;
598 my $protocol = $opt->{protocol} || 'https';
600 $server = $submit_url;
601 $server =~ s{^https://}{}i;
602 $server =~ s{(/.*)}{};
603 $port = $1 if $server =~ s/:(\d+)$//;
606 elsif ($opt->{host}) {
607 $server = $opt->{host};
608 $script = $opt->{script};
609 $script =~ s:^([^/]):/$1:;
610 $submit_url = join "",
614 ($port ? ":$port" : ''),
618 my %header = ( 'User-Agent' => "Vend::Payment (Interchange version $::VERSION)");
619 if($opt->{extra_headers}) {
620 for(keys %{$opt->{extra_headers}}) {
621 $header{$_} = $opt->{extra_headers}{$_};
626 if($opt->{use_wget}) {
627 ## Don't worry about OS independence with UNIX wget
628 my $bdir = "$Vend::Cfg->{ScratchDir}/wget";
633 my $msg = "Failed to create directory %s: %s";
634 $msg = errmsg($msg, $bdir, $!);
640 my $filebase = "$Vend::SessionID.wget";
641 my $statfile = Vend::File::get_filename("$filebase.stat", 1, 1, $bdir);
642 my $outfile = Vend::File::get_filename("$filebase.out", 1, 1, $bdir);
643 my $infile = Vend::File::get_filename("$filebase.in", 1, 1, $bdir);
644 my $cmd = $opt->{use_wget} =~ m{/} ? $opt->{use_wget} : 'wget';
647 while( my ($k,$v) = each %$query ) {
652 my $post = join "&", @post;
653 open WIN, "> $infile"
654 or die errmsg("Cannot create wget post input file %s: %s", $infile, $!) . "\n";
659 push @args, "--output-file=$statfile";
660 push @args, "--output-document=$outfile";
661 push @args, "--server-response";
662 push @args, "--post-file=$infile";
663 push @args, $submit_url;
665 #::logDebug("wget cmd line: " . join(" ", @args));
667 $result{reply_os_error} = $!;
668 $result{reply_os_status} = $?;
669 $result{result_page} = 'FAILED';
672 #::logDebug("wget finished.");
673 open WOUT, "< $outfile"
674 or die errmsg("Cannot read wget output from %s: %s", $outfile, $!) . "\n";
675 $result{result_page} = <WOUT>;
677 or die errmsg("Cannot close wget output %s: %s", $outfile, $!) . "\n";
678 unlink $outfile unless $opt->{debug};
682 or die errmsg("Cannot seek on wget input file %s: %s", $infile, $!) . "\n";
683 unless($opt->{debug}) {
684 my $len = int(length($post) / 8) + 1;
685 print WIN 'deadbeef' x $len;
689 or die errmsg("Cannot close wget post input file %s: %s", $infile, $!) . "\n";
690 unlink $infile unless $opt->{debug};
691 open WSTAT, "< $statfile"
692 or die errmsg("Cannot read wget status from %s: %s", $statfile, $!) . "\n";
695 or die errmsg("Cannot close wget status %s: %s", $statfile, $!) . "\n";
697 unlink $statfile unless $opt->{debug};
698 $result{wget_output} = $err;
699 $err =~ s/.*HTTP\s+request\s+sent,\s+awaiting\s+response[.\s]*//s;
700 my @raw = split /\r?\n/, $err;
707 $result{status_line} = shift @head;
708 $result{status_line} =~ /^HTTP\S+\s+(\d+)/
709 and $result{response_code} = $1;
710 $result{header_string} = join "\n", @head;
712 elsif($opt->{use_net_ssleay} or ! $opt->{use_crypt_ssl} && $Have_Net_SSLeay) {
713 #::logDebug("placing Net::SSLeay request: host=$server, port=$port, script=$script");
714 #::logDebug("values: " . ::uneval($query) );
715 my ($page, $response, %reply_headers)
717 $server, $port, $script,
718 make_headers( %header ),
719 make_form( %$query ),
721 my $header_string = '';
723 for(keys %reply_headers) {
724 $header_string .= "$_: $reply_headers{$_}\n";
726 #::logDebug("received Net::SSLeay header: $header_string");
727 $result{status_line} = $response;
728 $result{status_line} =~ /^HTTP\S+\s+(\d+)/
729 and $result{response_code} = $1;
730 $result{header_string} = $header_string;
731 $result{result_page} = $page;
734 my @query = %{$query};
735 my $ua = new LWP::UserAgent;
736 my $req = POST($submit_url, \@query, %header);
737 #::logDebug("placing LWP request: " . ::uneval_it($req) );
738 my $resp = $ua->request($req);
739 $result{status_line} = $resp->status_line();
740 $result{status_line} =~ /(\d+)/
741 and $result{response_code} = $1;
742 $result{header_string} = $resp->as_string();
743 $result{header_string} =~ s/\r?\n\r?\n.*//s;
744 #::logDebug("received LWP header: $header_string");
745 $result{result_page} = $resp->content();
747 #::logDebug("returning thing: " . ::uneval(\%result) );