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