(start of) customer move script, RT#5351
[freeside.git] / bin / move-customers
1 #!/usr/bin/perl -w
2
3 #script to move customers from one installation to another
4 # script is kinda-specific to a somewhat old source installation (1.7? older?)
5 # target installation has to be 1.9 (after 9/2009)
6
7 use strict;
8 use vars qw( $sdbh );
9 use DBI;
10 use FS::UID qw( adminsuidsetup dbh );
11 use FS::Schema qw( dbdef );
12 use FS::Record qw( qsearchs );
13 use FS::agent;
14 use FS::cust_main;
15 use FS::part_pkg;
16 use FS::part_svc;
17 use FS::cust_bill_ApplicationCommon;
18 use FS::svc_Common;
19
20 my $DANGEROUS = 1;
21 my $DRY = 0;
22
23 #ssh -p 2222 -L 1080:66.209.32.4:7219 -L 5454:localhost:5432 66.209.32.4
24
25 #my $source_datasrc = 'DBI:Pg:host=66.209.32.4;dbname=freeside;sslmode=require';
26 my $source_datasrc = 'DBI:Pg:host=localhost;port=5454;dbname=freeside';
27 my $source_user = 'readonly';
28 my $source_pw = '';
29
30 #my @source_agents = ( 2, 7, 3, 4, 5, 1 );
31 my @source_agents = ( 1, 2, 3, 4, 5, 7 );
32
33
34 my $dest_agent_typenum = 1; #?
35
36 my $dest_refnum = 1; #XXX
37
38 my $dest_legacy_credit_reasontype = 4;
39
40 my $dest_pkg_classnum = 1;
41
42 my %domsvc_map = (
43   1    => 1,
44   3653 => 1,
45   7634 => 1,
46 );
47
48 # XXX set passwordmin conf to 4
49 # XXX set passwordmax conf to 21
50
51 #--
52
53 my $user = shift
54   or die "Usage:\n  (edit variables at top of script and then)\n".
55          "  move-customers user\n";
56 adminsuidsetup $user;
57
58 $sdbh = DBI->connect($source_datasrc, $source_user, $source_pw)
59   or die $DBI::errstr;
60
61 $sdbh->{ChopBlanks} = 1;
62
63 import_table('pkg_class', 'nomap' => 1);
64 import_table('svc_acct_pop', 'nomap' => 1);
65
66 my $agent_sth = $sdbh->prepare(
67   'SELECT * FROM agent WHERE agentnum IN ( '. join(',', @source_agents ). ')'
68 ) or die $sdbh->errstr;
69
70 $agent_sth->execute or die $agent_sth->errstr;
71
72 my %map = ();
73 $map{'_DOMSVC'} = \%domsvc_map;
74
75 $FS::cust_main::ignore_expired_card = 1;
76 $FS::cust_main::ignore_expired_card = 1;
77
78 $FS::part_pkg::skip_pkg_svc_hack = 1;
79 $FS::part_pkg::skip_pkg_svc_hack = 1;
80
81 $FS::cust_bill_ApplicationCommon::skip_apply_to_lineitems_hack = 1;
82 $FS::cust_bill_ApplicationCommon::skip_apply_to_lineitems_hack = 1;
83
84 $FS::svc_Common::noexport_hack = 1;
85 $FS::svc_Common::noexport_hack = 1;
86
87 while ( my $agentrow = $agent_sth->fetchrow_hashref ) {
88
89   my $src_agent = $agentrow->{'agent'};
90
91   warn "importing customers for $src_agent\n";
92
93   my $agent = qsearchs('agent', { 'agent' => $src_agent } );
94
95   if ( $agent ) {
96
97     warn "  using existing agentnum ". $agent->agentnum. "\n";
98
99     if ( $DANGEROUS ) {
100       warn "DELETING ALL CUSTOMERS OF $src_agent locally \n";
101
102       foreach my $statement (
103         'DELETE FROM cust_main WHERE agentnum = '. $agent->agentnum,
104         ( map { "DELETE FROM $_
105                    WHERE 0 = ( SELECT COUNT(*) FROM cust_main
106                                  WHERE cust_main.custnum = $_.custnum )
107                 "
108               }
109               qw(
110                   cust_credit
111                   cust_main_invoice
112                   cust_main_note
113                   cust_pay
114                   cust_refund
115                 )
116         )
117         #part_pkg, pkg_svc, part_svc, part_svc_column
118         #pkg_class
119       ) {
120
121         #warn $statement;
122         my $sth = dbh->prepare($statement) or die dbh->errstr;
123         $sth->execute or die $sth->errstr;
124
125       }
126
127       dbh->commit or die dbh->errstr;
128
129     }
130
131   } else {
132
133     warn "  creating new agent...\n";
134
135     $agent = new FS::agent { 'agent' => $src_agent,
136                              'typenum' => $dest_agent_typenum };
137     my $error = $agent->insert;
138     die $error if $error;
139
140     warn "  agentnum ". $agent->agentnum. "\n";
141
142   }
143
144   $map{'agent'}->{ $agentrow->{'agentnum'} } = $agent->agentnum;
145
146 }
147
148   #my $customer_sth = $sdbh->prepare(
149   #  'SELECT * FROM cust_main WHERE agentnum = '. $agentrow->{'agentnum'}
150   #) or die $sdbh->errstr;
151 my $customer_sth = $sdbh->prepare(
152   'SELECT * FROM cust_main WHERE agentnum IN ( '. join(',', @source_agents ). ')
153      ORDER BY custnum'
154 ) or die $sdbh->errstr;
155
156 $customer_sth->execute or die $customer_sth->errstr;
157
158 while ( my $customerrow = $customer_sth->fetchrow_hashref ) {
159
160     #use Data::Dumper;
161     # warn Dumper($customerrow);
162     my $src_custnum = $customerrow->{'custnum'};
163
164     warn "   $src_custnum has referral_custnum ". $customerrow->{'referral_custnum'}
165       if $customerrow->{'referral_custnum'};
166
167     my $cust_main = new FS::cust_main {
168       %{ $customerrow },
169       'custnum'      => '',
170       'referral_custnum' => '', #restore afterwords?
171       'refnum'       => $dest_refnum,
172       'agentnum'     => $map{'agent'}->{ $customerrow->{'agentnum'} },
173       'agent_custid' => $src_custnum,
174     };
175
176     #$cust_main->ship_country('') if $cust_main->ship_country eq '  ';
177     #$cust_main->tax('') if $cust_main->tax =~ /^\s+$/;
178
179     my $error = $cust_main->insert;
180     if ( $error ) {
181       warn "*** WARNING: error importing customer src custnum $src_custnum: $error";
182       use Data::Dumper;
183       warn Dumper($cust_main) if $src_custnum == 6854;
184       next;
185     }
186
187     warn "inserting dest customer ". $cust_main->custnum. " for $src_custnum\n";
188
189     $map{'cust_main'}->{$src_custnum} = $cust_main->custnum;
190
191     #easy direct cust_main relations:
192
193     #XXX ivan showing up as cust_pay otaker?  just deal?
194
195     foreach my $table ( qw(
196       cust_main_note
197       cust_pay
198     ) ) {
199       import_table( $table, 'custnum' => $src_custnum );
200     }
201
202     # crap, cust_credit.reason is text in old db
203 #*** WARNING: error importing cust_credit src crednum 2200: failed to set reason for [ FS::cust_credit ]:  at ./move-customers line 232.
204     import_table( 'cust_credit', 'custnum' => $src_custnum,
205       'insert_opts' => [ 'reason_type' => $dest_legacy_credit_reasontype ],
206       'preinsert_callback' => sub {
207         my($row, $object) = @_;
208         $object->set('reason', '(none)') if $object->get('reason') =~ /^\s*$/;
209       },
210     );
211
212     import_table( 'cust_refund', 'custnum' => $src_custnum,
213       'post_callback' => sub {
214         #my( $src_refundnum, $dst_refundnum ) = @_;
215         my $src_refundnum = shift;
216
217         # cust_credit_refund (map refundnum and crednum...)
218         import_table( 'cust_credit_refund',
219                       'refundnum' => $src_refundnum,
220                       'search'    => 'refundnum',
221                       'map'       => 'cust_refund',
222                       'map2'      => 'cust_credit',
223                       'map2key'   => 'crednum',
224                     );
225
226         # cust_pay_refund (map refundnum and paynum...)
227         import_table( 'cust_pay_refund',
228                       'refundnum' => $src_refundnum,
229                       'search'    => 'refundnum',
230                       'map'       => 'cust_refund',
231                       'map2'      => 'cust_pay',
232                       'map2key'   => 'paynum',
233                     );
234
235       },
236     );
237
238     # dunno what's up with this (ship_country '  ', fixed)
239 #*** WARNING: error importing customer src custnum 6854: Illegal (name) (error code illegal_name) ship_last:  at ./move-customers line 129.
240
241     # XXX cust_pay_void (something w/ paynum???  huh)  or just deal?  there's only 110
242
243     # (not in old db: cust_attachment, cust_statement, cust_location,
244     #  cust_main_exemption, cust_pay_pending )
245     # (not used in old db: cust_pay_batch, cust_tax_exempt)
246     # (not useful to migrate: queue)
247
248     #werid direct cust_main relations: 
249
250     # cust_pkg (part_pkg, part_svc, etc.)
251     import_table( 'cust_pkg', 'custnum' => $src_custnum,
252       'preinsert_callback' => sub {
253         my($row, $object) = @_;
254         my $src_pkgpart = $row->{'pkgpart'} or die "wtf";
255         my $dest_pkgpart = $map{'part_pkg'}->{$src_pkgpart};
256         if ( $dest_pkgpart ) {
257           $object->pkgpart($dest_pkgpart);
258           return;
259         }
260
261         my $sth = $sdbh->prepare(
262           "SELECT * FROM part_pkg WHERE pkgpart = $src_pkgpart"
263         ) or die $sdbh->errstr;
264
265         $sth->execute or die $sth->errstr;
266
267         my $part_pkg_row = $sth->fetchrow_hashref
268           or die "cust_pkg.pkgpart missing in part_pkg?!";
269
270         my $hashref = {
271           %{ $part_pkg_row },
272           'pkgpart'  => '',
273         };
274         my $src_classnum = $part_pkg_row->{'classnum'};
275         $hashref->{'classnum'} = $map{'pkg_class'}->{ $src_classnum }
276           if $src_classnum;
277
278         my $part_pkg = new FS::part_pkg $hashref;
279
280         #$part_pkg->setuptax('') if $part_pkg->setuptax =~ /^\s+$/;
281         #$part_pkg->recurtax('') if $part_pkg->recurtax =~ /^\s+$/;
282
283         my $error = $part_pkg->insert( 'options' => {} );
284         die "*** FATAL: error importing part_pkg src pkgpart $src_pkgpart ".
285             ": $error"
286           if $error;
287
288         $map{ 'part_pkg' }->{ $part_pkg_row->{'pkgpart'} } = $part_pkg->pkgpart;
289         
290         # part_pkg_option
291         import_table( 'part_pkg_option',
292                       'pkgpart' => $src_pkgpart,
293                       'search' => 'pkgpart',
294                       'map'    => 'part_pkg',
295                     );
296         
297         my $osth = $sdbh->prepare(
298           "SELECT * FROM part_pkg_option WHERE pkgpart = $src_pkgpart"
299         ) or die $sdbh->errstr;
300
301         # pkg_svc, part_svc, part_svc_column
302         import_table( 'pkg_svc',
303           'pkgpart' => $src_pkgpart,
304           'search'  => 'pkgpart',
305           'map'     => 'part_pkg',
306           'preinsert_callback' => sub {
307
308             my($row, $object) = @_;
309             my $src_svcpart = $row->{'svcpart'} or die "wtf2";
310             my $dest_svcpart = $map{'part_svc'}->{$src_svcpart};
311             if ( $dest_svcpart ) {
312               $object->svcpart($dest_svcpart);
313               return;
314             }
315
316             my $sth = $sdbh->prepare(
317               "SELECT * FROM part_svc WHERE svcpart = $src_svcpart"
318             ) or die $sdbh->errstr;
319
320             $sth->execute or die $sth->errstr;
321
322             my $part_svc_row = $sth->fetchrow_hashref
323               or die "svcpart missing in part_svc?!";
324
325             my $hashref = {
326               %{ $part_svc_row },
327               'svcpart' => '',
328             };
329
330             my $part_svc = new FS::part_svc $hashref;
331             $part_svc->disabled('') if $part_svc->disabled =~ /^\s+$/;
332             my $error = $part_svc->insert;
333             die "*** FATAL: error importing part_svc src svcpart $src_svcpart ".
334                 ": $error"
335               if $error;
336
337             $map{ 'part_svc' }->{ $part_svc_row->{'svcpart'} } = $part_svc->svcpart;
338
339             # part_svc_column
340             import_table( 'part_svc_column',
341                           'svcpart' => $src_svcpart,
342                           'search'  => 'svcpart',
343                           'map'     => 'part_svc',
344                           'preinsert_callback' => sub {
345                             my($row, $object) = @_;
346                             if ( $object->columnname eq 'domsvc' ) {
347                                $object->columnvalue( $map{'_DOMSVC'}->{ $object->columnvalue } );
348                             }
349                           },
350                         );
351         
352             #what we came here for in the first place
353             $object->svcpart( $part_svc->svcpart );
354
355           }
356         );
357
358         #what we came here for in the first place
359         $object->pkgpart( $part_pkg->pkgpart );
360
361       },
362
363       'post_callback' => sub {
364         #my( $src_pkgnum, $dst_pkgnum ) = @_;
365         my $src_pkgnum = shift;
366
367         #cust_svc
368         import_table( 'cust_svc',
369                         'pkgnum'  => $src_pkgnum,
370                         'search'  => 'pkgnum',
371                         'map'     => 'cust_pkg',
372                         'map2'    => 'part_svc',
373                         'map2key' => 'svcpart',
374                         'post_callback' => sub {
375                           #my( $src_svcnum, $dst_svcnum ) = @_;
376                           my $src_svcnum = shift;
377
378                           #svc_domain
379                           import_table( 'svc_domain',
380                                           'svcnum' => $src_svcnum,
381                                           'search' => 'svcnum',
382                                           'map'    => 'cust_svc',
383                                           'noblank_primary' => 1,
384                                       );
385
386                           #svc_acct
387                           import_table( 'svc_acct',
388                                           'svcnum'  => $src_svcnum,
389                                           'search'  => 'svcnum',
390                                           'map'     => 'cust_svc',
391                                           'noblank_primary' => 1,
392                                           'map2'    => 'svc_acct_pop',
393                                           'map2key' => 'popnum',
394                                           #'map3'    => 'svc_domain',
395                                           'map3'    => '_DOMSVC',
396                                           'map3key' => 'domsvc',
397                                       );
398
399                           #radius_usergroup
400                           import_table( 'radius_usergroup',
401                                           'svcnum' => $src_svcnum,
402                                           'search' => 'svcnum',
403                                           'map'    => 'cust_svc',
404                                       );
405
406                           #other svc_ tables not in old db
407
408                         },
409                     );
410
411       },
412
413
414
415
416     );
417     # end of cust_pkg (part_pkg, part_svc, etc.)
418
419     # cust_bill (invnum move)
420     import_table( 'cust_bill', 'custnum' => $src_custnum,
421       'preinsert_callback' => sub {
422         my($row, $object) = @_;
423         $object->agent_invid( $row->{'invnum'} );
424       },
425       'post_callback' => sub {
426         #my( $src_invnum, $dst_invnum ) = @_;
427         my $src_invnum = shift;
428
429         # cust_bill_pkg ( map invnum and pkgnum... )
430         import_table( 'cust_bill_pkg',
431                       'invnum' => $src_invnum,
432                       'search'  => 'invnum',
433                       'map'     => 'cust_bill',
434                       'map2'    => 'cust_pkg',
435                       'map2key' => 'pkgnum',
436                       'post_callback' => sub {
437                         my $src_billpkgnum = shift;
438
439                         import_table( 'cust_bill_pkg_detail',
440                                       'billpkgnum' => $src_billpkgnum,
441                                       'search'    => 'billpkgnum',
442                                       'map'       => 'cust_bill_pkg',
443                                       'addl_from' => 'left join cust_bill_pkg using ( invnum, pkgnum )',
444                                     );
445
446                       },
447                     );
448
449         # cust_credit_bill (map invnum and crednum... )
450         import_table( 'cust_credit_bill',
451                       'invnum' => $src_invnum,
452                       'search'  => 'invnum',
453                       'map'     => 'cust_bill',
454                       'map2'    => 'cust_credit',
455                       'map2key' => 'crednum',
456                       'post_callback' => sub {
457                         my $src_creditbillnum = shift;
458                         #map creditbillnum and billpkgnum
459                         import_table( 'cust_credit_bill_pkg',
460                                       'creditbillnum' => $src_creditbillnum,
461                                       'search'    => 'creditbillnum',
462                                       'map'       => 'cust_credit_bill',
463                                       'map2'      => 'cust_bill_pkg',
464                                       'map2key'   => 'billpkgnum',
465                                     );
466
467                       },
468                     );
469
470         # cust_bill_pay (map invnum and paynum...)
471         import_table( 'cust_bill_pay',
472                       'invnum' => $src_invnum,
473                       'search'  => 'invnum',
474                       'map'     => 'cust_bill',
475                       'map2'    => 'cust_pay',
476                       'map2key' => 'paynum',
477                       'post_callback' => sub {
478                         my $src_billpaynum = shift;
479                         #map billpaynum and billpkgnum
480                         import_table( 'cust_bill_pay_pkg',
481                                       'billpaynum' => $src_billpaynum,
482                                       'search'    => 'billpaynum',
483                                       'map'       => 'cust_bill_pay',
484                                       'map2'      => 'cust_bill_pkg',
485                                       'map2key'   => 'billpkgnum',
486                                     );
487                       },
488                     );
489       },
490     );
491
492     # ---
493
494     # XXX
495     # cust_pkg_reason (shit, and bring in/remap reasons)
496
497     # (not in old db: cust_pkg_detail)
498     # (not used in old db: cust_bill_pay_batch, cust_pkg_option)
499
500     # ---
501
502     # (not in old db: cust_bill_pkg_display, cust_bill_pkg_tax_location,
503     #  cust_bill_pkg_tax_rate_location, cust_tax_adjustment, cust_svc_option, )
504     # (not used in old db: cust_tax_exempt_pkg)
505
506     #XXX then:
507     #need to do something about events. mark initial stuff as done or something?
508     # what else?  that's it?
509
510     #do this last, so no notices go out
511     import_table( 'cust_main_invoice', 'custnum' => $src_custnum );
512
513     #dbh->commit or die dbh->errstr;
514     warn "customer ". $cust_main->custnum. " inserted\n";
515     #exit;
516
517 }
518
519
520 warn "import successful!\n";
521 if ( $DRY ) {
522   warn "rolling back (dry run)\n";
523   dbh->rollback or die dbh->errstr;
524   warn "rolled back\n"
525 } else {
526   warn "commiting\n";
527   dbh->commit or die dbh->errstr;
528   warn "committed\n";
529 }
530
531 sub import_table {
532   my( $table, %opt ) = @_;
533
534   eval "use FS::$table;";
535   die $@ if $@;
536
537   my $map = $opt{'map'} || 'cust_main';
538   my $search = $opt{'search'} || 'custnum';
539
540   $opt{'insert_opts'} ||= [];
541
542   my $primary_key = dbdef->table($table)->primary_key;
543
544   my $addl_from = defined($opt{'addl_from'}) ? $opt{'addl_from'} : '';
545
546   my $sth = $sdbh->prepare(
547     "SELECT * FROM $table $addl_from ".
548     ( $opt{'nomap'} ? '' : " WHERE $search = ". $opt{$search} )
549   ) or die $sdbh->errstr;
550
551   $sth->execute or die "(searching $table): ". $sth->errstr;
552
553   while ( my $row = $sth->fetchrow_hashref ) {
554     #my $src_custnum = $customerrow->{'custnum'};
555
556     my $hashref = { %$row };
557     $hashref->{$primary_key} = ''
558       unless $opt{'noblank_primary'};
559     $hashref->{ $search } = $map{$map}->{ $row->{$search} }
560       unless $opt{'nomap'};
561
562     if ( $opt{'map2'} ) {
563       my $key2 = $opt{'map2key'};
564       $hashref->{$key2} = $map{ $opt{'map2'} }->{ $row->{$key2} }
565         unless $opt{map2key} eq 'pkgnum' && (    $row->{$key2} eq '0'
566                                               || $row->{$key2} eq '-1'
567                                             )
568             or ! defined($row->{$key2})
569             or $row->{$key2} eq '';
570       #warn "map $opt{map2}.$opt{map2key}: ". $row->{$key2}. " to ". $map{ $opt{'map2'} }->{ $row->{$key2} };
571     }
572
573     if ( $opt{'map3'} ) {
574       my $key3 = $opt{'map3key'};
575       $hashref->{$key3} = $map{ $opt{'map3'} }->{ $row->{$key3} };
576     }
577
578     my $object = eval "new FS::$table \$hashref;";
579     die $@ if $@;
580
581     &{ $opt{preinsert_callback} }( $row, $object )
582       if $opt{preinsert_callback};
583
584     my $error = $object->insert( @{ $opt{'insert_opts'} } );
585     if ( $error ) {
586       warn "*** WARNING: error importing $table src $primary_key ". $row->{$primary_key}. ": $error";
587       next;
588     }
589
590     $map{ $table }->{ $row->{$primary_key} } = $object->get($primary_key);
591
592     &{ $opt{post_callback} }( $row->{$primary_key}, $object->get($primary_key) )
593       if $opt{post_callback};
594
595   }
596
597 }
598
599 1;
600