X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2FClientAPI%2FMyAccount.pm;h=71e47b56743398a53a01522111a4b90c46d3640a;hp=d619e8493893b9af307e8c52a6232cabd192b092;hb=2a96f49fe4d847b711b24862bc5ec062e25d6e88;hpb=7353f670ef61e617a9dfb6c258907eb70e0345f6 diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index d619e8493..71e47b567 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -4,12 +4,16 @@ 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 IO::Scalar; 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 Business::CreditCard; +use HTML::Entities; +use Text::CSV_XS; +use Spreadsheet::WriteExcel; use FS::UI::Web::small_custview qw(small_custview); #less doh use FS::UI::Web; use FS::UI::bytecount qw( display_bytecount ); @@ -19,23 +23,24 @@ use FS::Record qw(qsearch qsearchs dbh); use FS::Msgcat qw(gettext); use FS::Misc qw(card_types); use FS::Misc::DateTime qw(parse_datetime); +use FS::TicketSystem; use FS::ClientAPI_SessionCache; +use FS::cust_svc; use FS::svc_acct; use FS::svc_domain; use FS::svc_phone; use FS::svc_external; +use FS::svc_dsl; +use FS::dsl_device; use FS::part_svc; use FS::cust_main; use FS::cust_bill; +use FS::legacy_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; -use Text::CSV_XS; -use IO::Scalar; -use Spreadsheet::WriteExcel; +use FS::msg_template; $DEBUG = 0; $me = '[FS::ClientAPI::MyAccount]'; @@ -43,13 +48,23 @@ $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 + county state zip country + daytime night fax mobile ship_first ship_last ship_company ship_address1 ship_address2 ship_city - ship_state ship_zip ship_country ship_daytime ship_night ship_fax + ship_state ship_zip ship_country + ship_daytime ship_night ship_fax ship_mobile + locale payby payinfo payname paystart_month paystart_year payissue payip ss paytype paystate stateid stateid_state ); +BEGIN { #preload to reduce time customer_info takes + if ( $FS::TicketSystem::system ) { + warn "$me: initializing ticket system\n" if $DEBUG; + FS::TicketSystem->init(); + } +} + sub _cache { $cache ||= new FS::ClientAPI_SessionCache( { 'namespace' => 'FS::ClientAPI::MyAccount', @@ -74,7 +89,7 @@ sub skin_info { or die "no agentnum for custnum $custnum"; #} elsif ( $context eq 'agent' ) { - } elsif ( $p->{'agentnum'} =~ /^(\d+)$/ ) { + } elsif ( defined($p->{'agentnum'}) and $p->{'agentnum'} =~ /^(\d+)$/ ) { $agentnum = $1; } @@ -97,7 +112,7 @@ sub skin_info { $skin_info_cache_agent = { 'agentnum' => $agentnum, ( map { $_ => scalar( $conf->config($_, $agentnum) ) } - qw( company_name ) ), + qw( company_name date_format ) ), ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) } qw( body_bgcolor box_bgcolor text_color link_color vlink_color hlink_color alink_color @@ -238,6 +253,25 @@ sub logout { } } +sub switch_acct { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $svc_acct = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct' ) + or return { 'error' => "Service not found" }; + + $session->{'svcnum'} = $svc_acct->svcnum; + + my $conf = new FS::Conf; + my $timeout = $conf->config('selfservice-session_timeout') || '1 hour'; + _cache->set( $p->{'session_id'}, $session, $timeout ); + + return { 'error' => '' }; + +} + sub payment_gateway { # internal use only # takes a cust_main and a cust_payby entry, returns the payment_gateway @@ -295,6 +329,10 @@ sub access_info { $info->{'self_suspend_reason'} = $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum); + $info->{'edit_ticket_subject'} = + $conf->exists('ticket_system-selfservice_edit_subject') && + $cust_main->edit_subject; + return { %$info, 'custnum' => $custnum, 'access_pkgnum' => $session->{'pkgnum'}, @@ -316,7 +354,12 @@ sub customer_info { }else{ $return{'require_address2'} = ''; } - + + if ( $FS::TicketSystem::system ) { + warn "$me customer_info: initializing ticket system\n" if $DEBUG; + FS::TicketSystem->init(); + } + if ( $custnum ) { #customer record my $search = { 'custnum' => $custnum }; @@ -330,7 +373,25 @@ sub customer_info { $return{balance} = $cust_main->balance; } - $return{tickets} = [ ($cust_main->tickets) ]; + my @tickets = $cust_main->tickets; + # unavoidable false laziness w/ httemplate/view/cust_main/tickets.html + if ( FS::TicketSystem->selfservice_priority ) { + my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1; + $return{tickets} = [ + sort { + ( + ($a->{'_selfservice_priority'} eq '') <=> + ($b->{'_selfservice_priority'} eq '') + ) || + ( $dir * + ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'}) + ) + } @tickets + ]; + } + else { + $return{tickets} = \@tickets; + } unless ( $session->{'pkgnum'} ) { my @open = map { @@ -350,6 +411,7 @@ sub customer_info { ); $return{name} = $cust_main->first. ' '. $cust_main->get('last'); + $return{ship_name} = $cust_main->ship_first. ' '. $cust_main->get('ship_last'); for (@cust_main_editable_fields) { $return{$_} = $cust_main->get($_); @@ -391,6 +453,12 @@ sub customer_info { $return{discount_terms_hash} = { $cust_main->discount_terms_hash }; } + if ( $session->{'svcnum'} ) { + my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} }); + $return{'svc_label'} = ($cust_svc->label)[1] if $cust_svc; + $return{'svcnum'} = $session->{'svcnum'}; + } + } elsif ( $session->{'svcnum'} ) { #no customer record my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } ) @@ -410,6 +478,72 @@ sub customer_info { } +sub customer_info_short { + 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 ( $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" }; + + $return{small_custview} = + small_custview( $cust_main, + scalar($conf->config('countrydefault')), + 1, ##nobalance + ); + + $return{name} = $cust_main->first. ' '. $cust_main->get('last'); + $return{ship_name} = $cust_main->ship_first. ' '. $cust_main->get('ship_last'); + + $return{payby} = $cust_main->payby; + + #none of these are terribly expensive if we want 'em... + 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 ( $session->{'svcnum'} ) { + my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} }); + $return{'svc_label'} = ($cust_svc->label)[1] if $cust_svc; + $return{'svcnum'} = $session->{'svcnum'}; + } + + } elsif ( $session->{'svcnum'} ) { #no customer record + + #uuh, not supproted yet... die? + return { 'error' => 'customer_info_short not yet supported as agent' }; + + } 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'}) @@ -450,7 +584,7 @@ sub edit_info { $p->{'payinfo1'} =~ /^([\dx]+)$/ or return { 'error' => "illegal account number ". $p->{'payinfo1'} }; my $payinfo1 = $1; - $p->{'payinfo2'} =~ /^([\dx]+)$/ + $p->{'payinfo2'} =~ /^([\dx\.]+)$/ # . turned on by -require-bank-branch? or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} }; my $payinfo2 = $1; $payinfo = $payinfo1. '@'. $payinfo2; @@ -533,6 +667,8 @@ sub payment_info { 'show_paystate' => $conf->exists('show_bankstate'), 'save_unchecked' => $conf->exists('selfservice-save_unchecked'), + + 'credit_card_surcharge_percentage' => $conf->config('credit-card-surcharge-percentage'), }; } @@ -595,19 +731,17 @@ sub payment_info { %return, }; -}; +} #some false laziness with httemplate/process/payment.cgi - look there for #ACH and CVV support stuff -sub process_payment { +sub validate_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 } ) @@ -661,7 +795,7 @@ sub process_payment { if $cust_main->paymask eq $payinfo; $payinfo =~ s/\D//g; - $payinfo =~ /^(\d{13,16})$/ + $payinfo =~ /^(\d{13,16}|\d{8,9})$/ or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo $payinfo = $1; @@ -692,37 +826,104 @@ sub process_payment { 'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ], ); + my $card_type = ''; + $card_type = cardtype($payinfo) if $payby eq 'CARD'; + + { + 'cust_main' => $cust_main, #XXX or just custnum?? + 'amount' => $amount, + 'payby' => $payby, + 'payinfo' => $payinfo, + 'paymask' => $cust_main->mask_payinfo( $payby, $payinfo ), + 'card_type' => $card_type, + 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01', + 'paydate_pretty' => $p->{'month'}. ' / '. $p->{'year'}, + 'payname' => $payname, + 'paybatch' => $paybatch, #this doesn't actually do anything + 'paycvv' => $paycvv, + 'payname' => $payname, + 'discount_term' => $discount_term, + 'pkgnum' => $session->{'pkgnum'}, + map { $_ => $p->{$_} } ( @{ $payby2fields{$payby} }, + qw( save auto ), + ) + }; + +} + +sub store_payment { + my $p = shift; + + my $validate = validate_payment($p); + return $validate if $validate->{'error'}; + + my $conf = new FS::Conf; + my $timeout = $conf->config('selfservice-session_timeout') || '1 hour'; #? + _cache->set( 'payment_'.$p->{'session_id'}, $validate, $timeout ); + + +{ map { $_=>$validate->{$_} } + qw( card_type paymask payname paydate_pretty amount ) + }; + +} + +sub process_stored_payment { + my $p = shift; + + my $session_id = $p->{'session_id'}; + + my $payment_info = _cache->get( "payment_$session_id" ) + or return { 'error' => "Can't resume session" }; #better error message + + do_process_payment($payment_info); + +} + +sub process_payment { + my $p = shift; + + my $payment_info = validate_payment($p); + return $payment_info if $payment_info->{'error'}; + + do_process_payment($payment_info); + +} + +sub do_process_payment { + my $validate = shift; + + my $cust_main = $validate->{'cust_main'}; + + my $amount = delete $validate->{'amount'}; + my $paynum = ''; + + my $payby = delete $validate->{'payby'}; + 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'}, - 'discount_term' => $discount_term, + 'quiet' => 1, 'selfservice' => 1, - map { $_ => $p->{$_} } @{ $payby2fields{$payby} } + 'paynum_ref' => \$paynum, + %$validate, ); return { 'error' => $error } if $error; $cust_main->apply_payments; - if ( $p->{'save'} ) { + if ( $validate->{'save'} ) { my $new = new FS::cust_main { $cust_main->hash }; - if ($payby eq 'CARD' || $payby eq 'DCRD') { - $new->set( $_ => $p->{$_} ) + if ($validate->{'payby'} eq 'CARD' || $validate->{'payby'} eq 'DCRD') { + $new->set( $_ => $validate->{$_} ) foreach qw( payname paystart_month paystart_year payissue payip address1 address2 city state zip country ); - $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' ); + $new->set( 'payby' => $validate->{'auto'} ? 'CARD' : 'DCRD' ); } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') { - $new->set( $_ => $p->{$_} ) + $new->set( $_ => $validate->{$_} ) foreach qw( payname payip paytype paystate stateid stateid_state ); - $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' ); + $new->set( 'payby' => $validate->{'auto'} ? 'CHEK' : 'DCHK' ); } - $new->set( 'payinfo' => $cust_main->card_token || $payinfo ); - $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' ); + $new->set( 'payinfo' => $cust_main->card_token || $validate->{'payinfo'} ); + $new->set( 'paydate' => $validate->{'paydate'} ); my $error = $new->replace($cust_main); if ( $error ) { #no, this causes customers to process their payments again @@ -730,18 +931,80 @@ sub process_payment { #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! + delete($validate->{'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"; + "PACKET: ". Dumper($validate)."\n"; #} else { #not needed... #$cust_main = $new; } } - return { 'error' => '' }; + my $cust_pay = ''; + my $receipt_html = ''; + if ($paynum) { + # currently supported for realtime CC only; send receipt data to SS + $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } ); + if($cust_pay) { + $receipt_html = qq! + + + + + + + + + + + + + + + + + + + + + + + + + +
Payment#! . $cust_pay->paynum . qq!
Date! . + time2str("%a %b %o, %Y %r", $cust_pay->_date) + . qq!
Amount! . sprintf('%.2f', $cust_pay->paid) . qq!
Payment method! . $cust_pay->payby_name .' #'. $cust_pay->paymask + . qq!
+!; + } + } + + if ( $cust_pay ) { + + my($gw, $auth, $order) = split(':', $cust_pay->paybatch); + + return { + 'error' => '', + 'amount' => sprintf('%.2f', $cust_pay->paid), + 'date' => $cust_pay->_date, + 'date_pretty' => time2str('%Y-%m-%d', $cust_pay->_date), + 'time_pretty' => time2str('%T', $cust_pay->_date), + 'auth_num' => $auth, + 'order_num' => $order, + 'receipt_html' => $receipt_html, + }; + + } else { + + return { + 'error' => '', + 'receipt_html' => '', + }; + + } } @@ -860,6 +1123,84 @@ sub invoice { } +sub invoice_pdf { + 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_pdf' => $cust_bill->print_pdf({ + 'unsquelch_cdr' => 1, + 'locale' => $p->{'locale'}, + }), + }; + +} + +sub legacy_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 $legacyinvnum = $p->{'legacyinvnum'}; + + my %hash = ( + 'legacyinvnum' => $legacyinvnum, + 'custnum' => $custnum, + ); + + my $legacy_cust_bill = + qsearchs('legacy_cust_bill', { %hash, 'locale' => $p->{'locale'} } ) + || qsearchs('legacy_cust_bill', \%hash ) + or return { 'error' => "Can't find legacyinvnum" }; + + #my %return; + + return { 'error' => '', + 'legacyinvnum' => $legacyinvnum, + 'legacyid' => $legacy_cust_bill->legacyid, + 'invoice_html' => $legacy_cust_bill->content_html, + }; + +} + +sub legacy_invoice_pdf { + 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 $legacyinvnum = $p->{'legacyinvnum'}; + + my $legacy_cust_bill = qsearchs('legacy_cust_bill', { + 'legacyinvnum' => $legacyinvnum, + 'custnum' => $custnum, + }) or return { 'error' => "Can't find legacyinvnum" }; + + #my %return; + + return { 'error' => '', + 'legacyinvnum' => $legacyinvnum, + 'legacyid' => $legacy_cust_bill->legacyid, + 'invoice_pdf' => $legacy_cust_bill->content_pdf, + }; + +} + sub invoice_logo { my $p = shift; @@ -903,15 +1244,55 @@ sub list_invoices { my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) or return { 'error' => "unknown custnum $custnum" }; + my $conf = new FS::Conf; + + my @legacy_cust_bill = $cust_main->legacy_cust_bill; + my @cust_bill = $cust_main->cust_bill; + my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass'); + if ( $hide_taxclass ) { + @cust_bill = grep { my @cust_bill_pkg = $_->cust_bill_pkg; + my @part_pkg= grep $_, map $_->part_pkg, @cust_bill_pkg; + grep { $_->taxclass ne $hide_taxclass } @part_pkg; + } + @cust_bill; + } + + my $balance = 0; + return { 'error' => '', - 'invoices' => [ map { { 'invnum' => $_->invnum, - '_date' => $_->_date, - 'date' => time2str("%b %o, %Y", $_->_date), - } - } @cust_bill - ] + 'balance' => $cust_main->balance, + 'invoices' => [ + map { + my $owed = $_->owed; + $balance += $owed; + +{ 'invnum' => $_->invnum, + '_date' => $_->_date, + 'date' => time2str("%b %o, %Y", $_->_date), + 'date_short' => time2str("%m-%d-%Y", $_->_date), + 'previous' => sprintf('%.2f', ($_->previous)[0]), + 'charged' => sprintf('%.2f', $_->charged), + 'owed' => sprintf('%.2f', $owed), + 'balance' => sprintf('%.2f', $balance), + } + } + @cust_bill + ], + 'legacy_invoices' => [ + map { + +{ 'legacyinvnum' => $_->legacyinvnum, + 'legacyid' => $_->legacyid, + '_date' => $_->_date, + 'date' => time2str("%b %o, %Y", $_->_date), + 'date_short' => time2str("%m-%d-%Y", $_->_date), + 'charged' => sprintf('%.2f', $_->charged), + 'has_content' => ( length($_->content_pdf) + || length($_->content_html) ), + } + } + @legacy_cust_bill + ], }; } @@ -986,8 +1367,10 @@ sub list_pkgs { { 'svcnum' => $session->{'svcnum'}, 'custnum' => $custnum, 'cust_pkg' => [ map { - { $_->hash, + my $primary_cust_svc = $_->primary_cust_svc; + +{ $_->hash, $_->part_pkg->hash, + status => $_->status, part_svc => [ map $_->hashref, $_->available_part_svc ], cust_svc => @@ -998,9 +1381,21 @@ sub list_pkgs { if $context eq 'agent' && $conf->exists('agent-showpasswords') && $_->part_svc->svcdb eq 'svc_acct'; + $ref->{svchash} = { $_->svc_x->hash } if + $_->part_svc->svcdb eq 'svc_phone'; + $ref->{svchash}->{svcpart} = $_->part_svc->svcpart + if $_->part_svc->svcdb eq 'svc_phone'; # hack $ref; } $_->cust_svc ], + primary_cust_svc => + $primary_cust_svc + ? { $primary_cust_svc->hash, + label => [ $primary_cust_svc->label ], + finger => $primary_cust_svc->svc_x->finger, #uuh + $primary_cust_svc->part_svc->hash, + } + : {}, #'' ? }; } $cust_main->ncancelled_pkgs ], @@ -1021,12 +1416,18 @@ sub list_svcs { my $cust_main = qsearchs('cust_main', $search ) or return { 'error' => "unknown custnum $custnum" }; + my $pkgnum = $session->{'pkgnum'} || $p->{'pkgnum'} || ''; + if ( ! $pkgnum && $p->{'svcnum'} ) { + my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $p->{'svcnum'} } ); + $pkgnum = $cust_svc->pkgnum if $cust_svc; + } + 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'}; + next if $pkgnum && $cust_pkg->pkgnum != $pkgnum; push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context } if ( $p->{'svcdb'} ) { @@ -1041,21 +1442,27 @@ sub list_svcs { #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username } # @svc_x; + my $conf = new FS::Conf; + { 'svcnum' => $session->{'svcnum'}, 'custnum' => $custnum, + 'date_format' => $conf->config('date_format') || '%m/%d/%Y', + 'view_usage_nodomain' => $conf->exists('selfservice-view_usage_nodomain'), 'svcs' => [ map { my $svc_x = $_->svc_x; my($label, $value) = $_->label; my $svcdb = $_->part_svc->svcdb; - my $part_pkg = $_->cust_pkg->part_pkg; + my $cust_pkg = $_->cust_pkg; + my $part_pkg = $cust_pkg->part_pkg; my %hash = ( - 'svcnum' => $_->svcnum, - 'svcdb' => $svcdb, - 'label' => $label, - 'value' => $value, + 'svcnum' => $_->svcnum, + 'svcdb' => $svcdb, + 'label' => $label, + 'value' => $value, + 'pkg_status' => $cust_pkg->status, ); if ( $svcdb eq 'svc_acct' ) { @@ -1063,6 +1470,7 @@ sub list_svcs { %hash, 'username' => $svc_x->username, 'email' => $svc_x->email, + 'finger' => $svc_x->finger, 'seconds' => $svc_x->seconds, 'upbytes' => display_bytecount($svc_x->upbytes), 'downbytes' => display_bytecount($svc_x->downbytes), @@ -1079,11 +1487,21 @@ sub list_svcs { # more... ); - } elsif ( $svcdb eq 'svc_phone' ) { - %hash = ( - %hash, - ); + } elsif ( $svcdb eq 'svc_dsl' ) { + $hash{'phonenum'} = $svc_x->phonenum; + if ( $svc_x->first || $svc_x->get('last') || $svc_x->company ) { + $hash{'name'} = $svc_x->first. ' '. $svc_x->get('last'); + $hash{'name'} = $svc_x->company. ' ('. $hash{'name'}. ')' + if $svc_x->company; + } else { + $hash{'name'} = $cust_main->name; + } } + # elsif ( $svcdb eq 'svc_phone' || $svcdb eq 'svc_port' ) { + # %hash = ( + # %hash, + # ); + #} \%hash; } @@ -1093,6 +1511,113 @@ sub list_svcs { } +sub _customer_svc_x { + my($custnum, $svcnum, $table) = @_; + + $custnum =~ /^(\d+)$/ or die "illegal custnum"; + my $search = " AND custnum = $1"; + #$search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent'; + + qsearchs( { + 'table' => ($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' => $svcnum, }, + 'extra_sql' => $search, #important + } ); + +} + +sub svc_status_html { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + #XXX only svc_dsl for now + my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_dsl') + or return { 'error' => "Service not found" }; + + my $html = $svc_x->getstatus_html; + + return { 'html' => $html }; + +} + +sub list_dsl_devices { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $svc_dsl = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_dsl' ) + or return { 'error' => "Service not found" }; + + return { + 'devices' => [ map { + +{ 'mac_addr' => $_->mac_addr }; + } $svc_dsl->dsl_device + ], + }; + +} + +sub add_dsl_device { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $svc_dsl = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_dsl' ) + or return { 'error' => "Service not found" }; + + return { 'error' => 'No MAC address supplied' } + unless length($p->{'mac_addr'}); + + my $dsl_device = new FS::dsl_device { 'svcnum' => $svc_dsl->svcnum, + 'mac_addr' => scalar($p->{'mac_addr'}), + }; + my $error = $dsl_device->insert; + return { 'error' => $error }; + +} + +sub delete_dsl_device { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $svc_dsl = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_dsl' ) + or return { 'error' => "Service not found" }; + + my $dsl_device = qsearchs('dsl_device', { 'svcnum' => $svc_dsl->svcnum, + 'mac_addr' => scalar($p->{'mac_addr'}), + } + ) + or return { 'error' => 'Unknown MAC address: '. $p->{'mac_addr'} }; + + my $error = $dsl_device->delete; + return { 'error' => $error }; + +} + +sub port_graph { + my $p = shift; + _usage_details( \&_port_graph, $p, + 'svcdb' => 'svc_port', + ); +} + +sub _port_graph { + my($svc_port, $begin, $end) = @_; + my @usage = (); + my $pngOrError = $svc_port->graph_png( start=>$begin, end=> $end ); + push @usage, { 'png' => $pngOrError }; + (@usage); +} + sub _list_svc_usage { my($svc_acct, $begin, $end) = @_; my @usage = (); @@ -1134,8 +1659,8 @@ sub 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, ); + map [ $_->downstream_csv('format' => 'default', 'keeparray' => 1) ], #XXX config for format + $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, ); } sub list_cdr_usage { @@ -1264,7 +1789,7 @@ sub order_pkg { 'svc_domain' => [ qw( domain ) ], 'svc_phone' => [ qw( phonenum pin sip_password phone_name ) ], 'svc_external' => [ qw( id title ) ], - 'svc_pbx' => [ qw( id name ) ], + 'svc_pbx' => [ qw( id title ) ], ); my $svc_x = "FS::$svcdb"->new( { @@ -1272,7 +1797,7 @@ sub order_pkg { map { $_ => $p->{$_} } @{$fields{$svcdb}} } ); - if ( $svcdb eq 'svc_acct' ) { + if ( $svcdb eq 'svc_acct' && exists($p->{"snarf_machine1"}) ) { my @acct_snarf; my $snarfnum = 1; while ( length($p->{"snarf_machine$snarfnum"}) ) { @@ -1302,7 +1827,7 @@ sub order_pkg { tie my %hash, 'Tie::RefHash'; %hash = ( $cust_pkg => \@svc ); #msgcat - $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 ); + $error = $cust_main->order_pkgs( \%hash, 'noexport' => 1 ); return { 'error' => $error } if $error; my $conf = new FS::Conf; @@ -1573,6 +2098,23 @@ sub provision_phone { my @bulkdid; @bulkdid = @{$p->{'bulkdid'}} if $p->{'bulkdid'}; + if($p->{'svcnum'} && $p->{'svcnum'} =~ /^\d+$/){ + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $svc_phone = qsearchs('svc_phone', { svcnum => $p->{'svcnum'} }); + return { 'error' => 'service not found' } unless $svc_phone; + return { 'error' => 'invalid svcnum' } + if $svc_phone && $svc_phone->cust_svc->cust_pkg->custnum != $custnum; + + $svc_phone->email($p->{'email'}) + if $svc_phone->email ne $p->{'email'} && $p->{'email'} =~ /^([\w\.\d@]+|)$/; + $svc_phone->forwarddst($p->{'forwarddst'}) + if $svc_phone->forwarddst ne $p->{'forwarddst'} + && $p->{'forwarddst'} =~ /^(\d+|)$/; + return { 'error' => $svc_phone->replace }; + } + # single DID LNP unless($p->{'lnp'}) { $p->{'lnp_desired_due_date'} = parse_datetime($p->{'lnp_desired_due_date'}); @@ -1735,7 +2277,7 @@ sub part_svc_info { my $conf = new FS::Conf; - return { + my $ret = { 'svc' => $part_svc->svc, 'svcdb' => $part_svc->svcdb, 'pkgnum' => $pkgnum, @@ -1754,6 +2296,17 @@ sub part_svc_info { }; + if ($p->{'svcnum'} && $p->{'svcnum'} =~ /^\d+$/ + && $ret->{'svcdb'} eq 'svc_phone') { + $ret->{'svcnum'} = $p->{'svcnum'}; + my $svc_phone = qsearchs('svc_phone', { svcnum => $p->{'svcnum'} }); + if ( $svc_phone && $svc_phone->cust_svc->cust_pkg->custnum == $custnum ) { + $ret->{'email'} = $svc_phone->email; + $ret->{'forwarddst'} = $svc_phone->forwarddst; + } + } + + $ret; } sub unprovision_svc { @@ -1812,6 +2365,11 @@ sub myaccount_passwd { } ) or return { 'error' => "Service not found" }; + if ( exists($p->{'old_password'}) ) { + return { 'error' => "Incorrect password." } + unless $svc_acct->check_password($p->{'old_password'}); + } + $svc_acct->_password($p->{'new_password'}); my $error = $svc_acct->replace(); @@ -1824,6 +2382,147 @@ sub myaccount_passwd { } +sub reset_passwd { + my $p = shift; + + my $conf = new FS::Conf; + my $verification = $conf->config('selfservice-password_reset_verification') + or return { 'error' => 'Password resets disabled' }; + + my $username = $p->{'username'}; + + my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } ) + or return { 'error' => 'Account not found' }; + + my $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'}, + 'domsvc' => $svc_domain->svcnum } + ) + or return { 'error' => 'Account not found' }; + + my $cust_pkg = $svc_acct->cust_svc->cust_pkg + or return { 'error' => 'Account not found' }; + + my $cust_main = $cust_pkg->cust_main; + + my %verify = ( + 'paymask' => sub { + my( $p, $cust_main ) = @_; + $cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ + && $p->{'paymask'} eq substr($cust_main->paymask, -4) + }, + 'amount' => sub { + my( $p, $cust_main ) = @_; + my $cust_pay = qsearchs({ + 'table' => 'cust_pay', + 'hashref' => { 'custnum' => $cust_main->custnum }, + 'order_by' => 'ORDER BY _date DESC LIMIT 1', + }) + or return 0; + + $p->{'amount'} == $cust_pay->paid; + }, + 'zip' => sub { + my( $p, $cust_main ) = @_; + $p->{'zip'} eq $cust_main->zip + || ( $cust_main->ship_zip && $p->{'zip'} eq $cust_main->ship_zip ); + }, + ); + + foreach my $verify ( split(',', $verification) ) { + + &{ $verify{$verify} }( $p, $cust_main ) + or return { 'error' => 'Account not found' }; + + } + + #okay, we're verified, now create a unique session + + my $reset_session = { + 'svcnum' => $svc_acct->svcnum, + }; + + my $timeout = '1 hour'; #? + + my $reset_session_id; + do { + $reset_session_id = md5_hex(md5_hex(time(). {}. rand(). $$)) + } until ( ! defined _cache->get("reset_passwd_$reset_session_id") ); #just in case + + _cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout ); + + #email it + + my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum); + #die "selfservice-password_reset_msgnum unset" unless $msgnum; + return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum; + my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } ); + my $error = $msg_template->send( 'cust_main' => $cust_main, + 'object' => $svc_acct, + 'substitutions' => { + 'session_id' => $reset_session_id, + } + ); + if ( $error ) { + return { 'error' => $error }; #???? + } + + return { 'error' => '' }; +} + +sub check_reset_passwd { + my $p = shift; + + my $conf = new FS::Conf; + my $verification = $conf->config('selfservice-password_reset_verification') + or return { 'error' => 'Password resets disabled' }; + + my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $svcnum = $reset_session->{'svcnum'}; + + my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } ) + or return { 'error' => "Service not found" }; + + return { 'error' => '', + 'username' => $svc_acct->username, + }; + +} + +sub process_reset_passwd { + my $p = shift; + + my $conf = new FS::Conf; + my $verification = $conf->config('selfservice-password_reset_verification') + or return { 'error' => 'Password resets disabled' }; + + 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 $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $svcnum = $reset_session->{'svcnum'}; + + my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } ) + 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); @@ -1847,13 +2546,13 @@ sub create_ticket { ); if ( ref($err_or_ticket) ) { - warn "$me create_ticket: sucessful: ". $err_or_ticket->id. "\n" + warn "$me create_ticket: successful: ". $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" + warn "$me create_ticket: unsuccessful: $err_or_ticket\n" if $DEBUG; return { 'error' => $err_or_ticket }; } @@ -1947,62 +2646,135 @@ sub get_ticket { warn "$me get_ticket: initializing ticket system\n" if $DEBUG; FS::TicketSystem->init(); + return { 'error' => 'get_ticket configuration error' } + if $FS::TicketSystem::system ne 'RT_Internal'; + + # check existence and ownership as part of this + warn "$me get_ticket: fetching ticket\n" if $DEBUG; + my $rt_session = FS::TicketSystem->session(''); + my $Ticket = FS::TicketSystem->get_ticket_object( + $rt_session, + ticket_id => $p->{'ticket_id'}, + custnum => $custnum + ); + return { 'error' => 'ticket not found' } if !$Ticket; + + if ( length( $p->{'subject'} || '' ) ) { + # subject change + if ( $p->{'subject'} ne $Ticket->Subject ) { + my ($val, $msg) = $Ticket->SetSubject($p->{'subject'}); + return { 'error' => "unable to set subject: $msg" } if !$val; + } + } if(length($p->{'reply'})) { -# currently this allows anyone to correspond on any ticket as fs_selfservice -# probably bad... - my @err_or_res = FS::TicketSystem->correspond_ticket( - '', #create RT session based on FS CurrentUser (fs_selfservice) - 'ticket_id' => $p->{'ticket_id'}, - 'content' => $p->{'reply'}, - ); - + my @err_or_res = FS::TicketSystem->correspond_ticket( + $rt_session, + 'ticket_id' => $p->{'ticket_id'}, + 'content' => $p->{'reply'}, + ); + return { 'error' => 'unable to reply to ticket' } - unless ( $err_or_res[0] != 0 && defined $err_or_res[2] ); + unless ( $err_or_res[0] != 0 && defined $err_or_res[2] ); } - warn "$me get_ticket: getting ticket\n" if $DEBUG; + warn "$me get_ticket: getting ticket history\n" if $DEBUG; my $err_or_ticket = FS::TicketSystem->get_ticket( - '', #create RT session based on FS CurrentUser (fs_selfservice) + $rt_session, 'ticket_id' => $p->{'ticket_id'}, ); - if ( ref($err_or_ticket) ) { + if ( !ref($err_or_ticket) ) { # there is no way this should ever happen + warn "$me get_ticket: unsuccessful: $err_or_ticket\n" + if $DEBUG; + return { 'error' => $err_or_ticket }; + } + + my @custs = @{$err_or_ticket->{'custs'}}; + my @txns = @{$err_or_ticket->{'txns'}}; + my @filtered_txns; + + # superseded by check in get_ticket_object + #return { 'error' => 'invalid ticket requested' } + #unless grep($_ eq $custnum, @custs); + + foreach my $txn ( @txns ) { + push @filtered_txns, $txn + if ($txn->{'type'} eq 'EmailRecord' + || $txn->{'type'} eq 'Correspond' + || $txn->{'type'} eq 'Create'); + } + + warn "$me get_ticket: successful: \n" + if $DEBUG; + return { 'error' => '', + 'transactions' => \@filtered_txns, + 'ticket_fields' => $err_or_ticket->{'fields'}, + 'ticket_id' => $p->{'ticket_id'}, + }; +} -# since we're bypassing the RT security/permissions model by always using -# fs_selfservice as the RT user (as opposed to a requestor, which we -# can't do since we want all tickets linked to a cust), we check below whether -# the requested ticket was actually linked to this customer - my @custs = @{$err_or_ticket->{'custs'}}; - my @txns = @{$err_or_ticket->{'txns'}}; - my @filtered_txns; +sub adjust_ticket_priority { + my $p = shift; + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; - return { 'error' => 'no customer' } unless ( $custnum && scalar(@custs) ); + warn "$me adjust_ticket_priority: initializing ticket system\n" if $DEBUG; + FS::TicketSystem->init; + my $ss_priority = FS::TicketSystem->selfservice_priority; - return { 'error' => 'invalid ticket requested' } - unless grep($_ eq $custnum, @custs); + return { 'error' => 'adjust_ticket_priority configuration error' } + if $FS::TicketSystem::system ne 'RT_Internal' + or !$ss_priority; - foreach my $txn ( @txns ) { - push @filtered_txns, $txn - if ($txn->{'type'} eq 'EmailRecord' - || $txn->{'type'} eq 'Correspond' - || $txn->{'type'} eq 'Create'); + my $values = $p->{'values'}; #hashref, id => priority value + my %ticket_error; + + foreach my $id (keys %$values) { + warn "$me adjust_ticket_priority: fetching ticket $id\n" if $DEBUG; + my $Ticket = FS::TicketSystem->get_ticket_object('', + 'ticket_id' => $id, + 'custnum' => $custnum, + ); + if ( !$Ticket ) { + $ticket_error{$id} = 'ticket not found'; + next; + } + + # RT API stuff--would we gain anything by wrapping this in FS::TicketSystem? + # We're not going to implement it for RT_External. + my $old_value = $Ticket->FirstCustomFieldValue($ss_priority); + my $new_value = $values->{$id}; + next if $old_value eq $new_value; + + warn "$me adjust_ticket_priority: updating ticket $id\n" if $DEBUG; + + # AddCustomFieldValue works fine (replacing any existing value) if it's + # a single-valued custom field, which it should be. If it's not, you're + # doing something wrong. + my ($val, $msg); + if ( length($new_value) ) { + ($val, $msg) = $Ticket->AddCustomFieldValue( + Field => $ss_priority, + Value => $new_value, + ); + } + else { + ($val, $msg) = $Ticket->DeleteCustomFieldValue( + Field => $ss_priority, + Value => $old_value, + ); } - warn "$me get_ticket: sucessful: \n" - if $DEBUG; - return { 'error' => '', - 'transactions' => \@filtered_txns, - 'ticket_id' => $p->{'ticket_id'}, - }; - } else { - warn "$me create_ticket: unsucessful: $err_or_ticket\n" - if $DEBUG; - return { 'error' => $err_or_ticket }; + $ticket_error{$id} = $msg if !$val; + warn "$me adjust_ticket_priority: $id: $msg\n" if $DEBUG and !$val; } + return { 'error' => '', + 'ticket_error' => \%ticket_error, + %{ customer_info($p) } # send updated customer info back + } } - #-- sub _custoragent_session_custnum {