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