use FS::cust_payby;
use FS::contact;
use FS::reason;
+use FS::Misc::Savepoint;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
if $DEBUG;
+ return "You are not permitted to change customer invoicing terms."
+ if $self->invoice_terms #i.e. not the default
+ && ! $FS::CurrentUser::CurrentUser->access_right('Edit customer invoice terms');
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
&& ! $self->locale
&& $conf->exists('cust_main-require_locale');
+ return "You are not permitted to change customer invoicing terms."
+ if $old->invoice_terms ne $self->invoice_terms
+ && ! $curuser->access_right('Edit customer invoice terms');
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
$implicit_contact->set('emailaddress', $email);
$implicit_contact->set('invoice_dest', 'Y');
$implicit_contact->set('custnum', $self->custnum);
+ my $i_cust_contact =
+ qsearchs('cust_contact', {
+ contactnum => $implicit_contact->contactnum,
+ custnum => $self->custnum,
+ }
+ );
+ if ( $i_cust_contact ) {
+ $implicit_contact->set($_, $i_cust_contact->$_)
+ foreach qw( classnum selfservice_access comment );
+ }
my $error;
if ( $implicit_contact->contactnum ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
- return '' if $payinfo =~ /^99\d{14}$/; #token
+ return '' if $self->tokenized($payinfo); #token
my %bop_card_types = map { $_=>1 } values %{ card_types() };
my $cardtype = cardtype($payinfo);
my( $self, %opt ) = @_;
# we're going to cancel services, which is not reversible
+ # unless exports are suppressed
die "cancel_pkgs cannot be run inside a transaction"
- if $FS::UID::AutoCommit == 0;
+ if !$FS::UID::AutoCommit && !$FS::svc_Common::noexport_hack;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
+ savepoint_create('cancel_pkgs');
+
return ( 'access denied' )
unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref;
my $error = $ban->insert;
if ($error) {
- dbh->rollback;
+ savepoint_rollback_and_release('cancel_pkgs');
+ dbh->rollback if $oldAutoCommit;
return ( $error );
}
'time' => $cancel_time );
if ($error) {
warn "Error billing during cancel, custnum ". $self->custnum. ": $error";
- dbh->rollback;
+ savepoint_rollback_and_release('cancel_pkgs');
+ dbh->rollback if $oldAutoCommit;
return ( "Error billing during cancellation: $error" );
}
}
- dbh->commit;
+ savepoint_release('cancel_pkgs');
+ dbh->commit if $oldAutoCommit;
- $FS::UID::AutoCommit = 1;
my @errors;
# now cancel all services, the same way we would for individual packages.
# if any of them fail, cancel the rest anyway.
warn "$me removing ".scalar(@sorted_cust_svc)." service(s) for customer ".
$self->custnum."\n"
if $DEBUG;
+ my $i = 0;
foreach my $cust_svc (@sorted_cust_svc) {
+ my $savepoint = 'cancel_pkgs_'.$i++;
+ savepoint_create( $savepoint );
my $part_svc = $cust_svc->part_svc;
next if ( defined($part_svc) and $part_svc->preserve );
- my $error = $cust_svc->cancel; # immediate cancel, no date option
- push @errors, $error if $error;
+ # immediate cancel, no date option
+ # transactionize individually
+ my $error = try { $cust_svc->cancel } catch { $_ };
+ if ( $error ) {
+ savepoint_rollback_and_release( $savepoint );
+ dbh->rollback if $oldAutoCommit;
+ push @errors, $error;
+ } else {
+ savepoint_release( $savepoint );
+ dbh->commit if $oldAutoCommit;
+ }
}
if (@errors) {
return @errors;
@cprs = @{ delete $opt{'cust_pkg_reason'} };
}
my $null_reason;
+ $i = 0;
foreach (@pkgs) {
my %lopt = %opt;
+ my $savepoint = 'cancel_pkgs_'.$i++;
+ savepoint_create( $savepoint );
if (@cprs) {
my $cpr = shift @cprs;
if ( $cpr ) {
}
}
my $error = $_->cancel(%lopt);
- push @errors, 'pkgnum '.$_->pkgnum.': '.$error if $error;
+ if ( $error ) {
+ savepoint_rollback_and_release( $savepoint );
+ dbh->rollback if $oldAutoCommit;
+ push @errors, 'pkgnum '.$_->pkgnum.': '.$error;
+ } else {
+ savepoint_release( $savepoint );
+ dbh->commit if $oldAutoCommit;
+ }
}
return @errors;
my $cust_pkg_ref = '';
my ( $bill_now, $invoice_terms ) = ( 0, '' );
my $locationnum;
+ my ( $discountnum, $discountnum_amount, $discountnum_percent ) = ( '','','' );
if ( ref( $_[0] ) ) {
$amount = $_[0]->{amount};
$setup_cost = $_[0]->{setup_cost};
$invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
$locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
$separate_bill = $_[0]->{separate_bill} || '';
+ $discountnum = $_[0]->{setup_discountnum};
+ $discountnum_amount = $_[0]->{setup_discountnum_amount};
+ $discountnum_percent = $_[0]->{setup_discountnum_percent};
} else { # yuck
$amount = shift;
$setup_cost = '';
}
my $cust_pkg = new FS::cust_pkg ( {
- 'custnum' => $self->custnum,
- 'pkgpart' => $pkgpart,
- 'quantity' => $quantity,
- 'start_date' => $start_date,
- 'no_auto' => $no_auto,
- 'separate_bill' => $separate_bill,
- 'locationnum'=> $locationnum,
+ 'custnum' => $self->custnum,
+ 'pkgpart' => $pkgpart,
+ 'quantity' => $quantity,
+ 'start_date' => $start_date,
+ 'no_auto' => $no_auto,
+ 'separate_bill' => $separate_bill,
+ 'locationnum' => $locationnum,
+ 'setup_discountnum' => $discountnum,
+ 'setup_discountnum_amount' => $discountnum_amount,
+ 'setup_discountnum_percent' => $discountnum_percent,
} );
$error = $cust_pkg->insert;
);
}
+=item max_invnum
+
+Returns the most recent invnum (invoice number) for this customer.
+
+=cut
+
+sub max_invnum {
+ my $self = shift;
+ $self->scalar_sql(
+ " SELECT MAX(invnum) FROM cust_bill WHERE custnum = ?",
+ $self->custnum
+ );
+}
+
=item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
Returns all the invoices (see L<FS::cust_bill>) for this customer.
$name;
}
+=item batch_payment_payname
+
+Returns a name string for this customer, either "cust_batch_payment->payname" or "First Last" or "Company,
+based on if a company name exists and is the account being used a business account.
+
+=cut
+
+sub batch_payment_payname {
+ my $self = shift;
+ my $cust_pay_batch = shift;
+ my $name;
+
+ if ($cust_pay_batch->{Hash}->{payby} eq "CARD") { $name = $cust_pay_batch->payname; }
+ else { $name = $self->first .' '. $self->last; }
+
+ $name = $self->company
+ if (($cust_pay_batch->{Hash}->{paytype} eq "Business checking" || $cust_pay_batch->{Hash}->{paytype} eq "Business savings") && $self->company);
+
+ $name;
+}
+
=item service_contact
Returns the L<FS::contact> object for this customer that has the 'Service'
CHEK only
+=item saved_cust_payby
+
+scalar reference, for returning saved object
+
=back
=cut
next if grep(/^$field$/, qw( custpaybynum payby weight ) );
next if grep(/^$field$/, @preserve );
next PAYBYLOOP unless $new->get($field) eq $cust_payby->get($field);
+ # check if paymask exists, if so stop and don't save, no need for a duplicate.
+ return '' if $new->get('paymask') eq $cust_payby->get('paymask');
}
# now check fields that can replace if one value is blank
my $replace = 0;
return $error;
}
+ ${$opt{'saved_cust_payby'}} = $new
+ if $opt{'saved_cust_payby'};
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
$cust_main->bill_and_collect( %$param );
}
+=item pending_invoice_count
+
+Return number of cust_bill with pending=Y for this customer
+
+=cut
+
+sub pending_invoice_count {
+ FS::cust_bill->count( 'custnum = '.shift->custnum."AND pending = 'Y'" );
+}
+
#starting to take quite a while for big dbs
# (JRNL: journaled so it only happens once per database)
# - seq scan of h_cust_main (yuck), but not going to index paycvv, so
# at the time we do this, also migrate paytype into cust_pay_batch
# so that batches that are open before the migration can still be
# processed
- my @cust_pay_batch = qsearch('cust_pay_batch', {
- 'custnum' => $cust_main->custnum,
- 'payby' => 'CHEK',
- 'paytype' => '',
- });
- foreach my $cust_pay_batch (@cust_pay_batch) {
- $cust_pay_batch->set('paytype', $cust_main->get('paytype'));
- my $error = $cust_pay_batch->replace;
- die "$error (setting cust_pay_batch.paytype)" if $error;
+ if ( $cust_main->get('paytype') ) {
+ my @cust_pay_batch = qsearch('cust_pay_batch', {
+ 'custnum' => $cust_main->custnum,
+ 'payby' => 'CHEK',
+ 'paytype' => '',
+ });
+ foreach my $cust_pay_batch (@cust_pay_batch) {
+ $cust_pay_batch->set('paytype', $cust_main->get('paytype'));
+ my $error = $cust_pay_batch->replace;
+ die "$error (setting cust_pay_batch.paytype)" if $error;
+ }
}
$cust_main->complimentary('Y') if $cust_main->payby eq 'COMP';
FS::upgrade_journal->set_done('clear_payinfo_history');
}
- # encrypt old records
- if ($conf->exists('encryption') && !FS::upgrade_journal->is_done('encryption_check')) {
+ # fix Tokenized paycardtype and encrypt old records
+ if ( ! FS::upgrade_journal->is_done('paycardtype_Tokenized')
+ || ! FS::upgrade_journal->is_done('encryption_check')
+ )
+ {
# allow replacement of closed cust_pay/cust_refund records
local $FS::payinfo_Mixin::allow_closed_replace = 1;
# window for possible conflict is practically nonexistant,
# but just in case...
$record = $record->select_for_update;
+ if (!$record->custnum && $table eq 'cust_pay_pending') {
+ $record->set('custnum_pending',1);
+ }
+ $record->paycardtype('') if $record->paycardtype eq 'Tokenized';
+
+ local($ignore_expired_card) = 1;
+ local($ignore_banned_card) = 1;
+ local($skip_fuzzyfiles) = 1;
+ local($import) = 1;#prevent automatic geocoding (need its own variable?)
+
my $error = $record->replace;
- die $error if $error;
+ die "Error replacing $table ".$record->get($record->primary_key).": $error" if $error;
}
}
- FS::upgrade_journal->set_done('encryption_check');
+ FS::upgrade_journal->set_done('paycardtype_Tokenized');
+ FS::upgrade_journal->set_done('encryption_check') if $conf->exists('encryption');
}
+ # now that everything's encrypted, tokenize...
+ FS::cust_main::Billing_Realtime::token_check(@_);
}
# not entirely false laziness w/ Billing_Realtime::_token_check_next_recnum
my $recnum = shift @$recnums;
return $recnum if $recnum;
my $tclass = 'FS::'.$table;
+ my $paycardtypecheck = ($table ne 'cust_pay_pending') ? q( OR paycardtype = 'Tokenized') : '';
my $sql = 'SELECT '.$tclass->primary_key.
' FROM '.$table.
' WHERE '.$tclass->primary_key.' > '.$$lastrecnum.
- ' ORDER BY '.$tclass->primary_key.' LIMIT 500';;
+ " AND payby IN ( 'CARD', 'DCRD', 'CHEK', 'DCHK' ) ".
+ " AND ( length(payinfo) < 80$paycardtypecheck ) ".
+ ' ORDER BY '.$tclass->primary_key.' LIMIT 500';
my $sth = $dbh->prepare($sql) or die $dbh->errstr;
$sth->execute() or die $sth->errstr;
my @recnums;