X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=0788e15dee66661646221214faf7ad0036eb3afa;hb=80e675c7305768010f8b08ef2ebdfb936432e84d;hp=22c16fbaabdbd9c296e5288f085f89a36db1c7fc;hpb=9b37fb82c94a480ef6af2f7b9a8b889d4381e703;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 22c16fbaa..0788e15de 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -68,6 +68,7 @@ use FS::banned_pay; use FS::cust_main_note; use FS::cust_attachment; use FS::contact; +use FS::Locales; # 1 is mostly method/subroutine entry and options # 2 traces progress of some operations @@ -210,6 +211,10 @@ phone (optional) phone (optional) +=item mobile + +phone (optional) + =item ship_first Shipping first name @@ -256,6 +261,10 @@ phone (optional) phone (optional) +=item ship_mobile + +phone (optional) + =item payby Payment Type (See L for valid payby values) @@ -324,6 +333,14 @@ A suggestion to events (see L) to delay until this unix ti Discourage individual CDR printing, empty or `Y' +=item edit_subject + +Allow self-service editing of ticket subjects, empty or 'Y' + +=item calling_list_exempt + +Do not call, empty or 'Y' + =back =head1 METHODS @@ -772,7 +789,7 @@ sub get_prepay { $prepay_credit = qsearchs( 'prepay_credit', - { 'identifier' => $prepay_credit }, + { 'identifier' => $identifier }, '', 'FOR UPDATE' ); @@ -1469,6 +1486,11 @@ sub replace { && $self->payby =~ /^(CARD|DCRD)$/ && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask ); + local($ignore_banned_card) = 1 + if ( $old->payby =~ /^(CARD|DCRD)$/ && $self->payby =~ /^(CARD|DCRD)$/ + || $old->payby =~ /^(CHEK|DCHK)$/ && $self->payby =~ /^(CHEK|DCHK)$/ ) + && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask ); + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -1681,6 +1703,10 @@ sub check { || $self->ut_alphan('geocode') || $self->ut_floatn('cdr_termination_percentage') || $self->ut_floatn('credit_limit') + || $self->ut_numbern('billday') + || $self->ut_enum('edit_subject', [ '', 'Y' ] ) + || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] ) + || $self->ut_enum('locale', [ '', FS::Locales->locales ]) ; #barf. need message catalogs. i18n. etc. @@ -1734,9 +1760,10 @@ sub check { } $error = - $self->ut_phonen('daytime', $self->country) - || $self->ut_phonen('night', $self->country) - || $self->ut_phonen('fax', $self->country) + $self->ut_phonen('daytime', $self->country) + || $self->ut_phonen('night', $self->country) + || $self->ut_phonen('fax', $self->country) + || $self->ut_phonen('mobile', $self->country) ; return $error if $error; @@ -1746,7 +1773,7 @@ sub check { } if ( $conf->exists('cust_main-require_phone') - && ! length($self->daytime) && ! length($self->night) + && ! length($self->daytime) && ! length($self->night) && ! length($self->mobile) ) { my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/ @@ -1755,8 +1782,12 @@ sub check { my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/ ? 'Night Phone' : FS::Msgcat::_gettext('night'); - - return "$daytime_label or $night_label is required" + + my $mobile_label = FS::Msgcat::_gettext('mobile') =~ /^(mobile)?$/ + ? 'Mobile Phone' + : FS::Msgcat::_gettext('mobile'); + + return "$daytime_label, $night_label or $mobile_label is required" } @@ -1794,9 +1825,10 @@ sub check { #eofalse $error = - $self->ut_phonen('ship_daytime', $self->ship_country) - || $self->ut_phonen('ship_night', $self->ship_country) - || $self->ut_phonen('ship_fax', $self->ship_country) + $self->ut_phonen('ship_daytime', $self->ship_country) + || $self->ut_phonen('ship_night', $self->ship_country) + || $self->ut_phonen('ship_fax', $self->ship_country) + || $self->ut_phonen('ship_mobile', $self->ship_country) ; return $error if $error; @@ -1847,7 +1879,7 @@ sub check { my $payinfo = $self->payinfo; $payinfo =~ s/\D//g; - $payinfo =~ /^(\d{13,16})$/ + $payinfo =~ /^(\d{13,16}|\d{8,9})$/ or return gettext('invalid_card'); # . ": ". $self->payinfo; $payinfo = $1; $self->payinfo($payinfo); @@ -1859,12 +1891,21 @@ sub check { && cardtype($self->payinfo) eq "Unknown"; unless ( $ignore_banned_card ) { - my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref); + my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } ); if ( $ban ) { - return 'Banned credit card: banned on '. - time2str('%a %h %o at %r', $ban->_date). - ' by '. $ban->otaker. - ' (ban# '. $ban->bannum. ')'; + 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. ')'; + } } } @@ -1909,8 +1950,11 @@ sub check { } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) { my $payinfo = $self->payinfo; - $payinfo =~ s/[^\d\@]//g; - if ( $conf->exists('echeck-nonus') ) { + $payinfo =~ s/[^\d\@\.]//g; + if ( $conf->exists('cust_main-require-bank-branch') ) { + $payinfo =~ /^(\d+)\@(\d+)\.(\d+)$/ or return 'invalid echeck account@branch.bank'; + $payinfo = "$1\@$2.$3"; + } elsif ( $conf->exists('echeck-nonus') ) { $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba'; $payinfo = "$1\@$2"; } else { @@ -1921,12 +1965,17 @@ sub check { $self->paycvv(''); unless ( $ignore_banned_card ) { - my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref); + my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } ); if ( $ban ) { - return 'Banned ACH account: banned on '. - time2str('%a %h %o at %r', $ban->_date). - ' by '. $ban->otaker. - ' (ban# '. $ban->bannum. ')'; + 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. ')'; + } } } @@ -2029,7 +2078,7 @@ Returns a list of fields which have ship_ duplicates. sub addr_fields { qw( last first company address1 address2 city county state zip country - daytime night fax + daytime night fax mobile ); } @@ -2206,7 +2255,7 @@ sub cancel { return ( "Can't (yet) ban encrypted credit cards" ) if $self->is_encrypted($self->payinfo); - my $ban = new FS::banned_pay $self->_banned_pay_hashref; + my $ban = new FS::banned_pay $self->_new_banned_pay_hashref; my $error = $ban->insert; return ( $error ) if $error; @@ -2240,11 +2289,18 @@ sub _banned_pay_hashref { { 'payby' => $payby2ban{$self->payby}, - 'payinfo' => md5_base64($self->payinfo), + 'payinfo' => $self->payinfo, #don't ever *search* on reason! #'reason' => }; } +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) for this customer. @@ -2425,6 +2481,7 @@ sub batch_card { 'status' => 'O', 'payby' => FS::payby->payby2payment($payby), ); + $pay_batch{agentnum} = $self->agentnum if $conf->exists('batch-spoolagent'); my $pay_batch = qsearchs( 'pay_batch', \%pay_batch ); @@ -2905,6 +2962,60 @@ sub paydate_monthyear { } } +=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. + +=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); + } +} + +=item paydate_epoch_sql + +Class method. Returns an SQL expression to obtain the payment expiration date +as a number of seconds. + +=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" +} + =item tax_exemption TAXNAME =cut @@ -3409,7 +3520,7 @@ sub charge { sub charge_postal_fee { my $self = shift; - my $pkgpart = $conf->config('postal_invoice-fee_pkgpart'); + my $pkgpart = $conf->config('postal_invoice-fee_pkgpart', $self->agentnum); return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list); my $cust_pkg = new FS::cust_pkg ( { @@ -3489,6 +3600,56 @@ sub cust_statement { qsearch($opt); } +=item svc_x SVCDB [ OPTION => VALUE | EXTRA_QSEARCH_PARAMS_HASHREF ] + +Returns all services of type SVCDB (such as 'svc_acct') for this customer. + +Optionally, a list or hashref of additional arguments to the qsearch call can +be passed following the SVCDB. + +=cut + +sub svc_x { + my $self = shift; + my $svcdb = shift; + if ( ! $svcdb =~ /^svc_\w+$/ ) { + warn "$me svc_x requires a svcdb"; + return; + } + my $opt = ref($_[0]) ? shift : { @_ }; + + $opt->{'table'} = $svcdb; + $opt->{'addl_from'} = + 'LEFT JOIN cust_svc USING (svcnum) LEFT JOIN cust_pkg USING (pkgnum) '. + ($opt->{'addl_from'} || ''); + + my $custnum = $self->custnum; + $custnum =~ /^\d+$/ or die "bad custnum '$custnum'"; + my $where = "cust_pkg.custnum = $custnum"; + + my $extra_sql = $opt->{'extra_sql'} || ''; + if ( keys %{ $opt->{'hashref'} } ) { + $extra_sql = " AND $where $extra_sql"; + } + else { + if ( $opt->{'extra_sql'} =~ /^\s*where\s(.*)/si ) { + $extra_sql = "WHERE $where AND $1"; + } + else { + $extra_sql = "WHERE $where $extra_sql"; + } + } + $opt->{'extra_sql'} = $extra_sql; + + qsearch($opt); +} + +# required for use as an eventtable; +sub svc_acct { + my $self = shift; + $self->svc_x('svc_acct', @_); +} + =item cust_credit Returns all the credits (see L) for this customer. @@ -4213,7 +4374,7 @@ sub balance_date_sql { =item unapplied_payments_date_sql START_TIME [ END_TIME ] Returns an SQL fragment to retreive the total unapplied payments for this -customer, only considering invoices with date earlier than START_TIME, and +customer, only considering payments with date earlier than START_TIME, and optionally not later than END_TIME. Times are specified as SQL fragments or numeric