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