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