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