X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2FClientAPI%2FMyAccount.pm;h=29e374c930abdd7eaba46af3769cf45fc8c8c297;hp=6e4545aee9768067025ba801b94c627282341e5d;hb=d7eafc9c9aae2bf47ea19a56cc5bb1380c5874e4;hpb=e8a09e945986a32f9b7d0a5d546142ada91654ca diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 6e4545aee..29e374c93 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -1,22 +1,27 @@ package FS::ClientAPI::MyAccount; +use 5.008; #require 5.8+ for Time::Local 1.05+ use strict; -use vars qw($cache); -use subs qw(_cache); +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 FS::CGI qw(small_custview); #doh +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; +use FS::UI::bytecount qw( display_bytecount ); use FS::Conf; -use FS::Record qw(qsearch qsearchs); +#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; @@ -26,14 +31,10 @@ use FS::cust_pkg; use FS::payby; use FS::acct_rt_transaction; use HTML::Entities; +use FS::TicketSystem; -#false laziness with FS::cust_main -BEGIN { - eval "use Time::Local;"; - die "Time::Local minimum version 1.05 required with Perl versions before 5.6" - if $] < 5.006 && !defined($Time::Local::VERSION); - eval "use Time::Local qw(timelocal_nocheck);"; -} +$DEBUG = 0; +$me = '[FS::ClientAPI::MyAccount]'; use vars qw( @cust_main_editable_fields ); @cust_main_editable_fields = qw( @@ -42,47 +43,167 @@ use vars qw( @cust_main_editable_fields ); 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 ); -use subs qw(_provision); - 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 $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } ) - or return { error => 'Domain '. $p->{'domain'}. ' not found' }; + my $conf = new FS::Conf; - my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{'username'}, - 'domsvc' => $svc_domain->svcnum, } - ); - return { error => 'User not found.' } unless $svc_acct; + my $svc_x = ''; + if ( $p->{'domain'} eq 'svc_phone' + && $conf->exists('selfservice_server-phone_login') ) { - my $conf = new FS::Conf; - 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 $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 password.' } - unless $svc_acct->check_password($p->{'password'}); + 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_acct->svcnum, + 'svcnum' => $svc_x->svcnum, }; - my $cust_pkg = $svc_acct->cust_svc->cust_pkg; + 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(). $$)) @@ -100,12 +221,61 @@ sub logout { my $p = shift; if ( $p->{'session_id'} ) { _cache->remove($p->{'session_id'}); - return { 'error' => '' }; + return { %{ skin_info($p) }, 'error' => '' }; } else { - return { 'error' => "Can't resume session" }; #better error message + 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} } + ]; + + $info->{'self_suspend_reason'} = + $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum); + + return { %$info, + 'custnum' => $custnum, + 'access_pkgnum' => $session->{'pkgnum'}, + 'access_svcnum' => $session->{'svcnum'}, + }; +} + sub customer_info { my $p = shift; @@ -113,6 +283,14 @@ sub customer_info { 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 }; @@ -120,22 +298,30 @@ sub customer_info { my $cust_main = qsearchs('cust_main', $search ) or return { 'error' => "unknown custnum $custnum" }; - $return{balance} = $cust_main->balance; + if ( $session->{'pkgnum'} ) { + $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} ); + } else { + $return{balance} = $cust_main->balance; + } $return{tickets} = [ ($cust_main->tickets) ]; - my @open = map { - { - invnum => $_->invnum, - date => time2str("%b %o, %Y", $_->_date), - owed => $_->owed, - }; - } $cust_main->open_cust_bill; - $return{open_invoices} = \@open; + 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; + } - my $conf = new FS::Conf; $return{small_custview} = - small_custview( $cust_main, $conf->config('countrydefault') ); + small_custview( $cust_main, + scalar($conf->config('countrydefault')), + ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance + ); $return{name} = $cust_main->first. ' '. $cust_main->get('last'); @@ -174,6 +360,11 @@ sub customer_info { $return{support_services} = \@support_services; } + if ( $conf->config('prepayment_discounts-credit_type') ) { + #need to eval? + $return{discount_terms_hash} = { $cust_main->discount_terms_hash }; + } + } elsif ( $session->{'svcnum'} ) { #no customer record my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } ) @@ -208,13 +399,47 @@ sub edit_info { $new->set( $_ => $p->{$_} ) foreach grep { exists $p->{$_} } @cust_main_editable_fields; - if ( $p->{'payby'} =~ /^(CARD|DCRD)$/ ) { + 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->paycvv($p->{'paycvv'}); + $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; @@ -242,15 +467,21 @@ sub payment_info { #generic ## + my $conf = new FS::Conf; 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 @@ -265,12 +496,17 @@ sub payment_info { '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'), }; } @@ -286,7 +522,21 @@ sub payment_info { my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) or return { 'error' => "unknown custnum $custnum" }; - $return{balance} = $cust_main->balance; + $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') ); @@ -313,6 +563,11 @@ sub payment_info { } + if ( $conf->config('prepayment_discounts-credit_type') ) { + #need to eval? + $return{discount_terms_hash} = { $cust_main->discount_terms_hash }; + } + #doubleclick protection my $_date = time; $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; @@ -339,6 +594,15 @@ sub process_payment { 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->{'discount_term'} =~ /^\s*(\d*)\s*$/ + or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} }; + my $discount_term = $1; + $p->{'payname'} =~ /^([\w \,\.\-\']+)$/ or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} }; my $payname = $1; @@ -347,10 +611,12 @@ sub process_payment { 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' ) { @@ -369,18 +635,21 @@ sub process_payment { } elsif ( $payby eq 'CARD' || $payby eq 'DCRD' ) { $payinfo = $p->{'payinfo'}; - $payinfo =~ s/[^\dx]//g; - $payinfo =~ /^(\d{13,16})$/ - or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo - $payinfo = $1; + #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 cardtype($payinfo) eq "Unknown"; + if $payinfo !~ /^99\d{14}$/ && cardtype($payinfo) eq "Unknown"; if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) { if ( cardtype($payinfo) eq 'American Express card' ) { @@ -399,17 +668,20 @@ sub process_payment { } my %payby2fields = ( - 'CARD' => [ qw( paystart_month paystart_year payissue address1 address2 city state zip payip ) ], + '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}, $p->{'amount'}, + 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, + 'paybatch' => $paybatch, #this doesn't actually do anything 'paycvv' => $paycvv, + 'pkgnum' => $session->{'pkgnum'}, + 'discount_term' => $discount_term, map { $_ => $p->{$_} } @{ $payby2fields{$payby} } ); return { 'error' => $error } if $error; @@ -421,25 +693,64 @@ sub process_payment { if ($payby eq 'CARD' || $payby eq 'DCRD') { $new->set( $_ => $p->{$_} ) foreach qw( payname paystart_month paystart_year payissue payip - address1 address2 city state zip payinfo ); + 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( 'payinfo' => $payinfo ); $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); - return { 'error' => $error } if $error; - $cust_main = $new; + 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; @@ -449,6 +760,15 @@ sub process_payment_order_pkg { 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; @@ -506,7 +826,7 @@ sub invoice { return { 'error' => '', 'invnum' => $invnum, 'invoice_text' => join('', $cust_bill->print_text ), - 'invoice_html' => $cust_bill->print_html, + 'invoice_html' => $cust_bill->print_html( { unsquelch_cdr => 1 } ), }; } @@ -517,7 +837,14 @@ sub invoice_logo { #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 $templatename = $p->{'templatename'}; + 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 @@ -531,7 +858,7 @@ sub invoice_logo { my $filename = "logo$templatename.png"; return { 'error' => '', - 'logo' => $conf->config_binary($filename), + 'logo' => $conf->config_binary($filename, $agentnum), 'content_type' => 'image/png', #should allow gif, jpg too }; } @@ -634,41 +961,69 @@ sub list_svcs { 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 } - @cust_svc = grep { $_->part_svc->svcdb eq $p->{'svcdb'} } @cust_svc - if $p->{'svcdb'}; + 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; { - #no#'svcnum' => $session->{'svcnum'}, + 'svcnum' => $session->{'svcnum'}, 'custnum' => $custnum, - 'svcs' => [ map { - my $svc_x = $_->svc_x; - my($label, $value) = $_->label; - my $part_pkg = $svc_x->cust_svc->cust_pkg->part_pkg; - - { 'svcnum' => $_->svcnum, - 'label' => $label, - 'value' => $value, - 'username' => $svc_x->username, - 'email' => $svc_x->email, - 'seconds' => $svc_x->seconds, - 'upbytes' => FS::UI::bytecount::display_bytecount($svc_x->upbytes), - 'downbytes' => FS::UI::bytecount::display_bytecount($svc_x->downbytes), - 'totalbytes'=> FS::UI::bytecount::display_bytecount($svc_x->totalbytes), - 'recharge_amount' => $part_pkg->option('recharge_amount', 1), - 'recharge_seconds' => $part_pkg->option('recharge_seconds', 1), - 'recharge_upbytes' => FS::UI::bytecount::display_bytecount($part_pkg->option('recharge_upbytes', 1)), - 'recharge_downbytes' => FS::UI::bytecount::display_bytecount($part_pkg->option('recharge_downbytes', 1)), - 'recharge_totalbytes' => FS::UI::bytecount::display_bytecount($part_pkg->option('recharge_totalbytes', 1)), - # more... - }; - } - @cust_svc - ], + '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 + ], }; } @@ -678,9 +1033,8 @@ sub _list_svc_usage { my @usage = (); foreach my $part_export ( map { qsearch ( 'part_export', { 'exporttype' => $_ } ) } - qw (sqlradius sqlradius_withdomain') + qw( sqlradius sqlradius_withdomain ) ) { - push @usage, @ { $part_export->usage_sessions($begin, $end, $svc_acct) }; } (@usage); @@ -713,29 +1067,50 @@ 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) = (shift,shift); + 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 $svc_acct = qsearchs ( 'svc_acct', $search ); + + my $svcdb = $opt{'svcdb'} || 'svc_acct'; + + my $svc_x = qsearchs( $svcdb, $search ); return { 'error' => 'No service selected in list_svc_usage' } - unless $svc_acct; + unless $svc_x; + + my $header = $svcdb eq 'svc_phone' + ? [ split(',', FS::cdr::invoice_header('default') ) ] #XXX + : []; - my $freq = $svc_acct->cust_svc->cust_pkg->part_pkg->freq; - my $start = $svc_acct->cust_svc->cust_pkg->setup; - #my $end = $svc_acct->cust_svc->cust_pkg->bill; # or time? - my $end = time; + 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} = $svc_acct->cust_svc->cust_pkg->last_bill; - $p->{ending} = $end; + unless ( $p->{beginning} ) { + $p->{beginning} = $cust_pkg->last_bill; + $p->{ending} = $end; } - my (@usage) = &$callback($svc_acct,$p->{beginning},$p->{ending}); + 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 @@ -778,6 +1153,7 @@ sub _usage_details { 'ending' => $p->{ending}, 'previous' => ($previous > $start) ? $previous : $start, 'next' => ($next < $end) ? $next : $end, + 'header' => $header, 'usage' => \@usage, }; } @@ -821,7 +1197,9 @@ sub order_pkg { 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 ) ], + 'svc_pbx' => [ qw( id name ) ], ); my $svc_x = "FS::$svcdb"->new( { @@ -878,7 +1256,9 @@ sub order_pkg { $cust_pkg->reexport; } - return { error => '', pkgnum => $cust_pkg->pkgnum }; + my $svcnum = $svc[0] ? $svc[0]->svcnum : ''; + + return { error=>'', pkgnum=>$cust_pkg->pkgnum, svcnum=>$svcnum }; } @@ -985,7 +1365,7 @@ sub _do_bop_realtime { my $bill_error = $cust_main->bill || $cust_main->apply_payments_and_credits - || $cust_main->collect('realtime' => 1); + || $cust_main->realtime_collect; if ( $cust_main->balance > $old_balance && $cust_main->balance > 0 @@ -1005,6 +1385,103 @@ sub _do_bop_realtime { ''; } +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 suspend_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 $conf = new FS::Conf; + my $reasonnum = + $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum) + or return { 'error' => 'Permission denied' }; + + my $pkgnum = $p->{'pkgnum'}; + + my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum, + 'pkgnum' => $pkgnum, } ) + or return { 'error' => "unknown pkgnum $pkgnum" }; + + my $error = $cust_pkg->suspend(reason => $reasonnum); + return { 'error' => $error }; + +} + sub cancel_pkg { my $p = shift; my $session = _cache->get($p->{'session_id'}) @@ -1021,22 +1498,33 @@ sub cancel_pkg { 'pkgnum' => $pkgnum, } ) or return { 'error' => "unknown pkgnum $pkgnum" }; - my $error = $cust_pkg->cancel( 'quiet'=>1 ); + 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)], - [qw(username _password)], + [qw(username _password domsvc)], + [qw(username _password domsvc)], $p, @_ ); @@ -1055,6 +1543,8 @@ sub provision_external { 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'; @@ -1066,27 +1556,42 @@ sub _provision { 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; - $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum }) - unless $error; - return { 'svc' => $part_svc->svc, - 'error' => $error, - map { $_ => $svc_x->get($_) } @$return_fields - }; + 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; } @@ -1206,6 +1711,43 @@ sub myaccount_passwd { } +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 { @@ -1230,6 +1772,7 @@ sub _custoragent_session_custnum { $custnum = $p->{'custnum'}; } else { + $context = 'error'; return ( 'error' => "Can't resume session" ); #better error message }