use Scalar::Util qw( blessed );
use List::Util qw( min );
use Time::Local qw(timelocal);
+use Storable qw(thaw);
+use MIME::Base64;
use Data::Dumper;
use Tie::IxHash;
use Digest::MD5 qw(md5_base64);
use FS::part_pkg_taxrate;
use FS::agent;
use FS::cust_main_invoice;
+use FS::cust_tag;
use FS::cust_credit_bill;
use FS::cust_bill_pay;
use FS::prepay_credit;
use FS::part_pkg;
use FS::part_event;
use FS::part_event_condition;
+use FS::part_export;
#use FS::cust_event;
use FS::type_pkgs;
use FS::payment_gateway;
$self->invoicing_list( $invoicing_list );
}
+ warn " setting customer tags\n"
+ if $DEBUG > 1;
+
+ foreach my $tagnum ( @{ $self->tagnum || [] } ) {
+ my $cust_tag = new FS::cust_tag { 'tagnum' => $tagnum,
+ 'custnum' => $self->custnum };
+ my $error = $cust_tag->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ if ( $invoicing_list ) {
+ $error = $self->check_invoicing_list( $invoicing_list );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ #return "checking invoicing_list (transaction rolled back): $error";
+ return $error;
+ }
+ $self->invoicing_list( $invoicing_list );
+ }
+
+
warn " setting cust_main_exemption\n"
if $DEBUG > 1;
}
}
+ # cust_main exports!
+ warn " exporting\n" if $DEBUG > 1;
+
+ my $export_args = $options{'export_args'} || [];
+
+ my @part_export =
+ map qsearch( 'part_export', {exportnum=>$_} ),
+ $conf->config('cust_main-exports'); #, $agentnum
+
+ foreach my $part_export ( @part_export ) {
+ my $error = $part_export->export_insert($self, @$export_args);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "exporting to ". $part_export->exporttype.
+ " (transaction rolled back): $error";
+ }
+ }
+
+ #foreach my $depend_jobnum ( @$depend_jobnums ) {
+ # warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
+ # if $DEBUG;
+ # foreach my $jobnum ( @jobnums ) {
+ # my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
+ # warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
+ # if $DEBUG;
+ # my $error = $queue->depend_insert($depend_jobnum);
+ # if ( $error ) {
+ # $dbh->rollback if $oldAutoCommit;
+ # return "error queuing job dependancy: $error";
+ # }
+ # }
+ # }
+ #
+ #}
+ #
+ #if ( exists $options{'jobnums'} ) {
+ # push @{ $options{'jobnums'} }, @jobnums;
+ #}
+
warn " insert complete; committing transaction\n"
if $DEBUG > 1;
}
}
- foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
- qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
- ) {
- my $error = $cust_main_invoice->delete;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- }
-
- foreach my $cust_main_exemption (
- qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } )
- ) {
- my $error = $cust_main_exemption->delete;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
+ foreach my $table (qw( cust_main_invoice cust_main_exemption cust_tag )) {
+ foreach my $record ( qsearch( 'table', { 'custnum' => $self->custnum } ) ) {
+ my $error = $record->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
}
}
return $error;
}
+ # cust_main exports!
+
+ #my $export_args = $options{'export_args'} || [];
+
+ my @part_export =
+ map qsearch( 'part_export', {exportnum=>$_} ),
+ $conf->config('cust_main-exports'); #, $agentnum
+
+ foreach my $part_export ( @part_export ) {
+ my $error = $part_export->export_delete( $self ); #, @$export_args);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "exporting to ". $part_export->exporttype.
+ " (transaction rolled back): $error";
+ }
+ }
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
$self->invoicing_list( $invoicing_list );
}
+ if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
+
+ #this could be more efficient than deleting and re-inserting, if it matters
+ foreach my $cust_tag (qsearch('cust_tag', {'custnum'=>$self->custnum} )) {
+ my $error = $cust_tag->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ foreach my $tagnum ( @{ $self->tagnum || [] } ) {
+ my $cust_tag = new FS::cust_tag { 'tagnum' => $tagnum,
+ 'custnum' => $self->custnum };
+ my $error = $cust_tag->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ }
+
my %options = @param;
my $tax_exemption = delete $options{'tax_exemption'};
}
}
+ # cust_main exports!
+
+ my $export_args = $options{'export_args'} || [];
+
+ my @part_export =
+ map qsearch( 'part_export', {exportnum=>$_} ),
+ $conf->config('cust_main-exports'); #, $agentnum
+
+ foreach my $part_export ( @part_export ) {
+ my $error = $part_export->export_replace( $self, $old, @$export_args);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "exporting to ". $part_export->exporttype.
+ " (transaction rolled back): $error";
+ }
+ }
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
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;
}
qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
}
+=item agent_name
+
+Returns the agent name (see L<FS::agent>) for this customer.
+
+=cut
+
+sub agent_name {
+ my $self = shift;
+ $self->agent->agent;
+}
+
+=item cust_tag
+
+Returns any tags associated with this customer, as FS::cust_tag objects,
+or an empty list if there are no tags.
+
+=cut
+
+sub cust_tag {
+ my $self = shift;
+ qsearch('cust_tag', { 'custnum' => $self->custnum } );
+}
+
+=item part_tag
+
+Returns any tags associated with this customer, as FS::part_tag objects,
+or an empty list if there are no tags.
+
+=cut
+
+sub part_tag {
+ my $self = shift;
+ map $_->part_tag, $self->cust_tag;
+}
+
+
=item cust_class
Returns the customer class, as an FS::cust_class object, or the empty string
Debugging level. Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
+=item job
+
+Optional FS::queue entry to receive status updates.
+
=back
Options are passed to the B<bill> and B<collect> methods verbatim, so all
#pre-printing invoices
$options{'actual_time'} ||= time;
+ my $job = $options{'job'};
+ $job->update_statustext('0,cleaning expired packages') if $job;
$error = $self->cancel_expired_pkgs( $options{actual_time} );
if ( $error ) {
$error = "Error expiring custnum ". $self->custnum. ": $error";
else { warn $error; }
}
+ $job->update_statustext('20,billing packages') if $job;
$error = $self->bill( %options );
if ( $error ) {
$error = "Error billing custnum ". $self->custnum. ": $error";
else { warn $error; }
}
+ $job->update_statustext('50,applying payments and credits') if $job;
$error = $self->apply_payments_and_credits;
if ( $error ) {
$error = "Error applying custnum ". $self->custnum. ": $error";
else { warn $error; }
}
+ $job->update_statustext('70,running collection events') if $job;
unless ( $conf->exists('cancelled_cust-noevents')
&& ! $self->num_ncancelled_pkgs
) {
else { warn $error; }
}
}
+ $job->update_statustext('100,finished') if $job;
'';
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ warn "$me acquiring lock on customer ". $self->custnum. "\n"
+ if $DEBUG;
+
$self->select_for_update; #mutex
+ warn "$me running pre-bill events for customer ". $self->custnum. "\n"
+ if $DEBUG;
+
my $error = $self->do_cust_event(
'debug' => ( $options{'debug'} || 0 ),
'time' => $invoice_time,
return $error;
}
+ warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
+ if $DEBUG;
+
#keep auto-charge and non-auto-charge line items separate
my @passes = ( '', 'no_auto' );
}
}
- my $error = $self->do_cust_event(
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ #never want to roll back an event just because it returned an error
+ local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+
+ $self->do_cust_event(
'debug' => ( $options{'debug'} || 0 ),
'time' => $invoice_time,
'check_freq' => $options{'check_freq'},
'stage' => 'collect',
);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
- '';
}
return $due_cust_event;
}
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ #never want to roll back an event just because it or a different one
+ # returned an error
+ local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+
foreach my $cust_event ( @$due_cust_event ) {
#XXX lock event
unless ( $cust_event->test_conditions( 'time' => $time ) ) {
#don't leave stray "new/locked" records around
my $error = $cust_event->delete;
- if ( $error ) {
- #gah, even with transactions
- $dbh->commit if $oldAutoCommit; #well.
- return $error;
- }
+ return $error if $error;
next;
}
warn " running cust_event ". $cust_event->eventnum. "\n"
if $DEBUG > 1;
-
#if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
if ( my $error = $cust_event->do_event() ) {
#XXX wtf is this? figure out a proper dealio with return value
#from do_event
- # gah, even with transactions.
- $dbh->commit if $oldAutoCommit; #well.
- return $error;
- }
+ return $error;
+ }
}
}
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
}
&& ! grep { $transaction->error_message =~ /$_/ }
$conf->config('emaildecline-exclude')
) {
- my @templ = $conf->config('declinetemplate');
- my $template = new Text::Template (
- TYPE => 'ARRAY',
- SOURCE => [ map "$_\n", @templ ],
- ) or return "($perror) can't create template: $Text::Template::ERROR";
- $template->compile()
- or return "($perror) can't compile template: $Text::Template::ERROR";
-
- my $templ_hash = {
- 'company_name' =>
- scalar( $conf->config('company_name', $self->agentnum ) ),
- 'company_address' =>
- join("\n", $conf->config('company_address', $self->agentnum ) ),
- 'error' => $transaction->error_message,
- };
- my $error = send_email(
- 'from' => $conf->config('invoice_from', $self->agentnum ),
- 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
- 'subject' => 'Your payment could not be processed',
- 'body' => [ $template->fill_in(HASH => $templ_hash) ],
- );
+ # Send a decline alert to the customer.
+ my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
+ my $error = '';
+ if ( $msgnum ) {
+ my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
+ $error = $msg_template->send( 'cust_main' => $self );
+ }
+ else { #!$msgnum
+
+ my @templ = $conf->config('declinetemplate');
+ my $template = new Text::Template (
+ TYPE => 'ARRAY',
+ SOURCE => [ map "$_\n", @templ ],
+ ) or return "($perror) can't create template: $Text::Template::ERROR";
+ $template->compile()
+ or return "($perror) can't compile template: $Text::Template::ERROR";
+
+ my $templ_hash = {
+ 'company_name' =>
+ scalar( $conf->config('company_name', $self->agentnum ) ),
+ 'company_address' =>
+ join("\n", $conf->config('company_address', $self->agentnum ) ),
+ 'error' => $transaction->error_message,
+ };
+
+ my $error = send_email(
+ 'from' => $conf->config('invoice_from', $self->agentnum ),
+ 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
+ 'subject' => 'Your payment could not be processed',
+ 'body' => [ $template->fill_in(HASH => $templ_hash) ],
+ );
+ }
$perror .= " (also received error sending decline notification: $error)"
if $error;
my $self = shift;
my $time = shift;
-# my $custnum = $self->custnum;
-#
-# my $owed_sql = FS::cust_bill->owed_sql;
-#
-# my $sql = "
-# SELECT SUM($owed_sql) FROM cust_bill
-# WHERE custnum = $custnum
-# AND _date <= $time
-# ";
-#
-# my $sth = dbh->prepare($sql) or die dbh->errstr;
-# $sth->execute() or die $sth->errstr;
-#
-# return sprintf( '%.2f', $sth->fetchrow_arrayref->[0] );
+ my $custnum = $self->custnum;
- my $total_bill = 0;
- foreach my $cust_bill (
- grep { $_->_date <= $time }
- qsearch('cust_bill', { 'custnum' => $self->custnum, } )
- ) {
- $total_bill += $cust_bill->owed;
- }
- sprintf( "%.2f", $total_bill );
+ my $owed_sql = FS::cust_bill->owed_sql;
+
+ my $sql = "
+ SELECT SUM($owed_sql) FROM cust_bill
+ WHERE custnum = $custnum
+ AND _date <= $time
+ ";
+
+ sprintf( "%.2f", $self->scalar_sql($sql) );
}
sub total_unapplied_credits {
my $self = shift;
- my $total_credit = 0;
- $total_credit += $_->credited foreach $self->cust_credit;
- sprintf( "%.2f", $total_credit );
+
+ my $custnum = $self->custnum;
+
+ my $unapplied_sql = FS::cust_credit->unapplied_sql;
+
+ my $sql = "
+ SELECT SUM($unapplied_sql) FROM cust_credit
+ WHERE custnum = $custnum
+ ";
+
+ sprintf( "%.2f", $self->scalar_sql($sql) );
+
}
=item total_unapplied_credits_pkgnum PKGNUM
sub total_unapplied_payments {
my $self = shift;
- my $total_unapplied = 0;
- $total_unapplied += $_->unapplied foreach $self->cust_pay;
- sprintf( "%.2f", $total_unapplied );
+
+ my $custnum = $self->custnum;
+
+ my $unapplied_sql = FS::cust_pay->unapplied_sql;
+
+ my $sql = "
+ SELECT SUM($unapplied_sql) FROM cust_pay
+ WHERE custnum = $custnum
+ ";
+
+ sprintf( "%.2f", $self->scalar_sql($sql) );
+
}
=item total_unapplied_payments_pkgnum PKGNUM
sub total_unapplied_refunds {
my $self = shift;
- my $total_unapplied = 0;
- $total_unapplied += $_->unapplied foreach $self->cust_refund;
- sprintf( "%.2f", $total_unapplied );
+ my $custnum = $self->custnum;
+
+ my $unapplied_sql = FS::cust_refund->unapplied_sql;
+
+ my $sql = "
+ SELECT SUM($unapplied_sql) FROM cust_refund
+ WHERE custnum = $custnum
+ ";
+
+ sprintf( "%.2f", $self->scalar_sql($sql) );
+
}
=item balance
sub balance {
my $self = shift;
- sprintf( "%.2f",
- $self->total_owed
- + $self->total_unapplied_refunds
- - $self->total_unapplied_credits
- - $self->total_unapplied_payments
- );
+ $self->balance_date_range;
}
=item balance_date TIME
sub balance_date {
my $self = shift;
- my $time = shift;
- sprintf( "%.2f",
- $self->total_owed_date($time)
- + $self->total_unapplied_refunds
- - $self->total_unapplied_credits
- - $self->total_unapplied_payments
- );
+ $self->balance_date_range(shift);
}
-=item balance_date_range START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
+=item balance_date_range [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
-Returns the balance for this customer, only considering invoices with date
-earlier than START_TIME, and optionally not later than END_TIME
+Returns the balance for this customer, optionally considering invoices with
+date earlier than START_TIME, and not later than END_TIME
(total_owed_date minus total_unapplied_credits minus total_unapplied_payments).
Times are specified as SQL fragments or numeric
=item prospect - No packages have ever been ordered
+=item ordered - Recurring packages all are new (not yet billed).
+
=item active - One or more recurring packages is active
=item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
sub cust_status {
my $self = shift;
- for my $status (qw( prospect active inactive suspended cancelled )) {
+ # prospect ordered active inactive suspended cancelled
+ for my $status ( FS::cust_main->statuses() ) {
my $method = $status.'_sql';
my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
tie %statuscolor, 'Tie::IxHash',
'prospect' => '7e0079', #'000000', #black? naw, purple
'active' => '00CC00', #green
+ 'ordered' => '009999', #teal? cyan?
'inactive' => '0000CC', #blue
'suspended' => 'FF9900', #yellow
'cancelled' => 'FF0000', #red
$select_count_pkgs;
}
-sub prospect_sql { "
- 0 = ( $select_count_pkgs )
-"; }
+sub prospect_sql {
+ " 0 = ( $select_count_pkgs ) ";
+}
+
+=item ordered_sql
+
+Returns an SQL expression identifying ordered cust_main records (customers with
+recurring packages not yet setup).
+
+=cut
+
+sub ordered_sql {
+ FS::cust_main->none_active_sql.
+ " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->ordered_sql. " ) ";
+}
=item active_sql
=cut
-sub active_sql { "
- 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. "
- )
-"; }
+sub active_sql {
+ " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
+}
+
+=item none_active_sql
+
+Returns an SQL expression identifying cust_main records with no active
+recurring packages. This includes customers of status prospect, ordered,
+inactive, and suspended.
+
+=cut
+
+sub none_active_sql {
+ " 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
+}
=item inactive_sql
=cut
-sub inactive_sql { "
- 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
- AND
- 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
-"; }
+sub inactive_sql {
+ FS::cust_main->none_active_sql.
+ " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " ) ";
+}
=item susp_sql
=item suspended_sql
sub suspended_sql { susp_sql(@_); }
-sub susp_sql { "
- 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " )
- AND
- 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
-"; }
+sub susp_sql {
+ FS::cust_main->none_active_sql.
+ " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " ) ";
+}
=item cancel_sql
=item cancelled_sql
WHERE cust_refund.custnum = cust_main.custnum )
"; }
-=item balance_date_sql START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
+=item balance_date_sql [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
-Returns an SQL fragment to retreive the balance for this customer, only
-considering invoices with date earlier than START_TIME, and optionally not
+Returns an SQL fragment to retreive the balance for this customer, optionally
+considering invoices with date earlier than START_TIME, and not
later than END_TIME (total_owed_date minus total_unapplied_credits minus
total_unapplied_payments).
# parse status
##
- #prospect active inactive suspended cancelled
+ #prospect ordered active inactive suspended cancelled
if ( grep { $params->{'status'} eq $_ } FS::cust_main->statuses() ) {
my $method = $params->{'status'}. '_sql';
#push @where, $class->$method();
my $subject = delete $params->{subject};
my $html_body = delete $params->{html_body};
my $text_body = delete $params->{text_body};
+ my $error = '';
- my $job = delete $params->{'job'};
+ my $job = delete $params->{'job'}
+ or die "email_search_result must run from the job queue.\n";
$params->{'payby'} = [ split(/\0/, $params->{'payby'}) ]
unless ref($params->{'payby'});
my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
+ my @retry_jobs = ();
+ my $success = 0;
#eventually order+limit magic to reduce memory use?
foreach my $cust_main ( qsearch($sql_query) ) {
+ #progressbar first, so that the count is right
+ $num++;
+ if ( time - $min_sec > $last ) {
+ my $error = $job->update_statustext(
+ int( 100 * $num / $num_cust )
+ );
+ die $error if $error;
+ $last = time;
+ }
+
my $to = $cust_main->invoicing_list_emailonly_scalar;
- next unless $to;
- my $error = send_email(
- generate_email(
+ if( $to ) {
+ my @message = (
'from' => $from,
'to' => $to,
'subject' => $subject,
'html_body' => $html_body,
'text_body' => $text_body,
- )
- );
- return $error if $error;
+ );
- if ( $job ) { #progressbar foo
- $num++;
- if ( time - $min_sec > $last ) {
- my $error = $job->update_statustext(
- int( 100 * $num / $num_cust )
- );
- die $error if $error;
- $last = time;
+ $error = send_email( generate_email( @message ) );
+
+ if($error) {
+ # queue the sending of this message so that the user can see what we
+ # tried to do, and retry if desired
+ my $queue = new FS::queue {
+ 'job' => 'FS::Misc::process_send_email',
+ 'custnum' => $cust_main->custnum,
+ 'status' => 'failed',
+ 'statustext' => $error,
+ };
+ $queue->insert(@message);
+ push @retry_jobs, $queue;
+ }
+ else {
+ $success++;
}
}
+ if($success == 0 and
+ (scalar(@retry_jobs) > 10 or $num == $num_cust)
+ ) {
+ # 10 is arbitrary, but if we have enough failures, that's
+ # probably a configuration or network problem, and we
+ # abort the batch and run away screaming.
+ # We NEVER do this if anything was successfully sent.
+ $_->delete foreach (@retry_jobs);
+ return "multiple failures: '$error'\n";
+ }
+ }
+
+ if(@retry_jobs) {
+ # fail the job, but with a status message that makes it clear
+ # something was sent.
+ return "Sent $success, failed ".scalar(@retry_jobs).". Failed attempts placed in job queue.\n";
}
return '';
}
-use Storable qw(thaw);
-use Data::Dumper;
-use MIME::Base64;
sub process_email_search_result {
my $job = shift;
#warn "$me process_re_X $method for job $job\n" if $DEBUG;
=item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
+Deprecated. Use event notification and message templates
+(L<FS::msg_template>) instead.
+
Sends a templated email notification to the customer (see L<Text::Template>).
OPTIONS is a hash and may include
$cust_main->bill_and_collect( %args );
}
+sub process_bill_and_collect {
+ my $job = shift;
+ my $param = thaw(decode_base64(shift));
+ my $cust_main = qsearchs( 'cust_main', { custnum => $param->{'custnum'} } )
+ or die "custnum '$param->{custnum}' not found!\n";
+ $param->{'job'} = $job;
+ $param->{'fatal'} = 1; # runs from job queue, will be caught
+ $param->{'retry'} = 1;
+
+ $cust_main->bill_and_collect( %$param );
+}
+
sub _upgrade_data { #class method
my ($class, %opts) = @_;
$sth->execute or die $sth->errstr;
local($ignore_expired_card) = 1;
+ local($skip_fuzzyfiles) = 1;
$class->_upgrade_otaker(%opts);
}