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