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