restore fallback to customer billing address for CC transactions, RT#77641, RT#71513
[freeside.git] / FS / FS / cust_main / Location.pm
1 package FS::cust_main::Location;
2
3 use strict;
4 use vars qw( $DEBUG $me @location_fields );
5 use FS::Record qw(qsearch qsearchs);
6 use FS::UID qw(dbh);
7 use FS::Cursor;
8 use FS::cust_location;
9
10 use Carp qw(carp);
11
12 $DEBUG = 0;
13 $me = '[FS::cust_main::Location]';
14
15 my $init = 0;
16 BEGIN {
17   # set up accessors for location fields
18   if (!$init) {
19     no strict 'refs';
20     @location_fields = qw(
21       locationname
22       address1 address2 city county state zip country
23       district latitude longitude coord_auto censustract censusyear geocode
24       addr_clean
25     );
26
27     foreach my $f (@location_fields) {
28       *{"FS::cust_main::Location::$f"} = sub {
29         carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1);
30         my $l = shift->bill_location;
31         $l ? $l->$f : '';
32       };
33       *{"FS::cust_main::Location::ship_$f"} = sub {
34         carp "WARNING: tried to set cust_main.ship_$f with accessor" if (@_ > 1);
35         my $l = shift->ship_location;
36         $l ? $l->$f : '';
37       };
38     }
39     $init++;
40   }
41 }
42
43 #debugging shim--probably a performance hit, so remove this at some point
44 sub get {
45   my $self = shift;
46   my $field = shift;
47   if ( $DEBUG and grep (/^(ship_)?($field)$/, @location_fields) ) {
48     carp "WARNING: tried to get() location field $field";
49     $self->$field;
50   }
51   $self->FS::Record::get($field);
52 }
53
54 =head1 NAME
55
56 FS::cust_main::Location - Location-related methods for cust_main
57
58 =head1 DESCRIPTION
59
60 These methods are available on FS::cust_main objects;
61
62 =head1 METHODS
63
64 =over 4
65
66 =item bill_location
67
68 Returns an L<FS::cust_location> object for the customer's billing address.
69
70 =cut
71
72 sub bill_location {
73   my $self = shift;
74   $self->hashref->{bill_location} 
75     ||= FS::cust_location->by_key($self->bill_locationnum)
76     # degraded mode--let the system keep running during upgrades
77     ||  ( $self->get('address1')
78             && FS::cust_location->new({
79                  map { $_ => $self->get($_) } @location_fields
80                })
81         );
82 }
83
84 =item ship_location
85
86 Returns an L<FS::cust_location> object for the customer's service address.
87
88 =cut
89
90 sub ship_location {
91   my $self = shift;
92   $self->hashref->{ship_location}
93     ||= FS::cust_location->by_key($self->ship_locationnum)
94     # degraded mode--let the system keep running during upgrades
95     ||  ( $self->get('ship_address1')
96             ? FS::cust_location->new({
97                 map { $_ => $self->get('ship_'.$_) } @location_fields
98               })
99             : $self->get('address1')
100                 ? FS::cust_location->new({
101                     map { $_ => $self->get($_) } @location_fields
102                   })
103                 : ''
104         );
105
106 }
107
108 =item location TYPE
109
110 An alternative way of saying "bill_location or ship_location, depending on 
111 if TYPE is 'bill' or 'ship'".
112
113 =cut
114
115 sub location {
116   my $self = shift;
117   return $self->bill_location if $_[0] eq 'bill';
118   return $self->ship_location if $_[0] eq 'ship';
119   die "bad location type '$_[0]'";
120 }
121
122 =back
123
124 =head1 CLASS METHODS
125
126 =over 4
127
128 =item location_fields
129
130 Returns a list of fields found in the location objects.  All of these fields
131 can be read (but not written) by calling them as methods on the 
132 L<FS::cust_main> object (prefixed with 'ship_' for the service address 
133 fields).
134
135 =cut
136
137 sub location_fields { @location_fields }
138
139 sub _upgrade_data {
140   my $class = shift;
141   my %opt = @_;
142
143   eval "use FS::contact;
144         use FS::contact_class;
145         use FS::contact_phone;
146         use FS::phone_type";
147
148   local $FS::cust_location::import = 1;
149   local $DEBUG = 0;
150   my $error;
151
152   # Step 0: set up contact classes and phone types
153   my $service_contact_class = 
154     qsearchs('contact_class', { classname => 'Service'})
155     || new FS::contact_class { classname => 'Service'};
156
157   if ( !$service_contact_class->classnum ) {
158     warn "Creating service contact class.\n";
159     $error = $service_contact_class->insert;
160     die "error creating contact class for Service: $error" if $error;
161   }
162   my %phone_type = ( # fudge slightly
163     daytime => 'Work',
164     night   => 'Home',
165     mobile  => 'Mobile',
166     fax     => 'Fax'
167   );
168   my $w = 10;
169   foreach (keys %phone_type) {
170     $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}})
171                       || new FS::phone_type  { typename => $phone_type{$_},
172                                                weight   => $w };
173     # just in case someone still doesn't have these
174     if ( !$phone_type{$_}->phonetypenum ) {
175       $error = $phone_type{$_}->insert;
176       die "error creating phone type '$_': $error" if $error;
177     }
178   }
179
180   my $num_to_upgrade = FS::cust_main->count('bill_locationnum is null or ship_locationnum is null');
181   my $num_jobs = FS::queue->count('job = \'FS::cust_main::Location::process_upgrade_location\' and status != \'failed\'');
182   if ( $num_to_upgrade > 0 ) {
183     warn "Need to migrate $num_to_upgrade customer locations.\n";
184
185     if ( $opt{queue} ) {
186       if ( $num_jobs > 0 ) {
187         warn "Upgrade already queued.\n";
188       } else {
189         warn "Scheduling upgrade.\n";
190         my $job = FS::queue->new({ job => 'FS::cust_main::Location::process_upgrade_location' });
191         $job->insert;
192       }
193     } else { #do it now
194       process_upgrade_location();
195     }
196
197   }
198
199   # repair an error in earlier upgrades
200   if (!FS::upgrade_journal->is_done('cust_location_censustract_repair')
201        and FS::Conf->new->exists('cust_main-require_censustract') ) {
202
203     foreach my $cust_location (
204       qsearch('cust_location', { 'censustract' => '' })
205     ) {
206       my $custnum = $cust_location->custnum;
207       next if !$custnum; # avoid doing this for prospect locations
208       my $address1 = $cust_location->address1;
209       # find the last history record that had that address
210       my $last_h = qsearchs({
211           table     => 'h_cust_main',
212           extra_sql => " WHERE custnum = $custnum AND address1 = ".
213                         dbh->quote($address1) .
214                         " AND censustract IS NOT NULL",
215           order_by  => " ORDER BY history_date DESC LIMIT 1",
216       });
217       if (!$last_h) {
218         # this is normal; just means it never had a census tract before
219         next;
220       }
221       $cust_location->set('censustract' => $last_h->get('censustract'));
222       $cust_location->set('censusyear'  => $last_h->get('censusyear'));
223       my $error = $cust_location->replace;
224       warn "Error setting census tract for customer #$custnum:\n  $error\n"
225         if $error;
226     } # foreach $cust_location
227     FS::upgrade_journal->set_done('cust_location_censustract_repair');
228   }
229 }
230
231 sub process_upgrade_location {
232   my $class = shift;
233
234   my $dbh = dbh;
235   local $FS::cust_main::import = 1;
236   local $FS::cust_location::import = 1;
237   local $FS::contact::skip_fuzzyfiles = 1;
238   local $FS::UID::AutoCommit = 0;
239
240   my $tax_prefix = 'bill_';
241   if ( FS::Conf->new->exists('tax-ship_address') ) {
242     $tax_prefix = 'ship_';
243   }
244
245   # load some records that were created during the initial upgrade
246   my $service_contact_class = 
247     qsearchs('contact_class', { classname => 'Service'});
248
249   my %phone_type = (
250     daytime => 'Work',
251     night   => 'Home',
252     mobile  => 'Mobile',
253     fax     => 'Fax'
254   );
255   foreach (keys %phone_type) {
256     $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}});
257   }
258
259   my %opt = (
260     tax_prefix            => $tax_prefix,
261     service_contact_class => $service_contact_class,
262     phone_type            => \%phone_type,
263   );
264
265   my $search = FS::Cursor->new('cust_main',
266                         { bill_locationnum => '',
267                           address1         => { op=>'!=', value=>'' }
268                         });
269   while (my $cust_main = $search->fetch) {
270     my $error = $cust_main->upgrade_location(%opt);
271     if ( $error ) {
272       warn "cust#".$cust_main->custnum.": $error\n";
273       $dbh->rollback;
274     } else {
275       # commit as we go
276       $dbh->commit;
277     }
278   }
279 }
280
281 sub upgrade_location { # instance method
282   my $cust_main = shift;
283   my %opt = @_;
284   my $error;
285
286   # Step 1: extract billing and service addresses into cust_location
287   my $custnum = $cust_main->custnum;
288   my $bill_location = FS::cust_location->new(
289     {
290       custnum => $custnum,
291       map { $_ => $cust_main->get($_) } location_fields(),
292     }
293   );
294   $bill_location->set('censustract', '');
295   $bill_location->set('censusyear', '');
296    # properly goes with ship_location; if they're the same, will be set
297    # on ship_location before inserting either one
298   my $ship_location = $bill_location; # until proven otherwise
299
300   if ( $cust_main->get('ship_address1') ) {
301     # detect duplicates
302     my $same = 1;
303     foreach (location_fields()) {
304       if ( length($cust_main->get("ship_$_")) and
305            $cust_main->get($_) ne $cust_main->get("ship_$_") ) {
306         $same = 0;
307       }
308     }
309
310     if ( !$same ) {
311       $ship_location = FS::cust_location->new(
312         {
313           custnum => $custnum,
314           map { $_ => $cust_main->get("ship_$_") } location_fields()
315         }
316       );
317     } # else it stays equal to $bill_location
318
319     # Step 2: Extract shipping address contact fields into contact
320     my %unlike = map { $_ => 1 }
321       grep { $cust_main->get($_) ne $cust_main->get("ship_$_") }
322       qw( last first company daytime night fax mobile );
323
324     if ( %unlike ) {
325       # then there IS a service contact
326       my $contact = FS::contact->new({
327         'custnum'     => $custnum,
328         'classnum'    => $opt{service_contact_class}->classnum,
329         'locationnum' => $ship_location->locationnum,
330         'last'        => $cust_main->get('ship_last'),
331         'first'       => $cust_main->get('ship_first'),
332       });
333       if ( !$cust_main->get('ship_last') or !$cust_main->get('ship_first') )
334       {
335         warn "customer $custnum has no service contact name; substituting ".
336              "customer name\n";
337         $contact->set('last' => $cust_main->get('last'));
338         $contact->set('first' => $cust_main->get('first'));
339       }
340
341       if ( $unlike{'company'} ) {
342         # there's no contact.company field, but keep a record of it
343         $contact->set(comment => 'Company: '.$cust_main->get('ship_company'));
344       }
345       $error = $contact->insert;
346       return "error migrating service contact for customer $custnum: $error"
347         if $error;
348
349       foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) {
350         my $phone = $cust_main->get("ship_$_");
351         next if !$phone;
352         my $contact_phone = FS::contact_phone->new({
353           'contactnum'    => $contact->contactnum,
354           'phonetypenum'  => $opt{phone_type}->{$_}->phonetypenum,
355           FS::contact::_parse_phonestring( $phone )
356         });
357         $error = $contact_phone->insert;
358         return "error migrating service contact phone for customer $custnum: $error"
359           if $error;
360         $cust_main->set("ship_$_" => '');
361       }
362
363       $cust_main->set("ship_$_" => '') foreach qw(last first company);
364     } #if %unlike
365   } #if ship_address1
366
367   # special case: should go with whichever location is used to calculate
368   # taxes, because that's the one it originally came from
369   if ( my $geocode = $cust_main->get('geocode') ) {
370     $bill_location->set('geocode' => '');
371     $ship_location->set('geocode' => '');
372
373     if ( $opt{tax_prefix} eq 'bill_' ) {
374       $bill_location->set('geocode', $geocode);
375     } elsif ( $opt{tax_prefix} eq 'ship_' ) {
376       $ship_location->set('geocode', $geocode);
377     }
378   }
379
380   # this always goes with the ship_location (whether it's the same as
381   # bill_location or not)
382   $ship_location->set('censustract', $cust_main->get('censustract'));
383   $ship_location->set('censusyear',  $cust_main->get('censusyear'));
384
385   $error = $bill_location->insert;
386   return "error migrating billing address for customer $custnum: $error"
387     if $error;
388
389   $cust_main->set(bill_locationnum => $bill_location->locationnum);
390
391   if (!$ship_location->locationnum) {
392     $error = $ship_location->insert;
393     return "error migrating service address for customer $custnum: $error"
394       if $error;
395   }
396
397   $cust_main->set(ship_locationnum => $ship_location->locationnum);
398
399   # Step 3: Wipe the migrated fields and update the cust_main
400
401   $cust_main->set("ship_$_" => '') foreach location_fields();
402   $cust_main->set($_ => '') foreach location_fields();
403
404   $error = $cust_main->replace;
405   return "error migrating addresses for customer $custnum: $error"
406     if $error;
407
408   # Step 4: set packages at the "default service location" to ship_location
409   my $pkg_search =
410     FS::Cursor->new('cust_pkg', { custnum => $custnum, locationnum => '' });
411   while (my $cust_pkg = $pkg_search->fetch) {
412     # not a location change
413     $cust_pkg->set('locationnum', $cust_main->ship_locationnum);
414     $error = $cust_pkg->replace;
415     return "error migrating package ".$cust_pkg->pkgnum.": $error"
416       if $error;
417   }
418   '';
419
420 }
421
422
423 =back
424
425 =cut
426
427 1;