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