+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 { %$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' => $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;
+
+ } elsif ( $p->{'username'} ) { #old style, looks in svc_acct only
+
+ my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
+ or return { %$info, '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 { %$info, 'error' => 'Account not found' };
+
+ $cust_main = $cust_pkg->cust_main;
+
+ }
+
+ my %verify = (
+ 'email' => sub { 1; },
+ '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 { %$info, 'error' => 'Account not found' };
+
+ }
+
+ #okay, we're verified
+
+ if ( $contact ) {
+
+ 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' => $svc_acct->cust_main->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 }; #????
+ }
+
+ }
+
+ return { %$info, '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
+
+ if ( $reset_session->{'svcnum'} ) {
+
+ my $svcnum = $reset_session->{'svcnum'};
+
+ 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' }; #??
+
+ }
+
+}
+
+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' };
+
+ 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 { %$info, 'error' => 'Enter new password' }
+ unless length($p->{'new_password'});
+
+ if ( $svc_acct ) {
+
+ my $error ||= $svc_acct->is_password_allowed($p->{'new_password'})
+ || $svc_acct->set_password($p->{'new_password'})
+ || $svc_acct->replace();
+
+ return { %$info, 'error' => $error } if $error;
+
+ #my($label, $value) = $svc_acct->cust_svc->label;
+ #return { 'error' => $error,
+ # #'label' => $label,
+ # #'value' => $value,
+ # };
+
+ }
+
+ if ( $contact ) {
+
+ my $error = $contact->is_password_allowed($p->{'new_password'})
+ || $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 validate_passwd {
+ my $p = shift;
+
+ my %result;
+ %result = ( 'fieldid' => $p->{'fieldid'} )
+ if $p->{'fieldid'} =~ /^\w+$/;
+
+ return { %result, 'password_invalid' => 'Enter new password' }
+ unless length($p->{'check_password'});
+
+ my $svc_acct;
+ if ($p->{'svcnum'}) {
+ # false laziness with myaccount_passwd
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { %result, 'error' => $session } if $context eq 'error';
+
+ $custnum =~ /^(\d+)$/ or die "illegal custnum";
+ my $search = " AND custnum = $1";
+ $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
+
+ $svc_acct = qsearchs( {
+ 'table' => 'svc_acct',
+ 'addl_from' => 'LEFT JOIN cust_svc USING ( svcnum ) '.
+ 'LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ 'LEFT JOIN cust_main USING ( custnum ) ',
+ 'hashref' => { 'svcnum' => $p->{'svcnum'}, },
+ 'extra_sql' => $search, #important
+ } )
+ or return { %result, 'error' => "Service not found" };
+ # end false laziness
+ }
+
+ unless ($svc_acct) {
+ my $conf = new FS::Conf;
+ my $agentnum = $p->{'agentnum'};
+ return { %result, 'password_valid' => 1 }
+ if $conf->config_bool('password-insecure', $p->{'agentnum'});
+ }
+
+ $svc_acct ||= new FS::svc_acct {};
+
+ my $error = $svc_acct->is_password_allowed($p->{'check_password'});
+ return { %result, 'password_invalid' => $error } if $error;
+ return { %result, 'password_valid' => 1 };
+}
+
+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 };
+ }
+
+}
+
+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: successful: ". $err_or_ticket->id. "\n"
+ if $DEBUG;
+ return { 'error' => '',
+ 'ticket_id' => $err_or_ticket->id,
+ };
+ } else {
+ warn "$me create_ticket: unsuccessful: $err_or_ticket\n"
+ if $DEBUG;
+ return { 'error' => $err_or_ticket };
+ }
+
+
+}
+
+sub did_report {
+ my $p = shift;
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ return { error => 'requested format not implemented' }
+ unless ($p->{'format'} eq 'csv' || $p->{'format'} eq 'xls');
+
+ my $conf = new FS::Conf;
+ my $age_threshold = 0;
+ $age_threshold = time() - $conf->config('selfservice-recent-did-age')
+ if ($p->{'recentonly'} && $conf->exists('selfservice-recent-did-age'));
+
+ 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" };
+
+# does it make more sense to just run one sql query for this instead of all the
+# insanity below? would increase performance greately for large data sets?
+ my @svc_phone = ();
+ foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
+ my @part_svc = $cust_pkg->part_svc;
+ foreach my $part_svc ( @part_svc ) {
+ if($part_svc->svcdb eq 'svc_phone'){
+ my @cust_pkg_svc = @{$part_svc->cust_pkg_svc};
+ foreach my $cust_pkg_svc ( @cust_pkg_svc ) {
+ push @svc_phone, $cust_pkg_svc->svc_x
+ if $cust_pkg_svc->date_inserted >= $age_threshold;
+ }
+ }
+ }
+ }
+
+ my $csv;
+ my $xls;
+ my($xls_r,$xls_c) = (0,0);
+ my $xls_workbook;
+ my $content = '';
+ my @fields = qw( countrycode phonenum pin sip_password phone_name );
+ if($p->{'format'} eq 'csv') {
+ $csv = new Text::CSV_XS { 'always_quote' => 1,
+ 'eol' => "\n",
+ };
+ return { 'error' => 'Unable to create CSV' } unless $csv->combine(@fields);
+ $content .= $csv->string;
+ }
+ elsif($p->{'format'} eq 'xls') {
+ my $XLS1 = new IO::Scalar \$content;
+ $xls_workbook = Spreadsheet::WriteExcel->new($XLS1)
+ or return { 'error' => "Error opening .xls file: $!" };
+ $xls = $xls_workbook->add_worksheet('DIDs');
+ foreach ( @fields ) {
+ $xls->write(0,$xls_c++,$_);
+ }
+ $xls_r++;
+ }
+
+ foreach my $svc_phone ( @svc_phone ) {
+ my @cols = map { $svc_phone->$_ } @fields;
+ if($p->{'format'} eq 'csv') {
+ return { 'error' => 'Unable to create CSV' }
+ unless $csv->combine(@cols);
+ $content .= $csv->string;
+ }
+ elsif($p->{'format'} eq 'xls') {
+ $xls_c = 0;
+ foreach ( @cols ) {
+ $xls->write($xls_r,$xls_c++,$_);
+ }
+ $xls_r++;
+ }
+ }
+
+ $xls_workbook->close() if $p->{'format'} eq 'xls';
+
+ { content => $content, format => $p->{'format'}, };
+}
+
+sub get_ticket {
+ my $p = shift;
+ 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';
+
+ # 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'})) {
+ 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] );
+ }
+
+ warn "$me get_ticket: getting ticket history\n" if $DEBUG;
+ my $err_or_ticket = FS::TicketSystem->get_ticket(
+ $rt_session,
+ 'ticket_id' => $p->{'ticket_id'},
+ );
+
+ 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'},
+ };
+}
+
+sub adjust_ticket_priority {
+ my $p = shift;
+ 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;
+ my $ss_priority = FS::TicketSystem->selfservice_priority;
+
+ return { 'error' => 'adjust_ticket_priority configuration error' }
+ if $FS::TicketSystem::system ne 'RT_Internal'
+ or !$ss_priority;
+
+ 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,
+ );
+ }
+
+ $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
+ }
+}
+