From 3676ccddeab727d3d7a929b1fe4fe19d81e0e8c0 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 22 Nov 2016 18:40:39 -0600 Subject: 71513: Card tokenization [cust_pay_pending handling, bug fixes] --- FS/FS/Upgrade.pm | 2 +- FS/FS/agent_payment_gateway.pm | 1 + FS/FS/cust_main/Billing_Realtime.pm | 92 ++++++++++++++++++++++++------------- 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index 0113bf92a..940ae2844 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -49,7 +49,7 @@ sub upgrade_config { # to simplify tokenization upgrades die "Conf selfservice-payment_gateway no longer supported" - if conf->config('selfservice-payment_gateway'); + if $conf->config('selfservice-payment_gateway'); $conf->touch('payment_receipt') if $conf->exists('payment_receipt_email') diff --git a/FS/FS/agent_payment_gateway.pm b/FS/FS/agent_payment_gateway.pm index 4991c1912..6a7cc06d1 100644 --- a/FS/FS/agent_payment_gateway.pm +++ b/FS/FS/agent_payment_gateway.pm @@ -1,5 +1,6 @@ package FS::agent_payment_gateway; use base qw(FS::Record); +use FS::Record qw( qsearch ); use strict; diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index d57be11ab..3757ca814 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -223,6 +223,7 @@ sub _bop_recurring_billing { } +#can run safely as class method if opt payment_gateway already exists sub _payment_gateway { my ($self, $options) = @_; @@ -239,8 +240,9 @@ sub _payment_gateway { $options->{payment_gateway}; } +# not a method!!! sub _bop_auth { - my ($self, $options) = @_; + my ($options) = @_; ( 'login' => $options->{payment_gateway}->gateway_username, @@ -282,8 +284,9 @@ sub _bop_defaults { } +# not a method! sub _bop_cust_payby_options { - my ($self,$options) = @_; + my ($options) = @_; my $cust_payby = $options->{'cust_payby'}; if ($cust_payby) { @@ -319,6 +322,8 @@ sub _bop_cust_payby_options { } } +# can be called as class method, +# but can't load default name/phone fields as class method sub _bop_content { my ($self, $options) = @_; my %content = (); @@ -339,16 +344,16 @@ sub _bop_content { /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ or return "Illegal payname $payname"; ($payfirst, $paylast) = ($1, $2); - } else { + } elsif (ref($self)) { # can't set payname if called as class method $payfirst = $self->getfield('first'); $paylast = $self->getfield('last'); $payname = "$payfirst $paylast"; } - $content{last_name} = $paylast; - $content{first_name} = $payfirst; + $content{last_name} = $paylast if $paylast; + $content{first_name} = $payfirst if $payfirst; - $content{name} = $payname; + $content{name} = $payname if $payname; $content{address} = $options->{'address1'}; my $address2 = $options->{'address2'}; @@ -359,7 +364,9 @@ sub _bop_content { $content{zip} = $options->{'zip'}; $content{country} = $options->{'country'}; - $content{phone} = $self->daytime || $self->night; + # can't set phone if called as class method + $content{phone} = $self->daytime || $self->night + if ref($self); my $currency = $conf->exists('business-onlinepayment-currency') && $conf->config('business-onlinepayment-currency'); @@ -369,6 +376,7 @@ sub _bop_content { } # updates payinfo and cust_payby options with token from transaction +# can be called as a class method sub _tokenize_card { my ($self,$transaction,$options) = @_; if ( $transaction->can('card_token') @@ -410,7 +418,7 @@ sub realtime_bop { } # set fields from passed cust_payby - $self->_bop_cust_payby_options(\%options); + _bop_cust_payby_options(\%options); # possibly run a separate transaction to tokenize card number, # so that we never store tokenized card info in cust_pay_pending @@ -698,7 +706,7 @@ sub realtime_bop { $transaction->content( 'type' => $options{method}, - $self->_bop_auth(\%options), + _bop_auth(\%options), 'action' => $action1, 'description' => $options{'description'}, 'amount' => $options{amount}, @@ -760,7 +768,7 @@ sub realtime_bop { %content, type => $options{method}, action => $action2, - $self->_bop_auth(\%options), + _bop_auth(\%options), order_number => $ordernum, amount => $options{amount}, authorization => $auth, @@ -1291,7 +1299,7 @@ sub realtime_botpp_capture { $transaction->content( 'type' => $method, - $self->_bop_auth(\%options), + _bop_auth(\%options), 'action' => 'Post Authorization', 'description' => $options{'description'}, 'amount' => $cust_pay_pending->paid, @@ -1764,7 +1772,7 @@ sub realtime_verify_bop { # set fields from passed cust_payby return "No cust_payby" unless $options{'cust_payby'}; - $self->_bop_cust_payby_options(\%options); + _bop_cust_payby_options(\%options); # possibly run a separate transaction to tokenize card number, # so that we never store tokenized card info in cust_pay_pending @@ -1911,7 +1919,7 @@ sub realtime_verify_bop { $transaction->content( 'type' => 'CC', - $self->_bop_auth(\%options), + _bop_auth(\%options), 'action' => 'Authorization Only', 'description' => $options{'description'}, 'amount' => '1.00', @@ -1958,7 +1966,7 @@ sub realtime_verify_bop { ); $reverse->content( 'action' => 'Reverse Authorization', - $self->_bop_auth(\%options), + _bop_auth(\%options), # B:OP 'amount' => '1.00', @@ -2177,8 +2185,13 @@ Otherwise, options I, I and other cust_payby fields may be passed. If options are passed as a hashref, I will be updated as appropriate in the passed hashref. +Can be run as a class method if option I is passed, +but default customer id/name/phone can't be set in that case. This +is really only intended for tokenizing old records on upgrade. + =cut +# careful--might be run as a class method sub realtime_tokenize { my $self = shift; @@ -2196,7 +2209,7 @@ sub realtime_tokenize { } # set fields from passed cust_payby - $self->_bop_cust_payby_options(\%options); + _bop_cust_payby_options(\%options); return '' unless $options{method} eq 'CC'; return '' if $self->tokenized($options{payinfo}); #already tokenized @@ -2241,6 +2254,11 @@ sub realtime_tokenize { # massage data ### + ### Currently, cardfortress only keys in on card number and exp date. + ### We pass everything we'd pass to a normal transaction, + ### for ease of current and future development, + ### but note, when tokenizing old records, we may only have access to payinfo/paydate + my $bop_content = $self->_bop_content(\%options); return $bop_content unless ref($bop_content); @@ -2264,6 +2282,9 @@ sub realtime_tokenize { my $payissue = $options{'payissue'}; $content{issue_number} = $payissue if $payissue; + $content{customer_id} = $self->custnum + if ref($self); + ### # run transaction ### @@ -2274,10 +2295,9 @@ sub realtime_tokenize { $transaction->content( 'type' => 'CC', - $self->_bop_auth(\%options), + _bop_auth(\%options), 'action' => 'Tokenize', - 'description' => $options{'description'}, - 'customer_id' => $self->custnum, + 'description' => $options{'description'} %$bop_content, %content, #after ); @@ -2315,7 +2335,9 @@ sub realtime_tokenize { Convenience wrapper for L -PAYINFO is required +PAYINFO is required. + +Can be run as class or object method, never loads from object. =cut @@ -2422,6 +2444,9 @@ sub token_check { ### Tokenize/mask transaction tables + # allow tokenization of closed cust_pay/cust_refund records + local $FS::payinfo_Mixin::allow_closed_replace = 1; + # 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) ) { @@ -2457,34 +2482,35 @@ sub token_check { next unless $info->{'can_tokenize'}; my $cust_main = $record->cust_main; - unless ($cust_main) { - # might happen for cust_pay_pending for failed verify records, - # in which case it *should* already be tokenized if possible - # but only get strict about it if we're expecting full tokenization - next if - $table eq 'cust_pay_pending' - && $record->{'custnum_pending'} - && !$disallow_untokenized; - # XXX we currently need a $cust_main to run realtime_tokenize - # even if we made it a class method, wouldn't have access to payname/etc. - # fail for now, but probably could handle this better... + unless ($cust_main || ( + # might happen for cust_pay_pending from failed verify records, + # in which case we attempt tokenization without cust_main # everything else should absolutely have a cust_main + $table eq 'cust_pay_pending' + && $record->{'custnum_pending'} + && !$disallow_untokenized + )) { $search->DESTROY; $dbh->rollback if $oldAutoCommit; return "Could not load cust_main for $table ".$record->get($record->primary_key); } + # no clear record of name/address/etc used for transaction, + # but will load name/phone/id from customer if run as an object method, + # so we try that if we can my %tokenopts = ( 'payment_gateway' => $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 + my $error = $cust_main + ? $cust_main->realtime_tokenize(\%tokenopts) + : FS::cust_main::Billing_Realtime->realtime_tokenize(\%tokenopts); + if (FS::cust_main::Billing_Realtime->tokenized($tokenopts{'payinfo'})) { # implies no error $record->payinfo($tokenopts{'payinfo'}); $error = $record->replace; } else { - $error = 'Unknown error'; + $error ||= 'Unknown error'; } if ($error) { $search->DESTROY; -- cgit v1.2.1 From e42db9683dee82d6991a4205642d5babb3d52f2f Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 23 Nov 2016 15:00:20 -0800 Subject: hide the notify-tickets widget if there are no tickets, or if the user turns it off, #73490 --- httemplate/elements/notify-tickets.html | 8 ++++++++ httemplate/pref/pref-process.html | 1 + httemplate/pref/pref.html | 7 +++++++ 3 files changed, 16 insertions(+) diff --git a/httemplate/elements/notify-tickets.html b/httemplate/elements/notify-tickets.html index e661737bc..16a04f4d9 100644 --- a/httemplate/elements/notify-tickets.html +++ b/httemplate/elements/notify-tickets.html @@ -14,12 +14,20 @@ use Class::Load 'load_class'; my $enabled = $FS::TicketSystem::system eq 'RT_Internal'; +$enabled = 0 if $FS::CurrentUser::CurrentUser->option('hide_notify_tickets'); my $UnrepliedTickets; if ($enabled) { my $class = 'RT::Search::UnrepliedTickets'; load_class($class); my $session = FS::TicketSystem->session; my $CurrentUser = $session->{CurrentUser}; + # if there are no tickets the current user could see, always hide it + my $AnyTickets = RT::Tickets->new($CurrentUser); + foreach my $status (qw(resolved rejected deleted)) { + $AnyTickets->LimitStatus( OPERATOR => '!=', VALUE => $status ); + } + $enabled = 0 if $AnyTickets->Count == 0; + $UnrepliedTickets = RT::Tickets->new($CurrentUser); my $search = $class->new(TicketsObj => $UnrepliedTickets); $search->Prepare; diff --git a/httemplate/pref/pref-process.html b/httemplate/pref/pref-process.html index 75e57958f..a87036b36 100644 --- a/httemplate/pref/pref-process.html +++ b/httemplate/pref/pref-process.html @@ -56,6 +56,7 @@ unless ( $error ) { # if ($access_user) { enable_mask_clipboard_hack dashboard_customers customer_view_emails printtofit + hide_notify_tickets email_address snom-ip snom-username snom-password vonage-fromnumber vonage-username vonage-password diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html index 0f23a0cba..f26d209d6 100644 --- a/httemplate/pref/pref.html +++ b/httemplate/pref/pref.html @@ -136,6 +136,13 @@ + + <% emt('Hide notification of new ticket activity') %> + + option('hide_notify_tickets') ? 'CHECKED' : '' %>> + + + <% emt("How many recently-modified customers displayed on dashboard") %> -- cgit v1.2.1 From bcfa8b9c306cc871b5851d7c07ef0759eb1bcb31 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 23 Nov 2016 15:25:31 -0800 Subject: avoid setting logfile path too early, from #37802 --- FS/FS/Daemon.pm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/FS/FS/Daemon.pm b/FS/FS/Daemon.pm index 4ecd80e98..a3c16d888 100644 --- a/FS/FS/Daemon.pm +++ b/FS/FS/Daemon.pm @@ -64,12 +64,6 @@ sub daemonize1 { $SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; }; } - # set the logfile sensibly - if (!$logfile) { - my $logname = $me; - $logname =~ s/^freeside-//; - logfile("%%%FREESIDE_LOG%%%/$logname-log.$FS::UID::datasrc"); - } } sub drop_root { @@ -122,6 +116,12 @@ sub _die { sub _logmsg { chomp( my $msg = shift ); + # set the logfile sensibly + if (!$logfile) { + my $logname = $me; + $logname =~ s/^freeside-//; + logfile("%%%FREESIDE_LOG%%%/$logname-log.$FS::UID::datasrc"); + } my $log = new IO::File ">>$logfile"; flock($log, LOCK_EX); seek($log, 0, 2); -- cgit v1.2.1 From 7c5f50804027577aac17d0fcefedcd0d0b6ca180 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sun, 27 Nov 2016 16:43:19 -0800 Subject: add RT ACL for bulk updating tickets, #72964 Conflicts: rt/lib/RT/System.pm --- FS/FS/TicketSystem.pm | 19 +++++++++++++++++++ rt/FREESIDE_MODIFIED | 6 ++++++ rt/lib/RT/System.pm | 3 +++ rt/share/html/Elements/Tabs | 5 ++++- rt/share/html/Search/Bulk.html | 7 +++++++ rt/share/html/Ticket/Elements/Tabs | 13 +++++++++---- 6 files changed, 48 insertions(+), 5 deletions(-) diff --git a/FS/FS/TicketSystem.pm b/FS/FS/TicketSystem.pm index 8f3d7af03..c973c8802 100644 --- a/FS/FS/TicketSystem.pm +++ b/FS/FS/TicketSystem.pm @@ -401,6 +401,25 @@ sub _upgrade_data { warn "Fixed $rows transactions with empty time values\n" if $rows > 0; } + # One-time fix: We've created a "BulkUpdateTickets" access right; grant + # it to all auth'd users initially. + eval "use FS::upgrade_journal;"; + my $upgrade = 'RT_add_BulkUpdateTickets_ACL'; + if (!FS::upgrade_journal->is_done($upgrade)) { + my $groups = RT::Groups->new(RT->SystemUser); + $groups->LimitToEnabled; + $groups->LimitToSystemInternalGroups; + $groups->Limit(FIELD => 'Type', VALUE => 'Privileged', OPERATOR => '='); + my $group = $groups->First + or die "No RT internal group found for Privileged users"; + my ($val, $msg) = $group->PrincipalObj->GrantRight( + Right => 'BulkUpdateTickets', Object => RT->System + ); + die "Couldn't grant BulkUpdateTickets right to all users: $msg\n" + if !$val; + FS::upgrade_journal->set_done($upgrade); + } + return; } diff --git a/rt/FREESIDE_MODIFIED b/rt/FREESIDE_MODIFIED index 05ffb2a46..db5212d5a 100644 --- a/rt/FREESIDE_MODIFIED +++ b/rt/FREESIDE_MODIFIED @@ -171,3 +171,9 @@ share/html/Search/Schedule.html share/html/Elements/CalendarSlotSchedule share/html/Ticket/Display.html +# BulkUpdateTickets ACL +lib/RT/System.pm +share/html/Elements/Tabs +share/html/Search/Bulk.html +share/html/Ticket/Elements/Tabs + diff --git a/rt/lib/RT/System.pm b/rt/lib/RT/System.pm index 388a1fd6e..af7a22bbb 100644 --- a/rt/lib/RT/System.pm +++ b/rt/lib/RT/System.pm @@ -92,6 +92,9 @@ __PACKAGE__->AddRight( General => LoadSavedSearch => 'Allow loading of saved __PACKAGE__->AddRight( General => CreateSavedSearch => 'Allow creation of saved searches'); # loc __PACKAGE__->AddRight( Admin => ExecuteCode => 'Allow writing Perl code in templates, scrips, etc'); # loc +#freeside +__PACKAGE__->AddRight( Staff => BulkUpdateTickets => 'Bulk update tickets'); + =head2 AvailableRights Returns a hashref of available rights for this object. The keys are the diff --git a/rt/share/html/Elements/Tabs b/rt/share/html/Elements/Tabs index 297d907a2..aef27edf4 100755 --- a/rt/share/html/Elements/Tabs +++ b/rt/share/html/Elements/Tabs @@ -847,7 +847,10 @@ my $build_main_nav = sub { } if ( $has_query ) { - $current_search_menu->child( bulk => title => loc('Bulk Update'), path => "/Search/Bulk.html$args" ); + #freeside + if ( $session{'CurrentUser'}->HasRight( Right => 'BulkUpdateTickets', Object => RT->System ) ) { + $current_search_menu->child( bulk => title => loc('Bulk Update'), path => "/Search/Bulk.html$args" ); + } $current_search_menu->child( chart => title => loc('Chart'), path => "/Search/Chart.html$args" ); #formerly Callbacks/RTx-Calendar/Ticket/Element/Tabs/Default diff --git a/rt/share/html/Search/Bulk.html b/rt/share/html/Search/Bulk.html index bacd96c3f..38b29fe33 100755 --- a/rt/share/html/Search/Bulk.html +++ b/rt/share/html/Search/Bulk.html @@ -203,6 +203,13 @@ unless ( defined $Rows ) { } my $title = loc("Update multiple tickets"); +#freeside +unless ( $session{'CurrentUser'} + ->HasRight( Right => 'BulkUpdateTickets', Object => RT->System) ) +{ + Abort('You are not allowed to bulk-update tickets.'); +} + # Iterate through the ARGS hash and remove anything with a null value. map ( $ARGS{$_} =~ /^$/ && ( delete $ARGS{$_} ), keys %ARGS ); diff --git a/rt/share/html/Ticket/Elements/Tabs b/rt/share/html/Ticket/Elements/Tabs index 2f89dc61c..bcc97e52a 100755 --- a/rt/share/html/Ticket/Elements/Tabs +++ b/rt/share/html/Ticket/Elements/Tabs @@ -326,10 +326,15 @@ if ($has_query) { title => loc('Show Results'), }; - $tabs->{"j"} = { - path => "Search/Bulk.html$args", - title => loc('Bulk Update'), - }; + #freeside + if ( $session{'CurrentUser'} + ->HasRight(Right => 'BulkUpdateTickets', Object => $RT::System) ) + { + $tabs->{"j"} = { + path => "Search/Bulk.html$args", + title => loc('Bulk Update'), + }; + } $tabs->{"k"} = { path => "Search/Chart.html$args", -- cgit v1.2.1 From 4cc0d96d34316ac01d2e204905bbe8de8dcd1469 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 29 Nov 2016 02:46:10 -0600 Subject: Bug fix to #73185, discovered via #71513 --- FS/FS/Cron/tax_rate_update.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/Cron/tax_rate_update.pm b/FS/FS/Cron/tax_rate_update.pm index b6ac63c2e..fec696fbb 100755 --- a/FS/FS/Cron/tax_rate_update.pm +++ b/FS/FS/Cron/tax_rate_update.pm @@ -31,7 +31,7 @@ sub tax_rate_update { my %opt = @_; my $oldAutoCommit = $FS::UID::AutoCommit; - $FS::UID::AutoCommit = 0; + local $FS::UID::AutoCommit = 0; my $dbh = dbh; my $conf = FS::Conf->new; -- cgit v1.2.1 From 51f97ec141f77064ca020634e7eccd85d9ead753 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 29 Nov 2016 04:21:46 -0600 Subject: 71513: Card tokenization [upgrade implemented] --- FS/FS/Cron/cleanup.pm | 16 ++- FS/FS/Upgrade.pm | 10 +- FS/FS/agent.pm | 80 ++--------- FS/FS/cust_main.pm | 5 + FS/FS/cust_main/Billing_Realtime.pm | 270 ++++++++++++++++++++++++++--------- FS/FS/log_context.pm | 1 + FS/FS/payinfo_Mixin.pm | 1 + FS/FS/payment_gateway.pm | 102 +++++++++++++ FS/bin/freeside-daily | 2 +- FS/t/suite/13-tokenization.t | 82 +++++++++++ httemplate/edit/elements/edit.html | 2 +- httemplate/edit/payment_gateway.html | 11 +- 12 files changed, 431 insertions(+), 151 deletions(-) create mode 100755 FS/t/suite/13-tokenization.t diff --git a/FS/FS/Cron/cleanup.pm b/FS/FS/Cron/cleanup.pm index 6ec401398..9d0c06740 100644 --- a/FS/FS/Cron/cleanup.pm +++ b/FS/FS/Cron/cleanup.pm @@ -8,12 +8,26 @@ use FS::Record qw( qsearch ); # start janitor jobs sub cleanup { -# fix locations that are missing coordinates + my %opt = @_; + + # fix locations that are missing coordinates my $job = FS::queue->new({ 'job' => 'FS::cust_location::process_set_coord', 'status' => 'new' }); $job->insert('_JOB'); + + # check card number tokenization + $job = FS::queue->new({ + 'job' => 'FS::cust_main::Billing_Realtime::token_check', + 'status' => 'new' + }); + $job->insert( + %opt, + 'queue' => 1, + 'daily' => 1, + ); + } sub cleanup_before_backup { diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index 940ae2844..41349a59a 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -362,7 +362,11 @@ sub upgrade_data { #fix whitespace - before cust_main 'cust_location' => [], - #cust_main (remove paycvv from history, locations, cust_payby, etc) + # need before cust_main tokenization upgrade, + # blocks tokenization upgrade if deprecated features still in use + 'agent_payment_gateway' => [], + + #cust_main (tokenizes cards, remove paycvv from history, locations, cust_payby, etc) 'cust_main' => [], #contact -> cust_contact / prospect_contact @@ -390,10 +394,6 @@ sub upgrade_data { #duplicate history records 'h_cust_svc' => [], - # need before transaction tables, - # blocks tokenization upgrade if deprecated features still in use - 'agent_payment_gateway' => [], - #populate cust_pay.otaker 'cust_pay' => [], diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm index 8aa78c2b7..b97e9b9b4 100644 --- a/FS/FS/agent.pm +++ b/FS/FS/agent.pm @@ -9,6 +9,7 @@ use FS::cust_main; use FS::cust_pkg; use FS::reg_code; use FS::agent_payment_gateway; +use FS::payment_gateway; use FS::TicketSystem; use FS::Conf; @@ -253,12 +254,7 @@ the business-onlinepayment-ach gateway will be returned if available. If I is set and the I is PAYPAL, the defined paypal gateway will be returned. -If I exists, then either the specified gateway or the -default gateway will be returned. Agent overrides are ignored, and this can -safely be called as a class method if this option is specified. Not -compatible with I. - -Exsisting I<$conf> may be passed for efficiency. +Exisisting I<$conf> may be passed for efficiency. =cut @@ -268,8 +264,8 @@ Exsisting I<$conf> may be passed for efficiency. sub payment_gateway { my ( $self, %options ) = @_; + $options{'conf'} ||= new FS::Conf; my $conf = $options{'conf'}; - $conf ||= new FS::Conf; if ( $options{thirdparty} ) { @@ -299,72 +295,12 @@ sub payment_gateway { } } - my ($override, $payment_gateway); - if (exists $options{'load_gatewaynum'}) { # no agent overrides if this opt is in use - if ($options{'load_gatewaynum'}) { - $payment_gateway = qsearchs('payment_gateway', { gatewaynumnum => $options{'load_gatewaynum'} } ); - # always fatal - die "Could not load payment gateway ".$options{'load_gatewaynum'} unless $payment_gateway; - } # else use default, loaded below - } else { - $override = qsearchs('agent_payment_gateway', { agentnum => $self->agentnum } ); - } - - if ( $override ) { #use a payment gateway override - - $payment_gateway = $override->payment_gateway; - - $payment_gateway->gateway_namespace('Business::OnlinePayment') - unless $payment_gateway->gateway_namespace; - - } elsif (!$payment_gateway) { #use the standard settings from the config - - # the standard settings from the config could be moved to a null agent - # agent_payment_gateway referenced payment_gateway - - # remember, this block might be run as a class method if false load_gatewaynum exists + my $override = qsearchs('agent_payment_gateway', { agentnum => $self->agentnum } ); - unless ( $conf->exists('business-onlinepayment') ) { - if ( $options{'nofatal'} ) { - return ''; - } else { - die "Real-time processing not enabled\n"; - } - } - - #load up config - my $bop_config = 'business-onlinepayment'; - $bop_config .= '-ach' - if ( $options{method} - && $options{method} =~ /^(ECHECK|CHEK)$/ - && $conf->exists($bop_config. '-ach') - ); - my ( $processor, $login, $password, $action, @bop_options ) = - $conf->config($bop_config); - $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; - - $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') || - 'Business::OnlinePayment'); - $payment_gateway->gateway_module($processor); - $payment_gateway->gateway_username($login); - $payment_gateway->gateway_password($password); - $payment_gateway->gateway_action($action); - $payment_gateway->set('options', [ @bop_options ]); - - } - - unless ( $payment_gateway->gateway_namespace ) { - $payment_gateway->gateway_namespace( - scalar($conf->config('business-onlinepayment-namespace')) - || 'Business::OnlinePayment' - ); - } + my $payment_gateway = FS::payment_gateway->by_key_or_default( + gatewaynum => $override ? $override->gatewaynum : '', + %options, + ); $payment_gateway; } diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 747776b26..51bde33fa 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -5356,6 +5356,11 @@ sub _upgrade_data { #class method } +sub queueable_upgrade { + my $class = shift; + FS::cust_main::Billing_Realtime::token_check(@_); +} + =back =head1 BUGS diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 3757ca814..ef17fce24 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -14,6 +14,7 @@ use FS::cust_pay_pending; use FS::cust_bill_pay; use FS::cust_refund; use FS::banned_pay; +use FS::payment_gateway; $realtime_bop_decline_quiet = 0; @@ -2297,7 +2298,7 @@ sub realtime_tokenize { 'type' => 'CC', _bop_auth(\%options), 'action' => 'Tokenize', - 'description' => $options{'description'} + 'description' => $options{'description'}, %$bop_content, %content, #after ); @@ -2347,7 +2348,7 @@ sub tokenized { FS::cust_pay->tokenized($payinfo); } -=item token_check +=item token_check [ quiet => 1, queue => 1, daily => 1 ] NOT A METHOD. Acts on all customers. Placed here because it makes use of module-internal methods, and to keep everything that uses @@ -2356,74 +2357,138 @@ Billing::OnlinePayment all in one place. Tokenizes all tokenizable card numbers from payinfo in cust_payby and CARD transactions in cust_pay_pending, cust_pay, cust_pay_void and cust_refund. -If all configured gateways have the ability to tokenize, then detection of -an untokenizable record will cause a fatal error. +If the I flag is set, newly tokenized records will be immediately +committed, regardless of AutoCommit, so as to release the mutex on the record. + +If all configured gateways have the ability to tokenize, detection of an +untokenizable record will cause a fatal error. However, if the I flag +is set, this will instead cause a critical error to be recorded in the log, +and any other tokenizable records will still be committed. + +If the I flag is also set, detection of existing untokenized records will +record a critical error in the system log (because they should have never appeared +in the first place.) Tokenization will still be attempted. + +If any configured gateways do NOT have the ability to tokenize, or if a +default gateway is not configured, then untokenized records are not considered +a threat, and no critical errors will be generated in the log. =cut sub token_check { - # no input, acts on all customers + #acts on all customers + my %opt = @_; + my $debug = !$opt{'quiet'} || $DEBUG; - eval "use FS::Cursor"; - return "Error initializing FS::Cursor: ".$@ if $@; + warn "token_check called with opts\n".Dumper(\%opt) if $debug; - my $dbh = dbh; + # force some explicitness when invoking this method + die "token_check must run with queue flag if run with daily flag" + if $opt{'daily'} && !$opt{'queue'}; + + my $conf = FS::Conf->new; + + my $log = FS::Log->new('FS::cust_main::Billing_Realtime::token_check'); - # get list of all gateways in table (not counting default gateway) my $cache = {}; #cache for module info - my $sth = $dbh->prepare('SELECT DISTINCT gatewaynum FROM payment_gateway') - or die $dbh->errstr; - $sth->execute or die $sth->errstr; - my @gatewaynums; - while (my $row = $sth->fetchrow_hashref) { - push(@gatewaynums,$row->{'gatewaynum'}); - } - $sth->finish; # look for a gateway that can't tokenize - my $disallow_untokenized = 1; - foreach my $gatewaynum ('',@gatewaynums) { - my $gateway = FS::agent->payment_gateway( load_gatewaynum => $gatewaynum, nofatal => 1 ); - if (!$gateway) { # already died if $gatewaynum + my $require_tokenized = 1; + foreach my $gateway ( + FS::payment_gateway->all_gateways( + 'method' => 'CC', + 'conf' => $conf, + 'nofatal' => 1, + ) + ) { + if (!$gateway) { # no default gateway, no promise to tokenize # can just load other gateways as-needeed below - $disallow_untokenized = 0; + $require_tokenized = 0; last; } my $info = _token_check_gateway_info($cache,$gateway); - return $info unless ref($info); # means it's an error message + die $info unless ref($info); # means it's an error message unless ($info->{'can_tokenize'}) { # a configured gateway can't tokenize, that's all we need to know right now # can just load other gateways as-needeed below - $disallow_untokenized = 0; + $require_tokenized = 0; last; } } + warn "REQUIRE TOKENIZED" if $require_tokenized && $debug; + + # upgrade does not call this with autocommit turned on, + # and autocommit will be ignored if opt queue is set, + # but might as well be thorough... my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + # for retrieving data in chunks + my $step = 500; + my $offset = 0; ### Tokenize cust_payby - my $cust_search = FS::Cursor->new({ table => 'cust_main' },$dbh); - while (my $cust_main = $cust_search->fetch) { + my @recnums; + +CUSTLOOP: + while (my $custnum = _token_check_next_recnum($dbh,'cust_main',$step,\$offset,\@recnums)) { + my $cust_main = FS::cust_main->by_key($custnum); + my $payment_gateway; 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({ - 'nofatal' => 1, # handle error smoothly below + + # see if it's already tokenized + if ($cust_payby->tokenized) { + warn "cust_payby ".$cust_payby->get($cust_payby->primary_key)." already tokenized" if $debug; + next; + } + + if ($require_tokenized && $opt{'daily'}) { + $log->critical("Untokenized card number detected in cust_payby ".$cust_payby->custpaybynum); + $dbh->commit or die $dbh->errstr; # commit log message + } + + # only load gateway if we need to, and only need to load it once + my $payment_gateway ||= $cust_main->_payment_gateway({ + 'method' => 'CC', + 'conf' => $conf, + 'nofatal' => 1, # handle lack of gateway smoothly below }); unless ($payment_gateway) { # no reason to have untokenized card numbers saved if no gateway, - # but only fatal if we expected everyone to tokenize card numbers - next unless $disallow_untokenized; - $cust_search->DESTROY; + # but only a problem if we expected everyone to tokenize card numbers + unless ($require_tokenized) { + warn "Skipping cust_payby for cust_main ".$cust_main->custnum.", no payment gateway" if $debug; + next CUSTLOOP; # can skip rest of customer + } + my $error = "No gateway found for custnum ".$cust_main->custnum; + if ($opt{'queue'}) { + $log->critical($error); + $dbh->commit or die $dbh->errstr; # commit error message + next; # not next CUSTLOOP, want to record error for every cust_payby + } $dbh->rollback if $oldAutoCommit; - return "No gateway found for custnum ".$cust_main->custnum; + die $error; } + my $info = _token_check_gateway_info($cache,$payment_gateway); + unless (ref($info)) { + # only throws error if Business::OnlinePayment won't load, + # which is just cause to abort this whole process, even if queue + $dbh->rollback if $oldAutoCommit; + die $info; # error message + } # no fail here--a configured gateway can't tokenize, so be it - next unless ref($info) && $info->{'can_tokenize'}; + unless ($info->{'can_tokenize'}) { + warn "Skipping ".$cust_main->custnum." cannot tokenize" if $debug; + next; + } + + # time to tokenize + $cust_payby = $cust_payby->select_for_update; my %tokenopts = ( 'payment_gateway' => $payment_gateway, 'cust_payby' => $cust_payby, @@ -2435,11 +2500,20 @@ sub token_check { $error ||= 'Unknown error'; } if ($error) { - $cust_search->DESTROY; + $error = "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error; + if ($opt{'queue'}) { + $log->critical($error); + $dbh->commit or die $dbh->errstr; # commit log message, release mutex + next; # not next CUSTLOOP, want to record error for every cust_payby + } $dbh->rollback if $oldAutoCommit; - return "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error; + die $error; } + $dbh->commit or die $dbh->errstr if $opt{'queue'}; # release mutex + warn "TOKENIZED cust_payby ".$cust_payby->get($cust_payby->primary_key) if $debug; } + warn "cust_payby upgraded for custnum ".$cust_main->custnum if $debug; + } ### Tokenize/mask transaction tables @@ -2450,50 +2524,83 @@ sub token_check { # 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 at least it's not a card number - next if $record->payinfo =~ /N\/A/; # ??? Not sure why we do this, but it's not a card number + warn "Checking $table" if $debug; + + # FS::Cursor does not seem to work over multiple commits (gives cursor not found errors) + # loading only record ids, then loading individual records one at a time + my $tclass = 'FS::'.$table; + $offset = 0; + @recnums = (); + + while (my $recnum = _token_check_next_recnum($dbh,$table,$step,\$offset,\@recnums)) { + my $record = $tclass->by_key($recnum); + if (FS::cust_main::Billing_Realtime->tokenized($record->payinfo)) { + warn "Skipping tokenized record for $table ".$record->get($record->primary_key) if $debug; + next; + } + if (!$record->payinfo) { #shouldn't happen, but at least it's not a card number + warn "Skipping blank payinfo for $table ".$record->get($record->primary_key) if $debug; + next; + } + if ($record->payinfo =~ /N\/A/) { # ??? Not sure why we do this, but it's not a card number + warn "Skipping NA payinfo for $table ".$record->get($record->primary_key) if $debug; + next; + } + + if ($require_tokenized && $opt{'daily'}) { + $log->critical("Untokenized card number detected in $table ".$record->get($record->primary_key)); + $dbh->commit or die $dbh->errstr; # commit log message + } # don't use customer agent gateway here, use the gatewaynum specified by the record - my $gatewaynum = $record->gatewaynum || ''; - my $gateway = FS::agent->payment_gateway( load_gatewaynum => $gatewaynum ); - unless ($gateway) { # already died if $gatewaynum - # only fatal if we expected everyone to tokenize - next unless $disallow_untokenized; - $search->DESTROY; - $dbh->rollback if $oldAutoCommit; - return "No gateway found for $table ".$record->get($record->primary_key); + my $gateway = FS::payment_gateway->by_key_or_default( + 'method' => 'CC', + 'conf' => $conf, + 'nofatal' => 1, + 'gatewaynum' => $record->gatewaynum || '', + ); + unless ($gateway) { + # means no default gateway, no promise to tokenize, can skip + warn "Skipping missing gateway for $table ".$record->get($record->primary_key) if $debug; + next; } + my $info = _token_check_gateway_info($cache,$gateway); unless (ref($info)) { # only throws error if Business::OnlinePayment won't load, - # which is just cause to abort this whole process - $search->DESTROY; + # which is just cause to abort this whole process, even if queue $dbh->rollback if $oldAutoCommit; - return $info; # error message + die $info; # error message } # a configured gateway can't tokenize, move along - next unless $info->{'can_tokenize'}; + unless ($info->{'can_tokenize'}) { + warn "Skipping, cannot tokenize $table ".$record->get($record->primary_key) if $debug; + next; + } my $cust_main = $record->cust_main; - unless ($cust_main || ( + if (!$cust_main) { # might happen for cust_pay_pending from failed verify records, # in which case we attempt tokenization without cust_main # everything else should absolutely have a cust_main - $table eq 'cust_pay_pending' - && $record->{'custnum_pending'} - && !$disallow_untokenized - )) { - $search->DESTROY; - $dbh->rollback if $oldAutoCommit; - return "Could not load cust_main for $table ".$record->get($record->primary_key); + if ($table eq 'cust_pay_pending' && $record->{'custnum_pending'}) { + warn "ATTEMPTING GATEWAY-ONLY TOKENIZE" if $debug; + } else { + my $error = "Could not load cust_main for $table ".$record->get($record->primary_key); + if ($opt{'queue'}) { + $log->critical($error); + $dbh->commit or die $dbh->errstr; # commit log message + next; + } + $dbh->rollback if $oldAutoCommit; + die $error; + } } + + # if we got this far, time to mutex + $record = $record->select_for_update; + # no clear record of name/address/etc used for transaction, # but will load name/phone/id from customer if run as an object method, # so we try that if we can @@ -2513,18 +2620,43 @@ sub token_check { $error ||= 'Unknown error'; } if ($error) { - $search->DESTROY; + $error = "Error tokenizing $table ".$record->get($record->primary_key).": ".$error; + if ($opt{'queue'}) { + $log->critical($error); + $dbh->commit or die $dbh->errstr; # commit log message, release mutex + next; + } $dbh->rollback if $oldAutoCommit; - return "Error tokenizing $table ".$record->get($record->primary_key).": ".$error; + die $error; } + $dbh->commit or die $dbh->errstr if $opt{'queue'}; # release mutex + warn "TOKENIZED $table ".$record->get($record->primary_key) if $debug; + } # end record loop } # end table loop - $dbh->commit if $oldAutoCommit; + $dbh->commit or die $dbh->errstr if $oldAutoCommit; return ''; } +# not a method! +sub _token_check_next_recnum { + my ($dbh,$table,$step,$offset,$recnums) = @_; + my $recnum = shift @$recnums; + return $recnum if $recnum; + my $tclass = 'FS::'.$table; + my $sth = $dbh->prepare('SELECT '.$tclass->primary_key.' FROM '.$table.' ORDER BY '.$tclass->primary_key.' LIMIT '.$step.' OFFSET '.$$offset) or die $dbh->errstr; + $sth->execute() or die $sth->errstr; + my @recnums; + while (my $rec = $sth->fetchrow_hashref) { + push @$recnums, $rec->{$tclass->primary_key}; + } + $sth->finish(); + $$offset += $step; + return shift @$recnums; +} + # not a method! sub _token_check_gateway_info { my ($cache,$payment_gateway) = @_; @@ -2563,8 +2695,6 @@ sub _token_check_gateway_info { $info->{'void_requires_card'} = 1 if $transaction->info('CC_void_requires_card'); - $cache->{$payment_gateway->gateway_module} = $info; - return $info; } diff --git a/FS/FS/log_context.pm b/FS/FS/log_context.pm index 51aa79de5..a41d3c837 100644 --- a/FS/FS/log_context.pm +++ b/FS/FS/log_context.pm @@ -11,6 +11,7 @@ my @contexts = ( qw( FS::cust_main::Billing_Realtime::realtime_bop FS::cust_main::Billing_Realtime::realtime_tokenize FS::cust_main::Billing_Realtime::realtime_verify_bop + FS::cust_main::Billing_Realtime::token_check FS::pay_batch::import_from_gateway FS::part_pkg FS::Misc::Geo::standardize_uscensus diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index 2f503129d..be37568ad 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -468,6 +468,7 @@ Optionally, an arbitrary payby and payinfo can be passed. sub tokenized { my $self = shift; my $payinfo = scalar(@_) ? shift : $self->payinfo; + return 0 unless $payinfo; #avoid uninitialized value error $payinfo =~ /^99\d{14}$/; } diff --git a/FS/FS/payment_gateway.pm b/FS/FS/payment_gateway.pm index afae2667e..170d37af9 100644 --- a/FS/FS/payment_gateway.pm +++ b/FS/FS/payment_gateway.pm @@ -323,6 +323,108 @@ sub processor { } } +=item default_gateway OPTIONS + +Class method. + +Returns default gateway (from business-onlinepayment conf) as a payment_gateway object. + +Accepts options + +conf - existing conf object + +nofatal - return blank instead of dying if no default gateway is configured + +method - if set to CHEK or ECHECK, returns object for business-onlinepayment-ach if available + +Before using this, be sure you wouldn't rather be using L or, +more likely, L. + +=cut + +# the standard settings from the config could be moved to a null agent +# agent_payment_gateway referenced payment_gateway + +sub default_gateway { + my ($self,%options) = @_; + + $options{'conf'} ||= new FS::Conf; + my $conf = $options{'conf'}; + + unless ( $conf->exists('business-onlinepayment') ) { + if ( $options{'nofatal'} ) { + return ''; + } else { + die "Real-time processing not enabled\n"; + } + } + + #load up config + my $bop_config = 'business-onlinepayment'; + $bop_config .= '-ach' + if ( $options{method} + && $options{method} =~ /^(ECHECK|CHEK)$/ + && $conf->exists($bop_config. '-ach') + ); + my ( $processor, $login, $password, $action, @bop_options ) = + $conf->config($bop_config); + $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; + + my $payment_gateway = new FS::payment_gateway; + $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') || + 'Business::OnlinePayment'); + $payment_gateway->gateway_module($processor); + $payment_gateway->gateway_username($login); + $payment_gateway->gateway_password($password); + $payment_gateway->gateway_action($action); + $payment_gateway->set('options', [ @bop_options ]); + return $payment_gateway; +} + +=item by_key_or_default OPTIONS + +Either returns the gateway specified by option gatewaynum, or the default gateway. + +Accepts the same options as L. + +Also ensures that the gateway_namespace has been set. + +=cut + +sub by_key_or_default { + my ($self,%options) = @_; + + if ($options{'gatewaynum'}) { + my $payment_gateway = $self->by_key($options{'gatewaynum'}); + # regardless of nofatal, which is only meant for handling lack of default gateway + die "payment_gateway ".$options{'gatewaynum'}." not found" + unless $payment_gateway; + $payment_gateway->gateway_namespace('Business::OnlinePayment') + unless $payment_gateway->gateway_namespace; + return $payment_gateway; + } else { + return $self->default_gateway(%options); + } +} + +# if it weren't for the way gateway_namespace default is set, this method would not be necessary +# that should really go in check() with an accompanying upgrade, so we could just use qsearch safely, +# but currently short on time to test deeper changes... +# +# if no default gateway is set and nofatal is passed, first value returned is blank string +sub all_gateways { + my ($self,%options) = @_; + my @out; + foreach my $gatewaynum ('',( map {$_->gatewaynum} qsearch('payment_gateway') )) { + push @out, $self->by_key_or_default( %options, gatewaynum => $gatewaynum ); + } + return @out; +} + # _upgrade_data # # Used by FS::Upgrade to migrate to a new database. diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily index ee95c14db..e1463f5da 100755 --- a/FS/bin/freeside-daily +++ b/FS/bin/freeside-daily @@ -97,7 +97,7 @@ use FS::Cron::backup qw(backup); backup(); #except we'd rather not start cleanup jobs until the backup is done -cleanup(); +cleanup( quiet => !$opt{'v'} ); $log->info('finish'); diff --git a/FS/t/suite/13-tokenization.t b/FS/t/suite/13-tokenization.t new file mode 100755 index 000000000..1b654add5 --- /dev/null +++ b/FS/t/suite/13-tokenization.t @@ -0,0 +1,82 @@ +#!/usr/bin/perl + +use FS::Test; +use Test::More tests => 8; +use FS::Conf; + +### can only run on test database (company name "Freeside Test") +### will run upgrade, which uses lots of prints & warns beyond regular test output + +my $fs = FS::Test->new( user => 'admin' ); +my $conf = new_ok('FS::Conf'); +my $err; +my $bopconf; + +like( $conf->config('company_name'), qr/^Freeside Test/, 'using test database' ) or BAIL_OUT(''); + +# some pre-upgrade cleanup, upgrade will fail if these are still configured +foreach my $cust_main ( $fs->qsearch('cust_main') ) { + my @count = $fs->qsearch('agent_payment_gateway', { agentnum => $cust_main->agentnum } ); + if (@count > 1) { + note("DELETING CARDTYPE GATEWAYS"); + foreach my $apg (@count) { + $err = $apg->delete if $apg->cardtype; + last if $err; + } + @count = $fs->qsearch('agent_payment_gateway', { agentnum => $cust_main->agentnum } ); + if (@count > 1) { + $err = "Still found ".@count." gateways for custnum ".$cust_main->custnum; + last; + } + } +} +ok( !$err, "remove obsolete payment gateways" ) or BAIL_OUT($err); + +$bopconf = +'IPPay +TESTTERMINAL'; +$conf->set('business-onlinepayment' => $bopconf); +is( join("\n",$conf->config('business-onlinepayment')), $bopconf, "setting first default gateway" ) or BAIL_OUT(''); + +$err = system('freeside-upgrade','admin'); +ok( !$err, 'initial upgrade' ) or BAIL_OUT('Error string: '.$!); + +$bopconf = +'CardFortress +cardfortresstest +(TEST54) +Normal Authorization +gateway +IPPay +gateway_login +TESTTERMINAL +gateway_password + +private_key +/usr/local/etc/freeside/cardfortresstest.txt'; +$conf->set('business-onlinepayment' => $bopconf); +is( join("\n",$conf->config('business-onlinepayment')), $bopconf, "setting tokenizable default gateway" ) or BAIL_OUT(''); + +foreach my $pg ($fs->qsearch('payment_gateway')) { + unless ($pg->gateway_module eq 'CardFortress') { + note('UPGRADING NON-CF PAYMENT GATEWAY'); + my %pgopts = ( + gateway => $pg->gateway_module, + gateway_login => $pg->gateway_username, + gateway_password => $pg->gateway_password, + private_key => '/usr/local/etc/freeside/cardfortresstest.txt', + ); + $pg->gateway_module('CardFortress'); + $pg->gateway_username('cardfortresstest'); + $pg->gateway_password('(TEST54)'); + $err = $pg->replace(\%pgopts); + last if $err; + } +} +ok( !$err, "remove non-CF payment gateways" ) or BAIL_OUT($err); + +$err = system('freeside-upgrade','admin'); +ok( !$err, 'tokenizable upgrade' ) or BAIL_OUT('Error string: '.$!); + +1; + diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html index b71558df5..a0023382e 100644 --- a/httemplate/edit/elements/edit.html +++ b/httemplate/edit/elements/edit.html @@ -247,7 +247,7 @@ Example: > - + <% defined($opt{'form_init'}) ? ( ref($opt{'form_init'}) diff --git a/httemplate/edit/payment_gateway.html b/httemplate/edit/payment_gateway.html index b44b31513..f9b8f2415 100644 --- a/httemplate/edit/payment_gateway.html +++ b/httemplate/edit/payment_gateway.html @@ -22,6 +22,9 @@