Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / ClientAPI / Signup.pm
1 package FS::ClientAPI::Signup;
2
3 use strict;
4 use vars qw( $DEBUG $me );
5 use subs qw( _myaccount_cache );
6 use Data::Dumper;
7 use Tie::RefHash;
8 use Digest::SHA qw(sha512_hex);
9 use FS::Conf;
10 use FS::Record qw(qsearch qsearchs dbdef dbh);
11 use FS::CGI qw(popurl);
12 use FS::Msgcat qw(gettext);
13 use FS::Misc qw(card_types);
14 use FS::ClientAPI_SessionCache;
15 use FS::agent;
16 use FS::cust_main_county;
17 use FS::part_pkg;
18 use FS::svc_acct_pop;
19 use FS::cust_main;
20 use FS::cust_pkg;
21 use FS::svc_acct;
22 use FS::svc_phone;
23 use FS::acct_snarf;
24 use FS::queue;
25 use FS::reg_code;
26 use FS::payby;
27 use FS::banned_pay;
28 use FS::part_tag;
29 use FS::cust_payby;
30
31 $DEBUG = 1;
32 $me = '[FS::ClientAPI::Signup]';
33
34 =head1 NAME
35
36 FS::ClientAPI::Signup - Front-end API for signing up customers
37
38 =head1 DESCRIPTION
39
40 This module provides the ClientAPI functions for talking to a signup
41 server. The signup server is open to the public, i.e. does not require a
42 login. The back-end Freeside server creates customers, orders packages and
43 services, and processes initial payments.
44
45 =head1 METHODS
46
47 =over 4
48
49 =cut
50
51 # document the rest of this as we work on it
52
53 sub clear_cache {
54   warn "$me clear_cache called\n" if $DEBUG;
55   my $cache = new FS::ClientAPI_SessionCache( {
56       'namespace' => 'FS::ClientAPI::Signup',
57   } );
58   $cache->clear();
59   return {};
60 }
61
62 sub signup_info {
63   my $packet = shift;
64
65   warn "$me signup_info called on $packet\n" if $DEBUG;
66
67   my $conf = new FS::Conf;
68   my $svc_x = $conf->config('signup_server-service') || 'svc_acct';
69
70   my $cache = new FS::ClientAPI_SessionCache( {
71     'namespace' => 'FS::ClientAPI::Signup',
72   } );
73   my $signup_info_cache = $cache->get('signup_info_cache');
74
75   if ( $signup_info_cache ) {
76
77     warn "$me loading cached signup info\n" if $DEBUG > 1;
78
79   } else {
80
81     warn "$me populating signup info cache\n" if $DEBUG > 1;
82
83     my $agentnum2part_pkg = 
84       {
85         map {
86           my $agent = $_;
87           my $href = $agent->pkgpart_hashref;
88           $agent->agentnum =>
89             [
90               map { { 'payby'       => [ $_->payby ],
91                       'freq_pretty' => $_->freq_pretty,
92                       'options'     => { $_->options },
93                       %{$_->hashref}
94                   } }
95                 grep { $_->svcpart($svc_x)
96                        && ( $href->{ $_->pkgpart }
97                             || ( $_->agentnum
98                                  && $_->agentnum == $agent->agentnum
99                                )
100                           )
101                      }
102                   qsearch( 'part_pkg', { 'disabled' => '' } )
103             ];
104         } qsearch('agent', { 'disabled' => '' })
105       };
106
107     my $msgcat = { map { $_=>gettext($_) }
108                        qw( passwords_dont_match invalid_card unknown_card_type
109                            not_a empty_password illegal_or_empty_text )
110                  };
111     warn "msgcat: ". Dumper($msgcat). "\n" if $DEBUG > 2;
112
113     my $label = { map { $_ => FS::Msgcat::_gettext($_) }
114                       qw( stateid stateid_state )
115                 };
116     warn "label: ". Dumper($label). "\n" if $DEBUG > 2;
117
118     my @agent_fields = qw( agentnum agent );
119
120     my @bools = qw( emailinvoiceonly security_phrase );
121
122     my @signup_bools = qw( no_company recommend_daytime recommend_email );
123
124     my @signup_server_scalars = qw( default_pkgpart default_svcpart default_domsvc );
125
126     my @selfservice_textareas = qw( head body_header body_footer );
127
128     my @selfservice_scalars = qw(
129       body_bgcolor box_bgcolor
130       text_color link_color vlink_color hlink_color alink_color
131       font title_color title_align title_size menu_bgcolor menu_fontsize
132     );
133
134     #XXX my @selfservice_bools = qw(
135     #  menu_skipblanks menu_skipheadings menu_nounderline
136     #);
137
138     #my $selfservice_binaries = qw(
139     #  title_left_image title_right_image
140     #  menu_top_image menu_body_image menu_bottom_image
141     #);
142
143     $signup_info_cache = {
144
145       'cust_main_county' => [ map $_->hashref,
146                                   qsearch('cust_main_county', {} )
147                             ],
148
149       'agent' => [ map { my $agent = $_;
150                          +{ map { $_ => $agent->get($_) } @agent_fields }
151                        }
152                        qsearch('agent', { 'disabled' => '' } )
153                  ],
154
155       'part_referral' => [ map $_->hashref,
156                                qsearch('part_referral', { 'disabled' => '' } )
157                          ],
158
159       'agentnum2part_pkg' => $agentnum2part_pkg,
160
161       'svc_acct_pop' => [ map $_->hashref, qsearch('svc_acct_pop',{} ) ],
162
163       'emailinvoiceonly' => $conf->exists('emailinvoiceonly'),
164
165       'security_phrase' => $conf->exists('security_phrase'),
166
167       'nomadix' => $conf->exists('signup_server-nomadix'),
168
169       'payby' => [ $conf->config('signup_server-payby') ],
170
171       'payby_longname' => [ map { FS::payby->longname($_) } 
172                             $conf->config('signup_server-payby') ],
173
174       'card_types' => card_types(),
175
176       ( map { $_ => $conf->exists("signup-$_") } @signup_bools ),
177
178       ( map { $_ => scalar($conf->config("signup_server-$_")) }
179             @signup_server_scalars
180       ),
181
182       ( map { $_ => join("\n", $conf->config("selfservice-$_")) }
183             @selfservice_textareas
184       ),
185       ( map { $_ => scalar($conf->config("selfservice-$_")) }
186             @selfservice_scalars
187       ),
188
189       #( map { $_ => scalar($conf->config_binary("selfservice-$_")) }
190       #      @selfservice_binaries
191       #),
192
193       'agentnum2part_pkg'  => $agentnum2part_pkg,
194       'svc_acct_pop'       => [ map $_->hashref, qsearch('svc_acct_pop',{} ) ],
195       'nomadix'            => $conf->exists('signup_server-nomadix'),
196       'payby'              => [ $conf->config('signup_server-payby') ],
197       'card_types'         => card_types(),
198       'paytypes'           => [ FS::cust_payby->paytypes ],
199       'cvv_enabled'        => 1,
200       'require_cvv'        => $conf->exists('signup-require_cvv'),
201       'stateid_enabled'    => $conf->exists('show_stateid'),
202       'paystate_enabled'   => $conf->exists('show_bankstate'),
203       'exempt_groups'      => [ grep /\S/, $conf->config('tax-cust_exempt-groups') ],
204       'ship_enabled'       => 1,
205       'msgcat'             => $msgcat,
206       'label'              => $label,
207       'statedefault'       => scalar($conf->config('statedefault')) || 'CA',
208       'countrydefault'     => scalar($conf->config('countrydefault')) || 'US',
209       'refnum'             => scalar($conf->config('signup_server-default_refnum')),
210       'signup_service'     => $svc_x,
211       'company_name'       => scalar($conf->config('company_name')),
212       #per-agent?
213       'logo'               => scalar($conf->config_binary('logo.png')),
214       'prepaid_template_custnum' => $conf->exists('signup_server-prepaid-template-custnum'),
215     };
216
217     $cache->set('signup_info_cache', $signup_info_cache);
218
219   }
220
221   my $signup_info = { %$signup_info_cache };
222   warn "$me signup info loaded\n" if $DEBUG > 1;
223   warn Dumper($signup_info). "\n" if $DEBUG > 2;
224
225   my @addl = qw( signup_server-classnum2 signup_server-classnum3 );
226
227   if ( grep { $conf->exists($_) } @addl ) {
228   
229     $signup_info->{optional_packages} = [];
230
231     foreach my $addl ( @addl ) {
232
233       warn "$me adding optional package info\n" if $DEBUG > 1;
234
235       my $classnum = $conf->config($addl) or next;
236
237       my @pkgs = map { {
238                          'freq_pretty' => $_->freq_pretty,
239                          'options'     => { $_->options },
240                          %{ $_->hashref }
241                        };
242                      }
243                      qsearch( 'part_pkg', { classnum => $classnum } );
244
245       push @{$signup_info->{optional_packages}}, \@pkgs;
246
247       warn "$me done adding opt. package info for $classnum\n" if $DEBUG > 1;
248
249     }
250
251   }
252
253   my $agentnum = $packet->{'agentnum'}
254                  || $conf->config('signup_server-default_agentnum');
255   $agentnum =~ /^(\d*)$/ or die "illegal agentnum";
256   $agentnum = $1;
257
258   my $session = '';
259   if ( exists $packet->{'session_id'} ) {
260
261     warn "$me loading agent session\n" if $DEBUG > 1;
262     my $cache = new FS::ClientAPI_SessionCache( {
263       'namespace' => 'FS::ClientAPI::Agent',
264     } );
265     $session = $cache->get($packet->{'session_id'});
266     if ( $session ) {
267       $agentnum = $session->{'agentnum'};
268     } else {
269       return { 'error' => "Can't resume session" }; #better error message
270     }
271     warn "$me done loading agent session\n" if $DEBUG > 1;
272
273   } elsif ( exists $packet->{'customer_session_id'} ) {
274
275     warn "$me loading customer session\n" if $DEBUG > 1;
276     my $cache = new FS::ClientAPI_SessionCache( {
277       'namespace' => 'FS::ClientAPI::MyAccount',
278     } );
279     $session = $cache->get($packet->{'customer_session_id'});
280     if ( $session ) {
281       my $custnum = $session->{'custnum'};
282       my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum });
283       return { 'error' => "Can't find your customer record" } unless $cust_main;
284       $agentnum = $cust_main->agentnum;
285     } else {
286       return { 'error' => "Can't resume session" }; #better error message
287     }
288     warn "$me done loading customer session\n" if $DEBUG > 1;
289
290   }
291
292   $signup_info->{'part_pkg'} = [];
293
294   if ( $packet->{'reg_code'} ) {
295
296     warn "$me setting package list via reg_code\n" if $DEBUG > 1;
297
298     $signup_info->{'part_pkg'} = 
299       [ map { { 'payby'       => [ $_->payby ],
300                 'freq_pretty' => $_->freq_pretty,
301                 'options'     => { $_->options },
302                 %{$_->hashref}
303               };
304             }
305           grep { $_->svcpart($svc_x) }
306           map { $_->part_pkg }
307             qsearchs( 'reg_code', { 'code'     => $packet->{'reg_code'},
308                                     'agentnum' => $agentnum,              } )
309
310       ];
311
312     $signup_info->{'error'} = 'Unknown registration code'
313       unless @{ $signup_info->{'part_pkg'} };
314
315     warn "$me done setting package list via reg_code\n" if $DEBUG > 1;
316
317   } elsif ( $packet->{'promo_code'} ) {
318
319     warn "$me setting package list via promo_code\n" if $DEBUG > 1;
320
321     $signup_info->{'part_pkg'} =
322       [ map { { 'payby'   => [ $_->payby ],
323                 'freq_pretty' => $_->freq_pretty,
324                 'options'     => { $_->options },
325                 %{$_->hashref}
326             } }
327           grep { $_->svcpart($svc_x) }
328             qsearch( 'part_pkg', { 'promo_code' => {
329                                      op=>'ILIKE',
330                                      value=>$packet->{'promo_code'}
331                                    },
332                                    'disabled'   => '',                  } )
333       ];
334
335     $signup_info->{'error'} = 'Unknown promotional code'
336       unless @{ $signup_info->{'part_pkg'} };
337
338     warn "$me done setting package list via promo_code\n" if $DEBUG > 1;
339   }
340
341   if ( $agentnum ) {
342
343     warn "$me setting agent-specific payment flag\n" if $DEBUG > 1;
344     my $agent = qsearchs('agent', { 'agentnum' => $agentnum } )
345       or return { 'error' => "Self-service agent #$agentnum does not exist" };
346     warn "$me has agent $agent\n" if $DEBUG > 1;
347     my @paybys = @{ $signup_info->{'payby'} };
348     $signup_info->{'hide_payment_fields'} = [];
349
350     my $gatewaynum = $conf->config('selfservice-payment_gateway');
351     my $force_gateway;
352     if ( $gatewaynum ) {
353       $force_gateway = qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
354       warn "using forced gateway #$gatewaynum - " .
355         $force_gateway->gateway_username . '@' . $force_gateway->gateway_module
356         if $DEBUG > 1;
357       die "configured gatewaynum $gatewaynum not found!" if !$force_gateway;
358     }
359     foreach my $payby (@paybys) {
360       warn "$me checking $payby payment fields\n" if $DEBUG > 1;
361       my $hide = 0;
362       if ( FS::payby->realtime($payby) ) {
363         my $gateway = $force_gateway || 
364           $agent->payment_gateway( 'method'  => FS::payby->payby2bop($payby),
365                                    'nofatal' => 1,
366                                  );
367         if ( $gateway && $gateway->gateway_namespace
368                     eq 'Business::OnlineThirdPartyPayment'
369            ) {
370           warn "$me hiding $payby payment fields\n" if $DEBUG > 1;
371           $hide = 1;
372         }
373       }
374       push @{$signup_info->{'hide_payment_fields'}}, $hide;
375     } # foreach $payby
376     warn "$me done setting agent-specific payment flag\n" if $DEBUG > 1;
377
378     warn "$me setting agent-specific package list\n" if $DEBUG > 1;
379     $signup_info->{'part_pkg'} = $signup_info->{'agentnum2part_pkg'}{$agentnum}
380       unless @{ $signup_info->{'part_pkg'} };
381     warn "$me done setting agent-specific package list\n" if $DEBUG > 1;
382
383     warn "$me setting agent-specific adv. source list\n" if $DEBUG > 1;
384     $signup_info->{'part_referral'} =
385       [
386         map { $_->hashref }
387           qsearch( {
388                      'table'     => 'part_referral',
389                      'hashref'   => { 'disabled' => '' },
390                      'extra_sql' => "AND (    agentnum = $agentnum  ".
391                                     "      OR agentnum IS NULL    ) ",
392                    },
393                  )
394       ];
395     warn "$me done setting agent-specific adv. source list\n" if $DEBUG > 1;
396
397     $signup_info->{'agent_name'} = $agent->agent;
398
399     $signup_info->{'company_name'} = $conf->config('company_name', $agentnum);
400
401     #some of the above could probably be cached, too
402
403     my $signup_info_cache_agent = $cache->get("signup_info_cache_agent$agentnum");
404
405     if ( $signup_info_cache_agent ) {
406
407       warn "$me loading cached signup info for agentnum $agentnum\n"
408         if $DEBUG > 1;
409
410     } else {
411
412       warn "$me populating signup info cache for agentnum $agentnum\n"
413         if $DEBUG > 1;
414
415       $signup_info_cache_agent = {
416         #( map { $_ => scalar( $conf->config($_, $agentnum) ) }
417         #  qw( company_name ) ),
418         ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) }
419           qw( body_bgcolor box_bgcolor menu_bgcolor ) ),
420         ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) }
421           qw( head body_header body_footer ) ),
422         ( map { $_ => join("\n", $conf->config("signup_server-$_", $agentnum ) ) }
423           qw( terms_of_service ) ),
424
425         ( map { $_ => scalar($conf->exists($_, $agentnum)) } 
426           qw(cust_main-require_phone agent-ship_address) ),
427       };
428
429       if ( $signup_info_cache_agent->{'agent-ship_address'} 
430            && $agent->agent_cust_main ) {
431
432         my $cust_main = $agent->agent_cust_main;
433         my $location = $cust_main->ship_location;
434         $signup_info_cache_agent->{"ship_$_"} = $location->get($_)
435           foreach qw( address1 city county state zip country );
436
437       }
438
439       $cache->set("signup_info_cache_agent$agentnum", $signup_info_cache_agent);
440
441     }
442
443     $signup_info->{$_} = $signup_info_cache_agent->{$_}
444       foreach keys %$signup_info_cache_agent;
445
446   }
447   # else {
448   # delete $signup_info->{'part_pkg'};
449   #}
450
451   warn "$me sorting package list\n" if $DEBUG > 1;
452   $signup_info->{'part_pkg'} = [ sort { $a->{pkg} cmp $b->{pkg} }  # case?
453                                       @{ $signup_info->{'part_pkg'} }
454                                ];
455   warn "$me done sorting package list\n" if $DEBUG > 1;
456
457   if ( exists $packet->{'session_id'} ) {
458     my $agent_signup_info = { %$signup_info };
459     delete $agent_signup_info->{agentnum2part_pkg};
460     $agent_signup_info->{'agent'} = $session->{'agent'};
461     return $agent_signup_info;
462   } 
463   elsif ( exists $packet->{'keys'} ) {
464     my @keys = @{ $packet->{'keys'} };
465     return { map { $_ => $signup_info->{$_} } @keys };
466   }
467   else {
468     return $signup_info;
469   }
470
471 }
472
473 sub domain_select_hash {
474   my $packet = shift;
475
476   my $response = {};
477
478   if ($packet->{pkgpart}) {
479     my $part_pkg = qsearchs('part_pkg' => { 'pkgpart' => $packet->{pkgpart} } );
480     #$packet->{svcpart} = $part_pkg->svcpart('svc_acct')
481     $packet->{svcpart} = $part_pkg->svcpart
482       if $part_pkg;
483   }
484
485   if ($packet->{svcpart}) {
486     my $part_svc = qsearchs('part_svc' => { 'svcpart' => $packet->{svcpart} } );
487     $response->{'domsvc'} = $part_svc->part_svc_column('domsvc')->columnvalue
488       if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag  eq 'D');
489   }
490
491   $response->{'domains'}
492     = { domain_select_hash FS::svc_acct( map { $_ => $packet->{$_} }
493                                                  qw(svcpart pkgnum)
494                                        ) };
495
496   $response;
497 }
498
499 sub new_customer {
500   my $packet = shift;
501
502   my $conf = new FS::Conf;
503   my $svc_x = $conf->config('signup_server-service') || 'svc_acct';
504
505   if ( $svc_x eq 'svc_acct' ) {
506   
507     #things that aren't necessary in base class, but are for signup server
508       #return "Passwords don't match"
509       #  if $hashref->{'_password'} ne $hashref->{'_password2'}
510     return { 'error' => gettext('empty_password') }
511       unless length($packet->{'_password'});
512     # a bit inefficient for large numbers of pops
513     return { 'error' => gettext('no_access_number_selected') }
514       unless $packet->{'popnum'} || !scalar(qsearch('svc_acct_pop',{} ));
515
516   }
517   elsif ( $svc_x eq 'svc_pbx' ) {
518     #possibly some validation will be needed
519   }
520
521   my $agentnum = get_agentnum($packet);
522   return $agentnum if ref($agentnum);
523
524   my ($bill_hash, $ship_hash);
525   foreach my $f (FS::cust_main->location_fields) {
526     # avoid having to change this in front-end code
527     $bill_hash->{$f} = $packet->{"bill_$f"} || $packet->{$f};
528     $ship_hash->{$f} = $packet->{"ship_$f"};
529   }
530
531   #shares some stuff with htdocs/edit/process/cust_main.cgi... take any
532   # common that are still here and library them.
533
534   my %cust_main = (
535     'agentnum' => $agentnum,
536     'refnum'   => $packet->{refnum}
537                   || $conf->config('signup_server-default_refnum'),
538     'tagnum'   => [ FS::part_tag->default_tags ],
539
540     ( map { $_ => $packet->{$_} } qw(
541             salesnum
542             ss stateid stateid_state
543             locale
544             referral_custnum comments
545           )
546     ),
547
548   );
549
550   my %insert_options = ();
551   if ( $packet->{payby} =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
552     $insert_options{cust_payby} = [
553       new FS::cust_payby {
554         map { $_ => $packet->{$_} } qw(
555           payby
556           payinfo paycvv paydate payname paystate paytype
557           paystart_month paystart_year payissue
558           payip
559         ),
560       }
561     ];
562   }
563
564   my $template_custnum = $conf->config('signup_server-prepaid-template-custnum');
565   my $cust_main;
566   if ( $template_custnum && $packet->{prepaid_shortform} ) {
567
568     my $template_cust = qsearchs('cust_main', { 'custnum' => $template_custnum } );
569     return { 'error' => 'Configuration error' } unless $template_cust;
570     $cust_main = new FS::cust_main ( {
571       %cust_main,
572       map { $_ => $template_cust->$_ } qw( 
573         last first company daytime night fax mobile
574       ),
575     } );
576
577     $bill_hash = { $template_cust->bill_location->location_hash };
578     $ship_hash = { $template_cust->ship_location->location_hash };
579
580   } else {
581
582     $cust_main = new FS::cust_main ( {
583       %cust_main,
584       map { $_ => $packet->{$_} } qw(
585         last first company daytime night fax mobile
586         override_ban_warn
587       ),
588     } );
589   }
590
591   my $bill_location = FS::cust_location->new($bill_hash);
592   my $ship_location;
593   my $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
594   if ( $conf->exists('agent-ship_address', $agentnum) 
595     && $agent->agent_custnum ) {
596
597     my $agent_cust_main = $agent->agent_cust_main;
598     my $prefix = length($agent_cust_main->ship_last) ? 'ship_' : '';
599     $ship_location = FS::cust_location->new({ 
600         $agent_cust_main->ship_location->location_hash
601     });
602
603   }
604   # we don't have an equivalent of the "same" checkbox in selfservice
605   # so is there a ship address, and if so, is it different from the billing 
606   # address?
607   elsif ( length($ship_hash->{address1}) > 0 and
608           grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
609          ) {
610
611     $ship_location = FS::cust_location->new( $ship_hash );
612   
613   }
614   else {
615     $ship_location = $bill_location;
616   }
617
618   $cust_main->set('bill_location' => $bill_location);
619   $cust_main->set('ship_location' => $ship_location);
620
621   return { 'error' => "Illegal payment type" }
622     unless grep { $_ eq $packet->{'payby'} }
623                 $conf->config('signup_server-payby');
624
625   if (FS::payby->realtime($packet->{payby})
626     and not $conf->exists('signup_server-third_party_as_card')) {
627     my $payby = $packet->{payby};
628
629     my $agent = qsearchs('agent', { 'agentnum' => $agentnum });
630     return { 'error' => "Unknown reseller" }
631       unless $agent;
632
633     my $gw;
634     my $gatewaynum = $conf->config('selfservice-payment_gateway');
635     if ( $gatewaynum ) {
636       $gw = qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
637       die "configured gatewaynum $gatewaynum not found!" if !$gw;
638     }
639     else {
640       $gw = $agent->payment_gateway( 'method'  => FS::payby->payby2bop($payby),
641                                      'nofatal' => 1,
642                                     );
643     }
644
645     $cust_main->payby('BILL')   # MCRD better?  no, that's for something else
646       if $gw && $gw->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
647   }
648
649   return { 'error' => "CVV2 is required" }
650     if $cust_main->payby =~ /^(CARD|DCRD)$/
651     && ! $cust_main->paycvv
652     && $conf->exists('signup-require_cvv');
653
654   my @invoicing_list = $packet->{'invoicing_list'}
655                          ? split( /\s*\,\s*/, $packet->{'invoicing_list'} )
656                          : ();
657
658   my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups');
659   my @tax_exempt = grep { $packet->{"tax_$_"} eq 'Y' } @exempt_groups;
660   $insert_options{'tax_exemption'} = {
661     map { $_ => $packet->{"tax_$_".'_num'} } @tax_exempt
662   };
663
664   $packet->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/;
665   my $pkgpart = $1;
666   return { 'error' => 'Please select a package' } unless $pkgpart; #msgcat
667
668   my $part_pkg =
669     qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } )
670       or return { 'error' => "WARNING: unknown pkgpart: $pkgpart" };
671
672   my $reg_code = '';
673   if ( $packet->{'reg_code'} ) {
674     $reg_code = qsearchs( 'reg_code', { 'code'     => $packet->{'reg_code'},
675                                         'agentnum' => $agentnum,             } )
676       or return { 'error' => 'Unknown registration code' };
677   }
678
679   my $cust_pkg = new FS::cust_pkg ( {
680     #later#'custnum' => $custnum,
681     'pkgpart'    => $packet->{'pkgpart'},
682     'promo_code' => $packet->{'promo_code'},
683     'reg_code'   => $packet->{'reg_code'},
684   } );
685   #my $error = $cust_pkg->check;
686   #return { 'error' => $error } if $error;
687
688   my @svc = ();
689   unless ( $svc_x eq 'none' ) {
690
691     my $svcpart = $part_pkg->svcpart($svc_x);
692     #should be all auto-magic and shit
693     if ( $svc_x eq 'svc_acct' ) {
694
695       my $svc = new FS::svc_acct {
696         'svcpart'   => $svcpart,
697         map { $_ => $packet->{$_} }
698           qw( username _password sec_phrase popnum domsvc ),
699       };
700       
701       my $error = $svc->is_password_allowed($packet->{_password});
702       return { error => $error } if $error;
703
704       my @acct_snarf;
705       my $snarfnum = 1;
706       while (    exists($packet->{"snarf_machine$snarfnum"})
707               && length($packet->{"snarf_machine$snarfnum"}) ) {
708         my $acct_snarf = new FS::acct_snarf ( {
709           'machine'   => $packet->{"snarf_machine$snarfnum"},
710           'protocol'  => $packet->{"snarf_protocol$snarfnum"},
711           'username'  => $packet->{"snarf_username$snarfnum"},
712           '_password' => $packet->{"snarf_password$snarfnum"},
713         } );
714         $snarfnum++;
715         push @acct_snarf, $acct_snarf;
716       }
717       $svc->child_objects( \@acct_snarf );
718       push @svc, $svc;
719
720     } elsif ( $svc_x eq 'svc_phone' ) {
721
722       push @svc, new FS::svc_phone ( {
723         'svcpart' => $svcpart,
724          map { $_ => $packet->{$_} }
725            qw( countrycode phonenum sip_password pin ),
726       } );
727
728     } elsif ( $svc_x eq 'svc_pbx' ) {
729
730       push @svc, new FS::svc_pbx ( {
731           'svcpart' => $svcpart,
732           map { $_ => $packet->{$_} } 
733             qw( id title ),
734           } );
735   
736     } else {
737       die "unknown signup service $svc_x";
738     }
739
740   }
741
742   if ($packet->{'mac_addr'} && $conf->exists('signup_server-mac_addr_svcparts'))
743   {
744
745     my %mac_addr_svcparts = map { $_ => 1 }
746                             $conf->config('signup_server-mac_addr_svcparts');
747     my @pkg_svc = grep { $_->quantity && $mac_addr_svcparts{$_->svcpart} }
748                   $cust_pkg->part_pkg->pkg_svc;
749
750     return { 'error' => 'No service defined to assign mac address' }
751       unless @pkg_svc;
752
753     my $svc = new FS::svc_acct {
754       'svcpart'   => $pkg_svc[0]->svcpart, #multiple matches? alas..
755       'username'  => $packet->{'mac_addr'},
756       '_password' => '', #blank as requested (set passwordmin to 0)
757     };
758
759     push @svc, $svc;
760
761   }
762
763   foreach my $svc ( @svc ) {
764     my $y = $svc->setdefault; # arguably should be in new method
765     return { 'error' => $y } if $y && !ref($y);
766     #$error = $svc->check;
767     #return { 'error' => $error } if $error;
768   }
769
770   #setup a job dependancy to delay provisioning
771   my $placeholder = new FS::queue ( {
772     'job'    => 'FS::ClientAPI::Signup::__placeholder',
773     'status' => 'locked',
774   } );
775   my $error = $placeholder->insert;
776   return { 'error' => $error } if $error;
777
778   use Tie::RefHash;
779   tie my %hash, 'Tie::RefHash';
780   %hash = ( $cust_pkg => \@svc );
781   #msgcat
782   $error = $cust_main->insert(
783     \%hash,
784     \@invoicing_list,
785     'depend_jobnum' => $placeholder->jobnum,
786      %insert_options,
787   );
788   if ( $error ) {
789     my $perror = $placeholder->delete;
790     $error .= " (Additionally, error removing placeholder: $perror)" if $perror;
791     return { 'error' => $error };
792   }
793
794   if ( $conf->exists('signup_server-realtime') ) {
795
796     #warn "$me Billing customer...\n" if $Debug;
797
798     my @cust_bill;
799     my $bill_error = $cust_main->bill(
800       'depend_jobnum' => $placeholder->jobnum,
801       'return_bill'   => \@cust_bill,
802     );
803     #warn "$me error billing new customer: $bill_error"
804     #  if $bill_error;
805
806     $bill_error = $cust_main->apply_payments_and_credits;
807     #warn "$me error applying payments and credits for".
808     #     " new customer: $bill_error"
809     #  if $bill_error;
810
811     unless ( $packet->{payby} eq 'PREPAY' ) {
812       $bill_error = $cust_main->realtime_collect(
813          method        => FS::payby->payby2bop( $packet->{payby} ),
814          depend_jobnum => $placeholder->jobnum,
815          selfservice   => 1,
816       );
817       #warn "$me error collecting from new customer: $bill_error"
818       #  if $bill_error;
819     }
820
821     if ($bill_error && ref($bill_error) eq 'HASH') {
822       return { 'error' => '_collect',
823                ( map { $_ => $bill_error->{$_} }
824                  qw(popup_url reference collectitems)
825                ),
826                amount => $cust_main->balance,
827              };
828     }
829
830     $bill_error = $cust_main->apply_payments_and_credits;
831     #warn "$me error applying payments and credits for".
832     #     " new customer: $bill_error"
833     #  if $bill_error;
834
835     if ( $cust_main->balance > 0 ) {
836
837       #this used to apply a credit, but now we can void invoices...
838       foreach my $cust_bill (@cust_bill) {
839         my $voiderror = $cust_bill->void('automatic payment failed');
840         warn "Error voiding cust bill after decline: $voiderror" if $voiderror;
841       }
842
843       #should check list for errors...
844       #$cust_main->suspend;
845       local $FS::svc_Common::noexport_hack = 1;
846       $cust_main->cancel('quiet'=>1);
847
848       my $perror = $placeholder->depended_delete;
849       warn "error removing provisioning jobs after decline: $perror" if $perror;
850       unless ( $perror ) {
851         $perror = $placeholder->delete;
852         warn "error removing placeholder after decline: $perror" if $perror;
853       }
854
855       return { 'error' => '_decline' };
856     }
857
858   }
859
860   if ( $reg_code ) {
861     $error = $reg_code->delete;
862     return { 'error' => $error } if $error;
863   }
864
865   $error = $placeholder->delete;
866   return { 'error' => $error } if $error;
867
868   if ( $conf->exists('signup-duplicate_cc-warn_hours') ) {
869     my $hours = $conf->config('signup-duplicate_cc-warn_hours');
870     my $ban = new FS::banned_pay $cust_main->_new_banned_pay_hashref;
871     $ban->end_date( int( time + $hours*3600 ) );
872     $ban->bantype('warn');
873     $ban->reason('signup-duplicate_cc-warn_hours');
874     $error = $ban->insert;
875     warn "WARNING: error inserting temporary banned_pay for ".
876          " signup-duplicate_cc-warn_hours (proceeding anyway): $error"
877       if $error;
878   }
879
880   my %return = ( 'error'          => '',
881                  'signup_service' => $svc_x,
882                  'custnum'        => $cust_main->custnum,
883                );
884
885   if ( $svc[0] ) {
886
887     $return{'svcnum'} = $svc[0]->svcnum;
888
889     if ( $svc_x eq 'svc_acct' ) {
890       $return{$_} = $svc[0]->$_() for qw( username _password );
891     } elsif ( $svc_x eq 'svc_phone' ) {
892       $return{$_} = $svc[0]->$_() for qw(countrycode phonenum sip_password pin);
893     } elsif ( $svc_x eq 'svc_pbx' ) {
894       #$return{$_} = $svc[0]->$_() for qw( ) #nothing yet
895      } else {
896       return {'error' => "configuration error: unknown signup service $svc_x"};
897       #die "unknown signup service $svc_x";
898       # return an error that's visible to someone somewhere
899     }
900
901   }
902
903   return \%return;
904
905 }
906
907 #false laziness w/ above
908 # fresh restart to support "free account" portals with 3.x/4.x-style
909 #  addressless accounts
910 # and a contact (for self-service login)
911 sub new_customer_minimal {
912   my $packet = shift;
913
914   my $conf = new FS::Conf;
915   my $svc_x = $conf->config('signup_server-service') || 'svc_acct';
916
917   if ( $svc_x eq 'svc_acct' ) {
918   
919     #things that aren't necessary in base class, but are for signup server
920       #return "Passwords don't match"
921       #  if $hashref->{'_password'} ne $hashref->{'_password2'}
922     return { 'error' => gettext('empty_password') }
923       unless length($packet->{'_password'});
924     # a bit inefficient for large numbers of pops
925     return { 'error' => gettext('no_access_number_selected') }
926       unless $packet->{'popnum'} || !scalar(qsearch('svc_acct_pop',{} ));
927
928   }
929   elsif ( $svc_x eq 'svc_pbx' ) {
930     #possibly some validation will be needed
931   }
932
933   my $agentnum = get_agentnum($packet);
934   return $agentnum if ref($agentnum);
935
936   #shares some stuff with htdocs/edit/process/cust_main.cgi... take any
937   # common that are still here and library them.
938
939   my $cust_main = new FS::cust_main ( {
940       'agentnum' => $agentnum,
941       'refnum'   => $packet->{refnum}
942                     || $conf->config('signup_server-default_refnum'),
943       'tagnum'   => [ FS::part_tag->default_tags ],
944
945       map { $_ => $packet->{$_} } qw(
946         salesnum
947         last first company daytime night fax mobile
948         ss stateid stateid_state
949
950         locale
951       ),
952
953   } );
954
955   my %opt = ();
956   if ( $packet->{payby} =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
957     $opt{cust_payby} = [
958       new FS::cust_payby {
959         map { $_ => $packet->{$_} } qw(
960           payby
961           payinfo paycvv paydate payname paystate paytype
962           paystart_month paystart_year payissue
963           payip
964         ),
965       }
966     ];
967   }
968
969   if ( grep length($packet->{$_}), FS::cust_main->location_fields ) {
970     my $bill_hash;
971     foreach my $f (FS::cust_main->location_fields) {
972       $bill_hash->{$f} =  $packet->{$f};
973     }
974     my $bill_location = FS::cust_location->new($bill_hash);
975     $cust_main->set('bill_location' => $bill_location);
976     $cust_main->set('ship_location' => $bill_location);
977   }
978
979   my @invoicing_list = $packet->{'invoicing_list'}
980                          ? split( /\s*\,\s*/, $packet->{'invoicing_list'} )
981                          : ();
982
983   use Tie::RefHash;
984   tie my %hash, 'Tie::RefHash', ();
985   my @svc = ();
986
987   $packet->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/;
988   my $pkgpart = $1;
989
990   if ( $pkgpart ) {
991
992     my $part_pkg =
993       qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } )
994         or return { 'error' => "WARNING: unknown pkgpart: $pkgpart" };
995
996     my $cust_pkg = new FS::cust_pkg ( {
997       #later#'custnum' => $custnum,
998       'pkgpart'    => $packet->{'pkgpart'},
999     } );
1000     #my $error = $cust_pkg->check;
1001     #return { 'error' => $error } if $error;
1002
1003     unless ( $svc_x eq 'none' ) {
1004
1005       my $svcpart = $part_pkg->svcpart($svc_x);
1006       #should be all auto-magic and shit
1007       if ( $svc_x eq 'svc_acct' ) {
1008
1009         my $svc = new FS::svc_acct {
1010           'svcpart'   => $svcpart,
1011           map { $_ => $packet->{$_} }
1012             qw( username _password sec_phrase popnum domsvc ),
1013         };
1014
1015         push @svc, $svc;
1016
1017       } elsif ( $svc_x eq 'svc_phone' ) {
1018
1019         push @svc, new FS::svc_phone ( {
1020           'svcpart' => $svcpart,
1021            map { $_ => $packet->{$_} }
1022              qw( countrycode phonenum sip_password pin ),
1023         } );
1024
1025       } elsif ( $svc_x eq 'svc_pbx' ) {
1026
1027         push @svc, new FS::svc_pbx ( {
1028             'svcpart' => $svcpart,
1029             map { $_ => $packet->{$_} } 
1030               qw( id title ),
1031             } );
1032     
1033       } else {
1034         die "unknown signup service $svc_x";
1035       }
1036
1037     }
1038
1039     foreach my $svc ( @svc ) {
1040       my $y = $svc->setdefault; # arguably should be in new method
1041       return { 'error' => $y } if $y && !ref($y);
1042       #$error = $svc->check;
1043       #return { 'error' => $error } if $error;
1044     }
1045
1046     use Tie::RefHash;
1047     tie my %hash, 'Tie::RefHash';
1048     $hash{ $cust_pkg } = \@svc;
1049
1050   }
1051
1052   if ( $invoicing_list[0] && $packet->{'_password'} ) {
1053     $opt{'contact'} = [
1054       new FS::contact { 'first'        => $cust_main->first,
1055                         'last'         => $cust_main->get('last'),
1056                         '_password'    => $packet->{'_password'},
1057                         'emailaddress' => $invoicing_list[0],
1058                         'selfservice_access' => 'Y',
1059                       }
1060     ];
1061   }
1062
1063   my $error = $cust_main->insert(
1064     \%hash,
1065     \@invoicing_list,
1066     %opt,
1067   );
1068   return { 'error' => $error } if $error;
1069
1070   my $session = { 'custnum' => $cust_main->custnum };
1071
1072   my $session_id;
1073   do {
1074     $session_id = sha512_hex(time(). {}. rand(). $$)
1075   } until ( ! defined _myaccount_cache->get($session_id) ); #just in case
1076
1077   _myaccount_cache->set( $session_id, $session, '1 hour' ); # 1 hour?
1078
1079   my %return = ( 'error'          => '',
1080                  'signup_service' => $svc_x,
1081                  'custnum'        => $cust_main->custnum,
1082                  'session_id'     => $session_id,
1083                  map { $_ => $cust_main->$_ } qw( first last company ),
1084                );
1085
1086   if ( $svc[0] ) {
1087
1088     $return{'svcnum'} = $svc[0]->svcnum;
1089
1090     if ( $svc_x eq 'svc_acct' ) {
1091       $return{$_} = $svc[0]->$_() for qw( username _password );
1092     } elsif ( $svc_x eq 'svc_phone' ) {
1093       $return{$_} = $svc[0]->$_() for qw(countrycode phonenum sip_password pin);
1094     } elsif ( $svc_x eq 'svc_pbx' ) {
1095       #$return{$_} = $svc[0]->$_() for qw( ) #nothing yet
1096      } else {
1097       return {'error' => "configuration error: unknown signup service $svc_x"};
1098       #die "unknown signup service $svc_x";
1099       # return an error that's visible to someone somewhere
1100     }
1101
1102   }
1103
1104   return \%return;
1105
1106 }
1107
1108 use vars qw( $myaccount_cache );
1109 sub _myaccount_cache {
1110   $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
1111                          'namespace' => 'FS::ClientAPI::MyAccount',
1112                        } );
1113 }
1114
1115 sub capture_payment {
1116   my $packet = shift;
1117
1118   warn "$me capture_payment called on $packet\n" if $DEBUG;
1119
1120   ###
1121   # identify processor/gateway from called back URL
1122   ###
1123
1124   my $conf = new FS::Conf;
1125
1126   my $payment_gateway;
1127   if ( my $gwnum = $conf->config('selfservice-payment_gateway') ) {
1128     $payment_gateway = qsearchs('payment_gateway', { 'gatewaynum' => $gwnum })
1129       or die "configured gatewaynum $gwnum not found!";
1130   }
1131   else {
1132     my $url = $packet->{url};
1133
1134     $payment_gateway = qsearchs('payment_gateway', 
1135         { 'gateway_callback_url' => popurl(0, $url) } 
1136       );
1137     if (!$payment_gateway) { 
1138
1139       my ( $processor, $login, $password, $action, @bop_options ) =
1140         $conf->config('business-onlinepayment');
1141       $action ||= 'normal authorization';
1142       pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1143       die "No real-time processor is enabled - ".
1144           "did you set the business-onlinepayment configuration value?\n"
1145         unless $processor;
1146
1147       $payment_gateway = new FS::payment_gateway( {
1148         gateway_namespace => $conf->config('business-onlinepayment-namespace'),
1149         gateway_module    => $processor,
1150         gateway_username  => $login,
1151         gateway_password  => $password,
1152         gateway_action    => $action,
1153         options   => [ ( @bop_options ) ],
1154       });
1155     }
1156   }
1157  
1158   die "No real-time third party processor is enabled - ".
1159       "did you set the business-onlinepayment configuration value?\n*"
1160     unless $payment_gateway->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
1161
1162   ###
1163   # locate pending transaction
1164   ###
1165
1166   eval "use Business::OnlineThirdPartyPayment";
1167   die $@ if $@;
1168
1169   my $transaction =
1170     new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1171                                            @{ [ $payment_gateway->options ] },
1172                                          );
1173
1174   my $paypendingnum = $transaction->reference($packet->{data});
1175
1176   my $cust_pay_pending =
1177     qsearchs('cust_pay_pending', { paypendingnum => $paypendingnum } );
1178
1179   unless ($cust_pay_pending) {
1180     my $bill_error = "No payment is being processed with id $paypendingnum".
1181                      "; Transaction aborted.";
1182     return { error => '_decline', bill_error => $bill_error };
1183   }
1184
1185   if ($cust_pay_pending->status ne 'thirdparty') {
1186     my $bill_error = "Payment with id $paypendingnum is not thirdparty, but ".
1187                      $cust_pay_pending->status.  "; Transaction aborted.";
1188     return { error => '_decline', bill_error => $bill_error };
1189   }
1190
1191   my $cust_main = $cust_pay_pending->cust_main;
1192   if ( $packet->{cancel} ) {
1193     # the user has chosen not to make this payment
1194     # (probably should be a separate API call, but I don't want to duplicate
1195     # all of the above...which should eventually go away)
1196     my $error = $cust_pay_pending->delete;
1197     # don't show any errors related to this; they're not meaningful
1198     warn "error canceling pending payment $paypendingnum: $error\n" if $error;
1199     return { 'error'      => '_cancel',
1200              'session_id' => $cust_pay_pending->session_id };
1201   } else {
1202     # create the payment
1203     my $bill_error =
1204       $cust_main->realtime_botpp_capture( $cust_pay_pending, 
1205         %{$packet->{data}},
1206         apply => 1,
1207     );
1208
1209     return { 'error'      => ( $bill_error->{bill_error} ? '_decline' : '' ),
1210              %$bill_error,
1211            };
1212   }
1213
1214 }
1215
1216 =item get_agentnum PACKET
1217
1218 Given a PACKET from the signup server, looks up the agentnum to use for signing
1219 up a customer. This will use 'session_id' if the agent is authenticated,
1220 otherwise 'agentnum', otherwise the 'signup_server-default_agentnum' config. If
1221 the agent can't be found, returns an error packet.
1222
1223 =cut
1224
1225 sub get_agentnum {
1226   my $packet = shift;
1227   my $conf = new FS::Conf;
1228   my $agentnum;
1229   if ( exists $packet->{'session_id'} ) {
1230     my $cache = new FS::ClientAPI_SessionCache( {
1231       'namespace' => 'FS::ClientAPI::Agent',
1232     } );
1233     my $session = $cache->get($packet->{'session_id'});
1234     if ( $session ) {
1235       $agentnum = $session->{'agentnum'};
1236     } else {
1237       return { 'error' => "Can't resume session" }; #better error message
1238     }
1239   } else {
1240     $agentnum = $packet->{agentnum}
1241                 || $conf->config('signup_server-default_agentnum');
1242   }
1243   if ( $agentnum and FS::agent->count('agentnum = ?', $agentnum) ) {
1244     return $agentnum;
1245   }
1246   return { 'error' => 'Signup is not configured' };
1247 }
1248
1249 =item new_prospect PACKET
1250
1251 Creates a new L<FS::prospect_main> entry. PACKET must contain:
1252
1253 - either agentnum or session_id; if not, signup_server-default_agentnum will
1254 be used and must not be empty
1255
1256 - either refnum or referral_title; if not, signup_server-default_refnum will
1257 be used and must not be empty
1258
1259 - last and first (names), and optionally company and title
1260
1261 - address1, city, state, country, zip, and optionally address2
1262
1263 - emailaddress
1264
1265 and can also contain:
1266
1267 - one or more of phone_daytime, phone_night, phone_mobile, and phone_fax
1268
1269 - a 'comment' (will be attached to the contact)
1270
1271 State and country will be normalized to Freeside state/country codes if
1272 necessary.
1273
1274 =cut
1275
1276 sub new_prospect {
1277
1278   my $packet = shift;
1279   warn "$me new_prospect called\n".Dumper($packet) if $DEBUG;
1280
1281   my $oldAutoCommit = $FS::UID::AutoCommit;
1282   local $FS::UID::AutoCommit = 0;
1283   my $dbh = dbh;
1284   my $conf = FS::Conf->new;
1285
1286   my $error;
1287
1288   my $agentnum = get_agentnum($packet);
1289   return $agentnum if ref $agentnum;
1290   my $refnum;
1291   if ( my $title = $packet->{referral_title} ) {
1292     my $part_referral = qsearchs('part_referral', {
1293         'agentnum'  => $agentnum,
1294         'title'     => $title,
1295     });
1296     $part_referral ||= qsearchs('part_referral', {
1297         'agentnum'  => '',
1298         'title'     => $title,
1299     });
1300     if (!$part_referral) {
1301       return { error => "Unknown referral type: '$title'" };
1302     }
1303     $refnum = $part_referral->refnum;
1304   } elsif ( $packet->{refnum} ) {
1305     $refnum = $packet->{refnum};
1306   }
1307   $refnum ||= $conf->config('signup_server-default_refnum');
1308   return { error => "Signup referral type is not configured" } if !$refnum;
1309
1310   my $prospect = FS::prospect_main->new({
1311       'agentnum' => $agentnum,
1312       'refnum'   => $refnum,
1313       'company'  => $packet->{company},
1314   });
1315
1316   my $location = FS::cust_location->new;
1317   foreach ( qw(address1 address2 city county zip ) ) {
1318     $location->set($_, $packet->{$_});
1319   }
1320   # normalize country and state if they're not already ISO codes
1321   # easier than doing it on the client side--we already have the tables here
1322   my $country = $packet->{country};
1323   my $state = $packet->{state};
1324   if (length($country) > 2) {
1325     # it likes title case
1326     $country = join(' ', map ucfirst, split(/\s+/, $country));
1327     my $lsc = Locale::SubCountry->new($country);
1328     if ($lsc) {
1329       $country = uc($lsc->country_code);
1330
1331       if ($lsc->has_sub_countries) {
1332         if ( $lsc->full_name($state) eq 'unknown' ) {
1333           # then we were probably given a full name, so resolve it
1334           $state = $lsc->code($state);
1335           if ( $state eq 'unknown' ) {
1336             # doesn't resolve as a full name either, return an error
1337             $error = "Unknown state: ".$packet->{state};
1338           } else {
1339             $state = uc($state);
1340           }
1341         }
1342       } # else state doesn't matter
1343     } else {
1344       # couldn't find the country in LSC
1345       $error = "Unknown country: $country";
1346     }
1347   }
1348   $location->set('country', $country);
1349   $location->set('state', $state);
1350   $prospect->set('cust_location', $location);
1351
1352   $error ||= $prospect->insert; # also does location
1353   return { error => $error } if $error;
1354
1355   my $contact = FS::contact->new({
1356       prospectnum   => $prospect->prospectnum,
1357       locationnum   => $location->locationnum,
1358       invoice_dest  => 'Y',
1359   });
1360   # use emailaddress pseudo-field behavior here
1361   foreach (qw(last first title emailaddress comment)) {
1362     $contact->set($_, $packet->{$_});
1363   }
1364   $error = $contact->insert;
1365   if ( $error ) {
1366     $dbh->rollback if $oldAutoCommit;
1367     return { error => $error };
1368   }
1369
1370   foreach my $phone_type (qsearch('phone_type', {})) {
1371     my $key = 'phone_' . lc($phone_type->typename);
1372     my $phonenum = $packet->{$key};
1373     if ( $phonenum ) {
1374       # just to not have to supply country code from the other end
1375       my $number = Number::Phone->new($location->country, $phonenum);
1376       if (!$number) {
1377         $error = 'invalid phone number';
1378       } else {
1379         my $phone = FS::contact_phone->new({
1380             contactnum    => $contact->contactnum,
1381             phonenum      => $phonenum,
1382             countrycode   => $number->country_code,
1383             phonetypenum  => $phone_type->phonetypenum,
1384         });
1385         $error = $phone->insert;
1386       }
1387       if ( $error ) {
1388         $dbh->rollback if $oldAutoCommit;
1389         return { error => $phone_type->typename . ' phone: ' . $error };
1390       }
1391     }
1392   } # foreach $phone_type
1393   
1394   $dbh->commit if $oldAutoCommit;
1395   return { prospectnum => $prospect->prospectnum };
1396 }
1397
1398 1;