add card-types configuration value for self-service
[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   use vars qw($payment_info); #cache for performance
208   unless ( $payment_info ) {
209
210     my $conf = new FS::Conf;
211     my %states = map { $_->state => 1 }
212                    qsearch('cust_main_county', {
213                      'country' => $conf->config('countrydefault') || 'US'
214                    } );
215
216     my %card_types = (
217       #displayname                    #value (Business::CreditCard)
218       "VISA"                       => "VISA card",
219       "MasterCard"                 => "MasterCard",
220       "Discover"                   => "Discover card",
221       "American Express"           => "American Express card",
222       "Diner's Club/Carte Blanche" => "Diner's Club/Carte Blanche",
223       "enRoute"                    => "enRoute",
224       "JCB"                        => "JCB",
225       "BankCard"                   => "BankCard",
226       "Switch"                     => "Switch",
227       "Solo"                       => "Solo",
228     );
229     my @conf_card_types = grep { ! /^\s*$/ } $conf->config('card-types');
230     if ( @conf_card_types ) {
231       #perhaps the hash is backwards for this, but this way works better for
232       #usage in selfservice
233       %card_types = map  { $_ => $card_types{$_} }
234                     grep {
235                            my $d = $_;
236                            grep { $card_types{$d} eq $_ } @conf_card_types
237                          }
238                     keys %card_types;
239     }
240
241     $payment_info = {
242
243       #list all counties/states/countries
244       'cust_main_county' => 
245         [ map { $_->hashref } qsearch('cust_main_county', {}) ],
246
247       #shortcut for one-country folks
248       'states' =>
249         [ sort { $a cmp $b } keys %states ],
250
251       'card_types' => {
252         'VISA' => 'VISA card',
253         'MasterCard' => 'MasterCard',
254         'Discover' => 'Discover card',
255         'American Express' => 'American Express card',
256         'Switch' => 'Switch',
257         'Solo' => 'Solo',
258       },
259
260     };
261
262   }
263
264   ##
265   #customer-specific
266   ##
267
268   my %return = %$payment_info;
269
270   my $custnum = $session->{'custnum'};
271
272   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
273     or return { 'error' => "unknown custnum $custnum" };
274
275   $return{balance} = $cust_main->balance;
276
277   $return{payname} = $cust_main->payname
278                      || ( $cust_main->first. ' '. $cust_main->get('last') );
279
280   $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip);
281
282   $return{payby} = $cust_main->payby;
283
284   if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
285     $return{card_type} = cardtype($cust_main->payinfo);
286     $return{payinfo} = $cust_main->payinfo;
287
288     @return{'month', 'year'} = $cust_main->paydate_monthyear;
289
290   }
291
292   #doubleclick protection
293   my $_date = time;
294   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
295
296   return { 'error' => '',
297            %return,
298          };
299
300 };
301
302 #some false laziness with httemplate/process/payment.cgi - look there for
303 #ACH and CVV support stuff
304 sub process_payment {
305
306   my $p = shift;
307
308   my $session = _cache->get($p->{'session_id'})
309     or return { 'error' => "Can't resume session" }; #better error message
310
311   my %return;
312
313   my $custnum = $session->{'custnum'};
314
315   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
316     or return { 'error' => "unknown custnum $custnum" };
317
318   $p->{'payname'} =~ /^([\w \,\.\-\']+)$/
319     or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
320   my $payname = $1;
321
322   $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
323     or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
324   my $paybatch = $1;
325
326   my $payinfo;
327   my $paycvv = '';
328   #if ( $payby eq 'CHEK' ) {
329   #
330   #  $p->{'payinfo1'} =~ /^(\d+)$/
331   #    or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
332   #  my $payinfo1 = $1;
333   #   $p->{'payinfo2'} =~ /^(\d+)$/
334   #    or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
335   #  my $payinfo2 = $1;
336   #  $payinfo = $payinfo1. '@'. $payinfo2;
337   # 
338   #} elsif ( $payby eq 'CARD' ) {
339    
340     $payinfo = $p->{'payinfo'};
341     $payinfo =~ s/\D//g;
342     $payinfo =~ /^(\d{13,16})$/
343       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
344     $payinfo = $1;
345     validate($payinfo)
346       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
347     return { 'error' => gettext('unknown_card_type') }
348       if cardtype($payinfo) eq "Unknown";
349
350     if ( defined $cust_main->dbdef_table->column('paycvv') ) {
351       if ( length($p->{'paycvv'} ) ) {
352         if ( cardtype($payinfo) eq 'American Express card' ) {
353           $p->{'paycvv'} =~ /^(\d{4})$/
354             or return { 'error' => "CVV2 (CID) for American Express cards is four digits." };
355           $paycvv = $1;
356         } else {
357           $p->{'paycvv'} =~ /^(\d{3})$/
358             or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
359           $paycvv = $1;
360         }
361       }
362     }
363   
364   #} else {
365   #  die "unknown payby $payby";
366   #}
367
368   my $error = $cust_main->realtime_bop( 'CC', $p->{'amount'},
369     'quiet'    => 1,
370     'payinfo'  => $payinfo,
371     'paydate'  => $p->{'year'}. '-'. $p->{'month'}. '-01',
372     'payname'  => $payname,
373     'paybatch' => $paybatch,
374     'paycvv'   => $paycvv,
375     map { $_ => $p->{$_} } qw( paystart_month paystart_year payissue payip
376                                address1 address2 city state zip )
377   );
378   return { 'error' => $error } if $error;
379
380   $cust_main->apply_payments;
381
382   if ( $p->{'save'} ) {
383     my $new = new FS::cust_main { $cust_main->hash };
384     $new->set( $_ => $p->{$_} )
385       foreach qw( payname paystart_month paystart_year payissue payip
386                   address1 address2 city state zip payinfo );
387     $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' );
388     $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
389     my $error = $new->replace($cust_main);
390     return { 'error' => $error } if $error;
391     $cust_main = $new;
392   }
393
394   return { 'error' => '' };
395
396 }
397
398 sub process_prepay {
399
400   my $p = shift;
401
402   my $session = _cache->get($p->{'session_id'})
403     or return { 'error' => "Can't resume session" }; #better error message
404
405   my %return;
406
407   my $custnum = $session->{'custnum'};
408
409   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
410     or return { 'error' => "unknown custnum $custnum" };
411
412   my( $amount, $seconds ) = ( 0, 0 );
413   my $error = $cust_main->recharge_prepay( $p->{'prepaid_cardnum'},
414                                            \$amount,
415                                            \$seconds
416                                          );
417
418   return { 'error' => $error } if $error;
419
420   return { 'error'    => '',
421            'amount'   => $amount,
422            'seconds'  => $seconds,
423            'duration' => duration_exact($seconds),
424          };
425
426 }
427
428 sub invoice {
429   my $p = shift;
430   my $session = _cache->get($p->{'session_id'})
431     or return { 'error' => "Can't resume session" }; #better error message
432
433   my $custnum = $session->{'custnum'};
434
435   my $invnum = $p->{'invnum'};
436
437   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
438                                           'custnum' => $custnum } )
439     or return { 'error' => "Can't find invnum" };
440
441   #my %return;
442
443   return { 'error'        => '',
444            'invnum'       => $invnum,
445            'invoice_text' => join('', $cust_bill->print_text ),
446            'invoice_html' => $cust_bill->print_html,
447          };
448
449 }
450
451 sub invoice_logo {
452   my $p = shift;
453
454   #sessioning for this?  how do we get the session id to the backend invoice
455   # template so it can add it to the link, blah
456
457   my $templatename = $p->{'templatename'};
458
459   #false laziness-ish w/view/cust_bill-logo.cgi
460
461   my $conf = new FS::Conf;
462   if ( $templatename =~ /^([^\.\/]*)$/ && $conf->exists("logo_$1.png") ) {
463     $templatename = "_$1";
464   } else {
465     $templatename = '';
466   }
467
468   my $filename = "logo$templatename.png";
469
470   return { 'error'        => '',
471            'logo'         => $conf->config_binary($filename),
472            'content_type' => 'image/png', #should allow gif, jpg too
473          };
474 }
475
476
477 sub list_invoices {
478   my $p = shift;
479   my $session = _cache->get($p->{'session_id'})
480     or return { 'error' => "Can't resume session" }; #better error message
481
482   my $custnum = $session->{'custnum'};
483
484   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
485     or return { 'error' => "unknown custnum $custnum" };
486
487   my @cust_bill = $cust_main->cust_bill;
488
489   return  { 'error'       => '',
490             'invoices'    =>  [ map { { 'invnum' => $_->invnum,
491                                         '_date'  => $_->_date,
492                                       }
493                                     } @cust_bill
494                               ]
495           };
496 }
497
498 sub cancel {
499   my $p = shift;
500   my $session = _cache->get($p->{'session_id'})
501     or return { 'error' => "Can't resume session" }; #better error message
502
503   my $custnum = $session->{'custnum'};
504
505   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
506     or return { 'error' => "unknown custnum $custnum" };
507
508   my @errors = $cust_main->cancel( 'quiet'=>1 );
509
510   my $error = scalar(@errors) ? join(' / ', @errors) : '';
511
512   return { 'error' => $error };
513
514 }
515
516 sub list_pkgs {
517   my $p = shift;
518
519   my($context, $session, $custnum) = _custoragent_session_custnum($p);
520   return { 'error' => $session } if $context eq 'error';
521
522   my $search = { 'custnum' => $custnum };
523   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
524   my $cust_main = qsearchs('cust_main', $search )
525     or return { 'error' => "unknown custnum $custnum" };
526
527   #return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] };
528
529   my $conf = new FS::Conf;
530
531   { 'svcnum'   => $session->{'svcnum'},
532     'custnum'  => $custnum,
533     'cust_pkg' => [ map {
534                           { $_->hash,
535                             $_->part_pkg->hash,
536                             part_svc =>
537                               [ map $_->hashref, $_->available_part_svc ],
538                             cust_svc => 
539                               [ map { my $ref = { $_->hash,
540                                                   label => [ $_->label ],
541                                                 };
542                                       $ref->{_password} = $_->svc_x->_password
543                                         if $context eq 'agent'
544                                         && $conf->exists('agent-showpasswords')
545                                         && $_->part_svc->svcdb eq 'svc_acct';
546                                       $ref;
547                                     } $_->cust_svc
548                               ],
549                           };
550                         } $cust_main->ncancelled_pkgs
551                   ],
552     'small_custview' =>
553       small_custview( $cust_main, $conf->config('countrydefault') ),
554   };
555
556 }
557
558 sub list_svcs {
559   my $p = shift;
560
561   use Data::Dumper;
562
563   my($context, $session, $custnum) = _custoragent_session_custnum($p);
564   return { 'error' => $session } if $context eq 'error';
565
566   my $search = { 'custnum' => $custnum };
567   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
568   my $cust_main = qsearchs('cust_main', $search )
569     or return { 'error' => "unknown custnum $custnum" };
570
571   my @cust_svc = ();
572   #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
573   foreach my $cust_pkg ( $cust_main->unsuspended_pkgs ) {
574     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
575   }
576   @cust_svc = grep { $_->part_svc->svcdb eq $p->{'svcdb'} } @cust_svc
577     if $p->{'svcdb'};
578
579   #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
580   #              @svc_x;
581
582   { 
583     #no#'svcnum'   => $session->{'svcnum'},
584     'custnum'  => $custnum,
585     'svcs'     => [ map { 
586                           my $svc_x = $_->svc_x;
587                           my($label, $value) = $_->label;
588
589                           { 'svcnum'   => $_->svcnum,
590                             'label'    => $label,
591                             'value'    => $value,
592                             'username' => $svc_x->username,
593                             'email'    => $svc_x->email,
594                             # more...
595                           };
596                         }
597                         @cust_svc
598                   ],
599   };
600
601 }
602
603 sub order_pkg {
604   my $p = shift;
605
606   my($context, $session, $custnum) = _custoragent_session_custnum($p);
607   return { 'error' => $session } if $context eq 'error';
608
609   my $search = { 'custnum' => $custnum };
610   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
611   my $cust_main = qsearchs('cust_main', $search )
612     or return { 'error' => "unknown custnum $custnum" };
613
614   #false laziness w/ClientAPI/Signup.pm
615
616   my $cust_pkg = new FS::cust_pkg ( {
617     'custnum' => $custnum,
618     'pkgpart' => $p->{'pkgpart'},
619   } );
620   my $error = $cust_pkg->check;
621   return { 'error' => $error } if $error;
622
623   my @svc = ();
624   unless ( $p->{'svcpart'} eq 'none' ) {
625
626     my $svcdb;
627     my $svcpart = '';
628     if ( $p->{'svcpart'} =~ /^(\d+)$/ ) {
629       $svcpart = $1;
630       my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
631       return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc;
632       $svcdb = $part_svc->svcdb;
633     } else {
634       $svcdb = 'svc_acct';
635     }
636     $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb);
637
638     my %fields = (
639       'svc_acct'     => [ qw( username _password sec_phrase popnum ) ],
640       'svc_domain'   => [ qw( domain ) ],
641       'svc_external' => [ qw( id title ) ],
642     );
643   
644     my $svc_x = "FS::$svcdb"->new( {
645       'svcpart'   => $svcpart,
646       map { $_ => $p->{$_} } @{$fields{$svcdb}}
647     } );
648     
649     if ( $svcdb eq 'svc_acct' ) {
650       my @acct_snarf;
651       my $snarfnum = 1;
652       while ( length($p->{"snarf_machine$snarfnum"}) ) {
653         my $acct_snarf = new FS::acct_snarf ( {
654           'machine'   => $p->{"snarf_machine$snarfnum"},
655           'protocol'  => $p->{"snarf_protocol$snarfnum"},
656           'username'  => $p->{"snarf_username$snarfnum"},
657           '_password' => $p->{"snarf_password$snarfnum"},
658         } );
659         $snarfnum++;
660         push @acct_snarf, $acct_snarf;
661       }
662       $svc_x->child_objects( \@acct_snarf );
663     }
664     
665     my $y = $svc_x->setdefault; # arguably should be in new method
666     return { 'error' => $y } if $y && !ref($y);
667   
668     $error = $svc_x->check;
669     return { 'error' => $error } if $error;
670
671     push @svc, $svc_x;
672
673   }
674
675   use Tie::RefHash;
676   tie my %hash, 'Tie::RefHash';
677   %hash = ( $cust_pkg => \@svc );
678   #msgcat
679   $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 );
680   return { 'error' => $error } if $error;
681
682   my $conf = new FS::Conf;
683   if ( $conf->exists('signup_server-realtime') ) {
684
685     my $old_balance = $cust_main->balance;
686
687     my $bill_error = $cust_main->bill;
688     $cust_main->apply_payments;
689     $cust_main->apply_credits;
690     $bill_error = $cust_main->collect('realtime' => 1);
691
692     if (    $cust_main->balance > $old_balance
693          && $cust_main->balance > 0
694          && $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ) {
695       #this makes sense.  credit is "un-doing" the invoice
696       $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ),
697                           'self-service decline' );
698       $cust_main->apply_credits( 'order' => 'newest' );
699
700       $cust_pkg->cancel('quiet'=>1);
701       return { 'error' => '_decline', 'bill_error' => $bill_error };
702     } else {
703       $cust_pkg->reexport;
704     }
705
706   } else {
707     $cust_pkg->reexport;
708   }
709
710   return { error => '', pkgnum => $cust_pkg->pkgnum };
711
712 }
713
714 sub cancel_pkg {
715   my $p = shift;
716   my $session = _cache->get($p->{'session_id'})
717     or return { 'error' => "Can't resume session" }; #better error message
718
719   my $custnum = $session->{'custnum'};
720
721   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
722     or return { 'error' => "unknown custnum $custnum" };
723
724   my $pkgnum = $p->{'pkgnum'};
725
726   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
727                                         'pkgnum'  => $pkgnum,   } )
728     or return { 'error' => "unknown pkgnum $pkgnum" };
729
730   my $error = $cust_pkg->cancel( 'quiet'=>1 );
731   return { 'error' => $error };
732
733 }
734
735 sub provision_acct {
736   my $p = shift;
737
738   return { 'error' => gettext('passwords_dont_match') }
739     if $p->{'_password'} ne $p->{'_password2'};
740   return { 'error' => gettext('empty_password') }
741     unless length($p->{'_password'});
742
743   _provision( 'FS::svc_acct',
744               [qw(username _password)],
745               [qw(username _password)],
746               $p,
747               @_
748             );
749 }
750
751 sub provision_external {
752   my $p = shift;
753   #_provision( 'FS::svc_external', [qw(id title)], [qw(id title)], $p, @_ );
754   _provision( 'FS::svc_external',
755               [],
756               [qw(id title)],
757               $p,
758               @_
759             );
760 }
761
762 sub _provision {
763   my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
764
765   my($context, $session, $custnum) = _custoragent_session_custnum($p);
766   return { 'error' => $session } if $context eq 'error';
767
768   my $search = { 'custnum' => $custnum };
769   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
770   my $cust_main = qsearchs('cust_main', $search )
771     or return { 'error' => "unknown custnum $custnum" };
772
773   my $pkgnum = $p->{'pkgnum'};
774
775   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
776                                         'pkgnum'  => $pkgnum,
777                                                                } )
778     or return { 'error' => "unknown pkgnum $pkgnum" };
779
780   my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
781     or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
782
783   my $svc_x = $class->new( {
784     'pkgnum'  => $p->{'pkgnum'},
785     'svcpart' => $p->{'svcpart'},
786     map { $_ => $p->{$_} } @$fields
787   } );
788   my $error = $svc_x->insert;
789   $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum })
790     unless $error;
791
792   return { 'svc'   => $part_svc->svc,
793            'error' => $error,
794            map { $_ => $svc_x->get($_) } @$return_fields
795          };
796
797 }
798
799 sub part_svc_info {
800   my $p = shift;
801
802   my($context, $session, $custnum) = _custoragent_session_custnum($p);
803   return { 'error' => $session } if $context eq 'error';
804
805   my $search = { 'custnum' => $custnum };
806   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
807   my $cust_main = qsearchs('cust_main', $search )
808     or return { 'error' => "unknown custnum $custnum" };
809
810   my $pkgnum = $p->{'pkgnum'};
811
812   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
813                                         'pkgnum'  => $pkgnum,
814                                                                } )
815     or return { 'error' => "unknown pkgnum $pkgnum" };
816
817   my $svcpart = $p->{'svcpart'};
818
819   my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart,
820                                       'svcpart' => $svcpart,           } )
821     or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" };
822   my $part_svc = $pkg_svc->part_svc;
823
824   my $conf = new FS::Conf;
825
826   return {
827     'svc'     => $part_svc->svc,
828     'svcdb'   => $part_svc->svcdb,
829     'pkgnum'  => $pkgnum,
830     'svcpart' => $svcpart,
831     'custnum' => $custnum,
832
833     'security_phrase' => 0, #XXX !
834     'svc_acct_pop'    => [], #XXX !
835     'popnum'          => '',
836     'init_popstate'   => '',
837     'popac'           => '',
838     'acstate'         => '',
839
840     'small_custview' =>
841       small_custview( $cust_main, $conf->config('countrydefault') ),
842
843   };
844
845 }
846
847 sub unprovision_svc {
848   my $p = shift;
849
850   my($context, $session, $custnum) = _custoragent_session_custnum($p);
851   return { 'error' => $session } if $context eq 'error';
852
853   my $search = { 'custnum' => $custnum };
854   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
855   my $cust_main = qsearchs('cust_main', $search )
856     or return { 'error' => "unknown custnum $custnum" };
857
858   my $svcnum = $p->{'svcnum'};
859
860   my $cust_svc = qsearchs('cust_svc', { 'svcnum'  => $svcnum, } )
861     or return { 'error' => "unknown svcnum $svcnum" };
862
863   return { 'error' => "Service $svcnum does not belong to customer $custnum" }
864     unless $cust_svc->cust_pkg->custnum == $custnum;
865
866   my $conf = new FS::Conf;
867
868   return { 'svc'   => $cust_svc->part_svc->svc,
869            'error' => $cust_svc->cancel,
870            'small_custview' =>
871              small_custview( $cust_main, $conf->config('countrydefault') ),
872          };
873
874 }
875
876 sub myaccount_passwd {
877   my $p = shift;
878   my($context, $session, $custnum) = _custoragent_session_custnum($p);
879   return { 'error' => $session } if $context eq 'error';
880
881   return { 'error' => "New passwords don't match." }
882     if $p->{'new_password'} ne $p->{'new_password2'};
883
884   return { 'error' => 'Enter new password' }
885     unless length($p->{'new_password'});
886
887   #my $search = { 'custnum' => $custnum };
888   #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
889   $custnum =~ /^(\d+)$/ or die "illegal custnum";
890   my $search = " AND custnum = $1";
891   $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
892
893   my $svc_acct = qsearchs( {
894     'table'     => 'svc_acct',
895     'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
896                    'LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
897                    'LEFT JOIN cust_main USING ( custnum ) ',
898     'hashref'   => { 'svcnum' => $p->{'svcnum'}, },
899     'extra_sql' => $search, #important
900   } )
901     or return { 'error' => "Service not found" };
902
903   $svc_acct->_password($p->{'new_password'});
904   my $error = $svc_acct->replace();
905
906   my($label, $value) = $svc_acct->cust_svc->label;
907
908   return { 'error' => $error,
909            'label' => $label,
910            'value' => $value,
911          };
912
913 }
914
915 #--
916
917 sub _custoragent_session_custnum {
918   my $p = shift;
919
920   my($context, $session, $custnum);
921   if ( $p->{'session_id'} ) {
922
923     $context = 'customer';
924     $session = _cache->get($p->{'session_id'})
925       or return ( 'error' => "Can't resume session" ); #better error message
926     $custnum = $session->{'custnum'};
927
928   } elsif ( $p->{'agent_session_id'} ) {
929
930     $context = 'agent';
931     my $agent_cache = new FS::ClientAPI_SessionCache( {
932       'namespace' => 'FS::ClientAPI::Agent',
933     } );
934     $session = $agent_cache->get($p->{'agent_session_id'})
935       or return ( 'error' => "Can't resume session" ); #better error message
936     $custnum = $p->{'custnum'};
937
938   } else {
939     return ( 'error' => "Can't resume session" ); #better error message
940   }
941
942   ($context, $session, $custnum);
943
944 }
945
946 1;
947