+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 = ();
+ foreach my $part_export (
+ map { qsearch ( 'part_export', { 'exporttype' => $_ } ) }
+ qw( sqlradius sqlradius_withdomain )
+ ) {
+ push @usage, @ { $part_export->usage_sessions($begin, $end, $svc_acct) };
+ }
+ (@usage);
+}
+
+sub list_svc_usage {
+ _usage_details(\&_list_svc_usage, @_);
+}
+
+sub _list_support_usage {
+ my($svc_acct, $begin, $end) = @_;
+ my @usage = ();
+ foreach ( grep { $begin <= $_->_date && $_->_date <= $end }
+ qsearch('acct_rt_transaction', { 'svcnum' => $svc_acct->svcnum })
+ ) {
+ push @usage, { 'seconds' => $_->seconds,
+ 'support' => $_->support,
+ '_date' => $_->_date,
+ 'id' => $_->transaction_id,
+ 'creator' => $_->creator,
+ 'subject' => $_->subject,
+ 'status' => $_->status,
+ 'ticketid' => $_->ticketid,
+ };
+ }
+ (@usage);
+}
+
+sub list_support_usage {
+ _usage_details(\&_list_support_usage, @_);
+}
+
+sub _list_cdr_usage {
+ # 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_x, $begin, $end, %opt) = @_;
+ map [ $_->downstream_csv(%opt, 'keeparray' => 1) ],
+ $svc_x->get_cdrs(
+ 'begin' => $begin,
+ 'end' => $end,
+ 'disable_charged_party' => 1,
+ %opt
+ );
+}
+
+sub list_cdr_usage {
+ my $p = shift;
+ _usage_details( \&_list_cdr_usage, $p );
+}
+
+sub _usage_details {
+ my($callback, $p, %opt) = @_;
+ my $conf = FS::Conf->new;
+
+ if ( $conf->exists('selfservice_hide-usage') ) {
+ return { 'error' => 'Viewing usage is not allowed.' };
+ }
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my $search = { 'svcnum' => $p->{'svcnum'} };
+ $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
+
+ my $cust_svc = qsearchs( 'cust_svc', $search );
+ return { 'error' => 'No service selected in list_svc_usage' }
+ unless $cust_svc;
+
+ my $svc_x = $cust_svc->svc_x;
+ my $svcdb = $svc_x->table;
+ my $cust_pkg = $cust_svc->cust_pkg;
+ my $freq = $cust_pkg->part_pkg->freq;
+ my %callback_opt;
+ my $header = [];
+ if ( $svcdb eq 'svc_phone' or $svcdb eq 'svc_pbx' ) {
+ 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';
+ }
+
+ $callback_opt{format} = $format;
+ $callback_opt{use_clid} = 1;
+ $header = [ split(',', FS::cdr::invoice_header($format) ) ];
+ }
+
+ my $start = $cust_pkg->setup;
+ #my $end = $cust_pkg->bill; # or time?
+ my $end = time;
+
+ unless ( $p->{beginning} ) {
+ $p->{beginning} = $cust_pkg->last_bill;
+ $p->{ending} = $end;
+ }
+
+ 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
+ #
+ #change this bit to use Date::Manip? CAREFUL with timezones (see
+ # mailing list archive)
+ my ($nsec,$nmin,$nhour,$nmday,$nmon,$nyear) =
+ (localtime($p->{ending}) )[0,1,2,3,4,5];
+ my ($psec,$pmin,$phour,$pmday,$pmon,$pyear) =
+ (localtime($p->{beginning}) )[0,1,2,3,4,5];
+
+ if ( $freq =~ /^\d+$/ ) {
+ $nmon += $freq;
+ until ( $nmon < 12 ) { $nmon -= 12; $nyear++; }
+ $pmon -= $freq;
+ until ( $pmon >= 0 ) { $pmon += 12; $pyear--; }
+ } elsif ( $freq =~ /^(\d+)w$/ ) {
+ my $weeks = $1;
+ $nmday += $weeks * 7;
+ $pmday -= $weeks * 7;
+ } elsif ( $freq =~ /^(\d+)d$/ ) {
+ my $days = $1;
+ $nmday += $days;
+ $pmday -= $days;
+ } elsif ( $freq =~ /^(\d+)h$/ ) {
+ my $hours = $1;
+ $nhour += $hours;
+ $phour -= $hours;
+ } else {
+ return { 'error' => "unparsable frequency: ". $freq };
+ }
+
+ my $previous = timelocal_nocheck($psec,$pmin,$phour,$pmday,$pmon,$pyear);
+ my $next = timelocal_nocheck($nsec,$nmin,$nhour,$nmday,$nmon,$nyear);
+
+ {
+ 'error' => '',
+ 'svcnum' => $p->{svcnum},
+ 'beginning' => $p->{beginning},
+ 'ending' => $p->{ending},
+ 'inbound' => $p->{inbound},
+ 'previous' => ($previous > $start) ? $previous : $start,
+ 'next' => ($next < $end) ? $next : $end,
+ 'header' => $header,
+ 'usage' => \@usage,
+ };
+}
+
+sub order_pkg {
+ my $p = shift;
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my $search = { 'custnum' => $custnum };
+ $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
+ my $cust_main = qsearchs('cust_main', $search )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my $status = $cust_main->status;
+
+ my %order_pkg_options = ();
+ if ( $p->{locationnum} > 0 ) {
+ $order_pkg_options{locationnum} = delete($p->{locationnum});
+ } elsif ( $p->{address1} ) {
+ $order_pkg_options{'cust_location'} = new FS::cust_location {
+ map { $_ => $p->{$_} }
+ qw( address1 address2 city county state zip country )
+ };
+ }
+
+ #false laziness w/ClientAPI/Signup.pm
+
+ my $cust_pkg = new FS::cust_pkg ( {
+ 'custnum' => $custnum,
+ 'pkgpart' => $p->{'pkgpart'},
+ 'quantity' => $p->{'quantity'} || 1,
+ } );
+ my $error = $cust_pkg->check;
+ return { 'error' => $error } if $error;
+
+ my @svc = ();
+ unless ( $p->{'svcpart'} eq 'none' ) {
+
+ my $svcdb;
+ my $svcpart = '';
+ if ( $p->{'svcpart'} =~ /^(\d+)$/ ) {
+ $svcpart = $1;
+ my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
+ return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc;
+ $svcdb = $part_svc->svcdb;
+ } else {
+ $svcdb = 'svc_acct';
+ }
+ $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb);
+
+ my %fields = (
+ 'svc_acct' => [ qw( username domsvc _password sec_phrase popnum ) ],
+ 'svc_domain' => [ qw( domain ) ],
+ 'svc_phone' => [ qw( phonenum pin sip_password phone_name ) ],
+ 'svc_external' => [ qw( id title ) ],
+ 'svc_pbx' => [ qw( id title ) ],
+ );
+
+ my $svc_x = "FS::$svcdb"->new( {
+ 'svcpart' => $svcpart,
+ map { $_ => $p->{$_} } @{$fields{$svcdb}}
+ } );
+
+ if ( $svcdb eq 'svc_acct' && exists($p->{"snarf_machine1"}) ) {
+ my @acct_snarf;
+ my $snarfnum = 1;
+ while ( length($p->{"snarf_machine$snarfnum"}) ) {
+ my $acct_snarf = new FS::acct_snarf ( {
+ 'machine' => $p->{"snarf_machine$snarfnum"},
+ 'protocol' => $p->{"snarf_protocol$snarfnum"},
+ 'username' => $p->{"snarf_username$snarfnum"},
+ '_password' => $p->{"snarf_password$snarfnum"},
+ } );
+ $snarfnum++;
+ push @acct_snarf, $acct_snarf;
+ }
+ $svc_x->child_objects( \@acct_snarf );
+ }
+
+ my $y = $svc_x->setdefault; # arguably should be in new method
+ return { 'error' => $y } if $y && !ref($y);
+
+ $error = $svc_x->check;
+ return { 'error' => $error } if $error;
+
+ push @svc, $svc_x;
+
+ }
+
+ $error = $cust_main->order_pkg(
+ 'cust_pkg' => $cust_pkg,
+ 'svcs' => \@svc,
+ 'noexport' => 1,
+ %order_pkg_options,
+ );
+ return { 'error' => $error } if $error;
+
+ my $conf = new FS::Conf;
+ if ( $conf->exists('signup_server-realtime') ) {
+
+ my $bill_error = _do_bop_realtime( $cust_main, $status, 'collect'=>$p->{run_bill_events} );
+
+ if ($bill_error) {
+ $cust_pkg->cancel('quiet'=>1);
+ return $bill_error;
+ } else {
+ $cust_pkg->reexport;
+ }
+
+ } else {
+ $cust_pkg->reexport;
+ }
+
+ my $svcnum = $svc[0] ? $svc[0]->svcnum : '';
+
+ return { error=>'', pkgnum=>$cust_pkg->pkgnum, svcnum=>$svcnum };
+
+}
+
+sub change_pkg {
+ my $p = shift;
+
+ 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 )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my $status = $cust_main->status;
+ my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $p->{pkgnum} } )
+ or return { 'error' => "unknown package $p->{pkgnum}" };
+
+ #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 $err_or_cust_pkg = $cust_pkg->change( 'pkgpart' => $p->{'pkgpart'},
+ 'quantity' => $p->{'quantity'} || 1,
+ );
+
+ return { error=>$err_or_cust_pkg, pkgnum=>$cust_pkg->pkgnum }
+ unless ref($err_or_cust_pkg);
+
+ if ( $conf->exists('signup_server-realtime') ) {
+
+ my $bill_error = _do_bop_realtime( $cust_main, $status, 'no_invoice_void'=>1 );
+
+ if ($bill_error) {
+ $err_or_cust_pkg->suspend;
+ return $bill_error;
+ } else {
+ $err_or_cust_pkg->reexport;
+ }
+
+ } else {
+ $err_or_cust_pkg->reexport;
+ }
+
+ return { error => '', pkgnum => $cust_pkg->pkgnum };
+
+}
+
+sub order_recharge {
+ my $p = shift;
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my $search = { 'custnum' => $custnum };
+ $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
+ my $cust_main = qsearchs('cust_main', $search )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my $status = $cust_main->status;
+ my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $p->{'svcnum'} } )
+ or return { 'error' => "unknown service " . $p->{'svcnum'} };
+
+ my $svc_x = $cust_svc->svc_x;
+ my $part_pkg = $cust_svc->cust_pkg->part_pkg;
+
+ my %vhash =
+ map { $_ =~ /^recharge_(.*)$/; $1, $part_pkg->option($_, 1) }
+ qw ( recharge_seconds recharge_upbytes recharge_downbytes
+ recharge_totalbytes );
+ my $amount = $part_pkg->option('recharge_amount', 1);
+
+ my ($l, $v, $d) = $cust_svc->label; # blah
+ my $pkg = "Recharge $v";
+
+ my $bill_error = $cust_main->charge($amount, $pkg,
+ "time: $vhash{seconds}, up: $vhash{upbytes}," .
+ "down: $vhash{downbytes}, total: $vhash{totalbytes}",
+ $part_pkg->taxclass); #meh
+
+ my $conf = new FS::Conf;
+ if ( $conf->exists('signup_server-realtime') && !$bill_error ) {
+
+ $bill_error = _do_bop_realtime( $cust_main, $status );
+
+ if ($bill_error) {
+ return $bill_error;
+ } else {
+ my $error = $svc_x->recharge (\%vhash);
+ return { 'error' => $error } if $error;
+ }
+
+ } else {
+ my $error = $bill_error;
+ $error ||= $svc_x->recharge (\%vhash);
+ return { 'error' => $error } if $error;
+ }
+
+ return { error => '', svc => $cust_svc->part_svc->svc };
+
+}
+
+sub _do_bop_realtime {
+ my ($cust_main, $status, %opt) = @_;
+
+ my $old_balance = $cust_main->balance;
+
+ my @cust_bill;
+ my $bill_error = $cust_main->bill(
+ 'return_bill' => \@cust_bill,
+ );
+
+ $bill_error ||= $cust_main->apply_payments_and_credits;
+
+ $bill_error ||= $cust_main->realtime_collect('selfservice' => 1);
+
+ if ( $cust_main->balance > $old_balance
+ && $cust_main->balance > 0
+ && ( $cust_main->has_cust_payby_auto || $status eq 'suspended' )
+ )
+ {
+ unless ( $opt{'no_invoice_void'} ) {
+
+ #this used to apply a credit, but now we can void invoices...
+ foreach my $cust_bill (@cust_bill) {
+ my $voiderror = $cust_bill->void('automatic payment failed');
+ warn "Error voiding cust bill after decline: $voiderror" if $voiderror;
+ }
+
+ }
+
+ return { 'error' => '_decline', 'bill_error' => $bill_error };
+ }
+
+ if ( $opt{'collect'} ) {
+ my $collect_error = $cust_main->collect();
+ return { 'error' => '_decline', 'bill_error' => $collect_error }
+ if $collect_error; #?
+ }
+
+ '';
+}
+
+sub renew_info {
+ my $p = shift;
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my @cust_pkg = sort { $a->bill <=> $b->bill }
+ grep { $_->part_pkg->freq ne '0' }
+ $cust_main->ncancelled_pkgs;
+
+ #return { 'error' => 'No active packages to renew.' } unless @cust_pkg;
+
+ my $total = $cust_main->balance;
+
+ my @array = map {
+ my $bill = $_->bill;
+ $total += $_->part_pkg->base_recur($_, \$bill);
+ my $renew_date = $_->part_pkg->add_freq($_->bill);
+ {
+ 'pkgnum' => $_->pkgnum,
+ 'amount' => sprintf('%.2f', $total),
+ 'bill_date' => $_->bill,
+ 'bill_date_pretty' => time2str('%x', $_->bill),
+ 'renew_date' => $renew_date,
+ 'renew_date_pretty' => time2str('%x', $renew_date),
+ 'expire_date' => $_->expire,
+ 'expire_date_pretty' => time2str('%x', $_->expire),
+ };
+ }
+ @cust_pkg;
+
+ return { 'dates' => \@array };
+
+}
+
+sub payment_info_renew_info {
+ my $p = shift;
+ my $renew_info = renew_info($p);
+ my $payment_info = payment_info($p);
+ return { %$renew_info,
+ %$payment_info,
+ };
+}
+
+sub order_renew {
+ my $p = shift;
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my $date = $p->{'date'};
+
+ my $now = time;
+
+ #freeside-daily -n -d $date fs_daily $custnum
+ $cust_main->bill_and_collect( 'time' => $date,
+ 'invoice_time' => $now,
+ 'actual_time' => $now,
+ 'check_freq' => '1d',
+ );
+
+ return { 'error' => '' };
+
+}
+
+sub suspend_pkg {
+ my $p = shift;
+ my $session = _cache->get($p->{'session_id'})
+ or return { 'error' => "Can't resume session" }; #better error message
+
+ my $custnum = $session->{'custnum'};
+
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my $conf = new FS::Conf;
+ my $reasonnum =
+ $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum)
+ or return { 'error' => 'Permission denied' };
+
+ my $pkgnum = $p->{'pkgnum'};
+
+ my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
+ 'pkgnum' => $pkgnum, } )
+ or return { 'error' => "unknown pkgnum $pkgnum" };
+
+ my $error = $cust_pkg->suspend(reason => $reasonnum);
+ return { 'error' => $error };
+
+}
+
+sub cancel_pkg {
+ my $p = shift;
+ my $session = _cache->get($p->{'session_id'})
+ or return { 'error' => "Can't resume session" }; #better error message
+
+ my $custnum = $session->{'custnum'};
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my $pkgnum = $p->{'pkgnum'};
+ my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
+ 'pkgnum' => $pkgnum, } )
+ or return { 'error' => "unknown pkgnum $pkgnum" };
+
+ my $error = $cust_pkg->cancel( 'quiet' => 1,
+ 'date' => $p->{'date'},
+ );
+ return { 'error' => $error };
+}
+
+sub provision_phone {
+ my $p = shift;
+ my @bulkdid;
+ @bulkdid = @{$p->{'bulkdid'}} if $p->{'bulkdid'};
+
+ #editing an existing phone number
+ 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'});
+ $p->{'lnp_status'} = "portingin";
+ return _provision( 'FS::svc_phone',
+ [qw(lnp_desired_due_date lnp_other_provider
+ lnp_other_provider_account phonenum countrycode lnp_status)],
+ [qw(phonenum countrycode)],
+ $p,
+ @_
+ );
+ }
+
+ # single DID order (the usual case)
+ unless (scalar(@bulkdid)) {
+ return _provision( 'FS::svc_phone',
+ [qw(phonenum countrycode)],
+ [qw(phonenum countrycode)],
+ $p,
+ @_
+ );
+ }
+
+ # bulk DID order case
+ my $error;
+ foreach my $did ( @bulkdid ) {
+ $did =~ s/[^0-9]//g;
+ $error = _provision( 'FS::svc_phone',
+ [qw(phonenum countrycode)],
+ [qw(phonenum countrycode)],
+ {
+ 'pkgnum' => $p->{'pkgnum'},
+ 'svcpart' => $p->{'svcpart'},
+ 'phonenum' => $did,
+ 'countrycode' => $p->{'countrycode'},
+ 'session_id' => $p->{'session_id'},
+ }
+ );
+ return $error if ($error->{'error'} && length($error->{'error'}) > 1);
+ }
+ { 'bulkdid' => [ @bulkdid ], 'svc' => $error->{'svc'} }
+}
+
+sub provision_pbx {
+ my $p = shift;
+ warn "provision_pbx called\n"
+ if $DEBUG;
+
+ warn "provision_pbx calling _provision\n"
+ if $DEBUG;
+ _provision( 'FS::svc_pbx',
+ [qw(id title max_extensions max_simultaneous ip_addr)],
+ [qw(id title max_extensions max_simultaneous ip_addr)],
+ $p,
+ @_
+ );
+}
+
+sub provision_acct {
+ my $p = shift;
+ warn "provision_acct called\n"
+ if $DEBUG;
+
+ return { 'error' => gettext('passwords_dont_match') }
+ if $p->{'_password'} ne $p->{'_password2'};
+ return { 'error' => gettext('empty_password') }
+ unless length($p->{'_password'});
+
+ if ($p->{'domsvc'}) {
+ my %domains = domain_select_hash FS::svc_acct(map { $_ => $p->{$_} }
+ qw ( svcpart pkgnum ) );
+ return { 'error' => gettext('invalid_domain') }
+ unless ($domains{$p->{'domsvc'}});
+ }
+
+ warn "provision_acct calling _provision\n"
+ if $DEBUG;
+ _provision( 'FS::svc_acct',
+ [qw(username _password domsvc)],
+ [qw(username _password domsvc)],
+ $p,
+ @_
+ );
+}
+
+sub provision_external {
+ my $p = shift;
+ #_provision( 'FS::svc_external', [qw(id title)], [qw(id title)], $p, @_ );
+ _provision( 'FS::svc_external',
+ [],
+ [qw(id title)],
+ $p,
+ @_
+ );
+}
+
+sub provision_forward {
+ my $p = shift;
+ _provision( 'FS::svc_forward',
+ ['srcsvc','src','dstsvc','dst'],
+ [],
+ $p,
+ );
+}
+
+sub _provision {
+ my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
+ warn "_provision called for $class\n"
+ if $DEBUG;
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my $search = { 'custnum' => $custnum };
+ $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
+ my $cust_main = qsearchs('cust_main', $search )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my $pkgnum = $p->{'pkgnum'};
+
+ warn "searching for custnum $custnum pkgnum $pkgnum\n"
+ if $DEBUG;
+ my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
+ 'pkgnum' => $pkgnum,
+ } )
+ or return { 'error' => "unknown pkgnum $pkgnum" };
+
+ warn "searching for svcpart ". $p->{'svcpart'}. "\n"
+ if $DEBUG;
+ my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
+ or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
+
+ return { error=> 'svcpart '. $p->{'svcpart'}. " is not a $class definition" }
+ if $class ne 'FS::'. $part_svc->svcdb;
+
+ warn "creating $class record\n"
+ if $DEBUG;
+ my $svc_x = $class->new( {
+ 'pkgnum' => $p->{'pkgnum'},
+ 'svcpart' => $p->{'svcpart'},
+ map { $_ => $p->{$_} } @$fields
+ } );
+
+ my %insert_args = ();
+ #i shouldn't be a special case here (pass an option or something)
+ if ( $class eq 'FS::svc_phone'
+ && grep length($p->{$_}), @location_editable_fields
+ )
+ {
+ $insert_args{'cust_location'} = new FS::cust_location {
+ map { $_ => $p->{$_} } @location_editable_fields
+ };
+ }
+
+ warn "inserting $class record\n"
+ if $DEBUG;
+ my $error = $svc_x->insert(%insert_args);
+
+ unless ( $error ) {
+ warn "finding inserted record for svcnum ". $svc_x->svcnum. "\n"
+ if $DEBUG;
+ $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum })
+ }
+
+ my $return = { 'svc' => $part_svc->svc,
+ 'error' => $error,
+ map { $_ => $svc_x->get($_) } @$return_fields
+ };
+ warn "_provision returning ". Dumper($return). "\n"
+ if $DEBUG;
+ return $return;
+
+}
+
+sub part_svc_info {
+ my $p = shift;
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my $search = { 'custnum' => $custnum };
+ $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
+ my $cust_main = qsearchs('cust_main', $search )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my $pkgnum = $p->{'pkgnum'};
+
+ my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
+ 'pkgnum' => $pkgnum,
+ } )
+ or return { 'error' => "unknown pkgnum $pkgnum" };
+
+ my $svcpart = $p->{'svcpart'};
+
+ my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart,
+ 'svcpart' => $svcpart, } )
+ or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" };
+ my $part_svc = $pkg_svc->part_svc;
+
+ my $conf = new FS::Conf;
+
+ my $ret = {
+ 'svc' => $part_svc->svc,
+ 'svcdb' => $part_svc->svcdb,
+ 'pkgnum' => $pkgnum,
+ 'svcpart' => $svcpart,
+ 'custnum' => $custnum,
+
+ 'security_phrase' => 0, #XXX !
+ 'svc_acct_pop' => [], #XXX !
+ 'popnum' => '',
+ 'init_popstate' => '',
+ 'popac' => '',
+ 'acstate' => '',
+
+ 'small_custview' =>
+ small_custview( $cust_main, $conf->config('countrydefault') ),
+
+ };
+
+ 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;
+ }
+ }
+
+ if ($ret->{'svcdb'} eq 'svc_forward') {
+ $ret->{'forward_emails'} = {$cust_pkg->forward_emails()};
+ }
+
+ $ret;
+}
+
+sub unprovision_svc {
+ my $p = shift;
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my $search = { 'custnum' => $custnum };
+ $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
+ my $cust_main = qsearchs('cust_main', $search )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ my $svcnum = $p->{'svcnum'};
+
+ my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svcnum, } )
+ or return { 'error' => "unknown svcnum $svcnum" };
+
+ return { 'error' => "Service $svcnum does not belong to customer $custnum" }
+ unless $cust_svc->cust_pkg->custnum == $custnum;
+
+ my $conf = new FS::Conf;
+
+ return { 'svc' => $cust_svc->part_svc->svc,
+ 'error' => $cust_svc->cancel,
+ 'small_custview' =>
+ small_custview( $cust_main, $conf->config('countrydefault') ),
+ };
+
+}
+
+sub myaccount_passwd {
+ my $p = shift;
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ return { 'error' => "New passwords don't match." }
+ if $p->{'new_password'} ne $p->{'new_password2'};
+
+ return { 'error' => 'Enter new password' }
+ unless length($p->{'new_password'});
+
+ #my $search = { 'custnum' => $custnum };
+ #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
+ $custnum =~ /^(\d+)$/ or die "illegal custnum";
+ my $search = " AND custnum = $1";
+ $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
+
+ my $svc_acct = qsearchs( {
+ 'table' => 'svc_acct',
+ 'addl_from' => 'LEFT JOIN cust_svc USING ( svcnum ) '.
+ 'LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ 'LEFT JOIN cust_main USING ( custnum ) ',
+ 'hashref' => { 'svcnum' => $p->{'svcnum'}, },
+ 'extra_sql' => $search, #important
+ } )
+ or return { 'error' => "Service not found" };
+
+ 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 ||= $svc_acct->is_password_allowed($p->{'new_password'})
+ || $svc_acct->set_password($p->{'new_password'})
+ || $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 && qsearchs('cust_contact', { contactnum=> $contact->contactnum,
+ custnum => $custnum,
+ selfservice_access => 'Y',
+ }
+ )
+ ) {
+ #svc_acct was successful but this one returns an error? "shouldn't happen"
+ #don't recheck is_password_allowed here; if the svc_acct password was
+ #legal, that's good enough
+ $error ||= $contact->change_password($p->{'new_password'});
+ }
+
+ my($label, $value) = $svc_acct->cust_svc->label;
+
+ return { 'error' => $error,
+ 'label' => $label,
+ 'value' => $value,
+ };
+
+}
+
+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'});
+
+ if ( $contact ) {
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
+ }
+
+ #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;
+
+ }
+
+ return { %$info, 'error' => 'Multi-customer contacts incompatible with customer-based verification' }
+ if ! $cust_main && $verification ne 'email';
+
+ 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;
+
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+ if scalar(@cust_contact) == 1;
+ 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" };
+
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+ if scalar(@cust_contact) == 1;
+ $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
+ }
+
+ $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
+ }
+}
+
+#--
+
+sub _custoragent_session_custnum {
+ my $p = shift;
+
+ my($context, $session, $custnum);
+ if ( $p->{'session_id'} ) {
+
+ $context = 'customer';
+ $session = _cache->get($p->{'session_id'})
+ or return ( 'error' => "Can't resume session" ); #better error message
+ $custnum = $session->{'custnum'};
+
+ } elsif ( $p->{'agent_session_id'} ) {
+
+ $context = 'agent';
+ my $agent_cache = new FS::ClientAPI_SessionCache( {
+ 'namespace' => 'FS::ClientAPI::Agent',
+ } );
+ $session = $agent_cache->get($p->{'agent_session_id'})
+ or return ( 'error' => "Can't resume session" ); #better error message
+ $custnum = $p->{'custnum'};
+
+ } else {
+ $context = 'error';
+ return ( 'error' => "Can't resume session" ); #better error message
+ }
+
+ ($context, $session, $custnum);
+
+}
+