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