add apply option to realtime_collect, RT#5071
[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       $payinfo = $self->paymask if $payby eq 'CARD' || $payby eq 'CHEK';
220       $payby =~ s/^CHEK$/Electronic check/;
221
222       $error = send_email(
223         'from'    => $conf->config('invoice_from'), #??? well as good as any
224         'to'      => \@invoicing_list,
225         'subject' => 'Payment receipt',
226         'body'    => [ $receipt_template->fill_in( HASH => {
227                        'date'    => time2str("%a %B %o, %Y", $self->_date),
228                        'name'    => $cust_main->name,
229                        'paynum'  => $self->paynum,
230                        'paid'    => sprintf("%.2f", $self->paid),
231                        'payby'   => ucfirst(lc($payby)),
232                        'payinfo' => $payinfo,
233                        'balance' => $cust_main->balance,
234                      } ) ],
235       );
236
237     } else {
238
239       my $queue = new FS::queue {
240          'paynum' => $self->paynum,
241          'job'    => 'FS::cust_bill::queueable_email',
242       };
243       $error = $queue->insert(
244         'invnum' => $cust_bill->invnum,
245         'template' => 'statement',
246       );
247
248     }
249
250     if ( $error ) {
251       warn "can't send payment receipt/statement: $error";
252     }
253
254   }
255
256   '';
257
258 }
259
260 =item void [ REASON ]
261
262 Voids this payment: deletes the payment and all associated applications and
263 adds a record of the voided payment to the FS::cust_pay_void table.
264
265 =cut
266
267 sub void {
268   my $self = shift;
269
270   local $SIG{HUP} = 'IGNORE';
271   local $SIG{INT} = 'IGNORE';
272   local $SIG{QUIT} = 'IGNORE';
273   local $SIG{TERM} = 'IGNORE';
274   local $SIG{TSTP} = 'IGNORE';
275   local $SIG{PIPE} = 'IGNORE';
276
277   my $oldAutoCommit = $FS::UID::AutoCommit;
278   local $FS::UID::AutoCommit = 0;
279   my $dbh = dbh;
280
281   my $cust_pay_void = new FS::cust_pay_void ( {
282     map { $_ => $self->get($_) } $self->fields
283   } );
284   $cust_pay_void->reason(shift) if scalar(@_);
285   my $error = $cust_pay_void->insert;
286   if ( $error ) {
287     $dbh->rollback if $oldAutoCommit;
288     return $error;
289   }
290
291   $error = $self->delete;
292   if ( $error ) {
293     $dbh->rollback if $oldAutoCommit;
294     return $error;
295   }
296
297   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
298
299   '';
300
301 }
302
303 =item delete
304
305 Unless the closed flag is set, deletes this payment and all associated
306 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>).  In most
307 cases, you want to use the void method instead to leave a record of the
308 deleted payment.
309
310 =cut
311
312 # very similar to FS::cust_credit::delete
313 sub delete {
314   my $self = shift;
315   return "Can't delete closed payment" if $self->closed =~ /^Y/i;
316
317   local $SIG{HUP} = 'IGNORE';
318   local $SIG{INT} = 'IGNORE';
319   local $SIG{QUIT} = 'IGNORE';
320   local $SIG{TERM} = 'IGNORE';
321   local $SIG{TSTP} = 'IGNORE';
322   local $SIG{PIPE} = 'IGNORE';
323
324   my $oldAutoCommit = $FS::UID::AutoCommit;
325   local $FS::UID::AutoCommit = 0;
326   my $dbh = dbh;
327
328   foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
329     my $error = $app->delete;
330     if ( $error ) {
331       $dbh->rollback if $oldAutoCommit;
332       return $error;
333     }
334   }
335
336   my $error = $self->SUPER::delete(@_);
337   if ( $error ) {
338     $dbh->rollback if $oldAutoCommit;
339     return $error;
340   }
341
342   if (    $conf->exists('deletepayments')
343        && $conf->config('deletepayments') ne '' ) {
344
345     my $cust_main = $self->cust_main;
346
347     my $error = send_email(
348       'from'    => $conf->config('invoice_from'), #??? well as good as any
349       'to'      => $conf->config('deletepayments'),
350       'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
351       'body'    => [
352         "This is an automatic message from your Freeside installation\n",
353         "informing you that the following payment has been deleted:\n",
354         "\n",
355         'paynum: '. $self->paynum. "\n",
356         'custnum: '. $self->custnum.
357           " (". $cust_main->last. ", ". $cust_main->first. ")\n",
358         'paid: $'. sprintf("%.2f", $self->paid). "\n",
359         'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
360         'payby: '. $self->payby. "\n",
361         'payinfo: '. $self->paymask. "\n",
362         'paybatch: '. $self->paybatch. "\n",
363       ],
364     );
365
366     if ( $error ) {
367       $dbh->rollback if $oldAutoCommit;
368       return "can't send payment deletion notification: $error";
369     }
370
371   }
372
373   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
374
375   '';
376
377 }
378
379 =item replace OLD_RECORD
380
381 You can, but probably shouldn't modify payments...
382
383 =cut
384
385 sub replace {
386   #return "Can't modify payment!"
387   my $self = shift;
388   return "Can't modify closed payment" if $self->closed =~ /^Y/i;
389   $self->SUPER::replace(@_);
390 }
391
392 =item check
393
394 Checks all fields to make sure this is a valid payment.  If there is an error,
395 returns the error, otherwise returns false.  Called by the insert method.
396
397 =cut
398
399 sub check {
400   my $self = shift;
401
402   $self->otaker(getotaker) unless ($self->otaker);
403
404   my $error =
405     $self->ut_numbern('paynum')
406     || $self->ut_numbern('custnum')
407     || $self->ut_numbern('_date')
408     || $self->ut_money('paid')
409     || $self->ut_alpha('otaker')
410     || $self->ut_textn('paybatch')
411     || $self->ut_textn('payunique')
412     || $self->ut_enum('closed', [ '', 'Y' ])
413     || $self->payinfo_check()
414   ;
415   return $error if $error;
416
417   return "paid must be > 0 " if $self->paid <= 0;
418
419   return "unknown cust_main.custnum: ". $self->custnum
420     unless $self->invnum
421            || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
422
423   $self->_date(time) unless $self->_date;
424
425 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
426 #  # UNIQUE index should catch this too, without race conditions, but this
427 #  # should give a better error message the other 99.9% of the time...
428 #  if ( length($self->payunique)
429 #       && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
430 #    #well, it *could* be a better error message
431 #    return "duplicate transaction".
432 #           " - a payment with unique identifer ". $self->payunique.
433 #           " already exists";
434 #  }
435
436   $self->SUPER::check;
437 }
438
439 =item batch_insert CUST_PAY_OBJECT, ...
440
441 Class method which inserts multiple payments.  Takes a list of FS::cust_pay
442 objects.  Returns a list, each element representing the status of inserting the
443 corresponding payment - empty.  If there is an error inserting any payment, the
444 entire transaction is rolled back, i.e. all payments are inserted or none are.
445
446 For example:
447
448   my @errors = FS::cust_pay->batch_insert(@cust_pay);
449   my $num_errors = scalar(grep $_, @errors);
450   if ( $num_errors == 0 ) {
451     #success; all payments were inserted
452   } else {
453     #failure; no payments were inserted.
454   }
455
456 =cut
457
458 sub batch_insert {
459   my $self = shift; #class method
460
461   local $SIG{HUP} = 'IGNORE';
462   local $SIG{INT} = 'IGNORE';
463   local $SIG{QUIT} = 'IGNORE';
464   local $SIG{TERM} = 'IGNORE';
465   local $SIG{TSTP} = 'IGNORE';
466   local $SIG{PIPE} = 'IGNORE';
467
468   my $oldAutoCommit = $FS::UID::AutoCommit;
469   local $FS::UID::AutoCommit = 0;
470   my $dbh = dbh;
471
472   my $errors = 0;
473   
474   my @errors = map {
475     my $error = $_->insert( 'manual' => 1 );
476     if ( $error ) { 
477       $errors++;
478     } else {
479       $_->cust_main->apply_payments;
480     }
481     $error;
482   } @_;
483
484   if ( $errors ) {
485     $dbh->rollback if $oldAutoCommit;
486   } else {
487     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
488   }
489
490   @errors;
491
492 }
493
494 =item cust_bill_pay
495
496 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
497 payment.
498
499 =cut
500
501 sub cust_bill_pay {
502   my $self = shift;
503   sort {    $a->_date  <=> $b->_date
504          || $a->invnum <=> $b->invnum }
505     qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
506   ;
507 }
508
509 =item cust_pay_refund
510
511 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
512 payment.
513
514 =cut
515
516 sub cust_pay_refund {
517   my $self = shift;
518   sort { $a->_date <=> $b->_date }
519     qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
520   ;
521 }
522
523
524 =item unapplied
525
526 Returns the amount of this payment that is still unapplied; which is
527 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
528 applications (see L<FS::cust_pay_refund>).
529
530 =cut
531
532 sub unapplied {
533   my $self = shift;
534   my $amount = $self->paid;
535   $amount -= $_->amount foreach ( $self->cust_bill_pay );
536   $amount -= $_->amount foreach ( $self->cust_pay_refund );
537   sprintf("%.2f", $amount );
538 }
539
540 =item unrefunded
541
542 Returns the amount of this payment that has not been refuned; which is
543 paid minus all  refund applications (see L<FS::cust_pay_refund>).
544
545 =cut
546
547 sub unrefunded {
548   my $self = shift;
549   my $amount = $self->paid;
550   $amount -= $_->amount foreach ( $self->cust_pay_refund );
551   sprintf("%.2f", $amount );
552 }
553
554 =item amount
555
556 Returns the "paid" field.
557
558 =cut
559
560 sub amount {
561   my $self = shift;
562   $self->paid();
563 }
564
565 =back
566
567 =head1 CLASS METHODS
568
569 =over 4
570
571 =item unapplied_sql
572
573 Returns an SQL fragment to retreive the unapplied amount.
574
575 =cut 
576
577 sub unapplied_sql {
578   #my $class = shift;
579
580   "paid
581         - COALESCE( 
582                     ( SELECT SUM(amount) FROM cust_bill_pay
583                         WHERE cust_pay.paynum = cust_bill_pay.paynum )
584                     ,0
585                   )
586         - COALESCE(
587                     ( SELECT SUM(amount) FROM cust_pay_refund
588                         WHERE cust_pay.paynum = cust_pay_refund.paynum )
589                     ,0
590                   )
591   ";
592
593 }
594
595 # _upgrade_data
596 #
597 # Used by FS::Upgrade to migrate to a new database.
598
599 use FS::h_cust_pay;
600
601 sub _upgrade_data {  #class method
602   my ($class, %opts) = @_;
603
604   warn "$me upgrading $class\n" if $DEBUG;
605
606   #not the most efficient, but hey, it only has to run once
607
608   my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
609               "  AND 0 < ( SELECT COUNT(*) FROM cust_main                 ".
610               "              WHERE cust_main.custnum = cust_pay.custnum ) ";
611
612   my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
613
614   my $sth = dbh->prepare($count_sql) or die dbh->errstr;
615   $sth->execute or die $sth->errstr;
616   my $total = $sth->fetchrow_arrayref->[0];
617   #warn "$total cust_pay records to update\n"
618   #  if $DEBUG;
619   local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
620
621   my $count = 0;
622   my $lastprog = 0;
623
624   my @cust_pay = qsearch( {
625       'table'     => 'cust_pay',
626       'hashref'   => {},
627       'extra_sql' => $where,
628       'order_by'  => 'ORDER BY paynum',
629   } );
630
631   foreach my $cust_pay (@cust_pay) {
632
633     my $h_cust_pay = $cust_pay->h_search('insert');
634     if ( $h_cust_pay ) {
635       next if $cust_pay->otaker eq $h_cust_pay->history_user;
636       $cust_pay->otaker($h_cust_pay->history_user);
637     } else {
638       $cust_pay->otaker('legacy');
639     }
640
641     delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
642     my $error = $cust_pay->replace;
643
644     if ( $error ) {
645       warn " *** WARNING: Error updating order taker for payment paynum ".
646            $cust_pay->paynun. ": $error\n";
647       next;
648     }
649
650     $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
651
652     $count++;
653     if ( $DEBUG > 1 && $lastprog + 30 < time ) {
654       warn "$me $count/$total (". sprintf('%.2f',100*$count/$total). '%)'. "\n";
655       $lastprog = time;
656     }
657
658   }
659
660 }
661
662 =back
663
664 =head1 SUBROUTINES
665
666 =over 4 
667
668 =item batch_import HASHREF
669
670 Inserts new payments.
671
672 =cut
673
674 sub batch_import {
675   my $param = shift;
676
677   my $fh = $param->{filehandle};
678   my $agentnum = $param->{agentnum};
679   my $format = $param->{'format'};
680   my $paybatch = $param->{'paybatch'};
681
682   # here is the agent virtualization
683   my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
684
685   my @fields;
686   my $payby;
687   if ( $format eq 'simple' ) {
688     @fields = qw( custnum agent_custid paid payinfo );
689     $payby = 'BILL';
690   } elsif ( $format eq 'extended' ) {
691     die "unimplemented\n";
692     @fields = qw( );
693     $payby = 'BILL';
694   } else {
695     die "unknown format $format";
696   }
697
698   eval "use Text::CSV_XS;";
699   die $@ if $@;
700
701   my $csv = new Text::CSV_XS;
702
703   my $imported = 0;
704
705   local $SIG{HUP} = 'IGNORE';
706   local $SIG{INT} = 'IGNORE';
707   local $SIG{QUIT} = 'IGNORE';
708   local $SIG{TERM} = 'IGNORE';
709   local $SIG{TSTP} = 'IGNORE';
710   local $SIG{PIPE} = 'IGNORE';
711
712   my $oldAutoCommit = $FS::UID::AutoCommit;
713   local $FS::UID::AutoCommit = 0;
714   my $dbh = dbh;
715   
716   my $line;
717   while ( defined($line=<$fh>) ) {
718
719     $csv->parse($line) or do {
720       $dbh->rollback if $oldAutoCommit;
721       return "can't parse: ". $csv->error_input();
722     };
723
724     my @columns = $csv->fields();
725
726     my %cust_pay = (
727       payby    => $payby,
728       paybatch => $paybatch,
729     );
730
731     my $cust_main;
732     foreach my $field ( @fields ) {
733
734       if ( $field eq 'agent_custid'
735         && $agentnum
736         && $columns[0] =~ /\S+/ )
737       {
738
739         my $agent_custid = $columns[0];
740         my %hash = ( 'agent_custid' => $agent_custid,
741                      'agentnum'     => $agentnum,
742                    );
743
744         if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
745           $dbh->rollback if $oldAutoCommit;
746           return "can't specify custnum with agent_custid $agent_custid";
747         }
748
749         $cust_main = qsearchs({
750                                 'table'     => 'cust_main',
751                                 'hashref'   => \%hash,
752                                 'extra_sql' => $extra_sql,
753                              });
754
755         unless ( $cust_main ) {
756           $dbh->rollback if $oldAutoCommit;
757           return "can't find customer with agent_custid $agent_custid";
758         }
759
760         $field = 'custnum';
761         $columns[0] = $cust_main->custnum;
762       }
763
764       $cust_pay{$field} = shift @columns; 
765     }
766
767     my $cust_pay = new FS::cust_pay( \%cust_pay );
768     my $error = $cust_pay->insert;
769
770     if ( $error ) {
771       $dbh->rollback if $oldAutoCommit;
772       return "can't insert payment for $line: $error";
773     }
774
775     if ( $format eq 'simple' ) {
776       # include agentnum for less surprise?
777       $cust_main = qsearchs({
778                              'table'     => 'cust_main',
779                              'hashref'   => { 'custnum' => $cust_pay->custnum },
780                              'extra_sql' => $extra_sql,
781                            })
782         unless $cust_main;
783
784       unless ( $cust_main ) {
785         $dbh->rollback if $oldAutoCommit;
786         return "can't find customer to which payments apply at line: $line";
787       }
788
789       $error = $cust_main->apply_payments_and_credits;
790       if ( $error ) {
791         $dbh->rollback if $oldAutoCommit;
792         return "can't apply payments to customer for $line: $error";
793       }
794
795     }
796
797     $imported++;
798   }
799
800   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
801
802   return "Empty file!" unless $imported;
803
804   ''; #no error
805
806 }
807
808 =back
809
810 =head1 BUGS
811
812 Delete and replace methods.  
813
814 =head1 SEE ALSO
815
816 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
817 schema.html from the base documentation.
818
819 =cut
820
821 1;
822