one-time charge enhancements
[freeside.git] / FS / FS / cust_main.pm
1 package FS::cust_main;
2
3 use strict;
4 use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf @encrypted_fields
5              $import $skip_fuzzyfiles $ignore_expired_card );
6 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Safe;
8 use Carp;
9 use Exporter;
10 BEGIN {
11   eval "use Time::Local;";
12   die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
13     if $] < 5.006 && !defined($Time::Local::VERSION);
14   #eval "use Time::Local qw(timelocal timelocal_nocheck);";
15   eval "use Time::Local qw(timelocal_nocheck);";
16 }
17 use Digest::MD5 qw(md5_base64);
18 use Date::Format;
19 use Date::Parse;
20 #use Date::Manip;
21 use String::Approx qw(amatch);
22 use Business::CreditCard 0.28;
23 use Locale::Country;
24 use FS::UID qw( getotaker dbh );
25 use FS::Record qw( qsearchs qsearch dbdef );
26 use FS::Misc qw( send_email );
27 use FS::Msgcat qw(gettext);
28 use FS::cust_pkg;
29 use FS::cust_svc;
30 use FS::cust_bill;
31 use FS::cust_bill_pkg;
32 use FS::cust_pay;
33 use FS::cust_pay_void;
34 use FS::cust_credit;
35 use FS::cust_refund;
36 use FS::part_referral;
37 use FS::cust_main_county;
38 use FS::agent;
39 use FS::cust_main_invoice;
40 use FS::cust_credit_bill;
41 use FS::cust_bill_pay;
42 use FS::prepay_credit;
43 use FS::queue;
44 use FS::part_pkg;
45 use FS::part_bill_event qw(due_events);
46 use FS::cust_bill_event;
47 use FS::cust_tax_exempt;
48 use FS::cust_tax_exempt_pkg;
49 use FS::type_pkgs;
50 use FS::payment_gateway;
51 use FS::agent_payment_gateway;
52 use FS::banned_pay;
53 use FS::payinfo_Mixin;
54
55 @ISA = qw( FS::Record FS::payinfo_Mixin );
56
57 @EXPORT_OK = qw( smart_search );
58
59 $realtime_bop_decline_quiet = 0;
60
61 # 1 is mostly method/subroutine entry and options
62 # 2 traces progress of some operations
63 # 3 is even more information including possibly sensitive data
64 $DEBUG = 0;
65 $me = '[FS::cust_main]';
66
67 $import = 0;
68 $skip_fuzzyfiles = 0;
69 $ignore_expired_card = 0;
70
71 @encrypted_fields = ('payinfo', 'paycvv');
72
73 #ask FS::UID to run this stuff for us later
74 #$FS::UID::callback{'FS::cust_main'} = sub { 
75 install_callback FS::UID sub { 
76   $conf = new FS::Conf;
77   #yes, need it for stuff below (prolly should be cached)
78 };
79
80 sub _cache {
81   my $self = shift;
82   my ( $hashref, $cache ) = @_;
83   if ( exists $hashref->{'pkgnum'} ) {
84     #@{ $self->{'_pkgnum'} } = ();
85     my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
86     $self->{'_pkgnum'} = $subcache;
87     #push @{ $self->{'_pkgnum'} },
88     FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
89   }
90 }
91
92 =head1 NAME
93
94 FS::cust_main - Object methods for cust_main records
95
96 =head1 SYNOPSIS
97
98   use FS::cust_main;
99
100   $record = new FS::cust_main \%hash;
101   $record = new FS::cust_main { 'column' => 'value' };
102
103   $error = $record->insert;
104
105   $error = $new_record->replace($old_record);
106
107   $error = $record->delete;
108
109   $error = $record->check;
110
111   @cust_pkg = $record->all_pkgs;
112
113   @cust_pkg = $record->ncancelled_pkgs;
114
115   @cust_pkg = $record->suspended_pkgs;
116
117   $error = $record->bill;
118   $error = $record->bill %options;
119   $error = $record->bill 'time' => $time;
120
121   $error = $record->collect;
122   $error = $record->collect %options;
123   $error = $record->collect 'invoice_time'   => $time,
124                           ;
125
126 =head1 DESCRIPTION
127
128 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
129 FS::Record.  The following fields are currently supported:
130
131 =over 4
132
133 =item custnum - primary key (assigned automatically for new customers)
134
135 =item agentnum - agent (see L<FS::agent>)
136
137 =item refnum - Advertising source (see L<FS::part_referral>)
138
139 =item first - name
140
141 =item last - name
142
143 =item ss - social security number (optional)
144
145 =item company - (optional)
146
147 =item address1
148
149 =item address2 - (optional)
150
151 =item city
152
153 =item county - (optional, see L<FS::cust_main_county>)
154
155 =item state - (see L<FS::cust_main_county>)
156
157 =item zip
158
159 =item country - (see L<FS::cust_main_county>)
160
161 =item daytime - phone (optional)
162
163 =item night - phone (optional)
164
165 =item fax - phone (optional)
166
167 =item ship_first - name
168
169 =item ship_last - name
170
171 =item ship_company - (optional)
172
173 =item ship_address1
174
175 =item ship_address2 - (optional)
176
177 =item ship_city
178
179 =item ship_county - (optional, see L<FS::cust_main_county>)
180
181 =item ship_state - (see L<FS::cust_main_county>)
182
183 =item ship_zip
184
185 =item ship_country - (see L<FS::cust_main_county>)
186
187 =item ship_daytime - phone (optional)
188
189 =item ship_night - phone (optional)
190
191 =item ship_fax - phone (optional)
192
193 =item payby - Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
194
195 =item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
196
197 =item paymask - Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
198
199 =item paycvv
200
201 Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
202
203 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
204
205 =item paystart_month - start date month (maestro/solo cards only)
206
207 =item paystart_year - start date year (maestro/solo cards only)
208
209 =item payissue - issue number (maestro/solo cards only)
210
211 =item payname - name on card or billing name
212
213 =item payip - IP address from which payment information was received
214
215 =item tax - tax exempt, empty or `Y'
216
217 =item otaker - order taker (assigned automatically, see L<FS::UID>)
218
219 =item comments - comments (optional)
220
221 =item referral_custnum - referring customer number
222
223 =item spool_cdr - Enable individual CDR spooling, empty or `Y'
224
225 =back
226
227 =head1 METHODS
228
229 =over 4
230
231 =item new HASHREF
232
233 Creates a new customer.  To add the customer to the database, see L<"insert">.
234
235 Note that this stores the hash reference, not a distinct copy of the hash it
236 points to.  You can ask the object for a copy with the I<hash> method.
237
238 =cut
239
240 sub table { 'cust_main'; }
241
242 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
243
244 Adds this customer to the database.  If there is an error, returns the error,
245 otherwise returns false.
246
247 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
248 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
249 are inserted atomicly, or the transaction is rolled back.  Passing an empty
250 hash reference is equivalent to not supplying this parameter.  There should be
251 a better explanation of this, but until then, here's an example:
252
253   use Tie::RefHash;
254   tie %hash, 'Tie::RefHash'; #this part is important
255   %hash = (
256     $cust_pkg => [ $svc_acct ],
257     ...
258   );
259   $cust_main->insert( \%hash );
260
261 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
262 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
263 expected and rollback the entire transaction; it is not necessary to call 
264 check_invoicing_list first.  The invoicing_list is set after the records in the
265 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
266 invoicing_list destination to the newly-created svc_acct.  Here's an example:
267
268   $cust_main->insert( {}, [ $email, 'POST' ] );
269
270 Currently available options are: I<depend_jobnum> and I<noexport>.
271
272 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
273 on the supplied jobnum (they will not run until the specific job completes).
274 This can be used to defer provisioning until some action completes (such
275 as running the customer's credit card successfully).
276
277 The I<noexport> option is deprecated.  If I<noexport> is set true, no
278 provisioning jobs (exports) are scheduled.  (You can schedule them later with
279 the B<reexport> method.)
280
281 =cut
282
283 sub insert {
284   my $self = shift;
285   my $cust_pkgs = @_ ? shift : {};
286   my $invoicing_list = @_ ? shift : '';
287   my %options = @_;
288   warn "$me insert called with options ".
289        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
290     if $DEBUG;
291
292   local $SIG{HUP} = 'IGNORE';
293   local $SIG{INT} = 'IGNORE';
294   local $SIG{QUIT} = 'IGNORE';
295   local $SIG{TERM} = 'IGNORE';
296   local $SIG{TSTP} = 'IGNORE';
297   local $SIG{PIPE} = 'IGNORE';
298
299   my $oldAutoCommit = $FS::UID::AutoCommit;
300   local $FS::UID::AutoCommit = 0;
301   my $dbh = dbh;
302
303   my $prepay_identifier = '';
304   my( $amount, $seconds ) = ( 0, 0 );
305   my $payby = '';
306   if ( $self->payby eq 'PREPAY' ) {
307
308     $self->payby('BILL');
309     $prepay_identifier = $self->payinfo;
310     $self->payinfo('');
311
312     warn "  looking up prepaid card $prepay_identifier\n"
313       if $DEBUG > 1;
314
315     my $error = $self->get_prepay($prepay_identifier, \$amount, \$seconds);
316     if ( $error ) {
317       $dbh->rollback if $oldAutoCommit;
318       #return "error applying prepaid card (transaction rolled back): $error";
319       return $error;
320     }
321
322     $payby = 'PREP' if $amount;
323
324   } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
325
326     $payby = $1;
327     $self->payby('BILL');
328     $amount = $self->paid;
329
330   }
331
332   warn "  inserting $self\n"
333     if $DEBUG > 1;
334
335   $self->signupdate(time) unless $self->signupdate;
336
337   my $error = $self->SUPER::insert;
338   if ( $error ) {
339     $dbh->rollback if $oldAutoCommit;
340     #return "inserting cust_main record (transaction rolled back): $error";
341     return $error;
342   }
343
344   warn "  setting invoicing list\n"
345     if $DEBUG > 1;
346
347   if ( $invoicing_list ) {
348     $error = $self->check_invoicing_list( $invoicing_list );
349     if ( $error ) {
350       $dbh->rollback if $oldAutoCommit;
351       return "checking invoicing_list (transaction rolled back): $error";
352     }
353     $self->invoicing_list( $invoicing_list );
354   }
355
356   if (    $conf->config('cust_main-skeleton_tables')
357        && $conf->config('cust_main-skeleton_custnum') ) {
358
359     warn "  inserting skeleton records\n"
360       if $DEBUG > 1;
361
362     my $error = $self->start_copy_skel;
363     if ( $error ) {
364       $dbh->rollback if $oldAutoCommit;
365       return $error;
366     }
367
368   }
369
370   warn "  ordering packages\n"
371     if $DEBUG > 1;
372
373   $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
374   if ( $error ) {
375     $dbh->rollback if $oldAutoCommit;
376     return $error;
377   }
378
379   if ( $seconds ) {
380     $dbh->rollback if $oldAutoCommit;
381     return "No svc_acct record to apply pre-paid time";
382   }
383
384   if ( $amount ) {
385     warn "  inserting initial $payby payment of $amount\n"
386       if $DEBUG > 1;
387     $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
388     if ( $error ) {
389       $dbh->rollback if $oldAutoCommit;
390       return "inserting payment (transaction rolled back): $error";
391     }
392   }
393
394   unless ( $import || $skip_fuzzyfiles ) {
395     warn "  queueing fuzzyfiles update\n"
396       if $DEBUG > 1;
397     $error = $self->queue_fuzzyfiles_update;
398     if ( $error ) {
399       $dbh->rollback if $oldAutoCommit;
400       return "updating fuzzy search cache: $error";
401     }
402   }
403
404   warn "  insert complete; committing transaction\n"
405     if $DEBUG > 1;
406
407   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
408   '';
409
410 }
411
412 sub start_copy_skel {
413   my $self = shift;
414
415   #'mg_user_preference' => {},
416   #'mg_user_indicator_profile.user_indicator_profile_id' => { 'mg_profile_indicator.profile_indicator_id' => { 'mg_profile_details.profile_detail_id' }, },
417   #'mg_watchlist_header.watchlist_header_id' => { 'mg_watchlist_details.watchlist_details_id' },
418   #'mg_user_grid_header.grid_header_id' => { 'mg_user_grid_details.user_grid_details_id' },
419   #'mg_portfolio_header.portfolio_header_id' => { 'mg_portfolio_trades.portfolio_trades_id' => { 'mg_portfolio_trades_positions.portfolio_trades_positions_id' } },
420   my @tables = eval($conf->config_binary('cust_main-skeleton_tables'));
421   die $@ if $@;
422
423   _copy_skel( 'cust_main',                                 #tablename
424               $conf->config('cust_main-skeleton_custnum'), #sourceid
425               $self->custnum,                              #destid
426               @tables,                                     #child tables
427             );
428 }
429
430 #recursive subroutine, not a method
431 sub _copy_skel {
432   my( $table, $sourceid, $destid, %child_tables ) = @_;
433
434   my $primary_key;
435   if ( $table =~ /^(\w+)\.(\w+)$/ ) {
436     ( $table, $primary_key ) = ( $1, $2 );
437   } else {
438     my $dbdef_table = dbdef->table($table);
439     $primary_key = $dbdef_table->primary_key
440       or return "$table has no primary key".
441                 " (or do you need to run dbdef-create?)";
442   }
443
444   warn "  _copy_skel: $table.$primary_key $sourceid to $destid for ".
445        join (', ', keys %child_tables). "\n"
446     if $DEBUG > 2;
447
448   foreach my $child_table_def ( keys %child_tables ) {
449
450     my $child_table;
451     my $child_pkey = '';
452     if ( $child_table_def =~ /^(\w+)\.(\w+)$/ ) {
453       ( $child_table, $child_pkey ) = ( $1, $2 );
454     } else {
455       $child_table = $child_table_def;
456
457       $child_pkey = dbdef->table($child_table)->primary_key;
458       #  or return "$table has no primary key".
459       #            " (or do you need to run dbdef-create?)\n";
460     }
461
462     my $sequence = '';
463     if ( keys %{ $child_tables{$child_table_def} } ) {
464
465       return "$child_table has no primary key".
466              " (run dbdef-create or try specifying it?)\n"
467         unless $child_pkey;
468
469       #false laziness w/Record::insert and only works on Pg
470       #refactor the proper last-inserted-id stuff out of Record::insert if this
471       # ever gets use for anything besides a quick kludge for one customer
472       my $default = dbdef->table($child_table)->column($child_pkey)->default;
473       $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i
474         or return "can't parse $child_table.$child_pkey default value ".
475                   " for sequence name: $default";
476       $sequence = $1;
477
478     }
479   
480     my @sel_columns = grep { $_ ne $primary_key }
481                            dbdef->table($child_table)->columns;
482     my $sel_columns = join(', ', @sel_columns );
483
484     my @ins_columns = grep { $_ ne $child_pkey } @sel_columns;
485     my $ins_columns = ' ( '. join(', ', $primary_key, @ins_columns ). ' ) ';
486     my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) ';
487
488     my $sel_st = "SELECT $sel_columns FROM $child_table".
489                  " WHERE $primary_key = $sourceid";
490     warn "    $sel_st\n"
491       if $DEBUG > 2;
492     my $sel_sth = dbh->prepare( $sel_st )
493       or return dbh->errstr;
494   
495     $sel_sth->execute or return $sel_sth->errstr;
496
497     while ( my $row = $sel_sth->fetchrow_hashref ) {
498
499       warn "    selected row: ".
500            join(', ', map { "$_=".$row->{$_} } keys %$row ). "\n"
501         if $DEBUG > 2;
502
503       my $statement =
504         "INSERT INTO $child_table $ins_columns VALUES $placeholders";
505       my $ins_sth =dbh->prepare($statement)
506           or return dbh->errstr;
507       my @param = ( $destid, map $row->{$_}, @ins_columns );
508       warn "    $statement: [ ". join(', ', @param). " ]\n"
509         if $DEBUG > 2;
510       $ins_sth->execute( @param )
511         or return $ins_sth->errstr;
512
513       #next unless keys %{ $child_tables{$child_table} };
514       next unless $sequence;
515       
516       #another section of that laziness
517       my $seq_sql = "SELECT currval('$sequence')";
518       my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr;
519       $seq_sth->execute or return $seq_sth->errstr;
520       my $insertid = $seq_sth->fetchrow_arrayref->[0];
521   
522       # don't drink soap!  recurse!  recurse!  okay!
523       my $error =
524         _copy_skel( $child_table_def,
525                     $row->{$child_pkey}, #sourceid
526                     $insertid, #destid
527                     %{ $child_tables{$child_table_def} },
528                   );
529       return $error if $error;
530
531     }
532
533   }
534
535   return '';
536
537 }
538
539 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
540
541 Like the insert method on an existing record, this method orders a package
542 and included services atomicaly.  Pass a Tie::RefHash data structure to this
543 method containing FS::cust_pkg and FS::svc_I<tablename> objects.  There should
544 be a better explanation of this, but until then, here's an example:
545
546   use Tie::RefHash;
547   tie %hash, 'Tie::RefHash'; #this part is important
548   %hash = (
549     $cust_pkg => [ $svc_acct ],
550     ...
551   );
552   $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
553
554 Services can be new, in which case they are inserted, or existing unaudited
555 services, in which case they are linked to the newly-created package.
556
557 Currently available options are: I<depend_jobnum> and I<noexport>.
558
559 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
560 on the supplied jobnum (they will not run until the specific job completes).
561 This can be used to defer provisioning until some action completes (such
562 as running the customer's credit card successfully).
563
564 The I<noexport> option is deprecated.  If I<noexport> is set true, no
565 provisioning jobs (exports) are scheduled.  (You can schedule them later with
566 the B<reexport> method for each cust_pkg object.  Using the B<reexport> method
567 on the cust_main object is not recommended, as existing services will also be
568 reexported.)
569
570 =cut
571
572 sub order_pkgs {
573   my $self = shift;
574   my $cust_pkgs = shift;
575   my $seconds = shift;
576   my %options = @_;
577   my %svc_options = ();
578   $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
579     if exists $options{'depend_jobnum'};
580   warn "$me order_pkgs called with options ".
581        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
582     if $DEBUG;
583
584   local $SIG{HUP} = 'IGNORE';
585   local $SIG{INT} = 'IGNORE';
586   local $SIG{QUIT} = 'IGNORE';
587   local $SIG{TERM} = 'IGNORE';
588   local $SIG{TSTP} = 'IGNORE';
589   local $SIG{PIPE} = 'IGNORE';
590
591   my $oldAutoCommit = $FS::UID::AutoCommit;
592   local $FS::UID::AutoCommit = 0;
593   my $dbh = dbh;
594
595   local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
596
597   foreach my $cust_pkg ( keys %$cust_pkgs ) {
598     $cust_pkg->custnum( $self->custnum );
599     my $error = $cust_pkg->insert;
600     if ( $error ) {
601       $dbh->rollback if $oldAutoCommit;
602       return "inserting cust_pkg (transaction rolled back): $error";
603     }
604     foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
605       if ( $svc_something->svcnum ) {
606         my $old_cust_svc = $svc_something->cust_svc;
607         my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
608         $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
609         $error = $new_cust_svc->replace($old_cust_svc);
610       } else {
611         $svc_something->pkgnum( $cust_pkg->pkgnum );
612         if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
613           $svc_something->seconds( $svc_something->seconds + $$seconds );
614           $$seconds = 0;
615         }
616         $error = $svc_something->insert(%svc_options);
617       }
618       if ( $error ) {
619         $dbh->rollback if $oldAutoCommit;
620         #return "inserting svc_ (transaction rolled back): $error";
621         return $error;
622       }
623     }
624   }
625
626   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
627   ''; #no error
628 }
629
630 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
631
632 Recharges this (existing) customer with the specified prepaid card (see
633 L<FS::prepay_credit>), specified either by I<identifier> or as an
634 FS::prepay_credit object.  If there is an error, returns the error, otherwise
635 returns false.
636
637 Optionally, four scalar references can be passed as well.  They will have their
638 values filled in with the amount, number of seconds, and number of upload and
639 download bytes applied by this prepaid
640 card.
641
642 =cut
643
644 sub recharge_prepay { 
645   my( $self, $prepay_credit, $amountref, $secondsref, 
646       $upbytesref, $downbytesref, $totalbytesref ) = @_;
647
648   local $SIG{HUP} = 'IGNORE';
649   local $SIG{INT} = 'IGNORE';
650   local $SIG{QUIT} = 'IGNORE';
651   local $SIG{TERM} = 'IGNORE';
652   local $SIG{TSTP} = 'IGNORE';
653   local $SIG{PIPE} = 'IGNORE';
654
655   my $oldAutoCommit = $FS::UID::AutoCommit;
656   local $FS::UID::AutoCommit = 0;
657   my $dbh = dbh;
658
659   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes) = ( 0, 0, 0, 0, 0 );
660
661   my $error = $self->get_prepay($prepay_credit, \$amount,
662                                 \$seconds, \$upbytes, \$downbytes, \$totalbytes)
663            || $self->increment_seconds($seconds)
664            || $self->increment_upbytes($upbytes)
665            || $self->increment_downbytes($downbytes)
666            || $self->increment_totalbytes($totalbytes)
667            || $self->insert_cust_pay_prepay( $amount,
668                                              ref($prepay_credit)
669                                                ? $prepay_credit->identifier
670                                                : $prepay_credit
671                                            );
672
673   if ( $error ) {
674     $dbh->rollback if $oldAutoCommit;
675     return $error;
676   }
677
678   if ( defined($amountref)  ) { $$amountref  = $amount;  }
679   if ( defined($secondsref) ) { $$secondsref = $seconds; }
680   if ( defined($upbytesref) ) { $$upbytesref = $upbytes; }
681   if ( defined($downbytesref) ) { $$downbytesref = $downbytes; }
682   if ( defined($totalbytesref) ) { $$totalbytesref = $totalbytes; }
683
684   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
685   '';
686
687 }
688
689 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ , AMOUNTREF, SECONDSREF
690
691 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
692 specified either by I<identifier> or as an FS::prepay_credit object.
693
694 References to I<amount> and I<seconds> scalars should be passed as arguments
695 and will be incremented by the values of the prepaid card.
696
697 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
698 check or set this customer's I<agentnum>.
699
700 If there is an error, returns the error, otherwise returns false.
701
702 =cut
703
704
705 sub get_prepay {
706   my( $self, $prepay_credit, $amountref, $secondsref,
707       $upref, $downref, $totalref) = @_;
708
709   local $SIG{HUP} = 'IGNORE';
710   local $SIG{INT} = 'IGNORE';
711   local $SIG{QUIT} = 'IGNORE';
712   local $SIG{TERM} = 'IGNORE';
713   local $SIG{TSTP} = 'IGNORE';
714   local $SIG{PIPE} = 'IGNORE';
715
716   my $oldAutoCommit = $FS::UID::AutoCommit;
717   local $FS::UID::AutoCommit = 0;
718   my $dbh = dbh;
719
720   unless ( ref($prepay_credit) ) {
721
722     my $identifier = $prepay_credit;
723
724     $prepay_credit = qsearchs(
725       'prepay_credit',
726       { 'identifier' => $prepay_credit },
727       '',
728       'FOR UPDATE'
729     );
730
731     unless ( $prepay_credit ) {
732       $dbh->rollback if $oldAutoCommit;
733       return "Invalid prepaid card: ". $identifier;
734     }
735
736   }
737
738   if ( $prepay_credit->agentnum ) {
739     if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
740       $dbh->rollback if $oldAutoCommit;
741       return "prepaid card not valid for agent ". $self->agentnum;
742     }
743     $self->agentnum($prepay_credit->agentnum);
744   }
745
746   my $error = $prepay_credit->delete;
747   if ( $error ) {
748     $dbh->rollback if $oldAutoCommit;
749     return "removing prepay_credit (transaction rolled back): $error";
750   }
751
752   $$amountref  += $prepay_credit->amount;
753   $$secondsref += $prepay_credit->seconds;
754   $$upref      += $prepay_credit->upbytes;
755   $$downref    += $prepay_credit->downbytes;
756   $$totalref   += $prepay_credit->totalbytes;
757
758   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
759   '';
760
761 }
762
763 =item increment_upbytes SECONDS
764
765 Updates this customer's single or primary account (see L<FS::svc_acct>) by
766 the specified number of upbytes.  If there is an error, returns the error,
767 otherwise returns false.
768
769 =cut
770
771 sub increment_upbytes {
772   _increment_column( shift, 'upbytes', @_);
773 }
774
775 =item increment_downbytes SECONDS
776
777 Updates this customer's single or primary account (see L<FS::svc_acct>) by
778 the specified number of downbytes.  If there is an error, returns the error,
779 otherwise returns false.
780
781 =cut
782
783 sub increment_downbytes {
784   _increment_column( shift, 'downbytes', @_);
785 }
786
787 =item increment_totalbytes SECONDS
788
789 Updates this customer's single or primary account (see L<FS::svc_acct>) by
790 the specified number of totalbytes.  If there is an error, returns the error,
791 otherwise returns false.
792
793 =cut
794
795 sub increment_totalbytes {
796   _increment_column( shift, 'totalbytes', @_);
797 }
798
799 =item increment_seconds SECONDS
800
801 Updates this customer's single or primary account (see L<FS::svc_acct>) by
802 the specified number of seconds.  If there is an error, returns the error,
803 otherwise returns false.
804
805 =cut
806
807 sub increment_seconds {
808   _increment_column( shift, 'seconds', @_);
809 }
810
811 =item _increment_column AMOUNT
812
813 Updates this customer's single or primary account (see L<FS::svc_acct>) by
814 the specified number of seconds or bytes.  If there is an error, returns
815 the error, otherwise returns false.
816
817 =cut
818
819 sub _increment_column {
820   my( $self, $column, $amount ) = @_;
821   warn "$me increment_column called: $column, $amount\n"
822     if $DEBUG;
823
824   return '' unless $amount;
825
826   my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
827                       $self->ncancelled_pkgs;
828
829   if ( ! @cust_pkg ) {
830     return 'No packages with primary or single services found'.
831            ' to apply pre-paid time';
832   } elsif ( scalar(@cust_pkg) > 1 ) {
833     #maybe have a way to specify the package/account?
834     return 'Multiple packages found to apply pre-paid time';
835   }
836
837   my $cust_pkg = $cust_pkg[0];
838   warn "  found package pkgnum ". $cust_pkg->pkgnum. "\n"
839     if $DEBUG > 1;
840
841   my @cust_svc =
842     $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
843
844   if ( ! @cust_svc ) {
845     return 'No account found to apply pre-paid time';
846   } elsif ( scalar(@cust_svc) > 1 ) {
847     return 'Multiple accounts found to apply pre-paid time';
848   }
849   
850   my $svc_acct = $cust_svc[0]->svc_x;
851   warn "  found service svcnum ". $svc_acct->pkgnum.
852        ' ('. $svc_acct->email. ")\n"
853     if $DEBUG > 1;
854
855   $column = "increment_$column";
856   $svc_acct->$column($amount);
857
858 }
859
860 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
861
862 Inserts a prepayment in the specified amount for this customer.  An optional
863 second argument can specify the prepayment identifier for tracking purposes.
864 If there is an error, returns the error, otherwise returns false.
865
866 =cut
867
868 sub insert_cust_pay_prepay {
869   shift->insert_cust_pay('PREP', @_);
870 }
871
872 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
873
874 Inserts a cash payment in the specified amount for this customer.  An optional
875 second argument can specify the payment identifier for tracking purposes.
876 If there is an error, returns the error, otherwise returns false.
877
878 =cut
879
880 sub insert_cust_pay_cash {
881   shift->insert_cust_pay('CASH', @_);
882 }
883
884 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
885
886 Inserts a Western Union payment in the specified amount for this customer.  An
887 optional second argument can specify the prepayment identifier for tracking
888 purposes.  If there is an error, returns the error, otherwise returns false.
889
890 =cut
891
892 sub insert_cust_pay_west {
893   shift->insert_cust_pay('WEST', @_);
894 }
895
896 sub insert_cust_pay {
897   my( $self, $payby, $amount ) = splice(@_, 0, 3);
898   my $payinfo = scalar(@_) ? shift : '';
899
900   my $cust_pay = new FS::cust_pay {
901     'custnum' => $self->custnum,
902     'paid'    => sprintf('%.2f', $amount),
903     #'_date'   => #date the prepaid card was purchased???
904     'payby'   => $payby,
905     'payinfo' => $payinfo,
906   };
907   $cust_pay->insert;
908
909 }
910
911 =item reexport
912
913 This method is deprecated.  See the I<depend_jobnum> option to the insert and
914 order_pkgs methods for a better way to defer provisioning.
915
916 Re-schedules all exports by calling the B<reexport> method of all associated
917 packages (see L<FS::cust_pkg>).  If there is an error, returns the error;
918 otherwise returns false.
919
920 =cut
921
922 sub reexport {
923   my $self = shift;
924
925   carp "WARNING: FS::cust_main::reexport is deprectated; ".
926        "use the depend_jobnum option to insert or order_pkgs to delay export";
927
928   local $SIG{HUP} = 'IGNORE';
929   local $SIG{INT} = 'IGNORE';
930   local $SIG{QUIT} = 'IGNORE';
931   local $SIG{TERM} = 'IGNORE';
932   local $SIG{TSTP} = 'IGNORE';
933   local $SIG{PIPE} = 'IGNORE';
934
935   my $oldAutoCommit = $FS::UID::AutoCommit;
936   local $FS::UID::AutoCommit = 0;
937   my $dbh = dbh;
938
939   foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
940     my $error = $cust_pkg->reexport;
941     if ( $error ) {
942       $dbh->rollback if $oldAutoCommit;
943       return $error;
944     }
945   }
946
947   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
948   '';
949
950 }
951
952 =item delete NEW_CUSTNUM
953
954 This deletes the customer.  If there is an error, returns the error, otherwise
955 returns false.
956
957 This will completely remove all traces of the customer record.  This is not
958 what you want when a customer cancels service; for that, cancel all of the
959 customer's packages (see L</cancel>).
960
961 If the customer has any uncancelled packages, you need to pass a new (valid)
962 customer number for those packages to be transferred to.  Cancelled packages
963 will be deleted.  Did I mention that this is NOT what you want when a customer
964 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
965
966 You can't delete a customer with invoices (see L<FS::cust_bill>),
967 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
968 refunds (see L<FS::cust_refund>).
969
970 =cut
971
972 sub delete {
973   my $self = shift;
974
975   local $SIG{HUP} = 'IGNORE';
976   local $SIG{INT} = 'IGNORE';
977   local $SIG{QUIT} = 'IGNORE';
978   local $SIG{TERM} = 'IGNORE';
979   local $SIG{TSTP} = 'IGNORE';
980   local $SIG{PIPE} = 'IGNORE';
981
982   my $oldAutoCommit = $FS::UID::AutoCommit;
983   local $FS::UID::AutoCommit = 0;
984   my $dbh = dbh;
985
986   if ( $self->cust_bill ) {
987     $dbh->rollback if $oldAutoCommit;
988     return "Can't delete a customer with invoices";
989   }
990   if ( $self->cust_credit ) {
991     $dbh->rollback if $oldAutoCommit;
992     return "Can't delete a customer with credits";
993   }
994   if ( $self->cust_pay ) {
995     $dbh->rollback if $oldAutoCommit;
996     return "Can't delete a customer with payments";
997   }
998   if ( $self->cust_refund ) {
999     $dbh->rollback if $oldAutoCommit;
1000     return "Can't delete a customer with refunds";
1001   }
1002
1003   my @cust_pkg = $self->ncancelled_pkgs;
1004   if ( @cust_pkg ) {
1005     my $new_custnum = shift;
1006     unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
1007       $dbh->rollback if $oldAutoCommit;
1008       return "Invalid new customer number: $new_custnum";
1009     }
1010     foreach my $cust_pkg ( @cust_pkg ) {
1011       my %hash = $cust_pkg->hash;
1012       $hash{'custnum'} = $new_custnum;
1013       my $new_cust_pkg = new FS::cust_pkg ( \%hash );
1014       my $error = $new_cust_pkg->replace($cust_pkg);
1015       if ( $error ) {
1016         $dbh->rollback if $oldAutoCommit;
1017         return $error;
1018       }
1019     }
1020   }
1021   my @cancelled_cust_pkg = $self->all_pkgs;
1022   foreach my $cust_pkg ( @cancelled_cust_pkg ) {
1023     my $error = $cust_pkg->delete;
1024     if ( $error ) {
1025       $dbh->rollback if $oldAutoCommit;
1026       return $error;
1027     }
1028   }
1029
1030   foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
1031     qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
1032   ) {
1033     my $error = $cust_main_invoice->delete;
1034     if ( $error ) {
1035       $dbh->rollback if $oldAutoCommit;
1036       return $error;
1037     }
1038   }
1039
1040   my $error = $self->SUPER::delete;
1041   if ( $error ) {
1042     $dbh->rollback if $oldAutoCommit;
1043     return $error;
1044   }
1045
1046   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1047   '';
1048
1049 }
1050
1051 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
1052
1053 Replaces the OLD_RECORD with this one in the database.  If there is an error,
1054 returns the error, otherwise returns false.
1055
1056 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
1057 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
1058 expected and rollback the entire transaction; it is not necessary to call 
1059 check_invoicing_list first.  Here's an example:
1060
1061   $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
1062
1063 =cut
1064
1065 sub replace {
1066   my $self = shift;
1067   my $old = shift;
1068   my @param = @_;
1069   warn "$me replace called\n"
1070     if $DEBUG;
1071
1072   local $SIG{HUP} = 'IGNORE';
1073   local $SIG{INT} = 'IGNORE';
1074   local $SIG{QUIT} = 'IGNORE';
1075   local $SIG{TERM} = 'IGNORE';
1076   local $SIG{TSTP} = 'IGNORE';
1077   local $SIG{PIPE} = 'IGNORE';
1078
1079   # We absolutely have to have an old vs. new record to make this work.
1080   if (!defined($old)) {
1081     $old = qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
1082   }
1083
1084   my $curuser = $FS::CurrentUser::CurrentUser;
1085   if (    $self->payby eq 'COMP'
1086        && $self->payby ne $old->payby
1087        && ! $curuser->access_right('Complimentary customer')
1088      )
1089   {
1090     return "You are not permitted to create complimentary accounts.";
1091   }
1092
1093   local($ignore_expired_card) = 1
1094     if $old->payby  =~ /^(CARD|DCRD)$/
1095     && $self->payby =~ /^(CARD|DCRD)$/
1096     && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
1097
1098   my $oldAutoCommit = $FS::UID::AutoCommit;
1099   local $FS::UID::AutoCommit = 0;
1100   my $dbh = dbh;
1101
1102   my $error = $self->SUPER::replace($old);
1103
1104   if ( $error ) {
1105     $dbh->rollback if $oldAutoCommit;
1106     return $error;
1107   }
1108
1109   if ( @param ) { # INVOICING_LIST_ARYREF
1110     my $invoicing_list = shift @param;
1111     $error = $self->check_invoicing_list( $invoicing_list );
1112     if ( $error ) {
1113       $dbh->rollback if $oldAutoCommit;
1114       return $error;
1115     }
1116     $self->invoicing_list( $invoicing_list );
1117   }
1118
1119   if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
1120        grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
1121     # card/check/lec info has changed, want to retry realtime_ invoice events
1122     my $error = $self->retry_realtime;
1123     if ( $error ) {
1124       $dbh->rollback if $oldAutoCommit;
1125       return $error;
1126     }
1127   }
1128
1129   unless ( $import || $skip_fuzzyfiles ) {
1130     $error = $self->queue_fuzzyfiles_update;
1131     if ( $error ) {
1132       $dbh->rollback if $oldAutoCommit;
1133       return "updating fuzzy search cache: $error";
1134     }
1135   }
1136
1137   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1138   '';
1139
1140 }
1141
1142 =item queue_fuzzyfiles_update
1143
1144 Used by insert & replace to update the fuzzy search cache
1145
1146 =cut
1147
1148 sub queue_fuzzyfiles_update {
1149   my $self = shift;
1150
1151   local $SIG{HUP} = 'IGNORE';
1152   local $SIG{INT} = 'IGNORE';
1153   local $SIG{QUIT} = 'IGNORE';
1154   local $SIG{TERM} = 'IGNORE';
1155   local $SIG{TSTP} = 'IGNORE';
1156   local $SIG{PIPE} = 'IGNORE';
1157
1158   my $oldAutoCommit = $FS::UID::AutoCommit;
1159   local $FS::UID::AutoCommit = 0;
1160   my $dbh = dbh;
1161
1162   my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1163   my $error = $queue->insert( map $self->getfield($_),
1164                                   qw(first last company)
1165                             );
1166   if ( $error ) {
1167     $dbh->rollback if $oldAutoCommit;
1168     return "queueing job (transaction rolled back): $error";
1169   }
1170
1171   if ( $self->ship_last ) {
1172     $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1173     $error = $queue->insert( map $self->getfield("ship_$_"),
1174                                  qw(first last company)
1175                            );
1176     if ( $error ) {
1177       $dbh->rollback if $oldAutoCommit;
1178       return "queueing job (transaction rolled back): $error";
1179     }
1180   }
1181
1182   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1183   '';
1184
1185 }
1186
1187 =item check
1188
1189 Checks all fields to make sure this is a valid customer record.  If there is
1190 an error, returns the error, otherwise returns false.  Called by the insert
1191 and replace methods.
1192
1193 =cut
1194
1195 sub check {
1196   my $self = shift;
1197
1198   warn "$me check BEFORE: \n". $self->_dump
1199     if $DEBUG > 2;
1200
1201   my $error =
1202     $self->ut_numbern('custnum')
1203     || $self->ut_number('agentnum')
1204     || $self->ut_textn('agent_custid')
1205     || $self->ut_number('refnum')
1206     || $self->ut_name('last')
1207     || $self->ut_name('first')
1208     || $self->ut_snumbern('birthdate')
1209     || $self->ut_snumbern('signupdate')
1210     || $self->ut_textn('company')
1211     || $self->ut_text('address1')
1212     || $self->ut_textn('address2')
1213     || $self->ut_text('city')
1214     || $self->ut_textn('county')
1215     || $self->ut_textn('state')
1216     || $self->ut_country('country')
1217     || $self->ut_anything('comments')
1218     || $self->ut_numbern('referral_custnum')
1219   ;
1220   #barf.  need message catalogs.  i18n.  etc.
1221   $error .= "Please select an advertising source."
1222     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1223   return $error if $error;
1224
1225   return "Unknown agent"
1226     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1227
1228   return "Unknown refnum"
1229     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1230
1231   return "Unknown referring custnum: ". $self->referral_custnum
1232     unless ! $self->referral_custnum 
1233            || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1234
1235   if ( $self->ss eq '' ) {
1236     $self->ss('');
1237   } else {
1238     my $ss = $self->ss;
1239     $ss =~ s/\D//g;
1240     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1241       or return "Illegal social security number: ". $self->ss;
1242     $self->ss("$1-$2-$3");
1243   }
1244
1245
1246 # bad idea to disable, causes billing to fail because of no tax rates later
1247 #  unless ( $import ) {
1248     unless ( qsearch('cust_main_county', {
1249       'country' => $self->country,
1250       'state'   => '',
1251      } ) ) {
1252       return "Unknown state/county/country: ".
1253         $self->state. "/". $self->county. "/". $self->country
1254         unless qsearch('cust_main_county',{
1255           'state'   => $self->state,
1256           'county'  => $self->county,
1257           'country' => $self->country,
1258         } );
1259     }
1260 #  }
1261
1262   $error =
1263     $self->ut_phonen('daytime', $self->country)
1264     || $self->ut_phonen('night', $self->country)
1265     || $self->ut_phonen('fax', $self->country)
1266     || $self->ut_zip('zip', $self->country)
1267   ;
1268   return $error if $error;
1269
1270   my @addfields = qw(
1271     last first company address1 address2 city county state zip
1272     country daytime night fax
1273   );
1274
1275   if ( defined $self->dbdef_table->column('ship_last') ) {
1276     if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1277                        @addfields )
1278          && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
1279        )
1280     {
1281       my $error =
1282         $self->ut_name('ship_last')
1283         || $self->ut_name('ship_first')
1284         || $self->ut_textn('ship_company')
1285         || $self->ut_text('ship_address1')
1286         || $self->ut_textn('ship_address2')
1287         || $self->ut_text('ship_city')
1288         || $self->ut_textn('ship_county')
1289         || $self->ut_textn('ship_state')
1290         || $self->ut_country('ship_country')
1291       ;
1292       return $error if $error;
1293
1294       #false laziness with above
1295       unless ( qsearchs('cust_main_county', {
1296         'country' => $self->ship_country,
1297         'state'   => '',
1298        } ) ) {
1299         return "Unknown ship_state/ship_county/ship_country: ".
1300           $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1301           unless qsearch('cust_main_county',{
1302             'state'   => $self->ship_state,
1303             'county'  => $self->ship_county,
1304             'country' => $self->ship_country,
1305           } );
1306       }
1307       #eofalse
1308
1309       $error =
1310         $self->ut_phonen('ship_daytime', $self->ship_country)
1311         || $self->ut_phonen('ship_night', $self->ship_country)
1312         || $self->ut_phonen('ship_fax', $self->ship_country)
1313         || $self->ut_zip('ship_zip', $self->ship_country)
1314       ;
1315       return $error if $error;
1316
1317     } else { # ship_ info eq billing info, so don't store dup info in database
1318       $self->setfield("ship_$_", '')
1319         foreach qw( last first company address1 address2 city county state zip
1320                     country daytime night fax );
1321     }
1322   }
1323
1324   #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
1325   #  or return "Illegal payby: ". $self->payby;
1326   #$self->payby($1);
1327   FS::payby->can_payby($self->table, $self->payby)
1328     or return "Illegal payby: ". $self->payby;
1329
1330   $error =    $self->ut_numbern('paystart_month')
1331            || $self->ut_numbern('paystart_year')
1332            || $self->ut_numbern('payissue')
1333   ;
1334   return $error if $error;
1335
1336   if ( $self->payip eq '' ) {
1337     $self->payip('');
1338   } else {
1339     $error = $self->ut_ip('payip');
1340     return $error if $error;
1341   }
1342
1343   # If it is encrypted and the private key is not availaible then we can't
1344   # check the credit card.
1345
1346   my $check_payinfo = 1;
1347
1348   if ($self->is_encrypted($self->payinfo)) {
1349     $check_payinfo = 0;
1350   }
1351
1352   if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1353
1354     my $payinfo = $self->payinfo;
1355     $payinfo =~ s/\D//g;
1356     $payinfo =~ /^(\d{13,16})$/
1357       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1358     $payinfo = $1;
1359     $self->payinfo($payinfo);
1360     validate($payinfo)
1361       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1362
1363     return gettext('unknown_card_type')
1364       if cardtype($self->payinfo) eq "Unknown";
1365
1366     my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1367     if ( $ban ) {
1368       return 'Banned credit card: banned on '.
1369              time2str('%a %h %o at %r', $ban->_date).
1370              ' by '. $ban->otaker.
1371              ' (ban# '. $ban->bannum. ')';
1372     }
1373
1374     if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
1375       if ( cardtype($self->payinfo) eq 'American Express card' ) {
1376         $self->paycvv =~ /^(\d{4})$/
1377           or return "CVV2 (CID) for American Express cards is four digits.";
1378         $self->paycvv($1);
1379       } else {
1380         $self->paycvv =~ /^(\d{3})$/
1381           or return "CVV2 (CVC2/CID) is three digits.";
1382         $self->paycvv($1);
1383       }
1384     } else {
1385       $self->paycvv('');
1386     }
1387
1388     my $cardtype = cardtype($payinfo);
1389     if ( $cardtype =~ /^(Switch|Solo)$/i ) {
1390
1391       return "Start date or issue number is required for $cardtype cards"
1392         unless $self->paystart_month && $self->paystart_year or $self->payissue;
1393
1394       return "Start month must be between 1 and 12"
1395         if $self->paystart_month
1396            and $self->paystart_month < 1 || $self->paystart_month > 12;
1397
1398       return "Start year must be 1990 or later"
1399         if $self->paystart_year
1400            and $self->paystart_year < 1990;
1401
1402       return "Issue number must be beween 1 and 99"
1403         if $self->payissue
1404           and $self->payissue < 1 || $self->payissue > 99;
1405
1406     } else {
1407       $self->paystart_month('');
1408       $self->paystart_year('');
1409       $self->payissue('');
1410     }
1411
1412   } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
1413
1414     my $payinfo = $self->payinfo;
1415     $payinfo =~ s/[^\d\@]//g;
1416     if ( $conf->exists('echeck-nonus') ) {
1417       $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
1418       $payinfo = "$1\@$2";
1419     } else {
1420       $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
1421       $payinfo = "$1\@$2";
1422     }
1423     $self->payinfo($payinfo);
1424     $self->paycvv('');
1425
1426     my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1427     if ( $ban ) {
1428       return 'Banned ACH account: banned on '.
1429              time2str('%a %h %o at %r', $ban->_date).
1430              ' by '. $ban->otaker.
1431              ' (ban# '. $ban->bannum. ')';
1432     }
1433
1434   } elsif ( $self->payby eq 'LECB' ) {
1435
1436     my $payinfo = $self->payinfo;
1437     $payinfo =~ s/\D//g;
1438     $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
1439     $payinfo = $1;
1440     $self->payinfo($payinfo);
1441     $self->paycvv('');
1442
1443   } elsif ( $self->payby eq 'BILL' ) {
1444
1445     $error = $self->ut_textn('payinfo');
1446     return "Illegal P.O. number: ". $self->payinfo if $error;
1447     $self->paycvv('');
1448
1449   } elsif ( $self->payby eq 'COMP' ) {
1450
1451     my $curuser = $FS::CurrentUser::CurrentUser;
1452     if (    ! $self->custnum
1453          && ! $curuser->access_right('Complimentary customer')
1454        )
1455     {
1456       return "You are not permitted to create complimentary accounts."
1457     }
1458
1459     $error = $self->ut_textn('payinfo');
1460     return "Illegal comp account issuer: ". $self->payinfo if $error;
1461     $self->paycvv('');
1462
1463   } elsif ( $self->payby eq 'PREPAY' ) {
1464
1465     my $payinfo = $self->payinfo;
1466     $payinfo =~ s/\W//g; #anything else would just confuse things
1467     $self->payinfo($payinfo);
1468     $error = $self->ut_alpha('payinfo');
1469     return "Illegal prepayment identifier: ". $self->payinfo if $error;
1470     return "Unknown prepayment identifier"
1471       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
1472     $self->paycvv('');
1473
1474   }
1475
1476   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
1477     return "Expiration date required"
1478       unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
1479     $self->paydate('');
1480   } else {
1481     my( $m, $y );
1482     if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
1483       ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
1484     } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1485       ( $m, $y ) = ( $3, "20$2" );
1486     } else {
1487       return "Illegal expiration date: ". $self->paydate;
1488     }
1489     $self->paydate("$y-$m-01");
1490     my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
1491     return gettext('expired_card')
1492       if !$import
1493       && !$ignore_expired_card 
1494       && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
1495   }
1496
1497   if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
1498        ( ! $conf->exists('require_cardname')
1499          || $self->payby !~ /^(CARD|DCRD)$/  ) 
1500   ) {
1501     $self->payname( $self->first. " ". $self->getfield('last') );
1502   } else {
1503     $self->payname =~ /^([\w \,\.\-\'\&]+)$/
1504       or return gettext('illegal_name'). " payname: ". $self->payname;
1505     $self->payname($1);
1506   }
1507
1508   foreach my $flag (qw( tax spool_cdr )) {
1509     $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1510     $self->$flag($1);
1511   }
1512
1513   $self->otaker(getotaker) unless $self->otaker;
1514
1515   warn "$me check AFTER: \n". $self->_dump
1516     if $DEBUG > 2;
1517
1518   $self->SUPER::check;
1519 }
1520
1521 =item all_pkgs
1522
1523 Returns all packages (see L<FS::cust_pkg>) for this customer.
1524
1525 =cut
1526
1527 sub all_pkgs {
1528   my $self = shift;
1529
1530   return $self->num_pkgs unless wantarray;
1531
1532   my @cust_pkg = ();
1533   if ( $self->{'_pkgnum'} ) {
1534     @cust_pkg = values %{ $self->{'_pkgnum'}->cache };
1535   } else {
1536     @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
1537   }
1538
1539   sort sort_packages @cust_pkg;
1540 }
1541
1542 =item ncancelled_pkgs
1543
1544 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
1545
1546 =cut
1547
1548 sub ncancelled_pkgs {
1549   my $self = shift;
1550
1551   return $self->num_ncancelled_pkgs unless wantarray;
1552
1553   my @cust_pkg = ();
1554   if ( $self->{'_pkgnum'} ) {
1555
1556     @cust_pkg = grep { ! $_->getfield('cancel') }
1557                 values %{ $self->{'_pkgnum'}->cache };
1558
1559   } else {
1560
1561     @cust_pkg =
1562       qsearch( 'cust_pkg', {
1563                              'custnum' => $self->custnum,
1564                              'cancel'  => '',
1565                            });
1566     push @cust_pkg,
1567       qsearch( 'cust_pkg', {
1568                              'custnum' => $self->custnum,
1569                              'cancel'  => 0,
1570                            });
1571   }
1572
1573   sort sort_packages @cust_pkg;
1574
1575 }
1576
1577 # This should be generalized to use config options to determine order.
1578 sub sort_packages {
1579   if ( $a->get('cancel') and $b->get('cancel') ) {
1580     $a->pkgnum <=> $b->pkgnum;
1581   } elsif ( $a->get('cancel') or $b->get('cancel') ) {
1582     return -1 if $b->get('cancel');
1583     return  1 if $a->get('cancel');
1584     return 0;
1585   } else {
1586     $a->pkgnum <=> $b->pkgnum;
1587   }
1588 }
1589
1590 =item suspended_pkgs
1591
1592 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
1593
1594 =cut
1595
1596 sub suspended_pkgs {
1597   my $self = shift;
1598   grep { $_->susp } $self->ncancelled_pkgs;
1599 }
1600
1601 =item unflagged_suspended_pkgs
1602
1603 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
1604 customer (thouse packages without the `manual_flag' set).
1605
1606 =cut
1607
1608 sub unflagged_suspended_pkgs {
1609   my $self = shift;
1610   return $self->suspended_pkgs
1611     unless dbdef->table('cust_pkg')->column('manual_flag');
1612   grep { ! $_->manual_flag } $self->suspended_pkgs;
1613 }
1614
1615 =item unsuspended_pkgs
1616
1617 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1618 this customer.
1619
1620 =cut
1621
1622 sub unsuspended_pkgs {
1623   my $self = shift;
1624   grep { ! $_->susp } $self->ncancelled_pkgs;
1625 }
1626
1627 =item num_cancelled_pkgs
1628
1629 Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
1630 customer.
1631
1632 =cut
1633
1634 sub num_cancelled_pkgs {
1635   shift->num_pkgs("cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0");
1636 }
1637
1638 sub num_ncancelled_pkgs {
1639   shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )");
1640 }
1641
1642 sub num_pkgs {
1643   my( $self, $sql ) = @_;
1644   $sql = "AND $sql" if $sql && $sql !~ /^\s*$/ && $sql !~ /^\s*AND/i;
1645   my $sth = dbh->prepare(
1646     "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? $sql"
1647   ) or die dbh->errstr;
1648   $sth->execute($self->custnum) or die $sth->errstr;
1649   $sth->fetchrow_arrayref->[0];
1650 }
1651
1652 =item unsuspend
1653
1654 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1655 and L<FS::cust_pkg>) for this customer.  Always returns a list: an empty list
1656 on success or a list of errors.
1657
1658 =cut
1659
1660 sub unsuspend {
1661   my $self = shift;
1662   grep { $_->unsuspend } $self->suspended_pkgs;
1663 }
1664
1665 =item suspend
1666
1667 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1668
1669 Returns a list: an empty list on success or a list of errors.
1670
1671 =cut
1672
1673 sub suspend {
1674   my $self = shift;
1675   grep { $_->suspend(@_) } $self->unsuspended_pkgs;
1676 }
1677
1678 =item suspend_if_pkgpart PKGPART [ , PKGPART ... ]
1679
1680 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1681 PKGPARTs (see L<FS::part_pkg>).
1682
1683 Returns a list: an empty list on success or a list of errors.
1684
1685 =cut
1686
1687 sub suspend_if_pkgpart {
1688   my $self = shift;
1689   my (@pkgparts, %opt);
1690   if (ref($_[0]) eq 'HASH'){
1691     @pkgparts = @{$_[0]{pkgparts}};
1692     %opt      = %{$_[0]};
1693   }else{
1694     @pkgparts = @_;
1695   }
1696   grep { $_->suspend(%opt) }
1697     grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1698       $self->unsuspended_pkgs;
1699 }
1700
1701 =item suspend_unless_pkgpart PKGPART [ , PKGPART ... ]
1702
1703 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1704 listed PKGPARTs (see L<FS::part_pkg>).
1705
1706 Returns a list: an empty list on success or a list of errors.
1707
1708 =cut
1709
1710 sub suspend_unless_pkgpart {
1711   my $self = shift;
1712   my (@pkgparts, %opt);
1713   if (ref($_[0]) eq 'HASH'){
1714     @pkgparts = @{$_[0]{pkgparts}};
1715     %opt      = %{$_[0]};
1716   }else{
1717     @pkgparts = @_;
1718   }
1719   grep { $_->suspend(%opt) }
1720     grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1721       $self->unsuspended_pkgs;
1722 }
1723
1724 =item cancel [ OPTION => VALUE ... ]
1725
1726 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1727
1728 Available options are: I<quiet>, I<reasonnum>, and I<ban>
1729
1730 I<quiet> can be set true to supress email cancellation notices.
1731
1732 # I<reasonnum> can be set to a cancellation reason (see L<FS::cancel_reason>)
1733
1734 I<ban> can be set true to ban this customer's credit card or ACH information,
1735 if present.
1736
1737 Always returns a list: an empty list on success or a list of errors.
1738
1739 =cut
1740
1741 sub cancel {
1742   my $self = shift;
1743   my %opt = @_;
1744
1745   if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
1746
1747     #should try decryption (we might have the private key)
1748     # and if not maybe queue a job for the server that does?
1749     return ( "Can't (yet) ban encrypted credit cards" )
1750       if $self->is_encrypted($self->payinfo);
1751
1752     my $ban = new FS::banned_pay $self->_banned_pay_hashref;
1753     my $error = $ban->insert;
1754     return ( $error ) if $error;
1755
1756   }
1757
1758   grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
1759 }
1760
1761 sub _banned_pay_hashref {
1762   my $self = shift;
1763
1764   my %payby2ban = (
1765     'CARD' => 'CARD',
1766     'DCRD' => 'CARD',
1767     'CHEK' => 'CHEK',
1768     'DCHK' => 'CHEK'
1769   );
1770
1771   {
1772     'payby'   => $payby2ban{$self->payby},
1773     'payinfo' => md5_base64($self->payinfo),
1774     #don't ever *search* on reason! #'reason'  =>
1775   };
1776 }
1777
1778 =item notes
1779
1780 Returns all notes (see L<FS::cust_main_note>) for this customer.
1781
1782 =cut
1783
1784 sub notes {
1785   my $self = shift;
1786   #order by?
1787   qsearch( 'cust_main_note',
1788            { 'custnum' => $self->custnum },
1789            '',
1790            'ORDER BY _DATE DESC'
1791          );
1792 }
1793
1794 =item agent
1795
1796 Returns the agent (see L<FS::agent>) for this customer.
1797
1798 =cut
1799
1800 sub agent {
1801   my $self = shift;
1802   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1803 }
1804
1805 =item bill OPTIONS
1806
1807 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
1808 conjunction with the collect method.
1809
1810 Options are passed as name-value pairs.
1811
1812 Currently available options are:
1813
1814 resetup - if set true, re-charges setup fees.
1815
1816 time - bills the customer as if it were that time.  Specified as a UNIX
1817 timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and
1818 L<Date::Parse> for conversion functions.  For example:
1819
1820  use Date::Parse;
1821  ...
1822  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1823
1824
1825 If there is an error, returns the error, otherwise returns false.
1826
1827 =cut
1828
1829 sub bill {
1830   my( $self, %options ) = @_;
1831   return '' if $self->payby eq 'COMP';
1832   warn "$me bill customer ". $self->custnum. "\n"
1833     if $DEBUG;
1834
1835   my $time = $options{'time'} || time;
1836
1837   my $error;
1838
1839   #put below somehow?
1840   local $SIG{HUP} = 'IGNORE';
1841   local $SIG{INT} = 'IGNORE';
1842   local $SIG{QUIT} = 'IGNORE';
1843   local $SIG{TERM} = 'IGNORE';
1844   local $SIG{TSTP} = 'IGNORE';
1845   local $SIG{PIPE} = 'IGNORE';
1846
1847   my $oldAutoCommit = $FS::UID::AutoCommit;
1848   local $FS::UID::AutoCommit = 0;
1849   my $dbh = dbh;
1850
1851   $self->select_for_update; #mutex
1852
1853   #create a new invoice
1854   #(we'll remove it later if it doesn't actually need to be generated [contains
1855   # no line items] and we're inside a transaciton so nothing else will see it)
1856   my $cust_bill = new FS::cust_bill ( {
1857     'custnum' => $self->custnum,
1858     '_date'   => $time,
1859     #'charged' => $charged,
1860     'charged' => 0,
1861   } );
1862   $error = $cust_bill->insert;
1863   if ( $error ) {
1864     $dbh->rollback if $oldAutoCommit;
1865     return "can't create invoice for customer #". $self->custnum. ": $error";
1866   }
1867   my $invnum = $cust_bill->invnum;
1868
1869   ###
1870   # find the packages which are due for billing, find out how much they are
1871   # & generate invoice database.
1872   ###
1873
1874   my( $total_setup, $total_recur ) = ( 0, 0 );
1875   my %tax;
1876   my @precommit_hooks = ();
1877
1878   foreach my $cust_pkg (
1879     qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1880   ) {
1881
1882     #NO!! next if $cust_pkg->cancel;  
1883     next if $cust_pkg->getfield('cancel');  
1884
1885     warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
1886
1887     #? to avoid use of uninitialized value errors... ?
1888     $cust_pkg->setfield('bill', '')
1889       unless defined($cust_pkg->bill);
1890  
1891     my $part_pkg = $cust_pkg->part_pkg;
1892
1893     my %hash = $cust_pkg->hash;
1894     my $old_cust_pkg = new FS::cust_pkg \%hash;
1895
1896     my @details = ();
1897
1898     ###
1899     # bill setup
1900     ###
1901
1902     my $setup = 0;
1903     if ( !$cust_pkg->setup || $options{'resetup'} ) {
1904     
1905       warn "    bill setup\n" if $DEBUG > 1;
1906
1907       $setup = eval { $cust_pkg->calc_setup( $time, \@details ) };
1908       if ( $@ ) {
1909         $dbh->rollback if $oldAutoCommit;
1910         return "$@ running calc_setup for $cust_pkg\n";
1911       }
1912
1913       $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1914     }
1915
1916     ###
1917     # bill recurring fee
1918     ### 
1919
1920     my $recur = 0;
1921     my $sdate;
1922     if ( $part_pkg->getfield('freq') ne '0' &&
1923          ! $cust_pkg->getfield('susp') &&
1924          ( $cust_pkg->getfield('bill') || 0 ) <= $time
1925     ) {
1926
1927       warn "    bill recur\n" if $DEBUG > 1;
1928
1929       # XXX shared with $recur_prog
1930       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1931
1932       #over two params!  lets at least switch to a hashref for the rest...
1933       my %param = ( 'precommit_hooks' => \@precommit_hooks, );
1934
1935       $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) };
1936       if ( $@ ) {
1937         $dbh->rollback if $oldAutoCommit;
1938         return "$@ running calc_recur for $cust_pkg\n";
1939       }
1940
1941       #change this bit to use Date::Manip? CAREFUL with timezones (see
1942       # mailing list archive)
1943       my ($sec,$min,$hour,$mday,$mon,$year) =
1944         (localtime($sdate) )[0,1,2,3,4,5];
1945
1946       #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1947       # only for figuring next bill date, nothing else, so, reset $sdate again
1948       # here
1949       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1950       $cust_pkg->last_bill($sdate)
1951         if $cust_pkg->dbdef_table->column('last_bill');
1952
1953       if ( $part_pkg->freq =~ /^\d+$/ ) {
1954         $mon += $part_pkg->freq;
1955         until ( $mon < 12 ) { $mon -= 12; $year++; }
1956       } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1957         my $weeks = $1;
1958         $mday += $weeks * 7;
1959       } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1960         my $days = $1;
1961         $mday += $days;
1962       } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) {
1963         my $hours = $1;
1964         $hour += $hours;
1965       } else {
1966         $dbh->rollback if $oldAutoCommit;
1967         return "unparsable frequency: ". $part_pkg->freq;
1968       }
1969       $cust_pkg->setfield('bill',
1970         timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1971     }
1972
1973     warn "\$setup is undefined" unless defined($setup);
1974     warn "\$recur is undefined" unless defined($recur);
1975     warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1976
1977     ###
1978     # If $cust_pkg has been modified, update it and create cust_bill_pkg records
1979     ###
1980
1981     if ( $cust_pkg->modified ) {
1982
1983       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
1984         if $DEBUG >1;
1985
1986       $error=$cust_pkg->replace($old_cust_pkg);
1987       if ( $error ) { #just in case
1988         $dbh->rollback if $oldAutoCommit;
1989         return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1990       }
1991
1992       $setup = sprintf( "%.2f", $setup );
1993       $recur = sprintf( "%.2f", $recur );
1994       if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1995         $dbh->rollback if $oldAutoCommit;
1996         return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1997       }
1998       if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1999         $dbh->rollback if $oldAutoCommit;
2000         return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
2001       }
2002
2003       if ( $setup != 0 || $recur != 0 ) {
2004
2005         warn "    charges (setup=$setup, recur=$recur); adding line items\n"
2006           if $DEBUG > 1;
2007         my $cust_bill_pkg = new FS::cust_bill_pkg ({
2008           'invnum'  => $invnum,
2009           'pkgnum'  => $cust_pkg->pkgnum,
2010           'setup'   => $setup,
2011           'recur'   => $recur,
2012           'sdate'   => $sdate,
2013           'edate'   => $cust_pkg->bill,
2014           'details' => \@details,
2015         });
2016         $error = $cust_bill_pkg->insert;
2017         if ( $error ) {
2018           $dbh->rollback if $oldAutoCommit;
2019           return "can't create invoice line item for invoice #$invnum: $error";
2020         }
2021         $total_setup += $setup;
2022         $total_recur += $recur;
2023
2024         ###
2025         # handle taxes
2026         ###
2027
2028         unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
2029
2030           my $prefix = 
2031             ( $conf->exists('tax-ship_address') && length($self->ship_last) )
2032             ? 'ship_'
2033             : '';
2034           my %taxhash = map { $_ => $self->get("$prefix$_") }
2035                             qw( state county country );
2036
2037           $taxhash{'taxclass'} = $part_pkg->taxclass;
2038
2039           my @taxes = qsearch( 'cust_main_county', \%taxhash );
2040
2041           unless ( @taxes ) {
2042             $taxhash{'taxclass'} = '';
2043             @taxes =  qsearch( 'cust_main_county', \%taxhash );
2044           }
2045
2046           #one more try at a whole-country tax rate
2047           unless ( @taxes ) {
2048             $taxhash{$_} = '' foreach qw( state county );
2049             @taxes =  qsearch( 'cust_main_county', \%taxhash );
2050           }
2051
2052           # maybe eliminate this entirely, along with all the 0% records
2053           unless ( @taxes ) {
2054             $dbh->rollback if $oldAutoCommit;
2055             return
2056               "fatal: can't find tax rate for state/county/country/taxclass ".
2057               join('/', ( map $self->get("$prefix$_"),
2058                               qw(state county country)
2059                         ),
2060                         $part_pkg->taxclass ). "\n";
2061           }
2062   
2063           foreach my $tax ( @taxes ) {
2064
2065             my $taxable_charged = 0;
2066             $taxable_charged += $setup
2067               unless $part_pkg->setuptax =~ /^Y$/i
2068                   || $tax->setuptax =~ /^Y$/i;
2069             $taxable_charged += $recur
2070               unless $part_pkg->recurtax =~ /^Y$/i
2071                   || $tax->recurtax =~ /^Y$/i;
2072             next unless $taxable_charged;
2073
2074             if ( $tax->exempt_amount && $tax->exempt_amount > 0 ) {
2075               #my ($mon,$year) = (localtime($sdate) )[4,5];
2076               my ($mon,$year) = (localtime( $sdate || $cust_bill->_date ) )[4,5];
2077               $mon++;
2078               my $freq = $part_pkg->freq || 1;
2079               if ( $freq !~ /(\d+)$/ ) {
2080                 $dbh->rollback if $oldAutoCommit;
2081                 return "daily/weekly package definitions not (yet?)".
2082                        " compatible with monthly tax exemptions";
2083               }
2084               my $taxable_per_month =
2085                 sprintf("%.2f", $taxable_charged / $freq );
2086
2087               #call the whole thing off if this customer has any old
2088               #exemption records...
2089               my @cust_tax_exempt =
2090                 qsearch( 'cust_tax_exempt' => { custnum=> $self->custnum } );
2091               if ( @cust_tax_exempt ) {
2092                 $dbh->rollback if $oldAutoCommit;
2093                 return
2094                   'this customer still has old-style tax exemption records; '.
2095                   'run bin/fs-migrate-cust_tax_exempt?';
2096               }
2097
2098               foreach my $which_month ( 1 .. $freq ) {
2099
2100                 #maintain the new exemption table now
2101                 my $sql = "
2102                   SELECT SUM(amount)
2103                     FROM cust_tax_exempt_pkg
2104                       LEFT JOIN cust_bill_pkg USING ( billpkgnum )
2105                       LEFT JOIN cust_bill     USING ( invnum     )
2106                     WHERE custnum = ?
2107                       AND taxnum  = ?
2108                       AND year    = ?
2109                       AND month   = ?
2110                 ";
2111                 my $sth = dbh->prepare($sql) or do {
2112                   $dbh->rollback if $oldAutoCommit;
2113                   return "fatal: can't lookup exising exemption: ". dbh->errstr;
2114                 };
2115                 $sth->execute(
2116                   $self->custnum,
2117                   $tax->taxnum,
2118                   1900+$year,
2119                   $mon,
2120                 ) or do {
2121                   $dbh->rollback if $oldAutoCommit;
2122                   return "fatal: can't lookup exising exemption: ". dbh->errstr;
2123                 };
2124                 my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
2125                 
2126                 my $remaining_exemption =
2127                   $tax->exempt_amount - $existing_exemption;
2128                 if ( $remaining_exemption > 0 ) {
2129                   my $addl = $remaining_exemption > $taxable_per_month
2130                     ? $taxable_per_month
2131                     : $remaining_exemption;
2132                   $taxable_charged -= $addl;
2133
2134                   my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
2135                     'billpkgnum' => $cust_bill_pkg->billpkgnum,
2136                     'taxnum'     => $tax->taxnum,
2137                     'year'       => 1900+$year,
2138                     'month'      => $mon,
2139                     'amount'     => sprintf("%.2f", $addl ),
2140                   } );
2141                   $error = $cust_tax_exempt_pkg->insert;
2142                   if ( $error ) {
2143                     $dbh->rollback if $oldAutoCommit;
2144                     return "fatal: can't insert cust_tax_exempt_pkg: $error";
2145                   }
2146                 } # if $remaining_exemption > 0
2147
2148                 #++
2149                 $mon++;
2150                 #until ( $mon < 12 ) { $mon -= 12; $year++; }
2151                 until ( $mon < 13 ) { $mon -= 12; $year++; }
2152   
2153               } #foreach $which_month
2154   
2155             } #if $tax->exempt_amount
2156
2157             $taxable_charged = sprintf( "%.2f", $taxable_charged);
2158
2159             #$tax += $taxable_charged * $cust_main_county->tax / 100
2160             $tax{ $tax->taxname || 'Tax' } +=
2161               $taxable_charged * $tax->tax / 100
2162
2163           } #foreach my $tax ( @taxes )
2164
2165         } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
2166
2167       } #if $setup != 0 || $recur != 0
2168       
2169     } #if $cust_pkg->modified
2170
2171   } #foreach my $cust_pkg
2172
2173   unless ( $cust_bill->cust_bill_pkg ) {
2174     $cust_bill->delete; #don't create an invoice w/o line items
2175     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2176     return '';
2177   }
2178
2179   my $charged = sprintf( "%.2f", $total_setup + $total_recur );
2180
2181   foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
2182     my $tax = sprintf("%.2f", $tax{$taxname} );
2183     $charged = sprintf( "%.2f", $charged+$tax );
2184   
2185     my $cust_bill_pkg = new FS::cust_bill_pkg ({
2186       'invnum'   => $invnum,
2187       'pkgnum'   => 0,
2188       'setup'    => $tax,
2189       'recur'    => 0,
2190       'sdate'    => '',
2191       'edate'    => '',
2192       'itemdesc' => $taxname,
2193     });
2194     $error = $cust_bill_pkg->insert;
2195     if ( $error ) {
2196       $dbh->rollback if $oldAutoCommit;
2197       return "can't create invoice line item for invoice #$invnum: $error";
2198     }
2199     $total_setup += $tax;
2200
2201   }
2202
2203   $cust_bill->charged( sprintf( "%.2f", $total_setup + $total_recur ) );
2204   $error = $cust_bill->replace;
2205   if ( $error ) {
2206     $dbh->rollback if $oldAutoCommit;
2207     return "can't update charged for invoice #$invnum: $error";
2208   }
2209
2210   foreach my $hook ( @precommit_hooks ) { 
2211     eval {
2212       &{$hook}; #($self) ?
2213     };
2214     if ( $@ ) {
2215       $dbh->rollback if $oldAutoCommit;
2216       return "$@ running precommit hook $hook\n";
2217     }
2218   }
2219   
2220   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2221   ''; #no error
2222 }
2223
2224 =item collect OPTIONS
2225
2226 (Attempt to) collect money for this customer's outstanding invoices (see
2227 L<FS::cust_bill>).  Usually used after the bill method.
2228
2229 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
2230 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
2231 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
2232
2233 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
2234 and the invoice events web interface.
2235
2236 If there is an error, returns the error, otherwise returns false.
2237
2238 Options are passed as name-value pairs.
2239
2240 Currently available options are:
2241
2242 invoice_time - Use this time when deciding when to print invoices and
2243 late notices on those invoices.  The default is now.  It is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse>
2244 for conversion functions.
2245
2246 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
2247 events.
2248
2249 quiet - set true to surpress email card/ACH decline notices.
2250
2251 freq - "1d" for the traditional, daily events (the default), or "1m" for the
2252 new monthly events
2253
2254 payby - allows for one time override of normal customer billing method
2255
2256 =cut
2257
2258 sub collect {
2259   my( $self, %options ) = @_;
2260   my $invoice_time = $options{'invoice_time'} || time;
2261
2262   #put below somehow?
2263   local $SIG{HUP} = 'IGNORE';
2264   local $SIG{INT} = 'IGNORE';
2265   local $SIG{QUIT} = 'IGNORE';
2266   local $SIG{TERM} = 'IGNORE';
2267   local $SIG{TSTP} = 'IGNORE';
2268   local $SIG{PIPE} = 'IGNORE';
2269
2270   my $oldAutoCommit = $FS::UID::AutoCommit;
2271   local $FS::UID::AutoCommit = 0;
2272   my $dbh = dbh;
2273
2274   $self->select_for_update; #mutex
2275
2276   my $balance = $self->balance;
2277   warn "$me collect customer ". $self->custnum. ": balance $balance\n"
2278     if $DEBUG;
2279   unless ( $balance > 0 ) { #redundant?????
2280     $dbh->rollback if $oldAutoCommit; #hmm
2281     return '';
2282   }
2283
2284   if ( exists($options{'retry_card'}) ) {
2285     carp 'retry_card option passed to collect is deprecated; use retry';
2286     $options{'retry'} ||= $options{'retry_card'};
2287   }
2288   if ( exists($options{'retry'}) && $options{'retry'} ) {
2289     my $error = $self->retry_realtime;
2290     if ( $error ) {
2291       $dbh->rollback if $oldAutoCommit;
2292       return $error;
2293     }
2294   }
2295
2296   my $extra_sql = '';
2297   if ( defined $options{'freq'} && $options{'freq'} eq '1m' ) {
2298     $extra_sql = " AND freq = '1m' ";
2299   } else {
2300     $extra_sql = " AND ( freq = '1d' OR freq IS NULL OR freq = '' ) ";
2301   }
2302
2303   foreach my $cust_bill ( $self->open_cust_bill ) {
2304
2305     # don't try to charge for the same invoice if it's already in a batch
2306     #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
2307
2308     last if $self->balance <= 0;
2309
2310     warn "  invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")\n"
2311       if $DEBUG > 1;
2312
2313     foreach my $part_bill_event ( due_events ( $cust_bill,
2314                                                exists($options{'payby'}) 
2315                                                  ? $options{'payby'}
2316                                                  : $self->payby,
2317                                                $invoice_time,
2318                                                $extra_sql ) ) {
2319
2320       last if $cust_bill->owed <= 0  # don't run subsequent events if owed<=0
2321            || $self->balance   <= 0; # or if balance<=0
2322
2323       {
2324         local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
2325         warn "  do_event " .  $cust_bill . " ". (%options) .  "\n"
2326           if $DEBUG > 1;
2327
2328         if (my $error = $part_bill_event->do_event($cust_bill, %options)) {
2329           # gah, even with transactions.
2330           $dbh->commit if $oldAutoCommit; #well.
2331           return $error;
2332         }
2333       }
2334
2335     }
2336
2337   }
2338
2339   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2340   '';
2341
2342 }
2343
2344 =item retry_realtime
2345
2346 Schedules realtime / batch  credit card / electronic check / LEC billing
2347 events for for retry.  Useful if card information has changed or manual
2348 retry is desired.  The 'collect' method must be called to actually retry
2349 the transaction.
2350
2351 Implementation details: For each of this customer's open invoices, changes
2352 the status of the first "done" (with statustext error) realtime processing
2353 event to "failed".
2354
2355 =cut
2356
2357 sub retry_realtime {
2358   my $self = shift;
2359
2360   local $SIG{HUP} = 'IGNORE';
2361   local $SIG{INT} = 'IGNORE';
2362   local $SIG{QUIT} = 'IGNORE';
2363   local $SIG{TERM} = 'IGNORE';
2364   local $SIG{TSTP} = 'IGNORE';
2365   local $SIG{PIPE} = 'IGNORE';
2366
2367   my $oldAutoCommit = $FS::UID::AutoCommit;
2368   local $FS::UID::AutoCommit = 0;
2369   my $dbh = dbh;
2370
2371   foreach my $cust_bill (
2372     grep { $_->cust_bill_event }
2373       $self->open_cust_bill
2374   ) {
2375     my @cust_bill_event =
2376       sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
2377         grep {
2378                #$_->part_bill_event->plan eq 'realtime-card'
2379                $_->part_bill_event->eventcode =~
2380                    /\$cust_bill\->(batch|realtime)_(card|ach|lec)/
2381                  && $_->status eq 'done'
2382                  && $_->statustext
2383              }
2384           $cust_bill->cust_bill_event;
2385     next unless @cust_bill_event;
2386     my $error = $cust_bill_event[0]->retry;
2387     if ( $error ) {
2388       $dbh->rollback if $oldAutoCommit;
2389       return "error scheduling invoice event for retry: $error";
2390     }
2391
2392   }
2393
2394   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2395   '';
2396
2397 }
2398
2399 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
2400
2401 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
2402 via a Business::OnlinePayment realtime gateway.  See
2403 L<http://420.am/business-onlinepayment> for supported gateways.
2404
2405 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2406
2407 Available options are: I<description>, I<invnum>, I<quiet>
2408
2409 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2410 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
2411 if set, will override the value from the customer record.
2412
2413 I<description> is a free-text field passed to the gateway.  It defaults to
2414 "Internet services".
2415
2416 If an I<invnum> is specified, this payment (if successful) is applied to the
2417 specified invoice.  If you don't specify an I<invnum> you might want to
2418 call the B<apply_payments> method.
2419
2420 I<quiet> can be set true to surpress email decline notices.
2421
2422 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
2423
2424 =cut
2425
2426 sub realtime_bop {
2427   my( $self, $method, $amount, %options ) = @_;
2428   if ( $DEBUG ) {
2429     warn "$me realtime_bop: $method $amount\n";
2430     warn "  $_ => $options{$_}\n" foreach keys %options;
2431   }
2432
2433   $options{'description'} ||= 'Internet services';
2434
2435   eval "use Business::OnlinePayment";  
2436   die $@ if $@;
2437
2438   my $payinfo = exists($options{'payinfo'})
2439                   ? $options{'payinfo'}
2440                   : $self->payinfo;
2441
2442   ###
2443   # select a gateway
2444   ###
2445
2446   my $taxclass = '';
2447   if ( $options{'invnum'} ) {
2448     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
2449     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
2450     my @taxclasses =
2451       map  { $_->part_pkg->taxclass }
2452       grep { $_ }
2453       map  { $_->cust_pkg }
2454       $cust_bill->cust_bill_pkg;
2455     unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
2456                                                            #different taxclasses
2457       $taxclass = $taxclasses[0];
2458     }
2459   }
2460
2461   #look for an agent gateway override first
2462   my $cardtype;
2463   if ( $method eq 'CC' ) {
2464     $cardtype = cardtype($payinfo);
2465   } elsif ( $method eq 'ECHECK' ) {
2466     $cardtype = 'ACH';
2467   } else {
2468     $cardtype = $method;
2469   }
2470
2471   my $override =
2472        qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2473                                            cardtype => $cardtype,
2474                                            taxclass => $taxclass,       } )
2475     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2476                                            cardtype => '',
2477                                            taxclass => $taxclass,       } )
2478     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2479                                            cardtype => $cardtype,
2480                                            taxclass => '',              } )
2481     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2482                                            cardtype => '',
2483                                            taxclass => '',              } );
2484
2485   my $payment_gateway = '';
2486   my( $processor, $login, $password, $action, @bop_options );
2487   if ( $override ) { #use a payment gateway override
2488
2489     $payment_gateway = $override->payment_gateway;
2490
2491     $processor   = $payment_gateway->gateway_module;
2492     $login       = $payment_gateway->gateway_username;
2493     $password    = $payment_gateway->gateway_password;
2494     $action      = $payment_gateway->gateway_action;
2495     @bop_options = $payment_gateway->options;
2496
2497   } else { #use the standard settings from the config
2498
2499     ( $processor, $login, $password, $action, @bop_options ) =
2500       $self->default_payment_gateway($method);
2501
2502   }
2503
2504   ###
2505   # massage data
2506   ###
2507
2508   my $address = exists($options{'address1'})
2509                     ? $options{'address1'}
2510                     : $self->address1;
2511   my $address2 = exists($options{'address2'})
2512                     ? $options{'address2'}
2513                     : $self->address2;
2514   $address .= ", ". $address2 if length($address2);
2515
2516   my $o_payname = exists($options{'payname'})
2517                     ? $options{'payname'}
2518                     : $self->payname;
2519   my($payname, $payfirst, $paylast);
2520   if ( $o_payname && $method ne 'ECHECK' ) {
2521     ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2522       or return "Illegal payname $payname";
2523     ($payfirst, $paylast) = ($1, $2);
2524   } else {
2525     $payfirst = $self->getfield('first');
2526     $paylast = $self->getfield('last');
2527     $payname =  "$payfirst $paylast";
2528   }
2529
2530   my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
2531   if ( $conf->exists('emailinvoiceauto')
2532        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
2533     push @invoicing_list, $self->all_emails;
2534   }
2535
2536   my $email = ($conf->exists('business-onlinepayment-email-override'))
2537               ? $conf->config('business-onlinepayment-email-override')
2538               : $invoicing_list[0];
2539
2540   my %content = ();
2541
2542   my $payip = exists($options{'payip'})
2543                 ? $options{'payip'}
2544                 : $self->payip;
2545   $content{customer_ip} = $payip
2546     if length($payip);
2547
2548   $content{invoice_number} = $options{'invnum'}
2549     if exists($options{'invnum'}) && length($options{'invnum'});
2550
2551   if ( $method eq 'CC' ) { 
2552
2553     $content{card_number} = $payinfo;
2554     my $paydate = exists($options{'paydate'})
2555                     ? $options{'paydate'}
2556                     : $self->paydate;
2557     $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2558     $content{expiration} = "$2/$1";
2559
2560     my $paycvv = exists($options{'paycvv'})
2561                    ? $options{'paycvv'}
2562                    : $self->paycvv;
2563     $content{cvv2} = $self->paycvv
2564       if length($paycvv);
2565
2566     my $paystart_month = exists($options{'paystart_month'})
2567                            ? $options{'paystart_month'}
2568                            : $self->paystart_month;
2569
2570     my $paystart_year  = exists($options{'paystart_year'})
2571                            ? $options{'paystart_year'}
2572                            : $self->paystart_year;
2573
2574     $content{card_start} = "$paystart_month/$paystart_year"
2575       if $paystart_month && $paystart_year;
2576
2577     my $payissue       = exists($options{'payissue'})
2578                            ? $options{'payissue'}
2579                            : $self->payissue;
2580     $content{issue_number} = $payissue if $payissue;
2581
2582     $content{recurring_billing} = 'YES'
2583       if qsearch('cust_pay', { 'custnum' => $self->custnum,
2584                                'payby'   => 'CARD',
2585                                'payinfo' => $payinfo,
2586                              } )
2587       || qsearch('cust_pay', { 'custnum' => $self->custnum,
2588                                'payby'   => 'CARD',
2589                                'paymask' => $self->mask_payinfo('CARD', $payinfo),
2590                              } );
2591
2592
2593   } elsif ( $method eq 'ECHECK' ) {
2594     ( $content{account_number}, $content{routing_code} ) =
2595       split('@', $payinfo);
2596     $content{bank_name} = $o_payname;
2597     $content{account_type} = 'CHECKING';
2598     $content{account_name} = $payname;
2599     $content{customer_org} = $self->company ? 'B' : 'I';
2600     $content{customer_ssn} = exists($options{'ss'})
2601                                ? $options{'ss'}
2602                                : $self->ss;
2603   } elsif ( $method eq 'LEC' ) {
2604     $content{phone} = $payinfo;
2605   }
2606
2607   ###
2608   # run transaction(s)
2609   ###
2610
2611   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
2612
2613   my $transaction = new Business::OnlinePayment( $processor, @bop_options );
2614   $transaction->content(
2615     'type'           => $method,
2616     'login'          => $login,
2617     'password'       => $password,
2618     'action'         => $action1,
2619     'description'    => $options{'description'},
2620     'amount'         => $amount,
2621     #'invoice_number' => $options{'invnum'},
2622     'customer_id'    => $self->custnum,
2623     'last_name'      => $paylast,
2624     'first_name'     => $payfirst,
2625     'name'           => $payname,
2626     'address'        => $address,
2627     'city'           => ( exists($options{'city'})
2628                             ? $options{'city'}
2629                             : $self->city          ),
2630     'state'          => ( exists($options{'state'})
2631                             ? $options{'state'}
2632                             : $self->state          ),
2633     'zip'            => ( exists($options{'zip'})
2634                             ? $options{'zip'}
2635                             : $self->zip          ),
2636     'country'        => ( exists($options{'country'})
2637                             ? $options{'country'}
2638                             : $self->country          ),
2639     'referer'        => 'http://cleanwhisker.420.am/',
2640     'email'          => $email,
2641     'phone'          => $self->daytime || $self->night,
2642     %content, #after
2643   );
2644   $transaction->submit();
2645
2646   if ( $transaction->is_success() && $action2 ) {
2647     my $auth = $transaction->authorization;
2648     my $ordernum = $transaction->can('order_number')
2649                    ? $transaction->order_number
2650                    : '';
2651
2652     my $capture =
2653       new Business::OnlinePayment( $processor, @bop_options );
2654
2655     my %capture = (
2656       %content,
2657       type           => $method,
2658       action         => $action2,
2659       login          => $login,
2660       password       => $password,
2661       order_number   => $ordernum,
2662       amount         => $amount,
2663       authorization  => $auth,
2664       description    => $options{'description'},
2665     );
2666
2667     foreach my $field (qw( authorization_source_code returned_ACI
2668                            transaction_identifier validation_code           
2669                            transaction_sequence_num local_transaction_date    
2670                            local_transaction_time AVS_result_code          )) {
2671       $capture{$field} = $transaction->$field() if $transaction->can($field);
2672     }
2673
2674     $capture->content( %capture );
2675
2676     $capture->submit();
2677
2678     unless ( $capture->is_success ) {
2679       my $e = "Authorization successful but capture failed, custnum #".
2680               $self->custnum. ': '.  $capture->result_code.
2681               ": ". $capture->error_message;
2682       warn $e;
2683       return $e;
2684     }
2685
2686   }
2687
2688   ###
2689   # remove paycvv after initial transaction
2690   ###
2691
2692   #false laziness w/misc/process/payment.cgi - check both to make sure working
2693   # correctly
2694   if ( defined $self->dbdef_table->column('paycvv')
2695        && length($self->paycvv)
2696        && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
2697   ) {
2698     my $error = $self->remove_cvv;
2699     if ( $error ) {
2700       warn "WARNING: error removing cvv: $error\n";
2701     }
2702   }
2703
2704   ###
2705   # result handling
2706   ###
2707
2708   if ( $transaction->is_success() ) {
2709
2710     my %method2payby = (
2711       'CC'     => 'CARD',
2712       'ECHECK' => 'CHEK',
2713       'LEC'    => 'LECB',
2714     );
2715
2716     my $paybatch = '';
2717     if ( $payment_gateway ) { # agent override
2718       $paybatch = $payment_gateway->gatewaynum. '-';
2719     }
2720
2721     $paybatch .= "$processor:". $transaction->authorization;
2722
2723     $paybatch .= ':'. $transaction->order_number
2724       if $transaction->can('order_number')
2725       && length($transaction->order_number);
2726
2727     my $cust_pay = new FS::cust_pay ( {
2728        'custnum'  => $self->custnum,
2729        'invnum'   => $options{'invnum'},
2730        'paid'     => $amount,
2731        '_date'     => '',
2732        'payby'    => $method2payby{$method},
2733        'payinfo'  => $payinfo,
2734        'paybatch' => $paybatch,
2735     } );
2736     my $error = $cust_pay->insert;
2737     if ( $error ) {
2738       $cust_pay->invnum(''); #try again with no specific invnum
2739       my $error2 = $cust_pay->insert;
2740       if ( $error2 ) {
2741         # gah, even with transactions.
2742         my $e = 'WARNING: Card/ACH debited but database not updated - '.
2743                 "error inserting payment ($processor): $error2".
2744                 " (previously tried insert with invnum #$options{'invnum'}" .
2745                 ": $error )";
2746         warn $e;
2747         return $e;
2748       }
2749     }
2750     return ''; #no error
2751
2752   } else {
2753
2754     my $perror = "$processor error: ". $transaction->error_message;
2755
2756     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
2757          && $conf->exists('emaildecline')
2758          && grep { $_ ne 'POST' } $self->invoicing_list
2759          && ! grep { $transaction->error_message =~ /$_/ }
2760                    $conf->config('emaildecline-exclude')
2761     ) {
2762       my @templ = $conf->config('declinetemplate');
2763       my $template = new Text::Template (
2764         TYPE   => 'ARRAY',
2765         SOURCE => [ map "$_\n", @templ ],
2766       ) or return "($perror) can't create template: $Text::Template::ERROR";
2767       $template->compile()
2768         or return "($perror) can't compile template: $Text::Template::ERROR";
2769
2770       my $templ_hash = { error => $transaction->error_message };
2771
2772       my $error = send_email(
2773         'from'    => $conf->config('invoice_from'),
2774         'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
2775         'subject' => 'Your payment could not be processed',
2776         'body'    => [ $template->fill_in(HASH => $templ_hash) ],
2777       );
2778
2779       $perror .= " (also received error sending decline notification: $error)"
2780         if $error;
2781
2782     }
2783   
2784     return $perror;
2785   }
2786
2787 }
2788
2789 =item default_payment_gateway
2790
2791 =cut
2792
2793 sub default_payment_gateway {
2794   my( $self, $method ) = @_;
2795
2796   die "Real-time processing not enabled\n"
2797     unless $conf->exists('business-onlinepayment');
2798
2799   #load up config
2800   my $bop_config = 'business-onlinepayment';
2801   $bop_config .= '-ach'
2802     if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
2803   my ( $processor, $login, $password, $action, @bop_options ) =
2804     $conf->config($bop_config);
2805   $action ||= 'normal authorization';
2806   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
2807   die "No real-time processor is enabled - ".
2808       "did you set the business-onlinepayment configuration value?\n"
2809     unless $processor;
2810
2811   ( $processor, $login, $password, $action, @bop_options )
2812 }
2813
2814 =item remove_cvv
2815
2816 Removes the I<paycvv> field from the database directly.
2817
2818 If there is an error, returns the error, otherwise returns false.
2819
2820 =cut
2821
2822 sub remove_cvv {
2823   my $self = shift;
2824   my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2825     or return dbh->errstr;
2826   $sth->execute($self->custnum)
2827     or return $sth->errstr;
2828   $self->paycvv('');
2829   '';
2830 }
2831
2832 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
2833
2834 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
2835 via a Business::OnlinePayment realtime gateway.  See
2836 L<http://420.am/business-onlinepayment> for supported gateways.
2837
2838 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2839
2840 Available options are: I<amount>, I<reason>, I<paynum>
2841
2842 Most gateways require a reference to an original payment transaction to refund,
2843 so you probably need to specify a I<paynum>.
2844
2845 I<amount> defaults to the original amount of the payment if not specified.
2846
2847 I<reason> specifies a reason for the refund.
2848
2849 Implementation note: If I<amount> is unspecified or equal to the amount of the
2850 orignal payment, first an attempt is made to "void" the transaction via
2851 the gateway (to cancel a not-yet settled transaction) and then if that fails,
2852 the normal attempt is made to "refund" ("credit") the transaction via the
2853 gateway is attempted.
2854
2855 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2856 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
2857 #if set, will override the value from the customer record.
2858
2859 #If an I<invnum> is specified, this payment (if successful) is applied to the
2860 #specified invoice.  If you don't specify an I<invnum> you might want to
2861 #call the B<apply_payments> method.
2862
2863 =cut
2864
2865 #some false laziness w/realtime_bop, not enough to make it worth merging
2866 #but some useful small subs should be pulled out
2867 sub realtime_refund_bop {
2868   my( $self, $method, %options ) = @_;
2869   if ( $DEBUG ) {
2870     warn "$me realtime_refund_bop: $method refund\n";
2871     warn "  $_ => $options{$_}\n" foreach keys %options;
2872   }
2873
2874   eval "use Business::OnlinePayment";  
2875   die $@ if $@;
2876
2877   ###
2878   # look up the original payment and optionally a gateway for that payment
2879   ###
2880
2881   my $cust_pay = '';
2882   my $amount = $options{'amount'};
2883
2884   my( $processor, $login, $password, @bop_options ) ;
2885   my( $auth, $order_number ) = ( '', '', '' );
2886
2887   if ( $options{'paynum'} ) {
2888
2889     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
2890     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
2891       or return "Unknown paynum $options{'paynum'}";
2892     $amount ||= $cust_pay->paid;
2893
2894     $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-]*)(:([\w\-]+))?$/
2895       or return "Can't parse paybatch for paynum $options{'paynum'}: ".
2896                 $cust_pay->paybatch;
2897     my $gatewaynum = '';
2898     ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
2899
2900     if ( $gatewaynum ) { #gateway for the payment to be refunded
2901
2902       my $payment_gateway =
2903         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
2904       die "payment gateway $gatewaynum not found"
2905         unless $payment_gateway;
2906
2907       $processor   = $payment_gateway->gateway_module;
2908       $login       = $payment_gateway->gateway_username;
2909       $password    = $payment_gateway->gateway_password;
2910       @bop_options = $payment_gateway->options;
2911
2912     } else { #try the default gateway
2913
2914       my( $conf_processor, $unused_action );
2915       ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
2916         $self->default_payment_gateway($method);
2917
2918       return "processor of payment $options{'paynum'} $processor does not".
2919              " match default processor $conf_processor"
2920         unless $processor eq $conf_processor;
2921
2922     }
2923
2924
2925   } else { # didn't specify a paynum, so look for agent gateway overrides
2926            # like a normal transaction 
2927
2928     my $cardtype;
2929     if ( $method eq 'CC' ) {
2930       $cardtype = cardtype($self->payinfo);
2931     } elsif ( $method eq 'ECHECK' ) {
2932       $cardtype = 'ACH';
2933     } else {
2934       $cardtype = $method;
2935     }
2936     my $override =
2937            qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2938                                                cardtype => $cardtype,
2939                                                taxclass => '',              } )
2940         || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2941                                                cardtype => '',
2942                                                taxclass => '',              } );
2943
2944     if ( $override ) { #use a payment gateway override
2945  
2946       my $payment_gateway = $override->payment_gateway;
2947
2948       $processor   = $payment_gateway->gateway_module;
2949       $login       = $payment_gateway->gateway_username;
2950       $password    = $payment_gateway->gateway_password;
2951       #$action      = $payment_gateway->gateway_action;
2952       @bop_options = $payment_gateway->options;
2953
2954     } else { #use the standard settings from the config
2955
2956       my $unused_action;
2957       ( $processor, $login, $password, $unused_action, @bop_options ) =
2958         $self->default_payment_gateway($method);
2959
2960     }
2961
2962   }
2963   return "neither amount nor paynum specified" unless $amount;
2964
2965   my %content = (
2966     'type'           => $method,
2967     'login'          => $login,
2968     'password'       => $password,
2969     'order_number'   => $order_number,
2970     'amount'         => $amount,
2971     'referer'        => 'http://cleanwhisker.420.am/',
2972   );
2973   $content{authorization} = $auth
2974     if length($auth); #echeck/ACH transactions have an order # but no auth
2975                       #(at least with authorize.net)
2976
2977   #first try void if applicable
2978   if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
2979     warn "  attempting void\n" if $DEBUG > 1;
2980     my $void = new Business::OnlinePayment( $processor, @bop_options );
2981     $void->content( 'action' => 'void', %content );
2982     $void->submit();
2983     if ( $void->is_success ) {
2984       my $error = $cust_pay->void($options{'reason'});
2985       if ( $error ) {
2986         # gah, even with transactions.
2987         my $e = 'WARNING: Card/ACH voided but database not updated - '.
2988                 "error voiding payment: $error";
2989         warn $e;
2990         return $e;
2991       }
2992       warn "  void successful\n" if $DEBUG > 1;
2993       return '';
2994     }
2995   }
2996
2997   warn "  void unsuccessful, trying refund\n"
2998     if $DEBUG > 1;
2999
3000   #massage data
3001   my $address = $self->address1;
3002   $address .= ", ". $self->address2 if $self->address2;
3003
3004   my($payname, $payfirst, $paylast);
3005   if ( $self->payname && $method ne 'ECHECK' ) {
3006     $payname = $self->payname;
3007     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
3008       or return "Illegal payname $payname";
3009     ($payfirst, $paylast) = ($1, $2);
3010   } else {
3011     $payfirst = $self->getfield('first');
3012     $paylast = $self->getfield('last');
3013     $payname =  "$payfirst $paylast";
3014   }
3015
3016   my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
3017   if ( $conf->exists('emailinvoiceauto')
3018        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
3019     push @invoicing_list, $self->all_emails;
3020   }
3021
3022   my $email = ($conf->exists('business-onlinepayment-email-override'))
3023               ? $conf->config('business-onlinepayment-email-override')
3024               : $invoicing_list[0];
3025
3026   my $payip = exists($options{'payip'})
3027                 ? $options{'payip'}
3028                 : $self->payip;
3029   $content{customer_ip} = $payip
3030     if length($payip);
3031
3032   my $payinfo = '';
3033   if ( $method eq 'CC' ) {
3034
3035     if ( $cust_pay ) {
3036       $content{card_number} = $payinfo = $cust_pay->payinfo;
3037       #$self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
3038       #$content{expiration} = "$2/$1";
3039     } else {
3040       $content{card_number} = $payinfo = $self->payinfo;
3041       $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
3042       $content{expiration} = "$2/$1";
3043     }
3044
3045   } elsif ( $method eq 'ECHECK' ) {
3046     ( $content{account_number}, $content{routing_code} ) =
3047       split('@', $payinfo = $self->payinfo);
3048     $content{bank_name} = $self->payname;
3049     $content{account_type} = 'CHECKING';
3050     $content{account_name} = $payname;
3051     $content{customer_org} = $self->company ? 'B' : 'I';
3052     $content{customer_ssn} = $self->ss;
3053   } elsif ( $method eq 'LEC' ) {
3054     $content{phone} = $payinfo = $self->payinfo;
3055   }
3056
3057   #then try refund
3058   my $refund = new Business::OnlinePayment( $processor, @bop_options );
3059   my %sub_content = $refund->content(
3060     'action'         => 'credit',
3061     'customer_id'    => $self->custnum,
3062     'last_name'      => $paylast,
3063     'first_name'     => $payfirst,
3064     'name'           => $payname,
3065     'address'        => $address,
3066     'city'           => $self->city,
3067     'state'          => $self->state,
3068     'zip'            => $self->zip,
3069     'country'        => $self->country,
3070     'email'          => $email,
3071     'phone'          => $self->daytime || $self->night,
3072     %content, #after
3073   );
3074   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
3075     if $DEBUG > 1;
3076   $refund->submit();
3077
3078   return "$processor error: ". $refund->error_message
3079     unless $refund->is_success();
3080
3081   my %method2payby = (
3082     'CC'     => 'CARD',
3083     'ECHECK' => 'CHEK',
3084     'LEC'    => 'LECB',
3085   );
3086
3087   my $paybatch = "$processor:". $refund->authorization;
3088   $paybatch .= ':'. $refund->order_number
3089     if $refund->can('order_number') && $refund->order_number;
3090
3091   while ( $cust_pay && $cust_pay->unappled < $amount ) {
3092     my @cust_bill_pay = $cust_pay->cust_bill_pay;
3093     last unless @cust_bill_pay;
3094     my $cust_bill_pay = pop @cust_bill_pay;
3095     my $error = $cust_bill_pay->delete;
3096     last if $error;
3097   }
3098
3099   my $cust_refund = new FS::cust_refund ( {
3100     'custnum'  => $self->custnum,
3101     'paynum'   => $options{'paynum'},
3102     'refund'   => $amount,
3103     '_date'    => '',
3104     'payby'    => $method2payby{$method},
3105     'payinfo'  => $payinfo,
3106     'paybatch' => $paybatch,
3107     'reason'   => $options{'reason'} || 'card or ACH refund',
3108   } );
3109   my $error = $cust_refund->insert;
3110   if ( $error ) {
3111     $cust_refund->paynum(''); #try again with no specific paynum
3112     my $error2 = $cust_refund->insert;
3113     if ( $error2 ) {
3114       # gah, even with transactions.
3115       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
3116               "error inserting refund ($processor): $error2".
3117               " (previously tried insert with paynum #$options{'paynum'}" .
3118               ": $error )";
3119       warn $e;
3120       return $e;
3121     }
3122   }
3123
3124   ''; #no error
3125
3126 }
3127
3128 =item total_owed
3129
3130 Returns the total owed for this customer on all invoices
3131 (see L<FS::cust_bill/owed>).
3132
3133 =cut
3134
3135 sub total_owed {
3136   my $self = shift;
3137   $self->total_owed_date(2145859200); #12/31/2037
3138 }
3139
3140 =item total_owed_date TIME
3141
3142 Returns the total owed for this customer on all invoices with date earlier than
3143 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
3144 see L<Time::Local> and L<Date::Parse> for conversion functions.
3145
3146 =cut
3147
3148 sub total_owed_date {
3149   my $self = shift;
3150   my $time = shift;
3151   my $total_bill = 0;
3152   foreach my $cust_bill (
3153     grep { $_->_date <= $time }
3154       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
3155   ) {
3156     $total_bill += $cust_bill->owed;
3157   }
3158   sprintf( "%.2f", $total_bill );
3159 }
3160
3161 =item apply_credits OPTION => VALUE ...
3162
3163 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
3164 to outstanding invoice balances in chronological order (or reverse
3165 chronological order if the I<order> option is set to B<newest>) and returns the
3166 value of any remaining unapplied credits available for refund (see
3167 L<FS::cust_refund>).
3168
3169 =cut
3170
3171 sub apply_credits {
3172   my $self = shift;
3173   my %opt = @_;
3174
3175   return 0 unless $self->total_credited;
3176
3177   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
3178       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
3179
3180   my @invoices = $self->open_cust_bill;
3181   @invoices = sort { $b->_date <=> $a->_date } @invoices
3182     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
3183
3184   my $credit;
3185   foreach my $cust_bill ( @invoices ) {
3186     my $amount;
3187
3188     if ( !defined($credit) || $credit->credited == 0) {
3189       $credit = pop @credits or last;
3190     }
3191
3192     if ($cust_bill->owed >= $credit->credited) {
3193       $amount=$credit->credited;
3194     }else{
3195       $amount=$cust_bill->owed;
3196     }
3197     
3198     my $cust_credit_bill = new FS::cust_credit_bill ( {
3199       'crednum' => $credit->crednum,
3200       'invnum'  => $cust_bill->invnum,
3201       'amount'  => $amount,
3202     } );
3203     my $error = $cust_credit_bill->insert;
3204     die $error if $error;
3205     
3206     redo if ($cust_bill->owed > 0);
3207
3208   }
3209
3210   return $self->total_credited;
3211 }
3212
3213 =item apply_payments
3214
3215 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
3216 to outstanding invoice balances in chronological order.
3217
3218  #and returns the value of any remaining unapplied payments.
3219
3220 =cut
3221
3222 sub apply_payments {
3223   my $self = shift;
3224
3225   #return 0 unless
3226
3227   my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
3228       qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
3229
3230   my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
3231       qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
3232
3233   my $payment;
3234
3235   foreach my $cust_bill ( @invoices ) {
3236     my $amount;
3237
3238     if ( !defined($payment) || $payment->unapplied == 0 ) {
3239       $payment = pop @payments or last;
3240     }
3241
3242     if ( $cust_bill->owed >= $payment->unapplied ) {
3243       $amount = $payment->unapplied;
3244     } else {
3245       $amount = $cust_bill->owed;
3246     }
3247
3248     my $cust_bill_pay = new FS::cust_bill_pay ( {
3249       'paynum' => $payment->paynum,
3250       'invnum' => $cust_bill->invnum,
3251       'amount' => $amount,
3252     } );
3253     my $error = $cust_bill_pay->insert;
3254     die $error if $error;
3255
3256     redo if ( $cust_bill->owed > 0);
3257
3258   }
3259
3260   return $self->total_unapplied_payments;
3261 }
3262
3263 =item total_credited
3264
3265 Returns the total outstanding credit (see L<FS::cust_credit>) for this
3266 customer.  See L<FS::cust_credit/credited>.
3267
3268 =cut
3269
3270 sub total_credited {
3271   my $self = shift;
3272   my $total_credit = 0;
3273   foreach my $cust_credit ( qsearch('cust_credit', {
3274     'custnum' => $self->custnum,
3275   } ) ) {
3276     $total_credit += $cust_credit->credited;
3277   }
3278   sprintf( "%.2f", $total_credit );
3279 }
3280
3281 =item total_unapplied_payments
3282
3283 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
3284 See L<FS::cust_pay/unapplied>.
3285
3286 =cut
3287
3288 sub total_unapplied_payments {
3289   my $self = shift;
3290   my $total_unapplied = 0;
3291   foreach my $cust_pay ( qsearch('cust_pay', {
3292     'custnum' => $self->custnum,
3293   } ) ) {
3294     $total_unapplied += $cust_pay->unapplied;
3295   }
3296   sprintf( "%.2f", $total_unapplied );
3297 }
3298
3299 =item balance
3300
3301 Returns the balance for this customer (total_owed minus total_credited
3302 minus total_unapplied_payments).
3303
3304 =cut
3305
3306 sub balance {
3307   my $self = shift;
3308   sprintf( "%.2f",
3309     $self->total_owed - $self->total_credited - $self->total_unapplied_payments
3310   );
3311 }
3312
3313 =item balance_date TIME
3314
3315 Returns the balance for this customer, only considering invoices with date
3316 earlier than TIME (total_owed_date minus total_credited minus
3317 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
3318 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
3319 functions.
3320
3321 =cut
3322
3323 sub balance_date {
3324   my $self = shift;
3325   my $time = shift;
3326   sprintf( "%.2f",
3327     $self->total_owed_date($time)
3328       - $self->total_credited
3329       - $self->total_unapplied_payments
3330   );
3331 }
3332
3333 =item in_transit_payments
3334
3335 Returns the total of requests for payments for this customer pending in 
3336 batches in transit to the bank.  See L<FS::pay_batch> and L<FS::cust_pay_batch>
3337
3338 =cut
3339
3340 sub in_transit_payments {
3341   my $self = shift;
3342   my $in_transit_payments = 0;
3343   foreach my $pay_batch ( qsearch('pay_batch', {
3344     'status' => 'I',
3345   } ) ) {
3346     foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
3347       'batchnum' => $pay_batch->batchnum,
3348       'custnum' => $self->custnum,
3349     } ) ) {
3350       $in_transit_payments += $cust_pay_batch->amount;
3351     }
3352   }
3353   sprintf( "%.2f", $in_transit_payments );
3354 }
3355
3356 =item paydate_monthyear
3357
3358 Returns a two-element list consisting of the month and year of this customer's
3359 paydate (credit card expiration date for CARD customers)
3360
3361 =cut
3362
3363 sub paydate_monthyear {
3364   my $self = shift;
3365   if ( $self->paydate  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
3366     ( $2, $1 );
3367   } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
3368     ( $1, $3 );
3369   } else {
3370     ('', '');
3371   }
3372 }
3373
3374 =item invoicing_list [ ARRAYREF ]
3375
3376 If an arguement is given, sets these email addresses as invoice recipients
3377 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
3378 (except as warnings), so use check_invoicing_list first.
3379
3380 Returns a list of email addresses (with svcnum entries expanded).
3381
3382 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
3383 check it without disturbing anything by passing nothing.
3384
3385 This interface may change in the future.
3386
3387 =cut
3388
3389 sub invoicing_list {
3390   my( $self, $arrayref ) = @_;
3391
3392   if ( $arrayref ) {
3393     my @cust_main_invoice;
3394     if ( $self->custnum ) {
3395       @cust_main_invoice = 
3396         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3397     } else {
3398       @cust_main_invoice = ();
3399     }
3400     foreach my $cust_main_invoice ( @cust_main_invoice ) {
3401       #warn $cust_main_invoice->destnum;
3402       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
3403         #warn $cust_main_invoice->destnum;
3404         my $error = $cust_main_invoice->delete;
3405         warn $error if $error;
3406       }
3407     }
3408     if ( $self->custnum ) {
3409       @cust_main_invoice = 
3410         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3411     } else {
3412       @cust_main_invoice = ();
3413     }
3414     my %seen = map { $_->address => 1 } @cust_main_invoice;
3415     foreach my $address ( @{$arrayref} ) {
3416       next if exists $seen{$address} && $seen{$address};
3417       $seen{$address} = 1;
3418       my $cust_main_invoice = new FS::cust_main_invoice ( {
3419         'custnum' => $self->custnum,
3420         'dest'    => $address,
3421       } );
3422       my $error = $cust_main_invoice->insert;
3423       warn $error if $error;
3424     }
3425   }
3426   
3427   if ( $self->custnum ) {
3428     map { $_->address }
3429       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3430   } else {
3431     ();
3432   }
3433
3434 }
3435
3436 =item check_invoicing_list ARRAYREF
3437
3438 Checks these arguements as valid input for the invoicing_list method.  If there
3439 is an error, returns the error, otherwise returns false.
3440
3441 =cut
3442
3443 sub check_invoicing_list {
3444   my( $self, $arrayref ) = @_;
3445   foreach my $address ( @{$arrayref} ) {
3446
3447     if ($address eq 'FAX' and $self->getfield('fax') eq '') {
3448       return 'Can\'t add FAX invoice destination with a blank FAX number.';
3449     }
3450
3451     my $cust_main_invoice = new FS::cust_main_invoice ( {
3452       'custnum' => $self->custnum,
3453       'dest'    => $address,
3454     } );
3455     my $error = $self->custnum
3456                 ? $cust_main_invoice->check
3457                 : $cust_main_invoice->checkdest
3458     ;
3459     return $error if $error;
3460   }
3461   '';
3462 }
3463
3464 =item set_default_invoicing_list
3465
3466 Sets the invoicing list to all accounts associated with this customer,
3467 overwriting any previous invoicing list.
3468
3469 =cut
3470
3471 sub set_default_invoicing_list {
3472   my $self = shift;
3473   $self->invoicing_list($self->all_emails);
3474 }
3475
3476 =item all_emails
3477
3478 Returns the email addresses of all accounts provisioned for this customer.
3479
3480 =cut
3481
3482 sub all_emails {
3483   my $self = shift;
3484   my %list;
3485   foreach my $cust_pkg ( $self->all_pkgs ) {
3486     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
3487     my @svc_acct =
3488       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3489         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3490           @cust_svc;
3491     $list{$_}=1 foreach map { $_->email } @svc_acct;
3492   }
3493   keys %list;
3494 }
3495
3496 =item invoicing_list_addpost
3497
3498 Adds postal invoicing to this customer.  If this customer is already configured
3499 to receive postal invoices, does nothing.
3500
3501 =cut
3502
3503 sub invoicing_list_addpost {
3504   my $self = shift;
3505   return if grep { $_ eq 'POST' } $self->invoicing_list;
3506   my @invoicing_list = $self->invoicing_list;
3507   push @invoicing_list, 'POST';
3508   $self->invoicing_list(\@invoicing_list);
3509 }
3510
3511 =item invoicing_list_emailonly
3512
3513 Returns the list of email invoice recipients (invoicing_list without non-email
3514 destinations such as POST and FAX).
3515
3516 =cut
3517
3518 sub invoicing_list_emailonly {
3519   my $self = shift;
3520   grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
3521 }
3522
3523 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3524
3525 Returns an array of customers referred by this customer (referral_custnum set
3526 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
3527 customers referred by customers referred by this customer and so on, inclusive.
3528 The default behavior is DEPTH 1 (no recursion).
3529
3530 =cut
3531
3532 sub referral_cust_main {
3533   my $self = shift;
3534   my $depth = @_ ? shift : 1;
3535   my $exclude = @_ ? shift : {};
3536
3537   my @cust_main =
3538     map { $exclude->{$_->custnum}++; $_; }
3539       grep { ! $exclude->{ $_->custnum } }
3540         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3541
3542   if ( $depth > 1 ) {
3543     push @cust_main,
3544       map { $_->referral_cust_main($depth-1, $exclude) }
3545         @cust_main;
3546   }
3547
3548   @cust_main;
3549 }
3550
3551 =item referral_cust_main_ncancelled
3552
3553 Same as referral_cust_main, except only returns customers with uncancelled
3554 packages.
3555
3556 =cut
3557
3558 sub referral_cust_main_ncancelled {
3559   my $self = shift;
3560   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3561 }
3562
3563 =item referral_cust_pkg [ DEPTH ]
3564
3565 Like referral_cust_main, except returns a flat list of all unsuspended (and
3566 uncancelled) packages for each customer.  The number of items in this list may
3567 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3568
3569 =cut
3570
3571 sub referral_cust_pkg {
3572   my $self = shift;
3573   my $depth = @_ ? shift : 1;
3574
3575   map { $_->unsuspended_pkgs }
3576     grep { $_->unsuspended_pkgs }
3577       $self->referral_cust_main($depth);
3578 }
3579
3580 =item referring_cust_main
3581
3582 Returns the single cust_main record for the customer who referred this customer
3583 (referral_custnum), or false.
3584
3585 =cut
3586
3587 sub referring_cust_main {
3588   my $self = shift;
3589   return '' unless $self->referral_custnum;
3590   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3591 }
3592
3593 =item credit AMOUNT, REASON
3594
3595 Applies a credit to this customer.  If there is an error, returns the error,
3596 otherwise returns false.
3597
3598 =cut
3599
3600 sub credit {
3601   my( $self, $amount, $reason ) = @_;
3602   my $cust_credit = new FS::cust_credit {
3603     'custnum' => $self->custnum,
3604     'amount'  => $amount,
3605     'reason'  => $reason,
3606   };
3607   $cust_credit->insert;
3608 }
3609
3610 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3611
3612 Creates a one-time charge for this customer.  If there is an error, returns
3613 the error, otherwise returns false.
3614
3615 =cut
3616
3617 sub charge {
3618   my $self = shift;
3619   my ( $amount, $pkg, $comment, $taxclass, $additional );
3620   if ( ref( $_[0] ) ) {
3621     $amount     = $_[0]->{amount};
3622     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
3623     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
3624                                            : '$'. sprintf("%.2f",$amount);
3625     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
3626     $additional = $_[0]->{additional};
3627   }else{
3628     $amount     = shift;
3629     $pkg        = @_ ? shift : 'One-time charge';
3630     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
3631     $taxclass   = @_ ? shift : '';
3632     $additional = [];
3633   }
3634
3635   local $SIG{HUP} = 'IGNORE';
3636   local $SIG{INT} = 'IGNORE';
3637   local $SIG{QUIT} = 'IGNORE';
3638   local $SIG{TERM} = 'IGNORE';
3639   local $SIG{TSTP} = 'IGNORE';
3640   local $SIG{PIPE} = 'IGNORE';
3641
3642   my $oldAutoCommit = $FS::UID::AutoCommit;
3643   local $FS::UID::AutoCommit = 0;
3644   my $dbh = dbh;
3645
3646   my $part_pkg = new FS::part_pkg ( {
3647     'pkg'      => $pkg,
3648     'comment'  => $comment,
3649     'plan'     => 'flat',
3650     'freq'     => 0,
3651     'disabled' => 'Y',
3652     'taxclass' => $taxclass,
3653   } );
3654
3655   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
3656                         ( 0 .. @$additional - 1 )
3657                   ),
3658                   'additional_count' => scalar(@$additional),
3659                   'setup_fee' => $amount,
3660                 );
3661
3662   my $error = $part_pkg->insert( options => \%options );
3663   if ( $error ) {
3664     $dbh->rollback if $oldAutoCommit;
3665     return $error;
3666   }
3667
3668   my $pkgpart = $part_pkg->pkgpart;
3669   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3670   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3671     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3672     $error = $type_pkgs->insert;
3673     if ( $error ) {
3674       $dbh->rollback if $oldAutoCommit;
3675       return $error;
3676     }
3677   }
3678
3679   my $cust_pkg = new FS::cust_pkg ( {
3680     'custnum' => $self->custnum,
3681     'pkgpart' => $pkgpart,
3682   } );
3683
3684   $error = $cust_pkg->insert;
3685   if ( $error ) {
3686     $dbh->rollback if $oldAutoCommit;
3687     return $error;
3688   }
3689
3690   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3691   '';
3692
3693 }
3694
3695 =item cust_bill
3696
3697 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3698
3699 =cut
3700
3701 sub cust_bill {
3702   my $self = shift;
3703   sort { $a->_date <=> $b->_date }
3704     qsearch('cust_bill', { 'custnum' => $self->custnum, } )
3705 }
3706
3707 =item open_cust_bill
3708
3709 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3710 customer.
3711
3712 =cut
3713
3714 sub open_cust_bill {
3715   my $self = shift;
3716   grep { $_->owed > 0 } $self->cust_bill;
3717 }
3718
3719 =item cust_credit
3720
3721 Returns all the credits (see L<FS::cust_credit>) for this customer.
3722
3723 =cut
3724
3725 sub cust_credit {
3726   my $self = shift;
3727   sort { $a->_date <=> $b->_date }
3728     qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3729 }
3730
3731 =item cust_pay
3732
3733 Returns all the payments (see L<FS::cust_pay>) for this customer.
3734
3735 =cut
3736
3737 sub cust_pay {
3738   my $self = shift;
3739   sort { $a->_date <=> $b->_date }
3740     qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
3741 }
3742
3743 =item cust_pay_void
3744
3745 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
3746
3747 =cut
3748
3749 sub cust_pay_void {
3750   my $self = shift;
3751   sort { $a->_date <=> $b->_date }
3752     qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
3753 }
3754
3755
3756 =item cust_refund
3757
3758 Returns all the refunds (see L<FS::cust_refund>) for this customer.
3759
3760 =cut
3761
3762 sub cust_refund {
3763   my $self = shift;
3764   sort { $a->_date <=> $b->_date }
3765     qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
3766 }
3767
3768 =item select_for_update
3769
3770 Selects this record with the SQL "FOR UPDATE" command.  This can be useful as
3771 a mutex.
3772
3773 =cut
3774
3775 sub select_for_update {
3776   my $self = shift;
3777   qsearch('cust_main', { 'custnum' => $self->custnum }, '*', 'FOR UPDATE' );
3778 }
3779
3780 =item name
3781
3782 Returns a name string for this customer, either "Company (Last, First)" or
3783 "Last, First".
3784
3785 =cut
3786
3787 sub name {
3788   my $self = shift;
3789   my $name = $self->contact;
3790   $name = $self->company. " ($name)" if $self->company;
3791   $name;
3792 }
3793
3794 =item ship_name
3795
3796 Returns a name string for this (service/shipping) contact, either
3797 "Company (Last, First)" or "Last, First".
3798
3799 =cut
3800
3801 sub ship_name {
3802   my $self = shift;
3803   if ( $self->get('ship_last') ) { 
3804     my $name = $self->ship_contact;
3805     $name = $self->ship_company. " ($name)" if $self->ship_company;
3806     $name;
3807   } else {
3808     $self->name;
3809   }
3810 }
3811
3812 =item contact
3813
3814 Returns this customer's full (billing) contact name only, "Last, First"
3815
3816 =cut
3817
3818 sub contact {
3819   my $self = shift;
3820   $self->get('last'). ', '. $self->first;
3821 }
3822
3823 =item ship_contact
3824
3825 Returns this customer's full (shipping) contact name only, "Last, First"
3826
3827 =cut
3828
3829 sub ship_contact {
3830   my $self = shift;
3831   $self->get('ship_last')
3832     ? $self->get('ship_last'). ', '. $self->ship_first
3833     : $self->contact;
3834 }
3835
3836 =item country_full
3837
3838 Returns this customer's full country name
3839
3840 =cut
3841
3842 sub country_full {
3843   my $self = shift;
3844   code2country($self->country);
3845 }
3846
3847 =item status
3848
3849 Returns a status string for this customer, currently:
3850
3851 =over 4
3852
3853 =item prospect - No packages have ever been ordered
3854
3855 =item active - One or more recurring packages is active
3856
3857 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
3858
3859 =item suspended - All non-cancelled recurring packages are suspended
3860
3861 =item cancelled - All recurring packages are cancelled
3862
3863 =back
3864
3865 =cut
3866
3867 sub status {
3868   my $self = shift;
3869   for my $status (qw( prospect active inactive suspended cancelled )) {
3870     my $method = $status.'_sql';
3871     my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
3872     my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
3873     $sth->execute( ($self->custnum) x $numnum ) or die $sth->errstr;
3874     return $status if $sth->fetchrow_arrayref->[0];
3875   }
3876 }
3877
3878 =item statuscolor
3879
3880 Returns a hex triplet color string for this customer's status.
3881
3882 =cut
3883
3884 use vars qw(%statuscolor);
3885 %statuscolor = (
3886   'prospect'  => '7e0079', #'000000', #black?  naw, purple
3887   'active'    => '00CC00', #green
3888   'inactive'  => '0000CC', #blue
3889   'suspended' => 'FF9900', #yellow
3890   'cancelled' => 'FF0000', #red
3891 );
3892
3893 sub statuscolor {
3894   my $self = shift;
3895   $statuscolor{$self->status};
3896 }
3897
3898 =back
3899
3900 =head1 CLASS METHODS
3901
3902 =over 4
3903
3904 =item prospect_sql
3905
3906 Returns an SQL expression identifying prospective cust_main records (customers
3907 with no packages ever ordered)
3908
3909 =cut
3910
3911 use vars qw($select_count_pkgs);
3912 $select_count_pkgs =
3913   "SELECT COUNT(*) FROM cust_pkg
3914     WHERE cust_pkg.custnum = cust_main.custnum";
3915
3916 sub select_count_pkgs_sql {
3917   $select_count_pkgs;
3918 }
3919
3920 sub prospect_sql { "
3921   0 = ( $select_count_pkgs )
3922 "; }
3923
3924 =item active_sql
3925
3926 Returns an SQL expression identifying active cust_main records (customers with
3927 no active recurring packages, but otherwise unsuspended/uncancelled).
3928
3929 =cut
3930
3931 sub active_sql { "
3932   0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. "
3933       )
3934 "; }
3935
3936 =item inactive_sql
3937
3938 Returns an SQL expression identifying inactive cust_main records (customers with
3939 active recurring packages).
3940
3941 =cut
3942
3943 sub inactive_sql { "
3944   0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
3945   AND
3946   0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
3947 "; }
3948
3949 =item susp_sql
3950 =item suspended_sql
3951
3952 Returns an SQL expression identifying suspended cust_main records.
3953
3954 =cut
3955
3956
3957 sub suspended_sql { susp_sql(@_); }
3958 sub susp_sql { "
3959     0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " )
3960     AND
3961     0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
3962 "; }
3963
3964 =item cancel_sql
3965 =item cancelled_sql
3966
3967 Returns an SQL expression identifying cancelled cust_main records.
3968
3969 =cut
3970
3971 sub cancelled_sql { cancel_sql(@_); }
3972 sub cancel_sql {
3973
3974   my $recurring_sql = FS::cust_pkg->recurring_sql;
3975   #my $recurring_sql = "
3976   #  '0' != ( select freq from part_pkg
3977   #             where cust_pkg.pkgpart = part_pkg.pkgpart )
3978   #";
3979
3980   "
3981     0 < ( $select_count_pkgs )
3982     AND 0 = ( $select_count_pkgs AND $recurring_sql
3983                   AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
3984             )
3985   ";
3986 }
3987
3988 =item uncancel_sql
3989 =item uncancelled_sql
3990
3991 Returns an SQL expression identifying un-cancelled cust_main records.
3992
3993 =cut
3994
3995 sub uncancelled_sql { uncancel_sql(@_); }
3996 sub uncancel_sql { "
3997   ( 0 < ( $select_count_pkgs
3998                    AND ( cust_pkg.cancel IS NULL
3999                          OR cust_pkg.cancel = 0
4000                        )
4001         )
4002     OR 0 = ( $select_count_pkgs )
4003   )
4004 "; }
4005
4006 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
4007
4008 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
4009 records.  Currently, I<first>, I<last> and/or I<company> may be specified (the
4010 appropriate ship_ field is also searched).
4011
4012 Additional options are the same as FS::Record::qsearch
4013
4014 =cut
4015
4016 sub fuzzy_search {
4017   my( $self, $fuzzy, $hash, @opt) = @_;
4018   #$self
4019   $hash ||= {};
4020   my @cust_main = ();
4021
4022   check_and_rebuild_fuzzyfiles();
4023   foreach my $field ( keys %$fuzzy ) {
4024
4025     my $all = $self->all_X($field);
4026     next unless scalar(@$all);
4027
4028     my %match = ();
4029     $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
4030
4031     my @fcust = ();
4032     foreach ( keys %match ) {
4033       push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
4034       push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt);
4035     }
4036     my %fsaw = ();
4037     push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust;
4038   }
4039
4040   # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes
4041   my %saw = ();
4042   @cust_main = grep { ++$saw{$_->custnum} == scalar(keys %$fuzzy) } @cust_main;
4043
4044   @cust_main;
4045
4046 }
4047
4048 =back
4049
4050 =head1 SUBROUTINES
4051
4052 =over 4
4053
4054 =item smart_search OPTION => VALUE ...
4055
4056 Accepts the following options: I<search>, the string to search for.  The string
4057 will be searched for as a customer number, phone number, name or company name,
4058 as an exact, or, in some cases, a substring or fuzzy match (see the source code
4059 for the exact heuristics used).
4060
4061 Any additional options are treated as an additional qualifier on the search
4062 (i.e. I<agentnum>).
4063
4064 Returns a (possibly empty) array of FS::cust_main objects.
4065
4066 =cut
4067
4068 sub smart_search {
4069   my %options = @_;
4070
4071   #here is the agent virtualization
4072   my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
4073
4074   my @cust_main = ();
4075
4076   my $search = delete $options{'search'};
4077   ( my $alphanum_search = $search ) =~ s/\W//g;
4078   
4079   if ( $alphanum_search =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { #phone# search
4080
4081     #false laziness w/Record::ut_phone
4082     my $phonen = "$1-$2-$3";
4083     $phonen .= " x$4" if $4;
4084
4085     push @cust_main, qsearch( {
4086       'table'   => 'cust_main',
4087       'hashref' => { %options },
4088       'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
4089                      ' ( '.
4090                          join(' OR ', map "$_ = '$phonen'",
4091                                           qw( daytime night fax
4092                                               ship_daytime ship_night ship_fax )
4093                              ).
4094                      ' ) '.
4095                      " AND $agentnums_sql", #agent virtualization
4096     } );
4097
4098     unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
4099       #try looking for matches with extensions unless one was specified
4100
4101       push @cust_main, qsearch( {
4102         'table'   => 'cust_main',
4103         'hashref' => { %options },
4104         'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
4105                        ' ( '.
4106                            join(' OR ', map "$_ LIKE '$phonen\%'",
4107                                             qw( daytime night
4108                                                 ship_daytime ship_night )
4109                                ).
4110                        ' ) '.
4111                        " AND $agentnums_sql", #agent virtualization
4112       } );
4113
4114     }
4115
4116   } elsif ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search
4117
4118     push @cust_main, qsearch( {
4119       'table'     => 'cust_main',
4120       'hashref'   => { 'custnum' => $1, %options },
4121       'extra_sql' => " AND $agentnums_sql", #agent virtualization
4122     } );
4123
4124   } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
4125
4126     my($company, $last, $first) = ( $1, $2, $3 );
4127
4128     # "Company (Last, First)"
4129     #this is probably something a browser remembered,
4130     #so just do an exact search
4131
4132     foreach my $prefix ( '', 'ship_' ) {
4133       push @cust_main, qsearch( {
4134         'table'     => 'cust_main',
4135         'hashref'   => { $prefix.'first'   => $first,
4136                          $prefix.'last'    => $last,
4137                          $prefix.'company' => $company,
4138                          %options,
4139                        },
4140         'extra_sql' => " AND $agentnums_sql",
4141       } );
4142     }
4143
4144   } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
4145                                               # try (ship_){last,company}
4146
4147     my $value = lc($1);
4148
4149     # # remove "(Last, First)" in "Company (Last, First)", otherwise the
4150     # # full strings the browser remembers won't work
4151     # $value =~ s/\([\w \,\.\-\']*\)$//; #false laziness w/Record::ut_name
4152
4153     use Lingua::EN::NameParse;
4154     my $NameParse = new Lingua::EN::NameParse(
4155              auto_clean     => 1,
4156              allow_reversed => 1,
4157     );
4158
4159     my($last, $first) = ( '', '' );
4160     #maybe disable this too and just rely on NameParse?
4161     if ( $value =~ /^(.+),\s*([^,]+)$/ ) { # Last, First
4162     
4163       ($last, $first) = ( $1, $2 );
4164     
4165     #} elsif  ( $value =~ /^(.+)\s+(.+)$/ ) {
4166     } elsif ( ! $NameParse->parse($value) ) {
4167
4168       my %name = $NameParse->components;
4169       $first = $name{'given_name_1'};
4170       $last  = $name{'surname_1'};
4171
4172     }
4173
4174     if ( $first && $last ) {
4175
4176       my($q_last, $q_first) = ( dbh->quote($last), dbh->quote($first) );
4177
4178       #exact
4179       my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
4180       $sql .= "
4181         (     ( LOWER(last) = $q_last AND LOWER(first) = $q_first )
4182            OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first )
4183         )";
4184
4185       push @cust_main, qsearch( {
4186         'table'     => 'cust_main',
4187         'hashref'   => \%options,
4188         'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
4189       } );
4190
4191       # or it just be something that was typed in... (try that in a sec)
4192
4193     }
4194
4195     my $q_value = dbh->quote($value);
4196
4197     #exact
4198     my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
4199     $sql .= " (    LOWER(last)         = $q_value
4200                 OR LOWER(company)      = $q_value
4201                 OR LOWER(ship_last)    = $q_value
4202                 OR LOWER(ship_company) = $q_value
4203               )";
4204
4205     push @cust_main, qsearch( {
4206       'table'     => 'cust_main',
4207       'hashref'   => \%options,
4208       'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
4209     } );
4210
4211     #always do substring & fuzzy,
4212     #getting complains searches are not returning enough
4213     #unless ( @cust_main ) {  #no exact match, trying substring/fuzzy
4214
4215       #still some false laziness w/ search/cust_main.cgi
4216
4217       #substring
4218
4219       my @hashrefs = (
4220         { 'company'      => { op=>'ILIKE', value=>"%$value%" }, },
4221         { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, },
4222       );
4223
4224       if ( $first && $last ) {
4225
4226         push @hashrefs,
4227           { 'first'        => { op=>'ILIKE', value=>"%$first%" },
4228             'last'         => { op=>'ILIKE', value=>"%$last%" },
4229           },
4230           { 'ship_first'   => { op=>'ILIKE', value=>"%$first%" },
4231             'ship_last'    => { op=>'ILIKE', value=>"%$last%" },
4232           },
4233         ;
4234
4235       } else {
4236
4237         push @hashrefs,
4238           { 'last'         => { op=>'ILIKE', value=>"%$value%" }, },
4239           { 'ship_last'    => { op=>'ILIKE', value=>"%$value%" }, },
4240         ;
4241       }
4242
4243       foreach my $hashref ( @hashrefs ) {
4244
4245         push @cust_main, qsearch( {
4246           'table'     => 'cust_main',
4247           'hashref'   => { %$hashref,
4248                            %options,
4249                          },
4250           'extra_sql' => " AND $agentnums_sql", #agent virtualizaiton
4251         } );
4252
4253       }
4254
4255       #fuzzy
4256       my @fuzopts = (
4257         \%options,                #hashref
4258         '',                       #select
4259         " AND $agentnums_sql",    #extra_sql  #agent virtualization
4260       );
4261
4262       if ( $first && $last ) {
4263         push @cust_main, FS::cust_main->fuzzy_search(
4264           { 'last'   => $last,    #fuzzy hashref
4265             'first'  => $first }, #
4266           @fuzopts
4267         );
4268       }
4269       foreach my $field ( 'last', 'company' ) {
4270         push @cust_main,
4271           FS::cust_main->fuzzy_search( { $field => $value }, @fuzopts );
4272       }
4273
4274     #}
4275
4276     #eliminate duplicates
4277     my %saw = ();
4278     @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
4279
4280   }
4281
4282   @cust_main;
4283
4284 }
4285
4286 =item check_and_rebuild_fuzzyfiles
4287
4288 =cut
4289
4290 use vars qw(@fuzzyfields);
4291 @fuzzyfields = ( 'last', 'first', 'company' );
4292
4293 sub check_and_rebuild_fuzzyfiles {
4294   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4295   rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields
4296 }
4297
4298 =item rebuild_fuzzyfiles
4299
4300 =cut
4301
4302 sub rebuild_fuzzyfiles {
4303
4304   use Fcntl qw(:flock);
4305
4306   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4307   mkdir $dir, 0700 unless -d $dir;
4308
4309   foreach my $fuzzy ( @fuzzyfields ) {
4310
4311     open(LOCK,">>$dir/cust_main.$fuzzy")
4312       or die "can't open $dir/cust_main.$fuzzy: $!";
4313     flock(LOCK,LOCK_EX)
4314       or die "can't lock $dir/cust_main.$fuzzy: $!";
4315
4316     open (CACHE,">$dir/cust_main.$fuzzy.tmp")
4317       or die "can't open $dir/cust_main.$fuzzy.tmp: $!";
4318
4319     foreach my $field ( $fuzzy, "ship_$fuzzy" ) {
4320       my $sth = dbh->prepare("SELECT $field FROM cust_main".
4321                              " WHERE $field != '' AND $field IS NOT NULL");
4322       $sth->execute or die $sth->errstr;
4323
4324       while ( my $row = $sth->fetchrow_arrayref ) {
4325         print CACHE $row->[0]. "\n";
4326       }
4327
4328     } 
4329
4330     close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!";
4331   
4332     rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy";
4333     close LOCK;
4334   }
4335
4336 }
4337
4338 =item all_X
4339
4340 =cut
4341
4342 sub all_X {
4343   my( $self, $field ) = @_;
4344   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4345   open(CACHE,"<$dir/cust_main.$field")
4346     or die "can't open $dir/cust_main.$field: $!";
4347   my @array = map { chomp; $_; } <CACHE>;
4348   close CACHE;
4349   \@array;
4350 }
4351
4352 =item append_fuzzyfiles LASTNAME COMPANY
4353
4354 =cut
4355
4356 sub append_fuzzyfiles {
4357   #my( $first, $last, $company ) = @_;
4358
4359   &check_and_rebuild_fuzzyfiles;
4360
4361   use Fcntl qw(:flock);
4362
4363   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4364
4365   foreach my $field (qw( first last company )) {
4366     my $value = shift;
4367
4368     if ( $value ) {
4369
4370       open(CACHE,">>$dir/cust_main.$field")
4371         or die "can't open $dir/cust_main.$field: $!";
4372       flock(CACHE,LOCK_EX)
4373         or die "can't lock $dir/cust_main.$field: $!";
4374
4375       print CACHE "$value\n";
4376
4377       flock(CACHE,LOCK_UN)
4378         or die "can't unlock $dir/cust_main.$field: $!";
4379       close CACHE;
4380     }
4381
4382   }
4383
4384   1;
4385 }
4386
4387 =item batch_import
4388
4389 =cut
4390
4391 sub batch_import {
4392   my $param = shift;
4393   #warn join('-',keys %$param);
4394   my $fh = $param->{filehandle};
4395   my $agentnum = $param->{agentnum};
4396
4397   my $refnum = $param->{refnum};
4398   my $pkgpart = $param->{pkgpart};
4399
4400   #my @fields = @{$param->{fields}};
4401   my $format = $param->{'format'};
4402   my @fields;
4403   my $payby;
4404   if ( $format eq 'simple' ) {
4405     @fields = qw( cust_pkg.setup dayphone first last
4406                   address1 address2 city state zip comments );
4407     $payby = 'BILL';
4408   } elsif ( $format eq 'extended' ) {
4409     @fields = qw( agent_custid refnum
4410                   last first address1 address2 city state zip country
4411                   daytime night
4412                   ship_last ship_first ship_address1 ship_address2
4413                   ship_city ship_state ship_zip ship_country
4414                   payinfo paycvv paydate
4415                   invoicing_list
4416                   cust_pkg.pkgpart
4417                   svc_acct.username svc_acct._password 
4418                 );
4419     $payby = 'BILL';
4420   } else {
4421     die "unknown format $format";
4422   }
4423
4424   eval "use Text::CSV_XS;";
4425   die $@ if $@;
4426
4427   my $csv = new Text::CSV_XS;
4428   #warn $csv;
4429   #warn $fh;
4430
4431   my $imported = 0;
4432   #my $columns;
4433
4434   local $SIG{HUP} = 'IGNORE';
4435   local $SIG{INT} = 'IGNORE';
4436   local $SIG{QUIT} = 'IGNORE';
4437   local $SIG{TERM} = 'IGNORE';
4438   local $SIG{TSTP} = 'IGNORE';
4439   local $SIG{PIPE} = 'IGNORE';
4440
4441   my $oldAutoCommit = $FS::UID::AutoCommit;
4442   local $FS::UID::AutoCommit = 0;
4443   my $dbh = dbh;
4444   
4445   #while ( $columns = $csv->getline($fh) ) {
4446   my $line;
4447   while ( defined($line=<$fh>) ) {
4448
4449     $csv->parse($line) or do {
4450       $dbh->rollback if $oldAutoCommit;
4451       return "can't parse: ". $csv->error_input();
4452     };
4453
4454     my @columns = $csv->fields();
4455     #warn join('-',@columns);
4456
4457     my %cust_main = (
4458       agentnum => $agentnum,
4459       refnum   => $refnum,
4460       country  => $conf->config('countrydefault') || 'US',
4461       payby    => $payby, #default
4462       paydate  => '12/2037', #default
4463     );
4464     my $billtime = time;
4465     my %cust_pkg = ( pkgpart => $pkgpart );
4466     my %svc_acct = ();
4467     foreach my $field ( @fields ) {
4468
4469       if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|expire|cancel)$/ ) {
4470
4471         #$cust_pkg{$1} = str2time( shift @$columns );
4472         if ( $1 eq 'pkgpart' ) {
4473           $cust_pkg{$1} = shift @columns;
4474         } elsif ( $1 eq 'setup' ) {
4475           $billtime = str2time(shift @columns);
4476         } else {
4477           $cust_pkg{$1} = str2time( shift @columns );
4478         } 
4479
4480       } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) {
4481
4482         $svc_acct{$1} = shift @columns;
4483         
4484       } else {
4485
4486         #refnum interception
4487         if ( $field eq 'refnum' && $columns[0] !~ /^\s*(\d+)\s*$/ ) {
4488
4489           my $referral = $columns[0];
4490           my %hash = ( 'referral' => $referral,
4491                        'agentnum' => $agentnum,
4492                        'disabled' => '',
4493                      );
4494
4495           my $part_referral = qsearchs('part_referral', \%hash )
4496                               || new FS::part_referral \%hash;
4497
4498           unless ( $part_referral->refnum ) {
4499             my $error = $part_referral->insert;
4500             if ( $error ) {
4501               $dbh->rollback if $oldAutoCommit;
4502               return "can't auto-insert advertising source: $referral: $error";
4503             }
4504           }
4505
4506           $columns[0] = $part_referral->refnum;
4507         }
4508
4509         #$cust_main{$field} = shift @$columns; 
4510         $cust_main{$field} = shift @columns; 
4511       }
4512     }
4513
4514     $cust_main{'payby'} = 'CARD' if length($cust_main{'payinfo'});
4515
4516     my $invoicing_list = $cust_main{'invoicing_list'}
4517                            ? [ delete $cust_main{'invoicing_list'} ]
4518                            : [];
4519
4520     my $cust_main = new FS::cust_main ( \%cust_main );
4521
4522     use Tie::RefHash;
4523     tie my %hash, 'Tie::RefHash'; #this part is important
4524
4525     if ( $cust_pkg{'pkgpart'} ) {
4526       my $cust_pkg = new FS::cust_pkg ( \%cust_pkg );
4527
4528       my @svc_acct = ();
4529       if ( $svc_acct{'username'} ) {
4530         my $part_pkg = $cust_pkg->part_pkg;
4531         unless ( $part_pkg ) {
4532           $dbh->rollback if $oldAutoCommit;
4533           return "unknown pkgnum ". $cust_pkg{'pkgpart'};
4534         } 
4535         $svc_acct{svcpart} = $part_pkg->svcpart( 'svc_acct' );
4536         push @svc_acct, new FS::svc_acct ( \%svc_acct )
4537       }
4538
4539       $hash{$cust_pkg} = \@svc_acct;
4540     }
4541
4542     my $error = $cust_main->insert( \%hash, $invoicing_list );
4543
4544     if ( $error ) {
4545       $dbh->rollback if $oldAutoCommit;
4546       return "can't insert customer for $line: $error";
4547     }
4548
4549     if ( $format eq 'simple' ) {
4550
4551       #false laziness w/bill.cgi
4552       $error = $cust_main->bill( 'time' => $billtime );
4553       if ( $error ) {
4554         $dbh->rollback if $oldAutoCommit;
4555         return "can't bill customer for $line: $error";
4556       }
4557   
4558       $cust_main->apply_payments;
4559       $cust_main->apply_credits;
4560   
4561       $error = $cust_main->collect();
4562       if ( $error ) {
4563         $dbh->rollback if $oldAutoCommit;
4564         return "can't collect customer for $line: $error";
4565       }
4566
4567     }
4568
4569     $imported++;
4570   }
4571
4572   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4573
4574   return "Empty file!" unless $imported;
4575
4576   ''; #no error
4577
4578 }
4579
4580 =item batch_charge
4581
4582 =cut
4583
4584 sub batch_charge {
4585   my $param = shift;
4586   #warn join('-',keys %$param);
4587   my $fh = $param->{filehandle};
4588   my @fields = @{$param->{fields}};
4589
4590   eval "use Text::CSV_XS;";
4591   die $@ if $@;
4592
4593   my $csv = new Text::CSV_XS;
4594   #warn $csv;
4595   #warn $fh;
4596
4597   my $imported = 0;
4598   #my $columns;
4599
4600   local $SIG{HUP} = 'IGNORE';
4601   local $SIG{INT} = 'IGNORE';
4602   local $SIG{QUIT} = 'IGNORE';
4603   local $SIG{TERM} = 'IGNORE';
4604   local $SIG{TSTP} = 'IGNORE';
4605   local $SIG{PIPE} = 'IGNORE';
4606
4607   my $oldAutoCommit = $FS::UID::AutoCommit;
4608   local $FS::UID::AutoCommit = 0;
4609   my $dbh = dbh;
4610   
4611   #while ( $columns = $csv->getline($fh) ) {
4612   my $line;
4613   while ( defined($line=<$fh>) ) {
4614
4615     $csv->parse($line) or do {
4616       $dbh->rollback if $oldAutoCommit;
4617       return "can't parse: ". $csv->error_input();
4618     };
4619
4620     my @columns = $csv->fields();
4621     #warn join('-',@columns);
4622
4623     my %row = ();
4624     foreach my $field ( @fields ) {
4625       $row{$field} = shift @columns;
4626     }
4627
4628     my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
4629     unless ( $cust_main ) {
4630       $dbh->rollback if $oldAutoCommit;
4631       return "unknown custnum $row{'custnum'}";
4632     }
4633
4634     if ( $row{'amount'} > 0 ) {
4635       my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
4636       if ( $error ) {
4637         $dbh->rollback if $oldAutoCommit;
4638         return $error;
4639       }
4640       $imported++;
4641     } elsif ( $row{'amount'} < 0 ) {
4642       my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
4643                                       $row{'pkg'}                         );
4644       if ( $error ) {
4645         $dbh->rollback if $oldAutoCommit;
4646         return $error;
4647       }
4648       $imported++;
4649     } else {
4650       #hmm?
4651     }
4652
4653   }
4654
4655   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4656
4657   return "Empty file!" unless $imported;
4658
4659   ''; #no error
4660
4661 }
4662
4663 =back
4664
4665 =head1 BUGS
4666
4667 The delete method.
4668
4669 The delete method should possibly take an FS::cust_main object reference
4670 instead of a scalar customer number.
4671
4672 Bill and collect options should probably be passed as references instead of a
4673 list.
4674
4675 There should probably be a configuration file with a list of allowed credit
4676 card types.
4677
4678 No multiple currency support (probably a larger project than just this module).
4679
4680 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
4681
4682 Birthdates rely on negative epoch values.
4683
4684 =head1 SEE ALSO
4685
4686 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
4687 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
4688 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
4689
4690 =cut
4691
4692 1;
4693