diff options
Diffstat (limited to 'FS/FS/ClientAPI')
-rw-r--r-- | FS/FS/ClientAPI/Agent.pm | 125 | ||||
-rw-r--r-- | FS/FS/ClientAPI/Bulk.pm | 384 | ||||
-rw-r--r-- | FS/FS/ClientAPI/MasonComponent.pm | 131 | ||||
-rw-r--r-- | FS/FS/ClientAPI/MyAccount.pm | 1736 | ||||
-rw-r--r-- | FS/FS/ClientAPI/PrepaidPhone.pm | 253 | ||||
-rw-r--r-- | FS/FS/ClientAPI/SGNG.pm | 277 | ||||
-rw-r--r-- | FS/FS/ClientAPI/Signup.pm | 796 | ||||
-rw-r--r-- | FS/FS/ClientAPI/passwd.pm | 46 |
8 files changed, 3748 insertions, 0 deletions
diff --git a/FS/FS/ClientAPI/Agent.pm b/FS/FS/ClientAPI/Agent.pm new file mode 100644 index 0000000..daede59 --- /dev/null +++ b/FS/FS/ClientAPI/Agent.pm @@ -0,0 +1,125 @@ +package FS::ClientAPI::Agent; + +#some false laziness w/MyAccount + +use strict; +use vars qw($cache); +use subs qw(_cache); +use Digest::MD5 qw(md5_hex); +use FS::Record qw(qsearchs); # qsearch dbdef dbh); +use FS::ClientAPI_SessionCache; +use FS::agent; +use FS::cust_main qw(smart_search); + +sub _cache { + $cache ||= new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::Agent', + } ); +} + +sub agent_login { + my $p = shift; + + #don't allow a blank login to first unconfigured agent with no user/pass + return { error => 'Must specify your reseller username and password.' } + unless length($p->{'username'}) && length($p->{'password'}); + + my $agent = qsearchs( 'agent', { + 'username' => $p->{'username'}, + '_password' => $p->{'password'}, + } ); + + unless ( $agent ) { return { error => 'Incorrect password.' } } + + my $session = { + 'agentnum' => $agent->agentnum, + 'agent' => $agent->agent, + }; + + my $session_id; + do { + $session_id = md5_hex(md5_hex(time(). {}. rand(). $$)) + } until ( ! defined _cache->get($session_id) ); #just in case + + _cache->set( $session_id, $session, '1 hour' ); + + { 'error' => '', + 'session_id' => $session_id, + }; +} + +sub agent_logout { + my $p = shift; + if ( $p->{'session_id'} ) { + _cache->remove($p->{'session_id'}); + return { 'error' => '' }; + } else { + return { 'error' => "Can't resume session" }; #better error message + } +} + +sub agent_info { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + #my %return; + + my $agentnum = $session->{'agentnum'}; + + my $agent = qsearchs( 'agent', { 'agentnum' => $agentnum } ) + or return { 'error' => "unknown agentnum $agentnum" }; + + { 'error' => '', + 'agentnum' => $agentnum, + 'agent' => $agent->agent, + 'num_prospect' => $agent->num_prospect_cust_main, + 'num_active' => $agent->num_active_cust_main, + 'num_susp' => $agent->num_susp_cust_main, + 'num_cancel' => $agent->num_cancel_cust_main, + #%return, + }; + +} + +sub agent_list_customers { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + #my %return; + + my $agentnum = $session->{'agentnum'}; + + my $agent = qsearchs( 'agent', { 'agentnum' => $agentnum } ) + or return { 'error' => "unknown agentnum $agentnum" }; + + my @cust_main = smart_search( 'search' => $p->{'search'}, + 'agentnum' => $agentnum, + ); + + #aggregate searches + push @cust_main, + map $agent->$_(), map $_.'_cust_main', + grep $p->{$_}, qw( prospect active susp cancel ); + + #eliminate dups? + my %saw = (); + @cust_main = grep { !$saw{$_->custnum}++ } @cust_main; + + { customers => [ map { + my $cust_main = $_; + my $hashref = $cust_main->hashref; + $hashref->{$_} = $cust_main->$_() + foreach qw(name status statuscolor); + delete $hashref->{$_} foreach qw( payinfo paycvv ); + $hashref; + } @cust_main + ], + } + +} + +1; diff --git a/FS/FS/ClientAPI/Bulk.pm b/FS/FS/ClientAPI/Bulk.pm new file mode 100644 index 0000000..ec617df --- /dev/null +++ b/FS/FS/ClientAPI/Bulk.pm @@ -0,0 +1,384 @@ +package FS::ClientAPI::Bulk; + +use strict; + +use vars qw( $DEBUG $cache ); +use Date::Parse; +use FS::Record qw( qsearchs ); +use FS::Conf; +use FS::ClientAPI_SessionCache; +use FS::cust_main; +use FS::cust_pkg; +use FS::cust_svc; +use FS::svc_acct; +use FS::svc_external; +use FS::cust_recon; +use Data::Dumper; + +$DEBUG = 1; + +sub _cache { + $cache ||= new FS::ClientAPI_SessionCache ( { + 'namespace' => 'FS::ClientAPI::Agent', #yes, share session_ids + } ); +} + +sub _izoom_ftp_row_fixup { + my $hash = shift; + + my @addr_fields = qw( address1 address2 city state zip ); + my @fields = ( qw( agent_custid username _password first last ), + @addr_fields, + map { "ship_$_" } @addr_fields ); + + $hash->{$_} =~ s/[&\/\*'"]/_/g foreach @fields; + + #$hash->{action} = '' if $hash->{action} eq 'R'; #unsupported for ftp + + $hash->{refnum} = 1; #ahem + $hash->{country} = 'US'; + $hash->{ship_country} = 'US'; + $hash->{payby} = 'LECB'; + $hash->{payinfo} = $hash->{daytime}; + $hash->{ship_fax} = '' if ( !$hash->{sms} || $hash->{sms} eq 'F' ); + + my $has_ship = + grep { $hash->{"ship_$_"} && + (! $hash->{$_} || $hash->{"ship_$_"} ne $hash->{$_} ) + } + ( @addr_fields, 'fax' ); + + if ( $has_ship ) { + foreach ( @addr_fields, qw( first last ) ) { + $hash->{"ship_$_"} = $hash->{$_} unless $hash->{"ship_$_"}; + } + } + + delete $hash->{sms}; + + ''; + +}; + +sub _izoom_ftp_result { + my ($hash, $error) = @_; + my $cust_main = + qsearchs( 'cust_main', { 'agent_custid' => $hash->{agent_custid}, + 'agentnum' => $hash->{agentnum} + } + ); + + my $custnum = $cust_main ? $cust_main->custnum : ''; + my @response = ( $hash->{action}, $hash->{agent_custid}, $custnum ); + + if ( $error ) { + push @response, ( 'ERROR', $error ); + } else { + push @response, ( 'OK', 'OK' ); + } + + join( ',', @response ); + +} + +sub _izoom_ftp_badaction { + "Invalid action: $_[0] record: @_ "; +} + +sub _izoom_soap_row_fixup { _izoom_ftp_row_fixup(@_) }; + +sub _izoom_soap_result { + my ($hash, $error) = @_; + + if ( $hash->{action} eq 'R' ) { + if ( $error ) { + return "Please check errors:\n $error"; # odd extra space + } else { + return join(' ', "Everything ok.", $hash->{pkg}, $hash->{adjourn} ); + } + } + + my $pkg = $hash->{pkg} || $hash->{saved_pkg} || ''; + if ( $error ) { + return join(' ', $hash->{agent_custid}, $error ); + } else { + return join(' ', $hash->{agent_custid}, $pkg, $hash->{adjourn} ); + } + +} + +sub _izoom_soap_badaction { + "Unknown action '$_[13]' "; +} + +my %format = ( + 'izoom-ftp' => { + 'fields' => [ qw ( action agent_custid username _password + daytime ship_fax sms first last + address1 address2 city state zip + pkg adjourn ship_address1 ship_address2 + ship_city ship_state ship_zip ) ], + 'fixup' => sub { _izoom_ftp_row_fixup(@_) }, + 'result' => sub { _izoom_ftp_result(@_) }, + 'action' => sub { _izoom_ftp_badaction(@_) }, + }, + 'izoom-soap' => { + 'fields' => [ qw ( agent_custid username _password + daytime first last address1 address2 + city state zip pkg action adjourn + ship_fax sms ship_address1 ship_address2 + ship_city ship_state ship_zip ) ], + 'fixup' => sub { _izoom_soap_row_fixup(@_) }, + 'result' => sub { _izoom_soap_result(@_) }, + 'action' => sub { _izoom_soap_badaction(@_) }, + }, +); + +sub processrow { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $conf = new FS::Conf; + my $format = $conf->config('selfservice-bulk_format', $session->{agentnum}) + || 'izoom-soap'; + my ( @row ) = @{ $p->{row} }; + + warn "processrow called with '". join("' '", @row). "'\n" if $DEBUG; + + return { 'error' => "unknown format: $format" } + unless exists $format{$format}; + + return { 'error' => "Invalid record record length: ". scalar(@row). + "record: @row " #sic + } + unless scalar(@row) == scalar(@{$format{$format}{fields}}); + + my %hash = ( 'agentnum' => $session->{agentnum} ); + my $error; + + foreach my $field ( @{ $format{ $format }{ fields } } ) { + $hash{$field} = shift @row; + } + + $error ||= &{ $format{ $format }{ fixup } }( \%hash ); + + # put in the fixup routine? + if ( 'R' eq $hash{action} ) { + warn "processing reconciliation\n" if $DEBUG; + $error ||= process_recon($hash{agentnum}, $hash{agent_custid}); + } elsif ( 'P' eq $hash{action} ) { + # do nothing + } elsif( 'D' eq $hash{action} ) { + $hash{promo_pkg} = 'disk-1-'. $session->{agent}; + } elsif ( 'S' eq $hash{action} ) { + $hash{promo_pkg} = 'disk-2-'. $session->{agent}; + $hash{saved_pkg} = $hash{pkg}; + $hash{pkg} = ''; + } else { + $error ||= &{ $format{ $format }{ action } }( @row ); + } + + warn "processing provision\n" if ($DEBUG && !$error && $hash{action} ne 'R'); + $error ||= provision( %hash ) unless $hash{action} eq 'R'; + + my $result = &{ $format{ $format }{ result } }( \%hash, $error ); + + warn "processrow returning '". join("' '", $result, $error). "'\n" + if $DEBUG; + + return { 'error' => $error, 'message' => $result }; + +} + +sub provision { + my %args = ( @_ ); + + delete $args{action}; + + my $cust_main = + qsearchs( 'cust_main', + { map { $_ => $args{$_} } qw ( agent_custid agentnum ) }, + ); + + unless ( $cust_main ) { + $cust_main = new FS::cust_main { %args }; + my $error = $cust_main->insert; + return $error if $error; + } + + my @pkgs = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs; + if ( scalar(@pkgs) > 1 ) { + return "Invalid account, should not be more then one active package ". #sic + "but found: ". scalar(@pkgs). " packages."; + } + + my $part_pkg = qsearchs( 'part_pkg', { 'pkg' => $args{pkg} } ) + or return "Unknown pkgpart: $args{pkg}" + if $args{pkg}; + + + my $create_package = $args{pkg}; + if ( scalar(@pkgs) && $create_package ) { + my $pkg = pop(@pkgs); + + if ( $part_pkg->pkgpart != $pkg->pkgpart ) { + my @cust_bill_pkg = $pkg->cust_bill_pkg(); + if ( 1 == scalar(@cust_bill_pkg) ) { + my $cbp= pop(@cust_bill_pkg); + my $cust_bill = $cbp->cust_bill; + $cust_bill->delete(); #really? wouldn't a credit be better? + } + $pkg->cancel(); + } else { + $create_package = ''; + $pkg->setfield('adjourn', str2time($args{adjourn})); + my $error = $pkg->replace(); + return $error if $error; + } + } + + if ( $create_package ) { + my $cust_pkg = new FS::cust_pkg ( { + 'pkgpart' => $part_pkg->pkgpart, + 'adjourn' => str2time( $args{adjourn} ), + } ); + + my $svcpart = $part_pkg->svcpart('svc_acct'); + + my $svc_acct = new FS::svc_acct ( { + 'svcpart' => $svcpart, + 'username' => $args{username}, + '_password' => $args{_password}, + } ); + + my $error = $cust_main->order_pkg( cust_pkg => $cust_pkg, + svcs => [ $svc_acct ], + ); + return $error if $error; + } + + if ( $args{promo_pkg} ) { + my $part_pkg = + qsearchs( 'part_pkg', { 'promo_code' => $args{promo_pkg} } ) + or return "unknown pkgpart: $args{promo_pkg}"; + + my $svcpart = $part_pkg->svcpart('svc_external') + or return "unknown svcpart: svc_external"; + + my $cust_pkg = new FS::cust_pkg ( { + 'svcpart' => $svcpart, + 'pkgpart' => $part_pkg->pkgpart, + } ); + + my $svc_ext = new FS::svc_external ( { 'svcpart' => $svcpart } ); + + my $ticket_subject = 'Send setup disk to customer '. $cust_main->custnum; + my $error = $cust_main->order_pkg ( cust_pkg => $cust_pkg, + svcs => [ $svc_ext ], + noexport => 1, + ticket_subject => $ticket_subject, + ticket_queue => "disk-$args{agentnum}", + ); + return $error if $error; + } + + my $error = $cust_main->bill(); + return $error if $error; +} + +sub process_recon { + my ( $agentnum, $id ) = @_; + my @recs = split /;/, $id; + my $err = ''; + foreach my $rec ( @recs ) { + my @record = split /,/, $rec; + my $result = process_recon_record(@record, $agentnum); + $err .= "$result\n" if $result; + } + return $err; +} + +sub process_recon_record { + my ( $agent_custid, $username, $_password, $daytime, $first, $last, $address1, $address2, $city, $state, $zip, $pkg, $adjourn, $agentnum) = @_; + + warn "process_recon_record called with '". join("','", @_). "'\n" if $DEBUG; + + my ($cust_pkg, $package); + + my $cust_main = + qsearchs( 'cust_main', + { 'agent_custid' => $agent_custid, 'agentnum' => $agentnum }, + ); + + my $comments = ''; + if ( $cust_main ) { + my @cust_pkg = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs; + if ( scalar(@cust_pkg) == 1) { + $cust_pkg = pop(@cust_pkg); + $package = $cust_pkg->part_pkg->pkg; + $comments = "$agent_custid wrong package, expected: $pkg found: $package" + if ( $pkg ne $package ); + } else { + $comments = "invalid account, should be one active package but found: ". + scalar(@cust_pkg). " packages."; + } + } else { + $comments = + "Customer not found agent_custid=$agent_custid, agentnum=$agentnum"; + } + + my $cust_recon = new FS::cust_recon( { + 'recondate' => time, + 'agentnum' => $agentnum, + 'first' => $first, + 'last' => $last, + 'address1' => $address1, + 'address2' => $address2, + 'city' => $city, + 'state' => $state, + 'zip' => $zip, + 'custnum' => $cust_main ? $cust_main->custnum : '', #really? + 'status' => $cust_main ? $cust_main->status : '', + 'pkg' => $package, + 'adjourn' => $cust_pkg ? $cust_pkg->adjourn : '', + 'agent_custid' => $agent_custid, # redundant? + 'agent_pkg' => $pkg, + 'agent_adjourn' => str2time($adjourn), + 'comments' => $comments, + } ); + + warn Dumper($cust_recon) if $DEBUG; + my $error = $cust_recon->insert; + return $error if $error; + + warn "process_recon_record returning $comments\n" if $DEBUG; + + $comments; + +} + +sub check_username { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $svc_domain = qsearchs( 'svc_domain', { 'domain' => $p->{domain} } ) + or return { 'error' => 'Unknown domain '. $p->{domain} }; + + my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{user}, + 'domsvc' => $svc_domain->svcnum, + }, + ); + + return { 'error' => $p->{user}. '@'. $p->{domain}. " alerady in use" } # sic + if $svc_acct; + + return { 'error' => '', + 'message' => $p->{user}. '@'. $p->{domain}. " is free" + }; +} + +1; diff --git a/FS/FS/ClientAPI/MasonComponent.pm b/FS/FS/ClientAPI/MasonComponent.pm new file mode 100644 index 0000000..20b4e5b --- /dev/null +++ b/FS/FS/ClientAPI/MasonComponent.pm @@ -0,0 +1,131 @@ +package FS::ClientAPI::MasonComponent; + +use strict; +use vars qw( $cache $DEBUG $me ); +use subs qw( _cache ); +use FS::Mason qw( mason_interps ); +use FS::Conf; +use FS::ClientAPI_SessionCache; +use FS::Record qw( qsearch qsearchs ); +use FS::cust_main; +use FS::part_pkg; + +$DEBUG = 0; +$me = '[FS::ClientAPI::MasonComponent]'; + +my %allowed_comps = map { $_=>1 } qw( + /elements/select-did.html + /misc/areacodes.cgi + /misc/exchanges.cgi + /misc/phonenums.cgi + /misc/states.cgi + /misc/counties.cgi + /misc/svc_acct-domains.cgi + /misc/part_svc-columns.cgi +); + +my %session_comps = map { $_=>1 } qw( + /elements/location.html + /edit/cust_main/first_pkg/select-part_pkg.html +); + +my %session_callbacks = ( + + '/elements/location.html' => sub { + my( $custnum, $argsref ) = @_; + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return "unknown custnum $custnum"; + my %args = @$argsref; + $args{object} = $cust_main; + @$argsref = ( %args ); + return ''; #no error + }, + + '/edit/cust_main/first_pkg/select-part_pkg.html' => sub { + my( $custnum, $argsref ) = @_; + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return "unknown custnum $custnum"; + + my $pkgpart = $cust_main->agent->pkgpart_hashref; + + #false laziness w/ edit/cust_main/first_pkg.html + my @first_svc = ( 'svc_acct', 'svc_phone' ); + + my @part_pkg = + grep { $_->svcpart(\@first_svc) + && ( $pkgpart->{ $_->pkgpart } + || ( $_->agentnum && $_->agentnum == $cust_main->agentnum ) + ) + } + qsearch( 'part_pkg', { 'disabled' => '' }, '', 'ORDER BY pkg' ); # case? + + my $conf = new FS::Conf; + if ( $conf->exists('pkg-addon_classnum') ) { + + my %classnum = map { ( $_->addon_classnum => 1 ) } + grep { $_->freq !~ /^0/ } + map { $_->part_pkg } + $cust_main->ncancelled_pkgs; + + unless ( $classnum{''} || ! keys %classnum ) { + @part_pkg = grep $classnum{ $_->classnum }, @part_pkg; + } + } + + my %args = @$argsref; + $args{part_pkg} = \@part_pkg; + @$argsref = ( %args ); + return ''; #no error + + }, + +); + +my $outbuf; +my( $fs_interp, $rt_interp ) = mason_interps('standalone', 'outbuf'=>\$outbuf); + +sub mason_comp { + my $packet = shift; + + warn "$me mason_comp called on $packet\n" if $DEBUG; + + my $comp = $packet->{'comp'}; + unless ( $allowed_comps{$comp} || $session_comps{$comp} ) { + return { 'error' => 'Illegal component' }; + } + + my @args = $packet->{'args'} ? @{ $packet->{'args'} } : (); + + if ( $session_comps{$comp} ) { + + my $session = _cache->get($packet->{'session_id'}) + or return ( 'error' => "Can't resume session" ); #better error message + my $custnum = $session->{'custnum'}; + + my $error = &{ $session_callbacks{$comp} }( $custnum, \@args ); + return { 'error' => $error } if $error; + + } + + my $conf = new FS::Conf; + $FS::Mason::Request::FSURL = $conf->config('selfservice_server-base_url'); + $FS::Mason::Request::FSURL .= '/' unless $FS::Mason::Request::FSURL =~ /\/$/; + $FS::Mason::Request::QUERY_STRING = $packet->{'query_string'} || ''; + + $outbuf = ''; + $fs_interp->exec($comp, @args); #only FS for now alas... + + #errors? (turn off in-line error reporting?) + + return { 'output' => $outbuf }; + +} + +#hmm +sub _cache { + $cache ||= new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::MyAccount', + } ); +} + +1; diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm new file mode 100644 index 0000000..8003613 --- /dev/null +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -0,0 +1,1736 @@ +package FS::ClientAPI::MyAccount; + +use 5.008; #require 5.8+ for Time::Local 1.05+ +use strict; +use vars qw( $cache $DEBUG $me ); +use subs qw( _cache _provision ); +use Data::Dumper; +use Digest::MD5 qw(md5_hex); +use Date::Format; +use Business::CreditCard; +use Time::Duration; +use Time::Local qw(timelocal_nocheck); +use FS::UI::Web::small_custview qw(small_custview); #less doh +use FS::UI::Web; +use FS::UI::bytecount qw( display_bytecount ); +use FS::Conf; +#use FS::UID qw(dbh); +use FS::Record qw(qsearch qsearchs dbh); +use FS::Msgcat qw(gettext); +use FS::Misc qw(card_types); +use FS::ClientAPI_SessionCache; +use FS::svc_acct; +use FS::svc_domain; +use FS::svc_phone; +use FS::svc_external; +use FS::part_svc; +use FS::cust_main; +use FS::cust_bill; +use FS::cust_main_county; +use FS::cust_pkg; +use FS::payby; +use FS::acct_rt_transaction; +use HTML::Entities; +use FS::TicketSystem; + +$DEBUG = 0; +$me = '[FS::ClientAPI::MyAccount]'; + +use vars qw( @cust_main_editable_fields ); +@cust_main_editable_fields = qw( + first last company address1 address2 city + county state zip country daytime night fax + ship_first ship_last ship_company ship_address1 ship_address2 ship_city + ship_state ship_zip ship_country ship_daytime ship_night ship_fax + payby payinfo payname paystart_month paystart_year payissue payip + ss paytype paystate stateid stateid_state +); + +sub _cache { + $cache ||= new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::MyAccount', + } ); +} + +sub skin_info { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + #return { 'error' => $session } if $context eq 'error'; + + my $agentnum = ''; + if ( $context eq 'customer' ) { + + my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?') + or die dbh->errstr; + + $sth->execute($custnum) or die $sth->errstr; + + $agentnum = $sth->fetchrow_arrayref->[0] + or die "no agentnum for custnum $custnum"; + + #} elsif ( $context eq 'agent' ) { + } elsif ( $p->{'agentnum'} =~ /^(\d+)$/ ) { + $agentnum = $1; + } + + my $conf = new FS::Conf; + + #false laziness w/Signup.pm + + my $skin_info_cache_agent = _cache->get("skin_info_cache_agent$agentnum"); + + if ( $skin_info_cache_agent ) { + + warn "$me loading cached skin info for agentnum $agentnum\n" + if $DEBUG > 1; + + } else { + + warn "$me populating skin info cache for agentnum $agentnum\n" + if $DEBUG > 1; + + $skin_info_cache_agent = { + 'agentnum' => $agentnum, + ( map { $_ => scalar( $conf->config($_, $agentnum) ) } + qw( company_name ) ), + ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) } + qw( body_bgcolor box_bgcolor + text_color link_color vlink_color hlink_color alink_color + font title_color title_align title_size menu_bgcolor menu_fontsize + ) + ), + ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) } + qw( menu_skipblanks menu_skipheadings menu_nounderline ) + ), + ( map { $_ => scalar($conf->config_binary("selfservice-$_", $agentnum)) } + qw( title_left_image title_right_image + menu_top_image menu_body_image menu_bottom_image + ) + ), + 'logo' => scalar($conf->config_binary('logo.png', $agentnum )), + ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) } + qw( head body_header body_footer company_address ) ), + }; + + _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent); + + } + + #{ %$skin_info_cache_agent }; + $skin_info_cache_agent; + +} + +sub login_info { + my $p = shift; + + my $conf = new FS::Conf; + + my %info = ( + %{ skin_info($p) }, + 'phone_login' => $conf->exists('selfservice_server-phone_login'), + 'single_domain'=> scalar($conf->config('selfservice_server-single_domain')), + ); + + return \%info; + +} + +#false laziness w/FS::ClientAPI::passwd::passwd +sub login { + my $p = shift; + + my $conf = new FS::Conf; + + my $svc_x = ''; + if ( $p->{'domain'} eq 'svc_phone' + && $conf->exists('selfservice_server-phone_login') ) { + + my $svc_phone = qsearchs( 'svc_phone', { 'phonenum' => $p->{'username'} } ); + return { error => 'Number not found.' } unless $svc_phone; + + #XXX? + #my $pkg_svc = $svc_acct->cust_svc->pkg_svc; + #return { error => 'Only primary user may log in.' } + # if $conf->exists('selfservice_server-primary_only') + # && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' ); + + return { error => 'Incorrect PIN.' } + unless $svc_phone->check_pin($p->{'password'}); + + $svc_x = $svc_phone; + + } else { + + my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } ) + or return { error => 'Domain '. $p->{'domain'}. ' not found' }; + + my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{'username'}, + 'domsvc' => $svc_domain->svcnum, } + ); + return { error => 'User not found.' } unless $svc_acct; + + return { error => 'Incorrect password.' } + unless $svc_acct->check_password($p->{'password'}); + + $svc_x = $svc_acct; + + } + + my $session = { + 'svcnum' => $svc_x->svcnum, + }; + + my $cust_svc = $svc_x->cust_svc; + my $cust_pkg = $cust_svc->cust_pkg; + if ( $cust_pkg ) { + my $cust_main = $cust_pkg->cust_main; + $session->{'custnum'} = $cust_main->custnum; + if ( $conf->exists('pkg-balances') ) { + my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ } + $cust_main->ncancelled_pkgs; + $session->{'pkgnum'} = $cust_pkg->pkgnum + if scalar(@cust_pkg) > 1; + } + } + + #my $pkg_svc = $svc_acct->cust_svc->pkg_svc; + #return { error => 'Only primary user may log in.' } + # if $conf->exists('selfservice_server-primary_only') + # && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' ); + my $part_pkg = $cust_pkg->part_pkg; + return { error => 'Only primary user may log in.' } + if $conf->exists('selfservice_server-primary_only') + && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]); + + my $session_id; + do { + $session_id = md5_hex(md5_hex(time(). {}. rand(). $$)) + } until ( ! defined _cache->get($session_id) ); #just in case + + my $timeout = $conf->config('selfservice-session_timeout') || '1 hour'; + _cache->set( $session_id, $session, $timeout ); + + return { 'error' => '', + 'session_id' => $session_id, + }; +} + +sub logout { + my $p = shift; + if ( $p->{'session_id'} ) { + _cache->remove($p->{'session_id'}); + return { %{ skin_info($p) }, 'error' => '' }; + } else { + return { %{ skin_info($p) }, 'error' => "Can't resume session" }; #better error message + } +} + +sub access_info { + my $p = shift; + + my $conf = new FS::Conf; + + my $info = skin_info($p); + + use vars qw( $cust_paybys ); #cache for performance + unless ( $cust_paybys ) { + + my %cust_paybys = map { $_ => 1 } + map { FS::payby->payby2payment($_) } + $conf->config('signup_server-payby'); + + $cust_paybys = [ keys %cust_paybys ]; + + } + $info->{'cust_paybys'} = $cust_paybys; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + $info->{hide_payment_fields} = + [ + map { my $pg = ''; + if ( FS::payby->realtime($_) ) { + $pg = $cust_main->agent->payment_gateway( + 'method' => FS::payby->payby2bop($_), + 'nofatal' => 1, + ); + } + $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; + } + @{ $info->{cust_paybys} } + ]; + + return { %$info, + 'custnum' => $custnum, + 'access_pkgnum' => $session->{'pkgnum'}, + 'access_svcnum' => $session->{'svcnum'}, + }; +} + +sub customer_info { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my %return; + + my $conf = new FS::Conf; + if ($conf->exists('cust_main-require_address2')) { + $return{'require_address2'} = '1'; + }else{ + $return{'require_address2'} = ''; + } + + if ( $custnum ) { #customer record + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + if ( $session->{'pkgnum'} ) { + $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} ); + } else { + $return{balance} = $cust_main->balance; + } + + $return{tickets} = [ ($cust_main->tickets) ]; + + unless ( $session->{'pkgnum'} ) { + my @open = map { + { + invnum => $_->invnum, + date => time2str("%b %o, %Y", $_->_date), + owed => $_->owed, + }; + } $cust_main->open_cust_bill; + $return{open_invoices} = \@open; + } + + $return{small_custview} = + small_custview( $cust_main, + scalar($conf->config('countrydefault')), + ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance + ); + + $return{name} = $cust_main->first. ' '. $cust_main->get('last'); + + for (@cust_main_editable_fields) { + $return{$_} = $cust_main->get($_); + } + + if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { + $return{payinfo} = $cust_main->paymask; + @return{'month', 'year'} = $cust_main->paydate_monthyear; + } + + $return{'invoicing_list'} = + join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list ); + $return{'postal_invoicing'} = + 0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list ); + + if (scalar($conf->config('support_packages'))) { + my @support_services = (); + foreach ($cust_main->support_services) { + my $seconds = $_->svc_x->seconds; + my $time_remaining = (($seconds < 0) ? '-' : '' ). + int(abs($seconds)/3600)."h". + sprintf("%02d",(abs($seconds)%3600)/60)."m"; + my $cust_pkg = $_->cust_pkg; + my $pkgnum = ''; + my $pkg = ''; + $pkgnum = $cust_pkg->pkgnum if $cust_pkg; + $pkg = $cust_pkg->part_pkg->pkg if $cust_pkg; + push @support_services, { svcnum => $_->svcnum, + time => $time_remaining, + pkgnum => $pkgnum, + pkg => $pkg, + }; + } + $return{support_services} = \@support_services; + } + + } elsif ( $session->{'svcnum'} ) { #no customer record + + my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } ) + or die "unknown svcnum"; + $return{name} = $svc_acct->email; + + } else { + + return { 'error' => 'Expired session' }; #XXX redirect to login w/this err! + + } + + return { 'error' => '', + 'custnum' => $custnum, + %return, + }; + +} + +sub edit_info { + my $p = shift; + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'} + or return { 'error' => "no customer record" }; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $new = new FS::cust_main { $cust_main->hash }; + $new->set( $_ => $p->{$_} ) + foreach grep { exists $p->{$_} } @cust_main_editable_fields; + + my $payby = ''; + if (exists($p->{'payby'})) { + $p->{'payby'} =~ /^([A-Z]{4})$/ + or return { 'error' => "illegal_payby " . $p->{'payby'} }; + $payby = $1; + } + + if ( $payby =~ /^(CARD|DCRD)$/ ) { + + $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01'); + + if ( $new->payinfo eq $cust_main->paymask ) { + $new->payinfo($cust_main->payinfo); + } else { + $new->payinfo($p->{'payinfo'}); + } + + $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' ); + + } elsif ( $payby =~ /^(CHEK|DCHK)$/ ) { + + my $payinfo; + $p->{'payinfo1'} =~ /^([\dx]+)$/ + or return { 'error' => "illegal account number ". $p->{'payinfo1'} }; + my $payinfo1 = $1; + $p->{'payinfo2'} =~ /^([\dx]+)$/ + or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} }; + my $payinfo2 = $1; + $payinfo = $payinfo1. '@'. $payinfo2; + + $new->payinfo( ($payinfo eq $cust_main->paymask) + ? $cust_main->payinfo + : $payinfo + ); + + $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' ); + + } elsif ( $payby =~ /^(BILL)$/ ) { + #no-op + } elsif ( $payby ) { #notyet ready + return { 'error' => "unknown payby $payby" }; + } + + my @invoicing_list; + if ( exists $p->{'invoicing_list'} || exists $p->{'postal_invoicing'} ) { + #false laziness with httemplate/edit/process/cust_main.cgi + @invoicing_list = split( /\s*\,\s*/, $p->{'invoicing_list'} ); + push @invoicing_list, 'POST' if $p->{'postal_invoicing'}; + } else { + @invoicing_list = $cust_main->invoicing_list; + } + + my $error = $new->replace($cust_main, \@invoicing_list); + return { 'error' => $error } if $error; + #$cust_main = $new; + + return { 'error' => '' }; +} + +sub payment_info { + my $p = shift; + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + ## + #generic + ## + + use vars qw($payment_info); #cache for performance + unless ( $payment_info ) { + + my $conf = new FS::Conf; + my %states = map { $_->state => 1 } + qsearch('cust_main_county', { + 'country' => $conf->config('countrydefault') || 'US' + } ); + + my %cust_paybys = map { $_ => 1 } + map { FS::payby->payby2payment($_) } + $conf->config('signup_server-payby'); + + my @cust_paybys = keys %cust_paybys; + + $payment_info = { + + #list all counties/states/countries + 'cust_main_county' => + [ map { $_->hashref } qsearch('cust_main_county', {}) ], + + #shortcut for one-country folks + 'states' => + [ sort { $a cmp $b } keys %states ], + + 'card_types' => card_types(), + + 'paytypes' => [ @FS::cust_main::paytypes ], + + 'paybys' => [ $conf->config('signup_server-payby') ], + 'cust_paybys' => \@cust_paybys, + + 'stateid_label' => FS::Msgcat::_gettext('stateid'), + 'stateid_state_label' => FS::Msgcat::_gettext('stateid_state'), + + 'show_ss' => $conf->exists('show_ss'), + 'show_stateid' => $conf->exists('show_stateid'), + 'show_paystate' => $conf->exists('show_bankstate'), + + 'save_unchecked' => $conf->exists('selfservice-save_unchecked'), + }; + + } + + ## + #customer-specific + ## + + my %return = %$payment_info; + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + $return{hide_payment_fields} = + [ + map { my $pg = ''; + if ( FS::payby->realtime($_) ) { + $pg = $cust_main->agent->payment_gateway( + 'method' => FS::payby->payby2bop($_), + 'nofatal' => 1, + ); + } + $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; + } + @{ $return{cust_paybys} } + ]; + + $return{balance} = $cust_main->balance; #XXX pkg-balances? + + $return{payname} = $cust_main->payname + || ( $cust_main->first. ' '. $cust_main->get('last') ); + + $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip); + + $return{payby} = $cust_main->payby; + $return{stateid_state} = $cust_main->stateid_state; + + if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { + $return{card_type} = cardtype($cust_main->payinfo); + $return{payinfo} = $cust_main->paymask; + + @return{'month', 'year'} = $cust_main->paydate_monthyear; + + } + + if ( $cust_main->payby =~ /^(CHEK|DCHK)$/ ) { + my ($payinfo1, $payinfo2) = split '@', $cust_main->paymask; + $return{payinfo1} = $payinfo1; + $return{payinfo2} = $payinfo2; + $return{paytype} = $cust_main->paytype; + $return{paystate} = $cust_main->paystate; + + } + + #doubleclick protection + my $_date = time; + $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; + + return { 'error' => '', + %return, + }; + +}; + +#some false laziness with httemplate/process/payment.cgi - look there for +#ACH and CVV support stuff +sub process_payment { + + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my %return; + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + $p->{'amount'} =~ /^\s*(\d+(\.\d{2})?)\s*$/ + or return { 'error' => gettext('illegal_amount') }; + my $amount = $1; + return { error => 'Amount must be greater than 0' } unless $amount > 0; + + $p->{'payname'} =~ /^([\w \,\.\-\']+)$/ + or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} }; + my $payname = $1; + + $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/ + or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} }; + my $paybatch = $1; + + $p->{'payby'} ||= 'CARD'; + $p->{'payby'} =~ /^([A-Z]{4})$/ + or return { 'error' => "illegal_payby " . $p->{'payby'} }; + my $payby = $1; + + #false laziness w/process/payment.cgi + my $payinfo; + my $paycvv = ''; + if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) { + + $p->{'payinfo1'} =~ /^([\dx]+)$/ + or return { 'error' => "illegal account number ". $p->{'payinfo1'} }; + my $payinfo1 = $1; + $p->{'payinfo2'} =~ /^([\dx]+)$/ + or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} }; + my $payinfo2 = $1; + $payinfo = $payinfo1. '@'. $payinfo2; + + $payinfo = $cust_main->payinfo + if $cust_main->paymask eq $payinfo; + + } elsif ( $payby eq 'CARD' || $payby eq 'DCRD' ) { + + $payinfo = $p->{'payinfo'}; + + #more intelligent mathing will be needed here if you change + #card_masking_method and don't remove existing paymasks + $payinfo = $cust_main->payinfo + if $cust_main->paymask eq $payinfo; + + $payinfo =~ s/\D//g; + $payinfo =~ /^(\d{13,16})$/ + or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo + $payinfo = $1; + + validate($payinfo) + or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo + return { 'error' => gettext('unknown_card_type') } + if $payinfo !~ /^99\d{14}$/ && cardtype($payinfo) eq "Unknown"; + + if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) { + if ( cardtype($payinfo) eq 'American Express card' ) { + $p->{'paycvv'} =~ /^\s*(\d{4})\s*$/ + or return { 'error' => "CVV2 (CID) for American Express cards is four digits." }; + $paycvv = $1; + } else { + $p->{'paycvv'} =~ /^\s*(\d{3})\s*$/ + or return { 'error' => "CVV2 (CVC2/CID) is three digits." }; + $paycvv = $1; + } + } + + } else { + die "unknown payby $payby"; + } + + my %payby2fields = ( + 'CARD' => [ qw( paystart_month paystart_year payissue payip + address1 address2 city state zip country ) ], + 'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ], + ); + + my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount, + 'quiet' => 1, + 'payinfo' => $payinfo, + 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01', + 'payname' => $payname, + 'paybatch' => $paybatch, #this doesn't actually do anything + 'paycvv' => $paycvv, + 'pkgnum' => $session->{'pkgnum'}, + map { $_ => $p->{$_} } @{ $payby2fields{$payby} } + ); + return { 'error' => $error } if $error; + + $cust_main->apply_payments; + + if ( $p->{'save'} ) { + my $new = new FS::cust_main { $cust_main->hash }; + if ($payby eq 'CARD' || $payby eq 'DCRD') { + $new->set( $_ => $p->{$_} ) + foreach qw( payname paystart_month paystart_year payissue payip + address1 address2 city state zip country ); + $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' ); + } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') { + $new->set( $_ => $p->{$_} ) + foreach qw( payname payip paytype paystate + stateid stateid_state ); + $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' ); + } + $new->set( 'payinfo' => $cust_main->card_token || $payinfo ); + $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' ); + my $error = $new->replace($cust_main); + if ( $error ) { + #no, this causes customers to process their payments again + #return { 'error' => $error }; + #XXX just warn verosely for now so i can figure out how these happen in + # the first place, eventually should redirect them to the "change + #address" page but indicate the payment did process?? + delete($p->{'payinfo'}); #don't want to log this! + warn "WARNING: error changing customer info when processing payment (not returning to customer as a processing error): $error\n". + "NEW: ". Dumper($new)."\n". + "OLD: ". Dumper($cust_main)."\n". + "PACKET: ". Dumper($p)."\n"; + #} else { + #not needed... + #$cust_main = $new; + } + } + + return { 'error' => '' }; + +} + +sub realtime_collect { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $error = $cust_main->realtime_collect( + 'method' => $p->{'method'}, + 'pkgnum' => $session->{'pkgnum'}, + 'session_id' => $p->{'session_id'}, + 'apply' => 1, + ); + return { 'error' => $error } unless ref( $error ); + + my $amount = $session->{'pkgnum'} + ? $cust_main->balance_pkgnum( $session->{'pkgnum'} ) + : $cust_main->balance; + + return { 'error' => '', amount => $amount, %$error }; +} + +sub process_payment_order_pkg { + my $p = shift; + + my $hr = process_payment($p); + return $hr if $hr->{'error'}; + + order_pkg($p); +} + +sub process_payment_order_renew { + my $p = shift; + + my $hr = process_payment($p); + return $hr if $hr->{'error'}; + + order_renew($p); +} + +sub process_prepay { + + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my %return; + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = ( 0, 0, 0, 0, 0 ); + my $error = $cust_main->recharge_prepay( $p->{'prepaid_cardnum'}, + \$amount, + \$seconds, + \$upbytes, + \$downbytes, + \$totalbytes, + ); + + return { 'error' => $error } if $error; + + return { 'error' => '', + 'amount' => $amount, + 'seconds' => $seconds, + 'duration' => duration_exact($seconds), + 'upbytes' => $upbytes, + 'upload' => FS::UI::bytecount::bytecount_unexact($upbytes), + 'downbytes' => $downbytes, + 'download' => FS::UI::bytecount::bytecount_unexact($downbytes), + 'totalbytes'=> $totalbytes, + 'totalload' => FS::UI::bytecount::bytecount_unexact($totalbytes), + }; + +} + +sub invoice { + my $p = shift; + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $invnum = $p->{'invnum'}; + + my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum, + 'custnum' => $custnum } ) + or return { 'error' => "Can't find invnum" }; + + #my %return; + + return { 'error' => '', + 'invnum' => $invnum, + 'invoice_text' => join('', $cust_bill->print_text ), + 'invoice_html' => $cust_bill->print_html( { unsquelch_cdr => 1 } ), + }; + +} + +sub invoice_logo { + my $p = shift; + + #sessioning for this? how do we get the session id to the backend invoice + # template so it can add it to the link, blah + + my $agentnum = ''; + if ( $p->{'invnum'} ) { + my $cust_bill = qsearchs('cust_bill', { 'invnum' => $p->{'invnum'} } ) + or return { 'error' => 'unknown invnum' }; + $agentnum = $cust_bill->cust_main->agentnum; + } + + my $templatename = $p->{'template'} || $p->{'templatename'}; + + #false laziness-ish w/view/cust_bill-logo.cgi + + my $conf = new FS::Conf; + if ( $templatename =~ /^([^\.\/]*)$/ && $conf->exists("logo_$1.png") ) { + $templatename = "_$1"; + } else { + $templatename = ''; + } + + my $filename = "logo$templatename.png"; + + return { 'error' => '', + 'logo' => $conf->config_binary($filename, $agentnum), + 'content_type' => 'image/png', #should allow gif, jpg too + }; +} + + +sub list_invoices { + my $p = shift; + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my @cust_bill = $cust_main->cust_bill; + + return { 'error' => '', + 'invoices' => [ map { { 'invnum' => $_->invnum, + '_date' => $_->_date, + } + } @cust_bill + ] + }; +} + +sub cancel { + my $p = shift; + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my @errors = $cust_main->cancel( 'quiet'=>1 ); + + my $error = scalar(@errors) ? join(' / ', @errors) : ''; + + return { 'error' => $error }; + +} + +sub list_pkgs { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + #return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] }; + + my $conf = new FS::Conf; + + { 'svcnum' => $session->{'svcnum'}, + 'custnum' => $custnum, + 'cust_pkg' => [ map { + { $_->hash, + $_->part_pkg->hash, + part_svc => + [ map $_->hashref, $_->available_part_svc ], + cust_svc => + [ map { my $ref = { $_->hash, + label => [ $_->label ], + }; + $ref->{_password} = $_->svc_x->_password + if $context eq 'agent' + && $conf->exists('agent-showpasswords') + && $_->part_svc->svcdb eq 'svc_acct'; + $ref; + } $_->cust_svc + ], + }; + } $cust_main->ncancelled_pkgs + ], + 'small_custview' => + small_custview( $cust_main, $conf->config('countrydefault') ), + }; + +} + +sub list_svcs { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + my @cust_svc = (); + #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) { + foreach my $cust_pkg ( $p->{'ncancelled'} + ? $cust_main->ncancelled_pkgs + : $cust_main->unsuspended_pkgs ) { + next if $session->{'pkgnum'} && $cust_pkg->pkgnum != $session->{'pkgnum'}; + push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context + } + if ( $p->{'svcdb'} ) { + my $svcdb = ref($p->{'svcdb'}) eq 'HASH' + ? $p->{'svcdb'} + : ref($p->{'svcdb'}) eq 'ARRAY' + ? { map { $_=>1 } @{ $p->{'svcdb'} } } + : { $p->{'svcdb'} => 1 }; + @cust_svc = grep $svcdb->{ $_->part_svc->svcdb }, @cust_svc + } + + #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username } + # @svc_x; + + { + 'svcnum' => $session->{'svcnum'}, + 'custnum' => $custnum, + 'svcs' => [ + map { + my $svc_x = $_->svc_x; + my($label, $value) = $_->label; + my $svcdb = $_->part_svc->svcdb; + my $part_pkg = $_->cust_pkg->part_pkg; + + my %hash = ( + 'svcnum' => $_->svcnum, + 'svcdb' => $svcdb, + 'label' => $label, + 'value' => $value, + ); + + if ( $svcdb eq 'svc_acct' ) { + %hash = ( + %hash, + 'username' => $svc_x->username, + 'email' => $svc_x->email, + 'seconds' => $svc_x->seconds, + 'upbytes' => display_bytecount($svc_x->upbytes), + 'downbytes' => display_bytecount($svc_x->downbytes), + 'totalbytes' => display_bytecount($svc_x->totalbytes), + + 'recharge_amount' => $part_pkg->option('recharge_amount',1), + 'recharge_seconds' => $part_pkg->option('recharge_seconds',1), + 'recharge_upbytes' => + display_bytecount($part_pkg->option('recharge_upbytes',1)), + 'recharge_downbytes' => + display_bytecount($part_pkg->option('recharge_downbytes',1)), + 'recharge_totalbytes' => + display_bytecount($part_pkg->option('recharge_totalbytes',1)), + # more... + ); + + } elsif ( $svcdb eq 'svc_phone' ) { + %hash = ( + %hash, + ); + } + + \%hash; + } + @cust_svc + ], + }; + +} + +sub _list_svc_usage { + my($svc_acct, $begin, $end) = @_; + my @usage = (); + foreach my $part_export ( + map { qsearch ( 'part_export', { 'exporttype' => $_ } ) } + qw( sqlradius sqlradius_withdomain ) + ) { + push @usage, @ { $part_export->usage_sessions($begin, $end, $svc_acct) }; + } + (@usage); +} + +sub list_svc_usage { + _usage_details(\&_list_svc_usage, @_); +} + +sub _list_support_usage { + my($svc_acct, $begin, $end) = @_; + my @usage = (); + foreach ( grep { $begin <= $_->_date && $_->_date <= $end } + qsearch('acct_rt_transaction', { 'svcnum' => $svc_acct->svcnum }) + ) { + push @usage, { 'seconds' => $_->seconds, + 'support' => $_->support, + '_date' => $_->_date, + 'id' => $_->transaction_id, + 'creator' => $_->creator, + 'subject' => $_->subject, + 'status' => $_->status, + 'ticketid' => $_->ticketid, + }; + } + (@usage); +} + +sub list_support_usage { + _usage_details(\&_list_support_usage, @_); +} + +sub _list_cdr_usage { + my($svc_phone, $begin, $end) = @_; + map [ $_->downstream_csv('format' => 'default') ], #XXX config for format + $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, ); +} + +sub list_cdr_usage { + my $p = shift; + _usage_details( \&_list_cdr_usage, $p, + 'svcdb' => 'svc_phone', + ); +} + +sub _usage_details { + my($callback, $p, %opt) = @_; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $search = { 'svcnum' => $p->{'svcnum'} }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + + my $svcdb = $opt{'svcdb'} || 'svc_acct'; + + my $svc_x = qsearchs( $svcdb, $search ); + return { 'error' => 'No service selected in list_svc_usage' } + unless $svc_x; + + my $header = $svcdb eq 'svc_phone' + ? [ split(',', FS::cdr::invoice_header('default') ) ] #XXX + : []; + + my $cust_pkg = $svc_x->cust_svc->cust_pkg; + my $freq = $cust_pkg->part_pkg->freq; + my $start = $cust_pkg->setup; + #my $end = $cust_pkg->bill; # or time? + my $end = time; + + unless ( $p->{beginning} ) { + $p->{beginning} = $cust_pkg->last_bill; + $p->{ending} = $end; + } + + my (@usage) = &$callback($svc_x, $p->{beginning}, $p->{ending}); + + #kinda false laziness with FS::cust_main::bill, but perhaps + #we should really change this bit to DateTime and DateTime::Duration + # + #change this bit to use Date::Manip? CAREFUL with timezones (see + # mailing list archive) + my ($nsec,$nmin,$nhour,$nmday,$nmon,$nyear) = + (localtime($p->{ending}) )[0,1,2,3,4,5]; + my ($psec,$pmin,$phour,$pmday,$pmon,$pyear) = + (localtime($p->{beginning}) )[0,1,2,3,4,5]; + + if ( $freq =~ /^\d+$/ ) { + $nmon += $freq; + until ( $nmon < 12 ) { $nmon -= 12; $nyear++; } + $pmon -= $freq; + until ( $pmon >= 0 ) { $pmon += 12; $pyear--; } + } elsif ( $freq =~ /^(\d+)w$/ ) { + my $weeks = $1; + $nmday += $weeks * 7; + $pmday -= $weeks * 7; + } elsif ( $freq =~ /^(\d+)d$/ ) { + my $days = $1; + $nmday += $days; + $pmday -= $days; + } elsif ( $freq =~ /^(\d+)h$/ ) { + my $hours = $1; + $nhour += $hours; + $phour -= $hours; + } else { + return { 'error' => "unparsable frequency: ". $freq }; + } + + my $previous = timelocal_nocheck($psec,$pmin,$phour,$pmday,$pmon,$pyear); + my $next = timelocal_nocheck($nsec,$nmin,$nhour,$nmday,$nmon,$nyear); + + { + 'error' => '', + 'svcnum' => $p->{svcnum}, + 'beginning' => $p->{beginning}, + 'ending' => $p->{ending}, + 'previous' => ($previous > $start) ? $previous : $start, + 'next' => ($next < $end) ? $next : $end, + 'header' => $header, + 'usage' => \@usage, + }; +} + +sub order_pkg { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + my $status = $cust_main->status; + #false laziness w/ClientAPI/Signup.pm + + my $cust_pkg = new FS::cust_pkg ( { + 'custnum' => $custnum, + 'pkgpart' => $p->{'pkgpart'}, + } ); + my $error = $cust_pkg->check; + return { 'error' => $error } if $error; + + my @svc = (); + unless ( $p->{'svcpart'} eq 'none' ) { + + my $svcdb; + my $svcpart = ''; + if ( $p->{'svcpart'} =~ /^(\d+)$/ ) { + $svcpart = $1; + my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } ); + return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc; + $svcdb = $part_svc->svcdb; + } else { + $svcdb = 'svc_acct'; + } + $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb); + + my %fields = ( + 'svc_acct' => [ qw( username domsvc _password sec_phrase popnum ) ], + 'svc_domain' => [ qw( domain ) ], + 'svc_phone' => [ qw( phonenum pin sip_password phone_name ) ], + 'svc_external' => [ qw( id title ) ], + ); + + my $svc_x = "FS::$svcdb"->new( { + 'svcpart' => $svcpart, + map { $_ => $p->{$_} } @{$fields{$svcdb}} + } ); + + if ( $svcdb eq 'svc_acct' ) { + my @acct_snarf; + my $snarfnum = 1; + while ( length($p->{"snarf_machine$snarfnum"}) ) { + my $acct_snarf = new FS::acct_snarf ( { + 'machine' => $p->{"snarf_machine$snarfnum"}, + 'protocol' => $p->{"snarf_protocol$snarfnum"}, + 'username' => $p->{"snarf_username$snarfnum"}, + '_password' => $p->{"snarf_password$snarfnum"}, + } ); + $snarfnum++; + push @acct_snarf, $acct_snarf; + } + $svc_x->child_objects( \@acct_snarf ); + } + + my $y = $svc_x->setdefault; # arguably should be in new method + return { 'error' => $y } if $y && !ref($y); + + $error = $svc_x->check; + return { 'error' => $error } if $error; + + push @svc, $svc_x; + + } + + use Tie::RefHash; + tie my %hash, 'Tie::RefHash'; + %hash = ( $cust_pkg => \@svc ); + #msgcat + $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 ); + return { 'error' => $error } if $error; + + my $conf = new FS::Conf; + if ( $conf->exists('signup_server-realtime') ) { + + my $bill_error = _do_bop_realtime( $cust_main, $status ); + + if ($bill_error) { + $cust_pkg->cancel('quiet'=>1); + return $bill_error; + } else { + $cust_pkg->reexport; + } + + } else { + $cust_pkg->reexport; + } + + return { error => '', pkgnum => $cust_pkg->pkgnum }; + +} + +sub change_pkg { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + my $status = $cust_main->status; + my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $p->{pkgnum} } ) + or return { 'error' => "unknown package $p->{pkgnum}" }; + + my @newpkg; + my $error = FS::cust_pkg::order( $custnum, + [$p->{pkgpart}], + [$p->{pkgnum}], + \@newpkg, + ); + + my $conf = new FS::Conf; + if ( $conf->exists('signup_server-realtime') ) { + + my $bill_error = _do_bop_realtime( $cust_main, $status ); + + if ($bill_error) { + $newpkg[0]->suspend; + return $bill_error; + } else { + $newpkg[0]->reexport; + } + + } else { + $newpkg[0]->reexport; + } + + return { error => '', pkgnum => $cust_pkg->pkgnum }; + +} + +sub order_recharge { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + my $status = $cust_main->status; + my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $p->{'svcnum'} } ) + or return { 'error' => "unknown service " . $p->{'svcnum'} }; + + my $svc_x = $cust_svc->svc_x; + my $part_pkg = $cust_svc->cust_pkg->part_pkg; + + my %vhash = + map { $_ =~ /^recharge_(.*)$/; $1, $part_pkg->option($_, 1) } + qw ( recharge_seconds recharge_upbytes recharge_downbytes + recharge_totalbytes ); + my $amount = $part_pkg->option('recharge_amount', 1); + + my ($l, $v, $d) = $cust_svc->label; # blah + my $pkg = "Recharge $v"; + + my $bill_error = $cust_main->charge($amount, $pkg, + "time: $vhash{seconds}, up: $vhash{upbytes}," . + "down: $vhash{downbytes}, total: $vhash{totalbytes}", + $part_pkg->taxclass); #meh + + my $conf = new FS::Conf; + if ( $conf->exists('signup_server-realtime') && !$bill_error ) { + + $bill_error = _do_bop_realtime( $cust_main, $status ); + + if ($bill_error) { + return $bill_error; + } else { + my $error = $svc_x->recharge (\%vhash); + return { 'error' => $error } if $error; + } + + } else { + my $error = $bill_error; + $error ||= $svc_x->recharge (\%vhash); + return { 'error' => $error } if $error; + } + + return { error => '', svc => $cust_svc->part_svc->svc }; + +} + +sub _do_bop_realtime { + my ($cust_main, $status) = (shift, shift); + + my $old_balance = $cust_main->balance; + + my $bill_error = $cust_main->bill + || $cust_main->apply_payments_and_credits + || $cust_main->realtime_collect; + + if ( $cust_main->balance > $old_balance + && $cust_main->balance > 0 + && ( $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ? + 1 : $status eq 'suspended' ) ) { + #this makes sense. credit is "un-doing" the invoice + my $conf = new FS::Conf; + $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ), + 'self-service decline', + 'reason_type' => $conf->config('signup_credit_type'), + ); + $cust_main->apply_credits( 'order' => 'newest' ); + + return { 'error' => '_decline', 'bill_error' => $bill_error }; + } + + ''; +} + +sub renew_info { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my @cust_pkg = sort { $a->bill <=> $b->bill } + grep { $_->part_pkg->freq ne '0' } + $cust_main->ncancelled_pkgs; + + #return { 'error' => 'No active packages to renew.' } unless @cust_pkg; + + my $total = $cust_main->balance; + + my @array = map { + my $bill = $_->bill; + $total += $_->part_pkg->base_recur($_, \$bill); + my $renew_date = $_->part_pkg->add_freq($_->bill); + { + 'pkgnum' => $_->pkgnum, + 'amount' => sprintf('%.2f', $total), + 'bill_date' => $_->bill, + 'bill_date_pretty' => time2str('%x', $_->bill), + 'renew_date' => $renew_date, + 'renew_date_pretty' => time2str('%x', $renew_date), + 'expire_date' => $_->expire, + 'expire_date_pretty' => time2str('%x', $_->expire), + }; + } + @cust_pkg; + + return { 'dates' => \@array }; + +} + +sub payment_info_renew_info { + my $p = shift; + my $renew_info = renew_info($p); + my $payment_info = payment_info($p); + return { %$renew_info, + %$payment_info, + }; +} + +sub order_renew { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $date = $p->{'date'}; + + my $now = time; + + #freeside-daily -n -d $date fs_daily $custnum + $cust_main->bill_and_collect( 'time' => $date, + 'invoice_time' => $now, + 'actual_time' => $now, + 'check_freq' => '1d', + ); + + return { 'error' => '' }; + +} + +sub cancel_pkg { + my $p = shift; + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $pkgnum = $p->{'pkgnum'}; + + my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum, + 'pkgnum' => $pkgnum, } ) + or return { 'error' => "unknown pkgnum $pkgnum" }; + + my $error = $cust_pkg->cancel( 'quiet'=>1 ); + return { 'error' => $error }; + +} + +sub provision_acct { + my $p = shift; + warn "provision_acct called\n" + if $DEBUG; + + return { 'error' => gettext('passwords_dont_match') } + if $p->{'_password'} ne $p->{'_password2'}; + return { 'error' => gettext('empty_password') } + unless length($p->{'_password'}); + + if ($p->{'domsvc'}) { + my %domains = domain_select_hash FS::svc_acct(map { $_ => $p->{$_} } + qw ( svcpart pkgnum ) ); + return { 'error' => gettext('invalid_domain') } + unless ($domains{$p->{'domsvc'}}); + } + + warn "provision_acct calling _provision\n" + if $DEBUG; + _provision( 'FS::svc_acct', + [qw(username _password domsvc)], + [qw(username _password domsvc)], + $p, + @_ + ); +} + +sub provision_external { + my $p = shift; + #_provision( 'FS::svc_external', [qw(id title)], [qw(id title)], $p, @_ ); + _provision( 'FS::svc_external', + [], + [qw(id title)], + $p, + @_ + ); +} + +sub _provision { + my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4); + warn "_provision called for $class\n" + if $DEBUG; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + my $pkgnum = $p->{'pkgnum'}; + + warn "searching for custnum $custnum pkgnum $pkgnum\n" + if $DEBUG; + my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum, + 'pkgnum' => $pkgnum, + } ) + or return { 'error' => "unknown pkgnum $pkgnum" }; + + warn "searching for svcpart ". $p->{'svcpart'}. "\n" + if $DEBUG; + my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } ) + or return { 'error' => "unknown svcpart $p->{'svcpart'}" }; + + warn "creating $class record\n" + if $DEBUG; + my $svc_x = $class->new( { + 'pkgnum' => $p->{'pkgnum'}, + 'svcpart' => $p->{'svcpart'}, + map { $_ => $p->{$_} } @$fields + } ); + warn "inserting $class record\n" + if $DEBUG; + my $error = $svc_x->insert; + + unless ( $error ) { + warn "finding inserted record for svcnum ". $svc_x->svcnum. "\n" + if $DEBUG; + $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum }) + } + + my $return = { 'svc' => $part_svc->svc, + 'error' => $error, + map { $_ => $svc_x->get($_) } @$return_fields + }; + warn "_provision returning ". Dumper($return). "\n" + if $DEBUG; + return $return; + +} + +sub part_svc_info { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + my $pkgnum = $p->{'pkgnum'}; + + my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum, + 'pkgnum' => $pkgnum, + } ) + or return { 'error' => "unknown pkgnum $pkgnum" }; + + my $svcpart = $p->{'svcpart'}; + + my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart, + 'svcpart' => $svcpart, } ) + or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" }; + my $part_svc = $pkg_svc->part_svc; + + my $conf = new FS::Conf; + + return { + 'svc' => $part_svc->svc, + 'svcdb' => $part_svc->svcdb, + 'pkgnum' => $pkgnum, + 'svcpart' => $svcpart, + 'custnum' => $custnum, + + 'security_phrase' => 0, #XXX ! + 'svc_acct_pop' => [], #XXX ! + 'popnum' => '', + 'init_popstate' => '', + 'popac' => '', + 'acstate' => '', + + 'small_custview' => + small_custview( $cust_main, $conf->config('countrydefault') ), + + }; + +} + +sub unprovision_svc { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + my $svcnum = $p->{'svcnum'}; + + my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svcnum, } ) + or return { 'error' => "unknown svcnum $svcnum" }; + + return { 'error' => "Service $svcnum does not belong to customer $custnum" } + unless $cust_svc->cust_pkg->custnum == $custnum; + + my $conf = new FS::Conf; + + return { 'svc' => $cust_svc->part_svc->svc, + 'error' => $cust_svc->cancel, + 'small_custview' => + small_custview( $cust_main, $conf->config('countrydefault') ), + }; + +} + +sub myaccount_passwd { + my $p = shift; + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + return { 'error' => "New passwords don't match." } + if $p->{'new_password'} ne $p->{'new_password2'}; + + return { 'error' => 'Enter new password' } + unless length($p->{'new_password'}); + + #my $search = { 'custnum' => $custnum }; + #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + $custnum =~ /^(\d+)$/ or die "illegal custnum"; + my $search = " AND custnum = $1"; + $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent'; + + my $svc_acct = qsearchs( { + 'table' => 'svc_acct', + 'addl_from' => 'LEFT JOIN cust_svc USING ( svcnum ) '. + 'LEFT JOIN cust_pkg USING ( pkgnum ) '. + 'LEFT JOIN cust_main USING ( custnum ) ', + 'hashref' => { 'svcnum' => $p->{'svcnum'}, }, + 'extra_sql' => $search, #important + } ) + or return { 'error' => "Service not found" }; + + $svc_acct->_password($p->{'new_password'}); + my $error = $svc_acct->replace(); + + my($label, $value) = $svc_acct->cust_svc->label; + + return { 'error' => $error, + 'label' => $label, + 'value' => $value, + }; + +} + +sub create_ticket { + my $p = shift; + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + warn "$me create_ticket: initializing ticket system\n" if $DEBUG; + FS::TicketSystem->init(); + + my $conf = new FS::Conf; + my $queue = $p->{'queue'} + || $conf->config('ticket_system-selfservice_queueid') + || $conf->config('ticket_system-default_queueid'); + + warn "$me create_ticket: creating ticket\n" if $DEBUG; + my $err_or_ticket = FS::TicketSystem->create_ticket( + '', #create RT session based on FS CurrentUser (fs_selfservice) + 'queue' => $queue, + 'custnum' => $custnum, + 'svcnum' => $session->{'svcnum'}, + map { $_ => $p->{$_} } qw( requestor cc subject message mime_type ) + ); + + if ( ref($err_or_ticket) ) { + warn "$me create_ticket: sucessful: ". $err_or_ticket->id. "\n" + if $DEBUG; + return { 'error' => '', + 'ticket_id' => $err_or_ticket->id, + }; + } else { + warn "$me create_ticket: unsucessful: $err_or_ticket\n" + if $DEBUG; + return { 'error' => $err_or_ticket }; + } + + +} + +#-- + +sub _custoragent_session_custnum { + my $p = shift; + + my($context, $session, $custnum); + if ( $p->{'session_id'} ) { + + $context = 'customer'; + $session = _cache->get($p->{'session_id'}) + or return ( 'error' => "Can't resume session" ); #better error message + $custnum = $session->{'custnum'}; + + } elsif ( $p->{'agent_session_id'} ) { + + $context = 'agent'; + my $agent_cache = new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::Agent', + } ); + $session = $agent_cache->get($p->{'agent_session_id'}) + or return ( 'error' => "Can't resume session" ); #better error message + $custnum = $p->{'custnum'}; + + } else { + return ( 'error' => "Can't resume session" ); #better error message + } + + ($context, $session, $custnum); + +} + +1; + diff --git a/FS/FS/ClientAPI/PrepaidPhone.pm b/FS/FS/ClientAPI/PrepaidPhone.pm new file mode 100644 index 0000000..00bc0ff --- /dev/null +++ b/FS/FS/ClientAPI/PrepaidPhone.pm @@ -0,0 +1,253 @@ +package FS::ClientAPI::PrepaidPhone; + +use strict; +use vars qw($DEBUG $me); +use FS::Record qw(qsearchs); +use FS::rate; +use FS::svc_phone; + +$DEBUG = 0; +$me = '[FS::ClientAPI::PrepaidPhone]'; + +#TODO: +# - shared-secret auth? (set a conf value) + +=item call_time HASHREF + +HASHREF contains the following parameters: + +=over 4 + +=item src + +Source number (with countrycode) + +=item dst + +Destination number (with countrycode) + +=back + +Always returns a hashref. If there is an error, the hashref contains a single +"error" key with the error message as a value. Otherwise, returns a hashref +with the following keys: + +=over 4 + +=item custnum + +Empty if no customer is found associated with the number, customer number +otherwise. + +=item seconds + +Number of seconds remaining for a call to destination number + +=back + +=cut + +sub call_time { + my $packet = shift; + + my $src = $packet->{'src'}; + my $dst = $packet->{'dst'}; + + my $chargeto; + my $rateby; + #my $conf = new FS::Conf; + #if ( #XXX toll-free? collect? + # $phonenum = $dst; + #} else { #use the src to find the customer + $chargeto = $src; + $rateby = $dst; + #} + + my( $countrycode, $phonenum ); + if ( $chargeto #an interesting regex to parse out 1&2 digit countrycodes + =~ /^(2[078]|3[0-469]|4[013-9]|5[1-8]|6[0-6]|7|8[1-469]|9[0-58])(\d*)$/ + || $chargeto =~ /^(\d{3})(\d*)$/ + ) + { + $countrycode = $1; + $phonenum = $2; + } else { + return { 'error' => "unparsable billing number: $chargeto" }; + } + + + my $svc_phone = qsearchs('svc_phone', { 'countrycode' => $countrycode, + 'phonenum' => $phonenum, + } + ); + + unless ( $svc_phone ) { + return { 'error' => "can't find customer for +$countrycode $phonenum" }; +# return { 'custnum' => '', +# 'seconds' => 0, +# #'balance' => 0, +# }; + }; + + my $cust_pkg = $svc_phone->cust_svc->cust_pkg; + my $cust_main = $cust_pkg->cust_main; + + my $part_pkg = $cust_pkg->part_pkg; + my @part_pkg = ( $part_pkg, map $_->dst_pkg, $part_pkg->bill_part_pkg_link ); + #XXX uuh, behavior indeterminate if you have more than one voip_cdr+prefix + #add-on, i guess. + warn "$me ". scalar(@part_pkg). ': '. + join('/', map { $_->plan. $_->option('rating_method') } @part_pkg ) + if $DEBUG; + @part_pkg = + grep { $_->plan eq 'voip_cdr' && $_->option('rating_method') eq 'prefix' } + @part_pkg; + + my %return = ( + 'custnum' => $cust_pkg->custnum, + #'balance' => $cust_pkg->cust_main->balance, + ); + + warn "$me: ". scalar(@part_pkg). ': '. + join('/', map { $_->plan. $_->option('rating_method') } @part_pkg ) + if $DEBUG; + return \%return unless @part_pkg; + + warn "$me searching for rate ". $part_pkg[0]->option('ratenum') + if $DEBUG; + + my $rate = qsearchs('rate', { 'ratenum'=>$part_pkg[0]->option('ratenum') } ); + + unless ( $rate ) { + my $error = 'ratenum '. $part_pkg[0]->option('ratenum'). ' not found'; + warn "$me $error" + if $DEBUG; + return { 'error'=>$error }; + } + + warn "$me found rate ". $rate->ratenum + if $DEBUG; + + #rate the call and arrive at a max # of seconds for the customer's balance + + my( $rate_countrycode, $rate_phonenum ); + if ( $rateby #this is an interesting regex to parse out 1&2 digit countrycodes + =~ /^(2[078]|3[0-469]|4[013-9]|5[1-8]|6[0-6]|7|8[1-469]|9[0-58])(\d*)$/ + || $rateby =~ /^(\d{3})(\d*)$/ + ) + { + $rate_countrycode = $1; + $rate_phonenum = $2; + } else { + return { 'error' => "unparsable rating number: $rateby" }; + } + + my $rate_detail = $rate->dest_detail({ 'countrycode' => $rate_countrycode, + 'phonenum' => $rate_phonenum, + }); + unless ( $rate_detail ) { + return { 'error'=>"can't find rate for +$rate_countrycode $rate_phonenum"}; + } + + unless ( $rate_detail->min_charge > 0 ) { + #XXX no charge?? return lots of seconds, a default, 0 or what? + #return { 'error' => '0 rate for +$rate_countrycode $rate_phonenum; prepaid service not available" }; + #customer wants no default for now# $return{'seconds'} = 1800; #half hour?! + return \%return; + } + + #XXX granularity? included minutes? another day... + if ( $cust_main->balance >= 0 ) { + return { 'error'=>'No balance' }; + } else { + $return{'seconds'} = int(60 * abs($cust_main->balance) / $rate_detail->min_charge); + } + + warn "$me returning seconds: ". $return{'seconds'}; + + return \%return; + +} + +=item call_time_nanpa + +Like I<call_time>, except countrycode 1 is not required, and all other +countrycodes must be prefixed with 011. + +=cut + +# - everything is assumed to be countrycode 1 unless it starts with 011(ccode) +sub call_time_nanpa { + my $packet = shift; + + foreach (qw( src dst )) { + if ( $packet->{$_} =~ /^011(\d+)/ ) { + $packet->{$_} = $1; + } elsif ( $packet->{$_} !~ /^1/ ) { + $packet->{$_} = '1'.$packet->{$_}; + } + } + + call_time($packet); + +} + +=item phonenum_balance HASHREF + +HASHREF contains the following parameters: + +=over 4 + +=item countrycode + +Optional countrycode. Defaults to 1. + +=item phonenum + +Phone number. + +=back + +Always returns a hashref. If there is an error, the hashref contains a single +"error" key with the error message as a value. Otherwise, returns a hashref +with the following keys: + +=over 4 + +=item custnum + +Empty if no customer is found associated with the number, customer number +otherwise. + +=item balance + +Customer balance. + +=back + +=cut + +sub phonenum_balance { + my $packet = shift; + + my $svc_phone = qsearchs('svc_phone', { + 'countrycode' => ( $packet->{'countrycode'} || 1 ), + 'phonenum' => $packet->{'phonenum'}, + }); + + unless ( $svc_phone ) { + return { 'custnum' => '', + 'balance' => 0, + }; + }; + + my $cust_pkg = $svc_phone->cust_svc->cust_pkg; + + return { + 'custnum' => $cust_pkg->custnum, + 'balance' => $cust_pkg->cust_main->balance, + }; + +} + +1; diff --git a/FS/FS/ClientAPI/SGNG.pm b/FS/FS/ClientAPI/SGNG.pm new file mode 100644 index 0000000..7f784dc --- /dev/null +++ b/FS/FS/ClientAPI/SGNG.pm @@ -0,0 +1,277 @@ +#this stuff is SG-specific (i.e. multi-customer company username hack) + +package FS::ClientAPI::SGNG; + +use strict; +use vars qw( $cache $DEBUG ); +use Time::Local qw(timelocal timelocal_nocheck); +use Business::CreditCard; +use FS::Record qw( qsearch qsearchs ); +use FS::Conf; +use FS::cust_main; +use FS::cust_pkg; +use FS::ClientAPI::MyAccount; #qw( payment_info process_payment ) + +$DEBUG = 0; + +sub _cache { + $cache ||= new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::MyAccount', #yes, share session_ids + } ); +} + +sub ping { + #my $p = shift; + + return { 'pong' => '1' }; + +} + +#this might almost be general-purpose +sub decompify_pkgs { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + return { 'error' => 'Not a complimentary customer' } + unless $cust_main->payby eq 'COMP'; + + my $paydate = + $cust_main->paydate =~ /^\S+$/ ? $cust_main->paydate : '2037-12-31'; + + my ($payyear,$paymonth,$payday) = split (/-/,$paydate); + + my $date = timelocal(0,0,0,$payday,--$paymonth,$payyear); + + foreach my $cust_pkg ( + qsearch({ 'table' => 'cust_pkg', + 'hashref' => { 'custnum' => $custnum, + 'bill' => '', + }, + 'extra_sql' => ' AND '. FS::cust_pkg->active_sql, + }) + ) { + $cust_pkg->set('bill', $date); + my $error = $cust_pkg->replace; + return { 'error' => $error } if $error; + } + + return { 'error' => '' }; + +} + +#find old payment info +# (should work just like MyAccount::payment_info, except returns previous info +# too) +# definitly sg-specific, no one else stores past customer records like this +sub previous_payment_info { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $payment_info = FS::ClientAPI::MyAccount::payment_info($p); + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + #? + return $payment_info if $cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/; + + foreach my $prev_cust_main ( + reverse _previous_cust_main( 'custnum' => $custnum, + 'username' => $cust_main->company, + 'with_payments' => 1, + ) + ) { + + next unless $prev_cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/; + + if ( $prev_cust_main->payby =~ /^(CARD|DCRD)$/ ) { + + #card expired? + my ($payyear,$paymonth,$payday) = split (/-/, $cust_main->paydate); + + my $expdate = timelocal_nocheck(0,0,0,1,$paymonth,$payyear); + + next if $expdate < time; + + } elsif ( $prev_cust_main->payby =~ /^(CHEK|DCHK)$/ ) { + + #any check? or just skip these in favor of cards? + + } + + return { %$payment_info, + #$prev_cust_main->payment_info + _cust_main_payment_info( $prev_cust_main ), + 'previous_custnum' => $prev_cust_main->custnum, + }; + + } + + #still nothing? return an error? + return $payment_info; + +} + +#this is really FS::cust_main::payment_info, but here for now +sub _cust_main_payment_info { + my $self = shift; + + my %return = (); + + $return{balance} = $self->balance; + + $return{payname} = $self->payname + || ( $self->first. ' '. $self->get('last') ); + + $return{$_} = $self->get($_) for qw(address1 address2 city state zip); + + $return{payby} = $self->payby; + $return{stateid_state} = $self->stateid_state; + + if ( $self->payby =~ /^(CARD|DCRD)$/ ) { + $return{card_type} = cardtype($self->payinfo); + $return{payinfo} = $self->paymask; + + @return{'month', 'year'} = $self->paydate_monthyear; + + } + + if ( $self->payby =~ /^(CHEK|DCHK)$/ ) { + my ($payinfo1, $payinfo2) = split '@', $self->paymask; + $return{payinfo1} = $payinfo1; + $return{payinfo2} = $payinfo2; + $return{paytype} = $self->paytype; + $return{paystate} = $self->paystate; + + } + + #doubleclick protection + my $_date = time; + $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; + + %return; + +} + +#find old cust_main records (with payments) +sub _previous_cust_main { + + #safety check! return nothing unless we're enabled explicitly + return () unless FS::Conf->new->exists('sg-multicustomer_hack'); + + my %opt = @_; + my $custnum = $opt{'custnum'}; + my $username = $opt{'username'}; + + my %search = (); + if ( $opt{'with_payments'} ) { + $search{'extra_sql'} = + ' AND 0 < ( SELECT COUNT(*) FROM cust_pay + WHERE cust_pay.custnum = cust_main.custnum + ) + '; + } + + qsearch( { + 'table' => 'cust_main', + 'hashref' => { 'company' => { op => 'ILIKE', value => $opt{'username'} }, + 'custnum' => { op => '!=', value => $opt{'custnum'} }, + }, + 'order_by' => 'ORDER BY custnum', + %search, + } ); + +} + +#since we could be passing masked old CC data, need to look that up and +#replace it (like regular process_payment does) w/info from old customer record +sub previous_process_payment { + my $p = shift; + + return FS::ClientAPI::MyAccount::process_payment($p) + unless $p->{'previous_custnum'} + && ( ( $p->{'payby'} =~ /^(CARD|DCRD)$/ && $p->{'payinfo'} =~ /x/i ) + || ( $p->{'payby'} =~ /^(CHEK|DCHK)$/ && $p->{'payinfo1'} =~ /x/i ) + ); + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + #make sure this is really a previous custnum of this customer + my @previous_cust_main = + grep { $_->custnum == $p->{'previous_custnum'} } + _previous_cust_main( 'custnum' => $custnum, + 'username' => $cust_main->company, + 'with_payments' => 1, + ); + + my $previous_cust_main = $previous_cust_main[0]; + + #causes problems with old data w/old masking method + #if $previous_cust_main->paymask eq $payinfo; + + if ( $p->{'payby'} =~ /^(CHEK|DCHK)$/ && $p->{'payinfo1'} =~ /x/i ) { + ( $p->{'payinfo1'}, $p->{'payinfo2'} ) = + split('@', $previous_cust_main->payinfo); + } elsif ( $p->{'payby'} =~ /^(CARD|DCRD)$/ && $p->{'payinfo'} =~ /x/i ) { + $p->{'payinfo'} = $previous_cust_main->payinfo; + } + + FS::ClientAPI::MyAccount::process_payment($p); + +} + +sub previous_payment_info_renew_info { + my $p = shift; + my $renew_info = renew_info($p); + my $payment_info = previous_payment_info($p); + return { %$renew_info, + %$payment_info, + }; +} + +sub previous_process_payment_order_pkg { + my $p = shift; + + my $hr = previous_process_payment($p); + return $hr if $hr->{'error'}; + + order_pkg($p); +} + +sub previous_process_payment_change_pkg { + my $p = shift; + + my $hr = previous_process_payment($p); + return $hr if $hr->{'error'}; + + change_pkg($p); +} + +sub previous_process_payment_order_renew { + my $p = shift; + + my $hr = previous_process_payment($p); + return $hr if $hr->{'error'}; + + order_renew($p); +} + +1; + diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm new file mode 100644 index 0000000..5d70325 --- /dev/null +++ b/FS/FS/ClientAPI/Signup.pm @@ -0,0 +1,796 @@ +package FS::ClientAPI::Signup; + +use strict; +use vars qw( $DEBUG $me ); +use Data::Dumper; +use Tie::RefHash; +use FS::Conf; +use FS::Record qw(qsearch qsearchs dbdef); +use FS::CGI qw(popurl); +use FS::Msgcat qw(gettext); +use FS::Misc qw(card_types); +use FS::ClientAPI_SessionCache; +use FS::agent; +use FS::cust_main_county; +use FS::part_pkg; +use FS::svc_acct_pop; +use FS::cust_main; +use FS::cust_pkg; +use FS::svc_acct; +use FS::svc_phone; +use FS::acct_snarf; +use FS::queue; +use FS::reg_code; +use FS::payby; + +$DEBUG = 0; +$me = '[FS::ClientAPI::Signup]'; + +sub signup_info { + my $packet = shift; + + warn "$me signup_info called on $packet\n" if $DEBUG; + + my $conf = new FS::Conf; + my $svc_x = $conf->config('signup_server-service') || 'svc_acct'; + + my $cache = new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::Signup', + } ); + my $signup_info_cache = $cache->get('signup_info_cache'); + + if ( $signup_info_cache ) { + + warn "$me loading cached signup info\n" if $DEBUG > 1; + + } else { + + warn "$me populating signup info cache\n" if $DEBUG > 1; + + my $agentnum2part_pkg = + { + map { + my $agent = $_; + my $href = $agent->pkgpart_hashref; + $agent->agentnum => + [ + map { { 'payby' => [ $_->payby ], + 'freq_pretty' => $_->freq_pretty, + 'options' => { $_->options }, + %{$_->hashref} + } } + grep { $_->svcpart($svc_x) + && ( $href->{ $_->pkgpart } + || ( $_->agentnum + && $_->agentnum == $agent->agentnum + ) + ) + } + qsearch( 'part_pkg', { 'disabled' => '' } ) + ]; + } qsearch('agent', { 'disabled' => '' }) + }; + + my $msgcat = { map { $_=>gettext($_) } + qw( passwords_dont_match invalid_card unknown_card_type + not_a empty_password illegal_or_empty_text ) + }; + warn "msgcat: ". Dumper($msgcat). "\n" if $DEBUG > 2; + + my $label = { map { $_ => FS::Msgcat::_gettext($_) } + qw( stateid stateid_state ) + }; + warn "label: ". Dumper($label). "\n" if $DEBUG > 2; + + my @agent_fields = qw( agentnum agent ); + + $signup_info_cache = { + 'cust_main_county' => [ map $_->hashref, + qsearch('cust_main_county', {} ) + ], + + 'agent' => [ map { my $agent = $_; + map { $_ => $agent->get($_) } @agent_fields; + } + qsearch('agent', { 'disabled' => '' } ) + ], + + 'part_referral' => [ map $_->hashref, + qsearch('part_referral', { 'disabled' => '' } ) + ], + + 'agentnum2part_pkg' => $agentnum2part_pkg, + + 'svc_acct_pop' => [ map $_->hashref, qsearch('svc_acct_pop',{} ) ], + + 'emailinvoiceonly' => $conf->exists('emailinvoiceonly'), + + 'security_phrase' => $conf->exists('security_phrase'), + + 'nomadix' => $conf->exists('signup_server-nomadix'), + + 'payby' => [ $conf->config('signup_server-payby') ], + + 'card_types' => card_types(), + + 'paytypes' => [ @FS::cust_main::paytypes ], + + 'cvv_enabled' => 1, + + 'stateid_enabled' => $conf->exists('show_stateid'), + + 'paystate_enabled' => $conf->exists('show_bankstate'), + + 'ship_enabled' => 1, + + 'msgcat' => $msgcat, + + 'label' => $label, + + 'statedefault' => scalar($conf->config('statedefault')) || 'CA', + + 'countrydefault' => scalar($conf->config('countrydefault')) || 'US', + + 'refnum' => scalar($conf->config('signup_server-default_refnum')), + + 'default_pkgpart' => scalar($conf->config('signup_server-default_pkgpart')), + + 'signup_service' => $svc_x, + 'default_svcpart' => scalar($conf->config('signup_server-default_svcpart')), + + 'head' => join("\n", $conf->config('selfservice-head') ), + 'body_header' => join("\n", $conf->config('selfservice-body_header') ), + 'body_footer' => join("\n", $conf->config('selfservice-body_footer') ), + 'body_bgcolor' => scalar( $conf->config('selfservice-body_bgcolor') ), + 'box_bgcolor' => scalar( $conf->config('selfservice-box_bgcolor') ), + + 'company_name' => scalar($conf->config('company_name')), + + #per-agent? + 'agent_ship_address' => scalar($conf->exists('agent-ship_address')), + + 'no_company' => scalar($conf->exists('signup-no_company')), + 'require_phone' => scalar($conf->exists('cust_main-require_phone')), + 'recommend_daytime' => scalar($conf->exists('signup-recommend_daytime')), + 'recommend_email' => scalar($conf->exists('signup-recommend_email')), + + }; + + $cache->set('signup_info_cache', $signup_info_cache); + + } + + my $signup_info = { %$signup_info_cache }; + warn "$me signup info loaded\n" if $DEBUG > 1; + warn Dumper($signup_info). "\n" if $DEBUG > 2; + + my @addl = qw( signup_server-classnum2 signup_server-classnum3 ); + + if ( grep { $conf->exists($_) } @addl ) { + + $signup_info->{optional_packages} = []; + + foreach my $addl ( @addl ) { + + warn "$me adding optional package info\n" if $DEBUG > 1; + + my $classnum = $conf->config($addl) or next; + + my @pkgs = map { { + 'freq_pretty' => $_->freq_pretty, + 'options' => { $_->options }, + %{ $_->hashref } + }; + } + qsearch( 'part_pkg', { classnum => $classnum } ); + + push @{$signup_info->{optional_packages}}, \@pkgs; + + warn "$me done adding opt. package info for $classnum\n" if $DEBUG > 1; + + } + + } + + my $agentnum = $packet->{'agentnum'} + || $conf->config('signup_server-default_agentnum'); + $agentnum =~ /^(\d*)$/ or die "illegal agentnum"; + $agentnum = $1; + + my $session = ''; + if ( exists $packet->{'session_id'} ) { + + warn "$me loading agent session\n" if $DEBUG > 1; + my $cache = new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::Agent', + } ); + $session = $cache->get($packet->{'session_id'}); + if ( $session ) { + $agentnum = $session->{'agentnum'}; + } else { + return { 'error' => "Can't resume session" }; #better error message + } + warn "$me done loading agent session\n" if $DEBUG > 1; + + } elsif ( exists $packet->{'customer_session_id'} ) { + + warn "$me loading customer session\n" if $DEBUG > 1; + my $cache = new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::MyAccount', + } ); + $session = $cache->get($packet->{'customer_session_id'}); + if ( $session ) { + my $custnum = $session->{'custnum'}; + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum }); + return { 'error' => "Can't find your customer record" } unless $cust_main; + $agentnum = $cust_main->agentnum; + } else { + return { 'error' => "Can't resume session" }; #better error message + } + warn "$me done loading customer session\n" if $DEBUG > 1; + + } + + $signup_info->{'part_pkg'} = []; + + if ( $packet->{'reg_code'} ) { + + warn "$me setting package list via reg_code\n" if $DEBUG > 1; + + $signup_info->{'part_pkg'} = + [ map { { 'payby' => [ $_->payby ], + 'freq_pretty' => $_->freq_pretty, + 'options' => { $_->options }, + %{$_->hashref} + }; + } + grep { $_->svcpart($svc_x) } + map { $_->part_pkg } + qsearchs( 'reg_code', { 'code' => $packet->{'reg_code'}, + 'agentnum' => $agentnum, } ) + + ]; + + $signup_info->{'error'} = 'Unknown registration code' + unless @{ $signup_info->{'part_pkg'} }; + + warn "$me done setting package list via reg_code\n" if $DEBUG > 1; + + } elsif ( $packet->{'promo_code'} ) { + + warn "$me setting package list via promo_code\n" if $DEBUG > 1; + + $signup_info->{'part_pkg'} = + [ map { { 'payby' => [ $_->payby ], + 'freq_pretty' => $_->freq_pretty, + 'options' => { $_->options }, + %{$_->hashref} + } } + grep { $_->svcpart($svc_x) } + qsearch( 'part_pkg', { 'promo_code' => { + op=>'ILIKE', + value=>$packet->{'promo_code'} + }, + 'disabled' => '', } ) + ]; + + $signup_info->{'error'} = 'Unknown promotional code' + unless @{ $signup_info->{'part_pkg'} }; + + warn "$me done setting package list via promo_code\n" if $DEBUG > 1; + } + + if ( $agentnum ) { + + warn "$me setting agent-specific payment flag\n" if $DEBUG > 1; + my $agent = qsearchs('agent', { 'agentnum' => $agentnum } ); + warn "$me has agent $agent\n" if $DEBUG > 1; + if ( $agent ) { #else complain loudly? + $signup_info->{'hide_payment_fields'} = []; + foreach my $payby (@{$signup_info->{payby}}) { + warn "$me checking $payby payment fields\n" if $DEBUG > 1; + my $hide = 0; + if ( FS::payby->realtime($payby) ) { + my $payment_gateway = + $agent->payment_gateway( 'method' => FS::payby->payby2bop($payby), + 'nofatal' => 1, + ); + if ( $payment_gateway + && $payment_gateway->gateway_namespace + eq 'Business::OnlineThirdPartyPayment' + ) { + warn "$me hiding $payby payment fields\n" if $DEBUG > 1; + $hide = 1; + } + } + push @{$signup_info->{'hide_payment_fields'}}, $hide; + } + } + warn "$me done setting agent-specific payment flag\n" if $DEBUG > 1; + + warn "$me setting agent-specific package list\n" if $DEBUG > 1; + $signup_info->{'part_pkg'} = $signup_info->{'agentnum2part_pkg'}{$agentnum} + unless @{ $signup_info->{'part_pkg'} }; + warn "$me done setting agent-specific package list\n" if $DEBUG > 1; + + warn "$me setting agent-specific adv. source list\n" if $DEBUG > 1; + $signup_info->{'part_referral'} = + [ + map { $_->hashref } + qsearch( { + 'table' => 'part_referral', + 'hashref' => { 'disabled' => '' }, + 'extra_sql' => "AND ( agentnum = $agentnum ". + " OR agentnum IS NULL ) ", + }, + ) + ]; + warn "$me done setting agent-specific adv. source list\n" if $DEBUG > 1; + + $signup_info->{'agent_name'} = $agent->agent; + + $signup_info->{'company_name'} = $conf->config('company_name', $agentnum); + + if ( $signup_info->{'agent_ship_address'} && $agent->agent_custnum ) { + my $cust_main = $agent->agent_cust_main; + my $prefix = length($cust_main->ship_last) ? 'ship_' : ''; + $signup_info->{"ship_$_"} = $cust_main->get("$prefix$_") + foreach qw( address1 city county state zip country ); + } + + #some of the above could probably be cached, too + + my $signup_info_cache_agent = $cache->get("signup_info_cache_agent$agentnum"); + + if ( $signup_info_cache_agent ) { + + warn "$me loading cached signup info for agentnum $agentnum\n" + if $DEBUG > 1; + + } else { + + warn "$me populating signup info cache for agentnum $agentnum\n" + if $DEBUG > 1; + + $signup_info_cache_agent = { + #( map { $_ => scalar( $conf->config($_, $agentnum) ) } + # qw( company_name ) ), + ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) } + qw( body_bgcolor box_bgcolor) ), + ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) } + qw( head body_header body_footer ) ), + }; + + $cache->set("signup_info_cache_agent$agentnum", $signup_info_cache_agent); + + } + + $signup_info->{$_} = $signup_info_cache_agent->{$_} + foreach keys %$signup_info_cache_agent; + + } + # else { + # delete $signup_info->{'part_pkg'}; + #} + + warn "$me sorting package list\n" if $DEBUG > 1; + $signup_info->{'part_pkg'} = [ sort { $a->{pkg} cmp $b->{pkg} } # case? + @{ $signup_info->{'part_pkg'} } + ]; + warn "$me done sorting package list\n" if $DEBUG > 1; + + if ( exists $packet->{'session_id'} ) { + my $agent_signup_info = { %$signup_info }; + delete $agent_signup_info->{agentnum2part_pkg}; + $agent_signup_info->{'agent'} = $session->{'agent'}; + $agent_signup_info; + } else { + $signup_info; + } + +} + +sub domain_select_hash { + my $packet = shift; + + my $response = {}; + + if ($packet->{pkgpart}) { + my $part_pkg = qsearchs('part_pkg' => { 'pkgpart' => $packet->{pkgpart} } ); + #$packet->{svcpart} = $part_pkg->svcpart('svc_acct') + $packet->{svcpart} = $part_pkg->svcpart + if $part_pkg; + } + + if ($packet->{svcpart}) { + my $part_svc = qsearchs('part_svc' => { 'svcpart' => $packet->{svcpart} } ); + $response->{'domsvc'} = $part_svc->part_svc_column('domsvc')->columnvalue + if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D'); + } + + $response->{'domains'} + = { domain_select_hash FS::svc_acct( map { $_ => $packet->{$_} } + qw(svcpart pkgnum) + ) }; + + $response; +} + +sub new_customer { + my $packet = shift; + + my $conf = new FS::Conf; + my $svc_x = $conf->config('signup_server-service') || 'svc_acct'; + + if ( $svc_x eq 'svc_acct' ) { + + #things that aren't necessary in base class, but are for signup server + #return "Passwords don't match" + # if $hashref->{'_password'} ne $hashref->{'_password2'} + return { 'error' => gettext('empty_password') } + unless length($packet->{'_password'}); + # a bit inefficient for large numbers of pops + return { 'error' => gettext('no_access_number_selected') } + unless $packet->{'popnum'} || !scalar(qsearch('svc_acct_pop',{} )); + + } + + my $agentnum; + if ( exists $packet->{'session_id'} ) { + my $cache = new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::Agent', + } ); + my $session = $cache->get($packet->{'session_id'}); + if ( $session ) { + $agentnum = $session->{'agentnum'}; + } else { + return { 'error' => "Can't resume session" }; #better error message + } + } else { + $agentnum = $packet->{agentnum} + || $conf->config('signup_server-default_agentnum'); + } + + #shares some stuff with htdocs/edit/process/cust_main.cgi... take any + # common that are still here and library them. + my $cust_main = new FS::cust_main ( { + #'custnum' => '', + 'agentnum' => $agentnum, + 'refnum' => $packet->{refnum} + || $conf->config('signup_server-default_refnum'), + + map { $_ => $packet->{$_} } qw( + + last first ss company address1 address2 + city county state zip country + daytime night fax stateid stateid_state + + ship_last ship_first ship_ss ship_company ship_address1 ship_address2 + ship_city ship_county ship_state ship_zip ship_country + ship_daytime ship_night ship_fax + + payby + payinfo paycvv paydate payname paystate paytype + paystart_month paystart_year payissue + payip + + referral_custnum comments + ) + + } ); + + my $agent = qsearchs('agent', { 'agentnum' => $agentnum } ); + if ( $conf->exists('agent_ship_address') && $agent->agent_custnum ) { + my $agent_cust_main = $agent->agent_cust_main; + my $prefix = length($agent_cust_main->ship_last) ? 'ship_' : ''; + $cust_main->set("ship_$_", $agent_cust_main->get("$prefix$_") ) + foreach qw( address1 city county state zip country ); + + $cust_main->set("ship_$_", $cust_main->get($_)) + foreach qw( last first ); + + } + + + return { 'error' => "Illegal payment type" } + unless grep { $_ eq $packet->{'payby'} } + $conf->config('signup_server-payby'); + + if (FS::payby->realtime($packet->{payby})) { + my $payby = $packet->{payby}; + + my $agent = qsearchs('agent', { 'agentnum' => $agentnum }); + return { 'error' => "Unknown reseller" } + unless $agent; + + my $gw = $agent->payment_gateway( 'method' => FS::payby->payby2bop($payby), + 'nofatal' => 1, + ); + + $cust_main->payby('BILL') # MCRD better? + if $gw && $gw->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; + } + + $cust_main->payinfo($cust_main->daytime) + if $cust_main->payby eq 'LECB' && ! $cust_main->payinfo; + + my @invoicing_list = $packet->{'invoicing_list'} + ? split( /\s*\,\s*/, $packet->{'invoicing_list'} ) + : (); + + $packet->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/; + my $pkgpart = $1; + return { 'error' => 'Please select a package' } unless $pkgpart; #msgcat + + my $part_pkg = + qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } ) + or return { 'error' => "WARNING: unknown pkgpart: $pkgpart" }; + my $svcpart = $part_pkg->svcpart($svc_x); + + my $reg_code = ''; + if ( $packet->{'reg_code'} ) { + $reg_code = qsearchs( 'reg_code', { 'code' => $packet->{'reg_code'}, + 'agentnum' => $agentnum, } ) + or return { 'error' => 'Unknown registration code' }; + } + + my $cust_pkg = new FS::cust_pkg ( { + #later#'custnum' => $custnum, + 'pkgpart' => $packet->{'pkgpart'}, + 'promo_code' => $packet->{'promo_code'}, + 'reg_code' => $packet->{'reg_code'}, + } ); + #my $error = $cust_pkg->check; + #return { 'error' => $error } if $error; + + #should be all auto-magic and shit + my @svc = (); + if ( $svc_x eq 'svc_acct' ) { + + my $svc = new FS::svc_acct { + 'svcpart' => $svcpart, + map { $_ => $packet->{$_} } + qw( username _password sec_phrase popnum ), + }; + + my @acct_snarf; + my $snarfnum = 1; + while ( exists($packet->{"snarf_machine$snarfnum"}) + && length($packet->{"snarf_machine$snarfnum"}) ) { + my $acct_snarf = new FS::acct_snarf ( { + 'machine' => $packet->{"snarf_machine$snarfnum"}, + 'protocol' => $packet->{"snarf_protocol$snarfnum"}, + 'username' => $packet->{"snarf_username$snarfnum"}, + '_password' => $packet->{"snarf_password$snarfnum"}, + } ); + $snarfnum++; + push @acct_snarf, $acct_snarf; + } + $svc->child_objects( \@acct_snarf ); + + push @svc, $svc; + + } elsif ( $svc_x eq 'svc_phone' ) { + + my $svc = new FS::svc_phone ( { + 'svcpart' => $svcpart, + map { $_ => $packet->{$_} } + qw( countrycode phonenum sip_password pin ), + } ); + + push @svc, $svc; + + } else { + die "unknown signup service $svc_x"; + } + my $y = $svc[0]->setdefault; # arguably should be in new method + return { 'error' => $y } if $y && !ref($y); + + if ($packet->{'mac_addr'} && $conf->exists('signup_server-mac_addr_svcparts')) + { + + my %mac_addr_svcparts = map { $_ => 1 } + $conf->config('signup_server-mac_addr_svcparts'); + my @pkg_svc = grep { $_->quantity && $mac_addr_svcparts{$_->svcpart} } + $cust_pkg->part_pkg->pkg_svc; + + return { 'error' => 'No service defined to assign mac address' } + unless @pkg_svc; + + my $svc = new FS::svc_acct { + 'svcpart' => $pkg_svc[0]->svcpart, #multiple matches? alas.. + 'username' => $packet->{'mac_addr'}, + '_password' => '', #blank as requested (set passwordmin to 0) + }; + + my $y = $svc->setdefault; # arguably should be in new method + return { 'error' => $y } if $y && !ref($y); + + push @svc, $svc; + + } + + #$error = $svc->check; + #return { 'error' => $error } if $error; + + #setup a job dependancy to delay provisioning + my $placeholder = new FS::queue ( { + 'job' => 'FS::ClientAPI::Signup::__placeholder', + 'status' => 'locked', + } ); + my $error = $placeholder->insert; + return { 'error' => $error } if $error; + + use Tie::RefHash; + tie my %hash, 'Tie::RefHash'; + %hash = ( $cust_pkg => \@svc ); + #msgcat + $error = $cust_main->insert( + \%hash, + \@invoicing_list, + 'depend_jobnum' => $placeholder->jobnum, + ); + if ( $error ) { + my $perror = $placeholder->delete; + $error .= " (Additionally, error removing placeholder: $perror)" if $perror; + return { 'error' => $error }; + } + + if ( $conf->exists('signup_server-realtime') ) { + + #warn "[fs_signup_server] Billing customer...\n" if $Debug; + + my $bill_error = $cust_main->bill; + #warn "[fs_signup_server] error billing new customer: $bill_error" + # if $bill_error; + + $bill_error = $cust_main->apply_payments_and_credits; + #warn "[fs_signup_server] error applying payments and credits for". + # " new customer: $bill_error" + # if $bill_error; + + $bill_error = $cust_main->realtime_collect( + method => FS::payby->payby2bop( $packet->{payby} ), + depend_jobnum => $placeholder->jobnum, + ); + #warn "[fs_signup_server] error collecting from new customer: $bill_error" + # if $bill_error; + + if ($bill_error && ref($bill_error) eq 'HASH') { + return { 'error' => '_collect', + ( map { $_ => $bill_error->{$_} } + qw(popup_url reference collectitems) + ), + amount => $cust_main->balance, + }; + } + + if ( $cust_main->balance > 0 ) { + + #this makes sense. credit is "un-doing" the invoice + $cust_main->credit( $cust_main->balance, 'signup server decline', + 'reason_type' => $conf->config('signup_credit_type'), + ); + $cust_main->apply_credits; + + #should check list for errors... + #$cust_main->suspend; + local $FS::svc_Common::noexport_hack = 1; + $cust_main->cancel('quiet'=>1); + + my $perror = $placeholder->depended_delete; + warn "error removing provisioning jobs after decline: $perror" if $perror; + unless ( $perror ) { + $perror = $placeholder->delete; + warn "error removing placeholder after decline: $perror" if $perror; + } + + return { 'error' => '_decline' }; + } + + } + + if ( $reg_code ) { + $error = $reg_code->delete; + return { 'error' => $error } if $error; + } + + $error = $placeholder->delete; + return { 'error' => $error } if $error; + + my %return = ( 'error' => '', + 'signup_service' => $svc_x, + ); + + if ( $svc_x eq 'svc_acct' ) { + $return{$_} = $svc[0]->$_() for qw( username _password ); + } elsif ( $svc_x eq 'svc_phone' ) { + $return{$_} = $svc[0]->$_() for qw( countrycode phonenum sip_password pin ); + } else { + die "unknown signup service $svc_x"; + } + + return \%return; + +} + +sub capture_payment { + my $packet = shift; + + warn "$me capture_payment called on $packet\n" if $DEBUG; + + ### + # identify processor/gateway from called back URL + ### + + my $conf = new FS::Conf; + + my $url = $packet->{url}; + my $payment_gateway = + qsearchs('payment_gateway', { 'gateway_callback_url' => popurl(0, $url) } ); + + unless ($payment_gateway) { + + my ( $processor, $login, $password, $action, @bop_options ) = + $conf->config('business-onlinepayment'); + $action ||= 'normal authorization'; + pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/; + die "No real-time processor is enabled - ". + "did you set the business-onlinepayment configuration value?\n" + unless $processor; + + $payment_gateway = new FS::payment_gateway( { + gateway_namespace => $conf->config('business-onlinepayment-namespace'), + gateway_module => $processor, + gateway_username => $login, + gateway_password => $password, + gateway_action => $action, + options => [ ( @bop_options ) ], + }); + + } + + die "No real-time third party processor is enabled - ". + "did you set the business-onlinepayment configuration value?\n*" + unless $payment_gateway->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; + + ### + # locate pending transaction + ### + + eval "use Business::OnlineThirdPartyPayment"; + die $@ if $@; + + my $transaction = + new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module, + @{ [ $payment_gateway->options ] }, + ); + + my $paypendingnum = $transaction->reference($packet->{data}); + + my $cust_pay_pending = + qsearchs('cust_pay_pending', { paypendingnum => $paypendingnum } ); + + unless ($cust_pay_pending) { + my $bill_error = "No payment is being processed with id $paypendingnum". + "; Transaction aborted."; + return { error => '_decline', bill_error => $bill_error }; + } + + if ($cust_pay_pending->status ne 'pending') { + my $bill_error = "Payment with id $paypendingnum is not pending, but ". + $cust_pay_pending->status. "; Transaction aborted."; + return { error => '_decline', bill_error => $bill_error }; + } + + my $cust_main = $cust_pay_pending->cust_main; + my $bill_error = + $cust_main->realtime_botpp_capture( $cust_pay_pending, %{$packet->{data}} ); + + return { 'error' => ( $bill_error->{bill_error} ? '_decline' : '' ), + %$bill_error, + }; + +} + +1; diff --git a/FS/FS/ClientAPI/passwd.pm b/FS/FS/ClientAPI/passwd.pm new file mode 100644 index 0000000..b22d761 --- /dev/null +++ b/FS/FS/ClientAPI/passwd.pm @@ -0,0 +1,46 @@ +package FS::ClientAPI::passwd; + +use strict; +use FS::Record qw(qsearchs); +use FS::svc_acct; +use FS::svc_domain; + +sub passwd { + my $packet = shift; + + my $domain = $FS::ClientAPI::domain || $packet->{'domain'}; + my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } ) + or return { error => "Domain $domain not found" }; + + my $old_password = $packet->{'old_password'}; + my $new_password = $packet->{'new_password'}; + my $new_gecos = $packet->{'new_gecos'}; + my $new_shell = $packet->{'new_shell'}; + + #false laziness w/FS::ClientAPI::MyAccount::login + + my $svc_acct = qsearchs( 'svc_acct', { 'username' => $packet->{'username'}, + 'domsvc' => $svc_domain->svcnum, } + ); + return { error => 'User not found.' } unless $svc_acct; + return { error => 'Incorrect password.' } + unless $svc_acct->check_password($old_password); + + my %hash = $svc_acct->hash; + my $new_svc_acct = new FS::svc_acct ( \%hash ); + $new_svc_acct->setfield('_password', $new_password ) + if $new_password && $new_password ne $old_password; + $new_svc_acct->setfield('finger',$new_gecos) if $new_gecos; + $new_svc_acct->setfield('shell',$new_shell) if $new_shell; + my $error = $new_svc_acct->replace($svc_acct); + + return { error => $error }; + +} + +sub chfn {} + +sub chsh {} + +1; + |