d2de5f80a05938e474de72c17c06932fcf308ffd
[freeside.git] / FS / FS / cust_pay.pm
1 package FS::cust_pay;
2
3 use strict;
4 use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin
5              FS::Record );
6 use vars qw( $DEBUG $me $conf @encrypted_fields
7              $unsuspendauto $ignore_noapply 
8            );
9 use Date::Format;
10 use Business::CreditCard;
11 use Text::Template;
12 use FS::UID qw( getotaker driver_name );
13 use FS::Misc qw( send_email );
14 use FS::Misc::DateTime qw( parse_datetime ); #for batch_import
15 use FS::Record qw( dbh qsearch qsearchs );
16 use FS::CurrentUser;
17 use FS::payby;
18 use FS::cust_main_Mixin;
19 use FS::payinfo_transaction_Mixin;
20 use FS::cust_bill;
21 use FS::cust_bill_pay;
22 use FS::cust_pay_refund;
23 use FS::cust_main;
24 use FS::cust_pkg;
25 use FS::cust_pay_void;
26 use FS::upgrade_journal;
27 use FS::Cursor;
28
29 $DEBUG = 0;
30
31 $me = '[FS::cust_pay]';
32
33 $ignore_noapply = 0;
34
35 #ask FS::UID to run this stuff for us later
36 FS::UID->install_callback( sub { 
37   $conf = new FS::Conf;
38   $unsuspendauto = $conf->exists('unsuspendauto');
39 } );
40
41 @encrypted_fields = ('payinfo');
42 sub nohistory_fields { ('payinfo'); }
43
44 =head1 NAME
45
46 FS::cust_pay - Object methods for cust_pay objects
47
48 =head1 SYNOPSIS
49
50   use FS::cust_pay;
51
52   $record = new FS::cust_pay \%hash;
53   $record = new FS::cust_pay { 'column' => 'value' };
54
55   $error = $record->insert;
56
57   $error = $new_record->replace($old_record);
58
59   $error = $record->delete;
60
61   $error = $record->check;
62
63 =head1 DESCRIPTION
64
65 An FS::cust_pay object represents a payment; the transfer of money from a
66 customer.  FS::cust_pay inherits from FS::Record.  The following fields are
67 currently supported:
68
69 =over 4
70
71 =item paynum
72
73 primary key (assigned automatically for new payments)
74
75 =item custnum
76
77 customer (see L<FS::cust_main>)
78
79 =item _date
80
81 specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
82 L<Time::Local> and L<Date::Parse> for conversion functions.
83
84 =item paid
85
86 Amount of this payment
87
88 =item usernum
89
90 order taker (see L<FS::access_user>)
91
92 =item payby
93
94 Payment Type (See L<FS::payinfo_Mixin> for valid values)
95
96 =item payinfo
97
98 Payment Information (See L<FS::payinfo_Mixin> for data format)
99
100 =item paymask
101
102 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
103
104 =item paybatch
105
106 obsolete text field for tracking card processing or other batch grouping
107
108 =item payunique
109
110 Optional unique identifer to prevent duplicate transactions.
111
112 =item closed
113
114 books closed flag, empty or `Y'
115
116 =item pkgnum
117
118 Desired pkgnum when using experimental package balances.
119
120 =item bank
121
122 The bank where the payment was deposited.
123
124 =item depositor
125
126 The name of the depositor.
127
128 =item account
129
130 The deposit account number.
131
132 =item teller
133
134 The teller number.
135
136 =item batchnum
137
138 The number of the batch this payment came from (see L<FS::pay_batch>), 
139 or null if it was processed through a realtime gateway or entered manually.
140
141 =item gatewaynum
142
143 The number of the realtime or batch gateway L<FS::payment_gateway>) this 
144 payment was processed through.  Null if it was entered manually or processed
145 by the "system default" gateway, which doesn't have a number.
146
147 =item processor
148
149 The name of the processor module (Business::OnlinePayment, ::BatchPayment, 
150 or ::OnlineThirdPartyPayment subclass) used for this payment.  Slightly
151 redundant with C<gatewaynum>.
152
153 =item auth
154
155 The authorization number returned by the credit card network.
156
157 =item order_number
158
159 The transaction ID returned by the gateway, if any.  This is usually what 
160 you would use to initiate a void or refund of the payment.
161
162 =back
163
164 =head1 METHODS
165
166 =over 4 
167
168 =item new HASHREF
169
170 Creates a new payment.  To add the payment to the databse, see L<"insert">.
171
172 =cut
173
174 sub table { 'cust_pay'; }
175 sub cust_linked { $_[0]->cust_main_custnum; } 
176 sub cust_unlinked_msg {
177   my $self = shift;
178   "WARNING: can't find cust_main.custnum ". $self->custnum.
179   ' (cust_pay.paynum '. $self->paynum. ')';
180 }
181
182 =item insert [ OPTION => VALUE ... ]
183
184 Adds this payment to the database.
185
186 For backwards-compatibility and convenience, if the additional field invnum
187 is defined, an FS::cust_bill_pay record for the full amount of the payment
188 will be created.  In this case, custnum is optional.
189
190 If the additional field discount_term is defined then a prepayment discount
191 is taken for that length of time.  It is an error for the customer to owe
192 after this payment is made.
193
194 A hash of optional arguments may be passed.  Currently "manual" is supported.
195 If true, a payment receipt is sent instead of a statement when
196 'payment_receipt_email' configuration option is set.
197
198 About the "manual" flag: Normally, if the 'payment_receipt' config option 
199 is set, and the customer has an invoice email address, inserting a payment
200 causes a I<statement> to be emailed to the customer.  If the payment is 
201 considered "manual" (or if the customer has no invoices), then it will 
202 instead send a I<payment receipt>.  "manual" should be true whenever a 
203 payment is created directly from the web interface, from a user-initiated
204 realtime payment, or from a third-party payment via self-service.  It should
205 be I<false> when creating a payment from a billing event or from a batch.
206
207 =cut
208
209 sub insert {
210   my($self, %options) = @_;
211
212   local $SIG{HUP} = 'IGNORE';
213   local $SIG{INT} = 'IGNORE';
214   local $SIG{QUIT} = 'IGNORE';
215   local $SIG{TERM} = 'IGNORE';
216   local $SIG{TSTP} = 'IGNORE';
217   local $SIG{PIPE} = 'IGNORE';
218
219   my $oldAutoCommit = $FS::UID::AutoCommit;
220   local $FS::UID::AutoCommit = 0;
221   my $dbh = dbh;
222
223   my $cust_bill;
224   if ( $self->invnum ) {
225     $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
226       or do {
227         $dbh->rollback if $oldAutoCommit;
228         return "Unknown cust_bill.invnum: ". $self->invnum;
229       };
230     if ($self->custnum && ($cust_bill->custnum ne $self->custnum)) {
231       $dbh->rollback if $oldAutoCommit;
232       return "Invoice custnum ".$cust_bill->custnum
233         ." does not match specified custnum ".$self->custnum
234         ." for invoice ".$self->invnum;
235     }
236     $self->custnum($cust_bill->custnum );
237   }
238
239   my $error = $self->check;
240   return $error if $error;
241
242   my $cust_main = $self->cust_main;
243   my $old_balance = $cust_main->balance;
244
245   $error = $self->SUPER::insert;
246   if ( $error ) {
247     $dbh->rollback if $oldAutoCommit;
248     return "error inserting cust_pay: $error";
249   }
250
251   if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
252     if ( my $months = $self->discount_term ) {
253       # XXX this should be moved out somewhere, but discount_term_values
254       # doesn't fit right
255       my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
256       return "can't accept prepayment for an unbilled customer" if !$cust_bill;
257
258       # %billing_pkgs contains this customer's active monthly packages. 
259       # Recurring fees for those packages will be credited and then rebilled 
260       # for the full discount term.  Other packages on the last invoice 
261       # (canceled, non-monthly recurring, or one-time charges) will be 
262       # left as they are.
263       my %billing_pkgs = map { $_->pkgnum => $_ } 
264                          grep { $_->part_pkg->freq eq '1' } 
265                          $cust_main->billing_pkgs;
266       my $credit = 0; # sum of recurring charges from that invoice
267       my $last_bill_date = 0; # the real bill date
268       foreach my $item ( $cust_bill->cust_bill_pkg ) {
269         next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
270         $credit += $item->recur;
271         $last_bill_date = $item->cust_pkg->last_bill 
272           if defined($item->cust_pkg) 
273             and $item->cust_pkg->last_bill > $last_bill_date
274       }
275
276       my $cust_credit = new FS::cust_credit {
277         'custnum' => $self->custnum,
278         'amount'  => sprintf('%.2f', $credit),
279         'reason'  => 'customer chose to prepay for discount',
280       };
281       $error = $cust_credit->insert('reason_type' => $credit_type);
282       if ( $error ) {
283         $dbh->rollback if $oldAutoCommit;
284         return "error inserting prepayment credit: $error";
285       }
286       # don't apply it yet
287
288       # bill for the entire term
289       $_->bill($_->last_bill) foreach (values %billing_pkgs);
290       $error = $cust_main->bill(
291         # no recurring_only, we want unbilled packages with start dates to 
292         # get billed
293         'no_usage_reset' => 1,
294         'time'           => $last_bill_date, # not $cust_bill->_date
295         'pkg_list'       => [ values %billing_pkgs ],
296         'freq_override'  => $months,
297       );
298       if ( $error ) {
299         $dbh->rollback if $oldAutoCommit;
300         return "error inserting cust_pay: $error";
301       }
302       $error = $cust_main->apply_payments_and_credits;
303       if ( $error ) {
304         $dbh->rollback if $oldAutoCommit;
305         return "error inserting cust_pay: $error";
306       }
307       my $new_balance = $cust_main->balance;
308       if ($new_balance > 0) {
309         $dbh->rollback if $oldAutoCommit;
310         return "balance after prepay discount attempt: $new_balance";
311       }
312       # user friendly: override the "apply only to this invoice" mode
313       $self->invnum('');
314       
315     }
316
317   }
318
319   if ( $self->invnum ) {
320     my $cust_bill_pay = new FS::cust_bill_pay {
321       'invnum' => $self->invnum,
322       'paynum' => $self->paynum,
323       'amount' => $self->paid,
324       '_date'  => $self->_date,
325     };
326     $error = $cust_bill_pay->insert(%options);
327     if ( $error ) {
328       if ( $ignore_noapply ) {
329         warn "warning: error inserting cust_bill_pay: $error ".
330              "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
331       } else {
332         $dbh->rollback if $oldAutoCommit;
333         return "error inserting cust_bill_pay: $error";
334       }
335     }
336   }
337
338   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
339
340   #false laziness w/ cust_credit::insert
341   if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
342     my @errors = $cust_main->unsuspend;
343     #return 
344     # side-fx with nested transactions?  upstack rolls back?
345     warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
346          join(' / ', @errors)
347       if @errors;
348   }
349   #eslaf
350
351   #bill setup fees for voip_cdr bill_every_call packages
352   #some false laziness w/search in freeside-cdrd
353   my $addl_from =
354     'LEFT JOIN part_pkg USING ( pkgpart ) '.
355     "LEFT JOIN part_pkg_option
356        ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
357             AND part_pkg_option.optionname = 'bill_every_call' )";
358
359   my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
360                   " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
361
362   my @cust_pkg = qsearch({
363     'table'     => 'cust_pkg',
364     'addl_from' => $addl_from,
365     'hashref'   => { 'custnum' => $self->custnum,
366                      'susp'    => '',
367                      'cancel'  => '',
368                    },
369     'extra_sql' => $extra_sql,
370   });
371
372   if ( @cust_pkg ) {
373     warn "voip_cdr bill_every_call packages found; billing customer\n";
374     my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
375     if ( $bill_error ) {
376       warn "WARNING: Error billing customer: $bill_error\n";
377     }
378   }
379   #end of billing setup fees for voip_cdr bill_every_call packages
380
381   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
382
383   #payment receipt
384   my $trigger = $conf->config('payment_receipt-trigger', 
385                               $self->cust_main->agentnum) || 'cust_pay';
386   if ( $trigger eq 'cust_pay' ) {
387     my $error = $self->send_receipt(
388       'manual'    => $options{'manual'},
389       'cust_bill' => $cust_bill,
390       'cust_main' => $cust_main,
391     );
392     warn "can't send payment receipt/statement: $error" if $error;
393   }
394
395   '';
396
397 }
398
399 =item void [ REASON ]
400
401 Voids this payment: deletes the payment and all associated applications and
402 adds a record of the voided payment to the FS::cust_pay_void table.
403
404 =cut
405
406 sub void {
407   my $self = shift;
408
409   local $SIG{HUP} = 'IGNORE';
410   local $SIG{INT} = 'IGNORE';
411   local $SIG{QUIT} = 'IGNORE';
412   local $SIG{TERM} = 'IGNORE';
413   local $SIG{TSTP} = 'IGNORE';
414   local $SIG{PIPE} = 'IGNORE';
415
416   my $oldAutoCommit = $FS::UID::AutoCommit;
417   local $FS::UID::AutoCommit = 0;
418   my $dbh = dbh;
419
420   my $cust_pay_void = new FS::cust_pay_void ( {
421     map { $_ => $self->get($_) } $self->fields
422   } );
423   $cust_pay_void->reason(shift) if scalar(@_);
424   my $error = $cust_pay_void->insert;
425
426   my $cust_pay_pending =
427     qsearchs('cust_pay_pending', { paynum => $self->paynum });
428   if ( $cust_pay_pending ) {
429     $cust_pay_pending->set('void_paynum', $self->paynum);
430     $cust_pay_pending->set('paynum', '');
431     $error ||= $cust_pay_pending->replace;
432   }
433
434   $error ||= $self->delete;
435
436   if ( $error ) {
437     $dbh->rollback if $oldAutoCommit;
438     return $error;
439   }
440
441   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
442
443   '';
444
445 }
446
447 =item delete
448
449 Unless the closed flag is set, deletes this payment and all associated
450 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>).  In most
451 cases, you want to use the void method instead to leave a record of the
452 deleted payment.
453
454 =cut
455
456 # very similar to FS::cust_credit::delete
457 sub delete {
458   my $self = shift;
459   return "Can't delete closed payment" if $self->closed =~ /^Y/i;
460
461   local $SIG{HUP} = 'IGNORE';
462   local $SIG{INT} = 'IGNORE';
463   local $SIG{QUIT} = 'IGNORE';
464   local $SIG{TERM} = 'IGNORE';
465   local $SIG{TSTP} = 'IGNORE';
466   local $SIG{PIPE} = 'IGNORE';
467
468   my $oldAutoCommit = $FS::UID::AutoCommit;
469   local $FS::UID::AutoCommit = 0;
470   my $dbh = dbh;
471
472   foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
473     my $error = $app->delete;
474     if ( $error ) {
475       $dbh->rollback if $oldAutoCommit;
476       return $error;
477     }
478   }
479
480   my $error = $self->SUPER::delete(@_);
481   if ( $error ) {
482     $dbh->rollback if $oldAutoCommit;
483     return $error;
484   }
485
486   if (    $conf->exists('deletepayments')
487        && $conf->config('deletepayments') ne '' ) {
488
489     my $cust_main = $self->cust_main;
490
491     my $error = send_email(
492       'from'    => $conf->config('invoice_from', $self->cust_main->agentnum),
493                                  #invoice_from??? well as good as any
494       'to'      => $conf->config('deletepayments'),
495       'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
496       'body'    => [
497         "This is an automatic message from your Freeside installation\n",
498         "informing you that the following payment has been deleted:\n",
499         "\n",
500         'paynum: '. $self->paynum. "\n",
501         'custnum: '. $self->custnum.
502           " (". $cust_main->last. ", ". $cust_main->first. ")\n",
503         'paid: $'. sprintf("%.2f", $self->paid). "\n",
504         'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
505         'payby: '. $self->payby. "\n",
506         'payinfo: '. $self->paymask. "\n",
507         'paybatch: '. $self->paybatch. "\n",
508       ],
509     );
510
511     if ( $error ) {
512       $dbh->rollback if $oldAutoCommit;
513       return "can't send payment deletion notification: $error";
514     }
515
516   }
517
518   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
519
520   '';
521
522 }
523
524 =item replace [ OLD_RECORD ]
525
526 You can, but probably shouldn't modify payments...
527
528 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
529 supplied, replaces this record.  If there is an error, returns the error,
530 otherwise returns false.
531
532 =cut
533
534 sub replace {
535   my $self = shift;
536   return "Can't modify closed payment" if $self->closed =~ /^Y/i;
537   $self->SUPER::replace(@_);
538 }
539
540 =item check
541
542 Checks all fields to make sure this is a valid payment.  If there is an error,
543 returns the error, otherwise returns false.  Called by the insert method.
544
545 =cut
546
547 sub check {
548   my $self = shift;
549
550   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
551
552   my $error =
553     $self->ut_numbern('paynum')
554     || $self->ut_numbern('custnum')
555     || $self->ut_numbern('_date')
556     || $self->ut_money('paid')
557     || $self->ut_alphan('otaker')
558     || $self->ut_textn('paybatch')
559     || $self->ut_textn('payunique')
560     || $self->ut_enum('closed', [ '', 'Y' ])
561     || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
562     || $self->ut_textn('bank')
563     || $self->ut_alphan('depositor')
564     || $self->ut_numbern('account')
565     || $self->ut_numbern('teller')
566     || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
567     || $self->payinfo_check()
568   ;
569   return $error if $error;
570
571   return "paid must be > 0 " if $self->paid <= 0;
572
573   return "unknown cust_main.custnum: ". $self->custnum
574     unless $self->invnum
575            || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
576
577   $self->_date(time) unless $self->_date;
578
579   return "invalid discount_term"
580    if ($self->discount_term && $self->discount_term < 2);
581
582   if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
583     foreach (qw(bank depositor account teller)) {
584       return "$_ required" if $self->get($_) eq '';
585     }
586   }
587
588 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
589 #  # UNIQUE index should catch this too, without race conditions, but this
590 #  # should give a better error message the other 99.9% of the time...
591 #  if ( length($self->payunique)
592 #       && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
593 #    #well, it *could* be a better error message
594 #    return "duplicate transaction".
595 #           " - a payment with unique identifer ". $self->payunique.
596 #           " already exists";
597 #  }
598
599   $self->SUPER::check;
600 }
601
602 =item send_receipt HASHREF | OPTION => VALUE ...
603
604 Sends a payment receipt for this payment..
605
606 Available options:
607
608 =over 4
609
610 =item manual
611
612 Flag indicating the payment is being made manually.
613
614 =item cust_bill
615
616 Invoice (FS::cust_bill) object.  If not specified, the most recent invoice
617 will be assumed.
618
619 =item cust_main
620
621 Customer (FS::cust_main) object (for efficiency).
622
623 =back
624
625 =cut
626
627 sub send_receipt {
628   my $self = shift;
629   my $opt = ref($_[0]) ? shift : { @_ };
630
631   my $cust_bill = $opt->{'cust_bill'};
632   my $cust_main = $opt->{'cust_main'} || $self->cust_main;
633
634   my $conf = new FS::Conf;
635
636   return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
637
638   my @invoicing_list = $cust_main->invoicing_list_emailonly;
639   return '' unless @invoicing_list;
640
641   $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
642
643   my $error = '';
644
645   if (    ( exists($opt->{'manual'}) && $opt->{'manual'} )
646        #|| ! $conf->exists('invoice_html_statement')
647        || ! $cust_bill
648      )
649   {
650     my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
651     if ( $msgnum ) {
652
653       my %substitutions = ();
654       $substitutions{invnum} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
655
656       my $queue = new FS::queue {
657         'job'     => 'FS::Misc::process_send_email',
658         'paynum'  => $self->paynum,
659         'custnum' => $cust_main->custnum,
660       };
661       $error = $queue->insert(
662         FS::msg_template->by_key($msgnum)->prepare(
663           'cust_main'     => $cust_main,
664           'object'        => $self,
665           'from_config'   => 'payment_receipt_from',
666           'substitutions' => \%substitutions,
667         ),
668         'msgtype' => 'receipt', # override msg_template's default
669       );
670
671     } elsif ( $conf->exists('payment_receipt_email') ) {
672
673       my $receipt_template = new Text::Template (
674         TYPE   => 'ARRAY',
675         SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
676       ) or do {
677         warn "can't create payment receipt template: $Text::Template::ERROR";
678         return '';
679       };
680
681       my $payby = $self->payby;
682       my $payinfo = $self->payinfo;
683       $payby =~ s/^BILL$/Check/ if $payinfo;
684       if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
685         $payinfo = $self->paymask
686       } else {
687         $payinfo = $self->decrypt($payinfo);
688       }
689       $payby =~ s/^CHEK$/Electronic check/;
690
691       my %fill_in = (
692         'date'         => time2str("%a %B %o, %Y", $self->_date),
693         'name'         => $cust_main->name,
694         'paynum'       => $self->paynum,
695         'paid'         => sprintf("%.2f", $self->paid),
696         'payby'        => ucfirst(lc($payby)),
697         'payinfo'      => $payinfo,
698         'balance'      => $cust_main->balance,
699         'company_name' => $conf->config('company_name', $cust_main->agentnum),
700       );
701
702       $fill_in{'invnum'} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
703
704       if ( $opt->{'cust_pkg'} ) {
705         $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
706         #setup date, other things?
707       }
708
709       my $queue = new FS::queue {
710         'job'     => 'FS::Misc::process_send_generated_email',
711         'paynum'  => $self->paynum,
712         'custnum' => $cust_main->custnum,
713         'msgtype' => 'receipt',
714       };
715       $error = $queue->insert(
716         'from'    => $conf->invoice_from_full( $cust_main->agentnum ),
717                                    #invoice_from??? well as good as any
718         'to'      => \@invoicing_list,
719         'subject' => 'Payment receipt',
720         'body'    => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
721       );
722
723     } else {
724
725       warn "payment_receipt is on, but no payment_receipt_msgnum\n";
726
727     }
728
729   } elsif ( ! $cust_main->invoice_noemail ) { #not manual
730
731     my $queue = new FS::queue {
732        'job'     => 'FS::cust_bill::queueable_email',
733        'paynum'  => $self->paynum,
734        'custnum' => $cust_main->custnum,
735     };
736
737     my %opt = (
738       'invnum'      => $cust_bill->invnum,
739       'no_coupon'   => 1,
740     );
741
742     if ( my $mode = $conf->config('payment_receipt_statement_mode') ) {
743       $opt{'mode'} = $mode;
744     } else {
745       # backward compatibility, no good fix for this yet as some people may
746       # still have "invoice_latex_statement" and such options
747       $opt{'template'} = 'statement';
748       $opt{'notice_name'} = 'Statement';
749     }
750
751     $error = $queue->insert(%opt);
752
753   }
754   
755   warn "send_receipt: $error\n" if $error;
756 }
757
758 =item cust_bill_pay
759
760 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
761 payment.
762
763 =cut
764
765 sub cust_bill_pay {
766   my $self = shift;
767   map { $_ } #return $self->num_cust_bill_pay unless wantarray;
768   sort {    $a->_date  <=> $b->_date
769          || $a->invnum <=> $b->invnum }
770     qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
771   ;
772 }
773
774 =item cust_pay_refund
775
776 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
777 payment.
778
779 =cut
780
781 sub cust_pay_refund {
782   my $self = shift;
783   map { $_ } #return $self->num_cust_pay_refund unless wantarray;
784   sort { $a->_date <=> $b->_date }
785     qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
786   ;
787 }
788
789
790 =item unapplied
791
792 Returns the amount of this payment that is still unapplied; which is
793 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
794 applications (see L<FS::cust_pay_refund>).
795
796 =cut
797
798 sub unapplied {
799   my $self = shift;
800   my $amount = $self->paid;
801   $amount -= $_->amount foreach ( $self->cust_bill_pay );
802   $amount -= $_->amount foreach ( $self->cust_pay_refund );
803   sprintf("%.2f", $amount );
804 }
805
806 =item unrefunded
807
808 Returns the amount of this payment that has not been refuned; which is
809 paid minus all  refund applications (see L<FS::cust_pay_refund>).
810
811 =cut
812
813 sub unrefunded {
814   my $self = shift;
815   my $amount = $self->paid;
816   $amount -= $_->amount foreach ( $self->cust_pay_refund );
817   sprintf("%.2f", $amount );
818 }
819
820 =item amount
821
822 Returns the "paid" field.
823
824 =cut
825
826 sub amount {
827   my $self = shift;
828   $self->paid();
829 }
830
831 =back
832
833 =head1 CLASS METHODS
834
835 =over 4
836
837 =item batch_insert CUST_PAY_OBJECT, ...
838
839 Class method which inserts multiple payments.  Takes a list of FS::cust_pay
840 objects.  Returns a list, each element representing the status of inserting the
841 corresponding payment - empty.  If there is an error inserting any payment, the
842 entire transaction is rolled back, i.e. all payments are inserted or none are.
843
844 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a 
845 reference to an array of (uninserted) FS::cust_bill_pay objects.  If so,
846 those objects will be inserted with the paynum of the payment, and for 
847 each one, an error message or an empty string will be inserted into the 
848 list of errors.
849
850 For example:
851
852   my @errors = FS::cust_pay->batch_insert(@cust_pay);
853   my $num_errors = scalar(grep $_, @errors);
854   if ( $num_errors == 0 ) {
855     #success; all payments were inserted
856   } else {
857     #failure; no payments were inserted.
858   }
859
860 =cut
861
862 sub batch_insert {
863   my $self = shift; #class method
864
865   local $SIG{HUP} = 'IGNORE';
866   local $SIG{INT} = 'IGNORE';
867   local $SIG{QUIT} = 'IGNORE';
868   local $SIG{TERM} = 'IGNORE';
869   local $SIG{TSTP} = 'IGNORE';
870   local $SIG{PIPE} = 'IGNORE';
871
872   my $oldAutoCommit = $FS::UID::AutoCommit;
873   local $FS::UID::AutoCommit = 0;
874   my $dbh = dbh;
875
876   my $num_errors = 0;
877   
878   my @errors;
879   foreach my $cust_pay (@_) {
880     my $error = $cust_pay->insert( 'manual' => 1 );
881     push @errors, $error;
882     $num_errors++ if $error;
883
884     if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
885
886       foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
887         if ( $error ) { # insert placeholders if cust_pay wasn't inserted
888           push @errors, '';
889         }
890         else {
891           $cust_bill_pay->set('paynum', $cust_pay->paynum);
892           my $apply_error = $cust_bill_pay->insert;
893           push @errors, $apply_error || '';
894           $num_errors++ if $apply_error;
895         }
896       }
897
898     } elsif ( !$error ) { #normal case: apply payments as usual
899       $cust_pay->cust_main->apply_payments;
900     }
901
902   }
903
904   if ( $num_errors ) {
905     $dbh->rollback if $oldAutoCommit;
906   } else {
907     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
908   }
909
910   @errors;
911
912 }
913
914 =item unapplied_sql
915
916 Returns an SQL fragment to retreive the unapplied amount.
917
918 =cut 
919
920 sub unapplied_sql {
921   my ($class, $start, $end) = @_;
922   my $bill_start   = $start ? "AND cust_bill_pay._date <= $start"   : '';
923   my $bill_end     = $end   ? "AND cust_bill_pay._date > $end"     : '';
924   my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
925   my $refund_end   = $end   ? "AND cust_pay_refund._date > $end"   : '';
926
927   "paid
928         - COALESCE( 
929                     ( SELECT SUM(amount) FROM cust_bill_pay
930                         WHERE cust_pay.paynum = cust_bill_pay.paynum
931                         $bill_start $bill_end )
932                     ,0
933                   )
934         - COALESCE(
935                     ( SELECT SUM(amount) FROM cust_pay_refund
936                         WHERE cust_pay.paynum = cust_pay_refund.paynum
937                         $refund_start $refund_end )
938                     ,0
939                   )
940   ";
941
942 }
943
944 # _upgrade_data
945 #
946 # Used by FS::Upgrade to migrate to a new database.
947
948 use FS::h_cust_pay;
949
950 sub _upgrade_data {  #class method
951   my ($class, %opt) = @_;
952
953   warn "$me upgrading $class\n" if $DEBUG;
954
955   local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
956
957   ##
958   # otaker/ivan upgrade
959   ##
960
961   unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
962
963     #not the most efficient, but hey, it only has to run once
964
965     my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
966                 "  AND usernum IS NULL ".
967                 "  AND 0 < ( SELECT COUNT(*) FROM cust_main                 ".
968                 "              WHERE cust_main.custnum = cust_pay.custnum ) ";
969
970     my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
971
972     my $sth = dbh->prepare($count_sql) or die dbh->errstr;
973     $sth->execute or die $sth->errstr;
974     my $total = $sth->fetchrow_arrayref->[0];
975     #warn "$total cust_pay records to update\n"
976     #  if $DEBUG;
977     local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
978
979     my $count = 0;
980     my $lastprog = 0;
981
982     my @cust_pay = qsearch( {
983         'table'     => 'cust_pay',
984         'hashref'   => {},
985         'extra_sql' => $where,
986         'order_by'  => 'ORDER BY paynum',
987     } );
988
989     foreach my $cust_pay (@cust_pay) {
990
991       my $h_cust_pay = $cust_pay->h_search('insert');
992       if ( $h_cust_pay ) {
993         next if $cust_pay->otaker eq $h_cust_pay->history_user;
994         #$cust_pay->otaker($h_cust_pay->history_user);
995         $cust_pay->set('otaker', $h_cust_pay->history_user);
996       } else {
997         $cust_pay->set('otaker', 'legacy');
998       }
999
1000       delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1001       my $error = $cust_pay->replace;
1002
1003       if ( $error ) {
1004         warn " *** WARNING: Error updating order taker for payment paynum ".
1005              $cust_pay->paynun. ": $error\n";
1006         next;
1007       }
1008
1009       $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1010
1011       $count++;
1012       if ( $DEBUG > 1 && $lastprog + 30 < time ) {
1013         warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
1014         $lastprog = time;
1015       }
1016
1017     }
1018
1019     FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
1020   }
1021
1022   ###
1023   # payinfo N/A upgrade
1024   ###
1025
1026   unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
1027
1028     #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
1029
1030     my @na_cust_pay = qsearch( {
1031       'table'     => 'cust_pay',
1032       'hashref'   => {}, #could be encrypted# { 'payinfo' => 'N/A' },
1033       'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
1034     } );
1035
1036     foreach my $na ( @na_cust_pay ) {
1037
1038       next unless $na->payinfo eq 'N/A';
1039
1040       my $cust_pay_pending =
1041         qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1042       unless ( $cust_pay_pending ) {
1043         warn " *** WARNING: not-yet recoverable N/A card for payment ".
1044              $na->paynum. " (no cust_pay_pending)\n";
1045         next;
1046       }
1047       $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1048       my $error = $na->replace;
1049       if ( $error ) {
1050         warn " *** WARNING: Error updating payinfo for payment paynum ".
1051              $na->paynun. ": $error\n";
1052         next;
1053       }
1054
1055     }
1056
1057     FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1058   }
1059
1060   ###
1061   # otaker->usernum upgrade
1062   ###
1063
1064   delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1065   $class->_upgrade_otaker(%opt);
1066   $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1067
1068   # if we do this anywhere else, it should become an FS::Upgrade method
1069   my $num_to_upgrade = $class->count('paybatch is not null');
1070   my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1071   if ( $num_to_upgrade > 0 ) {
1072     warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1073     if ( $opt{queue} ) {
1074       if ( $num_jobs > 0 ) {
1075         warn "Upgrade already queued.\n";
1076       } else {
1077         warn "Scheduling upgrade.\n";
1078         my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1079         $job->insert;
1080       }
1081     } else {
1082       process_upgrade_paybatch();
1083     }
1084   }
1085 }
1086
1087 sub process_upgrade_paybatch {
1088   my $dbh = dbh;
1089   local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1090   local $FS::UID::AutoCommit = 1;
1091
1092   ###
1093   # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1094   ###
1095   my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
1096   my $search = FS::Cursor->new( {
1097     'table'     => 'cust_pay',
1098     'addl_from' => " JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS $text) ",
1099   } );
1100   while (my $cust_pay = $search->fetch) {
1101     $cust_pay->set('batchnum' => $cust_pay->paybatch);
1102     $cust_pay->set('paybatch' => '');
1103     my $error = $cust_pay->replace;
1104     warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n  $error"
1105     if $error;
1106   }
1107
1108   ###
1109   # migrate gateway info from the misused 'paybatch' field
1110   ###
1111
1112   # not only cust_pay, but also voided and refunded payments
1113   if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1114     local $FS::Record::nowarn_classload=1;
1115     # really inefficient, but again, only has to run once
1116     foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1117       my $and_batchnum_is_null =
1118         ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1119       my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1120       my $search = FS::Cursor->new({
1121         table     => $table,
1122         extra_sql => "WHERE payby IN('CARD','CHEK') ".
1123                      "AND (paybatch IS NOT NULL ".
1124                      "OR (paybatch IS NULL AND auth IS NULL
1125                      $and_batchnum_is_null ) )
1126                      ORDER BY $pkey DESC"
1127       });
1128       while ( my $object = $search->fetch ) {
1129         if ( $object->paybatch eq '' ) {
1130           # repair for a previous upgrade that didn't save 'auth'
1131           my $pkey = $object->primary_key;
1132           # find the last history record that had a paybatch value
1133           my $h = qsearchs({
1134               table   => "h_$table",
1135               hashref => {
1136                 $pkey     => $object->$pkey,
1137                 paybatch  => { op=>'!=', value=>''},
1138                 history_action => 'replace_old',
1139               },
1140               order_by => 'ORDER BY history_date DESC LIMIT 1',
1141           });
1142           if (!$h) {
1143             warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1144             next;
1145           }
1146           # if the paybatch didn't have an auth string, then it's fine
1147           $h->paybatch =~ /:(\w+):/ or next;
1148           # set paybatch to what it was in that record
1149           $object->set('paybatch', $h->paybatch)
1150           # and then upgrade it like the old records
1151         }
1152
1153         my $parsed = $object->_parse_paybatch;
1154         if (keys %$parsed) {
1155           $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1156           $object->set('auth' => $parsed->{authorization});
1157           $object->set('paybatch', '');
1158           my $error = $object->replace;
1159           warn "error parsing CARD/CHEK paybatch fields on $object #".
1160             $object->get($object->primary_key).":\n  $error\n"
1161             if $error;
1162         }
1163       } #$object
1164     } #$table
1165     FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1166   }
1167 }
1168
1169 =back
1170
1171 =head1 SUBROUTINES
1172
1173 =over 4 
1174
1175 =item process_batch_import
1176
1177 =cut
1178
1179 sub process_batch_import {
1180   my $job = shift;
1181
1182   my $hashcb = sub {
1183     my %hash = @_;
1184     my $custnum = $hash{'custnum'};
1185     my $agentnum = $hash{'agentnum'};
1186     my $agent_custid = $hash{'agent_custid'};
1187     #standardize date
1188     $hash{'_date'} = parse_datetime($hash{'_date'})
1189       if $hash{'_date'} && $hash{'_date'} =~ /\D/;
1190     #remove custnum_prefix
1191     my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1192     my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1193     if (
1194       $custnum_prefix 
1195       && $custnum =~ /^$custnum_prefix(0*([1-9]\d*))$/
1196       && length($1) == $custnum_length 
1197     ) {
1198       $custnum = $2;
1199     }
1200     # check agentnum against custnum and
1201     # translate agent_custid into regular custnum
1202     if ($custnum && $agent_custid) {
1203       die "can't specify both custnum and agent_custid\n";
1204     } elsif ($agentnum || $agent_custid) {
1205       # here is the agent virtualization
1206       my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1207       my %search;
1208       $search{'agentnum'} = $agentnum
1209         if $agentnum;
1210       $search{'agent_custid'} = $agent_custid
1211         if $agent_custid;
1212       $search{'custnum'} = $custnum
1213         if $custnum;
1214       my $cust_main = qsearchs({
1215         'table'     => 'cust_main',
1216         'hashref'   => \%search,
1217         'extra_sql' => $extra_sql,
1218       });
1219       die "can't find customer with" .
1220         ($agentnum ? " agentnum $agentnum" : '') .
1221         ($custnum  ? " custnum $custnum" : '') .
1222         ($agent_custid ? " agent_custid $agent_custid" : '') . "\n"
1223         unless $cust_main;
1224       die "mismatched customer number\n"
1225         if $custnum && ($custnum ne $cust_main->custnum);
1226       $custnum = $cust_main->custnum;
1227     }
1228     $hash{'custnum'} = $custnum;
1229     delete($hash{'agent_custid'});
1230     return %hash;
1231   };
1232
1233   my $opt = { 'table'   => 'cust_pay',
1234               'params'  => [ '_date', 'agentnum', 'payby', 'paybatch' ],
1235                                          #agent_custid isn't a cust_pay field, see hash callback
1236               'formats' => { 'simple' => [ qw(custnum agent_custid paid payinfo invnum) ] },
1237               'format_types' => { 'simple' => '' }, #force infer from file extension
1238               'default_csv' => 1, #if it's not .xls, it'll read as csv, regardless of extension
1239               'format_hash_callbacks' => { 'simple' => $hashcb },
1240               'postinsert_callback' => sub {
1241                  my $cust_pay = shift;
1242                  my $cust_main = $cust_pay->cust_main ||
1243                    return "can't find customer to which payments apply";
1244                  my $error = $cust_main->apply_payments_and_credits;
1245                  return $error
1246                    ? "can't apply payments to customer ".$cust_pay->custnum."$error"
1247                    : '';
1248               },
1249             };
1250
1251   FS::Record::process_batch_import( $job, $opt, @_ );
1252
1253 }
1254
1255 =item batch_import HASHREF
1256
1257 Inserts new payments.
1258
1259 =cut
1260
1261 sub batch_import {
1262   my $param = shift;
1263
1264   my $fh       = $param->{filehandle};
1265   my $format   = $param->{'format'};
1266
1267   my $agentnum = $param->{agentnum};
1268   my $_date    = $param->{_date};
1269   $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1270   my $paybatch = $param->{'paybatch'};
1271
1272   my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1273   my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1274
1275   # here is the agent virtualization
1276   my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1277
1278   my @fields;
1279   my $payby;
1280   if ( $format eq 'simple' ) {
1281     @fields = qw( custnum agent_custid paid payinfo invnum );
1282     $payby = 'BILL';
1283   } elsif ( $format eq 'extended' ) {
1284     die "unimplemented\n";
1285     @fields = qw( );
1286     $payby = 'BILL';
1287   } else {
1288     die "unknown format $format";
1289   }
1290
1291   eval "use Text::CSV_XS;";
1292   die $@ if $@;
1293
1294   my $csv = new Text::CSV_XS;
1295
1296   my $imported = 0;
1297
1298   local $SIG{HUP} = 'IGNORE';
1299   local $SIG{INT} = 'IGNORE';
1300   local $SIG{QUIT} = 'IGNORE';
1301   local $SIG{TERM} = 'IGNORE';
1302   local $SIG{TSTP} = 'IGNORE';
1303   local $SIG{PIPE} = 'IGNORE';
1304
1305   my $oldAutoCommit = $FS::UID::AutoCommit;
1306   local $FS::UID::AutoCommit = 0;
1307   my $dbh = dbh;
1308   
1309   my $line;
1310   while ( defined($line=<$fh>) ) {
1311
1312     $csv->parse($line) or do {
1313       $dbh->rollback if $oldAutoCommit;
1314       return "can't parse: ". $csv->error_input();
1315     };
1316
1317     my @columns = $csv->fields();
1318
1319     my %cust_pay = (
1320       payby    => $payby,
1321       paybatch => $paybatch,
1322     );
1323     $cust_pay{_date} = $_date if $_date;
1324
1325     my $cust_main;
1326     foreach my $field ( @fields ) {
1327
1328       if ( $field eq 'agent_custid'
1329         && $agentnum
1330         && $columns[0] =~ /\S+/ )
1331       {
1332
1333         my $agent_custid = $columns[0];
1334         my %hash = ( 'agent_custid' => $agent_custid,
1335                      'agentnum'     => $agentnum,
1336                    );
1337
1338         if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1339           $dbh->rollback if $oldAutoCommit;
1340           return "can't specify custnum with agent_custid $agent_custid";
1341         }
1342
1343         $cust_main = qsearchs({
1344                                 'table'     => 'cust_main',
1345                                 'hashref'   => \%hash,
1346                                 'extra_sql' => $extra_sql,
1347                              });
1348
1349         unless ( $cust_main ) {
1350           $dbh->rollback if $oldAutoCommit;
1351           return "can't find customer with agent_custid $agent_custid";
1352         }
1353
1354         $field = 'custnum';
1355         $columns[0] = $cust_main->custnum;
1356       }
1357
1358       $cust_pay{$field} = shift @columns; 
1359     }
1360
1361     if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1362                          && length($1) == $custnum_length ) {
1363       $cust_pay{custnum} = $2;
1364     }
1365
1366     my $custnum = $cust_pay{custnum};
1367
1368     my $cust_pay = new FS::cust_pay( \%cust_pay );
1369     my $error = $cust_pay->insert;
1370
1371     if ( ! $error && $cust_pay->custnum != $custnum ) {
1372       #invnum was defined, and ->insert set custnum to the customer for that
1373       #invoice, but it wasn't the one the import specified.
1374       $dbh->rollback if $oldAutoCommit;
1375       $error = "specified invoice #". $cust_pay{invnum}.
1376                " is for custnum ". $cust_pay->custnum.
1377                ", not specified custnum $custnum";
1378     }
1379
1380     if ( $error ) {
1381       $dbh->rollback if $oldAutoCommit;
1382       return "can't insert payment for $line: $error";
1383     }
1384
1385     if ( $format eq 'simple' ) {
1386       # include agentnum for less surprise?
1387       $cust_main = qsearchs({
1388                              'table'     => 'cust_main',
1389                              'hashref'   => { 'custnum' => $cust_pay->custnum },
1390                              'extra_sql' => $extra_sql,
1391                            })
1392         unless $cust_main;
1393
1394       unless ( $cust_main ) {
1395         $dbh->rollback if $oldAutoCommit;
1396         return "can't find customer to which payments apply at line: $line";
1397       }
1398
1399       $error = $cust_main->apply_payments_and_credits;
1400       if ( $error ) {
1401         $dbh->rollback if $oldAutoCommit;
1402         return "can't apply payments to customer for $line: $error";
1403       }
1404
1405     }
1406
1407     $imported++;
1408   }
1409
1410   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1411
1412   return "Empty file!" unless $imported;
1413
1414   ''; #no error
1415
1416 }
1417
1418 =back
1419
1420 =head1 BUGS
1421
1422 Delete and replace methods.  
1423
1424 =head1 SEE ALSO
1425
1426 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1427 schema.html from the base documentation.
1428
1429 =cut
1430
1431 1;
1432