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