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