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