45f94872534eb582b60f5739a4b7968ff42b46d2
[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 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   $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_name', $cust_main->agentnum ) ?
711                      $conf->config('invoice_from_name', $cust_main->agentnum ) . ' <' .
712                      $conf->config('invoice_from', $cust_main->agentnum ) . '>' :
713                      $conf->config('invoice_from', $cust_main->agentnum ),
714                                    #invoice_from??? well as good as any
715         'to'      => \@invoicing_list,
716         'subject' => 'Payment receipt',
717         'body'    => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
718       );
719
720     } else {
721
722       warn "payment_receipt is on, but no payment_receipt_msgnum\n";
723
724     }
725
726   } elsif ( ! $cust_main->invoice_noemail ) { #not manual
727
728     my $queue = new FS::queue {
729        'job'     => 'FS::cust_bill::queueable_email',
730        'paynum'  => $self->paynum,
731        'custnum' => $cust_main->custnum,
732     };
733
734     $error = $queue->insert(
735       'invnum'      => $cust_bill->invnum,
736       'template'    => 'statement',
737       'notice_name' => 'Statement',
738       'no_coupon'   => 1,
739     );
740
741   }
742   
743   warn "send_receipt: $error\n" if $error;
744 }
745
746 =item cust_bill_pay
747
748 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
749 payment.
750
751 =cut
752
753 sub cust_bill_pay {
754   my $self = shift;
755   map { $_ } #return $self->num_cust_bill_pay unless wantarray;
756   sort {    $a->_date  <=> $b->_date
757          || $a->invnum <=> $b->invnum }
758     qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
759   ;
760 }
761
762 =item cust_pay_refund
763
764 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
765 payment.
766
767 =cut
768
769 sub cust_pay_refund {
770   my $self = shift;
771   map { $_ } #return $self->num_cust_pay_refund unless wantarray;
772   sort { $a->_date <=> $b->_date }
773     qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
774   ;
775 }
776
777
778 =item unapplied
779
780 Returns the amount of this payment that is still unapplied; which is
781 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
782 applications (see L<FS::cust_pay_refund>).
783
784 =cut
785
786 sub unapplied {
787   my $self = shift;
788   my $amount = $self->paid;
789   $amount -= $_->amount foreach ( $self->cust_bill_pay );
790   $amount -= $_->amount foreach ( $self->cust_pay_refund );
791   sprintf("%.2f", $amount );
792 }
793
794 =item unrefunded
795
796 Returns the amount of this payment that has not been refuned; which is
797 paid minus all  refund applications (see L<FS::cust_pay_refund>).
798
799 =cut
800
801 sub unrefunded {
802   my $self = shift;
803   my $amount = $self->paid;
804   $amount -= $_->amount foreach ( $self->cust_pay_refund );
805   sprintf("%.2f", $amount );
806 }
807
808 =item amount
809
810 Returns the "paid" field.
811
812 =cut
813
814 sub amount {
815   my $self = shift;
816   $self->paid();
817 }
818
819 =back
820
821 =head1 CLASS METHODS
822
823 =over 4
824
825 =item batch_insert CUST_PAY_OBJECT, ...
826
827 Class method which inserts multiple payments.  Takes a list of FS::cust_pay
828 objects.  Returns a list, each element representing the status of inserting the
829 corresponding payment - empty.  If there is an error inserting any payment, the
830 entire transaction is rolled back, i.e. all payments are inserted or none are.
831
832 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a 
833 reference to an array of (uninserted) FS::cust_bill_pay objects.  If so,
834 those objects will be inserted with the paynum of the payment, and for 
835 each one, an error message or an empty string will be inserted into the 
836 list of errors.
837
838 For example:
839
840   my @errors = FS::cust_pay->batch_insert(@cust_pay);
841   my $num_errors = scalar(grep $_, @errors);
842   if ( $num_errors == 0 ) {
843     #success; all payments were inserted
844   } else {
845     #failure; no payments were inserted.
846   }
847
848 =cut
849
850 sub batch_insert {
851   my $self = shift; #class method
852
853   local $SIG{HUP} = 'IGNORE';
854   local $SIG{INT} = 'IGNORE';
855   local $SIG{QUIT} = 'IGNORE';
856   local $SIG{TERM} = 'IGNORE';
857   local $SIG{TSTP} = 'IGNORE';
858   local $SIG{PIPE} = 'IGNORE';
859
860   my $oldAutoCommit = $FS::UID::AutoCommit;
861   local $FS::UID::AutoCommit = 0;
862   my $dbh = dbh;
863
864   my $num_errors = 0;
865   
866   my @errors;
867   foreach my $cust_pay (@_) {
868     my $error = $cust_pay->insert( 'manual' => 1 );
869     push @errors, $error;
870     $num_errors++ if $error;
871
872     if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
873
874       foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
875         if ( $error ) { # insert placeholders if cust_pay wasn't inserted
876           push @errors, '';
877         }
878         else {
879           $cust_bill_pay->set('paynum', $cust_pay->paynum);
880           my $apply_error = $cust_bill_pay->insert;
881           push @errors, $apply_error || '';
882           $num_errors++ if $apply_error;
883         }
884       }
885
886     } elsif ( !$error ) { #normal case: apply payments as usual
887       $cust_pay->cust_main->apply_payments;
888     }
889
890   }
891
892   if ( $num_errors ) {
893     $dbh->rollback if $oldAutoCommit;
894   } else {
895     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
896   }
897
898   @errors;
899
900 }
901
902 =item unapplied_sql
903
904 Returns an SQL fragment to retreive the unapplied amount.
905
906 =cut 
907
908 sub unapplied_sql {
909   my ($class, $start, $end) = @_;
910   my $bill_start   = $start ? "AND cust_bill_pay._date <= $start"   : '';
911   my $bill_end     = $end   ? "AND cust_bill_pay._date > $end"     : '';
912   my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
913   my $refund_end   = $end   ? "AND cust_pay_refund._date > $end"   : '';
914
915   "paid
916         - COALESCE( 
917                     ( SELECT SUM(amount) FROM cust_bill_pay
918                         WHERE cust_pay.paynum = cust_bill_pay.paynum
919                         $bill_start $bill_end )
920                     ,0
921                   )
922         - COALESCE(
923                     ( SELECT SUM(amount) FROM cust_pay_refund
924                         WHERE cust_pay.paynum = cust_pay_refund.paynum
925                         $refund_start $refund_end )
926                     ,0
927                   )
928   ";
929
930 }
931
932 # _upgrade_data
933 #
934 # Used by FS::Upgrade to migrate to a new database.
935
936 use FS::h_cust_pay;
937
938 sub _upgrade_data {  #class method
939   my ($class, %opt) = @_;
940
941   warn "$me upgrading $class\n" if $DEBUG;
942
943   local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
944
945   ##
946   # otaker/ivan upgrade
947   ##
948
949   unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
950
951     #not the most efficient, but hey, it only has to run once
952
953     my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
954                 "  AND usernum IS NULL ".
955                 "  AND 0 < ( SELECT COUNT(*) FROM cust_main                 ".
956                 "              WHERE cust_main.custnum = cust_pay.custnum ) ";
957
958     my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
959
960     my $sth = dbh->prepare($count_sql) or die dbh->errstr;
961     $sth->execute or die $sth->errstr;
962     my $total = $sth->fetchrow_arrayref->[0];
963     #warn "$total cust_pay records to update\n"
964     #  if $DEBUG;
965     local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
966
967     my $count = 0;
968     my $lastprog = 0;
969
970     my @cust_pay = qsearch( {
971         'table'     => 'cust_pay',
972         'hashref'   => {},
973         'extra_sql' => $where,
974         'order_by'  => 'ORDER BY paynum',
975     } );
976
977     foreach my $cust_pay (@cust_pay) {
978
979       my $h_cust_pay = $cust_pay->h_search('insert');
980       if ( $h_cust_pay ) {
981         next if $cust_pay->otaker eq $h_cust_pay->history_user;
982         #$cust_pay->otaker($h_cust_pay->history_user);
983         $cust_pay->set('otaker', $h_cust_pay->history_user);
984       } else {
985         $cust_pay->set('otaker', 'legacy');
986       }
987
988       delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
989       my $error = $cust_pay->replace;
990
991       if ( $error ) {
992         warn " *** WARNING: Error updating order taker for payment paynum ".
993              $cust_pay->paynun. ": $error\n";
994         next;
995       }
996
997       $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
998
999       $count++;
1000       if ( $DEBUG > 1 && $lastprog + 30 < time ) {
1001         warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
1002         $lastprog = time;
1003       }
1004
1005     }
1006
1007     FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
1008   }
1009
1010   ###
1011   # payinfo N/A upgrade
1012   ###
1013
1014   unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
1015
1016     #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
1017
1018     my @na_cust_pay = qsearch( {
1019       'table'     => 'cust_pay',
1020       'hashref'   => {}, #could be encrypted# { 'payinfo' => 'N/A' },
1021       'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
1022     } );
1023
1024     foreach my $na ( @na_cust_pay ) {
1025
1026       next unless $na->payinfo eq 'N/A';
1027
1028       my $cust_pay_pending =
1029         qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1030       unless ( $cust_pay_pending ) {
1031         warn " *** WARNING: not-yet recoverable N/A card for payment ".
1032              $na->paynum. " (no cust_pay_pending)\n";
1033         next;
1034       }
1035       $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1036       my $error = $na->replace;
1037       if ( $error ) {
1038         warn " *** WARNING: Error updating payinfo for payment paynum ".
1039              $na->paynun. ": $error\n";
1040         next;
1041       }
1042
1043     }
1044
1045     FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1046   }
1047
1048   ###
1049   # otaker->usernum upgrade
1050   ###
1051
1052   delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1053   $class->_upgrade_otaker(%opt);
1054   $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1055
1056   # if we do this anywhere else, it should become an FS::Upgrade method
1057   my $num_to_upgrade = $class->count('paybatch is not null');
1058   my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1059   if ( $num_to_upgrade > 0 ) {
1060     warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1061     if ( $opt{queue} ) {
1062       if ( $num_jobs > 0 ) {
1063         warn "Upgrade already queued.\n";
1064       } else {
1065         warn "Scheduling upgrade.\n";
1066         my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1067         $job->insert;
1068       }
1069     } else {
1070       process_upgrade_paybatch();
1071     }
1072   }
1073 }
1074
1075 sub process_upgrade_paybatch {
1076   my $dbh = dbh;
1077   local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1078   local $FS::UID::AutoCommit = 1;
1079
1080   ###
1081   # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1082   ###
1083   my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
1084   my $search = FS::Cursor->new( {
1085     'table'     => 'cust_pay',
1086     'addl_from' => " JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS $text) ",
1087   } );
1088   while (my $cust_pay = $search->fetch) {
1089     $cust_pay->set('batchnum' => $cust_pay->paybatch);
1090     $cust_pay->set('paybatch' => '');
1091     my $error = $cust_pay->replace;
1092     warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n  $error"
1093     if $error;
1094   }
1095
1096   ###
1097   # migrate gateway info from the misused 'paybatch' field
1098   ###
1099
1100   # not only cust_pay, but also voided and refunded payments
1101   if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1102     local $FS::Record::nowarn_classload=1;
1103     # really inefficient, but again, only has to run once
1104     foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1105       my $and_batchnum_is_null =
1106         ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1107       my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1108       my $search = FS::Cursor->new({
1109         table     => $table,
1110         extra_sql => "WHERE payby IN('CARD','CHEK') ".
1111                      "AND (paybatch IS NOT NULL ".
1112                      "OR (paybatch IS NULL AND auth IS NULL
1113                      $and_batchnum_is_null ) )
1114                      ORDER BY $pkey DESC"
1115       });
1116       while ( my $object = $search->fetch ) {
1117         if ( $object->paybatch eq '' ) {
1118           # repair for a previous upgrade that didn't save 'auth'
1119           my $pkey = $object->primary_key;
1120           # find the last history record that had a paybatch value
1121           my $h = qsearchs({
1122               table   => "h_$table",
1123               hashref => {
1124                 $pkey     => $object->$pkey,
1125                 paybatch  => { op=>'!=', value=>''},
1126                 history_action => 'replace_old',
1127               },
1128               order_by => 'ORDER BY history_date DESC LIMIT 1',
1129           });
1130           if (!$h) {
1131             warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1132             next;
1133           }
1134           # if the paybatch didn't have an auth string, then it's fine
1135           $h->paybatch =~ /:(\w+):/ or next;
1136           # set paybatch to what it was in that record
1137           $object->set('paybatch', $h->paybatch)
1138           # and then upgrade it like the old records
1139         }
1140
1141         my $parsed = $object->_parse_paybatch;
1142         if (keys %$parsed) {
1143           $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1144           $object->set('auth' => $parsed->{authorization});
1145           $object->set('paybatch', '');
1146           my $error = $object->replace;
1147           warn "error parsing CARD/CHEK paybatch fields on $object #".
1148             $object->get($object->primary_key).":\n  $error\n"
1149             if $error;
1150         }
1151       } #$object
1152     } #$table
1153     FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1154   }
1155 }
1156
1157 =back
1158
1159 =head1 SUBROUTINES
1160
1161 =over 4 
1162
1163 =item batch_import HASHREF
1164
1165 Inserts new payments.
1166
1167 =cut
1168
1169 sub batch_import {
1170   my $param = shift;
1171
1172   my $fh       = $param->{filehandle};
1173   my $format   = $param->{'format'};
1174
1175   my $agentnum = $param->{agentnum};
1176   my $_date    = $param->{_date};
1177   $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1178   my $paybatch = $param->{'paybatch'};
1179
1180   my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1181   my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1182
1183   # here is the agent virtualization
1184   my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1185
1186   my @fields;
1187   my $payby;
1188   if ( $format eq 'simple' ) {
1189     @fields = qw( custnum agent_custid paid payinfo invnum );
1190     $payby = 'BILL';
1191   } elsif ( $format eq 'extended' ) {
1192     die "unimplemented\n";
1193     @fields = qw( );
1194     $payby = 'BILL';
1195   } else {
1196     die "unknown format $format";
1197   }
1198
1199   eval "use Text::CSV_XS;";
1200   die $@ if $@;
1201
1202   my $csv = new Text::CSV_XS;
1203
1204   my $imported = 0;
1205
1206   local $SIG{HUP} = 'IGNORE';
1207   local $SIG{INT} = 'IGNORE';
1208   local $SIG{QUIT} = 'IGNORE';
1209   local $SIG{TERM} = 'IGNORE';
1210   local $SIG{TSTP} = 'IGNORE';
1211   local $SIG{PIPE} = 'IGNORE';
1212
1213   my $oldAutoCommit = $FS::UID::AutoCommit;
1214   local $FS::UID::AutoCommit = 0;
1215   my $dbh = dbh;
1216   
1217   my $line;
1218   while ( defined($line=<$fh>) ) {
1219
1220     $csv->parse($line) or do {
1221       $dbh->rollback if $oldAutoCommit;
1222       return "can't parse: ". $csv->error_input();
1223     };
1224
1225     my @columns = $csv->fields();
1226
1227     my %cust_pay = (
1228       payby    => $payby,
1229       paybatch => $paybatch,
1230     );
1231     $cust_pay{_date} = $_date if $_date;
1232
1233     my $cust_main;
1234     foreach my $field ( @fields ) {
1235
1236       if ( $field eq 'agent_custid'
1237         && $agentnum
1238         && $columns[0] =~ /\S+/ )
1239       {
1240
1241         my $agent_custid = $columns[0];
1242         my %hash = ( 'agent_custid' => $agent_custid,
1243                      'agentnum'     => $agentnum,
1244                    );
1245
1246         if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1247           $dbh->rollback if $oldAutoCommit;
1248           return "can't specify custnum with agent_custid $agent_custid";
1249         }
1250
1251         $cust_main = qsearchs({
1252                                 'table'     => 'cust_main',
1253                                 'hashref'   => \%hash,
1254                                 'extra_sql' => $extra_sql,
1255                              });
1256
1257         unless ( $cust_main ) {
1258           $dbh->rollback if $oldAutoCommit;
1259           return "can't find customer with agent_custid $agent_custid";
1260         }
1261
1262         $field = 'custnum';
1263         $columns[0] = $cust_main->custnum;
1264       }
1265
1266       $cust_pay{$field} = shift @columns; 
1267     }
1268
1269     if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1270                          && length($1) == $custnum_length ) {
1271       $cust_pay{custnum} = $2;
1272     }
1273
1274     my $custnum = $cust_pay{custnum};
1275
1276     my $cust_pay = new FS::cust_pay( \%cust_pay );
1277     my $error = $cust_pay->insert;
1278
1279     if ( ! $error && $cust_pay->custnum != $custnum ) {
1280       #invnum was defined, and ->insert set custnum to the customer for that
1281       #invoice, but it wasn't the one the import specified.
1282       $dbh->rollback if $oldAutoCommit;
1283       $error = "specified invoice #". $cust_pay{invnum}.
1284                " is for custnum ". $cust_pay->custnum.
1285                ", not specified custnum $custnum";
1286     }
1287
1288     if ( $error ) {
1289       $dbh->rollback if $oldAutoCommit;
1290       return "can't insert payment for $line: $error";
1291     }
1292
1293     if ( $format eq 'simple' ) {
1294       # include agentnum for less surprise?
1295       $cust_main = qsearchs({
1296                              'table'     => 'cust_main',
1297                              'hashref'   => { 'custnum' => $cust_pay->custnum },
1298                              'extra_sql' => $extra_sql,
1299                            })
1300         unless $cust_main;
1301
1302       unless ( $cust_main ) {
1303         $dbh->rollback if $oldAutoCommit;
1304         return "can't find customer to which payments apply at line: $line";
1305       }
1306
1307       $error = $cust_main->apply_payments_and_credits;
1308       if ( $error ) {
1309         $dbh->rollback if $oldAutoCommit;
1310         return "can't apply payments to customer for $line: $error";
1311       }
1312
1313     }
1314
1315     $imported++;
1316   }
1317
1318   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1319
1320   return "Empty file!" unless $imported;
1321
1322   ''; #no error
1323
1324 }
1325
1326 =back
1327
1328 =head1 BUGS
1329
1330 Delete and replace methods.  
1331
1332 =head1 SEE ALSO
1333
1334 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1335 schema.html from the base documentation.
1336
1337 =cut
1338
1339 1;
1340