FS::cust_main::Credit_Limit
FS::cust_main::Merge
FS::cust_main::API
- FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
+ FS::otaker_Mixin FS::cust_main_Mixin
FS::geocode_Mixin FS::Quotable_Mixin FS::Sales_Mixin
FS::o2m_Common
FS::Record
use Time::Local qw(timelocal);
use Data::Dumper;
use Tie::IxHash;
-use Digest::MD5 qw(md5_base64);
use Date::Format;
#use Date::Manip;
use File::Temp; #qw( tempfile );
use Business::CreditCard 0.28;
-use Locale::Country;
+use List::Util qw(min);
use FS::UID qw( dbh driver_name );
use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
use FS::Cursor;
-use FS::Misc qw( generate_email send_email generate_ps do_print );
+use FS::Misc qw( generate_ps do_print money_pretty card_types );
use FS::Msgcat qw(gettext);
use FS::CurrentUser;
use FS::TicketSystem;
use FS::banned_pay;
use FS::cust_main_note;
use FS::cust_attachment;
-use FS::contact;
+use FS::cust_contact;
use FS::Locales;
use FS::upgrade_journal;
use FS::sales;
use FS::cust_payby;
+use FS::contact;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
our $ucfirst_nowarn = 0;
+#this info is in cust_payby as of 4.x
+#this and the fields themselves can be removed in 5.x
our @encrypted_fields = ('payinfo', 'paycvv');
sub nohistory_fields { ('payinfo', 'paycvv'); }
-our @paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
-
our $conf;
+our $default_agent_custid;
+our $custnum_display_length;
#ask FS::UID to run this stuff for us later
#$FS::UID::callback{'FS::cust_main'} = sub {
install_callback FS::UID sub {
$conf = new FS::Conf;
- #yes, need it for stuff below (prolly should be cached)
- $ignore_invalid_card = $conf->exists('allow_invalid_cards');
+ $ignore_invalid_card = $conf->exists('allow_invalid_cards');
+ $default_agent_custid = $conf->exists('cust_main-default_agent_custid');
+ $custnum_display_length = $conf->config('cust_main-custnum-display_length');
};
sub _cache {
Do not call, empty or 'Y'
+=item invoice_ship_address
+
+Display ship_address ("Service address") on invoices for this customer, empty or 'Y'
+
=back
=head1 METHODS
);
$cust_main->insert( \%hash );
-INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
-be set as the invoicing list (see L<"invoicing_list">). Errors return as
-expected and rollback the entire transaction; it is not necessary to call
-check_invoicing_list first. The invoicing_list is set after the records in the
-CUST_PKG_HASHREF above are inserted, so it is now possible to set an
-invoicing_list destination to the newly-created svc_acct. Here's an example:
-
- $cust_main->insert( {}, [ $email, 'POST' ] );
+INVOICING_LIST_ARYREF: No longer supported.
Currently available options are: I<depend_jobnum>, I<noexport>,
-I<tax_exemption> and I<prospectnum>.
+I<tax_exemption>, I<prospectnum>, I<contact> and I<contact_params>.
If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
on the supplied jobnum (they will not run until the specific job completes).
If I<prospectnum> is set, moves contacts and locations from that prospect.
-If I<contact> is set to an arrayref of FS::contact objects, inserts those
-new contacts with this new customer.
+If I<contact> is set to an arrayref of FS::contact objects, those will be
+inserted.
+
+If I<contact_params> is set to a hashref of CGI parameters (and I<contact> is
+unset), inserts those new contacts with this new customer. Handles CGI
+paramaters for an "m2" multiple entry field as passed by edit/cust_main.cgi
+
+If I<cust_payby_params> is set to a hashref o fCGI parameters, inserts those
+new stored payment records with this new customer. Handles CGI parameters
+for an "m2" multiple entry field as passed by edit/cust_main.cgi
=cut
sub insert {
my $self = shift;
my $cust_pkgs = @_ ? shift : {};
- my $invoicing_list = @_ ? shift : '';
+ my $invoicing_list;
+ if ( $_[0] and ref($_[0]) eq 'ARRAY' ) {
+ warn "cust_main::insert using deprecated invoicing list argument";
+ $invoicing_list = shift;
+ }
my %options = @_;
warn "$me insert called with options ".
join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
my $payby = '';
if ( $self->payby eq 'PREPAY' ) {
- $self->payby('BILL');
+ $self->payby(''); #'BILL');
$prepay_identifier = $self->payinfo;
$self->payinfo('');
$payby = 'PREP' if $amount;
- } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|PPAL)$/ ) {
+ } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|MCHK|PPAL)$/ ) {
$payby = $1;
- $self->payby('BILL');
+ $self->payby(''); #'BILL');
$amount = $self->paid;
}
$self->auto_agent_custid()
if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
- my $error = $self->SUPER::insert;
+ my $error = $self->check_payinfo_cardtype
+ || $self->SUPER::insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
#return "inserting cust_main record (transaction rolled back): $error";
}
}
- warn " setting invoicing list\n"
- if $DEBUG > 1;
-
- 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 customer tags\n"
if $DEBUG > 1;
return $error;
}
- my @contact = $prospect_main->contact;
+ foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
+ my $cust_contact = new FS::cust_contact {
+ 'custnum' => $self->custnum,
+ map { $_ => $prospect_contact->$_() } qw( contactnum classnum comment )
+ };
+ my $error = $cust_contact->insert
+ || $prospect_contact->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
my @cust_location = $prospect_main->cust_location;
my @qual = $prospect_main->qual;
- foreach my $r ( @contact, @cust_location, @qual ) {
+ foreach my $r ( @cust_location, @qual ) {
$r->prospectnum('');
$r->custnum($self->custnum);
my $error = $r->replace;
}
- my $contact = delete $options{'contact'};
- if ( $contact ) {
+ warn " setting contacts\n"
+ if $DEBUG > 1;
+
+ $invoicing_list ||= $options{'invoicing_list'};
+ if ( $invoicing_list ) {
+
+ $invoicing_list = [ $invoicing_list ] if !ref($invoicing_list);
+
+ my $email = '';
+ foreach my $dest (@$invoicing_list ) {
+ if ($dest eq 'POST') {
+ $self->set('postal_invoice', 'Y');
+ } else {
+
+ my $contact_email = qsearchs('contact_email', { emailaddress => $dest });
+ if ( $contact_email ) {
+ my $cust_contact = FS::cust_contact->new({
+ contactnum => $contact_email->contactnum,
+ custnum => $self->custnum,
+ });
+ $cust_contact->set('invoice_dest', 'Y');
+ my $error = $cust_contact->contactnum ?
+ $cust_contact->replace : $cust_contact->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$error (linking to email address $dest)";
+ }
+
+ } else {
+ # this email address is not yet linked to any contact
+ $email .= ',' if length($email);
+ $email .= $dest;
+ }
+ }
+ }
+
+ my $contact = FS::contact->new({
+ 'custnum' => $self->get('custnum'),
+ 'last' => $self->get('last'),
+ 'first' => $self->get('first'),
+ 'emailaddress' => $email,
+ 'invoice_dest' => 'Y', # yes, you can set this via the contact
+ });
+ my $error = $contact->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ if ( my $contact = delete $options{'contact'} ) {
foreach my $c ( @$contact ) {
$c->custnum($self->custnum);
}
+ } elsif ( my $contact_params = delete $options{'contact_params'} ) {
+
+ my $error = $self->process_o2m( 'table' => 'contact',
+ 'fields' => FS::contact->cgi_contact_fields,
+ 'params' => $contact_params,
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ warn " setting cust_payby\n"
+ if $DEBUG > 1;
+
+ if ( $options{cust_payby} ) {
+
+ foreach my $cust_payby ( @{ $options{cust_payby} } ) {
+ $cust_payby->custnum($self->custnum);
+ my $error = $cust_payby->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ } elsif ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
+
+ my $error = $self->process_o2m(
+ 'table' => 'cust_payby',
+ 'fields' => FS::cust_payby->cgi_cust_payby_fields,
+ 'params' => $cust_payby_params,
+ 'hash_callback' => \&FS::cust_payby::cgi_hash_callback,
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
}
warn " setting cust_main_exemption\n"
#cust_tax_adjustment in financials?
#cust_pay_pending? ouch
- #cust_recon?
foreach my $table (qw(
cust_main_invoice cust_main_exemption cust_tag cust_attachment contact
- cust_location cust_main_note cust_tax_adjustment
+ cust_payby cust_location cust_main_note cust_tax_adjustment
cust_pay_void cust_pay_batch queue cust_tax_exempt
)) {
foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
C<ship_location>. The address will still only change if at least one of the
address fields differs from the existing values.
-INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
-be set as the invoicing list (see L<"invoicing_list">). Errors return as
-expected and rollback the entire transaction; it is not necessary to call
-check_invoicing_list first. Here's an example:
-
- $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
+INVOICING_LIST_ARYREF: If you pass an arrayref to this method, it will be
+set as the contact email address for a default contact with the same name as
+the customer.
-Currently available options are: I<tax_exemption>.
+Currently available options are: I<tax_exemption>, I<cust_payby_params>,
+I<contact_params>, I<invoicing_list>.
The I<tax_exemption> option can be set to an arrayref of tax names or a hashref
of tax names and exemption numbers. FS::cust_main_exemption records will be
deleted and inserted as appropriate.
+I<cust_payby_params> and I<contact_params> can be hashrefs of named parameter
+groups (describing the customer's payment methods and contacts, respectively)
+in the style supported by L<FS::o2m_Common/process_o2m>. See L<FS::cust_payby>
+and L<FS::contact> for the fields these can contain.
+
+I<invoicing_list> is a synonym for the INVOICING_LIST_ARYREF parameter, and
+should be used instead if possible.
+
=cut
sub replace {
if $DEBUG;
my $curuser = $FS::CurrentUser::CurrentUser;
- if ( $self->payby eq 'COMP'
- && $self->payby ne $old->payby
- && ! $curuser->access_right('Complimentary customer')
- )
- {
- return "You are not permitted to create complimentary accounts.";
- }
+ return "You are not permitted to create complimentary accounts."
+ if $self->complimentary eq 'Y'
+ && $self->complimentary ne $old->complimentary
+ && ! $curuser->access_right('Complimentary customer');
local($ignore_expired_card) = 1
if $old->payby =~ /^(CARD|DCRD)$/
|| $old->payby =~ /^(CHEK|DCHK)$/ && $self->payby =~ /^(CHEK|DCHK)$/ )
&& ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
+ if ( $self->payby =~ /^(CARD|DCRD)$/
+ && $old->payinfo ne $self->payinfo
+ && $old->paymask ne $self->paymask )
+ {
+ my $error = $self->check_payinfo_cardtype;
+ return $error if $error;
+ }
+
return "Invoicing locale is required"
if $old->locale
&& ! $self->locale
my $dbh = dbh;
for my $l (qw(bill_location ship_location)) {
- my $old_loc = $old->$l;
- my $new_loc = $self->$l;
+ #my $old_loc = $old->$l;
+ my $new_loc = $self->$l or next;
# find the existing location if there is one
$new_loc->set('custnum' => $self->custnum);
$self->set($l.'num', $new_loc->locationnum);
} #for $l
+ my $invoicing_list;
+ if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
+ warn "cust_main::replace: using deprecated invoicing list argument";
+ $invoicing_list = shift @param;
+ }
+
+ my %options = @param;
+
+ $invoicing_list ||= $options{invoicing_list};
+
+ my @contacts = map { $_->contact } $self->cust_contact;
+ # find a contact that matches the customer's name
+ my ($implicit_contact) = grep { $_->first eq $old->get('first')
+ and $_->last eq $old->get('last') }
+ @contacts;
+ $implicit_contact ||= FS::contact->new({
+ 'custnum' => $self->custnum,
+ 'locationnum' => $self->get('bill_locationnum'),
+ });
+
+ # for any of these that are already contact emails, link to the existing
+ # contact
+ if ( $invoicing_list ) {
+ my $email = '';
+
+ # kind of like process_m2m on these, except:
+ # - the other side is two tables in a join
+ # - and we might have to create new contact_emails
+ # - and possibly a new contact
+ #
+ # Find existing invoice emails that aren't on the implicit contact.
+ # Any of these that are not on the new invoicing list will be removed.
+ my %old_email_cust_contact;
+ foreach my $cust_contact ($self->cust_contact) {
+ next if !$cust_contact->invoice_dest;
+ next if $cust_contact->contactnum == ($implicit_contact->contactnum || 0);
+
+ foreach my $contact_email ($cust_contact->contact->contact_email) {
+ $old_email_cust_contact{ $contact_email->emailaddress } = $cust_contact;
+ }
+ }
+
+ foreach my $dest (@$invoicing_list) {
+
+ if ($dest eq 'POST') {
+
+ $self->set('postal_invoice', 'Y');
+
+ } elsif ( exists($old_email_cust_contact{$dest}) ) {
+
+ delete $old_email_cust_contact{$dest}; # don't need to remove it, then
+
+ } else {
+
+ # See if it belongs to some other contact; if so, link it.
+ my $contact_email = qsearchs('contact_email', { emailaddress => $dest });
+ if ( $contact_email
+ and $contact_email->contactnum != ($implicit_contact->contactnum || 0) ) {
+ my $cust_contact = qsearchs('cust_contact', {
+ contactnum => $contact_email->contactnum,
+ custnum => $self->custnum,
+ }) || FS::cust_contact->new({
+ contactnum => $contact_email->contactnum,
+ custnum => $self->custnum,
+ });
+ $cust_contact->set('invoice_dest', 'Y');
+ my $error = $cust_contact->custcontactnum ?
+ $cust_contact->replace : $cust_contact->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$error (linking to email address $dest)";
+ }
+
+ } else {
+ # This email address is not yet linked to any contact, so it will
+ # be added to the implicit contact.
+ $email .= ',' if length($email);
+ $email .= $dest;
+ }
+ }
+ }
+
+ foreach my $remove_dest (keys %old_email_cust_contact) {
+ my $cust_contact = $old_email_cust_contact{$remove_dest};
+ # These were not in the list of requested destinations, so take them off.
+ $cust_contact->set('invoice_dest', '');
+ my $error = $cust_contact->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$error (unlinking email address $remove_dest)";
+ }
+ }
+
+ # make sure it keeps up with the changed customer name, if any
+ $implicit_contact->set('last', $self->get('last'));
+ $implicit_contact->set('first', $self->get('first'));
+ $implicit_contact->set('emailaddress', $email);
+ $implicit_contact->set('invoice_dest', 'Y');
+ $implicit_contact->set('custnum', $self->custnum);
+
+ my $error;
+ if ( $implicit_contact->contactnum ) {
+ $error = $implicit_contact->replace;
+ } elsif ( length($email) ) { # don't create a new contact if not needed
+ $error = $implicit_contact->insert;
+ }
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$error (adding email address $email)";
+ }
+
+ }
+
# replace the customer record
my $error = $self->SUPER::replace($old);
}
}
- if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
- my $invoicing_list = shift @param;
- $error = $self->check_invoicing_list( $invoicing_list );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- $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
}
- my %options = @param;
-
my $tax_exemption = delete $options{'tax_exemption'};
if ( $tax_exemption ) {
}
- if ( $self->payby =~ /^(CARD|CHEK|LECB)$/
- && ( ( $self->get('payinfo') ne $old->get('payinfo')
- && $self->get('payinfo') !~ /^99\d{14}$/
- )
- || grep { $self->get($_) ne $old->get($_) } qw(paydate payname)
- )
- )
- {
+ if ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
- # card/check/lec info has changed, want to retry realtime_ invoice events
- my $error = $self->retry_realtime;
+ my $error = $self->process_o2m(
+ 'table' => 'cust_payby',
+ 'fields' => FS::cust_payby->cgi_cust_payby_fields,
+ 'params' => $cust_payby_params,
+ 'hash_callback' => \&FS::cust_payby::cgi_hash_callback,
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ if ( my $contact_params = delete $options{'contact_params'} ) {
+
+ # this can potentially replace contacts that were created by the
+ # invoicing list argument, but the UI shouldn't allow both of them
+ # to be specified
+
+ my $error = $self->process_o2m(
+ 'table' => 'contact',
+ 'fields' => FS::contact->cgi_contact_fields,
+ 'params' => $contact_params,
+ );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
+
}
unless ( $import || $skip_fuzzyfiles ) {
|| $self->ut_flag('message_noemail')
|| $self->ut_enum('locale', [ '', FS::Locales->locales ])
|| $self->ut_currencyn('currency')
+ || $self->ut_alphan('po_number')
+ || $self->ut_enum('complimentary', [ '', 'Y' ])
+ || $self->ut_flag('invoice_ship_address')
+ || $self->ut_flag('invoice_dest')
;
foreach (qw(company ship_company)) {
$self->ss("$1-$2-$3");
}
+ #turn off invoice_ship_address if ship & bill are the same
+ if ($self->bill_locationnum eq $self->ship_locationnum) {
+ $self->invoice_ship_address('');
+ }
+
# cust_main_county verification now handled by cust_location check
$error =
}
- ### start of stuff moved to cust_payby
- # then mostly kept here to support upgrades (can remove in 5.x)
- # but modified to allow everything to be empty
-
- if ( $self->payby ) {
- FS::payby->can_payby($self->table, $self->payby)
- or return "Illegal payby: ". $self->payby;
- } else {
- $self->payby('');
- }
-
- $error = $self->ut_numbern('paystart_month')
- || $self->ut_numbern('paystart_year')
- || $self->ut_numbern('payissue')
- || $self->ut_textn('paytype')
- ;
- return $error if $error;
+ return "Please select an invoicing locale"
+ if ! $self->locale
+ && ! $self->custnum
+ && $conf->exists('cust_main-require_locale');
- if ( $self->payip eq '' ) {
- $self->payip('');
- } else {
- $error = $self->ut_ip('payip');
- return $error if $error;
+ foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
+ $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
+ $self->$flag($1);
}
- # If it is encrypted and the private key is not availaible then we can't
- # check the credit card.
- my $check_payinfo = ! $self->is_encrypted($self->payinfo);
-
- # Need some kind of global flag to accept invalid cards, for testing
- # on scrubbed data.
- if ( !$import && !$ignore_invalid_card && $check_payinfo &&
- $self->payby =~ /^(CARD|DCRD)$/ ) {
-
- my $payinfo = $self->payinfo;
- $payinfo =~ s/\D//g;
- $payinfo =~ /^(\d{13,16}|\d{8,9})$/
- or return gettext('invalid_card'); # . ": ". $self->payinfo;
- $payinfo = $1;
- $self->payinfo($payinfo);
- validate($payinfo)
- or return gettext('invalid_card'); # . ": ". $self->payinfo;
-
- return gettext('unknown_card_type')
- if $self->payinfo !~ /^99\d{14}$/ #token
- && cardtype($self->payinfo) eq "Unknown";
-
- unless ( $ignore_banned_card ) {
- my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
- if ( $ban ) {
- if ( $ban->bantype eq 'warn' ) {
- #or others depending on value of $ban->reason ?
- return '_duplicate_card'.
- ': disabled from'. time2str('%a %h %o at %r', $ban->_date).
- ' until '. time2str('%a %h %o at %r', $ban->_end_date).
- ' (ban# '. $ban->bannum. ')'
- unless $self->override_ban_warn;
- } else {
- return 'Banned credit card: banned on '.
- time2str('%a %h %o at %r', $ban->_date).
- ' by '. $ban->otaker.
- ' (ban# '. $ban->bannum. ')';
- }
- }
- }
-
- if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
- if ( cardtype($self->payinfo) eq 'American Express card' ) {
- $self->paycvv =~ /^(\d{4})$/
- or return "CVV2 (CID) for American Express cards is four digits.";
- $self->paycvv($1);
- } else {
- $self->paycvv =~ /^(\d{3})$/
- or return "CVV2 (CVC2/CID) is three digits.";
- $self->paycvv($1);
- }
- } else {
- $self->paycvv('');
- }
-
- my $cardtype = cardtype($payinfo);
- if ( $cardtype =~ /^(Switch|Solo)$/i ) {
-
- return "Start date or issue number is required for $cardtype cards"
- unless $self->paystart_month && $self->paystart_year or $self->payissue;
-
- return "Start month must be between 1 and 12"
- if $self->paystart_month
- and $self->paystart_month < 1 || $self->paystart_month > 12;
-
- return "Start year must be 1990 or later"
- if $self->paystart_year
- and $self->paystart_year < 1990;
-
- return "Issue number must be beween 1 and 99"
- if $self->payissue
- and $self->payissue < 1 || $self->payissue > 99;
-
- } else {
- $self->paystart_month('');
- $self->paystart_year('');
- $self->payissue('');
- }
-
- } elsif ( !$ignore_invalid_card && $check_payinfo &&
- $self->payby =~ /^(CHEK|DCHK)$/ ) {
-
- my $payinfo = $self->payinfo;
- $payinfo =~ s/[^\d\@\.]//g;
- if ( $conf->config('echeck-country') eq 'CA' ) {
- $payinfo =~ /^(\d+)\@(\d{5})\.(\d{3})$/
- or return 'invalid echeck account@branch.bank';
- $payinfo = "$1\@$2.$3";
- } elsif ( $conf->config('echeck-country') eq 'US' ) {
- $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
- $payinfo = "$1\@$2";
- } else {
- $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@routing';
- $payinfo = "$1\@$2";
- }
- $self->payinfo($payinfo);
- $self->paycvv('');
-
- unless ( $ignore_banned_card ) {
- my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
- if ( $ban ) {
- if ( $ban->bantype eq 'warn' ) {
- #or others depending on value of $ban->reason ?
- return '_duplicate_ach' unless $self->override_ban_warn;
- } else {
- return 'Banned ACH account: banned on '.
- time2str('%a %h %o at %r', $ban->_date).
- ' by '. $ban->otaker.
- ' (ban# '. $ban->bannum. ')';
- }
- }
- }
-
- } elsif ( $self->payby eq 'LECB' ) {
-
- my $payinfo = $self->payinfo;
- $payinfo =~ s/\D//g;
- $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
- $payinfo = $1;
- $self->payinfo($payinfo);
- $self->paycvv('');
-
- } elsif ( $self->payby eq 'BILL' ) {
+ $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
- $error = $self->ut_textn('payinfo');
- return "Illegal P.O. number: ". $self->payinfo if $error;
- $self->paycvv('');
+ warn "$me check AFTER: \n". $self->_dump
+ if $DEBUG > 2;
- } elsif ( $self->payby eq 'COMP' ) {
+ $self->SUPER::check;
+}
- my $curuser = $FS::CurrentUser::CurrentUser;
- if ( ! $self->custnum
- && ! $curuser->access_right('Complimentary customer')
- )
- {
- return "You are not permitted to create complimentary accounts."
- }
+sub check_payinfo_cardtype {
+ my $self = shift;
- $error = $self->ut_textn('payinfo');
- return "Illegal comp account issuer: ". $self->payinfo if $error;
- $self->paycvv('');
+ return '' unless $self->payby =~ /^(CARD|DCRD)$/;
- } elsif ( $self->payby eq 'PREPAY' ) {
+ my $payinfo = $self->payinfo;
+ $payinfo =~ s/\D//g;
- my $payinfo = $self->payinfo;
- $payinfo =~ s/\W//g; #anything else would just confuse things
- $self->payinfo($payinfo);
- $error = $self->ut_alpha('payinfo');
- return "Illegal prepayment identifier: ". $self->payinfo if $error;
- return "Unknown prepayment identifier"
- unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
- $self->paycvv('');
+ return '' if $payinfo =~ /^99\d{14}$/; #token
- }
+ my %bop_card_types = map { $_=>1 } values %{ card_types() };
+ my $cardtype = cardtype($payinfo);
- if ( $self->paydate eq '' || $self->paydate eq '-' ) {
- return "Expiration date required"
- # shouldn't payinfo_check do this?
- unless ! $self->payby
- || $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
- $self->paydate('');
- } else {
- my( $m, $y );
- if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
- ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
- } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
- ( $m, $y ) = ( $2, "19$1" );
- } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
- ( $m, $y ) = ( $3, "20$2" );
- } else {
- return "Illegal expiration date: ". $self->paydate;
- }
- $m = sprintf('%02d',$m);
- $self->paydate("$y-$m-01");
- my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
- return gettext('expired_card')
- if !$import
- && !$ignore_expired_card
- && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
- }
+ return "$cardtype not accepted" unless $bop_card_types{$cardtype};
- if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
- ( ! $conf->exists('require_cardname')
- || $self->payby !~ /^(CARD|DCRD)$/ )
- ) {
- $self->payname( $self->first. " ". $self->getfield('last') );
- } else {
+ '';
- if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
- $self->payname =~ /^([\w \,\.\-\']*)$/
- or return gettext('illegal_name'). " payname: ". $self->payname;
- $self->payname($1);
- } else {
- $self->payname =~ /^([\w \,\.\-\'\&]*)$/
- or return gettext('illegal_name'). " payname: ". $self->payname;
- $self->payname($1);
- }
+}
- }
+=item replace_check
- ### end of stuff moved to cust_payby
+Additional checks for replace only.
- return "Please select an invoicing locale"
- if ! $self->locale
- && ! $self->custnum
- && $conf->exists('cust_main-require_locale');
+=cut
- foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
- $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
- $self->$flag($1);
+sub replace_check {
+ my ($new,$old) = @_;
+ #preserve old value if global config is set
+ if ($old && $conf->exists('invoice-ship_address')) {
+ $new->invoice_ship_address($old->invoice_ship_address);
}
-
- $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
-
- warn "$me check AFTER: \n". $self->_dump
- if $DEBUG > 2;
-
- $self->SUPER::check;
+ return '';
}
=item addr_fields
sub cust_location {
my $self = shift;
- qsearch('cust_location', { 'custnum' => $self->custnum,
- 'prospectnum' => '' } );
+ qsearch({
+ 'table' => 'cust_location',
+ 'hashref' => { 'custnum' => $self->custnum,
+ 'prospectnum' => '',
+ },
+ 'order_by' => 'ORDER BY country, LOWER(state), LOWER(city), LOWER(county), LOWER(address1), LOWER(address2)',
+ });
}
=item cust_contact
-Returns all contacts (see L<FS::contact>) for this customer.
+Returns all contact associations (see L<FS::cust_contact>) for this customer.
=cut
-#already used :/ sub contact {
sub cust_contact {
my $self = shift;
- qsearch('contact', { 'custnum' => $self->custnum } );
+ qsearch('cust_contact', { 'custnum' => $self->custnum } );
}
-=item cust_payby
+=item cust_payby PAYBY
Returns all payment methods (see L<FS::cust_payby>) for this customer.
+If one or more PAYBY are specified, returns only payment methods for specified PAYBY.
+Does not validate PAYBY.
+
=cut
sub cust_payby {
my $self = shift;
- qsearch({
+ my @payby = @_;
+ my $search = {
'table' => 'cust_payby',
'hashref' => { 'custnum' => $self->custnum },
- 'order_by' => 'ORDER BY weight ASC',
- });
+ 'order_by' => "ORDER BY payby IN ('CARD','CHEK') DESC, weight ASC",
+ };
+ $search->{'extra_sql'} = ' AND payby IN ( ' . join(',', map { dbh->quote($_) } @payby) . ' ) '
+ if @payby;
+
+ qsearch($search);
+}
+
+=item has_cust_payby_auto
+
+Returns true if customer has an automatic payment method ('CARD' or 'CHEK')
+
+=cut
+
+sub has_cust_payby_auto {
+ my $self = shift;
+ scalar( qsearch({
+ 'table' => 'cust_payby',
+ 'hashref' => { 'custnum' => $self->custnum, },
+ 'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
+ 'order_by' => 'LIMIT 1',
+ }) );
+
}
=item unsuspend
return ( 'access denied' )
unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
- if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
+ if ( $opt{'ban'} ) {
+
+ foreach my $cust_payby ( $self->cust_payby ) {
+
+ #well, if they didn't get decrypted on search, then we don't have to
+ # try again... queue a job for the server that does have decryption
+ # capability if we're in a paranoid multi-server implementation?
+ return ( "Can't (yet) ban encrypted credit cards" )
+ if $cust_payby->is_encrypted($cust_payby->payinfo);
- #should try decryption (we might have the private key)
- # and if not maybe queue a job for the server that does?
- return ( "Can't (yet) ban encrypted credit cards" )
- if $self->is_encrypted($self->payinfo);
+ my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref;
+ my $error = $ban->insert;
+ return ( $error ) if $error;
- my $ban = new FS::banned_pay $self->_new_banned_pay_hashref;
- my $error = $ban->insert;
- return ( $error ) if $error;
+ }
}
}
sub _banned_pay_hashref {
+ die 'cust_main->_banned_pay_hashref deprecated';
+
my $self = shift;
my %payby2ban = (
};
}
-sub _new_banned_pay_hashref {
- my $self = shift;
- my $hr = $self->_banned_pay_hashref;
- $hr->{payinfo} = md5_base64($hr->{payinfo});
- $hr;
-}
-
=item notes
Returns all notes (see L<FS::cust_main_note>) for this customer.
sub notes {
my($self,$orderby_classnum) = (shift,shift);
- my $orderby = "_DATE DESC";
- $orderby = "CLASSNUM ASC, $orderby" if $orderby_classnum;
+ my $orderby = "sticky DESC, _date DESC";
+ $orderby = "classnum ASC, $orderby" if $orderby_classnum;
qsearch( 'cust_main_note',
{ 'custnum' => $self->custnum },
'',
=cut
sub remove_cvv {
+ die 'cust_main->remove_cvv deprecated';
my $self = shift;
my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
or return dbh->errstr;
=cut
+#XXX i need to be updated for 4.x+
sub payment_info {
my $self = shift;
}
-=item paydate_monthyear
-
-Returns a two-element list consisting of the month and year of this customer's
-paydate (credit card expiration date for CARD customers)
-
-=cut
-
-sub paydate_monthyear {
- my $self = shift;
- if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
- ( $2, $1 );
- } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
- ( $1, $3 );
- } else {
- ('', '');
- }
-}
-
=item paydate_epoch
-Returns the exact time in seconds corresponding to the payment method
-expiration date. For CARD/DCRD customers this is the end of the month;
-for others (COMP is the only other payby that uses paydate) it's the start.
-Returns 0 if the paydate is empty or set to the far future.
+Returns the next payment expiration date for this customer. If they have no
+payment methods that will expire, returns 0.
=cut
sub paydate_epoch {
my $self = shift;
- my ($month, $year) = $self->paydate_monthyear;
- return 0 if !$year or $year >= 2037;
- if ( $self->payby eq 'CARD' or $self->payby eq 'DCRD' ) {
- $month++;
- if ( $month == 13 ) {
- $month = 1;
- $year++;
- }
- return timelocal(0,0,0,1,$month-1,$year) - 1;
- }
- else {
- return timelocal(0,0,0,1,$month-1,$year);
- }
+ # filter out the ones that individually return 0, but then return 0 if
+ # there are no results
+ my @epochs = grep { $_ > 0 } map { $_->paydate_epoch } $self->cust_payby;
+ min( @epochs ) || 0;
}
=item paydate_epoch_sql
-Class method. Returns an SQL expression to obtain the payment expiration date
-as a number of seconds.
+Returns an SQL expression to get the next payment expiration date for a
+customer. Returns 2143260000 (2037-12-01) if there are no payment expiration
+dates, so that it's safe to test for "will it expire before date X" for any
+date up to then.
=cut
-# Special expiration date behavior for non-CARD/DCRD customers has been
-# carefully preserved. Do we really use that?
sub paydate_epoch_sql {
my $class = shift;
- my $table = shift || 'cust_main';
- my ($case1, $case2);
- if ( driver_name eq 'Pg' ) {
- $case1 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) + INTERVAL '1 month') - 1";
- $case2 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) )";
- }
- elsif ( lc(driver_name) eq 'mysql' ) {
- $case1 = "UNIX_TIMESTAMP( DATE_ADD( CAST( $table.paydate AS DATETIME ), INTERVAL 1 month ) ) - 1";
- $case2 = "UNIX_TIMESTAMP( CAST( $table.paydate AS DATETIME ) )";
- }
- else { return '' }
- return "CASE WHEN $table.payby IN('CARD','DCRD')
- THEN ($case1)
- ELSE ($case2)
- END"
+ my $paydate = FS::cust_payby->paydate_epoch_sql;
+ "(SELECT COALESCE(MIN($paydate), 2143260000) FROM cust_payby WHERE cust_payby.custnum = cust_main.custnum)";
}
-=item tax_exemption TAXNAME
-
-=cut
-
sub tax_exemption {
my( $self, $taxname ) = @_;
=item cust_main_exemption
-=item invoicing_list [ ARRAYREF ]
-
-If an arguement is given, sets these email addresses as invoice recipients
-(see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
-(except as warnings), so use check_invoicing_list first.
-
-Returns a list of email addresses (with svcnum entries expanded).
+=item invoicing_list
-Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
-check it without disturbing anything by passing nothing.
-
-This interface may change in the future.
+Returns a list of email addresses (with svcnum entries expanded), and the word
+'POST' if the customer receives postal invoices.
=cut
my( $self, $arrayref ) = @_;
if ( $arrayref ) {
- my @cust_main_invoice;
- if ( $self->custnum ) {
- @cust_main_invoice =
- qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
- } else {
- @cust_main_invoice = ();
- }
- foreach my $cust_main_invoice ( @cust_main_invoice ) {
- #warn $cust_main_invoice->destnum;
- unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
- #warn $cust_main_invoice->destnum;
- my $error = $cust_main_invoice->delete;
- warn $error if $error;
- }
- }
- if ( $self->custnum ) {
- @cust_main_invoice =
- qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
- } else {
- @cust_main_invoice = ();
- }
- my %seen = map { $_->address => 1 } @cust_main_invoice;
- foreach my $address ( @{$arrayref} ) {
- next if exists $seen{$address} && $seen{$address};
- $seen{$address} = 1;
- my $cust_main_invoice = new FS::cust_main_invoice ( {
- 'custnum' => $self->custnum,
- 'dest' => $address,
- } );
- my $error = $cust_main_invoice->insert;
- warn $error if $error;
- }
+ warn "FS::cust_main::invoicing_list(ARRAY) is no longer supported.";
}
- if ( $self->custnum ) {
- map { $_->address }
- qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
- } else {
- ();
- }
+ my @emails = $self->invoicing_list_emailonly;
+ push @emails, 'POST' if $self->get('postal_invoice');
+ @emails;
}
=item check_invoicing_list ARRAYREF
'';
}
-=item set_default_invoicing_list
-
-Sets the invoicing list to all accounts associated with this customer,
-overwriting any previous invoicing list.
-
-=cut
-
-sub set_default_invoicing_list {
- my $self = shift;
- $self->invoicing_list($self->all_emails);
-}
-
=item all_emails
Returns the email addresses of all accounts provisioned for this customer.
sub invoicing_list_addpost {
my $self = shift;
- return if grep { $_ eq 'POST' } $self->invoicing_list;
- my @invoicing_list = $self->invoicing_list;
- push @invoicing_list, 'POST';
- $self->invoicing_list(\@invoicing_list);
+ if ( $self->get('postal_invoice') eq '' ) {
+ $self->set('postal_invoice', 'Y');
+ my $error = $self->replace;
+ warn $error if $error; # should fail harder, but this is traditional
+ }
}
=item invoicing_list_emailonly
my $self = shift;
warn "$me invoicing_list_emailonly called"
if $DEBUG;
- grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
+ return () if !$self->custnum; # not yet inserted
+ return map { $_->emailaddress }
+ qsearch({
+ table => 'cust_contact',
+ select => 'emailaddress',
+ addl_from => ' JOIN contact USING (contactnum) '.
+ ' JOIN contact_email USING (contactnum)',
+ hashref => { 'custnum' => $self->custnum, },
+ extra_sql => q( AND cust_contact.invoice_dest = 'Y'),
+ });
}
=item invoicing_list_emailonly_scalar
=cut
+#super false laziness w/quotation::charge
sub charge {
my $self = shift;
my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
my ( $setuptax, $taxclass ); #internal taxes
my ( $taxproduct, $override ); #vendor (CCH) taxes
my $no_auto = '';
+ my $separate_bill = '';
my $cust_pkg_ref = '';
my ( $bill_now, $invoice_terms ) = ( 0, '' );
my $locationnum;
$bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
$invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
$locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
- } else {
+ $separate_bill = $_[0]->{separate_bill} || '';
+ } else { # yuck
$amount = shift;
$setup_cost = '';
$quantity = 1;
'quantity' => $quantity,
'start_date' => $start_date,
'no_auto' => $no_auto,
+ 'separate_bill' => $separate_bill,
'locationnum'=> $locationnum,
} );
sub cust_credit {
my $self = shift;
- map { $_ } #return $self->num_cust_credit unless wantarray;
- sort { $a->_date <=> $b->_date }
- qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
+
+ #return $self->num_cust_credit unless wantarray;
+
+ map { $_ } #behavior of sort undefined in scalar context
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
}
=item cust_credit_pkgnum
sub display_custnum {
my $self = shift;
+ return $self->agent_custid
+ if $default_agent_custid && $self->agent_custid;
+
my $prefix = $conf->config('cust_main-custnum-display_prefix', $self->agentnum) || '';
- if ( my $special = $conf->config('cust_main-custnum-display_special') ) {
- if ( $special eq 'CoStAg' ) {
- $prefix = uc( join('',
- $self->country,
- ($self->state =~ /^(..)/),
- $prefix || ($self->agent->agent =~ /^(..)/)
- ) );
- }
- elsif ( $special eq 'CoStCl' ) {
- $prefix = uc( join('',
- $self->country,
- ($self->state =~ /^(..)/),
- ($self->classnum ? $self->cust_class->classname =~ /^(..)/ : '__')
- ) );
- }
- # add any others here if needed
- }
- my $length = $conf->config('cust_main-custnum-display_length');
- if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
- return $self->agent_custid;
- } elsif ( $prefix ) {
- $length = 8 if !defined($length);
+ if ( $prefix ) {
return $prefix .
- sprintf('%0'.$length.'d', $self->custnum)
- } elsif ( $length ) {
- return sprintf('%0'.$length.'d', $self->custnum);
+ sprintf('%0'.($custnum_display_length||8).'d', $self->custnum)
+ } elsif ( $custnum_display_length ) {
+ return sprintf('%0'.$custnum_display_length.'d', $self->custnum);
} else {
return $self->custnum;
}
my $classnum = $self->scalar_sql(
'SELECT classnum FROM contact_class WHERE classname = \'Service\''
) || 0; #if it's zero, qsearchs will return nothing
- $self->{service_contact} = qsearchs('contact', {
- 'classnum' => $classnum, 'custnum' => $self->custnum
- }) || undef;
+ my $cust_contact = qsearchs('cust_contact', {
+ 'classnum' => $classnum,
+ 'custnum' => $self->custnum,
+ });
+ $self->{service_contact} = $cust_contact->contact if $cust_contact;
}
$self->{service_contact};
}
$contact->get('first') . ' '. $contact->get('last');
}
-#XXX this doesn't work in 3.x+
-#=item country_full
-#
-#Returns this customer's full country name
-#
-#=cut
-#
-#sub country_full {
-# my $self = shift;
-# code2country($self->country);
-#}
-
sub bill_country_full {
my $self = shift;
- code2country($self->bill_location->country);
+ $self->bill_location->country_full;
}
sub ship_country_full {
my $self = shift;
- code2country($self->ship_location->country);
+ $self->ship_location->country_full;
}
=item county_state_county [ PREFIX ]
sub cust_status {
my $self = shift;
+ return $self->hashref->{cust_status} if $self->hashref->{cust_status};
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;
$sth->execute( ($self->custnum) x $numnum )
or die "Error executing 'SELECT $sql': ". $sth->errstr;
- return $status if $sth->fetchrow_arrayref->[0];
+ if ( $sth->fetchrow_arrayref->[0] ) {
+ $self->hashref->{cust_status} = $status;
+ return $status;
+ }
+ }
+}
+
+=item is_status_delay_cancel
+
+Returns true if customer status is 'suspended'
+and all suspended cust_pkg return true for
+cust_pkg->is_status_delay_cancel.
+
+This is not a real status, this only meant for hacking display
+values, because otherwise treating the customer as suspended is
+really the whole point of the delay_cancel option.
+
+=cut
+
+sub is_status_delay_cancel {
+ my ($self) = @_;
+ return 0 unless $self->status eq 'suspended';
+ foreach my $cust_pkg ($self->ncancelled_pkgs) {
+ return 0 unless $cust_pkg->is_status_delay_cancel;
}
+ return 1;
}
=item ucfirst_cust_status
(@tickets);
}
+=item appointments [ STATUS ]
+
+Returns an array of hashes representing the customer's RT tickets which
+are appointments.
+
+=cut
+
+sub appointments {
+ my $self = shift;
+ my $status = ( @_ && $_[0] ) ? shift : '';
+
+ return () unless $conf->config('ticket_system');
+
+ my $queueid = $conf->config('ticket_system-appointment-queueid');
+
+ @{ FS::TicketSystem->customer_tickets( $self->custnum,
+ 99,
+ undef,
+ $status,
+ $queueid,
+ )
+ };
+}
+
# Return services representing svc_accts in customer support packages
sub support_services {
my $self = shift;
}
+=item payment_history
+
+Returns an array of hashrefs standardizing information from cust_bill, cust_pay,
+cust_credit and cust_refund objects. Each hashref has the following fields:
+
+I<type> - one of 'Line item', 'Invoice', 'Payment', 'Credit', 'Refund' or 'Previous'
+
+I<date> - value of _date field, unix timestamp
+
+I<date_pretty> - user-friendly date
+
+I<description> - user-friendly description of item
+
+I<amount> - impact of item on user's balance
+(positive for Invoice/Refund/Line item, negative for Payment/Credit.)
+Not to be confused with the native 'amount' field in cust_credit, see below.
+
+I<amount_pretty> - includes money char
+
+I<balance> - customer balance, chronologically as of this item
+
+I<balance_pretty> - includes money char
+
+I<charged> - amount charged for cust_bill (Invoice or Line item) records, undef for other types
+
+I<paid> - amount paid for cust_pay records, undef for other types
+
+I<credit> - amount credited for cust_credit records, undef for other types.
+Literally the 'amount' field from cust_credit, renamed here to avoid confusion.
+
+I<refund> - amount refunded for cust_refund records, undef for other types
+
+The four table-specific keys always have positive values, whether they reflect charges or payments.
+
+The following options may be passed to this method:
+
+I<line_items> - if true, returns charges ('Line item') rather than invoices
+
+I<start_date> - unix timestamp, only include records on or after.
+If specified, an item of type 'Previous' will also be included.
+It does not have table-specific fields.
+
+I<end_date> - unix timestamp, only include records before
+
+I<reverse_sort> - order from newest to oldest (default is oldest to newest)
+
+I<conf> - optional already-loaded FS::Conf object.
+
+=cut
+
+# Caution: this gets used by FS::ClientAPI::MyAccount::billing_history,
+# and also for sending customer statements, which should both be kept customer-friendly.
+# If you add anything that shouldn't be passed on through the API or exposed
+# to customers, add a new option to include it, don't include it by default
+sub payment_history {
+ my $self = shift;
+ my $opt = ref($_[0]) ? $_[0] : { @_ };
+
+ my $conf = $$opt{'conf'} || new FS::Conf;
+ my $money_char = $conf->config("money_char") || '$',
+
+ #first load entire history,
+ #need previous to calculate previous balance
+ #loading after end_date shouldn't hurt too much?
+ my @history = ();
+ if ( $$opt{'line_items'} ) {
+
+ foreach my $cust_bill ( $self->cust_bill ) {
+
+ push @history, {
+ 'type' => 'Line item',
+ 'description' => $_->desc( $self->locale ).
+ ( $_->sdate && $_->edate
+ ? ' '. time2str('%d-%b-%Y', $_->sdate).
+ ' To '. time2str('%d-%b-%Y', $_->edate)
+ : ''
+ ),
+ 'amount' => sprintf('%.2f', $_->setup + $_->recur ),
+ 'charged' => sprintf('%.2f', $_->setup + $_->recur ),
+ 'date' => $cust_bill->_date,
+ 'date_pretty' => $self->time2str_local('short', $cust_bill->_date ),
+ }
+ foreach $cust_bill->cust_bill_pkg;
+
+ }
+
+ } else {
+
+ push @history, {
+ 'type' => 'Invoice',
+ 'description' => 'Invoice #'. $_->display_invnum,
+ 'amount' => sprintf('%.2f', $_->charged ),
+ 'charged' => sprintf('%.2f', $_->charged ),
+ 'date' => $_->_date,
+ 'date_pretty' => $self->time2str_local('short', $_->_date ),
+ }
+ foreach $self->cust_bill;
+
+ }
+
+ push @history, {
+ 'type' => 'Payment',
+ 'description' => 'Payment', #XXX type
+ 'amount' => sprintf('%.2f', 0 - $_->paid ),
+ 'paid' => sprintf('%.2f', $_->paid ),
+ 'date' => $_->_date,
+ 'date_pretty' => $self->time2str_local('short', $_->_date ),
+ }
+ foreach $self->cust_pay;
+
+ push @history, {
+ 'type' => 'Credit',
+ 'description' => 'Credit', #more info?
+ 'amount' => sprintf('%.2f', 0 -$_->amount ),
+ 'credit' => sprintf('%.2f', $_->amount ),
+ 'date' => $_->_date,
+ 'date_pretty' => $self->time2str_local('short', $_->_date ),
+ }
+ foreach $self->cust_credit;
+
+ push @history, {
+ 'type' => 'Refund',
+ 'description' => 'Refund', #more info? type, like payment?
+ 'amount' => $_->refund,
+ 'refund' => $_->refund,
+ 'date' => $_->_date,
+ 'date_pretty' => $self->time2str_local('short', $_->_date ),
+ }
+ foreach $self->cust_refund;
+
+ #put it all in chronological order
+ @history = sort { $a->{'date'} <=> $b->{'date'} } @history;
+
+ #calculate balance, filter items outside date range
+ my $previous = 0;
+ my $balance = 0;
+ my @out = ();
+ foreach my $item (@history) {
+ last if $$opt{'end_date'} && ($$item{'date'} >= $$opt{'end_date'});
+ $balance += $$item{'amount'};
+ if ($$opt{'start_date'} && ($$item{'date'} < $$opt{'start_date'})) {
+ $previous += $$item{'amount'};
+ next;
+ }
+ $$item{'balance'} = sprintf("%.2f",$balance);
+ foreach my $key ( qw(amount balance) ) {
+ $$item{$key.'_pretty'} = money_pretty($$item{$key});
+ }
+ push(@out,$item);
+ }
+
+ # start with previous balance, if there was one
+ if ($previous) {
+ my $item = {
+ 'type' => 'Previous',
+ 'description' => 'Previous balance',
+ 'amount' => sprintf("%.2f",$previous),
+ 'balance' => sprintf("%.2f",$previous),
+ 'date' => $$opt{'start_date'},
+ 'date_pretty' => $self->time2str_local('short', $$opt{'start_date'} ),
+ };
+ #false laziness with above
+ foreach my $key ( qw(amount balance) ) {
+ $$item{$key.'_pretty'} = $$item{$key};
+ $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/;
+ }
+ unshift(@out,$item);
+ }
+
+ @out = reverse @history if $$opt{'reverse_sort'};
+
+ return @out;
+}
+
+=item save_cust_payby
+
+Saves a new cust_payby for this customer, replacing an existing entry only
+in select circumstances. Does not validate input.
+
+If auto is specified, marks this as the customer's primary method, or the
+specified weight. Existing payment methods have their weight incremented as
+appropriate.
+
+If bill_location is specified with auto, also sets location in cust_main.
+
+Will not insert complete duplicates of existing records, or records in which the
+only difference from an existing record is to turn off automatic payment (will
+return without error.) Will replace existing records in which the only difference
+is to add a value to a previously empty preserved field and/or turn on automatic payment.
+Fields marked as preserved are optional, and existing values will not be overwritten with
+blanks when replacing.
+
+Accepts the following named parameters:
+
+=over 4
+
+=item payment_payby
+
+either CARD or CHEK
+
+=item auto
+
+save as an automatic payment type (CARD/CHEK if true, DCRD/DCHK if false)
+
+=item weight
+
+optional, set higher than 1 for secondary, etc.
+
+=item payinfo
+
+required
+
+=item paymask
+
+optional, but should be specified for anything that might be tokenized, will be preserved when replacing
+
+=item payname
+
+required
+
+=item payip
+
+optional, will be preserved when replacing
+
+=item paydate
+
+CARD only, required
+
+=item bill_location
+
+CARD only, required, FS::cust_location object
+
+=item paystart_month
+
+CARD only, optional, will be preserved when replacing
+
+=item paystart_year
+
+CARD only, optional, will be preserved when replacing
+
+=item payissue
+
+CARD only, optional, will be preserved when replacing
+
+=item paycvv
+
+CARD only, only used if conf cvv-save is set appropriately
+
+=item paytype
+
+CHEK only
+
+=item paystate
+
+CHEK only
+
+=back
+
+=cut
+
+#The code for this option is in place, but it's not currently used
+#
+# =item replace
+#
+# existing cust_payby object to be replaced (must match custnum)
+
+# stateid/stateid_state/ss are not currently supported in cust_payby,
+# might not even work properly in 4.x, but will need to work here if ever added
+
+sub save_cust_payby {
+ my $self = shift;
+ my %opt = @_;
+
+ my $old = $opt{'replace'};
+ my $new = new FS::cust_payby { $old ? $old->hash : () };
+ return "Customer number does not match" if $new->custnum and $new->custnum != $self->custnum;
+ $new->set( 'custnum' => $self->custnum );
+
+ my $payby = $opt{'payment_payby'};
+ return "Bad payby" unless grep(/^$payby$/,('CARD','CHEK'));
+
+ # don't allow turning off auto when replacing
+ $opt{'auto'} ||= 1 if $old and $old->payby !~ /^D/;
+
+ my @check_existing; # payby relevant to this payment_payby
+
+ # set payby based on auto
+ if ( $payby eq 'CARD' ) {
+ $new->set( 'payby' => ( $opt{'auto'} ? 'CARD' : 'DCRD' ) );
+ @check_existing = qw( CARD DCRD );
+ } elsif ( $payby eq 'CHEK' ) {
+ $new->set( 'payby' => ( $opt{'auto'} ? 'CHEK' : 'DCHK' ) );
+ @check_existing = qw( CHEK DCHK );
+ }
+
+ $new->set( 'weight' => $opt{'auto'} ? $opt{'weight'} : '' );
+
+ # basic fields
+ $new->payinfo($opt{'payinfo'}); # sets default paymask, but not if it's already tokenized
+ $new->paymask($opt{'paymask'}) if $opt{'paymask'}; # in case it's been tokenized, override with loaded paymask
+ $new->set( 'payname' => $opt{'payname'} );
+ $new->set( 'payip' => $opt{'payip'} ); # will be preserved below
+
+ my $conf = new FS::Conf;
+
+ # compare to FS::cust_main::realtime_bop - check both to make sure working correctly
+ if ( $payby eq 'CARD' &&
+ grep { $_ eq cardtype($opt{'payinfo'}) } $conf->config('cvv-save') ) {
+ $new->set( 'paycvv' => $opt{'paycvv'} );
+ } else {
+ $new->set( 'paycvv' => '');
+ }
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ # set fields specific to payment_payby
+ if ( $payby eq 'CARD' ) {
+ if ($opt{'bill_location'}) {
+ $opt{'bill_location'}->set('custnum' => $self->custnum);
+ my $error = $opt{'bill_location'}->find_or_insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $new->set( 'locationnum' => $opt{'bill_location'}->locationnum );
+ }
+ foreach my $field ( qw( paydate paystart_month paystart_year payissue ) ) {
+ $new->set( $field => $opt{$field} );
+ }
+ } else {
+ foreach my $field ( qw(paytype paystate) ) {
+ $new->set( $field => $opt{$field} );
+ }
+ }
+
+ # other cust_payby to compare this to
+ my @existing = $self->cust_payby(@check_existing);
+
+ # fields that can overwrite blanks with values, but not values with blanks
+ my @preserve = qw( paymask locationnum paystart_month paystart_year payissue payip );
+
+ my $skip_cust_payby = 0; # true if we don't need to save or reweight cust_payby
+ unless ($old) {
+ # generally, we don't want to overwrite existing cust_payby with this,
+ # but we can replace if we're only marking it auto or adding a preserved field
+ # and we can avoid saving a total duplicate or merely turning off auto
+PAYBYLOOP:
+ foreach my $cust_payby (@existing) {
+ # check fields that absolutely should not change
+ foreach my $field ($new->fields) {
+ next if grep(/^$field$/, qw( custpaybynum payby weight ) );
+ next if grep(/^$field$/, @preserve );
+ next PAYBYLOOP unless $new->get($field) eq $cust_payby->get($field);
+ }
+ # now check fields that can replace if one value is blank
+ my $replace = 0;
+ foreach my $field (@preserve) {
+ if (
+ ( $new->get($field) and !$cust_payby->get($field) ) or
+ ( $cust_payby->get($field) and !$new->get($field) )
+ ) {
+ # prevention of overwriting values with blanks happens farther below
+ $replace = 1;
+ } elsif ( $new->get($field) ne $cust_payby->get($field) ) {
+ next PAYBYLOOP;
+ }
+ }
+ unless ( $replace ) {
+ # nearly identical, now check weight
+ if ($new->get('weight') eq $cust_payby->get('weight') or !$new->get('weight')) {
+ # ignore identical cust_payby, and ignore attempts to turn off auto
+ # no need to save or re-weight cust_payby (but still need to update/commit $self)
+ $skip_cust_payby = 1;
+ last PAYBYLOOP;
+ }
+ # otherwise, only change is to mark this as primary
+ }
+ # if we got this far, we're definitely replacing
+ $old = $cust_payby;
+ last PAYBYLOOP;
+ } #PAYBYLOOP
+ }
+
+ if ($old) {
+ $new->set( 'custpaybynum' => $old->custpaybynum );
+ # don't turn off automatic payment (but allow it to be turned on)
+ if ($new->payby =~ /^D/ and $new->payby ne $old->payby) {
+ $opt{'auto'} = 1;
+ $new->set( 'payby' => $old->payby );
+ $new->set( 'weight' => 1 );
+ }
+ # make sure we're not overwriting values with blanks
+ foreach my $field (@preserve) {
+ if ( $old->get($field) and !$new->get($field) ) {
+ $new->set( $field => $old->get($field) );
+ }
+ }
+ }
+
+ # only overwrite cust_main bill_location if auto
+ if ($opt{'auto'} && $opt{'bill_location'}) {
+ $self->set('bill_location' => $opt{'bill_location'});
+ my $error = $self->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ # done with everything except reweighting and saving cust_payby
+ # still need to commit changes to cust_main and cust_location
+ if ($skip_cust_payby) {
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ return '';
+ }
+
+ # re-weight existing primary cust_pay for this payby
+ if ($opt{'auto'}) {
+ foreach my $cust_payby (@existing) {
+ # relies on cust_payby return order
+ last unless $cust_payby->payby !~ /^D/;
+ last if $cust_payby->weight > 1;
+ next if $new->custpaybynum eq $cust_payby->custpaybynum;
+ next if $cust_payby->weight < ($opt{'weight'} || 1);
+ $cust_payby->weight( $cust_payby->weight + 1 );
+ my $error = $cust_payby->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error reweighting cust_payby: $error";
+ }
+ }
+ }
+
+ # finally, save cust_payby
+ my $error = $old ? $new->replace($old) : $new->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
=back
=head1 CLASS METHODS
=over 4
-=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
-
-I<from> - the email sender (default is invoice_from)
-
-I<to> - comma-separated scalar or arrayref of recipients
- (default is invoicing_list)
-
-I<subject> - The subject line of the sent email notification
- (default is "Notice from company_name")
-
-I<extra_fields> - a hashref of name/value pairs which will be substituted
- into the template
-
-The following variables are vavailable in the template.
-
-I<$first> - the customer first name
-I<$last> - the customer last name
-I<$company> - the customer company
-I<$payby> - a description of the method of payment for the customer
- # would be nice to use FS::payby::shortname
-I<$payinfo> - the account information used to collect for this customer
-I<$expdate> - the expiration of the customer payment in seconds from epoch
-
-=cut
-
-sub notify {
- my ($self, $template, %options) = @_;
-
- return unless $conf->exists($template);
-
- my $from = $conf->config('invoice_from', $self->agentnum)
- if $conf->exists('invoice_from', $self->agentnum);
- $from = $options{from} if exists($options{from});
-
- my $to = join(',', $self->invoicing_list_emailonly);
- $to = $options{to} if exists($options{to});
-
- my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
- if $conf->exists('company_name', $self->agentnum);
- $subject = $options{subject} if exists($options{subject});
-
- my $notify_template = new Text::Template (TYPE => 'ARRAY',
- SOURCE => [ map "$_\n",
- $conf->config($template)]
- )
- or die "can't create new Text::Template object: Text::Template::ERROR";
- $notify_template->compile()
- or die "can't compile template: Text::Template::ERROR";
-
- $FS::notify_template::_template::company_name =
- $conf->config('company_name', $self->agentnum);
- $FS::notify_template::_template::company_address =
- join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
-
- my $paydate = $self->paydate || '2037-12-31';
- $FS::notify_template::_template::first = $self->first;
- $FS::notify_template::_template::last = $self->last;
- $FS::notify_template::_template::company = $self->company;
- $FS::notify_template::_template::payinfo = $self->mask_payinfo;
- my $payby = $self->payby;
- my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
- my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
-
- #credit cards expire at the end of the month/year of their exp date
- if ($payby eq 'CARD' || $payby eq 'DCRD') {
- $FS::notify_template::_template::payby = 'credit card';
- ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
- $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
- $expire_time--;
- }elsif ($payby eq 'COMP') {
- $FS::notify_template::_template::payby = 'complimentary account';
- }else{
- $FS::notify_template::_template::payby = 'current method';
- }
- $FS::notify_template::_template::expdate = $expire_time;
-
- for (keys %{$options{extra_fields}}){
- no strict "refs";
- ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
- }
-
- send_email(from => $from,
- to => $to,
- subject => $subject,
- body => $notify_template->fill_in( PACKAGE =>
- 'FS::notify_template::_template' ),
- );
-
-}
-
=item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
Generates a templated notification to the customer (see L<Text::Template>).
into the template. These values may override values mentioned below
and those from the customer record.
+I<template_text> - if present, ignores TEMPLATE_NAME and uses the provided text
+
The following variables are available in the template instead of or in addition
to the fields of the customer record.
-I<$payby> - a description of the method of payment for the customer
- # would be nice to use FS::payby::shortname
-I<$payinfo> - the masked account information used to collect for this customer
-I<$expdate> - the expiration of the customer payment method in seconds from epoch
I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
=cut
sub generate_letter {
my ($self, $template, %options) = @_;
- return unless $conf->exists($template);
+ warn "Template $template does not exist" && return
+ unless $conf->exists($template) || $options{'template_text'};
+
+ my $template_source = $options{'template_text'}
+ ? [ $options{'template_text'} ]
+ : [ map "$_\n", $conf->config($template) ];
my $letter_template = new Text::Template
( TYPE => 'ARRAY',
- SOURCE => [ map "$_\n", $conf->config($template)],
+ SOURCE => $template_source,
DELIMITERS => [ '[@--', '--@]' ],
)
or die "can't create new Text::Template object: Text::Template::ERROR";
or die "can't compile template: Text::Template::ERROR";
my %letter_data = map { $_ => $self->$_ } $self->fields;
- $letter_data{payinfo} = $self->mask_payinfo;
-
- #my $paydate = $self->paydate || '2037-12-31';
- my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
-
- my $payby = $self->payby;
- my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
- my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
-
- #credit cards expire at the end of the month/year of their exp date
- if ($payby eq 'CARD' || $payby eq 'DCRD') {
- $letter_data{payby} = 'credit card';
- ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
- $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
- $expire_time--;
- }elsif ($payby eq 'COMP') {
- $letter_data{payby} = 'complimentary account';
- }else{
- $letter_data{payby} = 'current method';
- }
- $letter_data{expdate} = $expire_time;
for (keys %{$options{extra_fields}}){
$letter_data{$_} = $options{extra_fields}->{$_};
my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
or die "invalid customer number: " . $opt{custnum};
- my $error = $self->print( { 'template' => $opt{template} } );
+#do not backport this change to 3.x
+# my $error = $self->print( { 'template' => $opt{template} } );
+ my $error = $self->print( $opt{'template'} );
die $error if $error;
}
}
+sub process_o2m_qsearch {
+ my $self = shift;
+ my $table = shift;
+ return qsearch($table, @_) unless $table eq 'contact';
+
+ my $hashref = shift;
+ my %hash = %$hashref;
+ ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
+ or die 'guru meditation #4343';
+
+ qsearch({ 'table' => 'contact',
+ 'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
+ 'hashref' => \%hash,
+ 'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
+ " cust_contact.custnum = $custnum "
+ });
+}
+
+sub process_o2m_qsearchs {
+ my $self = shift;
+ my $table = shift;
+ return qsearchs($table, @_) unless $table eq 'contact';
+
+ my $hashref = shift;
+ my %hash = %$hashref;
+ ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
+ or die 'guru meditation #2121';
+
+ qsearchs({ 'table' => 'contact',
+ 'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
+ 'hashref' => \%hash,
+ 'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
+ " cust_contact.custnum = $custnum "
+ });
+}
+
=item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
Subroutine (not a method), designed to be called from the queue.
sub _upgrade_data { #class method
my ($class, %opts) = @_;
- my @statements = (
- 'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL',
- );
+ my @statements = ();
#this seems to be the only expensive one.. why does it take so long?
unless ( FS::upgrade_journal->is_done('cust_main__signupdate') ) {
FS::upgrade_journal->set_done('cust_main__signupdate');
}
- unless ( FS::upgrade_journal->is_done('cust_main__paydate') ) {
-
- # fix yyyy-m-dd formatted paydates
- if ( driver_name =~ /^mysql/i ) {
- push @statements,
- "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
- } else { # the SQL standard
- push @statements,
- "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
- }
- FS::upgrade_journal->set_done('cust_main__paydate');
- }
-
- unless ( FS::upgrade_journal->is_done('cust_main__payinfo') ) {
-
- push @statements, #fix the weird BILL with a cc# in payinfo problem
- #DCRD to be safe
- "UPDATE cust_main SET payby = 'DCRD' WHERE payby = 'BILL' and length(payinfo) = 16 and payinfo ". regexp_sql. q( '^[0-9]*$' );
-
- FS::upgrade_journal->set_done('cust_main__payinfo');
-
- }
-
my $t = time;
foreach my $sql ( @statements ) {
my $sth = dbh->prepare($sql) or die dbh->errstr;
}
- unless ( FS::upgrade_journal->is_done('cust_main__cust_payby') ) {
-
- #we don't want to decrypt them, just stuff them as-is into cust_payby
- local(@encrypted_fields) = ();
-
- local($FS::cust_payby::ignore_expired_card) = 1;
- local($FS::cust_payby::ignore_banned_card) = 1;
-
- my @payfields = qw( payby payinfo paycvv paymask
- paydate paystart_month paystart_year payissue
- payname paystate paytype payip
- );
-
- my $search = new FS::Cursor {
- 'table' => 'cust_main',
- 'extra_sql' => " WHERE ( payby IS NOT NULL AND payby != '' ) ",
- };
-
- while (my $cust_main = $search->fetch) {
-
- my $cust_payby = new FS::cust_payby {
- 'custnum' => $cust_main->custnum,
- 'weight' => 1,
- map { $_ => $cust_main->$_(); } @payfields
- };
-
- my $error = $cust_payby->insert;
- die $error if $error;
-
- $cust_main->setfield($_, '') foreach @payfields;
- $error = $cust_main->replace;
- die $error if $error;
-
- };
-
- FS::upgrade_journal->set_done('cust_main__cust_payby');
- }
-
$class->_upgrade_otaker(%opts);
}
No multiple currency support (probably a larger project than just this module).
-payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
-
Birthdates rely on negative epoch values.
-The payby for card/check batches is broken. With mixed batching, bad
-things will happen.
-
B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
=head1 SEE ALSO