Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / Upgrade.pm
1 package FS::Upgrade;
2
3 use strict;
4 use vars qw( @ISA @EXPORT_OK $DEBUG );
5 use Exporter;
6 use Tie::IxHash;
7 use File::Slurp;
8 use FS::UID qw( dbh driver_name );
9 use FS::Conf;
10 use FS::Record qw(qsearchs qsearch str2time_sql);
11 use FS::upgrade_journal;
12
13 use FS::svc_domain;
14 $FS::svc_domain::whois_hack = 1;
15
16 @ISA = qw( Exporter );
17 @EXPORT_OK = qw( upgrade_schema upgrade_config upgrade upgrade_sqlradius );
18
19 $DEBUG = 1;
20
21 =head1 NAME
22
23 FS::Upgrade - Database upgrade routines
24
25 =head1 SYNOPSIS
26
27   use FS::Upgrade;
28
29 =head1 DESCRIPTION
30
31 Currently this module simply provides a place to store common subroutines for
32 database upgrades.
33
34 =head1 SUBROUTINES
35
36 =over 4
37
38 =item upgrade_config
39
40 =cut
41
42 #config upgrades
43 sub upgrade_config {
44   my %opt = @_;
45
46   my $conf = new FS::Conf;
47
48   $conf->touch('payment_receipt')
49     if $conf->exists('payment_receipt_email')
50     || $conf->config('payment_receipt_msgnum');
51
52   $conf->touch('geocode-require_nw_coordinates')
53     if $conf->exists('svc_broadband-require-nw-coordinates');
54
55   unless ( $conf->config('echeck-country') ) {
56     if ( $conf->exists('cust_main-require-bank-branch') ) {
57       $conf->set('echeck-country', 'CA');
58     } elsif ( $conf->exists('echeck-nonus') ) {
59       $conf->set('echeck-country', 'XX');
60     } else {
61       $conf->set('echeck-country', 'US');
62     }
63   }
64
65   upgrade_overlimit_groups($conf);
66   map { upgrade_overlimit_groups($conf,$_->agentnum) } qsearch('agent', {});
67
68   my $DIST_CONF = '/usr/local/etc/freeside/default_conf/';#DIST_CONF in Makefile
69   $conf->set($_, scalar(read_file( "$DIST_CONF/$_" )) )
70     foreach grep { ! $conf->exists($_) && -s "$DIST_CONF/$_" }
71       qw( quotation_html quotation_latex quotation_latexnotes );
72
73   # change 'fslongtable' to 'longtable'
74   # in invoice and quotation main templates, and also in all secondary 
75   # invoice templates
76   my @latex_confs =
77     qsearch('conf', { 'name' => {op=>'LIKE', value=>'%latex%'} });
78
79   foreach my $c (@latex_confs) {
80     my $value = $c->value;
81     if (length($value) and $value =~ /fslongtable/) {
82       $value =~ s/fslongtable/longtable/g;
83       $conf->set($c->name, $value, $c->agentnum);
84     }
85   }
86
87 }
88
89 sub upgrade_overlimit_groups {
90     my $conf = shift;
91     my $agentnum = shift;
92     my @groups = $conf->config('overlimit_groups',$agentnum); 
93     if(scalar(@groups)) {
94         my $groups = join(',',@groups);
95         my @groupnums;
96         my $error = '';
97         if ( $groups !~ /^[\d,]+$/ ) {
98             foreach my $groupname ( @groups ) {
99                 my $g = qsearchs('radius_group', { 'groupname' => $groupname } );
100                 unless ( $g ) {
101                     $g = new FS::radius_group {
102                                     'groupname' => $groupname,
103                                     'description' => $groupname,
104                                     };
105                     $error = $g->insert;
106                     die $error if $error;
107                 }
108                 push @groupnums, $g->groupnum;
109             }
110             $conf->set('overlimit_groups',join("\n",@groupnums),$agentnum);
111         }
112     }
113 }
114
115 =item upgrade
116
117 =cut
118
119 sub upgrade {
120   my %opt = @_;
121
122   my $data = upgrade_data(%opt);
123
124   my $oldAutoCommit = $FS::UID::AutoCommit;
125   local $FS::UID::AutoCommit = 0;
126   local $FS::UID::AutoCommit = 0;
127
128   foreach my $table ( keys %$data ) {
129
130     my $class = "FS::$table";
131     eval "use $class;";
132     die $@ if $@;
133
134     if ( $class->can('_upgrade_data') ) {
135       warn "Upgrading $table...\n";
136
137       my $start = time;
138
139       $class->_upgrade_data(%opt);
140
141       if ( $oldAutoCommit ) {
142         warn "  committing\n";
143         dbh->commit or die dbh->errstr;
144       }
145       
146       #warn "\e[1K\rUpgrading $table... done in ". (time-$start). " seconds\n";
147       warn "  done in ". (time-$start). " seconds\n";
148
149     } else {
150       warn "WARNING: asked for upgrade of $table,".
151            " but FS::$table has no _upgrade_data method\n";
152     }
153
154 #    my @records = @{ $data->{$table} };
155 #
156 #    foreach my $record ( @records ) {
157 #      my $args = delete($record->{'_upgrade_args'}) || [];
158 #      my $object = $class->new( $record );
159 #      my $error = $object->insert( @$args );
160 #      die "error inserting record into $table: $error\n"
161 #        if $error;
162 #    }
163
164   }
165
166   local($FS::cust_main::ignore_expired_card) = 1;
167   local($FS::cust_main::ignore_illegal_zip) = 1;
168   local($FS::cust_main::ignore_banned_card) = 1;
169   local($FS::cust_main::skip_fuzzyfiles) = 1;
170
171   # decrypt inadvertantly-encrypted payinfo where payby != CARD,DCRD,CHEK,DCHK
172   # kind of a weird spot for this, but it's better than duplicating
173   # all this code in each class...
174   my @decrypt_tables = qw( cust_main cust_pay_void cust_pay cust_refund cust_pay_pending );
175   foreach my $table ( @decrypt_tables ) {
176       my @objects = qsearch({
177         'table'     => $table,
178         'hashref'   => {},
179         'extra_sql' => "WHERE payby NOT IN ( 'CARD', 'DCRD', 'CHEK', 'DCHK' ) ".
180                        " AND LENGTH(payinfo) > 100",
181       });
182       foreach my $object ( @objects ) {
183           my $payinfo = $object->decrypt($object->payinfo);
184           die "error decrypting payinfo" if $payinfo eq $object->payinfo;
185           $object->payinfo($payinfo);
186           my $error = $object->replace;
187           die $error if $error;
188       }
189   }
190
191 }
192
193 =item upgrade_data
194
195 =cut
196
197 sub upgrade_data {
198   my %opt = @_;
199
200   tie my %hash, 'Tie::IxHash', 
201
202     #cust_main (remove paycvv from history)
203     'cust_main' => [],
204
205     #msgcat
206     'msgcat' => [],
207
208     #reason type and reasons
209     'reason_type'     => [],
210     'cust_pkg_reason' => [],
211
212     #need part_pkg before cust_credit...
213     'part_pkg' => [],
214
215     #customer credits
216     'cust_credit' => [],
217
218     #duplicate history records
219     'h_cust_svc'  => [],
220
221     #populate cust_pay.otaker
222     'cust_pay'    => [],
223
224     #populate part_pkg_taxclass for starters
225     'part_pkg_taxclass' => [],
226
227     #remove bad pending records
228     'cust_pay_pending' => [],
229
230     #replace invnum and pkgnum with billpkgnum
231     'cust_bill_pkg_detail' => [],
232
233     #usage_classes if we have none
234     'usage_class' => [],
235
236     #phone_type if we have none
237     'phone_type' => [],
238
239     #fixup access rights
240     'access_right' => [],
241
242     #change recur_flat and enable_prorate
243     'part_pkg_option' => [],
244
245     #add weights to pkg_category
246     'pkg_category' => [],
247
248     #cdrbatch fixes
249     'cdr' => [],
250
251     #otaker->usernum
252     'cust_attachment' => [],
253     #'cust_credit' => [],
254     #'cust_main' => [],
255     'cust_main_note' => [],
256     #'cust_pay' => [],
257     'cust_pay_void' => [],
258     'cust_pkg' => [],
259     #'cust_pkg_reason' => [],
260     'cust_pkg_discount' => [],
261     'cust_refund' => [],
262     'banned_pay' => [],
263
264     #default namespace
265     'payment_gateway' => [],
266
267     #migrate to templates
268     'msg_template' => [],
269
270     #return unprovisioned numbers to availability
271     'phone_avail' => [],
272
273     #insert scripcondition
274     'TicketSystem' => [],
275     
276     #insert LATA data if not already present
277     'lata' => [],
278     
279     #insert MSA data if not already present
280     'msa' => [],
281
282     # migrate to radius_group and groupnum instead of groupname
283     'radius_usergroup' => [],
284     'part_svc'         => [],
285     'part_export'      => [],
286
287     #insert default tower_sector if not present
288     'tower' => [],
289
290     #routernum/blocknum
291     'svc_broadband' => [],
292
293     #set up payment gateways if needed
294     'pay_batch' => [],
295
296     #flag monthly tax exemptions
297     'cust_tax_exempt_pkg' => [],
298
299     #kick off tax location history upgrade
300     'cust_bill_pkg' => [],
301   ;
302
303   \%hash;
304
305 }
306
307 =item upgrade_schema
308
309 =cut
310
311 sub upgrade_schema {
312   my %opt = @_;
313
314   my $data = upgrade_schema_data(%opt);
315
316   my $oldAutoCommit = $FS::UID::AutoCommit;
317   local $FS::UID::AutoCommit = 0;
318   local $FS::UID::AutoCommit = 0;
319
320   foreach my $table ( keys %$data ) {
321
322     my $class = "FS::$table";
323     eval "use $class;";
324     die $@ if $@;
325
326     if ( $class->can('_upgrade_schema') ) {
327       warn "Upgrading $table schema...\n";
328
329       my $start = time;
330
331       $class->_upgrade_schema(%opt);
332
333       if ( $oldAutoCommit ) {
334         warn "  committing\n";
335         dbh->commit or die dbh->errstr;
336       }
337       
338       #warn "\e[1K\rUpgrading $table... done in ". (time-$start). " seconds\n";
339       warn "  done in ". (time-$start). " seconds\n";
340
341     } else {
342       warn "WARNING: asked for schema upgrade of $table,".
343            " but FS::$table has no _upgrade_schema method\n";
344     }
345
346   }
347
348 }
349
350 =item upgrade_schema_data
351
352 =cut
353
354 sub upgrade_schema_data {
355   my %opt = @_;
356
357   tie my %hash, 'Tie::IxHash', 
358
359     #fix classnum character(1)
360     'cust_bill_pkg_detail' => [],
361     #add necessary columns to RT schema
362     'TicketSystem' => [],
363
364   ;
365
366   \%hash;
367
368 }
369
370 sub upgrade_sqlradius {
371   #my %opt = @_;
372
373   my $conf = new FS::Conf;
374
375   my @part_export = FS::part_export::sqlradius->all_sqlradius_withaccounting();
376
377   foreach my $part_export ( @part_export ) {
378
379     my $errmsg = 'Error adding FreesideStatus to '.
380                  $part_export->option('datasrc'). ': ';
381
382     my $dbh = DBI->connect(
383       ( map $part_export->option($_), qw ( datasrc username password ) ),
384       { PrintError => 0, PrintWarn => 0 }
385     ) or do {
386       warn $errmsg.$DBI::errstr;
387       next;
388     };
389
390     my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
391     my $group = "UserName";
392     $group .= ",Realm"
393       if ref($part_export) =~ /withdomain/
394       || $dbh->{Driver}->{Name} =~ /^Pg/; #hmm
395
396     my $sth_alter = $dbh->prepare(
397       "ALTER TABLE radacct ADD COLUMN FreesideStatus varchar(32) NULL"
398     );
399     if ( $sth_alter ) {
400       if ( $sth_alter->execute ) {
401         my $sth_update = $dbh->prepare(
402          "UPDATE radacct SET FreesideStatus = 'done' WHERE FreesideStatus IS NULL"
403         ) or die $errmsg.$dbh->errstr;
404         $sth_update->execute or die $errmsg.$sth_update->errstr;
405       } else {
406         my $error = $sth_alter->errstr;
407         warn $errmsg.$error
408           unless $error =~ /Duplicate column name/i  #mysql
409               || $error =~ /already exists/i;        #Pg
410 ;
411       }
412     } else {
413       my $error = $dbh->errstr;
414       warn $errmsg.$error; #unless $error =~ /exists/i;
415     }
416
417     my $sth_index = $dbh->prepare(
418       "CREATE INDEX FreesideStatus ON radacct ( FreesideStatus )"
419     );
420     if ( $sth_index ) {
421       unless ( $sth_index->execute ) {
422         my $error = $sth_index->errstr;
423         warn $errmsg.$error
424           unless $error =~ /Duplicate key name/i #mysql
425               || $error =~ /already exists/i;    #Pg
426       }
427     } else {
428       my $error = $dbh->errstr;
429       warn $errmsg.$error. ' (preparing statement)';#unless $error =~ /exists/i;
430     }
431
432     my $times = ($dbh->{Driver}->{Name} =~ /^mysql/)
433       ? ' AcctStartTime != 0 AND AcctStopTime != 0 '
434       : ' AcctStartTime IS NOT NULL AND AcctStopTime IS NOT NULL ';
435
436     my $sth = $dbh->prepare("SELECT UserName,
437                                     Realm,
438                                     $str2time max(AcctStartTime)),
439                                     $str2time max(AcctStopTime))
440                               FROM radacct
441                               WHERE FreesideStatus = 'done'
442                                 AND $times
443                               GROUP BY $group
444                             ")
445       or die $errmsg.$dbh->errstr;
446     $sth->execute() or die $errmsg.$sth->errstr;
447   
448     while (my $row = $sth->fetchrow_arrayref ) {
449       my ($username, $realm, $start, $stop) = @$row;
450   
451       $username = lc($username) unless $conf->exists('username-uppercase');
452
453       my $exportnum = $part_export->exportnum;
454       my $extra_sql = " AND exportnum = $exportnum ".
455                       " AND exportsvcnum IS NOT NULL ";
456
457       if ( ref($part_export) =~ /withdomain/ ) {
458         $extra_sql = " AND '$realm' = ( SELECT domain FROM svc_domain
459                          WHERE svc_domain.svcnum = svc_acct.domsvc ) ";
460       }
461   
462       my $svc_acct = qsearchs({
463         'select'    => 'svc_acct.*',
464         'table'     => 'svc_acct',
465         'addl_from' => 'LEFT JOIN cust_svc   USING ( svcnum )'.
466                        'LEFT JOIN export_svc USING ( svcpart )',
467         'hashref'   => { 'username' => $username },
468         'extra_sql' => $extra_sql,
469       });
470
471       if ($svc_acct) {
472         $svc_acct->last_login($start)
473           if $start && (!$svc_acct->last_login || $start > $svc_acct->last_login);
474         $svc_acct->last_logout($stop)
475           if $stop && (!$svc_acct->last_logout || $stop > $svc_acct->last_logout);
476       }
477     }
478   }
479
480 }
481
482 =back
483
484 =head1 BUGS
485
486 Sure.
487
488 =head1 SEE ALSO
489
490 =cut
491
492 1;
493