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 Date::Manip;
use File::Temp; #qw( tempfile );
use Business::CreditCard 0.28;
+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_ps do_print money_pretty );
+use FS::Misc qw( generate_ps do_print money_pretty card_types );
use FS::Msgcat qw(gettext);
use FS::CurrentUser;
use FS::TicketSystem;
sub nohistory_fields { ('payinfo', 'paycvv'); }
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 {
$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";
|| $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
}
- ### 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;
-
- if ( $self->payip eq '' ) {
- $self->payip('');
- } else {
- $error = $self->ut_ip('payip');
- return $error if $error;
- }
-
- # 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' ) {
-
- $error = $self->ut_textn('payinfo');
- return "Illegal P.O. number: ". $self->payinfo if $error;
- $self->paycvv('');
-
- } elsif ( $self->payby eq 'COMP' ) {
-
- my $curuser = $FS::CurrentUser::CurrentUser;
- if ( ! $self->custnum
- && ! $curuser->access_right('Complimentary customer')
- )
- {
- return "You are not permitted to create complimentary accounts."
- }
-
- $error = $self->ut_textn('payinfo');
- return "Illegal comp account issuer: ". $self->payinfo if $error;
- $self->paycvv('');
-
- } elsif ( $self->payby eq 'PREPAY' ) {
-
- 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 "You are not permitted to create complimentary accounts."
- if ! $self->custnum
- && $self->complimentary eq 'Y'
- && ! $FS::CurrentUser::CurrentUser->access_right('Complimentary customer');
-
- 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 ) );
- }
-
- 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);
- }
-
- }
-
- ### end of stuff moved to cust_payby
-
return "Please select an invoicing locale"
if ! $self->locale
&& ! $self->custnum
$self->SUPER::check;
}
+sub check_payinfo_cardtype {
+ my $self = shift;
+
+ return '' unless $self->payby =~ /^(CARD|DCRD)$/;
+
+ my $payinfo = $self->payinfo;
+ $payinfo =~ s/\D//g;
+
+ return '' if $payinfo =~ /^99\d{14}$/; #token
+
+ my %bop_card_types = map { $_=>1 } values %{ card_types() };
+ my $cardtype = cardtype($payinfo);
+
+ return "$cardtype not accepted" unless $bop_card_types{$cardtype};
+
+ '';
+
+}
+
=item replace_check
Additional checks for replace only.
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
}
sub _banned_pay_hashref {
+ die 'cust_main->_banned_pay_hashref deprecated';
+
my $self = shift;
my %payby2ban = (
=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_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 ) = @_;
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;
}
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;
+ }
}
}
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 (weight 1)
-and changes existing primary methods for that payby to secondary methods (weight 2.)
+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
Accepts the following named parameters:
-payment_payby - either CARD or CHEK
+=over 4
-auto - save as an automatic payment type (CARD/CHEK if true, DCRD/DCHK if false)
+=item payment_payby
-payinfo - required
+either CARD or CHEK
-paymask - optional, but should be specified for anything that might be tokenized, will be preserved when replacing
+=item auto
-payname - required
+save as an automatic payment type (CARD/CHEK if true, DCRD/DCHK if false)
-payip - optional, will be preserved when replacing
+=item weight
-paydate - CARD only, required
+optional, set higher than 1 for secondary, etc.
-bill_location - CARD only, required, FS::cust_location object
+=item payinfo
-paystart_month - CARD only, optional, will be preserved when replacing
+required
-paystart_year - CARD only, optional, will be preserved when replacing
+=item paymask
+
+optional, but should be specified for anything that might be tokenized, will be preserved when replacing
+
+=item payname
-payissue - CARD only, optional, will be preserved when replacing
+required
-paycvv - CARD only, only used if conf cvv-save is set appropriately
+=item payip
-paytype - CHEK only
+optional, will be preserved when replacing
-paystate - CHEK only
+=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
#
-# replace - existing cust_payby object to be replaced (must match custnum)
+# =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
@check_existing = qw( CHEK DCHK );
}
- # every automatic payment type added here will be marked primary
- $new->set( 'weight' => $opt{'auto'} ? 1 : '' );
+ $new->set( 'weight' => $opt{'auto'} ? $opt{'weight'} : '' );
# basic fields
$new->payinfo($opt{'payinfo'}); # sets default paymask, but not if it's already tokenized
# if we got this far, we're definitely replacing
$old = $cust_payby;
last PAYBYLOOP;
- }
+ } #PAYBYLOOP
}
if ($old) {
last unless $cust_payby->payby !~ /^D/;
last if $cust_payby->weight > 1;
next if $new->custpaybynum eq $cust_payby->custpaybynum;
- $cust_payby->set( 'weight' => 2 );
+ 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;
=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->invoice_from_full($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>).
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
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}->{$_};
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) {
-
- unless ( $cust_main->payby =~ /^(BILL|COMP)$/ ) {
-
- 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;
-
- }
-
- # at the time we do this, also migrate paytype into cust_pay_batch
- # so that batches that are open before the migration can still be
- # processed
- my @cust_pay_batch = qsearch('cust_pay_batch', {
- 'custnum' => $cust_main->custnum,
- 'payby' => 'CHEK',
- 'paytype' => '',
- });
- foreach my $cust_pay_batch (@cust_pay_batch) {
- $cust_pay_batch->set('paytype', $cust_main->get('paytype'));
- my $error = $cust_pay_batch->replace;
- die "$error (setting cust_pay_batch.paytype)" if $error;
- }
-
- $cust_main->complimentary('Y') if $cust_main->payby eq 'COMP';
-
- $cust_main->invoice_attn( $cust_main->payname )
- if $cust_main->payby eq 'BILL' && $cust_main->payname;
- $cust_main->po_number( $cust_main->payinfo )
- if $cust_main->payby eq 'BILL' && $cust_main->payinfo;
-
- $cust_main->setfield($_, '') foreach @payfields;
- my $error = $cust_main->replace;
- die "Error upgradging payment information for custnum ".
- $cust_main->custnum. ": $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