inappropriate cluckage
[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::UI::Web;
12 use FS::Conf;
13 use FS::Record qw(qsearch qsearchs);
14 use FS::Msgcat qw(gettext);
15 use FS::Misc qw(card_types);
16 use FS::ClientAPI_SessionCache;
17 use FS::svc_acct;
18 use FS::svc_domain;
19 use FS::svc_external;
20 use FS::part_svc;
21 use FS::cust_main;
22 use FS::cust_bill;
23 use FS::cust_main_county;
24 use FS::cust_pkg;
25 use HTML::Entities;
26
27 use vars qw( @cust_main_editable_fields );
28 @cust_main_editable_fields = qw(
29   first last company address1 address2 city
30     county state zip country daytime night fax
31   ship_first ship_last ship_company ship_address1 ship_address2 ship_city
32     ship_state ship_zip ship_country ship_daytime ship_night ship_fax
33   payby payinfo payname paystart_month paystart_year payissue payip
34 );
35
36 use subs qw(_provision);
37
38 sub _cache {
39   $cache ||= new FS::ClientAPI_SessionCache( {
40                'namespace' => 'FS::ClientAPI::MyAccount',
41              } );
42 }
43
44 #false laziness w/FS::ClientAPI::passwd::passwd
45 sub login {
46   my $p = shift;
47
48   my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
49     or return { error => 'Domain '. $p->{'domain'}. ' not found' };
50
51   my $svc_acct = qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
52                                          'domsvc'    => $svc_domain->svcnum, }
53                          );
54   return { error => 'User not found.' } unless $svc_acct;
55
56   my $conf = new FS::Conf;
57   my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
58   return { error => 'Only primary user may log in.' } 
59     if $conf->exists('selfservice_server-primary_only')
60        && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
61
62   return { error => 'Incorrect password.' }
63     unless $svc_acct->check_password($p->{'password'});
64
65   my $session = {
66     'svcnum' => $svc_acct->svcnum,
67   };
68
69   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
70   if ( $cust_pkg ) {
71     my $cust_main = $cust_pkg->cust_main;
72     $session->{'custnum'} = $cust_main->custnum;
73   }
74
75   my $session_id;
76   do {
77     $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
78   } until ( ! defined _cache->get($session_id) ); #just in case
79
80   _cache->set( $session_id, $session, '1 hour' );
81
82   return { 'error'      => '',
83            'session_id' => $session_id,
84          };
85 }
86
87 sub logout {
88   my $p = shift;
89   if ( $p->{'session_id'} ) {
90     _cache->remove($p->{'session_id'});
91     return { 'error' => '' };
92   } else {
93     return { 'error' => "Can't resume session" }; #better error message
94   }
95 }
96
97 sub customer_info {
98   my $p = shift;
99
100   my($context, $session, $custnum) = _custoragent_session_custnum($p);
101   return { 'error' => $session } if $context eq 'error';
102
103   my %return;
104   if ( $custnum ) { #customer record
105
106     my $search = { 'custnum' => $custnum };
107     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
108     my $cust_main = qsearchs('cust_main', $search )
109       or return { 'error' => "unknown custnum $custnum" };
110
111     $return{balance} = $cust_main->balance;
112
113     my @open = map {
114                      {
115                        invnum => $_->invnum,
116                        date   => time2str("%b %o, %Y", $_->_date),
117                        owed   => $_->owed,
118                      };
119                    } $cust_main->open_cust_bill;
120     $return{open_invoices} = \@open;
121
122     my $conf = new FS::Conf;
123     $return{small_custview} =
124       small_custview( $cust_main, $conf->config('countrydefault') );
125
126     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
127
128     for (@cust_main_editable_fields) {
129       $return{$_} = $cust_main->get($_);
130     }
131
132     if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
133       $return{payinfo} = $cust_main->paymask;
134       @return{'month', 'year'} = $cust_main->paydate_monthyear;
135     }
136
137     $return{'invoicing_list'} =
138       join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
139     $return{'postal_invoicing'} =
140       0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
141
142   } elsif ( $session->{'svcnum'} ) { #no customer record
143
144     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
145       or die "unknown svcnum";
146     $return{name} = $svc_acct->email;
147
148   } else {
149
150     return { 'error' => 'Expired session' }; #XXX redirect to login w/this err!
151
152   }
153
154   return { 'error'          => '',
155            'custnum'        => $custnum,
156            %return,
157          };
158
159 }
160
161 sub edit_info {
162   my $p = shift;
163   my $session = _cache->get($p->{'session_id'})
164     or return { 'error' => "Can't resume session" }; #better error message
165
166   my $custnum = $session->{'custnum'}
167     or return { 'error' => "no customer record" };
168
169   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
170     or return { 'error' => "unknown custnum $custnum" };
171
172   my $new = new FS::cust_main { $cust_main->hash };
173   $new->set( $_ => $p->{$_} )
174     foreach grep { exists $p->{$_} } @cust_main_editable_fields;
175
176   if ( $p->{'payby'} =~ /^(CARD|DCRD)$/ ) {
177     $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
178     if ( $new->payinfo eq $cust_main->paymask ) {
179       $new->payinfo($cust_main->payinfo);
180     } else {
181       $new->paycvv($p->{'paycvv'});
182     }
183   }
184
185   my @invoicing_list;
186   if ( exists $p->{'invoicing_list'} || exists $p->{'postal_invoicing'} ) {
187     #false laziness with httemplate/edit/process/cust_main.cgi
188     @invoicing_list = split( /\s*\,\s*/, $p->{'invoicing_list'} );
189     push @invoicing_list, 'POST' if $p->{'postal_invoicing'};
190   } else {
191     @invoicing_list = $cust_main->invoicing_list;
192   }
193
194   my $error = $new->replace($cust_main, \@invoicing_list);
195   return { 'error' => $error } if $error;
196   #$cust_main = $new;
197   
198   return { 'error' => '' };
199 }
200
201 sub payment_info {
202   my $p = shift;
203   my $session = _cache->get($p->{'session_id'})
204     or return { 'error' => "Can't resume session" }; #better error message
205
206   ##
207   #generic
208   ##
209
210   use vars qw($payment_info); #cache for performance
211   unless ( $payment_info ) {
212
213     my $conf = new FS::Conf;
214     my %states = map { $_->state => 1 }
215                    qsearch('cust_main_county', {
216                      'country' => $conf->config('countrydefault') || 'US'
217                    } );
218
219     $payment_info = {
220
221       #list all counties/states/countries
222       'cust_main_county' => 
223         [ map { $_->hashref } qsearch('cust_main_county', {}) ],
224
225       #shortcut for one-country folks
226       'states' =>
227         [ sort { $a cmp $b } keys %states ],
228
229       'card_types' => card_types(),
230
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     $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, $upbytes, $downbytes, $totalbytes ) = ( 0, 0, 0, 0, 0 );
384   my $error = $cust_main->recharge_prepay( $p->{'prepaid_cardnum'},
385                                            \$amount,
386                                            \$seconds,
387                                            \$upbytes,
388                                            \$downbytes,
389                                            \$totalbytes,
390                                          );
391
392   return { 'error' => $error } if $error;
393
394   return { 'error'     => '',
395            'amount'    => $amount,
396            'seconds'   => $seconds,
397            'duration'  => duration_exact($seconds),
398            'upbytes'   => $upbytes,
399            'upload'    => FS::UI::Web::bytecount_unexact($upbytes),
400            'downbytes' => $downbytes,
401            'download'  => FS::UI::Web::bytecount_unexact($downbytes),
402            'totalbytes'=> $totalbytes,
403            'totalload' => FS::UI::Web::bytecount_unexact($totalbytes),
404          };
405
406 }
407
408 sub invoice {
409   my $p = shift;
410   my $session = _cache->get($p->{'session_id'})
411     or return { 'error' => "Can't resume session" }; #better error message
412
413   my $custnum = $session->{'custnum'};
414
415   my $invnum = $p->{'invnum'};
416
417   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
418                                           'custnum' => $custnum } )
419     or return { 'error' => "Can't find invnum" };
420
421   #my %return;
422
423   return { 'error'        => '',
424            'invnum'       => $invnum,
425            'invoice_text' => join('', $cust_bill->print_text ),
426            'invoice_html' => $cust_bill->print_html,
427          };
428
429 }
430
431 sub invoice_logo {
432   my $p = shift;
433
434   #sessioning for this?  how do we get the session id to the backend invoice
435   # template so it can add it to the link, blah
436
437   my $templatename = $p->{'templatename'};
438
439   #false laziness-ish w/view/cust_bill-logo.cgi
440
441   my $conf = new FS::Conf;
442   if ( $templatename =~ /^([^\.\/]*)$/ && $conf->exists("logo_$1.png") ) {
443     $templatename = "_$1";
444   } else {
445     $templatename = '';
446   }
447
448   my $filename = "logo$templatename.png";
449
450   return { 'error'        => '',
451            'logo'         => $conf->config_binary($filename),
452            'content_type' => 'image/png', #should allow gif, jpg too
453          };
454 }
455
456
457 sub list_invoices {
458   my $p = shift;
459   my $session = _cache->get($p->{'session_id'})
460     or return { 'error' => "Can't resume session" }; #better error message
461
462   my $custnum = $session->{'custnum'};
463
464   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
465     or return { 'error' => "unknown custnum $custnum" };
466
467   my @cust_bill = $cust_main->cust_bill;
468
469   return  { 'error'       => '',
470             'invoices'    =>  [ map { { 'invnum' => $_->invnum,
471                                         '_date'  => $_->_date,
472                                       }
473                                     } @cust_bill
474                               ]
475           };
476 }
477
478 sub cancel {
479   my $p = shift;
480   my $session = _cache->get($p->{'session_id'})
481     or return { 'error' => "Can't resume session" }; #better error message
482
483   my $custnum = $session->{'custnum'};
484
485   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
486     or return { 'error' => "unknown custnum $custnum" };
487
488   my @errors = $cust_main->cancel( 'quiet'=>1 );
489
490   my $error = scalar(@errors) ? join(' / ', @errors) : '';
491
492   return { 'error' => $error };
493
494 }
495
496 sub list_pkgs {
497   my $p = shift;
498
499   my($context, $session, $custnum) = _custoragent_session_custnum($p);
500   return { 'error' => $session } if $context eq 'error';
501
502   my $search = { 'custnum' => $custnum };
503   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
504   my $cust_main = qsearchs('cust_main', $search )
505     or return { 'error' => "unknown custnum $custnum" };
506
507   #return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] };
508
509   my $conf = new FS::Conf;
510
511   { 'svcnum'   => $session->{'svcnum'},
512     'custnum'  => $custnum,
513     'cust_pkg' => [ map {
514                           { $_->hash,
515                             $_->part_pkg->hash,
516                             part_svc =>
517                               [ map $_->hashref, $_->available_part_svc ],
518                             cust_svc => 
519                               [ map { my $ref = { $_->hash,
520                                                   label => [ $_->label ],
521                                                 };
522                                       $ref->{_password} = $_->svc_x->_password
523                                         if $context eq 'agent'
524                                         && $conf->exists('agent-showpasswords')
525                                         && $_->part_svc->svcdb eq 'svc_acct';
526                                       $ref;
527                                     } $_->cust_svc
528                               ],
529                           };
530                         } $cust_main->ncancelled_pkgs
531                   ],
532     'small_custview' =>
533       small_custview( $cust_main, $conf->config('countrydefault') ),
534   };
535
536 }
537
538 sub list_svcs {
539   my $p = shift;
540
541   use Data::Dumper;
542
543   my($context, $session, $custnum) = _custoragent_session_custnum($p);
544   return { 'error' => $session } if $context eq 'error';
545
546   my $search = { 'custnum' => $custnum };
547   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
548   my $cust_main = qsearchs('cust_main', $search )
549     or return { 'error' => "unknown custnum $custnum" };
550
551   my @cust_svc = ();
552   #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
553   foreach my $cust_pkg ( $p->{'ncancelled'} 
554                          ? $cust_main->ncancelled_pkgs
555                          : $cust_main->unsuspended_pkgs ) {
556     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
557   }
558   @cust_svc = grep { $_->part_svc->svcdb eq $p->{'svcdb'} } @cust_svc
559     if $p->{'svcdb'};
560
561   #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
562   #              @svc_x;
563
564   { 
565     #no#'svcnum'   => $session->{'svcnum'},
566     'custnum'  => $custnum,
567     'svcs'     => [ map { 
568                           my $svc_x = $_->svc_x;
569                           my($label, $value) = $_->label;
570                           my $part_pkg = $svc_x->cust_svc->cust_pkg->part_pkg;
571
572                           { 'svcnum'    => $_->svcnum,
573                             'label'     => $label,
574                             'value'     => $value,
575                             'username'  => $svc_x->username,
576                             'email'     => $svc_x->email,
577                             'seconds'   => $svc_x->seconds,
578                             'upbytes'   => $svc_x->upbytes,
579                             'downbytes' => $svc_x->downbytes,
580                             'totalbytes'=> $svc_x->totalbytes,
581                             'recharge_amount' => $part_pkg->option('recharge_amount', 1),
582                             'recharge_seconds' => $part_pkg->option('recharge_seconds', 1),
583                             'recharge_upbytes' => $part_pkg->option('recharge_upbytes', 1),
584                             'recharge_downbytes' => $part_pkg->option('recharge_downbytes', 1),
585                             'recharge_totalbytes' => $part_pkg->option('recharge_totalbytes', 1),
586                             # more...
587                           };
588                         }
589                         @cust_svc
590                   ],
591   };
592
593 }
594
595 sub order_pkg {
596   my $p = shift;
597
598   my($context, $session, $custnum) = _custoragent_session_custnum($p);
599   return { 'error' => $session } if $context eq 'error';
600
601   my $search = { 'custnum' => $custnum };
602   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
603   my $cust_main = qsearchs('cust_main', $search )
604     or return { 'error' => "unknown custnum $custnum" };
605
606   #false laziness w/ClientAPI/Signup.pm
607
608   my $cust_pkg = new FS::cust_pkg ( {
609     'custnum' => $custnum,
610     'pkgpart' => $p->{'pkgpart'},
611   } );
612   my $error = $cust_pkg->check;
613   return { 'error' => $error } if $error;
614
615   my @svc = ();
616   unless ( $p->{'svcpart'} eq 'none' ) {
617
618     my $svcdb;
619     my $svcpart = '';
620     if ( $p->{'svcpart'} =~ /^(\d+)$/ ) {
621       $svcpart = $1;
622       my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
623       return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc;
624       $svcdb = $part_svc->svcdb;
625     } else {
626       $svcdb = 'svc_acct';
627     }
628     $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb);
629
630     my %fields = (
631       'svc_acct'     => [ qw( username _password sec_phrase popnum ) ],
632       'svc_domain'   => [ qw( domain ) ],
633       'svc_external' => [ qw( id title ) ],
634     );
635   
636     my $svc_x = "FS::$svcdb"->new( {
637       'svcpart'   => $svcpart,
638       map { $_ => $p->{$_} } @{$fields{$svcdb}}
639     } );
640     
641     if ( $svcdb eq 'svc_acct' ) {
642       my @acct_snarf;
643       my $snarfnum = 1;
644       while ( length($p->{"snarf_machine$snarfnum"}) ) {
645         my $acct_snarf = new FS::acct_snarf ( {
646           'machine'   => $p->{"snarf_machine$snarfnum"},
647           'protocol'  => $p->{"snarf_protocol$snarfnum"},
648           'username'  => $p->{"snarf_username$snarfnum"},
649           '_password' => $p->{"snarf_password$snarfnum"},
650         } );
651         $snarfnum++;
652         push @acct_snarf, $acct_snarf;
653       }
654       $svc_x->child_objects( \@acct_snarf );
655     }
656     
657     my $y = $svc_x->setdefault; # arguably should be in new method
658     return { 'error' => $y } if $y && !ref($y);
659   
660     $error = $svc_x->check;
661     return { 'error' => $error } if $error;
662
663     push @svc, $svc_x;
664
665   }
666
667   use Tie::RefHash;
668   tie my %hash, 'Tie::RefHash';
669   %hash = ( $cust_pkg => \@svc );
670   #msgcat
671   $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 );
672   return { 'error' => $error } if $error;
673
674   my $conf = new FS::Conf;
675   if ( $conf->exists('signup_server-realtime') ) {
676
677     my $old_balance = $cust_main->balance;
678
679     my $bill_error = $cust_main->bill;
680     $cust_main->apply_payments;
681     $cust_main->apply_credits;
682     $bill_error = $cust_main->collect('realtime' => 1);
683
684     if (    $cust_main->balance > $old_balance
685          && $cust_main->balance > 0
686          && $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ) {
687       #this makes sense.  credit is "un-doing" the invoice
688       $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ),
689                           'self-service decline' );
690       $cust_main->apply_credits( 'order' => 'newest' );
691
692       $cust_pkg->cancel('quiet'=>1);
693       return { 'error' => '_decline', 'bill_error' => $bill_error };
694     } else {
695       $cust_pkg->reexport;
696     }
697
698   } else {
699     $cust_pkg->reexport;
700   }
701
702   return { error => '', pkgnum => $cust_pkg->pkgnum };
703
704 }
705
706 sub order_recharge {
707   my $p = shift;
708
709   my($context, $session, $custnum) = _custoragent_session_custnum($p);
710   return { 'error' => $session } if $context eq 'error';
711
712   my $search = { 'custnum' => $custnum };
713   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
714   my $cust_main = qsearchs('cust_main', $search )
715     or return { 'error' => "unknown custnum $custnum" };
716
717   my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $p->{'svcnum'} } )
718     or return { 'error' => "unknown service " . $p->{'svcnum'} };
719
720   my $svc_x = $cust_svc->svc_x;
721   my $part_pkg = $cust_svc->cust_pkg->part_pkg;
722
723   my %vhash =
724     map { $_ =~ /^recharge_(.*)$/; $1, $part_pkg->option($_, 1) } 
725     qw ( recharge_seconds recharge_upbytes recharge_downbytes
726          recharge_totalbytes );
727   my $amount = $part_pkg->option('recharge_amount', 1); 
728   
729   my $old_balance = $cust_main->balance;
730
731   my ($l, $v, $d) = $cust_svc->label;  # blah
732   my $pkg = "Recharge $v"; 
733
734   my $bill_error = $cust_main->charge($amount, $pkg,
735      "time: $vhash{seconds}, up: $vhash{upbytes}," . 
736      "down: $vhash{downbytes}, total: $vhash{totalbytes}",
737      $part_pkg->taxclass); #meh
738
739   my $conf = new FS::Conf;
740   if ( $conf->exists('signup_server-realtime') && !$bill_error ) {
741
742     $bill_error = $cust_main->bill;
743
744     $cust_main->apply_payments;
745     $cust_main->apply_credits;
746     $bill_error = $cust_main->collect('realtime' => 1);
747
748     #false laziness with order_pkg
749     if (    $cust_main->balance > $old_balance
750          && $cust_main->balance > 0
751          && $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ) {
752       #this makes sense.  credit is "un-doing" the invoice
753       $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ),
754                           'self-service decline' );
755       $cust_main->apply_credits( 'order' => 'newest' );
756
757       return { 'error' => '_decline', 'bill_error' => encode_entities($bill_error) };
758     } else {
759       my $error = $svc_x->recharge (\%vhash);
760       return { 'error' => $error } if $error;
761     }
762
763   } else {  
764     my $error = $bill_error;
765     $error ||= $svc_x->recharge (\%vhash);
766     return { 'error' => $error } if $error;
767   }
768
769   return { error => '', svc => $cust_svc->part_svc->svc };
770
771 }
772
773 sub cancel_pkg {
774   my $p = shift;
775   my $session = _cache->get($p->{'session_id'})
776     or return { 'error' => "Can't resume session" }; #better error message
777
778   my $custnum = $session->{'custnum'};
779
780   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
781     or return { 'error' => "unknown custnum $custnum" };
782
783   my $pkgnum = $p->{'pkgnum'};
784
785   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
786                                         'pkgnum'  => $pkgnum,   } )
787     or return { 'error' => "unknown pkgnum $pkgnum" };
788
789   my $error = $cust_pkg->cancel( 'quiet'=>1 );
790   return { 'error' => $error };
791
792 }
793
794 sub provision_acct {
795   my $p = shift;
796
797   return { 'error' => gettext('passwords_dont_match') }
798     if $p->{'_password'} ne $p->{'_password2'};
799   return { 'error' => gettext('empty_password') }
800     unless length($p->{'_password'});
801
802   _provision( 'FS::svc_acct',
803               [qw(username _password)],
804               [qw(username _password)],
805               $p,
806               @_
807             );
808 }
809
810 sub provision_external {
811   my $p = shift;
812   #_provision( 'FS::svc_external', [qw(id title)], [qw(id title)], $p, @_ );
813   _provision( 'FS::svc_external',
814               [],
815               [qw(id title)],
816               $p,
817               @_
818             );
819 }
820
821 sub _provision {
822   my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
823
824   my($context, $session, $custnum) = _custoragent_session_custnum($p);
825   return { 'error' => $session } if $context eq 'error';
826
827   my $search = { 'custnum' => $custnum };
828   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
829   my $cust_main = qsearchs('cust_main', $search )
830     or return { 'error' => "unknown custnum $custnum" };
831
832   my $pkgnum = $p->{'pkgnum'};
833
834   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
835                                         'pkgnum'  => $pkgnum,
836                                                                } )
837     or return { 'error' => "unknown pkgnum $pkgnum" };
838
839   my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
840     or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
841
842   my $svc_x = $class->new( {
843     'pkgnum'  => $p->{'pkgnum'},
844     'svcpart' => $p->{'svcpart'},
845     map { $_ => $p->{$_} } @$fields
846   } );
847   my $error = $svc_x->insert;
848   $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum })
849     unless $error;
850
851   return { 'svc'   => $part_svc->svc,
852            'error' => $error,
853            map { $_ => $svc_x->get($_) } @$return_fields
854          };
855
856 }
857
858 sub part_svc_info {
859   my $p = shift;
860
861   my($context, $session, $custnum) = _custoragent_session_custnum($p);
862   return { 'error' => $session } if $context eq 'error';
863
864   my $search = { 'custnum' => $custnum };
865   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
866   my $cust_main = qsearchs('cust_main', $search )
867     or return { 'error' => "unknown custnum $custnum" };
868
869   my $pkgnum = $p->{'pkgnum'};
870
871   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
872                                         'pkgnum'  => $pkgnum,
873                                                                } )
874     or return { 'error' => "unknown pkgnum $pkgnum" };
875
876   my $svcpart = $p->{'svcpart'};
877
878   my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart,
879                                       'svcpart' => $svcpart,           } )
880     or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" };
881   my $part_svc = $pkg_svc->part_svc;
882
883   my $conf = new FS::Conf;
884
885   return {
886     'svc'     => $part_svc->svc,
887     'svcdb'   => $part_svc->svcdb,
888     'pkgnum'  => $pkgnum,
889     'svcpart' => $svcpart,
890     'custnum' => $custnum,
891
892     'security_phrase' => 0, #XXX !
893     'svc_acct_pop'    => [], #XXX !
894     'popnum'          => '',
895     'init_popstate'   => '',
896     'popac'           => '',
897     'acstate'         => '',
898
899     'small_custview' =>
900       small_custview( $cust_main, $conf->config('countrydefault') ),
901
902   };
903
904 }
905
906 sub unprovision_svc {
907   my $p = shift;
908
909   my($context, $session, $custnum) = _custoragent_session_custnum($p);
910   return { 'error' => $session } if $context eq 'error';
911
912   my $search = { 'custnum' => $custnum };
913   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
914   my $cust_main = qsearchs('cust_main', $search )
915     or return { 'error' => "unknown custnum $custnum" };
916
917   my $svcnum = $p->{'svcnum'};
918
919   my $cust_svc = qsearchs('cust_svc', { 'svcnum'  => $svcnum, } )
920     or return { 'error' => "unknown svcnum $svcnum" };
921
922   return { 'error' => "Service $svcnum does not belong to customer $custnum" }
923     unless $cust_svc->cust_pkg->custnum == $custnum;
924
925   my $conf = new FS::Conf;
926
927   return { 'svc'   => $cust_svc->part_svc->svc,
928            'error' => $cust_svc->cancel,
929            'small_custview' =>
930              small_custview( $cust_main, $conf->config('countrydefault') ),
931          };
932
933 }
934
935 sub myaccount_passwd {
936   my $p = shift;
937   my($context, $session, $custnum) = _custoragent_session_custnum($p);
938   return { 'error' => $session } if $context eq 'error';
939
940   return { 'error' => "New passwords don't match." }
941     if $p->{'new_password'} ne $p->{'new_password2'};
942
943   return { 'error' => 'Enter new password' }
944     unless length($p->{'new_password'});
945
946   #my $search = { 'custnum' => $custnum };
947   #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
948   $custnum =~ /^(\d+)$/ or die "illegal custnum";
949   my $search = " AND custnum = $1";
950   $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
951
952   my $svc_acct = qsearchs( {
953     'table'     => 'svc_acct',
954     'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
955                    'LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
956                    'LEFT JOIN cust_main USING ( custnum ) ',
957     'hashref'   => { 'svcnum' => $p->{'svcnum'}, },
958     'extra_sql' => $search, #important
959   } )
960     or return { 'error' => "Service not found" };
961
962   $svc_acct->_password($p->{'new_password'});
963   my $error = $svc_acct->replace();
964
965   my($label, $value) = $svc_acct->cust_svc->label;
966
967   return { 'error' => $error,
968            'label' => $label,
969            'value' => $value,
970          };
971
972 }
973
974 #--
975
976 sub _custoragent_session_custnum {
977   my $p = shift;
978
979   my($context, $session, $custnum);
980   if ( $p->{'session_id'} ) {
981
982     $context = 'customer';
983     $session = _cache->get($p->{'session_id'})
984       or return ( 'error' => "Can't resume session" ); #better error message
985     $custnum = $session->{'custnum'};
986
987   } elsif ( $p->{'agent_session_id'} ) {
988
989     $context = 'agent';
990     my $agent_cache = new FS::ClientAPI_SessionCache( {
991       'namespace' => 'FS::ClientAPI::Agent',
992     } );
993     $session = $agent_cache->get($p->{'agent_session_id'})
994       or return ( 'error' => "Can't resume session" ); #better error message
995     $custnum = $p->{'custnum'};
996
997   } else {
998     return ( 'error' => "Can't resume session" ); #better error message
999   }
1000
1001   ($context, $session, $custnum);
1002
1003 }
1004
1005 1;
1006