71720: Prevent billing events from running on holidays
[freeside.git] / FS / FS / part_event / Condition / holiday.pm
1 package FS::part_event::Condition::holiday;
2
3 use strict;
4 use base qw( FS::part_event::Condition );
5 use DateTime;
6 use DateTime::Format::ICal;
7 use Tie::IxHash;
8
9 # rules lifted from DateTime::Event::Holiday::US,
10 # but their list is unordered, and contains duplicates and frivolous holidays
11 # it's better for future development for us to use our own hard-coded list,
12 # and the actual code beyond the list is just trivial use of DateTime::Format::ICal
13
14 tie my %holidays, 'Tie::IxHash', 
15   'New Year\'s Day'
16     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1' },   # January 1
17   'Birthday of Martin Luther King, Jr.'
18     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=1;BYDAY=3mo' },      # Third Monday in January
19   'Washington\'s Birthday' 
20     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=2;BYDAY=3mo' },      # Third Monday in February
21   'Memorial Day'
22     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=5;BYDAY=-1mo' },     # Last Monday in May
23   'Independence Day'
24     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=4' },   # July 4
25   'Labor Day'
26     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=1mo' },      # First Monday in September
27   'Columbus Day'
28     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=2mo' },     # Second Monday in October
29   'Veterans Day'
30     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=11;BYMONTHDAY=11' }, # November 11
31   'Thanksgiving Day'
32     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=4th' },     # Fourth Thursday in November
33   'Christmas'
34     => { 'rule' => 'RRULE:FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=25' }, # December 25
35 ;
36
37 my $oneday = DateTime::Duration->new(days => 1);
38
39 sub description {
40   "Do not run on holidays",
41 }
42
43 sub option_fields {
44   (
45     'holidays' => {
46        label         => 'Do not run on',
47        type          => 'checkbox-multiple',
48        options       => [ keys %holidays ],
49        option_labels => { map { $_ => $_ } keys %holidays },
50        default_value => { map { $_ => 1  } keys %holidays }
51     },
52   );
53 }
54
55 sub condition {
56   my( $self, $object, %opt ) = @_;
57   my $today = DateTime->from_epoch(
58     epoch     => $opt{'time'} || time,
59     time_zone => 'local'
60   )->truncate( to => 'day' );
61
62   # if fri/mon, also check sat/sun respectively
63   # federal holidays on weekends "move" to nearest weekday
64   # eg Christmas 2016 is Mon Dec 26
65   # we'll check both, so eg both Dec 25 & 26 are holidays in 2016
66   my $offday;
67   if ($today->day_of_week == 1) {
68     $offday = $today->clone->subtract_duration($oneday);
69   } elsif ($today->day_of_week == 5) {
70     $offday = $today->clone->add_duration($oneday);
71   }
72
73   foreach my $holiday (keys %{$self->option('holidays')}) {
74     $holidays{$holiday}{'set'} ||= 
75       DateTime::Format::ICal->parse_recurrence(
76         'recurrence' => $holidays{$holiday}{'rule'}
77       );
78     my $set = $holidays{$holiday}{'set'};
79     return ''
80       if $set->contains($today) or $offday && $set->contains($offday);
81   }
82
83   return 1;
84
85 }
86
87
88 # no condition_sql
89
90 1;