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)
11 use FS::UID qw( adminsuidsetup dbh );
12 use FS::Schema qw( dbdef );
13 use FS::Record qw( qsearchs );
18 use FS::cust_bill_ApplicationCommon;
27 #ssh -p 2222 -L 1080:66.209.32.4:7219 -L 5454:localhost:5432 66.209.32.4
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';
34 #my @source_agents = ( 2, 7, 3, 4, 5, 1 );
35 my @source_agents = ( 1, 2, 3, 4, 5, 7 );
37 my $dest_agent_typenum = 12;
41 my $dest_legacy_credit_reasontype = 5;
43 my $dest_pkg_classnum = 6;
75 or die "Usage:\n (edit variables at top of script and then)\n".
76 " move-customers user\n";
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;
92 my $void_paynum = 2147483646; #top of int range
96 # source(remote) setup
98 $sdbh = DBI->connect($source_datasrc, $source_user, $source_pw)
101 $sdbh->{ChopBlanks} = 1;
106 $map{'_DOMSVC'} = \%domsvc_map;
108 import_table('pkg_class', 'nomap' => 1);
109 import_table('svc_acct_pop', 'nomap' => 1);
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',
120 my $agent_sth = $sdbh->prepare(
121 'SELECT * FROM agent WHERE agentnum IN ( '. join(',', @source_agents ). ')'
122 ) or die $sdbh->errstr;
124 $agent_sth->execute or die $agent_sth->errstr;
127 while ( my $agentrow = $agent_sth->fetchrow_hashref ) {
129 my $src_agent = $agentrow->{'agent'};
131 warn "importing customers for $src_agent\n";
133 my $agent = qsearchs('agent', { 'agent' => $src_agent, 'disabled' => '' } );
137 warn " using existing agentnum ". $agent->agentnum. "\n";
140 warn "DELETING ALL CUSTOMERS OF $src_agent locally \n";
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 )
157 #pkg_class, part_pkg_pop
158 #part_pkg, pkg_svc, part_svc, part_svc_column
159 #XXX more... does it matter?
163 my $sth = dbh->prepare($statement) or die dbh->errstr;
164 $sth->execute or die $sth->errstr;
168 dbh->commit or die dbh->errstr;
174 warn " creating new agent...\n";
176 $agent = new FS::agent { 'agent' => $src_agent,
177 'typenum' => $dest_agent_typenum };
178 my $error = $agent->insert;
179 die $error if $error;
181 warn " agentnum ". $agent->agentnum. "\n";
185 $map{'agent'}->{ $agentrow->{'agentnum'} } = $agent->agentnum;
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 ). ')
195 ) or die $sdbh->errstr;
197 $customer_sth->execute or die $customer_sth->errstr;
199 while ( my $customerrow = $customer_sth->fetchrow_hashref ) {
202 # warn Dumper($customerrow);
203 my $src_custnum = $customerrow->{'custnum'};
205 warn " $src_custnum has referral_custnum ". $customerrow->{'referral_custnum'}
206 if $customerrow->{'referral_custnum'};
208 my $cust_main = new FS::cust_main {
211 'referral_custnum' => '', #restore afterwords?
212 'refnum' => $dest_refnum,
213 'agentnum' => $map{'agent'}->{ $customerrow->{'agentnum'} },
214 'agent_custid' => $src_custnum,
217 #$cust_main->ship_country('') if $cust_main->ship_country eq ' ';
218 #$cust_main->tax('') if $cust_main->tax =~ /^\s+$/;
220 my $error = $cust_main->insert;
222 warn "*** WARNING: error importing customer src custnum $src_custnum: $error";
224 warn Dumper($cust_main) if $src_custnum == 6854;
228 warn "inserting dest customer ". $cust_main->custnum. " for $src_custnum\n";
230 $map{'cust_main'}->{$src_custnum} = $cust_main->custnum;
232 #now import the relations, easy and hard:
234 import_table( 'cust_main_note', 'custnum' => $src_custnum );
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) = @_;
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];
249 $cust_pay->otaker($otaker);
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*$/;
263 import_table( 'cust_refund', 'custnum' => $src_custnum,
264 'post_callback' => sub {
265 #my( $src_refundnum, $dst_refundnum ) = @_;
266 my $src_refundnum = shift;
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',
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',
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.
293 import_table( 'cust_pay_void', 'custnum' => $src_custnum,
294 'preinsert_callback' => sub {
295 my($row, $object) = @_;
296 $object->paynum( $void_paynum-- );
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)
305 #werid direct cust_main relations:
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);
318 my $sth = $sdbh->prepare(
319 "SELECT * FROM part_pkg WHERE pkgpart = $src_pkgpart"
320 ) or die $sdbh->errstr;
322 $sth->execute or die $sth->errstr;
324 my $part_pkg_row = $sth->fetchrow_hashref
325 or die "cust_pkg.pkgpart missing in part_pkg?!";
331 my $src_classnum = $part_pkg_row->{'classnum'};
332 $hashref->{'classnum'} = $map{'pkg_class'}->{ $src_classnum }
335 my $part_pkg = new FS::part_pkg $hashref;
337 #$part_pkg->setuptax('') if $part_pkg->setuptax =~ /^\s+$/;
338 #$part_pkg->recurtax('') if $part_pkg->recurtax =~ /^\s+$/;
340 my $error = $part_pkg->insert( 'options' => {} );
341 die "*** FATAL: error importing part_pkg src pkgpart $src_pkgpart ".
345 $map{ 'part_pkg' }->{ $part_pkg_row->{'pkgpart'} } = $part_pkg->pkgpart;
348 import_table( 'part_pkg_option',
349 'pkgpart' => $src_pkgpart,
350 'search' => 'pkgpart',
354 my $osth = $sdbh->prepare(
355 "SELECT * FROM part_pkg_option WHERE pkgpart = $src_pkgpart"
356 ) or die $sdbh->errstr;
358 # pkg_svc, part_svc, part_svc_column
359 import_table( 'pkg_svc',
360 'pkgpart' => $src_pkgpart,
361 'search' => 'pkgpart',
363 'preinsert_callback' => sub {
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);
373 my $sth = $sdbh->prepare(
374 "SELECT * FROM part_svc WHERE svcpart = $src_svcpart"
375 ) or die $sdbh->errstr;
377 $sth->execute or die $sth->errstr;
379 my $part_svc_row = $sth->fetchrow_hashref
380 or die "svcpart missing in part_svc?!";
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 ".
394 $map{ 'part_svc' }->{ $part_svc_row->{'svcpart'} } = $part_svc->svcpart;
397 import_table( 'part_svc_column',
398 'svcpart' => $src_svcpart,
399 'search' => 'svcpart',
401 'preinsert_callback' => sub {
402 my($row, $object) = @_;
403 if ( $object->columnname eq 'domsvc' ) {
404 $object->columnvalue( $map{'_DOMSVC'}->{ $object->columnvalue } );
409 #what we came here for in the first place
410 $object->svcpart( $part_svc->svcpart );
415 #what we came here for in the first place
416 $object->pkgpart( $part_pkg->pkgpart );
420 'post_callback' => sub {
421 #my( $src_pkgnum, $dst_pkgnum ) = @_;
422 my $src_pkgnum = shift;
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',
435 import_table( 'cust_svc',
436 'pkgnum' => $src_pkgnum,
437 'search' => 'pkgnum',
439 'map2' => 'part_svc',
440 'map2key' => 'svcpart',
441 'post_callback' => sub {
442 #my( $src_svcnum, $dst_svcnum ) = @_;
443 my $src_svcnum = shift;
446 import_table( 'svc_domain',
447 'svcnum' => $src_svcnum,
448 'search' => 'svcnum',
450 'noblank_primary' => 1,
454 import_table( 'svc_acct',
455 'svcnum' => $src_svcnum,
456 'search' => 'svcnum',
458 'noblank_primary' => 1,
459 'map2' => 'svc_acct_pop',
460 'map2key' => 'popnum',
461 #'map3' => 'svc_domain',
463 'map3key' => 'domsvc',
467 import_table( 'radius_usergroup',
468 'svcnum' => $src_svcnum,
469 'search' => 'svcnum',
473 #other svc_ tables not in old db
484 # end of cust_pkg (part_pkg, part_svc, etc.)
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'} );
492 'post_callback' => sub {
493 my( $src_invnum, $dst_invnum ) = @_;
494 #my $src_invnum = shift;
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;
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 )',
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',
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',
557 #need to do something about events. mark initial stuff as done
558 foreach my $eventpart ( @{ $eventparts{$cust_main->payby} } ) {
560 my $cust_event = new FS::cust_event {
561 'eventpart' => $eventpart,
562 'tablenum' => $dst_invnum,
563 '_date' => time, # XXX something? probably not
567 my $error = $cust_event->insert;
568 die "*** FATAL: error inserting cust_event for eventpart $eventpart,".
569 " tablenum (invnum) $dst_invnum: $error"
579 # (not in old db: cust_pkg_detail)
580 # (not used in old db: cust_bill_pay_batch, cust_pkg_option)
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)
588 #do this last, so no notices go out
589 import_table( 'cust_main_invoice', 'custnum' => $src_custnum );
591 #dbh->commit or die dbh->errstr;
592 warn "customer ". $cust_main->custnum. " inserted\n";
598 warn "import successful!\n";
600 warn "rolling back (dry run)\n";
601 dbh->rollback or die dbh->errstr;
605 dbh->commit or die dbh->errstr;
610 my( $table, %opt ) = @_;
612 eval "use FS::$table;";
615 my $map = $opt{'map'} || 'cust_main';
616 my $search = $opt{'search'} || 'custnum';
618 $opt{'insert_opts'} ||= [];
620 my $primary_key = dbdef->table($table)->primary_key;
622 my $addl_from = defined($opt{'addl_from'}) ? $opt{'addl_from'} : '';
624 my $sth = $sdbh->prepare(
625 "SELECT * FROM $table $addl_from ".
626 ( $opt{'nomap'} ? '' : " WHERE $search = ". $opt{$search} )
627 ) or die $sdbh->errstr;
629 $sth->execute or die "(searching $table): ". $sth->errstr;
631 while ( my $row = $sth->fetchrow_hashref ) {
632 #my $src_custnum = $customerrow->{'custnum'};
634 my $hashref = { %$row };
635 $hashref->{$primary_key} = ''
636 unless $opt{'noblank_primary'};
637 $hashref->{ $search } = $map{$map}->{ $row->{$search} }
638 unless $opt{'nomap'};
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'
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} };
651 if ( $opt{'map3'} ) {
652 my $key3 = $opt{'map3key'};
653 $hashref->{$key3} = $map{ $opt{'map3'} }->{ $row->{$key3} };
656 my $object = eval "new FS::$table \$hashref;";
659 &{ $opt{preinsert_callback} }( $row, $object )
660 if $opt{preinsert_callback};
662 my $error = $object->insert( @{ $opt{'insert_opts'} } );
664 warn "*** WARNING: error importing $table src $primary_key ". $row->{$primary_key}. ": $error";
668 $map{ $table }->{ $row->{$primary_key} } = $object->get($primary_key);
670 &{ $opt{post_callback} }( $row->{$primary_key}, $object->get($primary_key) )
671 if $opt{post_callback};