per-agent disable_previous_balance, #15863
[freeside.git] / FS / FS / cust_pkg_reason.pm
1 package FS::cust_pkg_reason;
2
3 use strict;
4 use vars qw( $ignore_empty_action );
5 use base qw( FS::otaker_Mixin FS::Record );
6 use FS::Record qw( qsearch qsearchs );
7
8 $ignore_empty_action = 0;
9
10 =head1 NAME
11
12 FS::cust_pkg_reason - Object methods for cust_pkg_reason records
13
14 =head1 SYNOPSIS
15
16   use FS::cust_pkg_reason;
17
18   $record = new FS::cust_pkg_reason \%hash;
19   $record = new FS::cust_pkg_reason { 'column' => 'value' };
20
21   $error = $record->insert;
22
23   $error = $new_record->replace($old_record);
24
25   $error = $record->delete;
26
27   $error = $record->check;
28
29 =head1 DESCRIPTION
30
31 An FS::cust_pkg_reason object represents a relationship between a cust_pkg
32 and a reason, for example cancellation or suspension reasons. 
33 FS::cust_pkg_reason inherits from FS::Record.  The following fields are
34 currently supported:
35
36 =over 4
37
38 =item num
39
40 primary key
41
42 =item pkgnum
43
44 =item reasonnum
45
46 =item usernum
47
48 =item date
49
50 =back
51
52 =head1 METHODS
53
54 =over 4
55
56 =item new HASHREF
57
58 Creates a new cust_pkg_reason.  To add the example to the database, see
59 L<"insert">.
60
61 Note that this stores the hash reference, not a distinct copy of the hash it
62 points to.  You can ask the object for a copy with the I<hash> method.
63
64 =cut
65
66 sub table { 'cust_pkg_reason'; }
67
68 =item insert
69
70 Adds this record to the database.  If there is an error, returns the error,
71 otherwise returns false.
72
73 =cut
74
75 =item delete
76
77 Delete this record from the database.
78
79 =cut
80
81 =item replace OLD_RECORD
82
83 Replaces the OLD_RECORD with this one in the database.  If there is an error,
84 returns the error, otherwise returns false.
85
86 =cut
87
88 =item check
89
90 Checks all fields to make sure this is a valid cust_pkg_reason.  If there is
91 an error, returns the error, otherwise returns false.  Called by the insert
92 and replace methods.
93
94 =cut
95
96 sub check {
97   my $self = shift;
98
99   my @actions = ( 'A', 'C', 'E', 'S' );
100   push @actions, '' if $ignore_empty_action;
101
102   my $error = 
103     $self->ut_numbern('num')
104     || $self->ut_number('pkgnum')
105     || $self->ut_number('reasonnum')
106     || $self->ut_enum('action', \@actions)
107     || $self->ut_alphan('otaker')
108     || $self->ut_numbern('date')
109   ;
110   return $error if $error;
111
112   $self->SUPER::check;
113 }
114
115 =item reason
116
117 Returns the reason (see L<FS::reason>) associated with this cust_pkg_reason.
118
119 =cut
120
121 sub reason {
122   my $self = shift;
123   qsearchs( 'reason', { 'reasonnum' => $self->reasonnum } );
124 }
125
126 =item reasontext
127
128 Returns the text of the reason (see L<FS::reason>) associated with this
129 cust_pkg_reason.
130
131 =cut
132
133 sub reasontext {
134   my $reason = shift->reason;
135   $reason ? $reason->reason : '';
136 }
137
138 # _upgrade_data
139 #
140 # Used by FS::Upgrade to migrate to a new database.
141
142 use FS::h_cust_pkg;
143 use FS::h_cust_pkg_reason;
144
145 sub _upgrade_data { # class method
146   my ($class, %opts) = @_;
147
148   my $action_replace =
149     " AND ( history_action = 'replace_old' OR history_action = 'replace_new' )";
150
151   my $count = 0;
152   my @unmigrated = qsearch('cust_pkg_reason', { 'action' => '' } ); 
153   foreach ( @unmigrated ) {
154
155     my @history_cust_pkg_reason = qsearch( 'h_cust_pkg_reason', { $_->hash } );
156     
157     next unless scalar(@history_cust_pkg_reason) == 1;
158
159     my $hashref = { pkgnum => $_->pkgnum,
160                     history_date   => $history_cust_pkg_reason[0]->history_date,
161                   };
162
163     my @history = qsearch({ table     => 'h_cust_pkg',
164                             hashref   => $hashref,
165                             extra_sql => $action_replace,
166                             order_by  => 'ORDER BY history_action',
167                          });
168
169     my $fuzz = 0;
170     while (scalar(@history) < 2 && $fuzz < 3) {
171       $hashref->{history_date}++;
172       $fuzz++;
173       push @history, qsearch({ table     => 'h_cust_pkg',
174                                hashref   => $hashref,
175                                extra_sql => $action_replace,
176                                order_by  => 'ORDER BY history_action',
177                             });
178     }
179
180     next unless scalar(@history) == 2;
181
182     my @new = grep { $_->history_action eq 'replace_new' } @history;
183     my @old = grep { $_->history_action eq 'replace_old' } @history;
184     
185     next if (scalar(@new) == 2 || scalar(@old) == 2);
186
187     if ( !$old[0]->get('cancel') && $new[0]->get('cancel') ) {
188       $_->action('C');
189     }elsif( !$old[0]->susp && $new[0]->susp ){
190       $_->action('S');
191     }elsif( $new[0]->expire &&
192             (!$old[0]->expire || !$old[0]->expire != $new[0]->expire )
193           ){
194       $_->action('E');
195       $_->date($new[0]->expire);
196     }elsif( $new[0]->adjourn &&
197             (!$old[0]->adjourn || $old[0]->adjourn != $new[0]->adjourn )
198           ){
199       $_->action('A');
200       $_->date($new[0]->adjourn);
201     }
202
203     my $error = $_->replace
204       if $_->modified;
205
206     die $error if $error;
207
208     $count++;
209   }
210
211   #remove nullability if scalar(@migrated) - $count == 0 && ->column('action');
212   
213   #seek expirations/adjourns without reason
214   foreach my $field (qw( expire adjourn cancel susp )) {
215     my $addl_from =
216       "LEFT JOIN h_cust_pkg ON ".
217       "(cust_pkg_reason.pkgnum = h_cust_pkg.pkgnum AND".
218       " cust_pkg_reason.date = h_cust_pkg.$field AND".
219       " history_action = 'replace_new')";
220
221     my $extra_sql = 'AND h_cust_pkg.pkgnum IS NULL';
222
223     my @unmigrated = qsearch({ table   => 'cust_pkg_reason',
224                                hashref => { action => uc(substr($field,0,1)) },
225                                addl_from => $addl_from,
226                                select    => 'cust_pkg_reason.*',
227                                extra_sql => $extra_sql,
228                             }); 
229     foreach ( @unmigrated ) {
230
231       my $hashref = { pkgnum => $_->pkgnum,
232                       history_date   => $_->date,
233                     };
234
235       my @history = qsearch({ table     => 'h_cust_pkg',
236                               hashref   => $hashref,
237                               extra_sql => $action_replace,
238                               order_by  => 'ORDER BY history_action',
239                            });
240
241       my $fuzz = 0;
242       while (scalar(@history) < 2 && $fuzz < 3) {
243         $hashref->{history_date}++;
244         $fuzz++;
245         push @history, qsearch({ table    => 'h_cust_pkg',
246                                  hashref  => $hashref,
247                                  extra_sql => $action_replace,
248                                  order_by => 'ORDER BY history_action',
249                               });
250       }
251
252       next unless scalar(@history) == 2;
253
254       my @new = grep { $_->history_action eq 'replace_new' } @history;
255       my @old = grep { $_->history_action eq 'replace_old' } @history;
256     
257       next if (scalar(@new) == 2 || scalar(@old) == 2);
258
259       $_->date($new[0]->get($field))
260         if ( $new[0]->get($field) &&
261              ( !$old[0]->get($field) ||
262                 $old[0]->get($field) != $new[0]->get($field)
263              )
264            );
265
266       my $error = $_->replace
267         if $_->modified;
268
269       die $error if $error;
270     }
271   }
272
273   #seek cancels/suspends without reason, but with expire/adjourn reason
274   foreach my $field (qw( cancel susp )) {
275
276     my %precursor_map = ( 'cancel' => 'expire', 'susp' => 'adjourn' );
277     my $precursor = $precursor_map{$field};
278     my $preaction = uc(substr($precursor,0,1));
279     my $action    = uc(substr($field,0,1));
280     my $addl_from =
281       "LEFT JOIN cust_pkg_reason ON ".
282       "(cust_pkg.pkgnum = cust_pkg_reason.pkgnum AND".
283       " cust_pkg.$precursor = cust_pkg_reason.date AND".
284       " cust_pkg_reason.action = '$preaction') ".
285       "LEFT JOIN cust_pkg_reason AS target ON ".
286       "(cust_pkg.pkgnum = target.pkgnum AND".
287       " cust_pkg.$field = target.date AND".
288       " target.action = '$action')"
289     ;
290
291     my $extra_sql = "WHERE target.pkgnum IS NULL AND ".
292                     "cust_pkg.$field IS NOT NULL AND ".
293                     "cust_pkg.$field < cust_pkg.$precursor + 86400 AND ".
294                     "cust_pkg_reason.action = '$preaction'";
295
296     my @unmigrated = qsearch({ table     => 'cust_pkg',
297                                hashref   => { },
298                                select    => 'cust_pkg.*',
299                                addl_from => $addl_from,
300                                extra_sql => $extra_sql,
301                             }); 
302     foreach ( @unmigrated ) {
303       my $cpr = new FS::cust_pkg_reason { $_->last_cust_pkg_reason($precursor)->hash, 'num' => '' };
304       $cpr->date($_->get($field));
305       $cpr->action($action);
306
307       my $error = $cpr->insert;
308       die $error if $error;
309     }
310   }
311
312   #still can't fill in an action?  don't abort the upgrade
313   local($ignore_empty_action) = 1;
314
315   $class->_upgrade_otaker(%opts);
316 }
317
318 =back
319
320 =head1 BUGS
321
322 Here be termites.  Don't use on wooden computers.
323
324 =head1 SEE ALSO
325
326 L<FS::Record>, schema.html from the base documentation.
327
328 =cut
329
330 1;
331