1 package FS::ClientAPI::MyAccount;
6 use Digest::MD5 qw(md5_hex);
8 use Business::CreditCard;
10 use FS::CGI qw(small_custview); #doh
12 use FS::UI::bytecount;
14 use FS::Record qw(qsearch qsearchs);
15 use FS::Msgcat qw(gettext);
16 use FS::Misc qw(card_types);
17 use FS::ClientAPI_SessionCache;
24 use FS::cust_main_county;
27 use FS::acct_rt_transaction;
30 #false laziness with FS::cust_main
32 eval "use Time::Local;";
33 die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
34 if $] < 5.006 && !defined($Time::Local::VERSION);
35 eval "use Time::Local qw(timelocal_nocheck);";
38 use vars qw( @cust_main_editable_fields );
39 @cust_main_editable_fields = qw(
40 first last company address1 address2 city
41 county state zip country daytime night fax
42 ship_first ship_last ship_company ship_address1 ship_address2 ship_city
43 ship_state ship_zip ship_country ship_daytime ship_night ship_fax
44 payby payinfo payname paystart_month paystart_year payissue payip
47 use subs qw(_provision);
50 $cache ||= new FS::ClientAPI_SessionCache( {
51 'namespace' => 'FS::ClientAPI::MyAccount',
55 #false laziness w/FS::ClientAPI::passwd::passwd
59 my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
60 or return { error => 'Domain '. $p->{'domain'}. ' not found' };
62 my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{'username'},
63 'domsvc' => $svc_domain->svcnum, }
65 return { error => 'User not found.' } unless $svc_acct;
67 my $conf = new FS::Conf;
68 my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
69 return { error => 'Only primary user may log in.' }
70 if $conf->exists('selfservice_server-primary_only')
71 && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
73 return { error => 'Incorrect password.' }
74 unless $svc_acct->check_password($p->{'password'});
77 'svcnum' => $svc_acct->svcnum,
80 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
82 my $cust_main = $cust_pkg->cust_main;
83 $session->{'custnum'} = $cust_main->custnum;
88 $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
89 } until ( ! defined _cache->get($session_id) ); #just in case
91 _cache->set( $session_id, $session, '1 hour' );
93 return { 'error' => '',
94 'session_id' => $session_id,
100 if ( $p->{'session_id'} ) {
101 _cache->remove($p->{'session_id'});
102 return { 'error' => '' };
104 return { 'error' => "Can't resume session" }; #better error message
111 my($context, $session, $custnum) = _custoragent_session_custnum($p);
112 return { 'error' => $session } if $context eq 'error';
115 if ( $custnum ) { #customer record
117 my $search = { 'custnum' => $custnum };
118 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
119 my $cust_main = qsearchs('cust_main', $search )
120 or return { 'error' => "unknown custnum $custnum" };
122 $return{balance} = $cust_main->balance;
124 $return{tickets} = [ ($cust_main->tickets) ];
128 invnum => $_->invnum,
129 date => time2str("%b %o, %Y", $_->_date),
132 } $cust_main->open_cust_bill;
133 $return{open_invoices} = \@open;
135 my $conf = new FS::Conf;
136 $return{small_custview} =
137 small_custview( $cust_main, $conf->config('countrydefault') );
139 $return{name} = $cust_main->first. ' '. $cust_main->get('last');
141 for (@cust_main_editable_fields) {
142 $return{$_} = $cust_main->get($_);
145 if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
146 $return{payinfo} = $cust_main->paymask;
147 @return{'month', 'year'} = $cust_main->paydate_monthyear;
150 $return{'invoicing_list'} =
151 join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
152 $return{'postal_invoicing'} =
153 0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
155 if (scalar($conf->config('support_packages'))) {
156 my @support_services = ();
157 foreach ($cust_main->support_services) {
158 my $seconds = $_->svc_x->seconds;
159 my $time_remaining = (($seconds < 0) ? '-' : '' ).
160 int(abs($seconds)/3600)."h".
161 sprintf("%02d",(abs($seconds)%3600)/60)."m";
162 my $cust_pkg = $_->cust_pkg;
165 $pkgnum = $cust_pkg->pkgnum if $cust_pkg;
166 $pkg = $cust_pkg->part_pkg->pkg if $cust_pkg;
167 push @support_services, { svcnum => $_->svcnum,
168 time => $time_remaining,
173 $return{support_services} = \@support_services;
176 } elsif ( $session->{'svcnum'} ) { #no customer record
178 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
179 or die "unknown svcnum";
180 $return{name} = $svc_acct->email;
184 return { 'error' => 'Expired session' }; #XXX redirect to login w/this err!
188 return { 'error' => '',
189 'custnum' => $custnum,
197 my $session = _cache->get($p->{'session_id'})
198 or return { 'error' => "Can't resume session" }; #better error message
200 my $custnum = $session->{'custnum'}
201 or return { 'error' => "no customer record" };
203 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
204 or return { 'error' => "unknown custnum $custnum" };
206 my $new = new FS::cust_main { $cust_main->hash };
207 $new->set( $_ => $p->{$_} )
208 foreach grep { exists $p->{$_} } @cust_main_editable_fields;
210 if ( $p->{'payby'} =~ /^(CARD|DCRD)$/ ) {
211 $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
212 if ( $new->payinfo eq $cust_main->paymask ) {
213 $new->payinfo($cust_main->payinfo);
215 $new->paycvv($p->{'paycvv'});
220 if ( exists $p->{'invoicing_list'} || exists $p->{'postal_invoicing'} ) {
221 #false laziness with httemplate/edit/process/cust_main.cgi
222 @invoicing_list = split( /\s*\,\s*/, $p->{'invoicing_list'} );
223 push @invoicing_list, 'POST' if $p->{'postal_invoicing'};
225 @invoicing_list = $cust_main->invoicing_list;
228 my $error = $new->replace($cust_main, \@invoicing_list);
229 return { 'error' => $error } if $error;
232 return { 'error' => '' };
237 my $session = _cache->get($p->{'session_id'})
238 or return { 'error' => "Can't resume session" }; #better error message
244 use vars qw($payment_info); #cache for performance
245 unless ( $payment_info ) {
247 my $conf = new FS::Conf;
248 my %states = map { $_->state => 1 }
249 qsearch('cust_main_county', {
250 'country' => $conf->config('countrydefault') || 'US'
255 #list all counties/states/countries
256 'cust_main_county' =>
257 [ map { $_->hashref } qsearch('cust_main_county', {}) ],
259 #shortcut for one-country folks
261 [ sort { $a cmp $b } keys %states ],
263 'card_types' => card_types(),
265 'paytypes' => [ @FS::cust_main::paytypes ],
267 'stateid_label' => FS::Msgcat::_gettext('stateid'),
268 'stateid_state_label' => FS::Msgcat::_gettext('stateid_state'),
270 'show_ss' => $conf->exists('show_ss'),
271 'show_stateid' => $conf->exists('show_stateid'),
272 'show_paystate' => $conf->exists('show_bankstate'),
281 my %return = %$payment_info;
283 my $custnum = $session->{'custnum'};
285 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
286 or return { 'error' => "unknown custnum $custnum" };
288 $return{balance} = $cust_main->balance;
290 $return{payname} = $cust_main->payname
291 || ( $cust_main->first. ' '. $cust_main->get('last') );
293 $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip);
295 $return{payby} = $cust_main->payby;
296 $return{stateid_state} = $cust_main->stateid_state;
298 if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
299 $return{card_type} = cardtype($cust_main->payinfo);
300 $return{payinfo} = $cust_main->payinfo;
302 @return{'month', 'year'} = $cust_main->paydate_monthyear;
306 if ( $cust_main->payby =~ /^(CHEK|DCHK)$/ ) {
307 my ($payinfo1, $payinfo2) = split '@', $cust_main->payinfo;
308 $return{payinfo1} = $payinfo1;
309 $return{payinfo2} = $payinfo2;
310 $return{paytype} = $cust_main->paytype;
311 $return{paystate} = $cust_main->paystate;
315 #doubleclick protection
317 $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
319 return { 'error' => '',
325 #some false laziness with httemplate/process/payment.cgi - look there for
326 #ACH and CVV support stuff
327 sub process_payment {
331 my $session = _cache->get($p->{'session_id'})
332 or return { 'error' => "Can't resume session" }; #better error message
336 my $custnum = $session->{'custnum'};
338 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
339 or return { 'error' => "unknown custnum $custnum" };
341 $p->{'payname'} =~ /^([\w \,\.\-\']+)$/
342 or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
345 $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
346 or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
349 $p->{'payby'} =~ /^([A-Z]{4})$/
350 or return { 'error' => "illegal_payby " . $p->{'payby'} };
355 if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
357 $p->{'payinfo1'} =~ /^(\d+)$/
358 or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
360 $p->{'payinfo2'} =~ /^(\d+)$/
361 or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
363 $payinfo = $payinfo1. '@'. $payinfo2;
365 } elsif ( $payby eq 'CARD' || $payby eq 'DCRD' ) {
367 $payinfo = $p->{'payinfo'};
369 $payinfo =~ /^(\d{13,16})$/
370 or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
373 or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
374 return { 'error' => gettext('unknown_card_type') }
375 if cardtype($payinfo) eq "Unknown";
377 if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) {
378 if ( cardtype($payinfo) eq 'American Express card' ) {
379 $p->{'paycvv'} =~ /^\s*(\d{4})\s*$/
380 or return { 'error' => "CVV2 (CID) for American Express cards is four digits." };
383 $p->{'paycvv'} =~ /^\s*(\d{3})\s*$/
384 or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
390 die "unknown payby $payby";
394 'CARD' => [ qw( paystart_month paystart_year payissue address1 address2 city state zip payip ) ],
395 'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ],
398 my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $p->{'amount'},
400 'payinfo' => $payinfo,
401 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01',
402 'payname' => $payname,
403 'paybatch' => $paybatch,
405 map { $_ => $p->{$_} } @{ $payby2fields{$payby} }
407 return { 'error' => $error } if $error;
409 $cust_main->apply_payments;
411 if ( $p->{'save'} ) {
412 my $new = new FS::cust_main { $cust_main->hash };
413 if ($payby eq 'CARD' || $payby eq 'DCRD') {
414 $new->set( $_ => $p->{$_} )
415 foreach qw( payname paystart_month paystart_year payissue payip
416 address1 address2 city state zip payinfo );
417 $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
418 } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') {
419 $new->set( $_ => $p->{$_} )
420 foreach qw( payname payip paytype paystate
421 stateid stateid_state );
422 $new->set( 'payinfo' => $payinfo );
423 $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' );
425 $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' );
426 my $error = $new->replace($cust_main);
427 return { 'error' => $error } if $error;
431 return { 'error' => '' };
439 my $session = _cache->get($p->{'session_id'})
440 or return { 'error' => "Can't resume session" }; #better error message
444 my $custnum = $session->{'custnum'};
446 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
447 or return { 'error' => "unknown custnum $custnum" };
449 my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = ( 0, 0, 0, 0, 0 );
450 my $error = $cust_main->recharge_prepay( $p->{'prepaid_cardnum'},
458 return { 'error' => $error } if $error;
460 return { 'error' => '',
462 'seconds' => $seconds,
463 'duration' => duration_exact($seconds),
464 'upbytes' => $upbytes,
465 'upload' => FS::UI::bytecount::bytecount_unexact($upbytes),
466 'downbytes' => $downbytes,
467 'download' => FS::UI::bytecount::bytecount_unexact($downbytes),
468 'totalbytes'=> $totalbytes,
469 'totalload' => FS::UI::bytecount::bytecount_unexact($totalbytes),
476 my $session = _cache->get($p->{'session_id'})
477 or return { 'error' => "Can't resume session" }; #better error message
479 my $custnum = $session->{'custnum'};
481 my $invnum = $p->{'invnum'};
483 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum,
484 'custnum' => $custnum } )
485 or return { 'error' => "Can't find invnum" };
489 return { 'error' => '',
491 'invoice_text' => join('', $cust_bill->print_text ),
492 'invoice_html' => $cust_bill->print_html,
500 #sessioning for this? how do we get the session id to the backend invoice
501 # template so it can add it to the link, blah
503 my $templatename = $p->{'templatename'};
505 #false laziness-ish w/view/cust_bill-logo.cgi
507 my $conf = new FS::Conf;
508 if ( $templatename =~ /^([^\.\/]*)$/ && $conf->exists("logo_$1.png") ) {
509 $templatename = "_$1";
514 my $filename = "logo$templatename.png";
516 return { 'error' => '',
517 'logo' => $conf->config_binary($filename),
518 'content_type' => 'image/png', #should allow gif, jpg too
525 my $session = _cache->get($p->{'session_id'})
526 or return { 'error' => "Can't resume session" }; #better error message
528 my $custnum = $session->{'custnum'};
530 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
531 or return { 'error' => "unknown custnum $custnum" };
533 my @cust_bill = $cust_main->cust_bill;
535 return { 'error' => '',
536 'invoices' => [ map { { 'invnum' => $_->invnum,
537 '_date' => $_->_date,
546 my $session = _cache->get($p->{'session_id'})
547 or return { 'error' => "Can't resume session" }; #better error message
549 my $custnum = $session->{'custnum'};
551 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
552 or return { 'error' => "unknown custnum $custnum" };
554 my @errors = $cust_main->cancel( 'quiet'=>1 );
556 my $error = scalar(@errors) ? join(' / ', @errors) : '';
558 return { 'error' => $error };
565 my($context, $session, $custnum) = _custoragent_session_custnum($p);
566 return { 'error' => $session } if $context eq 'error';
568 my $search = { 'custnum' => $custnum };
569 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
570 my $cust_main = qsearchs('cust_main', $search )
571 or return { 'error' => "unknown custnum $custnum" };
573 #return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] };
575 my $conf = new FS::Conf;
577 { 'svcnum' => $session->{'svcnum'},
578 'custnum' => $custnum,
579 'cust_pkg' => [ map {
583 [ map $_->hashref, $_->available_part_svc ],
585 [ map { my $ref = { $_->hash,
586 label => [ $_->label ],
588 $ref->{_password} = $_->svc_x->_password
589 if $context eq 'agent'
590 && $conf->exists('agent-showpasswords')
591 && $_->part_svc->svcdb eq 'svc_acct';
596 } $cust_main->ncancelled_pkgs
599 small_custview( $cust_main, $conf->config('countrydefault') ),
607 my($context, $session, $custnum) = _custoragent_session_custnum($p);
608 return { 'error' => $session } if $context eq 'error';
610 my $search = { 'custnum' => $custnum };
611 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
612 my $cust_main = qsearchs('cust_main', $search )
613 or return { 'error' => "unknown custnum $custnum" };
616 #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
617 foreach my $cust_pkg ( $p->{'ncancelled'}
618 ? $cust_main->ncancelled_pkgs
619 : $cust_main->unsuspended_pkgs ) {
620 push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
622 @cust_svc = grep { $_->part_svc->svcdb eq $p->{'svcdb'} } @cust_svc
625 #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
629 #no#'svcnum' => $session->{'svcnum'},
630 'custnum' => $custnum,
632 my $svc_x = $_->svc_x;
633 my($label, $value) = $_->label;
634 my $part_pkg = $svc_x->cust_svc->cust_pkg->part_pkg;
636 { 'svcnum' => $_->svcnum,
639 'username' => $svc_x->username,
640 'email' => $svc_x->email,
641 'seconds' => $svc_x->seconds,
642 'upbytes' => FS::UI::bytecount::display_bytecount($svc_x->upbytes),
643 'downbytes' => FS::UI::bytecount::display_bytecount($svc_x->downbytes),
644 'totalbytes'=> FS::UI::bytecount::display_bytecount($svc_x->totalbytes),
645 'recharge_amount' => $part_pkg->option('recharge_amount', 1),
646 'recharge_seconds' => $part_pkg->option('recharge_seconds', 1),
647 'recharge_upbytes' => FS::UI::bytecount::display_bytecount($part_pkg->option('recharge_upbytes', 1)),
648 'recharge_downbytes' => FS::UI::bytecount::display_bytecount($part_pkg->option('recharge_downbytes', 1)),
649 'recharge_totalbytes' => FS::UI::bytecount::display_bytecount($part_pkg->option('recharge_totalbytes', 1)),
659 sub _list_svc_usage {
660 my($svc_acct, $begin, $end) = @_;
662 foreach my $part_export (
663 map { qsearch ( 'part_export', { 'exporttype' => $_ } ) }
664 qw (sqlradius sqlradius_withdomain')
667 push @usage, @ { $part_export->usage_sessions($begin, $end, $svc_acct) };
673 _usage_details(\&_list_svc_usage, @_);
676 sub _list_support_usage {
677 my($svc_acct, $begin, $end) = @_;
679 foreach ( grep { $begin <= $_->_date && $_->_date <= $end }
680 qsearch('acct_rt_transaction', { 'svcnum' => $svc_acct->svcnum })
682 push @usage, { 'seconds' => $_->seconds,
683 'support' => $_->support,
684 '_date' => $_->_date,
685 'id' => $_->transaction_id,
686 'creator' => $_->creator,
687 'subject' => $_->subject,
688 'status' => $_->status,
689 'ticketid' => $_->ticketid,
695 sub list_support_usage {
696 _usage_details(\&_list_support_usage, @_);
700 my ($callback, $p) = (shift,shift);
702 my($context, $session, $custnum) = _custoragent_session_custnum($p);
703 return { 'error' => $session } if $context eq 'error';
705 my $search = { 'svcnum' => $p->{'svcnum'} };
706 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
707 my $svc_acct = qsearchs ( 'svc_acct', $search );
708 return { 'error' => 'No service selected in list_svc_usage' }
711 my $freq = $svc_acct->cust_svc->cust_pkg->part_pkg->freq;
712 my $start = $svc_acct->cust_svc->cust_pkg->setup;
713 #my $end = $svc_acct->cust_svc->cust_pkg->bill; # or time?
716 unless($p->{beginning}){
717 $p->{beginning} = $svc_acct->cust_svc->cust_pkg->last_bill;
721 my (@usage) = &$callback($svc_acct,$p->{beginning},$p->{ending});
723 #kinda false laziness with FS::cust_main::bill, but perhaps
724 #we should really change this bit to DateTime and DateTime::Duration
726 #change this bit to use Date::Manip? CAREFUL with timezones (see
727 # mailing list archive)
728 my ($nsec,$nmin,$nhour,$nmday,$nmon,$nyear) =
729 (localtime($p->{ending}) )[0,1,2,3,4,5];
730 my ($psec,$pmin,$phour,$pmday,$pmon,$pyear) =
731 (localtime($p->{beginning}) )[0,1,2,3,4,5];
733 if ( $freq =~ /^\d+$/ ) {
735 until ( $nmon < 12 ) { $nmon -= 12; $nyear++; }
737 until ( $pmon >= 0 ) { $pmon += 12; $pyear--; }
738 } elsif ( $freq =~ /^(\d+)w$/ ) {
740 $nmday += $weeks * 7;
741 $pmday -= $weeks * 7;
742 } elsif ( $freq =~ /^(\d+)d$/ ) {
746 } elsif ( $freq =~ /^(\d+)h$/ ) {
751 return { 'error' => "unparsable frequency: ". $freq };
754 my $previous = timelocal_nocheck($psec,$pmin,$phour,$pmday,$pmon,$pyear);
755 my $next = timelocal_nocheck($nsec,$nmin,$nhour,$nmday,$nmon,$nyear);
759 'svcnum' => $p->{svcnum},
760 'beginning' => $p->{beginning},
761 'ending' => $p->{ending},
762 'previous' => ($previous > $start) ? $previous : $start,
763 'next' => ($next < $end) ? $next : $end,
771 my($context, $session, $custnum) = _custoragent_session_custnum($p);
772 return { 'error' => $session } if $context eq 'error';
774 my $search = { 'custnum' => $custnum };
775 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
776 my $cust_main = qsearchs('cust_main', $search )
777 or return { 'error' => "unknown custnum $custnum" };
779 #false laziness w/ClientAPI/Signup.pm
781 my $cust_pkg = new FS::cust_pkg ( {
782 'custnum' => $custnum,
783 'pkgpart' => $p->{'pkgpart'},
785 my $error = $cust_pkg->check;
786 return { 'error' => $error } if $error;
789 unless ( $p->{'svcpart'} eq 'none' ) {
793 if ( $p->{'svcpart'} =~ /^(\d+)$/ ) {
795 my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
796 return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc;
797 $svcdb = $part_svc->svcdb;
801 $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb);
804 'svc_acct' => [ qw( username domsvc _password sec_phrase popnum ) ],
805 'svc_domain' => [ qw( domain ) ],
806 'svc_external' => [ qw( id title ) ],
809 my $svc_x = "FS::$svcdb"->new( {
810 'svcpart' => $svcpart,
811 map { $_ => $p->{$_} } @{$fields{$svcdb}}
814 if ( $svcdb eq 'svc_acct' ) {
817 while ( length($p->{"snarf_machine$snarfnum"}) ) {
818 my $acct_snarf = new FS::acct_snarf ( {
819 'machine' => $p->{"snarf_machine$snarfnum"},
820 'protocol' => $p->{"snarf_protocol$snarfnum"},
821 'username' => $p->{"snarf_username$snarfnum"},
822 '_password' => $p->{"snarf_password$snarfnum"},
825 push @acct_snarf, $acct_snarf;
827 $svc_x->child_objects( \@acct_snarf );
830 my $y = $svc_x->setdefault; # arguably should be in new method
831 return { 'error' => $y } if $y && !ref($y);
833 $error = $svc_x->check;
834 return { 'error' => $error } if $error;
841 tie my %hash, 'Tie::RefHash';
842 %hash = ( $cust_pkg => \@svc );
844 $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 );
845 return { 'error' => $error } if $error;
847 my $conf = new FS::Conf;
848 if ( $conf->exists('signup_server-realtime') ) {
850 my $bill_error = _do_bop_realtime( $cust_main );
853 $cust_pkg->cancel('quiet'=>1);
863 return { error => '', pkgnum => $cust_pkg->pkgnum };
870 my($context, $session, $custnum) = _custoragent_session_custnum($p);
871 return { 'error' => $session } if $context eq 'error';
873 my $search = { 'custnum' => $custnum };
874 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
875 my $cust_main = qsearchs('cust_main', $search )
876 or return { 'error' => "unknown custnum $custnum" };
878 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $p->{pkgnum} } )
879 or return { 'error' => "unknown package $p->{pkgnum}" };
882 my $error = FS::cust_pkg::order( $custnum,
888 my $conf = new FS::Conf;
889 if ( $conf->exists('signup_server-realtime') ) {
891 my $bill_error = _do_bop_realtime( $cust_main );
897 $newpkg[0]->reexport;
901 $newpkg[0]->reexport;
904 return { error => '', pkgnum => $cust_pkg->pkgnum };
911 my($context, $session, $custnum) = _custoragent_session_custnum($p);
912 return { 'error' => $session } if $context eq 'error';
914 my $search = { 'custnum' => $custnum };
915 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
916 my $cust_main = qsearchs('cust_main', $search )
917 or return { 'error' => "unknown custnum $custnum" };
919 my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $p->{'svcnum'} } )
920 or return { 'error' => "unknown service " . $p->{'svcnum'} };
922 my $svc_x = $cust_svc->svc_x;
923 my $part_pkg = $cust_svc->cust_pkg->part_pkg;
926 map { $_ =~ /^recharge_(.*)$/; $1, $part_pkg->option($_, 1) }
927 qw ( recharge_seconds recharge_upbytes recharge_downbytes
928 recharge_totalbytes );
929 my $amount = $part_pkg->option('recharge_amount', 1);
931 my ($l, $v, $d) = $cust_svc->label; # blah
932 my $pkg = "Recharge $v";
934 my $bill_error = $cust_main->charge($amount, $pkg,
935 "time: $vhash{seconds}, up: $vhash{upbytes}," .
936 "down: $vhash{downbytes}, total: $vhash{totalbytes}",
937 $part_pkg->taxclass); #meh
939 my $conf = new FS::Conf;
940 if ( $conf->exists('signup_server-realtime') && !$bill_error ) {
942 $bill_error = _do_bop_realtime( $cust_main );
947 my $error = $svc_x->recharge (\%vhash);
948 return { 'error' => $error } if $error;
952 my $error = $bill_error;
953 $error ||= $svc_x->recharge (\%vhash);
954 return { 'error' => $error } if $error;
957 return { error => '', svc => $cust_svc->part_svc->svc };
961 sub _do_bop_realtime {
962 my ($cust_main) = @_;
964 my $old_balance = $cust_main->balance;
966 my $bill_error = $cust_main->bill
967 || $cust_main->apply_payments_and_credits
968 || $cust_main->collect('realtime' => 1);
970 if ( $cust_main->balance > $old_balance
971 && $cust_main->balance > 0
972 && $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ) {
973 #this makes sense. credit is "un-doing" the invoice
974 $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ),
975 'self-service decline' );
976 $cust_main->apply_credits( 'order' => 'newest' );
978 return { 'error' => '_decline', 'bill_error' => $bill_error };
986 my $session = _cache->get($p->{'session_id'})
987 or return { 'error' => "Can't resume session" }; #better error message
989 my $custnum = $session->{'custnum'};
991 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
992 or return { 'error' => "unknown custnum $custnum" };
994 my $pkgnum = $p->{'pkgnum'};
996 my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
997 'pkgnum' => $pkgnum, } )
998 or return { 'error' => "unknown pkgnum $pkgnum" };
1000 my $error = $cust_pkg->cancel( 'quiet'=>1 );
1001 return { 'error' => $error };
1005 sub provision_acct {
1008 return { 'error' => gettext('passwords_dont_match') }
1009 if $p->{'_password'} ne $p->{'_password2'};
1010 return { 'error' => gettext('empty_password') }
1011 unless length($p->{'_password'});
1013 _provision( 'FS::svc_acct',
1014 [qw(username _password)],
1015 [qw(username _password)],
1021 sub provision_external {
1023 #_provision( 'FS::svc_external', [qw(id title)], [qw(id title)], $p, @_ );
1024 _provision( 'FS::svc_external',
1033 my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
1035 my($context, $session, $custnum) = _custoragent_session_custnum($p);
1036 return { 'error' => $session } if $context eq 'error';
1038 my $search = { 'custnum' => $custnum };
1039 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1040 my $cust_main = qsearchs('cust_main', $search )
1041 or return { 'error' => "unknown custnum $custnum" };
1043 my $pkgnum = $p->{'pkgnum'};
1045 my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
1046 'pkgnum' => $pkgnum,
1048 or return { 'error' => "unknown pkgnum $pkgnum" };
1050 my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
1051 or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
1053 my $svc_x = $class->new( {
1054 'pkgnum' => $p->{'pkgnum'},
1055 'svcpart' => $p->{'svcpart'},
1056 map { $_ => $p->{$_} } @$fields
1058 my $error = $svc_x->insert;
1059 $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum })
1062 return { 'svc' => $part_svc->svc,
1064 map { $_ => $svc_x->get($_) } @$return_fields
1072 my($context, $session, $custnum) = _custoragent_session_custnum($p);
1073 return { 'error' => $session } if $context eq 'error';
1075 my $search = { 'custnum' => $custnum };
1076 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1077 my $cust_main = qsearchs('cust_main', $search )
1078 or return { 'error' => "unknown custnum $custnum" };
1080 my $pkgnum = $p->{'pkgnum'};
1082 my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
1083 'pkgnum' => $pkgnum,
1085 or return { 'error' => "unknown pkgnum $pkgnum" };
1087 my $svcpart = $p->{'svcpart'};
1089 my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart,
1090 'svcpart' => $svcpart, } )
1091 or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" };
1092 my $part_svc = $pkg_svc->part_svc;
1094 my $conf = new FS::Conf;
1097 'svc' => $part_svc->svc,
1098 'svcdb' => $part_svc->svcdb,
1099 'pkgnum' => $pkgnum,
1100 'svcpart' => $svcpart,
1101 'custnum' => $custnum,
1103 'security_phrase' => 0, #XXX !
1104 'svc_acct_pop' => [], #XXX !
1106 'init_popstate' => '',
1111 small_custview( $cust_main, $conf->config('countrydefault') ),
1117 sub unprovision_svc {
1120 my($context, $session, $custnum) = _custoragent_session_custnum($p);
1121 return { 'error' => $session } if $context eq 'error';
1123 my $search = { 'custnum' => $custnum };
1124 $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1125 my $cust_main = qsearchs('cust_main', $search )
1126 or return { 'error' => "unknown custnum $custnum" };
1128 my $svcnum = $p->{'svcnum'};
1130 my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svcnum, } )
1131 or return { 'error' => "unknown svcnum $svcnum" };
1133 return { 'error' => "Service $svcnum does not belong to customer $custnum" }
1134 unless $cust_svc->cust_pkg->custnum == $custnum;
1136 my $conf = new FS::Conf;
1138 return { 'svc' => $cust_svc->part_svc->svc,
1139 'error' => $cust_svc->cancel,
1141 small_custview( $cust_main, $conf->config('countrydefault') ),
1146 sub myaccount_passwd {
1148 my($context, $session, $custnum) = _custoragent_session_custnum($p);
1149 return { 'error' => $session } if $context eq 'error';
1151 return { 'error' => "New passwords don't match." }
1152 if $p->{'new_password'} ne $p->{'new_password2'};
1154 return { 'error' => 'Enter new password' }
1155 unless length($p->{'new_password'});
1157 #my $search = { 'custnum' => $custnum };
1158 #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1159 $custnum =~ /^(\d+)$/ or die "illegal custnum";
1160 my $search = " AND custnum = $1";
1161 $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
1163 my $svc_acct = qsearchs( {
1164 'table' => 'svc_acct',
1165 'addl_from' => 'LEFT JOIN cust_svc USING ( svcnum ) '.
1166 'LEFT JOIN cust_pkg USING ( pkgnum ) '.
1167 'LEFT JOIN cust_main USING ( custnum ) ',
1168 'hashref' => { 'svcnum' => $p->{'svcnum'}, },
1169 'extra_sql' => $search, #important
1171 or return { 'error' => "Service not found" };
1173 $svc_acct->_password($p->{'new_password'});
1174 my $error = $svc_acct->replace();
1176 my($label, $value) = $svc_acct->cust_svc->label;
1178 return { 'error' => $error,
1187 sub _custoragent_session_custnum {
1190 my($context, $session, $custnum);
1191 if ( $p->{'session_id'} ) {
1193 $context = 'customer';
1194 $session = _cache->get($p->{'session_id'})
1195 or return ( 'error' => "Can't resume session" ); #better error message
1196 $custnum = $session->{'custnum'};
1198 } elsif ( $p->{'agent_session_id'} ) {
1201 my $agent_cache = new FS::ClientAPI_SessionCache( {
1202 'namespace' => 'FS::ClientAPI::Agent',
1204 $session = $agent_cache->get($p->{'agent_session_id'})
1205 or return ( 'error' => "Can't resume session" ); #better error message
1206 $custnum = $p->{'custnum'};
1209 return ( 'error' => "Can't resume session" ); #better error message
1212 ($context, $session, $custnum);