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