X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2FClientAPI%2FMyAccount.pm;h=0131d8969ed764713aaeac17617876b8ab6bd4ac;hp=e9394e4df36ba913a57dc3aadbbf702068238392;hb=7bcf726ffbb331ad01e717f7de36bfb42f5ca4ba;hpb=62939905ff73736ea366791726e3372f59f274a9 diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index e9394e4df..0131d8969 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -7,6 +7,7 @@ use subs qw( _cache _provision ); use IO::Scalar; use Data::Dumper; use Digest::MD5 qw(md5_hex); +use Digest::SHA qw(sha512_hex); use Date::Format; use Time::Duration; use Time::Local qw(timelocal_nocheck); @@ -14,6 +15,7 @@ use Business::CreditCard; use HTML::Entities; use Text::CSV_XS; use Spreadsheet::WriteExcel; +use OLE::Storage_Lite; use FS::UI::Web::small_custview qw(small_custview); #less doh use FS::UI::Web; use FS::UI::bytecount qw( display_bytecount ); @@ -38,26 +40,27 @@ use FS::cust_main; use FS::cust_bill; use FS::legacy_cust_bill; use FS::cust_main_county; +use FS::part_pkg; use FS::cust_pkg; use FS::payby; use FS::acct_rt_transaction; use FS::msg_template; +use FS::contact; -$DEBUG = 0; +$DEBUG = 1; $me = '[FS::ClientAPI::MyAccount]'; -use vars qw( @cust_main_editable_fields ); +use vars qw( @cust_main_editable_fields @location_editable_fields ); @cust_main_editable_fields = qw( - first last company address1 address2 city - 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_mobile + first last company daytime night fax mobile locale payby payinfo payname paystart_month paystart_year payissue payip ss paytype paystate stateid stateid_state ); +@location_editable_fields = qw( + address1 address2 city county state zip country +); + BEGIN { #preload to reduce time customer_info takes if ( $FS::TicketSystem::system ) { @@ -93,6 +96,7 @@ sub skin_info { } elsif ( defined($p->{'agentnum'}) and $p->{'agentnum'} =~ /^(\d+)$/ ) { $agentnum = $1; } + $p->{'agentnum'} = $agentnum; my $conf = new FS::Conf; @@ -115,11 +119,12 @@ sub skin_info { ( map { $_ => scalar( $conf->config($_, $agentnum) ) } qw( company_name date_format ) ), ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) } - qw( body_bgcolor box_bgcolor + qw( body_bgcolor box_bgcolor stripe1_bgcolor stripe2_bgcolor text_color link_color vlink_color hlink_color alink_color font title_color title_align title_size menu_bgcolor menu_fontsize ) ), + 'menu_disable' => [ $conf->config('selfservice-menu_disable',$agentnum) ], ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) } qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo ) ), @@ -131,6 +136,36 @@ sub skin_info { 'logo' => scalar($conf->config_binary('logo.png', $agentnum )), ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) } qw( head body_header body_footer company_address ) ), + 'menu' => join("\n", $conf->config("ng_selfservice-menu", $agentnum ) ) || + 'main.php Home + + services.php Services + services.php My Services + services_new.php Order a new service + + personal.php Profile + personal.php Personal Information + password.php Change Password + + payment.php Payments + payment_cc.php Credit Card Payment + payment_ach.php Electronic Check Payment + payment_paypal.php PayPal Payment + payment_webpay.php Webpay Payments + + usage.php Usage + usage_data.php Data usage + usage_cdr.php Call usage + + tickets.php Help Desk + tickets.php Open Tickets + tickets_resolved.php Resolved Tickets + ticket_create.php Create a new ticket + + docs.php FAQs + + logout.php Logout + ', }; _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent); @@ -177,6 +212,7 @@ sub login { my $conf = new FS::Conf; my $svc_x = ''; + my $session = {}; if ( $p->{'domain'} eq 'svc_phone' && $conf->exists('selfservice_server-phone_login') ) { @@ -194,8 +230,19 @@ sub login { $svc_x = $svc_phone; + } elsif ( $p->{email} + && (my $contact = FS::contact->by_selfservice_email($p->{email})) + ) + { + return { error => 'Incorrect contact password.' } + unless $contact->authenticate_password($p->{'password'}); + + $session->{'custnum'} = $contact->custnum; + } else { + ( $p->{username}, $p->{domain} ) = split('@', $p->{email}) if $p->{email}; + my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } ) or return { error => 'Domain '. $p->{'domain'}. ' not found' }; @@ -218,35 +265,37 @@ sub login { } - my $session = { - 'svcnum' => $svc_x->svcnum, - }; + if ( $svc_x ) { - 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; + $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 $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(). $$)) + $session_id = sha512_hex(time(). {}. rand(). $$) } until ( ! defined _cache->get($session_id) ); #just in case my $timeout = $conf->config('selfservice-session_timeout') || '1 hour'; @@ -347,6 +396,8 @@ sub access_info { $conf->exists('ticket_system-selfservice_edit_subject') && $cust_main->edit_subject; + $info->{'timeout'} = $conf->config('selfservice-timeout') || 3600; + return { %$info, 'custnum' => $custnum, 'access_pkgnum' => $session->{'pkgnum'}, @@ -363,51 +414,29 @@ sub customer_info { my %return; my $conf = new FS::Conf; - if ($conf->exists('cust_main-require_address2')) { - $return{'require_address2'} = '1'; - }else{ - $return{'require_address2'} = ''; - } + $return{'require_address2'} = $conf->exists('cust_main-require_address2'); - if ( $FS::TicketSystem::system ) { - warn "$me customer_info: initializing ticket system\n" if $DEBUG; - FS::TicketSystem->init(); - } +# if ( $FS::TicketSystem::system ) { +# warn "$me customer_info: initializing ticket system\n" if $DEBUG; +# FS::TicketSystem->init(); +# } if ( $custnum ) { #customer record + %return = ( %return, %{ customer_info_short($p) } ); + + #redundant with customer_info_short, but we need it for several things below 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; - } - - 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; - } + my $list_tickets = list_tickets($p); + $return{'tickets'} = $list_tickets->{'tickets'}; - unless ( $session->{'pkgnum'} ) { + if ( $session->{'pkgnum'} ) { + #XXX open invoices in the pkg-balances case + } else { my @open = map { { invnum => $_->invnum, @@ -416,35 +445,37 @@ sub customer_info { }; } $cust_main->open_cust_bill; $return{open_invoices} = \@open; + + my $sql = 'SELECT MAX(_date) FROM cust_bill WHERE custnum = ?'; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute($custnum) or die $sth->errstr; + $return{'last_invoice_date'} = $sth->fetchrow_arrayref->[0]; + $return{'last_invoice_date_pretty'} = + time2str('%m/%d/%Y', $return{'last_invoice_date'} ); } + #customer_info_short always has nobalance on.. $return{small_custview} = small_custview( $cust_main, - scalar($conf->config('countrydefault')), + $return{countrydefault}, ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance ); - $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($_); - } + $return{has_ship_address} = $cust_main->has_ship_address; + $return{status} = $cust_main->status; + $return{statuscolor} = $cust_main->statuscolor; - if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { - $return{payinfo} = $cust_main->paymask; - @return{'month', 'year'} = $cust_main->paydate_monthyear; + # compatibility: some places in selfservice use this to determine + # if there's a ship address + if ( $return{has_ship_address} ) { + $return{ship_last} = $cust_main->last; + $return{ship_first} = $cust_main->first; } - $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 $seconds = $_->svc_x->seconds || 0; my $time_remaining = (($seconds < 0) ? '-' : '' ). int(abs($seconds)/3600)."h". sprintf("%02d",(abs($seconds)%3600)/60)."m"; @@ -467,12 +498,6 @@ 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'} } ) @@ -485,8 +510,8 @@ sub customer_info { } - return { 'error' => '', - 'custnum' => $custnum, + return { 'error' => '', + 'custnum' => $custnum, %return, }; @@ -509,14 +534,30 @@ sub customer_info_short { my $cust_main = qsearchs('cust_main', $search ) or return { 'error' => "unknown custnum $custnum" }; + $return{display_custnum} = $cust_main->display_custnum; + + if ( $session->{'pkgnum'} ) { + $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} ); + #next_bill_date from cust_pkg? + } else { + $return{balance} = $cust_main->balance; + $return{next_bill_date} = $cust_main->next_bill_date; + $return{next_bill_date_pretty} = + $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} ) + : '(none)'; + } + + $return{countrydefault} = scalar($conf->config('countrydefault')); + $return{small_custview} = small_custview( $cust_main, - scalar($conf->config('countrydefault')), + $return{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{first} = $cust_main->first; + $return{'last'} = $cust_main->get('last'); + $return{name} = $cust_main->first. ' '. $cust_main->get('last'); $return{payby} = $cust_main->payby; @@ -524,16 +565,24 @@ sub customer_info_short { for (@cust_main_editable_fields) { $return{$_} = $cust_main->get($_); } - + #maybe a little more expensive, but it should be cached by now + for (@location_editable_fields) { + $return{$_} = $cust_main->bill_location->get($_) + if $cust_main->bill_locationnum; + $return{'ship_'.$_} = $cust_main->ship_location->get($_) + if $cust_main->ship_locationnum; + } + if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { $return{payinfo} = $cust_main->paymask; @return{'month', 'year'} = $cust_main->paydate_monthyear; } + my @invoicing_list = $cust_main->invoicing_list; $return{'invoicing_list'} = - join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list ); - #$return{'postal_invoicing'} = - # 0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list ); + join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ); + $return{'postal_invoicing'} = + 0 < ( grep { $_ eq 'POST' } @invoicing_list ); if ( $session->{'svcnum'} ) { my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} }); @@ -558,6 +607,105 @@ sub customer_info_short { }; } +sub billing_history { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + return { 'error' => 'No customer' } unless $custnum; + + 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 %return = (); + + if ( $session->{'pkgnum'} ) { + #$return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} ); + #next_bill_date from cust_pkg? + return { 'error' => 'No history for package' }; + } + + $return{balance} = $cust_main->balance; + $return{next_bill_date} = $cust_main->next_bill_date; + $return{next_bill_date_pretty} = + $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} ) + : '(none)'; + + my @history = (); + + my $conf = new FS::Conf; + + if ( $conf->exists('selfservice-billing_history-line_items') ) { + + foreach my $cust_bill ( $cust_main->cust_bill ) { + + push @history, { + 'type' => 'Line item', + 'description' => $_->desc( $cust_main->locale ). + ( $_->sdate && $_->edate + ? ' '. time2str('%d-%b-%Y', $_->sdate). + ' To '. time2str('%d-%b-%Y', $_->edate) + : '' + ), + 'amount' => sprintf('%.2f', $_->setup + $_->recur ), + 'date' => $cust_bill->_date, + 'date_pretty' => time2str('%m/%d/%Y', $cust_bill->_date ), + } + foreach $cust_bill->cust_bill_pkg; + + } + + } else { + + push @history, { + 'type' => 'Invoice', + 'description' => 'Invoice #'. $_->display_invnum, + 'amount' => sprintf('%.2f', $_->charged ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $cust_main->cust_bill; + + } + + push @history, { + 'type' => 'Payment', + 'description' => 'Payment', #XXX type + 'amount' => sprintf('%.2f', 0 - $_->paid ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $cust_main->cust_pay; + + push @history, { + 'type' => 'Credit', + 'description' => 'Credit', #more info? + 'amount' => sprintf('%.2f', 0 -$_->amount ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $cust_main->cust_credit; + + push @history, { + 'type' => 'Refund', + 'description' => 'Refund', #more info? type, like payment? + 'amount' => $_->refund, + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $cust_main->cust_refund; + + @history = sort { $b->{'date'} <=> $a->{'date'} } @history; + + $return{'history'} = \@history; + + return \%return; + +} + sub edit_info { my $p = shift; my $session = _cache->get($p->{'session_id'}) @@ -570,9 +718,32 @@ sub edit_info { 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; + if ( exists($p->{address1}) ) { + my $bill_location = FS::cust_location->new({ + map { $_ => $p->{$_} } @location_editable_fields + }); + # if this is unchanged from before, cust_main::replace will ignore it + $new->set('bill_location' => $bill_location); + } + + if ( exists($p->{ship_address1}) ) { + my $ship_location = FS::cust_location->new({ + map { $_ => $p->{"ship_$_"} } @location_editable_fields + }); + if ( !grep { length($p->{"ship_$_"}) } @location_editable_fields ) { + # Selfservice unfortunately tries to indicate "same as billing + # address" by sending all fields empty. Did this ever work? + $ship_location = $cust_main->bill_location; + } + $new->set('ship_location' => $ship_location); + } + # but if it hasn't been passed in at all, leave ship_location alone-- + # DON'T change it to match bill_location. + my $payby = ''; if (exists($p->{'payby'})) { $p->{'payby'} =~ /^([A-Z]{4})$/ @@ -580,6 +751,8 @@ sub edit_info { $payby = $1; } + my $conf = new FS::Conf; + if ( $payby =~ /^(CARD|DCRD)$/ ) { $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01'); @@ -592,6 +765,10 @@ sub edit_info { $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' ); + if ( $conf->exists('selfservice-onfile_require_cvv') ){ + return { 'error' => 'CVV2 is required' } unless $p->{'paycvv'}; + } + } elsif ( $payby =~ /^(CHEK|DCHK)$/ ) { my $payinfo; @@ -668,6 +845,10 @@ sub payment_info { 'card_types' => card_types(), + 'withcvv' => $conf->exists('selfservice-require_cvv'), #or enable optional cvv? + 'require_cvv' => $conf->exists('selfservice-require_cvv'), + 'onfile_require_cvv' => $conf->exists('selfservice-onfile_require_cvv'), + 'paytypes' => [ @FS::cust_main::paytypes ], 'paybys' => [ $conf->config('signup_server-payby') ], @@ -682,7 +863,7 @@ sub payment_info { 'save_unchecked' => $conf->exists('selfservice-save_unchecked'), - 'credit_card_surcharge_percentage' => $conf->config('credit-card-surcharge-percentage'), + 'credit_card_surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')), }; } @@ -710,7 +891,8 @@ sub payment_info { $return{payname} = $cust_main->payname || ( $cust_main->first. ' '. $cust_main->get('last') ); - $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip); + $return{$_} = $cust_main->bill_location->get($_) + for qw(address1 address2 city state zip); $return{payby} = $cust_main->payby; $return{stateid_state} = $cust_main->stateid_state; @@ -739,7 +921,8 @@ sub payment_info { #doubleclick protection my $_date = time; - $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; + $return{payunique} = "webui-MyAccount-$_date-$$-". rand() * 2**32; #new + $return{paybatch} = $return{payunique}; #back compat return { 'error' => '', %return, @@ -766,6 +949,21 @@ sub validate_payment { my $amount = $1; return { error => 'Amount must be greater than 0' } unless $amount > 0; + #false laziness w/tr-amount_fee.html, but we don't want selfservice users + #changing the hidden form values + my $conf = new FS::Conf; + my $fee_display = $conf->config('selfservice_process-display') || 'add'; + my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum); + my $fee_skip_first = $conf->exists('selfservice_process-skip_first'); + if ( $fee_display eq 'add' + and $fee_pkgpart + and ! $fee_skip_first || scalar($cust_main->cust_pay) + ) + { + my $fee_pkg = qsearchs('part_pkg', { pkgpart=>$fee_pkgpart } ); + $amount = sprintf('%.2f', $amount + $fee_pkg->option('setup_fee') ); + } + $p->{'discount_term'} =~ /^\s*(\d*)\s*$/ or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} }; my $discount_term = $1; @@ -774,10 +972,16 @@ sub validate_payment { or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} }; my $payname = $1; + $p->{'payunique'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/ + or return { 'error' => gettext('illegal_text'). " payunique: ". $p->{'payunique'} }; + my $payunique = $1; + $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/ or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} }; my $paybatch = $1; + $payunique = $paybatch if ! length($payunique) && length($paybatch); + $p->{'payby'} ||= 'CARD'; $p->{'payby'} =~ /^([A-Z]{4})$/ or return { 'error' => "illegal_payby " . $p->{'payby'} }; @@ -803,10 +1007,14 @@ sub validate_payment { $payinfo = $p->{'payinfo'}; + my $onfile = 0; + #more intelligent matching 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; + if ( $cust_main->paymask eq $payinfo ) { + $payinfo = $cust_main->payinfo; + $onfile = 1; + } $payinfo =~ s/\D//g; $payinfo =~ /^(\d{13,16}|\d{8,9})$/ @@ -828,6 +1036,10 @@ sub validate_payment { or return { 'error' => "CVV2 (CVC2/CID) is three digits." }; $paycvv = $1; } + } elsif ( $conf->exists('selfservice-onfile_require_cvv') ) { + return { 'error' => 'CVV2 is required' }; + } elsif ( !$onfile && $conf->exists('selfservice-require_cvv') ) { + return { 'error' => 'CVV2 is required' }; } } else { @@ -845,15 +1057,18 @@ sub validate_payment { { 'cust_main' => $cust_main, #XXX or just custnum?? - 'amount' => $amount, + 'amount' => sprintf('%.2f', $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'}, + 'month' => $p->{'month'}, + 'year' => $p->{'year'}, 'payname' => $payname, - 'paybatch' => $paybatch, #this doesn't actually do anything + 'payunique' => $payunique, + 'paybatch' => $paybatch, 'paycvv' => $paycvv, 'payname' => $payname, 'discount_term' => $discount_term, @@ -876,7 +1091,9 @@ sub store_payment { _cache->set( 'payment_'.$p->{'session_id'}, $validate, $timeout ); +{ map { $_=>$validate->{$_} } - qw( card_type paymask payname paydate_pretty amount ) + qw( card_type paymask payname paydate_pretty month year amount + address1 address2 city state zip country + ) }; } @@ -921,15 +1138,42 @@ sub do_process_payment { ); return { 'error' => $error } if $error; + #no error, so order the fee package if applicable... + my $conf = new FS::Conf; + my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum); + my $fee_skip_first = $conf->exists('selfservice_process-skip_first'); + + if ( $fee_pkgpart and ! $fee_skip_first || scalar($cust_main->cust_pay) ) { + + my $cust_pkg = new FS::cust_pkg { 'pkgpart' => $fee_pkgpart }; + + $error = $cust_main->order_pkg( 'cust_pkg' => $cust_pkg ); + return { 'error' => "payment processed successfully, but error ordering fee: $error" } + if $error; + + #and generate an invoice for it now too + $error = $cust_main->bill( 'pkg_list' => [ $cust_pkg ] ); + return { 'error' => "payment processed and fee ordered sucessfully, but error billing fee: $error" } + if $error; + + } + $cust_main->apply_payments; if ( $validate->{'save'} ) { my $new = new FS::cust_main { $cust_main->hash }; if ($payby eq 'CARD' || $payby eq 'DCRD') { $new->set( $_ => $validate->{$_} ) - foreach qw( payname paystart_month paystart_year payissue payip - address1 address2 city state zip country ); + foreach qw( payname paystart_month paystart_year payissue payip ); $new->set( 'payby' => $validate->{'auto'} ? 'CARD' : 'DCRD' ); + + my $bill_location = FS::cust_location->new({ + map { $_ => $validate->{$_} } + qw(address1 address2 city state country zip) + }); # county? + $new->set('bill_location' => $bill_location); + # but don't allow the service address to change this way. + } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') { $new->set( $_ => $validate->{$_} ) foreach qw( payname payip paytype paystate @@ -998,16 +1242,14 @@ sub do_process_payment { 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, + 'auth_num' => $cust_pay->auth, + 'order_num' => $cust_pay->order_number, 'receipt_html' => $receipt_html, }; @@ -1057,6 +1299,50 @@ sub realtime_collect { return { 'error' => '', amount => $amount, %$error }; } +sub start_thirdparty { + 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 = FS::cust_main->by_key($custnum); + + my $amount = $p->{'amount'} + or return { error => 'no amount' }; + + my $result = $cust_main->create_payment( + 'method' => $p->{'method'}, + 'amount' => $p->{'amount'}, + 'pkgnum' => $session->{'pkgnum'}, + 'session_id' => $p->{'session_id'}, + ); + + if ( ref($result) ) { # hashref or error + return $result; + } else { + return { error => $result }; + } +} + +sub finish_thirdparty { + my $p = shift; + my $session_id = delete $p->{'session_id'}; + my $session = _cache->get($session_id) + or return { 'error' => "Can't resume session" }; + my $custnum = $session->{'custnum'}; + my $cust_main = FS::cust_main->by_key($custnum); + + if ( $p->{_cancel} ) { + # customer backed out of making a payment + return $cust_main->cancel_payment( $session_id ); + } + my $result = $cust_main->execute_payment( $session_id, %$p ); + if ( ref($result) ) { + return $result; + } else { + return { error => $result }; + } +} + sub process_payment_order_pkg { my $p = shift; @@ -1331,6 +1617,7 @@ sub list_pkgs { or return { 'error' => "unknown custnum $custnum" }; my $conf = new FS::Conf; + my $immutable = $conf->exists('selfservice_immutable-package'); # the duplication below is necessary: # 1. to maintain the current buggy behaviour wrt the cust_pkg and part_pkg @@ -1343,6 +1630,7 @@ sub list_pkgs { 'custnum' => $custnum, 'cust_pkg' => [ map { { $_->hash, + immutable => $immutable, part_pkg => [ map $_->hashref, $_->part_pkg ], part_svc => [ map $_->hashref, $_->available_part_svc ], @@ -1375,9 +1663,15 @@ sub list_pkgs { my $primary_cust_svc = $_->primary_cust_svc; +{ $_->hash, $_->part_pkg->hash, - status => $_->status, + immutable => $immutable, + pkg_label => $_->pkg_locale, + status => $_->status, + statuscolor => $_->statuscolor, part_svc => - [ map $_->hashref, $_->available_part_svc ], + [ map { $_->hashref } + grep { $_->selfservice_access ne 'hidden' } + $_->available_part_svc + ], cust_svc => [ map { my $ref = { $_->hash, label => [ $_->label ], @@ -1391,7 +1685,9 @@ sub list_pkgs { $ref->{svchash}->{svcpart} = $_->part_svc->svcpart if $_->part_svc->svcdb eq 'svc_phone'; # hack $ref; - } $_->cust_svc + } + grep { $_->part_svc->selfservice_access ne 'hidden' } + $_->cust_svc ], primary_cust_svc => $primary_cust_svc @@ -1406,6 +1702,7 @@ sub list_pkgs { ], 'small_custview' => small_custview( $cust_main, $conf->config('countrydefault') ), + 'date_format' => $conf->config('date_format') || '%m/%d/%Y', }; } @@ -1428,15 +1725,26 @@ sub list_svcs { } my @cust_svc = (); + my @cust_pkg_usage = (); #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) { foreach my $cust_pkg ( $p->{'ncancelled'} ? $cust_main->ncancelled_pkgs : $cust_main->unsuspended_pkgs ) { next if $pkgnum && $cust_pkg->pkgnum != $pkgnum; push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context + push @cust_pkg_usage, $cust_pkg->cust_pkg_usage; } @cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc; + my %usage_pools; + foreach (@cust_pkg_usage) { + my $part = $_->part_pkg_usage; + my $tag = $part->description . ($part->shared ? 1 : 0); + my $row = $usage_pools{$tag} + ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ]; + $row->[1] += sprintf('%.1f', $_->minutes); # minutes remaining + $row->[2] += $part->minutes; # minutes total + } if ( $p->{'svcdb'} ) { my $svcdb = ref($p->{'svcdb'}) eq 'HASH' @@ -1467,12 +1775,14 @@ sub list_svcs { my $part_pkg = $cust_pkg->part_pkg; my %hash = ( - 'svcnum' => $_->svcnum, - 'svcdb' => $svcdb, - 'label' => $label, - 'value' => $value, - 'pkg_status' => $cust_pkg->status, - 'readonly' => ( $part_svc->selfservice_access eq 'readonly' ), + 'svcnum' => $_->svcnum, + 'display_svcnum' => $_->display_svcnum, + 'svcdb' => $svcdb, + 'label' => $label, + 'value' => $value, + 'pkg_label' => $cust_pkg->pkg_locale, + 'pkg_status' => $cust_pkg->status, + 'readonly' => ($part_svc->selfservice_access eq 'readonly'), ); if ( $svcdb eq 'svc_acct' ) { @@ -1506,7 +1816,35 @@ sub list_svcs { } else { $hash{'name'} = $cust_main->name; } + } elsif ( $svcdb eq 'svc_phone' ) { + # could potentially show lots of things... + $hash{'outbound'} = 1; + $hash{'inbound'} = 0; + if ( $part_pkg->plan eq 'voip_inbound' ) { + $hash{'outbound'} = 0; + $hash{'inbound'} = 1; + } elsif ( $part_pkg->option('selfservice_inbound_format') + or $conf->config('selfservice-default_inbound_cdr_format') + ) { + $hash{'inbound'} = 1; + } + foreach (qw(inbound outbound)) { + # hmm...we can't filter by status here, because there might + # not be cdr_terminations at all. have to go by date. + # find all since the last bill date. + # XXX cdr types? we are going to need them. + if ( $hash{$_} ) { + my $sum_cdr = $svc_x->sum_cdrs( + 'inbound' => ( $_ eq 'inbound' ? 1 : 0 ), + 'begin' => ($cust_pkg->last_bill || 0), + 'nonzero' => 1, + 'disable_charged_party' => 1, + ); + $hash{$_} = $sum_cdr->hashref; + } + } } + # elsif ( $svcdb eq 'svc_phone' || $svcdb eq 'svc_port' ) { # %hash = ( # %hash, @@ -1517,6 +1855,11 @@ sub list_svcs { } @cust_svc ], + 'usage_pools' => [ + map { $usage_pools{$_} } + sort { $a cmp $b } + keys %usage_pools + ], }; } @@ -1571,8 +1914,14 @@ sub svc_status_hash { } -sub set_svc_status_hash { - my $p = shift; +sub set_svc_status_hash { _svc_method_X(shift, 'export_setstatus') } +sub set_svc_status_listadd { _svc_method_X(shift, 'export_setstatus_listadd') } +sub set_svc_status_listdel { _svc_method_X(shift, 'export_setstatus_listdel') } +sub set_svc_status_vacationadd { _svc_method_X(shift, 'export_setstatus_vacationadd') } +sub set_svc_status_vacationdel { _svc_method_X(shift, 'export_setstatus_vacationdel') } + +sub _svc_method_X { + my( $p, $method ) = @_; my($context, $session, $custnum) = _custoragent_session_custnum($p); return { 'error' => $session } if $context eq 'error'; @@ -1581,16 +1930,15 @@ sub set_svc_status_hash { my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct') or return { 'error' => "Service not found" }; - warn "set_svc_status_hash ". join(' / ', map "$_=>".$p->{$_}, keys %$p ) + warn "$method ". join(' / ', map "$_=>".$p->{$_}, keys %$p ) if $DEBUG; - my $error = $svc_x->export_setstatus($p); #$p? returns error? + my $error = $svc_x->$method($p); #$p? returns error? return { 'error' => $error } if $error; return {}; #? { 'error' => '' } } - sub acct_forward_info { my $p = shift; @@ -1770,9 +2118,16 @@ sub list_support_usage { sub _list_cdr_usage { # XXX CDR type support... + # XXX any way to do a paged search on this? + # we have to return the results all at once... my($svc_phone, $begin, $end, %opt) = @_; map [ $_->downstream_csv(%opt, 'keeparray' => 1) ], - $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, ); + $svc_phone->get_cdrs( + 'begin'=>$begin, + 'end'=>$end, + 'disable_charged_party' => 1, + %opt + ); } sub list_cdr_usage { @@ -1784,6 +2139,7 @@ sub list_cdr_usage { sub _usage_details { my($callback, $p, %opt) = @_; + my $conf = FS::Conf->new; my($context, $session, $custnum) = _custoragent_session_custnum($p); return { 'error' => $session } if $context eq 'error'; @@ -1802,18 +2158,20 @@ sub _usage_details { my %callback_opt; my $header = []; if ( $svcdb eq 'svc_phone' ) { - my $format = $cust_pkg->part_pkg->option('output_format') || ''; - $format = '' if $format =~ /^sum_/; - # sensible default if there is no format or it's a summary format - if ( $cust_pkg->part_pkg->plan eq 'voip_inbound' ) { - $format ||= 'source_default'; + my $format = ''; + if ( $p->{inbound} ) { + $format = $cust_pkg->part_pkg->option('selfservice_inbound_format') + || $conf->config('selfservice-default_inbound_cdr_format') + || 'source_default'; $callback_opt{inbound} = 1; + } else { + $format = $cust_pkg->part_pkg->option('selfservice_format') + || $conf->config('selfservice-default_cdr_format') + || 'default'; } - else { - $format ||= 'default'; - } - + $callback_opt{format} = $format; + $callback_opt{use_clid} = 1; $header = [ split(',', FS::cdr::invoice_header($format) ) ]; } @@ -1826,10 +2184,21 @@ sub _usage_details { $p->{ending} = $end; } + die "illegal beginning" if $p->{beginning} !~ /^\d*$/; + die "illegal ending" if $p->{ending} !~ /^\d*$/; + my (@usage) = &$callback($svc_x, $p->{beginning}, $p->{ending}, %callback_opt ); + if ( $conf->exists('selfservice-hide_cdr_price') ) { + # ugly kludge, I know + my ($delete_col) = grep { $header->[$_] eq 'Price' } (0..scalar(@$header)); + if (defined $delete_col) { + delete($_->[$delete_col]) foreach ($header, @usage); + } + } + #kinda false laziness with FS::cust_main::bill, but perhaps #we should really change this bit to DateTime and DateTime::Duration # @@ -1869,6 +2238,7 @@ sub _usage_details { 'svcnum' => $p->{svcnum}, 'beginning' => $p->{beginning}, 'ending' => $p->{ending}, + 'inbound' => $p->{inbound}, 'previous' => ($previous > $start) ? $previous : $start, 'next' => ($next < $end) ? $next : $end, 'header' => $header, @@ -1986,6 +2356,10 @@ sub change_pkg { my($context, $session, $custnum) = _custoragent_session_custnum($p); return { 'error' => $session } if $context eq 'error'; + my $conf = new FS::Conf; + my $immutable = $conf->exists('selfservice_immutable-package'); + return { 'error' => "Package modification disabled" } if $immutable; + my $search = { 'custnum' => $custnum }; $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; my $cust_main = qsearchs('cust_main', $search ) @@ -1995,6 +2369,11 @@ sub change_pkg { my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $p->{pkgnum} } ) or return { 'error' => "unknown package $p->{pkgnum}" }; + #if someone does need self-service package change of suspended packages, + # figure out how to be more discriminating + return { error=>"Can't change a suspended package", pkgnum=>$cust_pkg->pkgnum} + if $cust_pkg->status eq 'suspended'; + my @newpkg; my $error = FS::cust_pkg::order( $custnum, [$p->{pkgpart}], @@ -2002,10 +2381,9 @@ sub change_pkg { \@newpkg, ); - my $conf = new FS::Conf; if ( $conf->exists('signup_server-realtime') ) { - my $bill_error = _do_bop_realtime( $cust_main, $status ); + my $bill_error = _do_bop_realtime( $cust_main, $status, 'no_credit'=>1 ); if ($bill_error) { $newpkg[0]->suspend; @@ -2077,25 +2455,32 @@ sub order_recharge { } sub _do_bop_realtime { - my ($cust_main, $status) = (shift, shift); + my ($cust_main, $status, %opt) = @_; my $old_balance = $cust_main->balance; my $bill_error = $cust_main->bill - || $cust_main->apply_payments_and_credits - || $cust_main->realtime_collect('selfservice' => 1); + || $cust_main->apply_payments_and_credits; + + $bill_error ||= $cust_main->realtime_collect('selfservice' => 1) + if $cust_main->payby =~ /^(CARD|CHEK)$/; 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' ); + && ( $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ + || $status eq 'suspended' + ) + ) + { + unless ( $opt{'no_credit'} ) { + #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 }; } @@ -2493,13 +2878,33 @@ 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'}); - } + my $error = ''; + + my $conf = new FS::Conf; + + return { 'error' => 'Incorrect current password.' } + if ( exists($p->{'old_password'}) + || $conf->exists('selfservice-password_change_oldpass') + ) + && ! $svc_acct->check_password($p->{'old_password'}); + + $error = 'Password too short.' + if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6); + $error = 'Password too long.' + if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8); $svc_acct->set_password($p->{'new_password'}); - my $error = $svc_acct->replace(); + $error ||= $svc_acct->replace(); + + #regular pw change in self-service should change contact pw too, otherwise its + #way too confusing. hell its confusing they're separate at all, but alas. + #need to support the "ISP provides email that's used as a contact email" case + #as well as we can. + my $contact = FS::contact->by_selfservice_email($svc_acct->email); + if ( $contact && $contact->custnum == $custnum ) { + #svc_acct was successful but this one returns an error? "shouldn't happen" + $error ||= $contact->change_password($p->{'new_password'}); + } my($label, $value) = $svc_acct->cust_svc->label; @@ -2510,29 +2915,113 @@ sub myaccount_passwd { } +# sub contact_passwd { +# my $p = shift; +# my($context, $session, $custnum) = _custoragent_session_custnum($p); +# return { 'error' => $session } if $context eq 'error'; +# +# return { 'error' => 'Not logged in as a contact.' } +# unless $session->{'contactnum'}; +# +# 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 selfservice_access IS NOT NULL ". +# " AND selfservice_access = 'Y' ". +# " AND ( disabled IS NULL OR disabled = '' )". +# " AND custnum IS NOT NULL AND custnum = $1"; +# $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent'; +# +# my $contact = qsearchs( { +# 'table' => 'contact', +# 'addl_from' => 'LEFT JOIN cust_main USING ( custnum ) ', +# 'hashref' => { 'contactnum' => $session->{'contactnum'}, }, +# 'extra_sql' => $search, #important +# } ) +# or return { 'error' => "Email not found" }; #? how did we get logged in? +# # deleted since then? +# +# my $error = ''; +# +# # use these svc_acct length restrictions?? +# my $conf = new FS::Conf; +# $error = 'Password too short.' +# if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6); +# $error = 'Password too long.' +# if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8); +# +# $error ||= $contact->change_password($p->{'new_password'}); +# +# return { 'error' => $error, }; +# +# } + sub reset_passwd { my $p = shift; + my $info = skin_info($p); + my $conf = new FS::Conf; my $verification = $conf->config('selfservice-password_reset_verification') - or return { 'error' => 'Password resets disabled' }; + or return { %$info, 'error' => 'Password resets disabled' }; + + my $contact = ''; + my $svc_acct = ''; + my $cust_main = ''; + if ( $p->{'email'} ) { #new-style, changes contact and svc_acct + + $contact = FS::contact->by_selfservice_email($p->{'email'}); + + $cust_main = $contact->cust_main if $contact; + + #also look for an svc_acct, otherwise it would be super confusing + + my($username, $domain) = split('@', $p->{'email'}); + my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } ); + if ( $svc_domain ) { + $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'}, + 'domsvc' => $svc_domain->svcnum } + ); + if ( $svc_acct ) { + my $cust_pkg = $svc_acct->cust_svc->cust_pkg; + $cust_main ||= $cust_pkg->cust_main if $cust_pkg; + + #precaution: don't change svc_acct password not part of the same + # customer as contact + $svc_acct = '' if ! $cust_pkg + || $cust_pkg->custnum != $cust_main->custnum; + } + + } + + return { %$info, 'error' => 'Email address not found' } + unless $contact || $svc_acct; - my $username = $p->{'username'}; + } elsif ( $p->{'username'} ) { #old style, looks in svc_acct only - my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } ) - or return { 'error' => 'Account not found' }; + my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } ) + or return { %$info, 'error' => 'Account not found' }; - my $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'}, - 'domsvc' => $svc_domain->svcnum } - ) - or return { 'error' => 'Account not found' }; + $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'}, + 'domsvc' => $svc_domain->svcnum } + ) + or return { %$info, 'error' => 'Account not found' }; - my $cust_pkg = $svc_acct->cust_svc->cust_pkg - or return { 'error' => 'Account not found' }; + my $cust_pkg = $svc_acct->cust_svc->cust_pkg + or return { %$info, 'error' => 'Account not found' }; - my $cust_main = $cust_pkg->cust_main; + $cust_main = $cust_pkg->cust_main; + + } my %verify = ( + 'email' => sub { 1; }, 'paymask' => sub { my( $p, $cust_main ) = @_; $cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ @@ -2559,42 +3048,62 @@ sub reset_passwd { foreach my $verify ( split(',', $verification) ) { &{ $verify{$verify} }( $p, $cust_main ) - or return { 'error' => 'Account not found' }; + or return { %$info, 'error' => 'Account not found' }; } - #okay, we're verified, now create a unique session + #okay, we're verified - my $reset_session = { - 'svcnum' => $svc_acct->svcnum, - }; + if ( $contact ) { - my $timeout = '1 hour'; #? + my $error = $contact->send_reset_email( + 'svcnum' => ($svc_acct ? $svc_acct->svcnum : ''), + ); + + if ( $error ) { + return { %$info, 'error' => $error }; #???? + } + + } elsif ( $svc_acct ) { + + #create a unique session + + my $reset_session = { + 'svcnum' => $svc_acct->svcnum, + 'agentnum' => + }; + + my $timeout = '1 hour'; #? + + my $reset_session_id; + do { + $reset_session_id = sha512_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 { %$info, '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 { %$info, 'error' => $error }; #???? + } - 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' => '' }; + return { %$info, 'error' => '' }; } sub check_reset_passwd { @@ -2607,14 +3116,46 @@ sub check_reset_passwd { 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'}; + if ( $reset_session->{'svcnum'} ) { - my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } ) - or return { 'error' => "Service not found" }; + my $svcnum = $reset_session->{'svcnum'}; - return { 'error' => '', - 'username' => $svc_acct->username, - }; + my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } ) + or return { 'error' => "Service not found" }; + + $p->{'agentnum'} = $svc_acct->cust_svc->cust_pkg->cust_main->agentnum; + my $info = skin_info($p); + + return { %$info, + 'error' => '', + 'session_id' => $p->{'session_id'}, + 'username' => $svc_acct->username, + }; + + } elsif ( $reset_session->{'contactnum'} ) { + + my $contactnum = $reset_session->{'contactnum'}; + + my $contact = qsearchs('contact', { 'contactnum' => $contactnum } ) + or return { 'error' => "Contact not found" }; + + my @contact_email = $contact->contact_email; + return { 'error' => 'No contact email' } unless @contact_email; + + $p->{'agentnum'} = $contact->cust_main->agentnum; + my $info = skin_info($p); + + return { %$info, + 'error' => '', + 'session_id' => $p->{'session_id'}, + 'email' => $contact_email[0]->email, #the first? + }; + + } else { + + return { 'error' => 'No svcnum or contactnum in session' }; #?? + + } } @@ -2625,29 +3166,116 @@ sub process_reset_passwd { my $verification = $conf->config('selfservice-password_reset_verification') or return { 'error' => 'Password resets disabled' }; - return { 'error' => "New passwords don't match." } + my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $info = ''; + + my $svc_acct = ''; + if ( $reset_session->{'svcnum'} ) { + + my $svcnum = $reset_session->{'svcnum'}; + + $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } ) + or return { 'error' => "Service not found" }; + + $p->{'agentnum'} ||= $svc_acct->cust_svc->cust_pkg->cust_main->agentnum; + $info ||= skin_info($p); + + } + + my $contact = ''; + if ( $reset_session->{'contactnum'} ) { + + my $contactnum = $reset_session->{'contactnum'}; + + $contact = qsearchs('contact', { 'contactnum' => $contactnum } ) + or return { 'error' => "Contact not found" }; + + $p->{'agentnum'} ||= $contact->cust_main->agentnum; + $info ||= skin_info($p); + + } + + return { %$info, 'error' => "New passwords don't match." } if $p->{'new_password'} ne $p->{'new_password2'}; - return { 'error' => 'Enter new password' } + return { %$info, '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 + if ( $svc_acct ) { - my $svcnum = $reset_session->{'svcnum'}; + $svc_acct->set_password($p->{'new_password'}); + my $error = $svc_acct->replace(); - my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } ) - or return { 'error' => "Service not found" }; + return { %$info, 'error' => $error } if $error; - $svc_acct->set_password($p->{'new_password'}); - my $error = $svc_acct->replace(); + #my($label, $value) = $svc_acct->cust_svc->label; + #return { 'error' => $error, + # #'label' => $label, + # #'value' => $value, + # }; - my($label, $value) = $svc_acct->cust_svc->label; + } - return { 'error' => $error, - #'label' => $label, - #'value' => $value, - }; + if ( $contact ) { + + my $error = $contact->change_password($p->{'new_password'}); + + return { %$info, 'error' => $error }; # if $error; + + } + + #password changed ,so remove session, don't want it reused + _cache->remove($p->{'session_id'}); + + return { %$info, 'error' => '' }; + +} + +sub list_tickets { + my $p = shift; + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my @tickets = (); + if ( $session->{'pkgnum'} ) { + + #tickets for specific service with pkg-balances on + my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum, + 'pkgnum' => $session->{'pkgnum'} }) + or return { 'error' => 'unknown package' }; + foreach my $cust_svc ( $cust_pkg->cust_svc ) { + push @tickets, $cust_svc->tickets( $p->{status} ); + } + + } else { + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + @tickets = $cust_main->tickets( $p->{status} ); + } + + # unavoidable false laziness w/ httemplate/view/cust_main/tickets.html + if ( $FS::TicketSystem::system && FS::TicketSystem->selfservice_priority ) { + my $conf = new FS::Conf; + my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1; + +{ tickets => [ + sort { + ( + ($a->{'_selfservice_priority'} eq '') <=> + ($b->{'_selfservice_priority'} eq '') + ) || + ( $dir * + ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'}) + ) + } @tickets + ] + }; + } else { + +{ tickets => \@tickets }; + } } @@ -2772,10 +3400,10 @@ sub get_ticket { my($context, $session, $custnum) = _custoragent_session_custnum($p); return { 'error' => $session } if $context eq 'error'; - 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'; +# 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; @@ -2847,8 +3475,8 @@ sub adjust_ticket_priority { my($context, $session, $custnum) = _custoragent_session_custnum($p); return { 'error' => $session } if $context eq 'error'; - warn "$me adjust_ticket_priority: initializing ticket system\n" if $DEBUG; - FS::TicketSystem->init; +# warn "$me adjust_ticket_priority: initializing ticket system\n" if $DEBUG; +# FS::TicketSystem->init; my $ss_priority = FS::TicketSystem->selfservice_priority; return { 'error' => 'adjust_ticket_priority configuration error' }