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