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