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