does pg try to finish the query when the job addition is committed? well, if this...
[freeside.git] / FS / FS / Cron / bill.pm
1 package FS::Cron::bill;
2
3 use strict;
4 use vars qw( @ISA @EXPORT_OK );
5 use Exporter;
6 use Date::Parse;
7 use DBI 1.33; #The "clone" method was added in DBI 1.33. 
8 use FS::UID qw(dbh);
9 use FS::Record qw(qsearchs);
10 use FS::cust_main;
11 use FS::part_event;
12 use FS::part_event_condition;
13
14 @ISA = qw( Exporter );
15 @EXPORT_OK = qw ( bill );
16
17 sub bill {
18
19   my %opt = @_;
20
21   my $check_freq = $opt{'check_freq'} || '1d';
22
23   my $debug = 0;
24   $debug = 1 if $opt{'v'};
25   $debug = $opt{'l'} if $opt{'l'};
26  
27   $FS::cust_main::DEBUG = $debug;
28   #$FS::cust_event::DEBUG = $opt{'l'} if $opt{'l'};
29
30   my @search = ();
31
32   push @search, "( cust_main.archived != 'Y' OR archived IS NULL )"; #disable?
33
34   push @search, "cust_main.payby    = '". $opt{'p'}. "'"
35     if $opt{'p'};
36   push @search, "cust_main.agentnum =  ". $opt{'a'}
37     if $opt{'a'};
38
39   if ( @ARGV ) {
40     push @search, "( ".
41       join(' OR ', map "cust_main.custnum = $_", @ARGV ).
42     " )";
43   }
44
45   ###
46   # generate where_pkg/where_event search clause
47   ###
48
49   #we're at now now (and later).
50   my($time)= $opt{'d'} ? str2time($opt{'d'}) : $^T;
51   $time += $opt{'y'} * 86400 if $opt{'y'};
52
53   my $invoice_time = $opt{'n'} ? $^T : $time;
54
55   # select * from cust_main where
56   my $where_pkg = <<"END";
57     0 < ( select count(*) from cust_pkg
58             where cust_main.custnum = cust_pkg.custnum
59               and ( cancel is null or cancel = 0 )
60               and (    setup is null or setup =  0
61                     or bill  is null or bill  <= $time 
62                     or ( expire is not null and expire <= $^T )
63                     or ( adjourn is not null and adjourn <= $^T )
64                   )
65         )
66 END
67
68   my $where_event = join(' OR ', map {
69     my $eventtable = $_;
70
71     my $join  = FS::part_event_condition->join_conditions_sql(  $eventtable );
72     my $where = FS::part_event_condition->where_conditions_sql( $eventtable,
73                                                                 'time'=>$time,
74                                                               );
75
76     my $are_part_event = 
77       "0 < ( SELECT COUNT(*) FROM part_event $join
78                WHERE check_freq = '$check_freq'
79                  AND eventtable = '$eventtable'
80                  AND ( disabled = '' OR disabled IS NULL )
81                  AND $where
82            )
83       ";
84
85     if ( $eventtable eq 'cust_main' ) { 
86       $are_part_event;
87     } else {
88       "0 < ( SELECT COUNT(*) FROM $eventtable
89                WHERE cust_main.custnum = $eventtable.custnum
90                  AND $are_part_event
91            )
92       ";
93     }
94
95   } FS::part_event->eventtables);
96
97   push @search, "( $where_pkg OR $where_event )";
98
99   ###
100   # get a list of custnums
101   ###
102
103   warn "searching for customers:\n". join("\n", @search). "\n"
104     if $opt{'v'} || $opt{'l'};
105
106   my $cursor_dbh = dbh->clone;
107
108   $cursor_dbh->do(
109     "DECLARE cron_bill_cursor CURSOR FOR ".
110     "  SELECT custnum FROM cust_main ".
111     "    WHERE ". join(' AND ', @search).
112     "    ORDER BY custnum " #LIMIT 1000 "
113   ) or die $cursor_dbh->errstr;
114
115   while ( 1 ) {
116
117     my $sth = $cursor_dbh->prepare('FETCH 100 FROM cron_bill_cursor'); #mysql?
118
119     $sth->execute or die $sth->errstr;
120
121     my @custnums = map { $_->[0] } @{ $sth->fetchall_arrayref };
122
123     last unless scalar(@custnums);
124
125     ###
126     # for each custnum, queue or make one customer object and bill
127     # (one at a time, to reduce memory footprint with large #s of customers)
128     ###
129     
130     foreach my $custnum ( @custnums ) {
131     
132       my %args = (
133           'time'         => $time,
134           'invoice_time' => $invoice_time,
135           'actual_time'  => $^T, #when freeside-bill was started
136                                  #(not, when using -m, freeside-queued)
137           'check_freq'   => $check_freq,
138           'resetup'      => ( $opt{'s'} ? $opt{'s'} : 0 ),
139       );
140
141       if ( $opt{'m'} ) {
142
143         #add job to queue that calls bill_and_collect with options
144         my $queue = new FS::queue {
145           'job'      => 'FS::cust_main::queued_bill',
146           'secure'   => 'Y',
147           'priority' => 99, #don't get in the way of provisioning jobs
148         };
149         my $error = $queue->insert( 'custnum'=>$custnum, %args );
150
151       } else {
152
153         my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } );
154         $cust_main->bill_and_collect( %args, 'debug' => $debug );
155
156       }
157
158     }
159
160   }
161
162   $cursor_dbh->commit or die $cursor_dbh->errstr;
163
164 }
165
166 1;