refactor giant cust_main.pm slightly more, almost not the biggest non-data .pm
[freeside.git] / FS / FS / cust_main / Billing_Batch.pm
1 package FS::cust_main::Billing_Batch;
2
3 use strict;
4 use vars qw( $conf );
5 use FS::Record qw( qsearch qsearchs dbh );
6 use FS::pay_batch;
7 use FS::cust_pay_batch;
8
9 install_callback FS::UID sub { 
10   $conf = new FS::Conf;
11   #yes, need it for stuff below (prolly should be cached)
12 };
13
14 =item batch_card OPTION => VALUE...
15
16 Adds a payment for this invoice to the pending credit card batch (see
17 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
18 runs the payment using a realtime gateway.
19
20 Options may include:
21
22 B<amount>: the amount to be paid; defaults to the customer's balance minus
23 any payments in transit.
24
25 B<payby>: the payment method; defaults to cust_main.payby
26
27 B<realtime>: runs this as a realtime payment instead of adding it to a 
28 batch.  Deprecated.
29
30 B<invnum>: sets cust_pay_batch.invnum.
31
32 B<address1>, B<address2>, B<city>, B<state>, B<zip>, B<country>: sets 
33 the billing address for the payment; defaults to the customer's billing
34 location.
35
36 B<payinfo>, B<paydate>, B<payname>: sets the payment account, expiration
37 date, and name; defaults to those fields in cust_main.
38
39 =cut
40
41 sub batch_card {
42   my ($self, %options) = @_;
43
44   my $amount;
45   if (exists($options{amount})) {
46     $amount = $options{amount};
47   }else{
48     $amount = sprintf("%.2f", $self->balance - $self->in_transit_payments);
49   }
50   return '' unless $amount > 0;
51   
52   my $invnum = delete $options{invnum};
53   my $payby = $options{payby} || $self->payby;  #still dubious
54
55   if ($options{'realtime'}) {
56     return $self->realtime_bop( FS::payby->payby2bop($self->payby),
57                                 $amount,
58                                 %options,
59                               );
60   }
61
62   my $oldAutoCommit = $FS::UID::AutoCommit;
63   local $FS::UID::AutoCommit = 0;
64   my $dbh = dbh;
65
66   #this needs to handle mysql as well as Pg, like svc_acct.pm
67   #(make it into a common function if folks need to do batching with mysql)
68   $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
69     or return "Cannot lock pay_batch: " . $dbh->errstr;
70
71   my %pay_batch = (
72     'status' => 'O',
73     'payby'  => FS::payby->payby2payment($payby),
74   );
75   $pay_batch{agentnum} = $self->agentnum if $conf->exists('batch-spoolagent');
76
77   my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
78
79   unless ( $pay_batch ) {
80     $pay_batch = new FS::pay_batch \%pay_batch;
81     my $error = $pay_batch->insert;
82     if ( $error ) {
83       $dbh->rollback if $oldAutoCommit;
84       die "error creating new batch: $error\n";
85     }
86   }
87
88   my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
89       'batchnum' => $pay_batch->batchnum,
90       'custnum'  => $self->custnum,
91   } );
92
93   foreach (qw( address1 address2 city state zip country latitude longitude
94                payby payinfo paydate payname ))
95   {
96     $options{$_} = '' unless exists($options{$_});
97   }
98
99   my $loc = $self->bill_location;
100
101   my $cust_pay_batch = new FS::cust_pay_batch ( {
102     'batchnum' => $pay_batch->batchnum,
103     'invnum'   => $invnum || 0,                    # is there a better value?
104                                                    # this field should be
105                                                    # removed...
106                                                    # cust_bill_pay_batch now
107     'custnum'  => $self->custnum,
108     'last'     => $self->getfield('last'),
109     'first'    => $self->getfield('first'),
110     'address1' => $options{address1} || $loc->address1,
111     'address2' => $options{address2} || $loc->address2,
112     'city'     => $options{city}     || $loc->city,
113     'state'    => $options{state}    || $loc->state,
114     'zip'      => $options{zip}      || $loc->zip,
115     'country'  => $options{country}  || $loc->country,
116     'payby'    => $options{payby}    || $self->payby,
117     'payinfo'  => $options{payinfo}  || $self->payinfo,
118     'exp'      => $options{paydate}  || $self->paydate,
119     'payname'  => $options{payname}  || $self->payname,
120     'amount'   => $amount,                         # consolidating
121   } );
122   
123   $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
124     if $old_cust_pay_batch;
125
126   my $error;
127   if ($old_cust_pay_batch) {
128     $error = $cust_pay_batch->replace($old_cust_pay_batch)
129   } else {
130     $error = $cust_pay_batch->insert;
131   }
132
133   if ( $error ) {
134     $dbh->rollback if $oldAutoCommit;
135     die $error;
136   }
137
138   my $unapplied =   $self->total_unapplied_credits
139                   + $self->total_unapplied_payments
140                   + $self->in_transit_payments;
141   foreach my $cust_bill ($self->open_cust_bill) {
142     #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
143     my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
144       'invnum' => $cust_bill->invnum,
145       'paybatchnum' => $cust_pay_batch->paybatchnum,
146       'amount' => $cust_bill->owed,
147       '_date' => time,
148     };
149     if ($unapplied >= $cust_bill_pay_batch->amount){
150       $unapplied -= $cust_bill_pay_batch->amount;
151       next;
152     }else{
153       $cust_bill_pay_batch->amount(sprintf ( "%.2f", 
154                                    $cust_bill_pay_batch->amount - $unapplied ));      $unapplied = 0;
155     }
156     $error = $cust_bill_pay_batch->insert;
157     if ( $error ) {
158       $dbh->rollback if $oldAutoCommit;
159       die $error;
160     }
161   }
162
163   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
164   '';
165 }
166
167 =item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
168
169 Returns all batched payments (see L<FS::cust_pay_batch>) for this customer.
170
171 Optionally, a list or hashref of additional arguments to the qsearch call can
172 be passed.
173
174 =cut
175
176 sub cust_pay_batch {
177   my $self = shift;
178   my $opt = ref($_[0]) ? shift : { @_ };
179
180   #return $self->num_cust_statement unless wantarray || keys %$opt;
181
182   $opt->{'table'} = 'cust_pay_batch';
183   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
184   $opt->{'hashref'}{'custnum'} = $self->custnum;
185   $opt->{'order_by'} ||= 'ORDER BY paybatchnum ASC';
186
187   map { $_ } #behavior of sort undefined in scalar context
188     sort { $a->paybatchnum <=> $b->paybatchnum }
189       qsearch($opt);
190 }
191
192 =item in_transit_payments
193
194 Returns the total of requests for payments for this customer pending in 
195 batches in transit to the bank.  See L<FS::pay_batch> and L<FS::cust_pay_batch>
196
197 =cut
198
199 sub in_transit_payments {
200   my $self = shift;
201   my $in_transit_payments = 0;
202   foreach my $pay_batch ( qsearch('pay_batch', {
203     'status' => 'I',
204   } ) ) {
205     foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
206       'batchnum' => $pay_batch->batchnum,
207       'custnum' => $self->custnum,
208     } ) ) {
209       $in_transit_payments += $cust_pay_batch->amount;
210     }
211   }
212   sprintf( "%.2f", $in_transit_payments );
213 }
214
215 1;