"day-of-month of customer signup" condition, #16827
[freeside.git] / FS / FS / part_event.pm
1 package FS::part_event;
2
3 use strict;
4 use vars qw( @ISA $DEBUG );
5 use Carp qw(confess);
6 use FS::Record qw( dbh qsearch qsearchs );
7 use FS::option_Common;
8 use FS::m2name_Common;
9 use FS::Conf;
10 use FS::part_event_option;
11 use FS::part_event_condition;
12 use FS::cust_event;
13 use FS::agent;
14
15 @ISA = qw( FS::m2name_Common FS::option_Common ); # FS::Record );
16 $DEBUG = 0;
17
18 =head1 NAME
19
20 FS::part_event - Object methods for part_event records
21
22 =head1 SYNOPSIS
23
24   use FS::part_event;
25
26   $record = new FS::part_event \%hash;
27   $record = new FS::part_event { 'column' => 'value' };
28
29   $error = $record->insert( { 'option' => 'value' } );
30   $error = $record->insert( \%options );
31
32   $error = $new_record->replace($old_record);
33
34   $error = $record->delete;
35
36   $error = $record->check;
37
38   $error = $record->do_event( $direct_object );
39   
40 =head1 DESCRIPTION
41
42 An FS::part_event object represents an event definition - a billing, collection
43 or other callback which is triggered when certain customer, invoice, package or
44 other conditions are met.  FS::part_event inherits from FS::Record.  The
45 following fields are currently supported:
46
47 =over 4
48
49 =item eventpart - primary key
50
51 =item agentnum - Optional agentnum (see L<FS::agent>)
52
53 =item event - event name
54
55 =item eventtable - table name against which this event is triggered; currently "cust_bill" (the traditional invoice events), "cust_main" (customer events) or "cust_pkg (package events) (or "cust_statement")
56
57 =item check_freq - how often events of this type are checked; currently "1d" (daily) and "1m" (monthly) are recognized.  Note that the apprioriate freeside-daily and/or freeside-monthly cron job needs to be in place.
58
59 =item weight - ordering for events
60
61 =item action - event action (like part_bill_event.plan - eventcode plan)
62
63 =item disabled - Disabled flag, empty or `Y'
64
65 =back
66
67 =head1 METHODS
68
69 =over 4
70
71 =item new HASHREF
72
73 Creates a new invoice event definition.  To add the invoice event definition to
74 the database, see L<"insert">.
75
76 Note that this stores the hash reference, not a distinct copy of the hash it
77 points to.  You can ask the object for a copy with the I<hash> method.
78
79 =cut
80
81 # the new method can be inherited from FS::Record, if a table method is defined
82
83 sub table { 'part_event'; }
84
85 =item insert [ HASHREF ]
86
87 Adds this record to the database.  If there is an error, returns the error,
88 otherwise returns false.
89
90 If a list or hash reference of options is supplied, part_export_option records
91 are created (see L<FS::part_event_option>).
92
93 =cut
94
95 # the insert method can be inherited from FS::Record
96
97 =item delete
98
99 Delete this record from the database.
100
101 =cut
102
103 # the delete method can be inherited from FS::Record
104
105 =item replace OLD_RECORD [ HASHREF | OPTION => VALUE ... ]
106
107 Replaces the OLD_RECORD with this one in the database.  If there is an error,
108 returns the error, otherwise returns false.
109
110 If a list or hash reference of options is supplied, part_event_option
111 records are created or modified (see L<FS::part_event_option>).
112
113 =cut
114
115 # the replace method can be inherited from FS::Record
116
117 =item check
118
119 Checks all fields to make sure this is a valid invoice event definition.  If
120 there is an error, returns the error, otherwise returns false.  Called by the
121 insert and replace methods.
122
123 =cut
124
125 # the check method should currently be supplied - FS::Record contains some
126 # data checking routines
127
128 sub check {
129   my $self = shift;
130
131   $self->weight(0) unless $self->weight;
132
133   my $error = 
134        $self->ut_numbern('eventpart')
135     || $self->ut_text('event')
136     || $self->ut_enum('eventtable', [ $self->eventtables ] )
137     || $self->ut_enum('check_freq', [ '1d', '1m' ])
138     || $self->ut_number('weight')
139     || $self->ut_alpha('action')
140     || $self->ut_enum('disabled', [ '', 'Y' ] )
141     || $self->ut_agentnum_acl('agentnum', 'Edit global billing events')
142   ;
143   return $error if $error;
144
145   #XXX check action to make sure a module exists?
146   # well it'll die in _rebless...
147
148   $self->SUPER::check;
149 }
150
151 =item _rebless
152
153 Reblesses the object into the FS::part_event::Action::ACTION class, where
154 ACTION is the object's I<action> field.
155
156 =cut
157
158 sub _rebless {
159   my $self = shift;
160   my $action = $self->action or return $self;
161   #my $class = ref($self). "::$action";
162   my $class = "FS::part_event::Action::$action";
163   eval "use $class";
164   die $@ if $@;
165   bless($self, $class); # unless $@;
166   $self;
167 }
168
169 =item part_event_condition
170
171 Returns the conditions associated with this event, as FS::part_event_condition
172 objects (see L<FS::part_event_condition>)
173
174 =cut
175
176 sub part_event_condition {
177   my $self = shift;
178   qsearch( 'part_event_condition', { 'eventpart' => $self->eventpart } );
179 }
180
181 =item new_cust_event OBJECT
182
183 Creates a new customer event (see L<FS::cust_event>) for the provided object.
184
185 =cut
186
187 sub new_cust_event {
188   my( $self, $object ) = @_;
189
190   confess "**** $object is not a ". $self->eventtable
191     if ref($object) ne "FS::". $self->eventtable;
192
193   my $pkey = $object->primary_key;
194
195   new FS::cust_event {
196     'eventpart' => $self->eventpart,
197     'tablenum'  => $object->$pkey(),
198     '_date'     => time, #i think we always want the real "now" here.
199     'status'    => 'new',
200   };
201 }
202
203 #surely this doesn't work
204 sub reasontext { confess "part_event->reasontext deprecated"; }
205 #=item reasontext
206 #
207 #Returns the text of any reason associated with this event.
208 #
209 #=cut
210 #
211 #sub reasontext {
212 #  my $self = shift;
213 #  my $r = qsearchs('reason', { 'reasonnum' => $self->reason });
214 #  if ($r){
215 #    $r->reason;
216 #  }else{
217 #    '';
218 #  }
219 #}
220
221 =item agent 
222
223 Returns the associated agent for this event, if any, as an FS::agent object.
224
225 =cut
226
227 sub agent {
228   my $self = shift;
229   qsearchs('agent', { 'agentnum' => $self->agentnum } );
230 }
231
232 =item templatename
233
234 Returns the alternate invoice template name, if any, or false if there is
235 no alternate template for this event.
236
237 =cut
238
239 sub templatename {
240
241   my $self = shift;
242   if (    $self->action   =~ /^cust_bill_send_(alternate|agent)$/
243           && (    $self->option('agent_templatename')
244                || $self->option('templatename')       )
245      )
246   {
247        $self->option('agent_templatename')
248     || $self->option('templatename');
249
250   } else {
251     '';
252   }
253 }
254
255 =item targets OPTIONS
256
257 Returns all objects (of type C<FS::eventtable>, for this object's 
258 C<eventtable>) eligible for processing under this event, as of right now.
259 The L<FS::cust_event> object used to test event conditions will be 
260 included in each object as the 'cust_event' pseudo-field.
261
262 This is not used in normal event processing (which is done on a 
263 per-customer basis to control timing of pre- and post-billing events)
264 but can be useful when configuring events.
265
266 =cut
267
268 sub targets {
269   my $self = shift;
270   my %opt = @_;
271   my $time = $opt{'time'} || time;
272
273   my $eventpart = $self->eventpart;
274   $eventpart =~ /^\d+$/ or die "bad eventpart $eventpart";
275   my $eventtable = $self->eventtable;
276
277   # find all objects that meet the conditions for this part_event
278   my $linkage = '';
279   # this is the 'object' side of the FROM clause
280   if ( $eventtable ne 'cust_main' ) {
281     $linkage = 
282         #($self->eventtables_cust_join->{$eventtable} || '') . #2.3
283         ' LEFT JOIN cust_main USING (custnum) ';
284   }
285
286   # this is the 'event' side
287   my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
288   my $where = FS::part_event_condition->where_conditions_sql( $eventtable,
289     'time' => $time
290   );
291   $join = $linkage .
292       " INNER JOIN part_event ON ( part_event.eventpart = $eventpart ) $join";
293
294   $where .= ' AND cust_main.agentnum = '.$self->agentnum
295     if $self->agentnum;
296   # don't enforce check_freq since this is a special, out-of-order check
297   # and don't enforce disabled because we want to be able to see targets 
298   # for a disabled event
299
300   my @objects = qsearch({
301       table     => $eventtable,
302       hashref   => {},
303       addl_from => $join,
304       extra_sql => "WHERE $where",
305   });
306   my @tested_objects;
307   foreach my $object ( @objects ) {
308     my $cust_event = $self->new_cust_event($object);
309     next unless $cust_event->test_conditions('time' => $time);
310
311     $object->set('cust_event', $cust_event);
312     push @tested_objects, $object;
313   }
314   @tested_objects;
315 }
316
317 =back
318
319 =head1 CLASS METHODS
320
321 =over 4
322
323 =item eventtable_labels
324
325 Returns a hash reference of labels for eventtable values,
326 i.e. 'cust_main'=>'Customer'
327
328 =cut
329
330 sub eventtable_labels {
331   #my $class = shift;
332
333   tie my %hash, 'Tie::IxHash',
334     'cust_pkg'       => 'Package',
335     'cust_bill'      => 'Invoice',
336     'cust_main'      => 'Customer',
337     'cust_pay_batch' => 'Batch payment',
338     'cust_statement' => 'Statement',  #too general a name here? "Invoice group"?
339   ;
340
341   \%hash
342 }
343
344 =item eventtable_pkey_sql
345
346 Returns a hash reference of full SQL primary key names for eventtable values,
347 i.e. 'cust_main'=>'cust_main.custnum'
348
349 =cut
350
351 sub eventtable_pkey_sql {
352   my $class = shift;
353
354   my $hashref = $class->eventtable_pkey;
355
356   my %hash = map { $_ => "$_.". $hashref->{$_} } keys %$hashref;
357
358   \%hash;
359 }
360
361 =item eventtable_pkey
362
363 Returns a hash reference of full SQL primary key names for eventtable values,
364 i.e. 'cust_main'=>'custnum'
365
366 =cut
367
368 sub eventtable_pkey {
369   #my $class = shift;
370
371   {
372     'cust_main'      => 'custnum',
373     'cust_bill'      => 'invnum',
374     'cust_pkg'       => 'pkgnum',
375     'cust_pay_batch' => 'paybatchnum',
376     'cust_statement' => 'statementnum',
377   };
378 }
379
380 =item eventtables
381
382 Returns a list of eventtable values (default ordering; suited for display).
383
384 =cut
385
386 sub eventtables {
387   my $class = shift;
388   my $eventtables = $class->eventtable_labels;
389   keys %$eventtables;
390 }
391
392 =item eventtables_runorder
393
394 Returns a list of eventtable values (run order).
395
396 =cut
397
398 sub eventtables_runorder {
399   shift->eventtables; #same for now
400 }
401
402 =item check_freq_labels
403
404 Returns a hash reference of labels for check_freq values,
405 i.e. '1d'=>'daily'
406
407 =cut
408
409 sub check_freq_labels {
410   #my $class = shift;
411
412   #Tie::IxHash??
413   {
414     '1d' => 'daily',
415     '1m' => 'monthly',
416   };
417 }
418
419 =item actions [ EVENTTABLE ]
420
421 Return information about the available actions.  If an eventtable is specified,
422 only return information about actions available for that eventtable.
423
424 Information is returned as key-value pairs.  Keys are event names.  Values are
425 hashrefs with the following keys:
426
427 =over 4
428
429 =item description
430
431 =item eventtable_hashref
432
433 =item option_fields
434
435 =item default_weight
436
437 =item deprecated
438
439 =back
440
441 See L<FS::part_event::Action> for more information.
442
443 =cut
444
445 #false laziness w/part_event_condition.pm
446 #some false laziness w/part_export & part_pkg
447 my %actions;
448 foreach my $INC ( @INC ) {
449   foreach my $file ( glob("$INC/FS/part_event/Action/*.pm") ) {
450     warn "attempting to load Action from $file\n" if $DEBUG;
451     $file =~ /\/(\w+)\.pm$/ or do {
452       warn "unrecognized file in $INC/FS/part_event/Action/: $file\n";
453       next;
454     };
455     my $mod = $1;
456     eval "use FS::part_event::Action::$mod;";
457     if ( $@ ) {
458       die "error using FS::part_event::Action::$mod (skipping): $@\n" if $@;
459       #warn "error using FS::part_event::Action::$mod (skipping): $@\n" if $@;
460       #next;
461     }
462     $actions{$mod} = {
463       ( map { $_ => "FS::part_event::Action::$mod"->$_() }
464             qw( description eventtable_hashref default_weight deprecated )
465             #option_fields_hashref
466       ),
467       'option_fields' => [ "FS::part_event::Action::$mod"->option_fields() ],
468     };
469   }
470 }
471
472 sub actions {
473   my( $class, $eventtable ) = @_;
474   (
475     map  { $_ => $actions{$_} }
476     sort { $actions{$a}->{'default_weight'}<=>$actions{$b}->{'default_weight'} }
477     $class->all_actions( $eventtable )
478   );
479
480 }
481
482 =item all_actions [ EVENTTABLE ]
483
484 Returns a list of just the action names
485
486 =cut
487
488 sub all_actions {
489   my ( $class, $eventtable ) = @_;
490
491   grep { !$eventtable || $actions{$_}->{'eventtable_hashref'}{$eventtable} }
492        keys %actions
493 }
494
495 =back
496
497 =head1 SEE ALSO
498
499 L<FS::part_event_option>, L<FS::part_event_condition>, L<FS::cust_main>,
500 L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_bill_event>, L<FS::Record>,
501 schema.html from the base documentation.
502
503 =cut
504
505 1;
506