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