have signup page use card-types config too
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
1 package FS::ClientAPI::MyAccount;
2
3 use strict;
4 use vars qw($cache);
5 use subs qw(_cache);
6 use Digest::MD5 qw(md5_hex);
7 use Date::Format;
8 use Business::CreditCard;
9 use Time::Duration;
10 use FS::CGI qw(small_custview); #doh
11 use FS::Conf;
12 use FS::Record qw(qsearch qsearchs);
13 use FS::Msgcat qw(gettext);
14 use FS::Misc qw(card_types);
15 use FS::ClientAPI_SessionCache;
16 use FS::svc_acct;
17 use FS::svc_domain;
18 use FS::svc_external;
19 use FS::part_svc;
20 use FS::cust_main;
21 use FS::cust_bill;
22 use FS::cust_main_county;
23 use FS::cust_pkg;
24
25 use vars qw( @cust_main_editable_fields );
26 @cust_main_editable_fields = qw(
27   first last company address1 address2 city
28     county state zip country daytime night fax
29   ship_first ship_last ship_company ship_address1 ship_address2 ship_city
30     ship_state ship_zip ship_country ship_daytime ship_night ship_fax
31   payby payinfo payname paystart_month paystart_year payissue payip
32 );
33
34 use subs qw(_provision);
35
36 sub _cache {
37   $cache ||= new FS::ClientAPI_SessionCache( {
38                'namespace' => 'FS::ClientAPI::MyAccount',
39              } );
40 }
41
42 #false laziness w/FS::ClientAPI::passwd::passwd
43 sub login {
44   my $p = shift;
45
46   my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
47     or return { error => 'Domain '. $p->{'domain'}. ' not found' };
48
49   my $svc_acct = qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
50                                          'domsvc'    => $svc_domain->svcnum, }
51                          );
52   return { error => 'User not found.' } unless $svc_acct;
53
54   my $conf = new FS::Conf;
55   my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
56   return { error => 'Only primary user may log in.' } 
57     if $conf->exists('selfservice_server-primary_only')
58        && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
59
60   return { error => 'Incorrect password.' }
61     unless $svc_acct->check_password($p->{'password'});
62
63   my $session = {
64     'svcnum' => $svc_acct->svcnum,
65   };
66
67   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
68   if ( $cust_pkg ) {
69     my $cust_main = $cust_pkg->cust_main;
70     $session->{'custnum'} = $cust_main->custnum;
71   }
72
73   my $session_id;
74   do {
75     $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
76   } until ( ! defined _cache->get($session_id) ); #just in case
77
78   _cache->set( $session_id, $session, '1 hour' );
79
80   return { 'error'      => '',
81            'session_id' => $session_id,
82          };
83 }
84
85 sub logout {
86   my $p = shift;
87   if ( $p->{'session_id'} ) {
88     _cache->remove($p->{'session_id'});
89     return { 'error' => '' };
90   } else {
91     return { 'error' => "Can't resume session" }; #better error message
92   }
93 }
94
95 sub customer_info {
96   my $p = shift;
97
98   my($context, $session, $custnum) = _custoragent_session_custnum($p);
99   return { 'error' => $session } if $context eq 'error';
100
101   my %return;
102   if ( $custnum ) { #customer record
103
104     my $search = { 'custnum' => $custnum };
105     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
106     my $cust_main = qsearchs('cust_main', $search )
107       or return { 'error' => "unknown custnum $custnum" };
108
109     $return{balance} = $cust_main->balance;
110
111     my @open = map {
112                      {
113                        invnum => $_->invnum,
114                        date   => time2str("%b %o, %Y", $_->_date),
115                        owed   => $_->owed,
116                      };
117                    } $cust_main->open_cust_bill;
118     $return{open_invoices} = \@open;
119
120     my $conf = new FS::Conf;
121     $return{small_custview} =
122       small_custview( $cust_main, $conf->config('countrydefault') );
123
124     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
125
126     for (@cust_main_editable_fields) {
127       $return{$_} = $cust_main->get($_);
128     }
129
130     if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
131       $return{payinfo} = $cust_main->payinfo_masked;
132       @return{'month', 'year'} = $cust_main->paydate_monthyear;
133     }
134
135     $return{'invoicing_list'} =
136       join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
137     $return{'postal_invoicing'} =
138       0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
139
140   } elsif ( $session->{'svcnum'} ) { #no customer record
141
142     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
143       or die "unknown svcnum";
144     $return{name} = $svc_acct->email;
145
146   } else {
147
148     return { 'error' => 'Expired session' }; #XXX redirect to login w/this err!
149
150   }
151
152   return { 'error'          => '',
153            'custnum'        => $custnum,
154            %return,
155          };
156
157 }
158
159 sub edit_info {
160   my $p = shift;
161   my $session = _cache->get($p->{'session_id'})
162     or return { 'error' => "Can't resume session" }; #better error message
163
164   my $custnum = $session->{'custnum'}
165     or return { 'error' => "no customer record" };
166
167   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
168     or return { 'error' => "unknown custnum $custnum" };
169
170   my $new = new FS::cust_main { $cust_main->hash };
171   $new->set( $_ => $p->{$_} )
172     foreach grep { exists $p->{$_} } @cust_main_editable_fields;
173
174   if ( $p->{'payby'} =~ /^(CARD|DCRD)$/ ) {
175     $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
176     if ( $new->payinfo eq $cust_main->payinfo_masked ) {
177       $new->payinfo($cust_main->payinfo);
178     } else {
179       $new->paycvv($p->{'paycvv'});
180     }
181   }
182
183   my @invoicing_list;
184   if ( exists $p->{'invoicing_list'} || exists $p->{'postal_invoicing'} ) {
185     #false laziness with httemplate/edit/process/cust_main.cgi
186     @invoicing_list = split( /\s*\,\s*/, $p->{'invoicing_list'} );
187     push @invoicing_list, 'POST' if $p->{'postal_invoicing'};
188   } else {
189     @invoicing_list = $cust_main->invoicing_list;
190   }
191
192   my $error = $new->replace($cust_main, \@invoicing_list);
193   return { 'error' => $error } if $error;
194   #$cust_main = $new;
195   
196   return { 'error' => '' };
197 }
198
199 sub payment_info {
200   my $p = shift;
201   my $session = _cache->get($p->{'session_id'})
202     or return { 'error' => "Can't resume session" }; #better error message
203
204   ##
205   #generic
206   ##
207
208   use vars qw($payment_info); #cache for performance
209   unless ( $payment_info ) {
210
211     my $conf = new FS::Conf;
212     my %states = map { $_->state => 1 }
213                    qsearch('cust_main_county', {
214                      'country' => $conf->config('countrydefault') || 'US'
215                    } );
216
217     $payment_info = {
218
219       #list all counties/states/countries
220       'cust_main_county' => 
221         [ map { $_->hashref } qsearch('cust_main_county', {}) ],
222
223       #shortcut for one-country folks
224       'states' =>
225         [ sort { $a cmp $b } keys %states ],
226
227       'card_types' => card_types(),
228
229     };
230
231   }
232
233   ##
234   #customer-specific
235   ##
236
237   my %return = %$payment_info;
238
239   my $custnum = $session->{'custnum'};
240
241   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
242     or return { 'error' => "unknown custnum $custnum" };
243
244   $return{balance} = $cust_main->balance;
245
246   $return{payname} = $cust_main->payname
247                      || ( $cust_main->first. ' '. $cust_main->get('last') );
248
249   $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip);
250
251   $return{payby} = $cust_main->payby;
252
253   if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
254     $return{card_type} = cardtype($cust_main->payinfo);
255     $return{payinfo} = $cust_main->payinfo;
256
257     @return{'month', 'year'} = $cust_main->paydate_monthyear;
258
259   }
260
261   #doubleclick protection
262   my $_date = time;
263   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
264
265   return { 'error' => '',
266            %return,
267          };
268
269 };
270
271 #some false laziness with httemplate/process/payment.cgi - look there for
272 #ACH and CVV support stuff
273 sub process_payment {
274
275   my $p = shift;
276
277   my $session = _cache->get($p->{'session_id'})
278     or return { 'error' => "Can't resume session" }; #better error message
279
280   my %return;
281
282   my $custnum = $session->{'custnum'};
283
284   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
285     or return { 'error' => "unknown custnum $custnum" };
286
287   $p->{'payname'} =~ /^([\w \,\.\-\']+)$/
288     or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
289   my $payname = $1;
290
291   $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
292     or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
293   my $paybatch = $1;
294
295   my $payinfo;
296   my $paycvv = '';
297   #if ( $payby eq 'CHEK' ) {
298   #
299   #  $p->{'payinfo1'} =~ /^(\d+)$/
300   #    or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
301   #  my $payinfo1 = $1;
302   #   $p->{'payinfo2'} =~ /^(\d+)$/
303   #    or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
304   #  my $payinfo2 = $1;
305   #  $payinfo = $payinfo1. '@'. $payinfo2;
306   # 
307   #} elsif ( $payby eq 'CARD' ) {
308    
309     $payinfo = $p->{'payinfo'};
310     $payinfo =~ s/\D//g;
311     $payinfo =~ /^(\d{13,16})$/
312       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
313     $payinfo = $1;
314     validate($payinfo)
315       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
316     return { 'error' => gettext('unknown_card_type') }
317       if cardtype($payinfo) eq "Unknown";
318
319     if ( defined $cust_main->dbdef_table->column('paycvv') ) {
320       if ( length($p->{'paycvv'} ) ) {
321         if ( cardtype($payinfo) eq 'American Express card' ) {
322           $p->{'paycvv'} =~ /^(\d{4})$/
323             or return { 'error' => "CVV2 (CID) for American Express cards is four digits." };
324           $paycvv = $1;
325         } else {
326           $p->{'paycvv'} =~ /^(\d{3})$/
327             or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
328           $paycvv = $1;
329         }
330       }
331     }
332   
333   #} else {
334   #  die "unknown payby $payby";
335   #}
336
337   my $error = $cust_main->realtime_bop( 'CC', $p->{'amount'},
338     'quiet'    => 1,
339     'payinfo'  => $payinfo,
340     'paydate'  => $p->{'year'}. '-'. $p->{'month'}. '-01',
341     'payname'  => $payname,
342     'paybatch' => $paybatch,
343     'paycvv'   => $paycvv,
344     map { $_ => $p->{$_} } qw( paystart_month paystart_year payissue payip
345                                address1 address2 city state zip )
346   );
347   return { 'error' => $error } if $error;
348
349   $cust_main->apply_payments;
350
351   if ( $p->{'save'} ) {
352     my $new = new FS::cust_main { $cust_main->hash };
353     $new->set( $_ => $p->{$_} )
354       foreach qw( payname paystart_month paystart_year payissue payip
355                   address1 address2 city state zip payinfo );
356     $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' );
357     $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
358     my $error = $new->replace($cust_main);
359     return { 'error' => $error } if $error;
360     $cust_main = $new;
361   }
362
363   return { 'error' => '' };
364
365 }
366
367 sub process_prepay {
368
369   my $p = shift;
370
371   my $session = _cache->get($p->{'session_id'})
372     or return { 'error' => "Can't resume session" }; #better error message
373
374   my %return;
375
376   my $custnum = $session->{'custnum'};
377
378   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
379     or return { 'error' => "unknown custnum $custnum" };
380
381   my( $amount, $seconds ) = ( 0, 0 );
382   my $error = $cust_main->recharge_prepay( $p->{'prepaid_cardnum'},
383                                            \$amount,
384                                            \$seconds
385                                          );
386
387   return { 'error' => $error } if $error;
388
389   return { 'error'    => '',
390            'amount'   => $amount,
391            'seconds'  => $seconds,
392            'duration' => duration_exact($seconds),
393          };
394
395 }
396
397 sub invoice {
398   my $p = shift;
399   my $session = _cache->get($p->{'session_id'})
400     or return { 'error' => "Can't resume session" }; #better error message
401
402   my $custnum = $session->{'custnum'};
403
404   my $invnum = $p->{'invnum'};
405
406   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
407                                           'custnum' => $custnum } )
408     or return { 'error' => "Can't find invnum" };
409
410   #my %return;
411
412   return { 'error'        => '',
413            'invnum'       => $invnum,
414            'invoice_text' => join('', $cust_bill->print_text ),
415            'invoice_html' => $cust_bill->print_html,
416          };
417
418 }
419
420 sub invoice_logo {
421   my $p = shift;
422
423   #sessioning for this?  how do we get the session id to the backend invoice
424   # template so it can add it to the link, blah
425
426   my $templatename = $p->{'templatename'};
427
428   #false laziness-ish w/view/cust_bill-logo.cgi
429
430   my $conf = new FS::Conf;
431   if ( $templatename =~ /^([^\.\/]*)$/ && $conf->exists("logo_$1.png") ) {
432     $templatename = "_$1";
433   } else {
434     $templatename = '';
435   }
436
437   my $filename = "logo$templatename.png";
438
439   return { 'error'        => '',
440            'logo'         => $conf->config_binary($filename),
441            'content_type' => 'image/png', #should allow gif, jpg too
442          };
443 }
444
445
446 sub list_invoices {
447   my $p = shift;
448   my $session = _cache->get($p->{'session_id'})
449     or return { 'error' => "Can't resume session" }; #better error message
450
451   my $custnum = $session->{'custnum'};
452
453   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
454     or return { 'error' => "unknown custnum $custnum" };
455
456   my @cust_bill = $cust_main->cust_bill;
457
458   return  { 'error'       => '',
459             'invoices'    =>  [ map { { 'invnum' => $_->invnum,
460                                         '_date'  => $_->_date,
461                                       }
462                                     } @cust_bill
463                               ]
464           };
465 }
466
467 sub cancel {
468   my $p = shift;
469   my $session = _cache->get($p->{'session_id'})
470     or return { 'error' => "Can't resume session" }; #better error message
471
472   my $custnum = $session->{'custnum'};
473
474   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
475     or return { 'error' => "unknown custnum $custnum" };
476
477   my @errors = $cust_main->cancel( 'quiet'=>1 );
478
479   my $error = scalar(@errors) ? join(' / ', @errors) : '';
480
481   return { 'error' => $error };
482
483 }
484
485 sub list_pkgs {
486   my $p = shift;
487
488   my($context, $session, $custnum) = _custoragent_session_custnum($p);
489   return { 'error' => $session } if $context eq 'error';
490
491   my $search = { 'custnum' => $custnum };
492   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
493   my $cust_main = qsearchs('cust_main', $search )
494     or return { 'error' => "unknown custnum $custnum" };
495
496   #return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] };
497
498   my $conf = new FS::Conf;
499
500   { 'svcnum'   => $session->{'svcnum'},
501     'custnum'  => $custnum,
502     'cust_pkg' => [ map {
503                           { $_->hash,
504                             $_->part_pkg->hash,
505                             part_svc =>
506                               [ map $_->hashref, $_->available_part_svc ],
507                             cust_svc => 
508                               [ map { my $ref = { $_->hash,
509                                                   label => [ $_->label ],
510                                                 };
511                                       $ref->{_password} = $_->svc_x->_password
512                                         if $context eq 'agent'
513                                         && $conf->exists('agent-showpasswords')
514                                         && $_->part_svc->svcdb eq 'svc_acct';
515                                       $ref;
516                                     } $_->cust_svc
517                               ],
518                           };
519                         } $cust_main->ncancelled_pkgs
520                   ],
521     'small_custview' =>
522       small_custview( $cust_main, $conf->config('countrydefault') ),
523   };
524
525 }
526
527 sub list_svcs {
528   my $p = shift;
529
530   use Data::Dumper;
531
532   my($context, $session, $custnum) = _custoragent_session_custnum($p);
533   return { 'error' => $session } if $context eq 'error';
534
535   my $search = { 'custnum' => $custnum };
536   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
537   my $cust_main = qsearchs('cust_main', $search )
538     or return { 'error' => "unknown custnum $custnum" };
539
540   my @cust_svc = ();
541   #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
542   foreach my $cust_pkg ( $cust_main->unsuspended_pkgs ) {
543     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
544   }
545   @cust_svc = grep { $_->part_svc->svcdb eq $p->{'svcdb'} } @cust_svc
546     if $p->{'svcdb'};
547
548   #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
549   #              @svc_x;
550
551   { 
552     #no#'svcnum'   => $session->{'svcnum'},
553     'custnum'  => $custnum,
554     'svcs'     => [ map { 
555                           my $svc_x = $_->svc_x;
556                           my($label, $value) = $_->label;
557
558                           { 'svcnum'   => $_->svcnum,
559                             'label'    => $label,
560                             'value'    => $value,
561                             'username' => $svc_x->username,
562                             'email'    => $svc_x->email,
563                             # more...
564                           };
565                         }
566                         @cust_svc
567                   ],
568   };
569
570 }
571
572 sub order_pkg {
573   my $p = shift;
574
575   my($context, $session, $custnum) = _custoragent_session_custnum($p);
576   return { 'error' => $session } if $context eq 'error';
577
578   my $search = { 'custnum' => $custnum };
579   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
580   my $cust_main = qsearchs('cust_main', $search )
581     or return { 'error' => "unknown custnum $custnum" };
582
583   #false laziness w/ClientAPI/Signup.pm
584
585   my $cust_pkg = new FS::cust_pkg ( {
586     'custnum' => $custnum,
587     'pkgpart' => $p->{'pkgpart'},
588   } );
589   my $error = $cust_pkg->check;
590   return { 'error' => $error } if $error;
591
592   my @svc = ();
593   unless ( $p->{'svcpart'} eq 'none' ) {
594
595     my $svcdb;
596     my $svcpart = '';
597     if ( $p->{'svcpart'} =~ /^(\d+)$/ ) {
598       $svcpart = $1;
599       my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
600       return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc;
601       $svcdb = $part_svc->svcdb;
602     } else {
603       $svcdb = 'svc_acct';
604     }
605     $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb);
606
607     my %fields = (
608       'svc_acct'     => [ qw( username _password sec_phrase popnum ) ],
609       'svc_domain'   => [ qw( domain ) ],
610       'svc_external' => [ qw( id title ) ],
611     );
612   
613     my $svc_x = "FS::$svcdb"->new( {
614       'svcpart'   => $svcpart,
615       map { $_ => $p->{$_} } @{$fields{$svcdb}}
616     } );
617     
618     if ( $svcdb eq 'svc_acct' ) {
619       my @acct_snarf;
620       my $snarfnum = 1;
621       while ( length($p->{"snarf_machine$snarfnum"}) ) {
622         my $acct_snarf = new FS::acct_snarf ( {
623           'machine'   => $p->{"snarf_machine$snarfnum"},
624           'protocol'  => $p->{"snarf_protocol$snarfnum"},
625           'username'  => $p->{"snarf_username$snarfnum"},
626           '_password' => $p->{"snarf_password$snarfnum"},
627         } );
628         $snarfnum++;
629         push @acct_snarf, $acct_snarf;
630       }
631       $svc_x->child_objects( \@acct_snarf );
632     }
633     
634     my $y = $svc_x->setdefault; # arguably should be in new method
635     return { 'error' => $y } if $y && !ref($y);
636   
637     $error = $svc_x->check;
638     return { 'error' => $error } if $error;
639
640     push @svc, $svc_x;
641
642   }
643
644   use Tie::RefHash;
645   tie my %hash, 'Tie::RefHash';
646   %hash = ( $cust_pkg => \@svc );
647   #msgcat
648   $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 );
649   return { 'error' => $error } if $error;
650
651   my $conf = new FS::Conf;
652   if ( $conf->exists('signup_server-realtime') ) {
653
654     my $old_balance = $cust_main->balance;
655
656     my $bill_error = $cust_main->bill;
657     $cust_main->apply_payments;
658     $cust_main->apply_credits;
659     $bill_error = $cust_main->collect('realtime' => 1);
660
661     if (    $cust_main->balance > $old_balance
662          && $cust_main->balance > 0
663          && $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ) {
664       #this makes sense.  credit is "un-doing" the invoice
665       $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ),
666                           'self-service decline' );
667       $cust_main->apply_credits( 'order' => 'newest' );
668
669       $cust_pkg->cancel('quiet'=>1);
670       return { 'error' => '_decline', 'bill_error' => $bill_error };
671     } else {
672       $cust_pkg->reexport;
673     }
674
675   } else {
676     $cust_pkg->reexport;
677   }
678
679   return { error => '', pkgnum => $cust_pkg->pkgnum };
680
681 }
682
683 sub cancel_pkg {
684   my $p = shift;
685   my $session = _cache->get($p->{'session_id'})
686     or return { 'error' => "Can't resume session" }; #better error message
687
688   my $custnum = $session->{'custnum'};
689
690   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
691     or return { 'error' => "unknown custnum $custnum" };
692
693   my $pkgnum = $p->{'pkgnum'};
694
695   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
696                                         'pkgnum'  => $pkgnum,   } )
697     or return { 'error' => "unknown pkgnum $pkgnum" };
698
699   my $error = $cust_pkg->cancel( 'quiet'=>1 );
700   return { 'error' => $error };
701
702 }
703
704 sub provision_acct {
705   my $p = shift;
706
707   return { 'error' => gettext('passwords_dont_match') }
708     if $p->{'_password'} ne $p->{'_password2'};
709   return { 'error' => gettext('empty_password') }
710     unless length($p->{'_password'});
711
712   _provision( 'FS::svc_acct',
713               [qw(username _password)],
714               [qw(username _password)],
715               $p,
716               @_
717             );
718 }
719
720 sub provision_external {
721   my $p = shift;
722   #_provision( 'FS::svc_external', [qw(id title)], [qw(id title)], $p, @_ );
723   _provision( 'FS::svc_external',
724               [],
725               [qw(id title)],
726               $p,
727               @_
728             );
729 }
730
731 sub _provision {
732   my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
733
734   my($context, $session, $custnum) = _custoragent_session_custnum($p);
735   return { 'error' => $session } if $context eq 'error';
736
737   my $search = { 'custnum' => $custnum };
738   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
739   my $cust_main = qsearchs('cust_main', $search )
740     or return { 'error' => "unknown custnum $custnum" };
741
742   my $pkgnum = $p->{'pkgnum'};
743
744   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
745                                         'pkgnum'  => $pkgnum,
746                                                                } )
747     or return { 'error' => "unknown pkgnum $pkgnum" };
748
749   my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
750     or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
751
752   my $svc_x = $class->new( {
753     'pkgnum'  => $p->{'pkgnum'},
754     'svcpart' => $p->{'svcpart'},
755     map { $_ => $p->{$_} } @$fields
756   } );
757   my $error = $svc_x->insert;
758   $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum })
759     unless $error;
760
761   return { 'svc'   => $part_svc->svc,
762            'error' => $error,
763            map { $_ => $svc_x->get($_) } @$return_fields
764          };
765
766 }
767
768 sub part_svc_info {
769   my $p = shift;
770
771   my($context, $session, $custnum) = _custoragent_session_custnum($p);
772   return { 'error' => $session } if $context eq 'error';
773
774   my $search = { 'custnum' => $custnum };
775   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
776   my $cust_main = qsearchs('cust_main', $search )
777     or return { 'error' => "unknown custnum $custnum" };
778
779   my $pkgnum = $p->{'pkgnum'};
780
781   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
782                                         'pkgnum'  => $pkgnum,
783                                                                } )
784     or return { 'error' => "unknown pkgnum $pkgnum" };
785
786   my $svcpart = $p->{'svcpart'};
787
788   my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart,
789                                       'svcpart' => $svcpart,           } )
790     or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" };
791   my $part_svc = $pkg_svc->part_svc;
792
793   my $conf = new FS::Conf;
794
795   return {
796     'svc'     => $part_svc->svc,
797     'svcdb'   => $part_svc->svcdb,
798     'pkgnum'  => $pkgnum,
799     'svcpart' => $svcpart,
800     'custnum' => $custnum,
801
802     'security_phrase' => 0, #XXX !
803     'svc_acct_pop'    => [], #XXX !
804     'popnum'          => '',
805     'init_popstate'   => '',
806     'popac'           => '',
807     'acstate'         => '',
808
809     'small_custview' =>
810       small_custview( $cust_main, $conf->config('countrydefault') ),
811
812   };
813
814 }
815
816 sub unprovision_svc {
817   my $p = shift;
818
819   my($context, $session, $custnum) = _custoragent_session_custnum($p);
820   return { 'error' => $session } if $context eq 'error';
821
822   my $search = { 'custnum' => $custnum };
823   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
824   my $cust_main = qsearchs('cust_main', $search )
825     or return { 'error' => "unknown custnum $custnum" };
826
827   my $svcnum = $p->{'svcnum'};
828
829   my $cust_svc = qsearchs('cust_svc', { 'svcnum'  => $svcnum, } )
830     or return { 'error' => "unknown svcnum $svcnum" };
831
832   return { 'error' => "Service $svcnum does not belong to customer $custnum" }
833     unless $cust_svc->cust_pkg->custnum == $custnum;
834
835   my $conf = new FS::Conf;
836
837   return { 'svc'   => $cust_svc->part_svc->svc,
838            'error' => $cust_svc->cancel,
839            'small_custview' =>
840              small_custview( $cust_main, $conf->config('countrydefault') ),
841          };
842
843 }
844
845 sub myaccount_passwd {
846   my $p = shift;
847   my($context, $session, $custnum) = _custoragent_session_custnum($p);
848   return { 'error' => $session } if $context eq 'error';
849
850   return { 'error' => "New passwords don't match." }
851     if $p->{'new_password'} ne $p->{'new_password2'};
852
853   return { 'error' => 'Enter new password' }
854     unless length($p->{'new_password'});
855
856   #my $search = { 'custnum' => $custnum };
857   #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
858   $custnum =~ /^(\d+)$/ or die "illegal custnum";
859   my $search = " AND custnum = $1";
860   $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
861
862   my $svc_acct = qsearchs( {
863     'table'     => 'svc_acct',
864     'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
865                    'LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
866                    'LEFT JOIN cust_main USING ( custnum ) ',
867     'hashref'   => { 'svcnum' => $p->{'svcnum'}, },
868     'extra_sql' => $search, #important
869   } )
870     or return { 'error' => "Service not found" };
871
872   $svc_acct->_password($p->{'new_password'});
873   my $error = $svc_acct->replace();
874
875   my($label, $value) = $svc_acct->cust_svc->label;
876
877   return { 'error' => $error,
878            'label' => $label,
879            'value' => $value,
880          };
881
882 }
883
884 #--
885
886 sub _custoragent_session_custnum {
887   my $p = shift;
888
889   my($context, $session, $custnum);
890   if ( $p->{'session_id'} ) {
891
892     $context = 'customer';
893     $session = _cache->get($p->{'session_id'})
894       or return ( 'error' => "Can't resume session" ); #better error message
895     $custnum = $session->{'custnum'};
896
897   } elsif ( $p->{'agent_session_id'} ) {
898
899     $context = 'agent';
900     my $agent_cache = new FS::ClientAPI_SessionCache( {
901       'namespace' => 'FS::ClientAPI::Agent',
902     } );
903     $session = $agent_cache->get($p->{'agent_session_id'})
904       or return ( 'error' => "Can't resume session" ); #better error message
905     $custnum = $p->{'custnum'};
906
907   } else {
908     return ( 'error' => "Can't resume session" ); #better error message
909   }
910
911   ($context, $session, $custnum);
912
913 }
914
915 1;
916