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