From: Mark Wells Date: Wed, 16 Nov 2016 19:58:33 +0000 (-0800) Subject: Merge branch 'master' of git.freeside.biz:/home/git/freeside X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=d8844f49839b56eac11ee931f730d6e47f1ef628;hp=5b2872a065cd65c823e9056b9153e4548ee7c201 Merge branch 'master' of git.freeside.biz:/home/git/freeside --- diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 091d6ac68..4a878f8d2 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -401,20 +401,12 @@ sub payment_gateway { my $conf = new FS::Conf; my $cust_main = shift; my $cust_payby = shift; - my $gatewaynum = $conf->config('selfservice-payment_gateway'); - if ( $gatewaynum ) { - my $pg = qsearchs('payment_gateway', { gatewaynum => $gatewaynum }); - die "configured gatewaynum $gatewaynum not found!" if !$pg; - return $pg; - } - else { - return '' if ! FS::payby->realtime($cust_payby); - my $pg = $cust_main->agent->payment_gateway( - 'method' => FS::payby->payby2bop($cust_payby), - 'nofatal' => 1 - ); - return $pg; - } + return '' if ! FS::payby->realtime($cust_payby); + my $pg = $cust_main->agent->payment_gateway( + 'method' => FS::payby->payby2bop($cust_payby), + 'nofatal' => 1 + ); + return $pg; } sub access_info { diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm index e11a47a06..5ced42b2a 100644 --- a/FS/FS/ClientAPI/Signup.pm +++ b/FS/FS/ClientAPI/Signup.pm @@ -344,20 +344,11 @@ sub signup_info { my @paybys = @{ $signup_info->{'payby'} }; $signup_info->{'hide_payment_fields'} = []; - my $gatewaynum = $conf->config('selfservice-payment_gateway'); - my $force_gateway; - if ( $gatewaynum ) { - $force_gateway = qsearchs('payment_gateway', { gatewaynum => $gatewaynum }); - warn "using forced gateway #$gatewaynum - " . - $force_gateway->gateway_username . '@' . $force_gateway->gateway_module - if $DEBUG > 1; - die "configured gatewaynum $gatewaynum not found!" if !$force_gateway; - } foreach my $payby (@paybys) { warn "$me checking $payby payment fields\n" if $DEBUG > 1; my $hide = 0; if ( FS::payby->realtime($payby) ) { - my $gateway = $force_gateway || + my $gateway = $agent->payment_gateway( 'method' => FS::payby->payby2bop($payby), 'nofatal' => 1, ); @@ -627,17 +618,9 @@ sub new_customer { return { 'error' => "Unknown reseller" } unless $agent; - my $gw; - my $gatewaynum = $conf->config('selfservice-payment_gateway'); - if ( $gatewaynum ) { - $gw = qsearchs('payment_gateway', { gatewaynum => $gatewaynum }); - die "configured gatewaynum $gatewaynum not found!" if !$gw; - } - else { - $gw = $agent->payment_gateway( 'method' => FS::payby->payby2bop($payby), - 'nofatal' => 1, + my $gw = $agent->payment_gateway( 'method' => FS::payby->payby2bop($payby), + 'nofatal' => 1, ); - } $cust_main->payby('BILL') # MCRD better? no, that's for something else if $gw && $gw->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; @@ -1120,36 +1103,28 @@ sub capture_payment { my $conf = new FS::Conf; - my $payment_gateway; - if ( my $gwnum = $conf->config('selfservice-payment_gateway') ) { - $payment_gateway = qsearchs('payment_gateway', { 'gatewaynum' => $gwnum }) - or die "configured gatewaynum $gwnum not found!"; - } - else { - my $url = $packet->{url}; - - $payment_gateway = qsearchs('payment_gateway', + my $url = $packet->{url}; + my $payment_gateway = qsearchs('payment_gateway', { 'gateway_callback_url' => popurl(0, $url) } ); - if (!$payment_gateway) { - - my ( $processor, $login, $password, $action, @bop_options ) = - $conf->config('business-onlinepayment'); - $action ||= 'normal authorization'; - pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/; - die "No real-time processor is enabled - ". - "did you set the business-onlinepayment configuration value?\n" - unless $processor; - - $payment_gateway = new FS::payment_gateway( { - gateway_namespace => $conf->config('business-onlinepayment-namespace'), - gateway_module => $processor, - gateway_username => $login, - gateway_password => $password, - gateway_action => $action, - options => [ ( @bop_options ) ], - }); - } + if (!$payment_gateway) { + + my ( $processor, $login, $password, $action, @bop_options ) = + $conf->config('business-onlinepayment'); + $action ||= 'normal authorization'; + pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/; + die "No real-time processor is enabled - ". + "did you set the business-onlinepayment configuration value?\n" + unless $processor; + + $payment_gateway = new FS::payment_gateway( { + gateway_namespace => $conf->config('business-onlinepayment-namespace'), + gateway_module => $processor, + gateway_username => $login, + gateway_password => $password, + gateway_action => $action, + options => [ ( @bop_options ) ], + }); } die "No real-time third party processor is enabled - ". diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 51af38bb6..ea1d391b1 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -770,6 +770,13 @@ my $validate_email = sub { $_[0] =~ }, { + 'key' => 'no_saved_cardnumbers', + 'section' => 'credit_cards', + 'description' => 'Do not allow credit card numbers to be written to the database. Prevents realtime processing unless payment gateway supports tokenization.', + 'type' => 'checkbox', + }, + + { 'key' => 'credit-card-surcharge-percentage', 'section' => 'credit_cards', 'description' => 'Add a credit card surcharge to invoices, as a % of the invoice total. WARNING: Although recently permitted to US merchants in general, specific consumer protection laws may prohibit or restrict this practice in California, Colorado, Connecticut, Florda, Kansas, Maine, Massachusetts, New York, Oklahome, and Texas. Surcharging is also generally prohibited in most countries outside the US, AU and UK. When allowed, typically not permitted to be above 4%.', @@ -2185,8 +2192,8 @@ and customer address. Include units.', { 'key' => 'selfservice-payment_gateway', - 'section' => 'self-service', - 'description' => 'Force the use of this payment gateway for self-service.', + 'section' => 'deprecated', + 'description' => '(no longer supported) Force the use of this payment gateway for self-service.', %payment_gateway_options, }, diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm index fc234334d..c102e7be8 100644 --- a/FS/FS/agent.pm +++ b/FS/FS/agent.pm @@ -265,24 +265,14 @@ sub payment_gateway { my $conf = new FS::Conf; if ( $options{thirdparty} ) { - # still a kludge, but it gets the job done - # and the 'cardtype' semantics don't really apply to thirdparty - # gateways because we have to choose a gateway without ever - # seeing the card number - my $gatewaynum = - $conf->config('selfservice-payment_gateway', $self->agentnum); - my $gateway; - $gateway = FS::payment_gateway->by_key($gatewaynum) if $gatewaynum; - return $gateway if $gateway; - - # a little less kludgey than the above, and allows PayPal to coexist - # with credit card gateways + + # allows PayPal to coexist with credit card gateways my $is_paypal = { op => '!=', value => 'PayPal' }; if ( uc($options{method}) eq 'PAYPAL' ) { $is_paypal = 'PayPal'; } - $gateway = qsearchs({ + my $gateway = qsearchs({ table => 'payment_gateway', addl_from => ' JOIN agent_payment_gateway USING (gatewaynum) ', hashref => { diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 3f3b222c0..c008c2dd3 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -226,14 +226,6 @@ sub _bop_recurring_billing { sub _payment_gateway { my ($self, $options) = @_; - if ( $options->{'selfservice'} ) { - my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway'); - if ( $gatewaynum ) { - return $options->{payment_gateway} ||= - qsearchs('payment_gateway', { gatewaynum => $gatewaynum }); - } - } - if ( $options->{'fake_gatewaynum'} ) { $options->{payment_gateway} = qsearchs('payment_gateway', @@ -421,15 +413,17 @@ sub realtime_bop { # possibly run a separate transaction to tokenize card number, # so that we never store tokenized card info in cust_pay_pending - if (!$self->tokenized($options{'payinfo'})) { + if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) { my $token_error = $self->realtime_tokenize(\%options); return $token_error if $token_error; # in theory, all cust_payby will be tokenized during original save, # so we shouldn't get here with opt cust_payby...but just in case... - if ($options{'cust_payby'}) { + if ($options{'cust_payby'} && $self->tokenized($options{'payinfo'})) { $token_error = $options{'cust_payby'}->replace; return $token_error if $token_error; } + return "Cannot tokenize card info" + if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'}); } ### @@ -1773,11 +1767,13 @@ sub realtime_verify_bop { # possibly run a separate transaction to tokenize card number, # so that we never store tokenized card info in cust_pay_pending - if (!$self->tokenized($options{'payinfo'})) { + if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) { my $token_error = $self->realtime_tokenize(\%options); return $token_error if $token_error; #important that we not replace cust_payby here, #because cust_payby->replace uses realtime_verify_bop! + return "Cannot tokenize card info" + if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'}); } ### @@ -2224,6 +2220,8 @@ sub realtime_tokenize { $self->_bop_options(\%options), ); + return '' unless $transaction->can('info'); + my %supported_actions = $transaction->info('supported_actions'); return '' unless $supported_actions{'CC'} && grep /^Tokenize$/, @{$supported_actions{'CC'}}; @@ -2326,12 +2324,205 @@ sub tokenized { FS::cust_pay->tokenized($payinfo); } +=item remove_card_numbers + +NOT AN OBJECT METHOD. Acts on all customers. Placed here because it makes +use of module-internal methods, and to keep everything that uses +Billing::OnlinePayment all in one place. + +Removes all stored card numbers from payinfo in cust_payby and +CARD transactions in cust_pay_pending, cust_pay, cust_pay_void and cust_refund. +Will fail if cust_payby records can't be tokenized. Transaction records that +cannot be tokenized will have their payinfo replaced with their paymask. + +THIS WILL OVERWRITE STORED PAYINFO ON OLD TRANSACTIONS. + +If the gateway originally used for the transaction can't tokenize, this may +prevent the transaction from being voided or refunded. Hence, it should +not (yet) be run as part of a regular upgrade. This is only intended to be +run on systems with current gateways that tokenize, after the window has +passed for voiding/refunding transactions from previous gateways, in order +to remove all real card numbers from the system. + +Also sets the no_saved_cardnumbers conf, to keep things this way. + +=cut + +# ??? probably should add MCRD handling to this + +sub remove_card_numbers { + # no input, always does the same thing + + my $cache = {}; #cache for module info + + eval "use FS::Cursor"; + return "Error initializing FS::Cursor: ".$@ if $@; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + # turn this on + $conf->touch('no_saved_cardnumbers'); + + ### Tokenize cust_payby + + my $cust_search = FS::Cursor->new({ table => 'cust_main' },$dbh); + while (my $cust_main = $cust_search->fetch) { + foreach my $cust_payby ($cust_main->cust_payby('CARD','DCRD')) { + next if $cust_payby->tokenized; + # load gateway first, just so we can cache it + my $payment_gateway = $cust_main->_payment_gateway({ + 'payinfo' => $cust_payby->payinfo, # for cardtype agent overrides + 'nofatal' => 1, # handle error smoothly below + # invnum -- XXX need to figure out how to handle taxclass overrides + }); + unless ($payment_gateway) { + $cust_search->DESTROY; + $dbh->rollback if $oldAutoCommit; + return "No gateway found for custnum ".$cust_main->custnum; + } + my $info = $cust_main->_remove_card_numbers_gateway_info($cache,$payment_gateway); + unless (ref($info) && $info->{'can_tokenize'}) { + $cust_search->DESTROY; + $dbh->rollback if $oldAutoCommit; + my $error = ref($info) + ? "Gateway ".$payment_gateway->gatewaynum." cannot tokenize, for custnum ".$cust_main->custnum + : $info; + return $error; + } + my %tokenopts = ( + 'payment_gateway' => $payment_gateway, + 'cust_payby' => $cust_payby, + ); + my $error = $cust_main->realtime_tokenize(\%tokenopts); + if ($cust_payby->tokenized) { # implies no error + $error = $cust_payby->replace; + } else { + $error = 'Unknown error'; + } + if ($error) { + $cust_search->DESTROY; + $dbh->rollback if $oldAutoCommit; + return "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error; + } + } + } + + ### Tokenize/mask transaction tables + + # grep assistance: + # $cust_pay_pending->replace, $cust_pay->replace, $cust_pay_void->replace, $cust_refund->replace all run here + foreach my $table ( qw(cust_pay_pending cust_pay cust_pay_void cust_refund) ) { + my $search = FS::Cursor->new({ + table => $table, + hashref => { 'payby' => 'CARD' }, + },$dbh); + while (my $record = $search->fetch) { + next if $record->tokenized; + next if !$record->payinfo; #shouldn't happen, but just in case, no need to mask + next if $record->payinfo =~ /N\/A/; # ??? Not sure what's up with these, but no need to mask + next if $record->payinfo eq $record->paymask; #already masked + my $old_gateway; + if (my $old_gatewaynum = $record->gatewaynum) { + $old_gateway = + qsearchs('payment_gateway',{ 'gatewaynum' => $old_gatewaynum, }); + # not erring out if gateway can't be found, just use paymask + } + # first try to tokenize + my $cust_main = $record->cust_main; + if ($cust_main && $old_gateway) { + my $info = $cust_main->_remove_card_numbers_gateway_info($cache,$old_gateway); + unless (ref($info)) { + # only throws error if Business::OnlinePayment won't load, + # which is just cause to abort this whole process + $search->DESTROY; + $dbh->rollback if $oldAutoCommit; + return $info; + } + if ($info->{'can_tokenize'}) { + my %tokenopts = ( + 'payment_gateway' => $old_gateway, + 'method' => 'CC', + 'payinfo' => $record->payinfo, + 'paydate' => $record->paydate, + ); + my $error = $cust_main->realtime_tokenize(\%tokenopts); + if ($cust_main->tokenized($tokenopts{'payinfo'})) { # implies no error + $record->payinfo($tokenopts{'payinfo'}); + $error = $record->replace; + } else { + $error = 'Unknown error'; + } + if ($error) { + $search->DESTROY; + $dbh->rollback if $oldAutoCommit; + return "Error tokenizing $table ".$record->get($record->primary_key).": ".$error; + } + next; + } + } + # can't tokenize, so just replace with paymask + $record->set('payinfo',$record->paymask); #deliberately evade ->payinfo() remasking effects + my $error = $record->replace; + if ($error) { + $search->DESTROY; + $dbh->rollback if $oldAutoCommit; + return "Error masking payinfo for $table ".$record->get($record->primary_key).": ".$error; + } + } + } + + $dbh->commit if $oldAutoCommit; + + return ''; +} + +sub _remove_card_numbers_gateway_info { + my ($self,$cache,$payment_gateway) = @_; + + return $cache->{$payment_gateway->gateway_module} + if $cache->{$payment_gateway->gateway_module}; + + my $info = {}; + $cache->{$payment_gateway->gateway_module} = $info; + + my $namespace = $payment_gateway->gateway_namespace; + return $info unless $namespace eq 'Business::OnlinePayment'; + $info->{'is_bop'} = 1; + + # only need to load this once, + # don't want to load if nothing is_bop + unless ($cache->{'Business::OnlinePayment'}) { + eval "use $namespace"; + return "Error initializing Business:OnlinePayment: ".$@ if $@; + $cache->{'Business::OnlinePayment'} = 1; + } + + my $transaction = new $namespace( $payment_gateway->gateway_module, + $self->_bop_options({ 'payment_gateway' => $payment_gateway }), + ); + + return $info unless $transaction->can('info'); + $info->{'can_info'} = 1; + + my %supported_actions = $transaction->info('supported_actions'); + $info->{'can_tokenize'} = 1 + if $supported_actions{'CC'} + && grep /^Tokenize$/, @{$supported_actions{'CC'}}; + + $info->{'void_requires_card'} = 1 + if $transaction->info('CC_void_requires_card'); + + $cache->{$payment_gateway->gateway_module} = $info; + + return $info; +} + =back =head1 BUGS -Not autoloaded. - =head1 SEE ALSO L, L diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index dfcce2ffc..7a3dcf0e7 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -194,6 +194,8 @@ sub payinfo_check { FS::payby->can_payby($self->table, $self->payby) or return "Illegal payby: ". $self->payby; + my $conf = new FS::Conf; + if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) { my $payinfo = $self->payinfo; @@ -212,8 +214,10 @@ sub payinfo_check { $self->payinfo($1); validate($self->payinfo) or return "Illegal credit card number"; return "Unknown card type" if $cardtype eq "Unknown"; + return "Card number not tokenized" + if $conf->exists('no_saved_cardnumbers') && !$self->tokenized; } else { - $self->payinfo('N/A'); #??? + $self->payinfo('N/A'); #??? re-masks card } } } else { diff --git a/FS/FS/payinfo_transaction_Mixin.pm b/FS/FS/payinfo_transaction_Mixin.pm index 50659ac1e..6e4b511d2 100644 --- a/FS/FS/payinfo_transaction_Mixin.pm +++ b/FS/FS/payinfo_transaction_Mixin.pm @@ -102,7 +102,11 @@ auth, and order_number) as well as payby and payinfo sub payinfo_check { my $self = shift; - # All of these can be null, so in principle this could go in payinfo_Mixin. + my $conf = new FS::Conf; + + # allow masked payinfo if we never save card numbers + local $FS::payinfo_Mixin::ignore_masked_payinfo = + $conf->exists('no_saved_cardnumbers') ? 1 : $FS::payinfo_Mixin::ignore_masked_payinfo; $self->SUPER::payinfo_check() || $self->ut_numbern('gatewaynum') diff --git a/bin/bulk_void b/bin/bulk_void index a1428180e..8f0c882a8 100755 --- a/bin/bulk_void +++ b/bin/bulk_void @@ -1,9 +1,13 @@ #!/usr/bin/perl +use strict; +use warnings; +use vars qw( %opt ); use FS::Misc::Getopt; use FS::Record qw(qsearch qsearchs dbh); -getopts('cpifXr:'); +getopts('cpiXr:t:'); + my $dbh = dbh; $FS::UID::AutoCommit = 0; @@ -11,11 +15,13 @@ sub usage() { "Usage: bulk_void -s start -e end -r void_reason { -c | -p | -i } + [ -t payby ] [ -X ] -s, -e: date range (required) -r: void reason text (required) --c, -p, -i, -f: void credits, payments, invoices +-c, -p, -i: void credits, payments, invoices +-t: only void payments with this payby -X: commit changes "; } @@ -26,7 +32,11 @@ if (!$opt{start} or !$opt{end} or !$opt{r}) { print "DRY RUN--changes will not be committed.\n" unless $opt{X}; -my $date = " WHERE _date >= $opt{start} AND _date <= $opt{end}"; +my %search = (); +$search{payby} = $opt{t} if $opt{t} && $opt{p}; + +my $date = (keys %search ? ' AND ' : ' WHERE '). + " _date >= $opt{start} AND _date <= $opt{end}"; my %tables = ( c => 'cust_credit', @@ -45,6 +55,7 @@ foreach my $k (keys %tables) { my $cursor = FS::Cursor->new({ table => $table, + hashref => \%search, extra_sql => $date, }); my $error;