RT# 78131 - added ability to use message template for auto payment receipt and fixed...
[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 $msg_template = qsearchs('msg_template',{ msgnum => $msgnum});
756       unless ($msg_template) {
757         warn "send_receipt could not load msg_template";
758         return;
759       }
760
761       my $cust_msg = $msg_template->prepare(
762           'cust_main'     => $cust_main,
763           'object'        => $self,
764           'from_config'   => 'payment_receipt_from',
765           'substitutions' => \%substitutions,
766           'msgtype'       => 'receipt',
767       );
768       $error = $cust_msg ? $cust_msg->insert : 'error preparing msg_template';
769       if ($error) {
770         warn "send_receipt: $error";
771         return;
772       }
773
774       my $queue = new FS::queue {
775         'job'     => 'FS::cust_msg::process_send',
776         'paynum'  => $self->paynum,
777         'custnum' => $cust_main->custnum,
778       };
779       $error = $queue->insert( $cust_msg->custmsgnum );
780
781     }
782     else {
783       my $queue = new FS::queue {
784         'job'     => 'FS::cust_bill::queueable_email',
785         'paynum'  => $self->paynum,
786         'custnum' => $cust_main->custnum,
787       };
788
789       my %opt = (
790         'invnum'      => $cust_bill->invnum,
791         'no_coupon'   => 1,
792       );
793
794       if ( my $mode = $conf->config('payment_receipt_statement_mode') ) {
795         $opt{'mode'} = $mode;
796       } else {
797         # backward compatibility, no good fix for this yet as some people may
798         # still have "invoice_latex_statement" and such options
799         $opt{'template'} = 'statement';
800         $opt{'notice_name'} = 'Statement';
801       }
802
803       $error = $queue->insert(%opt);
804     }
805
806
807
808   }
809   
810   warn "send_receipt: $error\n" if $error;
811 }
812
813 =item cust_bill_pay
814
815 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
816 payment.
817
818 =cut
819
820 sub cust_bill_pay {
821   my $self = shift;
822   map { $_ } #return $self->num_cust_bill_pay unless wantarray;
823   sort {    $a->_date  <=> $b->_date
824          || $a->invnum <=> $b->invnum }
825     qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
826   ;
827 }
828
829 =item cust_pay_refund
830
831 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
832 payment.
833
834 =cut
835
836 sub cust_pay_refund {
837   my $self = shift;
838   map { $_ } #return $self->num_cust_pay_refund unless wantarray;
839   sort { $a->_date <=> $b->_date }
840     qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
841   ;
842 }
843
844
845 =item unapplied
846
847 Returns the amount of this payment that is still unapplied; which is
848 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
849 applications (see L<FS::cust_pay_refund>).
850
851 =cut
852
853 sub unapplied {
854   my $self = shift;
855   my $amount = $self->paid;
856   $amount -= $_->amount foreach ( $self->cust_bill_pay );
857   $amount -= $_->amount foreach ( $self->cust_pay_refund );
858   sprintf("%.2f", $amount );
859 }
860
861 =item unrefunded
862
863 Returns the amount of this payment that has not been refuned; which is
864 paid minus all  refund applications (see L<FS::cust_pay_refund>).
865
866 =cut
867
868 sub unrefunded {
869   my $self = shift;
870   my $amount = $self->paid;
871   $amount -= $_->amount foreach ( $self->cust_pay_refund );
872   sprintf("%.2f", $amount );
873 }
874
875 =item amount
876
877 Returns the "paid" field.
878
879 =cut
880
881 sub amount {
882   my $self = shift;
883   $self->paid();
884 }
885
886 =item delete_cust_bill_pay OPTIONS
887
888 Deletes all associated cust_bill_pay records.
889
890 If option 'unapplied' is a specified, only deletes until
891 this object's 'unapplied' value is >= the specified amount.  
892 (Deletes in order returned by L</cust_bill_pay>.)
893
894 =cut
895
896 sub delete_cust_bill_pay {
897   my $self = shift;
898   my %opt = @_;
899
900   local $SIG{HUP} = 'IGNORE';
901   local $SIG{INT} = 'IGNORE';
902   local $SIG{QUIT} = 'IGNORE';
903   local $SIG{TERM} = 'IGNORE';
904   local $SIG{TSTP} = 'IGNORE';
905   local $SIG{PIPE} = 'IGNORE';
906
907   my $oldAutoCommit = $FS::UID::AutoCommit;
908   local $FS::UID::AutoCommit = 0;
909   my $dbh = dbh;
910
911   my $unapplied = $self->unapplied; #only need to look it up once
912
913   my $error = '';
914
915   # Maybe we should reverse the order these get deleted in?
916   # ie delete newest first?
917   # keeping consistent with how bop refunds work, for now...
918   foreach my $cust_bill_pay ( $self->cust_bill_pay ) {
919     last if $opt{'unapplied'} && ($unapplied > $opt{'unapplied'});
920     $unapplied += $cust_bill_pay->amount;
921     $error = $cust_bill_pay->delete;
922     last if $error;
923   }
924
925   if ($error) {
926     $dbh->rollback if $oldAutoCommit;
927     return $error;
928   }
929
930   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
931   return '';
932 }
933
934 =item refund HASHREF
935
936 Accepts input for creating a new FS::cust_refund object.
937 Unapplies payment from invoices up to the amount of the refund,
938 creates the refund and applies payment to refund.  Allows entire
939 process to be handled in one transaction.
940
941 Causes a fatal error if called on CARD or CHEK payments.
942
943 =cut
944
945 sub refund {
946   my $self = shift;
947   my $hash = shift;
948   die "Cannot call cust_pay->refund on " . $self->payby
949     if grep { $_ eq $self->payby } qw(CARD CHEK);
950
951   local $SIG{HUP} = 'IGNORE';
952   local $SIG{INT} = 'IGNORE';
953   local $SIG{QUIT} = 'IGNORE';
954   local $SIG{TERM} = 'IGNORE';
955   local $SIG{TSTP} = 'IGNORE';
956   local $SIG{PIPE} = 'IGNORE';
957
958   my $oldAutoCommit = $FS::UID::AutoCommit;
959   local $FS::UID::AutoCommit = 0;
960   my $dbh = dbh;
961
962   my $error = $self->delete_cust_bill_pay('amount' => $hash->{'amount'});
963
964   if ($error) {
965     $dbh->rollback if $oldAutoCommit;
966     return $error;
967   }
968
969   $hash->{'paynum'} = $self->paynum;
970   my $new = new FS::cust_refund ( $hash );
971   $error = $new->insert;
972
973   if ($error) {
974     $dbh->rollback if $oldAutoCommit;
975     return $error;
976   }
977
978   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
979   return '';
980 }
981
982 ### refund_to_unapply/unapply_refund false laziness with FS::cust_credit
983
984 =item refund_to_unapply
985
986 Returns L<FS::cust_pay_refund> objects that will be deleted by L</unapply_refund>
987 (all currently applied refunds that aren't closed.)
988 Returns empty list if payment itself is closed.
989
990 =cut
991
992 sub refund_to_unapply {
993   my $self = shift;
994   return () if $self->closed;
995   qsearch({
996     'table'   => 'cust_pay_refund',
997     'hashref' => { 'paynum' => $self->paynum },
998     'addl_from' => 'LEFT JOIN cust_refund USING (refundnum)',
999     'extra_sql' => "AND cust_refund.closed IS NULL AND cust_refund.source_paynum IS NULL",
1000   });
1001 }
1002
1003 =item unapply_refund
1004
1005 Deletes all objects returned by L</refund_to_unapply>.
1006
1007 =cut
1008
1009 sub unapply_refund {
1010   my $self = shift;
1011
1012   local $SIG{HUP} = 'IGNORE';
1013   local $SIG{INT} = 'IGNORE';
1014   local $SIG{QUIT} = 'IGNORE';
1015   local $SIG{TERM} = 'IGNORE';
1016   local $SIG{TSTP} = 'IGNORE';
1017   local $SIG{PIPE} = 'IGNORE';
1018
1019   my $oldAutoCommit = $FS::UID::AutoCommit;
1020   local $FS::UID::AutoCommit = 0;
1021
1022   foreach my $cust_pay_refund ($self->refund_to_unapply) {
1023     my $error = $cust_pay_refund->delete;
1024     if ($error) {
1025       dbh->rollback if $oldAutoCommit;
1026       return $error;
1027     }
1028   }
1029
1030   dbh->commit or die dbh->errstr if $oldAutoCommit;
1031   return '';
1032 }
1033
1034 =back
1035
1036 =head1 CLASS METHODS
1037
1038 =over 4
1039
1040 =item batch_insert CUST_PAY_OBJECT, ...
1041
1042 Class method which inserts multiple payments.  Takes a list of FS::cust_pay
1043 objects.  Returns a list, each element representing the status of inserting the
1044 corresponding payment - empty.  If there is an error inserting any payment, the
1045 entire transaction is rolled back, i.e. all payments are inserted or none are.
1046
1047 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a 
1048 reference to an array of (uninserted) FS::cust_bill_pay objects.  If so,
1049 those objects will be inserted with the paynum of the payment, and for 
1050 each one, an error message or an empty string will be inserted into the 
1051 list of errors.
1052
1053 For example:
1054
1055   my @errors = FS::cust_pay->batch_insert(@cust_pay);
1056   my $num_errors = scalar(grep $_, @errors);
1057   if ( $num_errors == 0 ) {
1058     #success; all payments were inserted
1059   } else {
1060     #failure; no payments were inserted.
1061   }
1062
1063 =cut
1064
1065 sub batch_insert {
1066   my $self = shift; #class method
1067
1068   local $SIG{HUP} = 'IGNORE';
1069   local $SIG{INT} = 'IGNORE';
1070   local $SIG{QUIT} = 'IGNORE';
1071   local $SIG{TERM} = 'IGNORE';
1072   local $SIG{TSTP} = 'IGNORE';
1073   local $SIG{PIPE} = 'IGNORE';
1074
1075   my $oldAutoCommit = $FS::UID::AutoCommit;
1076   local $FS::UID::AutoCommit = 0;
1077   my $dbh = dbh;
1078
1079   my $num_errors = 0;
1080   
1081   my @errors;
1082   foreach my $cust_pay (@_) {
1083     my $error = $cust_pay->insert( 'manual' => 1 );
1084     push @errors, $error;
1085     $num_errors++ if $error;
1086
1087     if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
1088
1089       foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
1090         if ( $error ) { # insert placeholders if cust_pay wasn't inserted
1091           push @errors, '';
1092         }
1093         else {
1094           $cust_bill_pay->set('paynum', $cust_pay->paynum);
1095           my $apply_error = $cust_bill_pay->insert;
1096           push @errors, $apply_error || '';
1097           $num_errors++ if $apply_error;
1098         }
1099       }
1100
1101     } elsif ( !$error ) { #normal case: apply payments as usual
1102       $cust_pay->cust_main->apply_payments( 'manual'=>1 );
1103     }
1104
1105   }
1106
1107   if ( $num_errors ) {
1108     $dbh->rollback if $oldAutoCommit;
1109   } else {
1110     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1111   }
1112
1113   @errors;
1114
1115 }
1116
1117 =item unapplied_sql
1118
1119 Returns an SQL fragment to retreive the unapplied amount.
1120
1121 =cut
1122
1123 sub unapplied_sql {
1124   my ($class, $start, $end) = @_;
1125   my $bill_start   = $start ? "AND cust_bill_pay._date <= $start"   : '';
1126   my $bill_end     = $end   ? "AND cust_bill_pay._date > $end"     : '';
1127   my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
1128   my $refund_end   = $end   ? "AND cust_pay_refund._date > $end"   : '';
1129
1130   "paid
1131         - COALESCE( 
1132                     ( SELECT SUM(amount) FROM cust_bill_pay
1133                         WHERE cust_pay.paynum = cust_bill_pay.paynum
1134                         $bill_start $bill_end )
1135                     ,0
1136                   )
1137         - COALESCE(
1138                     ( SELECT SUM(amount) FROM cust_pay_refund
1139                         WHERE cust_pay.paynum = cust_pay_refund.paynum
1140                         $refund_start $refund_end )
1141                     ,0
1142                   )
1143   ";
1144
1145 }
1146
1147 =item SSAPI_getinfo
1148
1149 =cut
1150
1151 sub SSAPI_getinfo {
1152   #my( $self, %opt ) = @_;
1153   my $self = shift;
1154
1155   +{ 'paynum'       => $self->paynum,
1156      '_date'        => $self->_date,
1157      'date'         => time2str("%b %o, %Y", $self->_date),
1158      'date_short'   => time2str("%m-%d-%Y",  $self->_date),
1159      'paid'         => sprintf('%.2f', $self->paid),
1160      'payby'        => $self->payby,
1161      'paycardtype'  => $self->paycardtype,
1162      'paymask'      => $self->paymask,
1163      'processor'    => $self->processor,
1164      'auth'         => $self->auth,
1165      'order_number' => $self->order_number,
1166   };
1167
1168 }
1169
1170
1171 # _upgrade_data
1172 #
1173 # Used by FS::Upgrade to migrate to a new database.
1174
1175 use FS::h_cust_pay;
1176
1177 sub _upgrade_data {  #class method
1178   my ($class, %opt) = @_;
1179
1180   warn "$me upgrading $class\n" if $DEBUG;
1181
1182   local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1183
1184   ##
1185   # otaker/ivan upgrade
1186   ##
1187
1188   unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
1189
1190     #not the most efficient, but hey, it only has to run once
1191
1192     my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
1193                 "  AND usernum IS NULL ".
1194                 "  AND 0 < ( SELECT COUNT(*) FROM cust_main                 ".
1195                 "              WHERE cust_main.custnum = cust_pay.custnum ) ";
1196
1197     my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
1198
1199     my $sth = dbh->prepare($count_sql) or die dbh->errstr;
1200     $sth->execute or die $sth->errstr;
1201     my $total = $sth->fetchrow_arrayref->[0];
1202     #warn "$total cust_pay records to update\n"
1203     #  if $DEBUG;
1204     local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
1205
1206     my $count = 0;
1207     my $lastprog = 0;
1208
1209     my @cust_pay = qsearch( {
1210         'table'     => 'cust_pay',
1211         'hashref'   => {},
1212         'extra_sql' => $where,
1213         'order_by'  => 'ORDER BY paynum',
1214     } );
1215
1216     foreach my $cust_pay (@cust_pay) {
1217
1218       my $h_cust_pay = $cust_pay->h_search('insert');
1219       if ( $h_cust_pay ) {
1220         next if $cust_pay->otaker eq $h_cust_pay->history_user;
1221         #$cust_pay->otaker($h_cust_pay->history_user);
1222         $cust_pay->set('otaker', $h_cust_pay->history_user);
1223       } else {
1224         $cust_pay->set('otaker', 'legacy');
1225       }
1226
1227       delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1228       my $error = $cust_pay->replace;
1229
1230       if ( $error ) {
1231         warn " *** WARNING: Error updating order taker for payment paynum ".
1232              $cust_pay->paynun. ": $error\n";
1233         next;
1234       }
1235
1236       $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1237
1238       $count++;
1239       if ( $DEBUG > 1 && $lastprog + 30 < time ) {
1240         warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
1241         $lastprog = time;
1242       }
1243
1244     }
1245
1246     FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
1247   }
1248
1249   ###
1250   # payinfo N/A upgrade
1251   ###
1252
1253   unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
1254
1255     #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
1256
1257     my @na_cust_pay = qsearch( {
1258       'table'     => 'cust_pay',
1259       'hashref'   => {}, #could be encrypted# { 'payinfo' => 'N/A' },
1260       'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
1261     } );
1262
1263     foreach my $na ( @na_cust_pay ) {
1264
1265       next unless $na->payinfo eq 'N/A';
1266
1267       my $cust_pay_pending =
1268         qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1269       unless ( $cust_pay_pending ) {
1270         warn " *** WARNING: not-yet recoverable N/A card for payment ".
1271              $na->paynum. " (no cust_pay_pending)\n";
1272         next;
1273       }
1274       $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1275       my $error = $na->replace;
1276       if ( $error ) {
1277         warn " *** WARNING: Error updating payinfo for payment paynum ".
1278              $na->paynun. ": $error\n";
1279         next;
1280       }
1281
1282     }
1283
1284     FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1285   }
1286
1287   ###
1288   # otaker->usernum upgrade
1289   ###
1290
1291   delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1292   $class->_upgrade_otaker(%opt);
1293   $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1294
1295   # if we do this anywhere else, it should become an FS::Upgrade method
1296   my $num_to_upgrade = $class->count('paybatch is not null');
1297   my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1298   if ( $num_to_upgrade > 0 ) {
1299     warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1300     if ( $opt{queue} ) {
1301       if ( $num_jobs > 0 ) {
1302         warn "Upgrade already queued.\n";
1303       } else {
1304         warn "Scheduling upgrade.\n";
1305         my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1306         $job->insert;
1307       }
1308     } else {
1309       process_upgrade_paybatch();
1310     }
1311   }
1312
1313   ###
1314   # don't set paycardtype until 4.x
1315   ###
1316   #$class->upgrade_set_cardtype;
1317
1318   # for batch payments, make sure paymask is set
1319   do {
1320     local $FS::payinfo_Mixin::allow_closed_replace = 1;
1321     local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1322
1323     my $cursor = FS::Cursor->new({
1324       table => 'cust_pay',
1325       extra_sql => ' WHERE paymask IS NULL AND payinfo IS NOT NULL
1326                     AND payby IN(\'CARD\', \'CHEK\')
1327                     AND batchnum IS NOT NULL',
1328     });
1329
1330     # records from cursors for some reason don't decrypt payinfo, so
1331     # call replace_old to fetch the record "normally"
1332     while (my $cust_pay = $cursor->fetch) {
1333       $cust_pay = $cust_pay->replace_old;
1334       $cust_pay->set('paymask', $cust_pay->mask_payinfo);
1335       my $error = $cust_pay->replace;
1336       if ($error) {
1337         die "$error (setting masked payinfo on payment#". $cust_pay->paynum.
1338           ")\n"
1339       }
1340     }
1341   };
1342 }
1343
1344 sub process_upgrade_paybatch {
1345   my $dbh = dbh;
1346   local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1347   local $FS::UID::AutoCommit = 1;
1348
1349   ###
1350   # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1351   ###
1352   my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
1353   my $search = FS::Cursor->new( {
1354     'table'     => 'cust_pay',
1355     'addl_from' => " JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS $text) ",
1356   } );
1357   while (my $cust_pay = $search->fetch) {
1358     $cust_pay->set('batchnum' => $cust_pay->paybatch);
1359     $cust_pay->set('paybatch' => '');
1360     my $error = $cust_pay->replace;
1361     warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n  $error"
1362     if $error;
1363   }
1364
1365   ###
1366   # migrate gateway info from the misused 'paybatch' field
1367   ###
1368
1369   # not only cust_pay, but also voided and refunded payments
1370   if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1371     local $FS::Record::nowarn_classload=1;
1372     # really inefficient, but again, only has to run once
1373     foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1374       my $and_batchnum_is_null =
1375         ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1376       my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1377       my $search = FS::Cursor->new({
1378         table     => $table,
1379         extra_sql => "WHERE payby IN('CARD','CHEK') ".
1380                      "AND (paybatch IS NOT NULL ".
1381                      "OR (paybatch IS NULL AND auth IS NULL
1382                      $and_batchnum_is_null ) )
1383                      ORDER BY $pkey DESC"
1384       });
1385       while ( my $object = $search->fetch ) {
1386         if ( $object->paybatch eq '' ) {
1387           # repair for a previous upgrade that didn't save 'auth'
1388           my $pkey = $object->primary_key;
1389           # find the last history record that had a paybatch value
1390           my $h = qsearchs({
1391               table   => "h_$table",
1392               hashref => {
1393                 $pkey     => $object->$pkey,
1394                 paybatch  => { op=>'!=', value=>''},
1395                 history_action => 'replace_old',
1396               },
1397               order_by => 'ORDER BY history_date DESC LIMIT 1',
1398           });
1399           if (!$h) {
1400             warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1401             next;
1402           }
1403           # if the paybatch didn't have an auth string, then it's fine
1404           $h->paybatch =~ /:(\w+):/ or next;
1405           # set paybatch to what it was in that record
1406           $object->set('paybatch', $h->paybatch)
1407           # and then upgrade it like the old records
1408         }
1409
1410         my $parsed = $object->_parse_paybatch;
1411         if (keys %$parsed) {
1412           $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1413           $object->set('auth' => $parsed->{authorization});
1414           $object->set('paybatch', '');
1415           my $error = $object->replace;
1416           warn "error parsing CARD/CHEK paybatch fields on $object #".
1417             $object->get($object->primary_key).":\n  $error\n"
1418             if $error;
1419         }
1420       } #$object
1421     } #$table
1422     FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1423   }
1424 }
1425
1426 =back
1427
1428 =head1 SUBROUTINES
1429
1430 =over 4 
1431
1432 =item process_batch_import
1433
1434 =cut
1435
1436 sub process_batch_import {
1437   my $job = shift;
1438
1439   my $hashcb = sub {
1440     my %hash = @_;
1441     my $custnum = $hash{'custnum'};
1442     my $agentnum = $hash{'agentnum'};
1443     my $agent_custid = $hash{'agent_custid'};
1444     #standardize date
1445     $hash{'_date'} = parse_datetime($hash{'_date'})
1446       if $hash{'_date'} && $hash{'_date'} =~ /\D/;
1447     #remove custnum_prefix
1448     my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1449     my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1450     if (
1451       $custnum_prefix 
1452       && $custnum =~ /^$custnum_prefix(0*([1-9]\d*))$/
1453       && length($1) == $custnum_length 
1454     ) {
1455       $custnum = $2;
1456     }
1457     # check agentnum against custnum and
1458     # translate agent_custid into regular custnum
1459     if ($custnum && $agent_custid) {
1460       die "can't specify both custnum and agent_custid\n";
1461     } elsif ($agentnum || $agent_custid) {
1462       # here is the agent virtualization
1463       my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1464       my %search;
1465       $search{'agentnum'} = $agentnum
1466         if $agentnum;
1467       $search{'agent_custid'} = $agent_custid
1468         if $agent_custid;
1469       $search{'custnum'} = $custnum
1470         if $custnum;
1471       my $cust_main = qsearchs({
1472         'table'     => 'cust_main',
1473         'hashref'   => \%search,
1474         'extra_sql' => $extra_sql,
1475       });
1476       die "can't find customer with" .
1477         ($agentnum ? " agentnum $agentnum" : '') .
1478         ($custnum  ? " custnum $custnum" : '') .
1479         ($agent_custid ? " agent_custid $agent_custid" : '') . "\n"
1480         unless $cust_main;
1481       die "mismatched customer number\n"
1482         if $custnum && ($custnum ne $cust_main->custnum);
1483       $custnum = $cust_main->custnum;
1484     }
1485     $hash{'custnum'} = $custnum;
1486     delete($hash{'agent_custid'});
1487     return %hash;
1488   };
1489
1490   my $opt = {
1491     'table'        => 'cust_pay',
1492     'params'       => [ '_date', 'agentnum', 'payby', 'paybatch' ],
1493                         #agent_custid isn't a cust_pay field, see hash callback
1494     'formats'      => { 'simple' =>
1495                           [ qw(custnum agent_custid paid payinfo invnum) ] },
1496     'format_types' => { 'simple' => '' }, #force infer from file extension
1497     'default_csv'  => 1, #if not .xls, will read as csv, regardless of extension
1498     'format_hash_callbacks' => { 'simple' => $hashcb },
1499     'insert_args_callback'  => sub { ( 'manual'=>1 ); },
1500     'postinsert_callback'   => sub {
1501       my $cust_pay = shift;
1502       my $cust_main = $cust_pay->cust_main
1503                         or return "can't find customer to which payments apply";
1504       my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
1505       return $error
1506                ? "can't apply payments to customer ".$cust_pay->custnum."$error"
1507                : '';
1508     },
1509   };
1510
1511   FS::Record::process_batch_import( $job, $opt, @_ );
1512
1513 }
1514
1515 =item batch_import HASHREF
1516
1517 Inserts new payments.
1518
1519 =cut
1520
1521 sub batch_import {
1522   my $param = shift;
1523
1524   my $fh       = $param->{filehandle};
1525   my $format   = $param->{'format'};
1526
1527   my $agentnum = $param->{agentnum};
1528   my $_date    = $param->{_date};
1529   $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1530   my $paybatch = $param->{'paybatch'};
1531
1532   my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1533   my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1534
1535   # here is the agent virtualization
1536   my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1537
1538   my @fields;
1539   my $payby;
1540   if ( $format eq 'simple' ) {
1541     @fields = qw( custnum agent_custid paid payinfo invnum );
1542     $payby = 'BILL';
1543   } elsif ( $format eq 'extended' ) {
1544     die "unimplemented\n";
1545     @fields = qw( );
1546     $payby = 'BILL';
1547   } else {
1548     die "unknown format $format";
1549   }
1550
1551   eval "use Text::CSV_XS;";
1552   die $@ if $@;
1553
1554   my $csv = new Text::CSV_XS;
1555
1556   my $imported = 0;
1557
1558   local $SIG{HUP} = 'IGNORE';
1559   local $SIG{INT} = 'IGNORE';
1560   local $SIG{QUIT} = 'IGNORE';
1561   local $SIG{TERM} = 'IGNORE';
1562   local $SIG{TSTP} = 'IGNORE';
1563   local $SIG{PIPE} = 'IGNORE';
1564
1565   my $oldAutoCommit = $FS::UID::AutoCommit;
1566   local $FS::UID::AutoCommit = 0;
1567   my $dbh = dbh;
1568   
1569   my $line;
1570   while ( defined($line=<$fh>) ) {
1571
1572     $csv->parse($line) or do {
1573       $dbh->rollback if $oldAutoCommit;
1574       return "can't parse: ". $csv->error_input();
1575     };
1576
1577     my @columns = $csv->fields();
1578
1579     my %cust_pay = (
1580       payby    => $payby,
1581       paybatch => $paybatch,
1582     );
1583     $cust_pay{_date} = $_date if $_date;
1584
1585     my $cust_main;
1586     foreach my $field ( @fields ) {
1587
1588       if ( $field eq 'agent_custid'
1589         && $agentnum
1590         && $columns[0] =~ /\S+/ )
1591       {
1592
1593         my $agent_custid = $columns[0];
1594         my %hash = ( 'agent_custid' => $agent_custid,
1595                      'agentnum'     => $agentnum,
1596                    );
1597
1598         if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1599           $dbh->rollback if $oldAutoCommit;
1600           return "can't specify custnum with agent_custid $agent_custid";
1601         }
1602
1603         $cust_main = qsearchs({
1604                                 'table'     => 'cust_main',
1605                                 'hashref'   => \%hash,
1606                                 'extra_sql' => $extra_sql,
1607                              });
1608
1609         unless ( $cust_main ) {
1610           $dbh->rollback if $oldAutoCommit;
1611           return "can't find customer with agent_custid $agent_custid";
1612         }
1613
1614         $field = 'custnum';
1615         $columns[0] = $cust_main->custnum;
1616       }
1617
1618       $cust_pay{$field} = shift @columns; 
1619     }
1620
1621     if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1622                          && length($1) == $custnum_length ) {
1623       $cust_pay{custnum} = $2;
1624     }
1625
1626     my $custnum = $cust_pay{custnum};
1627
1628     my $cust_pay = new FS::cust_pay( \%cust_pay );
1629     my $error = $cust_pay->insert;
1630
1631     if ( ! $error && $cust_pay->custnum != $custnum ) {
1632       #invnum was defined, and ->insert set custnum to the customer for that
1633       #invoice, but it wasn't the one the import specified.
1634       $dbh->rollback if $oldAutoCommit;
1635       $error = "specified invoice #". $cust_pay{invnum}.
1636                " is for custnum ". $cust_pay->custnum.
1637                ", not specified custnum $custnum";
1638     }
1639
1640     if ( $error ) {
1641       $dbh->rollback if $oldAutoCommit;
1642       return "can't insert payment for $line: $error";
1643     }
1644
1645     if ( $format eq 'simple' ) {
1646       # include agentnum for less surprise?
1647       $cust_main = qsearchs({
1648                              'table'     => 'cust_main',
1649                              'hashref'   => { 'custnum' => $cust_pay->custnum },
1650                              'extra_sql' => $extra_sql,
1651                            })
1652         unless $cust_main;
1653
1654       unless ( $cust_main ) {
1655         $dbh->rollback if $oldAutoCommit;
1656         return "can't find customer to which payments apply at line: $line";
1657       }
1658
1659       $error = $cust_main->apply_payments_and_credits;
1660       if ( $error ) {
1661         $dbh->rollback if $oldAutoCommit;
1662         return "can't apply payments to customer for $line: $error";
1663       }
1664
1665     }
1666
1667     $imported++;
1668   }
1669
1670   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1671
1672   return "Empty file!" unless $imported;
1673
1674   ''; #no error
1675
1676 }
1677
1678 =back
1679
1680 =head1 BUGS
1681
1682 Delete and replace methods.  
1683
1684 =head1 SEE ALSO
1685
1686 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1687 schema.html from the base documentation.
1688
1689 =cut
1690
1691 1;
1692