Optimize "Customer has a referring customer" condition, RT#74452
[freeside.git] / FS / FS / part_event / Condition.pm
1 package FS::part_event::Condition;
2
3 use strict;
4 use base qw( FS::part_event_condition );
5 use Time::Local qw(timelocal_nocheck);
6 use FS::UID qw( driver_name );
7
8 =head1 NAME
9
10 FS::part_event::Condition - Base class for event conditions
11
12 =head1 SYNOPSIS
13
14 package FS::part_event::Condition::mycondition;
15
16 use base FS::part_event::Condition;
17
18 =head1 DESCRIPTION
19
20 FS::part_event::Condition is a base class for event conditions classes.
21
22 =head1 METHODS
23
24 These methods are implemented in each condition class.
25
26 =over 4
27
28 =item description
29
30 Condition classes must define a description method.  This method should return
31 a scalar description of the condition.
32
33 =item eventtable_hashref
34
35 Condition classes must define an eventtable_hashref method if they can only be
36 tested against some kinds of tables. This method should return a hash reference
37 of eventtables (values set true indicate the condition can be tested):
38
39   sub eventtable_hashref {
40     { 'cust_main'      => 1,
41       'cust_bill'      => 1,
42       'cust_pkg'       => 0,
43       'cust_pay_batch' => 0,
44       'cust_statement' => 0,
45     };
46   }
47
48 =cut
49
50 #fallback
51 sub eventtable_hashref {
52     { 'cust_main'      => 1,
53       'cust_bill'      => 1,
54       'cust_pkg'       => 1,
55       'cust_pay'       => 1,
56       'cust_pay_batch' => 1,
57       'cust_statement' => 1,
58       'svc_acct'       => 1,
59     };
60 }
61
62 =item option_fields
63
64 Condition classes may define an option_fields method to indicate that they
65 accept one or more options.
66
67 This method should return a list of option names and option descriptions.
68 Each option description can be a scalar description, for simple options, or a
69 hashref with the following values:
70
71 =over 4
72
73 =item label - Description
74
75 =item type - Currently text, money, checkbox, checkbox-multiple, select, select-agent, select-pkg_class, select-part_referral, select-table, fixed, hidden, (others can be implemented as httemplate/elements/tr-TYPE.html mason components).  Defaults to text.
76
77 =item options - For checkbox-multiple and select, a list reference of available option values.
78
79 =item option_labels - For checkbox-multiple (and select?), a hash reference of availble option values and labels.
80
81 =item value - for checkbox, fixed, hidden (also a default for text, money, more?)
82
83 =item table - for select-table
84
85 =item name_col - for select-table
86
87 =item NOTE: See httemplate/elements/select-table.html for a full list of the optinal options for the select-table type
88
89 =back
90
91 NOTE: A database connection is B<not> yet available when this subroutine is
92 executed.
93
94 Example:
95
96   sub option_fields {
97     (
98       'field'         => 'description',
99
100       'another_field' => { 'label'=>'Amount', 'type'=>'money', },
101
102       'third_field'   => { 'label'         => 'Types',
103                            'type'          => 'checkbox-multiple',
104                            'options'       => [ 'h', 's' ],
105                            'option_labels' => { 'h' => 'Happy',
106                                                 's' => 'Sad',
107                                               },
108     );
109   }
110
111 =cut
112
113 #fallback
114 sub option_fields {
115   ();
116 }
117
118 =item condition CUSTOMER_EVENT_OBJECT
119
120 Condition classes must define a condition method.  This method is evaluated
121 to determine if the condition has been met.  The object which triggered the
122 event (an FS::cust_main, FS::cust_bill or FS::cust_pkg object) is passed as
123 the first argument.  Additional arguments are list of key-value pairs.
124
125 To retreive option values, call the option method on the desired option, i.e.:
126
127   my( $self, $cust_object, %opts ) = @_;
128   $value_of_field = $self->option('field');
129
130 Available additional arguments:
131
132   $time = $opt{'time'}; #use this instead of time or $^T
133
134   $cust_event = $opt{'cust_event'}; #to retreive the cust_event object being tested
135
136 Return a true value if the condition has been met, and a false value if it has
137 not.
138
139 =item condition_sql EVENTTABLE
140
141 Condition classes may optionally define a condition_sql method.  This B<class>
142 method should return an SQL fragment that tests for this condition.  The
143 fragment is evaluated and a true value of this expression indicates that the
144 condition has been met.  The event table (cust_main, cust_bill or cust_pkg) is
145 passed as an argument.
146
147 This method is used for optimizing event queries.  You may want to add indices
148 for any columns referenced.  It is acceptable to return an SQL fragment which
149 partially tests the condition; doing so will still reduce the number of
150 records which must be returned and tested with the B<condition> method.
151
152 =cut
153
154 # fallback.
155 sub condition_sql {
156   my( $class, $eventtable ) = @_;
157   #...
158   'true';
159 }
160
161 =item disabled
162
163 Condition classes may optionally define a disabled method.  Returning a true
164 value disbles the condition entirely.
165
166 =cut
167
168 sub disabled {
169   0;
170 }
171
172 =item implicit_flag
173
174 This is used internally by the I<once> and I<balance> conditions.  You probably
175 do B<not> want to define this method for new custom conditions, unless you're
176 sure you want B<every> new action to start with your condition.
177
178 Condition classes may define an implicit_flag method that returns true to
179 indicate that all new events should start with this condition.  (Currently,
180 condition classes which do so should be applicable to all kinds of
181 I<eventtable>s.)  The numeric value of the flag also defines the ordering of
182 implicit conditions.
183
184 =cut
185
186 #fallback
187 sub implicit_flag { 0; }
188
189 =item remove_warning
190
191 Again, used internally by the I<once> and I<balance> conditions; probably not
192 a good idea for new custom conditions.
193
194 Condition classes may define a remove_warning method containing a string
195 warning message to enable a confirmation dialog triggered when the condition
196 is removed from an event.
197
198 =cut
199
200 #fallback
201 sub remove_warning { ''; }
202
203 =item order_sql
204
205 This is used internally by the I<balance_age> and I<cust_bill_age> conditions
206 to declare ordering; probably not of general use for new custom conditions.
207
208 =item order_sql_weight
209
210 In conjunction with order_sql, this defines which order the ordering fragments
211 supplied by different B<order_sql> should be used.
212
213 =cut
214
215 sub order_sql_weight { ''; }
216
217 =back
218
219 =head1 BASE METHODS
220
221 These methods are defined in the base class for use in condition classes.
222
223 =over 4 
224
225 =item cust_main CUST_OBJECT
226
227 Return the customer object (see L<FS::cust_main>) associated with the provided
228 object (the object itself if it is already a customer object).
229
230 =cut
231
232 sub cust_main {
233   my( $self, $cust_object ) = @_;
234
235   $cust_object->isa('FS::cust_main') ? $cust_object : $cust_object->cust_main;
236
237 }
238
239 =item cust_pkg OBJECT
240
241 Return the package object (L<FS::cust_pkg>) associated with the provided 
242 object.  The object must be either a service (L<FS::svc_Common>) or a 
243 package.
244
245 =cut
246
247 sub cust_pkg {
248   my( $self, $object ) = @_;
249   $object->isa('FS::cust_pkg')      ? $object :
250   $object->isa('FS::svc_Common')    ? $object->cust_svc->cust_pkg :
251   undef;
252 }
253
254 =item option_label OPTIONNAME
255
256 Returns the label for the specified option name.
257
258 =cut
259
260 sub option_label {
261   my( $self, $optionname ) = @_;
262
263   my %option_fields = $self->option_fields;
264
265   ref( $option_fields{$optionname} )
266     ? $option_fields{$optionname}->{'label'}
267     : $option_fields{$optionname}
268   or $optionname;
269 }
270
271 =back
272
273 =item option_type OPTION
274
275 Returns the type of the option, as a string: 'text', 'money', 'date',
276 or 'freq'.
277
278 =cut
279
280 sub option_type {
281   my( $self, $optionname ) = @_;
282
283   my %option_fields = $self->option_fields;
284
285   ref( $option_fields{$optionname} )
286     ? $option_fields{$optionname}->{'type'} 
287     : 'text'
288 }
289
290 =item option_age_from OPTION FROM_TIMESTAMP
291
292 Retreives a condition option, parses it from a frequency (such as "1d", "1w" or
293 "12m"), and subtracts that interval from the supplied timestamp.  It is
294 primarily intended for use in B<condition>.
295
296 =cut
297
298 sub option_age_from {
299   my( $self, $option, $time ) = @_;
300   my $age = $self->option($option);
301   $age = '0m' unless length($age);
302
303   my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($time) )[0,1,2,3,4,5];
304
305   if ( $age =~ /^(\d+)m$/i ) {
306     $mon -= $1;
307     until ( $mon >= 0 ) { $mon += 12; $year--; }
308   } elsif ( $age =~ /^(\d+)y$/i ) {
309     $year -= $1;
310   } elsif ( $age =~ /^(\d+)w$/i ) {
311     $mday -= $1 * 7;
312   } elsif ( $age =~ /^(\d+)d$/i ) {
313     $mday -= $1;
314   } elsif ( $age =~ /^(\d+)h$/i ) {
315     $hour -= $1;
316   } else {
317     die "unparsable age: $age";
318   }
319
320   timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year);
321
322 }
323
324 =item condition_sql_option OPTION
325
326 This is a class method that returns an SQL fragment for retreiving a condition
327 option.  It is primarily intended for use in B<condition_sql>.
328
329 =cut
330
331 sub condition_sql_option {
332   my( $class, $option ) = @_;
333
334   ( my $condname = $class ) =~ s/^.*:://;
335
336   "( SELECT optionvalue FROM part_event_condition_option
337       WHERE part_event_condition_option.eventconditionnum =
338             cond_$condname.eventconditionnum
339         AND part_event_condition_option.optionname = '$option'
340    )";
341 }
342
343 #c.f. part_event_condition_option.pm / part_event_condition_option_option
344 #used for part_event/Condition/payby.pm
345 sub condition_sql_option_option {
346   my( $class, $option ) = @_;
347
348   ( my $condname = $class ) =~ s/^.*:://;
349
350   my $optionnum = 
351     "( SELECT optionnum FROM part_event_condition_option
352         WHERE part_event_condition_option.eventconditionnum =
353               cond_$condname.eventconditionnum
354           AND part_event_condition_option.optionname  = '$option'
355           AND part_event_condition_option.optionvalue = 'HASH'
356      )";
357
358   "( SELECT optionname FROM part_event_condition_option_option
359        WHERE optionnum IN $optionnum
360    )";
361
362 }
363
364 #used for part_event/Condition/cust_bill_has_service.pm and has_cust_tag.pm
365 #a little false laziness w/above and condition_sql_option_integer
366 sub condition_sql_option_option_integer {
367   my( $class, $option ) = @_;
368
369   ( my $condname = $class ) =~ s/^.*:://;
370
371   my $optionnum = 
372     "( SELECT optionnum FROM part_event_condition_option
373         WHERE part_event_condition_option.eventconditionnum =
374               cond_$condname.eventconditionnum
375           AND part_event_condition_option.optionname  = '$option'
376           AND part_event_condition_option.optionvalue = 'HASH'
377      )";
378
379   my $integer = (driver_name =~ /^mysql/) ? 'UNSIGNED INTEGER' : 'INTEGER';
380
381   my $optionname = "CAST(optionname AS $integer)";
382
383   "( SELECT $optionname FROM part_event_condition_option_option
384        WHERE optionnum IN $optionnum
385    )";
386
387 }
388
389 =item condition_sql_option_age_from OPTION FROM_TIMESTAMP
390
391 This is a class method that returns an SQL fragment that will retreive a
392 condition option, parse it from a frequency (such as "1d", "1w" or "12m"),
393 and subtract that interval from the supplied timestamp.  It is primarily
394 intended for use in B<condition_sql>.
395
396 =cut
397
398 sub condition_sql_option_age_from {
399   my( $class, $option, $from ) = @_;
400
401   my $value = $class->condition_sql_option($option);
402
403 #  my $str2time = str2time_sql;
404
405   if ( driver_name =~ /^Pg/i ) {
406
407     #can we do better with Pg now that we have $from?  yes we can, bob
408     "( $from - EXTRACT( EPOCH FROM REPLACE( $value, 'm', 'mon')::interval ) )";
409
410   } elsif ( driver_name =~ /^mysql/i ) {
411
412     #hmm... is there a way we can save $value?  we're just an expression, hmm
413     #we might be able to do something like "AS ${option}_value" except we get
414     #used in more complicated expressions and we need some sort of unique
415     #identifer passed down too... yow
416
417     "CASE WHEN $value IS NULL OR $value = ''
418        THEN $from
419      WHEN $value LIKE '%m'
420        THEN UNIX_TIMESTAMP(
421               FROM_UNIXTIME($from) - INTERVAL REPLACE( $value, 'm', '' ) MONTH
422             )
423      WHEN $value LIKE '%y'
424        THEN UNIX_TIMESTAMP(
425               FROM_UNIXTIME($from) - INTERVAL REPLACE( $value, 'y', '' ) YEAR
426             )
427      WHEN $value LIKE '%w'
428        THEN UNIX_TIMESTAMP(
429               FROM_UNIXTIME($from) - INTERVAL REPLACE( $value, 'w', '' ) WEEK
430             )
431      WHEN $value LIKE '%d'
432        THEN UNIX_TIMESTAMP(
433               FROM_UNIXTIME($from) - INTERVAL REPLACE( $value, 'd', '' ) DAY
434             )
435      WHEN $value LIKE '%h'
436        THEN UNIX_TIMESTAMP(
437               FROM_UNIXTIME($from) - INTERVAL REPLACE( $value, 'h', '' ) HOUR
438             )
439      END
440     "
441   } else {
442
443     die "FATAL: don't know how to subtract frequencies from dates for ".
444         driver_name. " databases";
445
446   }
447
448 }
449
450 =item condition_sql_option_age OPTION
451
452 This is a class method that returns an SQL fragment for retreiving a condition
453 option, and additionaly parsing it from a frequency (such as "1d", "1w" or
454 "12m") into an approximate number of seconds.
455
456 Note that since months vary in length, the results of this method should B<not>
457 be used in computations (use condition_sql_option_age_from for that).  They are
458 useful for for ordering and comparison to other ages.
459
460 This method is primarily intended for use in B<order_sql>.
461
462 =cut
463
464 sub condition_sql_option_age {
465   my( $class, $option ) = @_;
466   $class->age2seconds_sql( $class->condition_sql_option($option) );
467 }
468
469 =item age2seconds_sql
470
471 Class method returns an SQL fragment for parsing an arbitrary frequeny (such
472 as "1d", "1w", "12m", "2y" or "12h") into an approximate number of seconds.
473
474 Approximate meaning: months are considered to be 30 days, years to be
475 365.25 days.  Otherwise the numbers of seconds returned is exact.
476
477 =cut
478
479 sub age2seconds_sql {
480   my( $class, $value ) = @_;
481
482   if ( driver_name =~ /^Pg/i ) {
483
484     "EXTRACT( EPOCH FROM REPLACE( $value, 'm', 'mon')::interval )";
485
486   } elsif ( driver_name =~ /^mysql/i ) {
487
488     #hmm... is there a way we can save $value?  we're just an expression, hmm
489     #we might be able to do something like "AS ${option}_age" except we get
490     #used in more complicated expressions and we need some sort of unique
491     #identifer passed down too... yow
492     # 2592000  = 30d "1 month"
493     # 31557600 = 365.25d "1 year"
494
495     "CASE WHEN $value IS NULL OR $value = ''
496        THEN 0
497      WHEN $value LIKE '%m'
498        THEN REPLACE( $value, 'm', '' ) * 2592000 
499      WHEN $value LIKE '%y'
500        THEN REPLACE( $value, 'y', '' ) * 31557600
501      WHEN $value LIKE '%w'
502        THEN REPLACE( $value, 'w', '' ) * 604800
503      WHEN $value LIKE '%d'
504        THEN REPLACE( $value, 'd', '' ) * 86400
505      WHEN $value LIKE '%h'
506        THEN REPLACE( $value, 'h', '' ) * 3600
507      END
508     "
509   } else {
510
511     die "FATAL: don't know how to approximate frequencies for ". driver_name.
512         " databases";
513
514   }
515
516 }
517
518 =item condition_sql_option_integer OPTION [ DRIVER_NAME ]
519
520 As I<condition_sql_option>, but cast the option value to an integer so that
521 comparison to other integers is type-correct.
522
523 =cut
524
525 sub condition_sql_option_integer {
526   my ($class, $option, $driver_name) = @_;
527
528   my $integer = (driver_name() =~ /^mysql/) ? 'UNSIGNED INTEGER' : 'INTEGER';
529
530   'CAST(
531          COALESCE('. $class->condition_sql_option($option).
532                 " ,'0') ".
533        " AS $integer )";
534 }
535
536 =item condition_sql_option_money OPTION
537
538 As I<condition_sql_option>, but cast the option value to DECIMAL so that
539 comparison to other monetary values is type-correct.
540
541 =cut
542
543 sub condition_sql_option_money {
544   my ($class, $option ) = @_;
545
546   'CAST(
547          COALESCE('. $class->condition_sql_option($option).
548                 " ,'0') ".
549        " AS DECIMAL(10,2) )";
550 }
551
552 =head1 NEW CONDITION CLASSES
553
554 A module should be added in FS/FS/part_event/Condition/ which implements the
555 methods desribed above in L</METHODS>.  An example may be found in the
556 eg/part_event-Condition-template.pm file.
557
558 =cut
559
560 1;
561
562