use FS::cust_bill_pkg;
use FS::cust_bill_pkg_display;
use FS::cust_bill_pkg_tax_location;
+use FS::cust_bill_pkg_tax_rate_location;
use FS::cust_pay;
use FS::cust_pay_pending;
use FS::cust_pay_void;
use FS::cust_main_county;
use FS::cust_location;
use FS::tax_rate;
+use FS::tax_rate_location;
use FS::cust_tax_location;
use FS::part_pkg_taxrate;
use FS::agent;
$ignore_expired_card = 0;
@encrypted_fields = ('payinfo', 'paycvv');
+sub nohistory_fields { ('paycvv'); }
+
@paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
#ask FS::UID to run this stuff for us later
specific job completes). This can be used to defer provisioning until some
action completes (such as running the customer's credit card successfully).
+=item ticket_subject
+
+Optional subject for a ticket created and attached to this customer
+
+=item ticket_subject
+
+Optional queue name for ticket additions
+
=back
=cut
$svc_options{'depend_jobnum'} = $opt->{'depend_jobnum'}
if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'};
+ my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () }
+ qw( ticket_subject ticket_queue );
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
$cust_pkg->custnum( $self->custnum );
- my $error = $cust_pkg->insert;
+ my $error = $cust_pkg->insert( %insert_params );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "inserting cust_pkg (transaction rolled back): $error";
$self->payname($1);
}
- foreach my $flag (qw( tax spool_cdr squelch_cdr )) {
+ foreach my $flag (qw( tax spool_cdr squelch_cdr archived )) {
$self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
$self->$flag($1);
}
my $self = shift;
my $extra_qsearch = ref($_[0]) ? shift : {};
- return $self->num_pkgs unless wantarray; #XXX doesn't work w/$extra_qsearch
+ return $self->num_pkgs unless wantarray || keys(%$extra_qsearch);
my @cust_pkg = ();
if ( $self->{'_pkgnum'} ) {
@cust_pkg = values %{ $self->{'_pkgnum'}->cache };
} else {
- @cust_pkg = qsearch({
- %$extra_qsearch,
- 'table' => 'cust_pkg',
- 'hashref' => { 'custnum' => $self->custnum },
- });
+ @cust_pkg = $self->_cust_pkg($extra_qsearch);
}
sort sort_packages @cust_pkg;
$extra_qsearch->{'extra_sql'} .= ' AND ( cancel IS NULL OR cancel = 0 ) ';
- @cust_pkg = qsearch({
- %$extra_qsearch,
- 'table' => 'cust_pkg',
- 'hashref' => { 'custnum' => $self->custnum },
- });
+ @cust_pkg = $self->_cust_pkg($extra_qsearch);
}
}
+sub _cust_pkg {
+ my $self = shift;
+ my $extra_qsearch = ref($_[0]) ? shift : {};
+
+ $extra_qsearch->{'select'} ||= '*';
+ $extra_qsearch->{'select'} .=
+ ',( SELECT COUNT(*) FROM cust_svc WHERE cust_pkg.pkgnum = cust_svc.pkgnum )
+ AS _num_cust_svc';
+
+ map {
+ $_->{'_num_cust_svc'} = $_->get('_num_cust_svc');
+ $_;
+ }
+ qsearch({
+ %$extra_qsearch,
+ 'table' => 'cust_pkg',
+ 'hashref' => { 'custnum' => $self->custnum },
+ });
+
+}
+
# This should be generalized to use config options to determine order.
sub sort_packages {
#shouldn't get here...
return 0;
} else {
+ my $a_num_cust_svc = $a->num_cust_svc;
+ my $b_num_cust_svc = $b->num_cust_svc;
+ return 0 if !$a_num_cust_svc && !$b_num_cust_svc;
+ return -1 if $a_num_cust_svc && !$b_num_cust_svc;
+ return 1 if !$a_num_cust_svc && $b_num_cust_svc;
my @a_cust_svc = $a->cust_svc;
my @b_cust_svc = $b->cust_svc;
- return 0 if !scalar(@a_cust_svc) && !scalar(@b_cust_svc);
- return -1 if scalar(@a_cust_svc) && !scalar(@b_cust_svc);
- return 1 if !scalar(@a_cust_svc) && scalar(@b_cust_svc);
$a_cust_svc[0]->svc_x->label cmp $b_cust_svc[0]->svc_x->label;
}
sub bill_and_collect {
my( $self, %options ) = @_;
- ###
- # cancel packages
- ###
-
#$options{actual_time} not $options{time} because freeside-daily -d is for
#pre-printing invoices
- my @cancel_pkgs = grep { $_->expire && $_->expire <= $options{actual_time} }
+ $self->cancel_expired_pkgs( $options{actual_time} );
+ $self->suspend_adjourned_pkgs( $options{actual_time} );
+
+ my $error = $self->bill( %options );
+ warn "Error billing, custnum ". $self->custnum. ": $error" if $error;
+
+ $self->apply_payments_and_credits;
+
+ unless ( $conf->exists('cancelled_cust-noevents')
+ && ! $self->num_ncancelled_pkgs
+ ) {
+
+ $error = $self->collect( %options );
+ warn "Error collecting, custnum". $self->custnum. ": $error" if $error;
+
+ }
+
+}
+
+sub cancel_expired_pkgs {
+ my ( $self, $time ) = @_;
+
+ my @cancel_pkgs = grep { $_->expire && $_->expire <= $time }
$self->ncancelled_pkgs;
foreach my $cust_pkg ( @cancel_pkgs ) {
my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
- my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
+ my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
'reason_otaker' => $cpr->otaker
)
: ()
if $error;
}
- ###
- # suspend packages
- ###
+}
+
+sub suspend_adjourned_pkgs {
+ my ( $self, $time ) = @_;
- #$options{actual_time} not $options{time} because freeside-daily -d is for
- #pre-printing invoices
my @susp_pkgs =
grep { ! $_->susp
&& ( ( $_->part_pkg->is_prepaid
&& $_->bill
- && $_->bill < $options{actual_time}
+ && $_->bill < $time
)
|| ( $_->adjourn
- && $_->adjourn <= $options{actual_time}
+ && $_->adjourn <= $time
)
)
}
if $error;
}
- ###
- # bill and collect
- ###
-
- my $error = $self->bill( %options );
- warn "Error billing, custnum ". $self->custnum. ": $error" if $error;
-
- $self->apply_payments_and_credits;
-
- $error = $self->collect( %options );
- warn "Error collecting, custnum". $self->custnum. ": $error" if $error;
-
}
=item bill OPTIONS
return '';
}
- my $postal_pkg = $self->charge_postal_fee();
- if ( $postal_pkg && !ref( $postal_pkg ) ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't charge postal invoice fee for customer ".
- $self->custnum. ": $postal_pkg";
- }
- if ( $postal_pkg &&
- ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
+ if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
!$conf->exists('postal_invoice-recurring_only')
- )
)
{
+
+ my $postal_pkg = $self->charge_postal_fee();
+ if ( $postal_pkg && !ref( $postal_pkg ) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't charge postal invoice fee for customer ".
+ $self->custnum. ": $postal_pkg";
+ }
+
foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) {
my $error =
$self->_make_lines( 'part_pkg' => $part_pkg,
# values are listrefs of cust_bill_pkg_tax_location hashrefs
my %tax_location = ();
+ # keys are taxlisthash keys (internal identifiers)
+ # values are listrefs of cust_bill_pkg_tax_rate_location hashrefs
+ my %tax_rate_location = ();
+
foreach my $tax ( keys %taxlisthash ) {
my $tax_object = shift @{ $taxlisthash{$tax} };
warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
+ warn " ". join('/', @{ $taxlisthash{$tax} } ). "\n" if $DEBUG > 2;
my $hashref_or_error =
$tax_object->taxline( $taxlisthash{$tax},
'custnum' => $self->custnum,
};
}
+ $tax_rate_location{ $tax } ||= [];
+ if ( ref($tax_object) eq 'FS::tax_rate' ) {
+ my $taxratelocationnum =
+ $tax_object->tax_rate_location->taxratelocationnum;
+ push @{ $tax_rate_location{ $tax } },
+ {
+ 'taxnum' => $tax_object->taxnum,
+ 'taxtype' => ref($tax_object),
+ 'amount' => sprintf('%.2f', $amount ),
+ 'locationtaxid' => $tax_object->location,
+ 'taxratelocationnum' => $taxratelocationnum,
+ };
+ }
+
}
#move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg;
foreach my $tax ( keys %taxlisthash ) {
foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) {
- next unless ref($_) eq 'FS::cust_bill_pkg'; # shouldn't happen
+ next unless ref($_) eq 'FS::cust_bill_pkg';
push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
splice( @{ $_->_cust_tax_exempt_pkg } );
}
}
- #some taxes are taxed
- my %totlisthash;
-
- warn "finding taxed taxes...\n" if $DEBUG > 2;
- foreach my $tax ( keys %taxlisthash ) {
- my $tax_object = shift @{ $taxlisthash{$tax} };
- warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
- if $DEBUG > 2;
- next unless $tax_object->can('tax_on_tax');
-
- foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
- my $totname = ref( $tot ). ' '. $tot->taxnum;
-
- warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
- if $DEBUG > 2;
- next unless exists( $taxlisthash{ $totname } ); # only increase
- # existing taxes
- warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
- if ( exists( $totlisthash{ $totname } ) ) {
- push @{ $totlisthash{ $totname } }, $tax{ $tax };
- }else{
- $totlisthash{ $totname } = [ $tot, $tax{ $tax } ];
- }
- }
- }
-
- warn "having a look at taxed taxes...\n" if $DEBUG > 2;
- foreach my $tax ( keys %totlisthash ) {
- my $tax_object = shift @{ $totlisthash{$tax} };
- warn "found previously found taxed tax ". $tax_object->taxname. "\n"
- if $DEBUG > 2;
- my $listref_or_error =
- $tax_object->taxline( $totlisthash{$tax},
- 'custnum' => $self->custnum,
- 'invoice_time' => $invoice_time
- );
- unless (ref($listref_or_error)) {
- $dbh->rollback if $oldAutoCommit;
- return $listref_or_error;
- }
-
- warn "adding taxed tax amount ". $listref_or_error->[1].
- " as ". $tax_object->taxname. "\n"
- if $DEBUG;
- $tax{ $tax } += $listref_or_error->[1];
- }
-
#consolidate and create tax line items
warn "consolidating and generating...\n" if $DEBUG > 2;
foreach my $taxname ( keys %taxname ) {
my $tax = 0;
my %seen = ();
my @cust_bill_pkg_tax_location = ();
+ my @cust_bill_pkg_tax_rate_location = ();
warn "adding $taxname\n" if $DEBUG > 1;
foreach my $taxitem ( @{ $taxname{$taxname} } ) {
next if $seen{$taxitem}++;
push @cust_bill_pkg_tax_location,
map { new FS::cust_bill_pkg_tax_location $_ }
@{ $tax_location{ $taxitem } };
+ push @cust_bill_pkg_tax_rate_location,
+ map { new FS::cust_bill_pkg_tax_rate_location $_ }
+ @{ $tax_rate_location{ $taxitem } };
}
next unless $tax;
'edate' => '',
'itemdesc' => $taxname,
'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
+ 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
};
}
my $total_recur = $params{recur} or die "no recur accumulator specified";
my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
my $time = $params{'time'} or die "no time specified";
- my (%options) = %{$params{options}}; #hmmm only for 'resetup'
+ my (%options) = %{$params{options}};
my $dbh = dbh;
my $real_pkgpart = $cust_pkg->pkgpart;
###
my $error =
- $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg);
+ $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time});
return $error if $error;
push @$cust_bill_pkgs, $cust_bill_pkg;
my $taxlisthash = shift;
my $cust_bill_pkg = shift;
my $cust_pkg = shift;
+ my $invoice_time = shift;
my %cust_bill_pkg = ();
my %taxes = ();
my @taxes = @{ $taxes{$key} || [] };
my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
+ my %localtaxlisthash = ();
foreach my $tax ( @taxes ) {
- my $taxname = ref( $tax ). ' taxnum'. $tax->taxnum;
+ my $taxname = ref( $tax ). ' '. $tax->taxnum;
# $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
# ' locationnum'. $cust_pkg->locationnum
# if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
- if ( exists( $taxlisthash->{ $taxname } ) ) {
- push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
- }else{
- $taxlisthash->{ $taxname } = [ $tax, $tax_cust_bill_pkg ];
+ $taxlisthash->{ $taxname } ||= [ $tax ];
+ push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
+
+ $localtaxlisthash{ $taxname } ||= [ $tax ];
+ push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
+
+ }
+
+ warn "finding taxed taxes...\n" if $DEBUG > 2;
+ foreach my $tax ( keys %localtaxlisthash ) {
+ my $tax_object = shift @{ $localtaxlisthash{$tax} };
+ warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
+ if $DEBUG > 2;
+ next unless $tax_object->can('tax_on_tax');
+
+ foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
+ my $totname = ref( $tot ). ' '. $tot->taxnum;
+
+ warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
+ if $DEBUG > 2;
+ next unless exists( $localtaxlisthash{ $totname } ); # only increase
+ # existing taxes
+ warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
+ my $hashref_or_error =
+ $tax_object->taxline( $localtaxlisthash{$tax},
+ 'custnum' => $self->custnum,
+ 'invoice_time' => $invoice_time,
+ );
+ return $hashref_or_error
+ unless ref($hashref_or_error);
+
+ $taxlisthash->{ $totname } ||= [ $tot ];
+ push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
+
}
}
+
}
'';
return "Banned credit card" if $ban;
###
- # select a gateway
+ # set taxclass and trans_is_recur based on invnum if there is one
###
my $taxclass = '';
+ my $trans_is_recur = 0;
if ( $options{'invnum'} ) {
+
my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
- my @taxclasses =
- map { $_->part_pkg->taxclass }
+
+ my @part_pkg =
+ map { $_->part_pkg }
grep { $_ }
map { $_->cust_pkg }
$cust_bill->cust_bill_pkg;
- unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
- #different taxclasses
- $taxclass = $taxclasses[0];
- }
+
+ my @taxclasses = map $_->taxclass, @part_pkg;
+ $taxclass = $taxclasses[0]
+ unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
+ #different taxclasses
+ $trans_is_recur = 1
+ if grep { $_->freq ne '0' } @part_pkg;
+
}
+ ###
+ # select a gateway
+ ###
+
#look for an agent gateway override first
my $cardtype;
if ( $method eq 'CC' ) {
: $self->payissue;
$content{issue_number} = $payissue if $payissue;
- $content{recurring_billing} = 'YES'
- if qsearch('cust_pay', { 'custnum' => $self->custnum,
- 'payby' => 'CARD',
- 'payinfo' => $payinfo,
- } )
- || qsearch('cust_pay', { 'custnum' => $self->custnum,
- 'payby' => 'CARD',
- 'paymask' => $self->mask_payinfo('CARD', $payinfo),
- } );
-
+ if ( $self->_bop_recurring_billing( 'payinfo' => $payinfo,
+ 'trans_is_recur' => $trans_is_recur,
+ )
+ )
+ {
+ $content{recurring_billing} = 'YES';
+ $content{acct_code} = 'rebill'
+ if $conf->exists('credit_card-recurring_billing_acct_code');
+ }
} elsif ( $method eq 'ECHECK' ) {
( $content{account_number}, $content{routing_code} ) =
#okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
my $cust_pay_pending = new FS::cust_pay_pending {
- 'custnum' => $self->custnum,
- #'invnum' => $options{'invnum'},
- 'paid' => $amount,
- '_date' => '',
- 'payby' => $method2payby{$method},
- 'payinfo' => $payinfo,
- 'paydate' => $paydate,
- 'status' => 'new',
- 'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
+ 'custnum' => $self->custnum,
+ #'invnum' => $options{'invnum'},
+ 'paid' => $amount,
+ '_date' => '',
+ 'payby' => $method2payby{$method},
+ 'payinfo' => $payinfo,
+ 'paydate' => $paydate,
+ 'recurring_billing' => $content{recurring_billing},
+ 'status' => 'new',
+ 'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
};
$cust_pay_pending->payunique( $options{payunique} )
if defined($options{payunique}) && length($options{payunique});
}
+sub _bop_recurring_billing {
+ my( $self, %opt ) = @_;
+
+ my $method = $conf->config('credit_card-recurring_billing_flag');
+
+ if ( $method eq 'transaction_is_recur' ) {
+
+ return 1 if $opt{'trans_is_recur'};
+
+ } else {
+
+ my %hash = ( 'custnum' => $self->custnum,
+ 'payby' => 'CARD',
+ );
+
+ return 1
+ if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
+ || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
+ $opt{'payinfo'} )
+ } );
+
+ }
+
+ return 0;
+
+}
+
+
=item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
$self->_bop_defaults(\%options);
+ ###
+ # set trans_is_recur based on invnum if there is one
+ ###
+
+ my $trans_is_recur = 0;
+ if ( $options{'invnum'} ) {
+
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
+ die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
+
+ my @part_pkg =
+ map { $_->part_pkg }
+ grep { $_ }
+ map { $_->cust_pkg }
+ $cust_bill->cust_bill_pkg;
+
+ $trans_is_recur = 1
+ if grep { $_->freq ne '0' } @part_pkg;
+
+ }
+
###
# select a gateway
###
: $self->payissue;
$content{issue_number} = $payissue if $payissue;
- $content{recurring_billing} = 'YES'
- if qsearch('cust_pay', { 'custnum' => $self->custnum,
- 'payby' => 'CARD',
- 'payinfo' => $options{payinfo},
- } )
- || qsearch('cust_pay', { 'custnum' => $self->custnum,
- 'payby' => 'CARD',
- 'paymask' => $self->mask_payinfo('CARD', $options{payinfo}),
- } );
-
+ if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
+ 'trans_is_recur' => $trans_is_recur,
+ )
+ )
+ {
+ $content{recurring_billing} = 'YES';
+ $content{acct_code} = 'rebill'
+ if $conf->exists('credit_card-recurring_billing_acct_code');
+ }
} elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
( $content{account_number}, $content{routing_code} ) =
#okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
my $cust_pay_pending = new FS::cust_pay_pending {
- 'custnum' => $self->custnum,
- #'invnum' => $options{'invnum'},
- 'paid' => $options{amount},
- '_date' => '',
- 'payby' => $bop_method2payby{$options{method}},
- 'payinfo' => $options{payinfo},
- 'paydate' => $paydate,
- 'status' => 'new',
- 'gatewaynum' => $payment_gateway->gatewaynum || '',
- 'session_id' => $options{session_id} || '',
- 'jobnum' => $options{depend_jobnum} || '',
+ 'custnum' => $self->custnum,
+ #'invnum' => $options{'invnum'},
+ 'paid' => $options{amount},
+ '_date' => '',
+ 'payby' => $bop_method2payby{$options{method}},
+ 'payinfo' => $options{payinfo},
+ 'paydate' => $paydate,
+ 'recurring_billing' => $content{recurring_billing},
+ 'status' => 'new',
+ 'gatewaynum' => $payment_gateway->gatewaynum || '',
+ 'session_id' => $options{session_id} || '',
+ 'jobnum' => $options{depend_jobnum} || '',
};
$cust_pay_pending->payunique( $options{payunique} )
if defined($options{payunique}) && length($options{payunique});
sub cust_pay_batch {
my $self = shift;
- sort { $a->_date <=> $b->_date }
+ sort { $a->paybatchnum <=> $b->paybatchnum }
qsearch( 'cust_pay_batch', { 'custnum' => $self->custnum } )
}
);
}
+sub _upgrade_data { #class method
+ my ($class, %opts) = @_;
+
+ my $sql = 'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL';
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+
+}
+
=back
=head1 BUGS