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