avoid harmless warning: Use of uninitialized value in string ne
[freeside.git] / FS / FS / cust_pay.pm
1 package FS::cust_pay;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me $conf @encrypted_fields
5              $unsuspendauto $ignore_noapply 
6            );
7 use Date::Format;
8 use Business::CreditCard;
9 use Text::Template;
10 use FS::UID qw( getotaker );
11 use FS::Misc qw( send_email );
12 use FS::Record qw( dbh qsearch qsearchs );
13 use FS::payby;
14 use FS::cust_main_Mixin;
15 use FS::payinfo_transaction_Mixin;
16 use FS::cust_bill;
17 use FS::cust_bill_pay;
18 use FS::cust_pay_refund;
19 use FS::cust_main;
20 use FS::cust_pay_void;
21
22 @ISA = qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record );
23
24 $DEBUG = 1;
25
26 $me = '[FS::cust_pay]';
27
28 $ignore_noapply = 0;
29
30 #ask FS::UID to run this stuff for us later
31 FS::UID->install_callback( sub { 
32   $conf = new FS::Conf;
33   $unsuspendauto = $conf->exists('unsuspendauto');
34 } );
35
36 @encrypted_fields = ('payinfo');
37
38 =head1 NAME
39
40 FS::cust_pay - Object methods for cust_pay objects
41
42 =head1 SYNOPSIS
43
44   use FS::cust_pay;
45
46   $record = new FS::cust_pay \%hash;
47   $record = new FS::cust_pay { 'column' => 'value' };
48
49   $error = $record->insert;
50
51   $error = $new_record->replace($old_record);
52
53   $error = $record->delete;
54
55   $error = $record->check;
56
57 =head1 DESCRIPTION
58
59 An FS::cust_pay object represents a payment; the transfer of money from a
60 customer.  FS::cust_pay inherits from FS::Record.  The following fields are
61 currently supported:
62
63 =over 4
64
65 =item paynum - primary key (assigned automatically for new payments)
66
67 =item custnum - customer (see L<FS::cust_main>)
68
69 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
70 L<Time::Local> and L<Date::Parse> for conversion functions.
71
72 =item paid - Amount of this payment
73
74 =item otaker - order taker (assigned automatically, see L<FS::UID>)
75
76 =item payby - Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
77
78 =item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
79
80 =item paymask - Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
81
82 =item paybatch - text field for tracking card processing or other batch grouping
83
84 =item payunique - Optional unique identifer to prevent duplicate transactions.
85
86 =item closed - books closed flag, empty or `Y'
87
88 =back
89
90 =head1 METHODS
91
92 =over 4 
93
94 =item new HASHREF
95
96 Creates a new payment.  To add the payment to the databse, see L<"insert">.
97
98 =cut
99
100 sub table { 'cust_pay'; }
101 sub cust_linked { $_[0]->cust_main_custnum; } 
102 sub cust_unlinked_msg {
103   my $self = shift;
104   "WARNING: can't find cust_main.custnum ". $self->custnum.
105   ' (cust_pay.paynum '. $self->paynum. ')';
106 }
107
108 =item insert
109
110 Adds this payment to the database.
111
112 For backwards-compatibility and convenience, if the additional field invnum
113 is defined, an FS::cust_bill_pay record for the full amount of the payment
114 will be created.  In this case, custnum is optional.  An hash of optional
115 arguments may be passed.  Currently "manual" is supported.  If true, a
116 payment receipt is sent instead of a statement when 'payment_receipt_email'
117 configuration option is set.
118
119 =cut
120
121 sub insert {
122   my ($self, %options) = @_;
123
124   local $SIG{HUP} = 'IGNORE';
125   local $SIG{INT} = 'IGNORE';
126   local $SIG{QUIT} = 'IGNORE';
127   local $SIG{TERM} = 'IGNORE';
128   local $SIG{TSTP} = 'IGNORE';
129   local $SIG{PIPE} = 'IGNORE';
130
131   my $oldAutoCommit = $FS::UID::AutoCommit;
132   local $FS::UID::AutoCommit = 0;
133   my $dbh = dbh;
134
135   my $cust_bill;
136   if ( $self->invnum ) {
137     $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
138       or do {
139         $dbh->rollback if $oldAutoCommit;
140         return "Unknown cust_bill.invnum: ". $self->invnum;
141       };
142     $self->custnum($cust_bill->custnum );
143   }
144
145
146   my $error = $self->check;
147   return $error if $error;
148
149   my $cust_main = $self->cust_main;
150   my $old_balance = $cust_main->balance;
151
152   $error = $self->SUPER::insert;
153   if ( $error ) {
154     $dbh->rollback if $oldAutoCommit;
155     return "error inserting $self: $error";
156   }
157
158   if ( $self->invnum ) {
159     my $cust_bill_pay = new FS::cust_bill_pay {
160       'invnum' => $self->invnum,
161       'paynum' => $self->paynum,
162       'amount' => $self->paid,
163       '_date'  => $self->_date,
164     };
165     $error = $cust_bill_pay->insert;
166     if ( $error ) {
167       if ( $ignore_noapply ) {
168         warn "warning: error inserting $cust_bill_pay: $error ".
169              "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
170       } else {
171         $dbh->rollback if $oldAutoCommit;
172         return "error inserting $cust_bill_pay: $error";
173       }
174     }
175   }
176
177   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
178
179   #false laziness w/ cust_credit::insert
180   if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
181     my @errors = $cust_main->unsuspend;
182     #return 
183     # side-fx with nested transactions?  upstack rolls back?
184     warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
185          join(' / ', @errors)
186       if @errors;
187   }
188   #eslaf
189
190   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
191
192   #my $cust_main = $self->cust_main;
193   if ( $conf->exists('payment_receipt_email')
194        && grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list
195   ) {
196
197     $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
198
199     my $error;
200     if (    ( exists($options{'manual'}) && $options{'manual'} )
201          || ! $conf->exists('invoice_html_statement')
202          || ! $cust_bill
203        ) {
204
205       my $receipt_template = new Text::Template (
206         TYPE   => 'ARRAY',
207         SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
208       ) or do {
209         warn "can't create payment receipt template: $Text::Template::ERROR";
210         return '';
211       };
212
213       my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
214                              $cust_main->invoicing_list;
215
216       my $payby = $self->payby;
217       my $payinfo = $self->payinfo;
218       $payby =~ s/^BILL$/Check/ if $payinfo;
219       if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
220         $payinfo = $self->paymask
221       } else {
222         $payinfo = $self->decrypt($payinfo);
223       }
224       $payby =~ s/^CHEK$/Electronic check/;
225
226       $error = send_email(
227         'from'    => $conf->config('invoice_from', $cust_main->agentnum),
228                                    #invoice_from??? well as good as any
229         'to'      => \@invoicing_list,
230         'subject' => 'Payment receipt',
231         'body'    => [ $receipt_template->fill_in( HASH => {
232                        'date'         => time2str("%a %B %o, %Y", $self->_date),
233                        'name'         => $cust_main->name,
234                        'paynum'       => $self->paynum,
235                        'paid'         => sprintf("%.2f", $self->paid),
236                        'payby'        => ucfirst(lc($payby)),
237                        'payinfo'      => $payinfo,
238                        'balance'      => $cust_main->balance,
239                        'company_name' => $conf->config('company_name'),
240                      } ) ],
241       );
242
243     } else {
244
245       my $queue = new FS::queue {
246          'paynum' => $self->paynum,
247          'job'    => 'FS::cust_bill::queueable_email',
248       };
249       $error = $queue->insert(
250         'invnum' => $cust_bill->invnum,
251         'template' => 'statement',
252       );
253
254     }
255
256     if ( $error ) {
257       warn "can't send payment receipt/statement: $error";
258     }
259
260   }
261
262   '';
263
264 }
265
266 =item void [ REASON ]
267
268 Voids this payment: deletes the payment and all associated applications and
269 adds a record of the voided payment to the FS::cust_pay_void table.
270
271 =cut
272
273 sub void {
274   my $self = shift;
275
276   local $SIG{HUP} = 'IGNORE';
277   local $SIG{INT} = 'IGNORE';
278   local $SIG{QUIT} = 'IGNORE';
279   local $SIG{TERM} = 'IGNORE';
280   local $SIG{TSTP} = 'IGNORE';
281   local $SIG{PIPE} = 'IGNORE';
282
283   my $oldAutoCommit = $FS::UID::AutoCommit;
284   local $FS::UID::AutoCommit = 0;
285   my $dbh = dbh;
286
287   my $cust_pay_void = new FS::cust_pay_void ( {
288     map { $_ => $self->get($_) } $self->fields
289   } );
290   $cust_pay_void->reason(shift) if scalar(@_);
291   my $error = $cust_pay_void->insert;
292   if ( $error ) {
293     $dbh->rollback if $oldAutoCommit;
294     return $error;
295   }
296
297   $error = $self->delete;
298   if ( $error ) {
299     $dbh->rollback if $oldAutoCommit;
300     return $error;
301   }
302
303   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
304
305   '';
306
307 }
308
309 =item delete
310
311 Unless the closed flag is set, deletes this payment and all associated
312 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>).  In most
313 cases, you want to use the void method instead to leave a record of the
314 deleted payment.
315
316 =cut
317
318 # very similar to FS::cust_credit::delete
319 sub delete {
320   my $self = shift;
321   return "Can't delete closed payment" if $self->closed =~ /^Y/i;
322
323   local $SIG{HUP} = 'IGNORE';
324   local $SIG{INT} = 'IGNORE';
325   local $SIG{QUIT} = 'IGNORE';
326   local $SIG{TERM} = 'IGNORE';
327   local $SIG{TSTP} = 'IGNORE';
328   local $SIG{PIPE} = 'IGNORE';
329
330   my $oldAutoCommit = $FS::UID::AutoCommit;
331   local $FS::UID::AutoCommit = 0;
332   my $dbh = dbh;
333
334   foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
335     my $error = $app->delete;
336     if ( $error ) {
337       $dbh->rollback if $oldAutoCommit;
338       return $error;
339     }
340   }
341
342   my $error = $self->SUPER::delete(@_);
343   if ( $error ) {
344     $dbh->rollback if $oldAutoCommit;
345     return $error;
346   }
347
348   if (    $conf->exists('deletepayments')
349        && $conf->config('deletepayments') ne '' ) {
350
351     my $cust_main = $self->cust_main;
352
353     my $error = send_email(
354       'from'    => $conf->config('invoice_from', $self->cust_main->agentnum),
355                                  #invoice_from??? well as good as any
356       'to'      => $conf->config('deletepayments'),
357       'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
358       'body'    => [
359         "This is an automatic message from your Freeside installation\n",
360         "informing you that the following payment has been deleted:\n",
361         "\n",
362         'paynum: '. $self->paynum. "\n",
363         'custnum: '. $self->custnum.
364           " (". $cust_main->last. ", ". $cust_main->first. ")\n",
365         'paid: $'. sprintf("%.2f", $self->paid). "\n",
366         'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
367         'payby: '. $self->payby. "\n",
368         'payinfo: '. $self->paymask. "\n",
369         'paybatch: '. $self->paybatch. "\n",
370       ],
371     );
372
373     if ( $error ) {
374       $dbh->rollback if $oldAutoCommit;
375       return "can't send payment deletion notification: $error";
376     }
377
378   }
379
380   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
381
382   '';
383
384 }
385
386 =item replace OLD_RECORD
387
388 You can, but probably shouldn't modify payments...
389
390 =cut
391
392 sub replace {
393   #return "Can't modify payment!"
394   my $self = shift;
395   return "Can't modify closed payment" if $self->closed =~ /^Y/i;
396   $self->SUPER::replace(@_);
397 }
398
399 =item check
400
401 Checks all fields to make sure this is a valid payment.  If there is an error,
402 returns the error, otherwise returns false.  Called by the insert method.
403
404 =cut
405
406 sub check {
407   my $self = shift;
408
409   $self->otaker(getotaker) unless ($self->otaker);
410
411   my $error =
412     $self->ut_numbern('paynum')
413     || $self->ut_numbern('custnum')
414     || $self->ut_numbern('_date')
415     || $self->ut_money('paid')
416     || $self->ut_alpha('otaker')
417     || $self->ut_textn('paybatch')
418     || $self->ut_textn('payunique')
419     || $self->ut_enum('closed', [ '', 'Y' ])
420     || $self->payinfo_check()
421   ;
422   return $error if $error;
423
424   return "paid must be > 0 " if $self->paid <= 0;
425
426   return "unknown cust_main.custnum: ". $self->custnum
427     unless $self->invnum
428            || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
429
430   $self->_date(time) unless $self->_date;
431
432 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
433 #  # UNIQUE index should catch this too, without race conditions, but this
434 #  # should give a better error message the other 99.9% of the time...
435 #  if ( length($self->payunique)
436 #       && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
437 #    #well, it *could* be a better error message
438 #    return "duplicate transaction".
439 #           " - a payment with unique identifer ". $self->payunique.
440 #           " already exists";
441 #  }
442
443   $self->SUPER::check;
444 }
445
446 =item batch_insert CUST_PAY_OBJECT, ...
447
448 Class method which inserts multiple payments.  Takes a list of FS::cust_pay
449 objects.  Returns a list, each element representing the status of inserting the
450 corresponding payment - empty.  If there is an error inserting any payment, the
451 entire transaction is rolled back, i.e. all payments are inserted or none are.
452
453 For example:
454
455   my @errors = FS::cust_pay->batch_insert(@cust_pay);
456   my $num_errors = scalar(grep $_, @errors);
457   if ( $num_errors == 0 ) {
458     #success; all payments were inserted
459   } else {
460     #failure; no payments were inserted.
461   }
462
463 =cut
464
465 sub batch_insert {
466   my $self = shift; #class method
467
468   local $SIG{HUP} = 'IGNORE';
469   local $SIG{INT} = 'IGNORE';
470   local $SIG{QUIT} = 'IGNORE';
471   local $SIG{TERM} = 'IGNORE';
472   local $SIG{TSTP} = 'IGNORE';
473   local $SIG{PIPE} = 'IGNORE';
474
475   my $oldAutoCommit = $FS::UID::AutoCommit;
476   local $FS::UID::AutoCommit = 0;
477   my $dbh = dbh;
478
479   my $errors = 0;
480   
481   my @errors = map {
482     my $error = $_->insert( 'manual' => 1 );
483     if ( $error ) { 
484       $errors++;
485     } else {
486       $_->cust_main->apply_payments;
487     }
488     $error;
489   } @_;
490
491   if ( $errors ) {
492     $dbh->rollback if $oldAutoCommit;
493   } else {
494     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
495   }
496
497   @errors;
498
499 }
500
501 =item cust_bill_pay
502
503 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
504 payment.
505
506 =cut
507
508 sub cust_bill_pay {
509   my $self = shift;
510   sort {    $a->_date  <=> $b->_date
511          || $a->invnum <=> $b->invnum }
512     qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
513   ;
514 }
515
516 =item cust_pay_refund
517
518 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
519 payment.
520
521 =cut
522
523 sub cust_pay_refund {
524   my $self = shift;
525   sort { $a->_date <=> $b->_date }
526     qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
527   ;
528 }
529
530
531 =item unapplied
532
533 Returns the amount of this payment that is still unapplied; which is
534 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
535 applications (see L<FS::cust_pay_refund>).
536
537 =cut
538
539 sub unapplied {
540   my $self = shift;
541   my $amount = $self->paid;
542   $amount -= $_->amount foreach ( $self->cust_bill_pay );
543   $amount -= $_->amount foreach ( $self->cust_pay_refund );
544   sprintf("%.2f", $amount );
545 }
546
547 =item unrefunded
548
549 Returns the amount of this payment that has not been refuned; which is
550 paid minus all  refund applications (see L<FS::cust_pay_refund>).
551
552 =cut
553
554 sub unrefunded {
555   my $self = shift;
556   my $amount = $self->paid;
557   $amount -= $_->amount foreach ( $self->cust_pay_refund );
558   sprintf("%.2f", $amount );
559 }
560
561 =item amount
562
563 Returns the "paid" field.
564
565 =cut
566
567 sub amount {
568   my $self = shift;
569   $self->paid();
570 }
571
572 =back
573
574 =head1 CLASS METHODS
575
576 =over 4
577
578 =item unapplied_sql
579
580 Returns an SQL fragment to retreive the unapplied amount.
581
582 =cut 
583
584 sub unapplied_sql {
585   #my $class = shift;
586
587   "paid
588         - COALESCE( 
589                     ( SELECT SUM(amount) FROM cust_bill_pay
590                         WHERE cust_pay.paynum = cust_bill_pay.paynum )
591                     ,0
592                   )
593         - COALESCE(
594                     ( SELECT SUM(amount) FROM cust_pay_refund
595                         WHERE cust_pay.paynum = cust_pay_refund.paynum )
596                     ,0
597                   )
598   ";
599
600 }
601
602 # _upgrade_data
603 #
604 # Used by FS::Upgrade to migrate to a new database.
605
606 use FS::h_cust_pay;
607
608 sub _upgrade_data {  #class method
609   my ($class, %opts) = @_;
610
611   warn "$me upgrading $class\n" if $DEBUG;
612
613   #not the most efficient, but hey, it only has to run once
614
615   my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
616               "  AND 0 < ( SELECT COUNT(*) FROM cust_main                 ".
617               "              WHERE cust_main.custnum = cust_pay.custnum ) ";
618
619   my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
620
621   my $sth = dbh->prepare($count_sql) or die dbh->errstr;
622   $sth->execute or die $sth->errstr;
623   my $total = $sth->fetchrow_arrayref->[0];
624   #warn "$total cust_pay records to update\n"
625   #  if $DEBUG;
626   local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
627
628   my $count = 0;
629   my $lastprog = 0;
630
631   my @cust_pay = qsearch( {
632       'table'     => 'cust_pay',
633       'hashref'   => {},
634       'extra_sql' => $where,
635       'order_by'  => 'ORDER BY paynum',
636   } );
637
638   foreach my $cust_pay (@cust_pay) {
639
640     my $h_cust_pay = $cust_pay->h_search('insert');
641     if ( $h_cust_pay ) {
642       next if $cust_pay->otaker eq $h_cust_pay->history_user;
643       $cust_pay->otaker($h_cust_pay->history_user);
644     } else {
645       $cust_pay->otaker('legacy');
646     }
647
648     delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
649     my $error = $cust_pay->replace;
650
651     if ( $error ) {
652       warn " *** WARNING: Error updating order taker for payment paynum ".
653            $cust_pay->paynun. ": $error\n";
654       next;
655     }
656
657     $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
658
659     $count++;
660     if ( $DEBUG > 1 && $lastprog + 30 < time ) {
661       warn "$me $count/$total (". sprintf('%.2f',100*$count/$total). '%)'. "\n";
662       $lastprog = time;
663     }
664
665   }
666
667 }
668
669 =back
670
671 =head1 SUBROUTINES
672
673 =over 4 
674
675 =item batch_import HASHREF
676
677 Inserts new payments.
678
679 =cut
680
681 sub batch_import {
682   my $param = shift;
683
684   my $fh = $param->{filehandle};
685   my $agentnum = $param->{agentnum};
686   my $format = $param->{'format'};
687   my $paybatch = $param->{'paybatch'};
688
689   # here is the agent virtualization
690   my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
691
692   my @fields;
693   my $payby;
694   if ( $format eq 'simple' ) {
695     @fields = qw( custnum agent_custid paid payinfo );
696     $payby = 'BILL';
697   } elsif ( $format eq 'extended' ) {
698     die "unimplemented\n";
699     @fields = qw( );
700     $payby = 'BILL';
701   } else {
702     die "unknown format $format";
703   }
704
705   eval "use Text::CSV_XS;";
706   die $@ if $@;
707
708   my $csv = new Text::CSV_XS;
709
710   my $imported = 0;
711
712   local $SIG{HUP} = 'IGNORE';
713   local $SIG{INT} = 'IGNORE';
714   local $SIG{QUIT} = 'IGNORE';
715   local $SIG{TERM} = 'IGNORE';
716   local $SIG{TSTP} = 'IGNORE';
717   local $SIG{PIPE} = 'IGNORE';
718
719   my $oldAutoCommit = $FS::UID::AutoCommit;
720   local $FS::UID::AutoCommit = 0;
721   my $dbh = dbh;
722   
723   my $line;
724   while ( defined($line=<$fh>) ) {
725
726     $csv->parse($line) or do {
727       $dbh->rollback if $oldAutoCommit;
728       return "can't parse: ". $csv->error_input();
729     };
730
731     my @columns = $csv->fields();
732
733     my %cust_pay = (
734       payby    => $payby,
735       paybatch => $paybatch,
736     );
737
738     my $cust_main;
739     foreach my $field ( @fields ) {
740
741       if ( $field eq 'agent_custid'
742         && $agentnum
743         && $columns[0] =~ /\S+/ )
744       {
745
746         my $agent_custid = $columns[0];
747         my %hash = ( 'agent_custid' => $agent_custid,
748                      'agentnum'     => $agentnum,
749                    );
750
751         if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
752           $dbh->rollback if $oldAutoCommit;
753           return "can't specify custnum with agent_custid $agent_custid";
754         }
755
756         $cust_main = qsearchs({
757                                 'table'     => 'cust_main',
758                                 'hashref'   => \%hash,
759                                 'extra_sql' => $extra_sql,
760                              });
761
762         unless ( $cust_main ) {
763           $dbh->rollback if $oldAutoCommit;
764           return "can't find customer with agent_custid $agent_custid";
765         }
766
767         $field = 'custnum';
768         $columns[0] = $cust_main->custnum;
769       }
770
771       $cust_pay{$field} = shift @columns; 
772     }
773
774     my $cust_pay = new FS::cust_pay( \%cust_pay );
775     my $error = $cust_pay->insert;
776
777     if ( $error ) {
778       $dbh->rollback if $oldAutoCommit;
779       return "can't insert payment for $line: $error";
780     }
781
782     if ( $format eq 'simple' ) {
783       # include agentnum for less surprise?
784       $cust_main = qsearchs({
785                              'table'     => 'cust_main',
786                              'hashref'   => { 'custnum' => $cust_pay->custnum },
787                              'extra_sql' => $extra_sql,
788                            })
789         unless $cust_main;
790
791       unless ( $cust_main ) {
792         $dbh->rollback if $oldAutoCommit;
793         return "can't find customer to which payments apply at line: $line";
794       }
795
796       $error = $cust_main->apply_payments_and_credits;
797       if ( $error ) {
798         $dbh->rollback if $oldAutoCommit;
799         return "can't apply payments to customer for $line: $error";
800       }
801
802     }
803
804     $imported++;
805   }
806
807   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
808
809   return "Empty file!" unless $imported;
810
811   ''; #no error
812
813 }
814
815 =back
816
817 =head1 BUGS
818
819 Delete and replace methods.  
820
821 =head1 SEE ALSO
822
823 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
824 schema.html from the base documentation.
825
826 =cut
827
828 1;
829