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