fix race condition where ->apply_payments_and_credits could double-apply in rare...
[freeside.git] / FS / FS / ClientAPI / Signup.pm
1 package FS::ClientAPI::Signup;
2
3 use strict;
4 use vars qw($DEBUG $me);
5 use Data::Dumper;
6 use Tie::RefHash;
7 use FS::Conf;
8 use FS::Record qw(qsearch qsearchs dbdef);
9 use FS::Msgcat qw(gettext);
10 use FS::Misc qw(card_types);
11 use FS::ClientAPI_SessionCache;
12 use FS::agent;
13 use FS::cust_main_county;
14 use FS::part_pkg;
15 use FS::svc_acct_pop;
16 use FS::cust_main;
17 use FS::cust_pkg;
18 use FS::svc_acct;
19 use FS::acct_snarf;
20 use FS::queue;
21 use FS::reg_code;
22
23 $DEBUG = 0;
24 $me = '[FS::ClientAPI::Signup]';
25
26 sub signup_info {
27   my $packet = shift;
28
29   warn "$me signup_info called on $packet\n" if $DEBUG;
30
31   my $conf = new FS::Conf;
32
33   my $cache = new FS::ClientAPI_SessionCache( {
34     'namespace' => 'FS::ClientAPI::Signup',
35   } );
36   my $signup_info_cache = $cache->get('signup_info_cache');
37
38   if ( $signup_info_cache ) {
39
40     warn "$me loading cached signup info\n" if $DEBUG > 1;
41
42   } else {
43
44     warn "$me populating signup info cache\n" if $DEBUG > 1;
45
46     my $agentnum2part_pkg = 
47       {
48         map {
49           my $href = $_->pkgpart_hashref;
50           $_->agentnum =>
51             [
52               map { { 'payby'       => [ $_->payby ],
53                       'freq_pretty' => $_->freq_pretty,
54                       'options'     => { $_->options },
55                       %{$_->hashref}
56                   } }
57                 grep { $_->svcpart('svc_acct') && $href->{ $_->pkgpart } }
58                   qsearch( 'part_pkg', { 'disabled' => '' } )
59             ];
60         } qsearch('agent', { 'disabled' => '' })
61       };
62
63     my $msgcat = { map { $_=>gettext($_) }
64                        qw( passwords_dont_match invalid_card unknown_card_type
65                            not_a empty_password illegal_or_empty_text )
66                  };
67     warn "msgcat: ". Dumper($msgcat). "\n" if $DEBUG > 2;
68
69     my $label = { map { $_ => FS::Msgcat::_gettext($_) }
70                       qw( stateid stateid_state )
71                 };
72     warn "label: ". Dumper($label). "\n" if $DEBUG > 2;
73
74     $signup_info_cache = {
75       'cust_main_county' => [ map $_->hashref,
76                                   qsearch('cust_main_county', {} )
77                             ],
78
79       'agent' => [ map $_->hashref,
80                        qsearch('agent', { 'disabled' => '' } )
81                  ],
82
83       'part_referral' => [ map $_->hashref,
84                                qsearch('part_referral', { 'disabled' => '' } )
85                          ],
86
87       'agentnum2part_pkg' => $agentnum2part_pkg,
88
89       'svc_acct_pop' => [ map $_->hashref, qsearch('svc_acct_pop',{} ) ],
90
91       'emailinvoiceonly' => $conf->exists('emailinvoiceonly'),
92
93       'security_phrase' => $conf->exists('security_phrase'),
94
95       'payby' => [ $conf->config('signup_server-payby') ],
96
97       'card_types' => card_types(),
98
99       'paytypes' => [ @FS::cust_main::paytypes ],
100
101       'cvv_enabled' => 1,
102
103       'stateid_enabled' => $conf->exists('show_stateid'),
104
105       'paystate_enabled' => $conf->exists('show_bankstate'),
106
107       'ship_enabled' => 1,
108
109       'msgcat' => $msgcat,
110
111       'label' => $label,
112
113       'statedefault' => scalar($conf->config('statedefault')) || 'CA',
114
115       'countrydefault' => scalar($conf->config('countrydefault')) || 'US',
116
117       'refnum' => scalar($conf->config('signup_server-default_refnum')),
118
119       'default_pkgpart' => scalar($conf->config('signup_server-default_pkgpart')),
120
121     };
122
123     $cache->set('signup_info_cache', $signup_info_cache);
124
125   }
126
127   my $signup_info = { %$signup_info_cache };
128   warn "$me signup info loaded\n" if $DEBUG > 1;
129   warn Dumper($signup_info). "\n" if $DEBUG > 2;
130
131   my @addl = qw( signup_server-classnum2 signup_server-classnum3 );
132
133   if ( grep { $conf->exists($_) } @addl ) {
134   
135     $signup_info->{optional_packages} = [];
136
137     foreach my $addl ( @addl ) {
138
139       warn "$me adding optional package info\n" if $DEBUG > 1;
140
141       my $classnum = $conf->config($addl) or next;
142
143       my @pkgs = map { {
144                          'freq_pretty' => $_->freq_pretty,
145                          'options'     => { $_->options },
146                          %{ $_->hashref }
147                        };
148                      }
149                      qsearch( 'part_pkg', { classnum => $classnum } );
150
151       push @{$signup_info->{optional_packages}}, \@pkgs;
152
153       warn "$me done adding opt. package info for $classnum\n" if $DEBUG > 1;
154
155     }
156
157   }
158
159   my $agentnum = $packet->{'agentnum'}
160                  || $conf->config('signup_server-default_agentnum');
161   $agentnum =~ /^(\d*)$/ or die "illegal agentnum";
162   $agentnum = $1;
163
164   my $session = '';
165   if ( exists $packet->{'session_id'} ) {
166
167     warn "$me loading agent session\n" if $DEBUG > 1;
168     my $cache = new FS::ClientAPI_SessionCache( {
169       'namespace' => 'FS::ClientAPI::Agent',
170     } );
171     $session = $cache->get($packet->{'session_id'});
172     if ( $session ) {
173       $agentnum = $session->{'agentnum'};
174     } else {
175       return { 'error' => "Can't resume session" }; #better error message
176     }
177     warn "$me done loading agent session\n" if $DEBUG > 1;
178
179   } elsif ( exists $packet->{'customer_session_id'} ) {
180
181     warn "$me loading customer session\n" if $DEBUG > 1;
182     my $cache = new FS::ClientAPI_SessionCache( {
183       'namespace' => 'FS::ClientAPI::MyAccount',
184     } );
185     $session = $cache->get($packet->{'customer_session_id'});
186     if ( $session ) {
187       my $custnum = $session->{'custnum'};
188       my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum });
189       return { 'error' => "Can't find your customer record" } unless $cust_main;
190       $agentnum = $cust_main->agentnum;
191     } else {
192       return { 'error' => "Can't resume session" }; #better error message
193     }
194     warn "$me done loading customer session\n" if $DEBUG > 1;
195
196   }
197
198   $signup_info->{'part_pkg'} = [];
199
200   if ( $packet->{'reg_code'} ) {
201
202     warn "$me setting package list via reg_code\n" if $DEBUG > 1;
203
204     $signup_info->{'part_pkg'} = 
205       [ map { { 'payby'       => [ $_->payby ],
206                 'freq_pretty' => $_->freq_pretty,
207                 'options'     => { $_->options },
208                 %{$_->hashref}
209               };
210             }
211           grep { $_->svcpart('svc_acct') }
212           map { $_->part_pkg }
213             qsearchs( 'reg_code', { 'code'     => $packet->{'reg_code'},
214                                     'agentnum' => $agentnum,              } )
215
216       ];
217
218     $signup_info->{'error'} = 'Unknown registration code'
219       unless @{ $signup_info->{'part_pkg'} };
220
221     warn "$me done setting package list via reg_code\n" if $DEBUG > 1;
222
223   } elsif ( $packet->{'promo_code'} ) {
224
225     warn "$me setting package list via promo_code\n" if $DEBUG > 1;
226
227     $signup_info->{'part_pkg'} =
228       [ map { { 'payby'   => [ $_->payby ],
229                 'freq_pretty' => $_->freq_pretty,
230                 'options'     => { $_->options },
231                 %{$_->hashref}
232             } }
233           grep { $_->svcpart('svc_acct') }
234             qsearch( 'part_pkg', { 'promo_code' => {
235                                      op=>'ILIKE',
236                                      value=>$packet->{'promo_code'}
237                                    },
238                                    'disabled'   => '',                  } )
239       ];
240
241     $signup_info->{'error'} = 'Unknown promotional code'
242       unless @{ $signup_info->{'part_pkg'} };
243
244     warn "$me done setting package list via promo_code\n" if $DEBUG > 1;
245   }
246
247   if ( $agentnum ) {
248
249     warn "$me setting agent-specific package list\n" if $DEBUG > 1;
250     $signup_info->{'part_pkg'} = $signup_info->{'agentnum2part_pkg'}{$agentnum}
251       unless @{ $signup_info->{'part_pkg'} };
252     warn "$me done setting agent-specific package list\n" if $DEBUG > 1;
253
254     warn "$me setting agent-specific adv. source list\n" if $DEBUG > 1;
255     $signup_info->{'part_referral'} =
256       [
257         map { $_->hashref }
258           qsearch( {
259                      'table'     => 'part_referral',
260                      'hashref'   => { 'disabled' => '' },
261                      'extra_sql' => "AND (    agentnum = $agentnum  ".
262                                     "      OR agentnum IS NULL    ) ",
263                    },
264                  )
265       ];
266     warn "$me done setting agent-specific adv. source list\n" if $DEBUG > 1;
267
268   }
269   # else {
270   # delete $signup_info->{'part_pkg'};
271   #}
272
273   warn "$me sorting package list\n" if $DEBUG > 1;
274   $signup_info->{'part_pkg'} = [ sort { $a->{pkg} cmp $b->{pkg} }  # case?
275                                       @{ $signup_info->{'part_pkg'} }
276                                ];
277   warn "$me done sorting package list\n" if $DEBUG > 1;
278
279   if ( exists $packet->{'session_id'} ) {
280     my $agent_signup_info = { %$signup_info };
281     delete $agent_signup_info->{agentnum2part_pkg};
282     $agent_signup_info->{'agent'} = $session->{'agent'};
283     $agent_signup_info;
284   } else {
285     $signup_info;
286   }
287
288 }
289
290 sub new_customer {
291   my $packet = shift;
292
293   my $conf = new FS::Conf;
294   
295   #things that aren't necessary in base class, but are for signup server
296     #return "Passwords don't match"
297     #  if $hashref->{'_password'} ne $hashref->{'_password2'}
298   return { 'error' => gettext('empty_password') }
299     unless length($packet->{'_password'});
300   # a bit inefficient for large numbers of pops
301   return { 'error' => gettext('no_access_number_selected') }
302     unless $packet->{'popnum'} || !scalar(qsearch('svc_acct_pop',{} ));
303
304   my $agentnum;
305   if ( exists $packet->{'session_id'} ) {
306     my $cache = new FS::ClientAPI_SessionCache( {
307       'namespace' => 'FS::ClientAPI::Agent',
308     } );
309     my $session = $cache->get($packet->{'session_id'});
310     if ( $session ) {
311       $agentnum = $session->{'agentnum'};
312     } else {
313       return { 'error' => "Can't resume session" }; #better error message
314     }
315   } else {
316     $agentnum = $packet->{agentnum}
317                 || $conf->config('signup_server-default_agentnum');
318   }
319
320   #shares some stuff with htdocs/edit/process/cust_main.cgi... take any
321   # common that are still here and library them.
322   my $cust_main = new FS::cust_main ( {
323     #'custnum'          => '',
324     'agentnum'      => $agentnum,
325     'refnum'        => $packet->{refnum}
326                        || $conf->config('signup_server-default_refnum'),
327
328     map { $_ => $packet->{$_} } qw(
329
330       last first ss company address1 address2
331       city county state zip country
332       daytime night fax stateid stateid_state
333
334       ship_last ship_first ship_ss ship_company ship_address1 ship_address2
335       ship_city ship_county ship_state ship_zip ship_country
336       ship_daytime ship_night ship_fax
337
338       payby
339       payinfo paycvv paydate payname paystate paytype
340       paystart_month paystart_year payissue
341       payip
342
343       referral_custnum comments
344     )
345
346   } );
347
348   return { 'error' => "Illegal payment type" }
349     unless grep { $_ eq $packet->{'payby'} }
350                 $conf->config('signup_server-payby');
351
352   $cust_main->payinfo($cust_main->daytime)
353     if $cust_main->payby eq 'LECB' && ! $cust_main->payinfo;
354
355   my @invoicing_list = $packet->{'invoicing_list'}
356                          ? split( /\s*\,\s*/, $packet->{'invoicing_list'} )
357                          : ();
358
359   $packet->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/;
360   my $pkgpart = $1;
361   return { 'error' => 'Please select a package' } unless $pkgpart; #msgcat
362
363   my $part_pkg =
364     qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } )
365       or return { 'error' => "WARNING: unknown pkgpart: $pkgpart" };
366   my $svcpart = $part_pkg->svcpart('svc_acct');
367
368   my $reg_code = '';
369   if ( $packet->{'reg_code'} ) {
370     $reg_code = qsearchs( 'reg_code', { 'code'     => $packet->{'reg_code'},
371                                         'agentnum' => $agentnum,             } )
372       or return { 'error' => 'Unknown registration code' };
373   }
374
375   my $cust_pkg = new FS::cust_pkg ( {
376     #later#'custnum' => $custnum,
377     'pkgpart'    => $packet->{'pkgpart'},
378     'promo_code' => $packet->{'promo_code'},
379     'reg_code'   => $packet->{'reg_code'},
380   } );
381   #my $error = $cust_pkg->check;
382   #return { 'error' => $error } if $error;
383
384   my $svc_acct = new FS::svc_acct ( {
385     'svcpart'   => $svcpart,
386     map { $_ => $packet->{$_} }
387       qw( username _password sec_phrase popnum ),
388   } );
389
390   my @acct_snarf;
391   my $snarfnum = 1;
392   while (    exists($packet->{"snarf_machine$snarfnum"})
393           && length($packet->{"snarf_machine$snarfnum"}) ) {
394     my $acct_snarf = new FS::acct_snarf ( {
395       'machine'   => $packet->{"snarf_machine$snarfnum"},
396       'protocol'  => $packet->{"snarf_protocol$snarfnum"},
397       'username'  => $packet->{"snarf_username$snarfnum"},
398       '_password' => $packet->{"snarf_password$snarfnum"},
399     } );
400     $snarfnum++;
401     push @acct_snarf, $acct_snarf;
402   }
403   $svc_acct->child_objects( \@acct_snarf );
404
405   my $y = $svc_acct->setdefault; # arguably should be in new method
406   return { 'error' => $y } if $y && !ref($y);
407
408   #$error = $svc_acct->check;
409   #return { 'error' => $error } if $error;
410
411   #setup a job dependancy to delay provisioning
412   my $placeholder = new FS::queue ( {
413     'job'    => 'FS::ClientAPI::Signup::__placeholder',
414     'status' => 'locked',
415   } );
416   my $error = $placeholder->insert;
417   return { 'error' => $error } if $error;
418
419   use Tie::RefHash;
420   tie my %hash, 'Tie::RefHash';
421   %hash = ( $cust_pkg => [ $svc_acct ] );
422   #msgcat
423   $error = $cust_main->insert(
424     \%hash,
425     \@invoicing_list,
426     'depend_jobnum' => $placeholder->jobnum,
427   );
428   if ( $error ) {
429     my $perror = $placeholder->delete;
430     $error .= " (Additionally, error removing placeholder: $perror)" if $perror;
431     return { 'error' => $error };
432   }
433
434   if ( $conf->exists('signup_server-realtime') ) {
435
436     #warn "[fs_signup_server] Billing customer...\n" if $Debug;
437
438     my $bill_error = $cust_main->bill;
439     #warn "[fs_signup_server] error billing new customer: $bill_error"
440     #  if $bill_error;
441
442     $bill_error = $cust_main->apply_payments_and_credits;
443     #warn "[fs_signup_server] error applying payments and credits for".
444     #     " new customer: $bill_error"
445     #  if $bill_error;
446
447     $bill_error = $cust_main->collect('realtime' => 1);
448     #warn "[fs_signup_server] error collecting from new customer: $bill_error"
449     #  if $bill_error;
450
451     if ( $cust_main->balance > 0 ) {
452
453       #this makes sense.  credit is "un-doing" the invoice
454       $cust_main->credit( $cust_main->balance, 'signup server decline' );
455       $cust_main->apply_credits;
456
457       #should check list for errors...
458       #$cust_main->suspend;
459       local $FS::svc_Common::noexport_hack = 1;
460       $cust_main->cancel('quiet'=>1);
461
462       my $perror = $placeholder->depended_delete;
463       warn "error removing provisioning jobs after decline: $perror" if $perror;
464       unless ( $perror ) {
465         $perror = $placeholder->delete;
466         warn "error removing placeholder after decline: $perror" if $perror;
467       }
468
469       return { 'error' => '_decline' };
470     }
471
472   }
473
474   if ( $reg_code ) {
475     $error = $reg_code->delete;
476     return { 'error' => $error } if $error;
477   }
478
479   $error = $placeholder->delete;
480   return { 'error' => $error } if $error;
481
482   return { error => '' };
483
484 }
485
486 1;