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