selfservice command to suspend packages, RT#9989
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
index 9d031be..29e374c 100644 (file)
@@ -1,18 +1,21 @@
 package FS::ClientAPI::MyAccount;
 
+use 5.008; #require 5.8+ for Time::Local 1.05+
 use strict;
-use vars qw( $cache $DEBUG );
+use vars qw( $cache $DEBUG $me );
 use subs qw( _cache _provision );
 use Data::Dumper;
 use Digest::MD5 qw(md5_hex);
 use Date::Format;
 use Business::CreditCard;
 use Time::Duration;
+use Time::Local qw(timelocal_nocheck);
 use FS::UI::Web::small_custview qw(small_custview); #less doh
 use FS::UI::Web;
 use FS::UI::bytecount qw( display_bytecount );
 use FS::Conf;
-use FS::Record qw(qsearch qsearchs);
+#use FS::UID qw(dbh);
+use FS::Record qw(qsearch qsearchs dbh);
 use FS::Msgcat qw(gettext);
 use FS::Misc qw(card_types);
 use FS::ClientAPI_SessionCache;
@@ -28,16 +31,10 @@ use FS::cust_pkg;
 use FS::payby;
 use FS::acct_rt_transaction;
 use HTML::Entities;
+use FS::TicketSystem;
 
 $DEBUG = 0;
-
-#false laziness with FS::cust_main
-BEGIN {
-  eval "use Time::Local;";
-  die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
-    if $] < 5.006 && !defined($Time::Local::VERSION);
-  eval "use Time::Local qw(timelocal_nocheck);";
-}
+$me = '[FS::ClientAPI::MyAccount]';
 
 use vars qw( @cust_main_editable_fields );
 @cust_main_editable_fields = qw(
@@ -56,21 +53,72 @@ sub _cache {
 }
 
 sub skin_info {
-  #my $p = shift;
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  #return { 'error' => $session } if $context eq 'error';
+
+  my $agentnum = '';
+  if ( $context eq 'customer' ) {
+
+    my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?')
+      or die dbh->errstr;
+
+    $sth->execute($custnum) or die $sth->errstr;
+
+    $agentnum = $sth->fetchrow_arrayref->[0]
+      or die "no agentnum for custnum $custnum";
+
+  #} elsif ( $context eq 'agent' ) {
+  } elsif ( $p->{'agentnum'} =~ /^(\d+)$/ ) {
+    $agentnum = $1;
+  }
 
   my $conf = new FS::Conf;
 
-  my %skin = (
-    'head'         => join("\n", $conf->config('selfservice-head') ),
-    'body_header'  => join("\n", $conf->config('selfservice-body_header') ),
-    'body_footer'  => join("\n", $conf->config('selfservice-body_footer') ),
-    'body_bgcolor' => scalar( $conf->config('selfservice-body_bgcolor') ),
-    'box_bgcolor'  => scalar( $conf->config('selfservice-box_bgcolor')  ),
+  #false laziness w/Signup.pm
 
-    'company_name'   => scalar($conf->config('company_name')),
-  );
+  my $skin_info_cache_agent = _cache->get("skin_info_cache_agent$agentnum");
+
+  if ( $skin_info_cache_agent ) {
+
+    warn "$me loading cached skin info for agentnum $agentnum\n"
+      if $DEBUG > 1;
 
-  \%skin;
+  } else {
+
+    warn "$me populating skin info cache for agentnum $agentnum\n"
+      if $DEBUG > 1;
+
+    $skin_info_cache_agent = {
+      'agentnum' => $agentnum,
+      ( map { $_ => scalar( $conf->config($_, $agentnum) ) }
+        qw( company_name ) ),
+      ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) }
+        qw( body_bgcolor box_bgcolor
+            text_color link_color vlink_color hlink_color alink_color
+            font title_color title_align title_size menu_bgcolor menu_fontsize
+          )
+      ),
+      ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) }
+        qw( menu_skipblanks menu_skipheadings menu_nounderline )
+      ),
+      ( map { $_ => scalar($conf->config_binary("selfservice-$_", $agentnum)) }
+        qw( title_left_image title_right_image
+            menu_top_image menu_body_image menu_bottom_image
+          )
+      ),
+      'logo' => scalar($conf->config_binary('logo.png', $agentnum )),
+      ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) }
+        qw( head body_header body_footer company_address ) ),
+    };
+
+    _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent);
+
+  }
+
+  #{ %$skin_info_cache_agent };
+  $skin_info_cache_agent;
 
 }
 
@@ -80,6 +128,7 @@ sub login_info {
   my $conf = new FS::Conf;
 
   my %info = (
+    %{ skin_info($p) },
     'phone_login'  => $conf->exists('selfservice_server-phone_login'),
     'single_domain'=> scalar($conf->config('selfservice_server-single_domain')),
   );
@@ -122,16 +171,6 @@ sub login {
                            );
     return { error => 'User not found.' } unless $svc_acct;
 
-    #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 $cust_svc = $svc_acct->cust_svc;
-    my $part_pkg = $cust_svc->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('svc_acct');
-
     return { error => 'Incorrect password.' }
       unless $svc_acct->check_password($p->{'password'});
 
@@ -143,14 +182,28 @@ sub login {
     'svcnum' => $svc_x->svcnum,
   };
 
-  my $cust_pkg = $svc_x->cust_svc->cust_pkg;
+  my $cust_svc = $svc_x->cust_svc;
+  my $cust_pkg = $cust_svc->cust_pkg;
   if ( $cust_pkg ) {
     my $cust_main = $cust_pkg->cust_main;
     $session->{'custnum'} = $cust_main->custnum;
-    $session->{'pkgnum'} = $cust_pkg->pkgnum
-      if $conf->exists('pkg-balances');
+    if ( $conf->exists('pkg-balances') ) {
+      my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
+                          $cust_main->ncancelled_pkgs;
+      $session->{'pkgnum'} = $cust_pkg->pkgnum
+        if scalar(@cust_pkg) > 1;
+    }
   }
 
+  #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
+  #return { error => 'Only primary user may log in.' } 
+  #  if $conf->exists('selfservice_server-primary_only')
+  #    && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
+  my $part_pkg = $cust_pkg->part_pkg;
+  return { error => 'Only primary user may log in.' }
+    if $conf->exists('selfservice_server-primary_only')
+       && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
+
   my $session_id;
   do {
     $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
@@ -168,12 +221,61 @@ sub logout {
   my $p = shift;
   if ( $p->{'session_id'} ) {
     _cache->remove($p->{'session_id'});
-    return { 'error' => '' };
+    return { %{ skin_info($p) }, 'error' => '' };
   } else {
-    return { 'error' => "Can't resume session" }; #better error message
+    return { %{ skin_info($p) }, 'error' => "Can't resume session" }; #better error message
   }
 }
 
+sub access_info {
+  my $p = shift;
+
+  my $conf = new FS::Conf;
+
+  my $info = skin_info($p);
+
+  use vars qw( $cust_paybys ); #cache for performance
+  unless ( $cust_paybys ) {
+
+    my %cust_paybys = map { $_ => 1 }
+                      map { FS::payby->payby2payment($_) }
+                          $conf->config('signup_server-payby');
+
+    $cust_paybys = [ keys %cust_paybys ];
+
+  }
+  $info->{'cust_paybys'} = $cust_paybys;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or return { 'error' => "unknown custnum $custnum" };
+
+  $info->{hide_payment_fields} =
+  [
+    map { my $pg = '';
+          if ( FS::payby->realtime($_) ) {
+            $pg = $cust_main->agent->payment_gateway(
+              'method'  => FS::payby->payby2bop($_),
+              'nofatal' => 1,
+            );
+          }
+          $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
+        }
+    @{ $info->{cust_paybys} }
+  ];
+
+  $info->{'self_suspend_reason'} = 
+      $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum);
+
+  return { %$info,
+           'custnum'       => $custnum,
+           'access_pkgnum' => $session->{'pkgnum'},
+           'access_svcnum' => $session->{'svcnum'},
+         };
+}
+
 sub customer_info {
   my $p = shift;
 
@@ -196,21 +298,30 @@ sub customer_info {
     my $cust_main = qsearchs('cust_main', $search )
       or return { 'error' => "unknown custnum $custnum" };
 
-    $return{balance} = $cust_main->balance;
+    if ( $session->{'pkgnum'} ) { 
+      $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
+    } else {
+      $return{balance} = $cust_main->balance;
+    }
 
     $return{tickets} = [ ($cust_main->tickets) ];
 
-    my @open = map {
-                     {
-                       invnum => $_->invnum,
-                       date   => time2str("%b %o, %Y", $_->_date),
-                       owed   => $_->owed,
-                     };
-                   } $cust_main->open_cust_bill;
-    $return{open_invoices} = \@open;
+    unless ( $session->{'pkgnum'} ) {
+      my @open = map {
+                       {
+                         invnum => $_->invnum,
+                         date   => time2str("%b %o, %Y", $_->_date),
+                         owed   => $_->owed,
+                       };
+                     } $cust_main->open_cust_bill;
+      $return{open_invoices} = \@open;
+    }
 
     $return{small_custview} =
-      small_custview( $cust_main, $conf->config('countrydefault') );
+      small_custview( $cust_main,
+                      scalar($conf->config('countrydefault')),
+                      ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance
+                    );
 
     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
 
@@ -249,6 +360,11 @@ sub customer_info {
       $return{support_services} = \@support_services;
     }
 
+    if ( $conf->config('prepayment_discounts-credit_type') ) {
+      #need to eval?
+      $return{discount_terms_hash} = { $cust_main->discount_terms_hash };
+    }
+
   } elsif ( $session->{'svcnum'} ) { #no customer record
 
     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
@@ -302,7 +418,8 @@ sub edit_info {
 
     $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
 
-  }elsif ( $payby =~ /^(CHEK|DCHK)$/ ) {
+  } elsif ( $payby =~ /^(CHEK|DCHK)$/ ) {
+
     my $payinfo;
     $p->{'payinfo1'} =~ /^([\dx]+)$/
       or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
@@ -312,15 +429,15 @@ sub edit_info {
     my $payinfo2 = $1;
     $payinfo = $payinfo1. '@'. $payinfo2;
 
-    if ( $payinfo eq $cust_main->paymask ) {
-      $new->payinfo($cust_main->payinfo);
-    } else {
-      $new->payinfo($payinfo);
-    }
+    $new->payinfo( ($payinfo eq $cust_main->paymask)
+                     ? $cust_main->payinfo
+                     : $payinfo
+                 );
 
     $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' );
 
-  }elsif ( $payby =~ /^(BILL)$/ ) {
+  } elsif ( $payby =~ /^(BILL)$/ ) {
+    #no-op
   } elsif ( $payby ) {  #notyet ready
     return { 'error' => "unknown payby $payby" };
   }
@@ -350,15 +467,21 @@ sub payment_info {
   #generic
   ##
 
+  my $conf = new FS::Conf;
   use vars qw($payment_info); #cache for performance
   unless ( $payment_info ) {
 
-    my $conf = new FS::Conf;
     my %states = map { $_->state => 1 }
                    qsearch('cust_main_county', {
                      'country' => $conf->config('countrydefault') || 'US'
                    } );
 
+    my %cust_paybys = map { $_ => 1 }
+                      map { FS::payby->payby2payment($_) }
+                          $conf->config('signup_server-payby');
+
+    my @cust_paybys = keys %cust_paybys;
+
     $payment_info = {
 
       #list all counties/states/countries
@@ -374,9 +497,7 @@ sub payment_info {
       'paytypes' => [ @FS::cust_main::paytypes ],
 
       'paybys' => [ $conf->config('signup_server-payby') ],
-      'cust_paybys' => [ map { FS::payby->payby2payment($_) }
-                                 $conf->config('signup_server-payby')
-                       ],
+      'cust_paybys' => \@cust_paybys,
 
       'stateid_label' => FS::Msgcat::_gettext('stateid'),
       'stateid_state_label' => FS::Msgcat::_gettext('stateid_state'),
@@ -384,6 +505,8 @@ sub payment_info {
       'show_ss'  => $conf->exists('show_ss'),
       'show_stateid' => $conf->exists('show_stateid'),
       'show_paystate' => $conf->exists('show_bankstate'),
+
+      'save_unchecked' => $conf->exists('selfservice-save_unchecked'),
     };
 
   }
@@ -401,17 +524,19 @@ sub payment_info {
 
   $return{hide_payment_fields} =
   [
-    map { FS::payby->realtime($_) &&
-          $cust_main
-            ->agent
-            ->payment_gateway( 'method' => FS::payby->payby2bop($_) )
-            ->gateway_namespace
-            eq 'Business::OnlineThirdPartyPayment'
+    map { my $pg = '';
+          if ( FS::payby->realtime($_) ) {
+            $pg = $cust_main->agent->payment_gateway(
+              'method'  => FS::payby->payby2bop($_),
+              'nofatal' => 1,
+            );
+          }
+          $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
         }
     @{ $return{cust_paybys} }
   ];
 
-  $return{balance} = $cust_main->balance;
+  $return{balance} = $cust_main->balance; #XXX pkg-balances?
 
   $return{payname} = $cust_main->payname
                      || ( $cust_main->first. ' '. $cust_main->get('last') );
@@ -438,6 +563,11 @@ sub payment_info {
 
   }
 
+  if ( $conf->config('prepayment_discounts-credit_type') ) {
+    #need to eval?
+    $return{discount_terms_hash} = { $cust_main->discount_terms_hash };
+  }
+
   #doubleclick protection
   my $_date = time;
   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
@@ -464,6 +594,15 @@ sub process_payment {
   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
     or return { 'error' => "unknown custnum $custnum" };
 
+  $p->{'amount'} =~ /^\s*(\d+(\.\d{2})?)\s*$/
+    or return { 'error' => gettext('illegal_amount') };
+  my $amount = $1;
+  return { error => 'Amount must be greater than 0' } unless $amount > 0;
+
+  $p->{'discount_term'} =~ /^\s*(\d*)\s*$/
+    or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} };
+  my $discount_term = $1;
+
   $p->{'payname'} =~ /^([\w \,\.\-\']+)$/
     or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
   my $payname = $1;
@@ -497,6 +636,8 @@ sub process_payment {
    
     $payinfo = $p->{'payinfo'};
 
+    #more intelligent mathing will be needed here if you change
+    #card_masking_method and don't remove existing paymasks
     $payinfo = $cust_main->payinfo
       if $cust_main->paymask eq $payinfo;
 
@@ -508,7 +649,7 @@ sub process_payment {
     validate($payinfo)
       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
     return { 'error' => gettext('unknown_card_type') }
-      if cardtype($payinfo) eq "Unknown";
+      if $payinfo !~ /^99\d{14}$/ && cardtype($payinfo) eq "Unknown";
 
     if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) {
       if ( cardtype($payinfo) eq 'American Express card' ) {
@@ -532,7 +673,7 @@ sub process_payment {
     'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ],
   );
 
-  my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $p->{'amount'},
+  my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
     'quiet'    => 1,
     'payinfo'  => $payinfo,
     'paydate'  => $p->{'year'}. '-'. $p->{'month'}. '-01',
@@ -540,6 +681,7 @@ sub process_payment {
     'paybatch' => $paybatch, #this doesn't actually do anything
     'paycvv'   => $paycvv,
     'pkgnum'   => $session->{'pkgnum'},
+    'discount_term' => $discount_term,
     map { $_ => $p->{$_} } @{ $payby2fields{$payby} }
   );
   return { 'error' => $error } if $error;
@@ -550,20 +692,33 @@ sub process_payment {
     my $new = new FS::cust_main { $cust_main->hash };
     if ($payby eq 'CARD' || $payby eq 'DCRD') {
       $new->set( $_ => $p->{$_} )
-        foreach qw( payinfo payname paystart_month paystart_year payissue payip
+        foreach qw( payname paystart_month paystart_year payissue payip
                     address1 address2 city state zip country );
       $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
     } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') {
       $new->set( $_ => $p->{$_} )
         foreach qw( payname payip paytype paystate
                     stateid stateid_state );
-      $new->set( 'payinfo' => $payinfo );
       $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' );
     }
+    $new->set( 'payinfo' => $cust_main->card_token || $payinfo );
     $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' );
     my $error = $new->replace($cust_main);
-    return { 'error' => $error } if $error;
-    $cust_main = $new;
+    if ( $error ) {
+      #no, this causes customers to process their payments again
+      #return { 'error' => $error };
+      #XXX just warn verosely for now so i can figure out how these happen in
+      # the first place, eventually should redirect them to the "change
+      #address" page but indicate the payment did process??
+      delete($p->{'payinfo'}); #don't want to log this!
+      warn "WARNING: error changing customer info when processing payment (not returning to customer as a processing error): $error\n".
+           "NEW: ". Dumper($new)."\n".
+           "OLD: ". Dumper($cust_main)."\n".
+           "PACKET: ". Dumper($p)."\n";
+    #} else {
+      #not needed...
+      #$cust_main = $new;
+    }
   }
 
   return { 'error' => '' };
@@ -571,7 +726,6 @@ sub process_payment {
 }
 
 sub realtime_collect {
-
   my $p = shift;
 
   my $session = _cache->get($p->{'session_id'})
@@ -586,10 +740,15 @@ sub realtime_collect {
     'method'     => $p->{'method'},
     'pkgnum'     => $session->{'pkgnum'},
     'session_id' => $p->{'session_id'},
+    'apply'      => 1,
   );
   return { 'error' => $error } unless ref( $error );
 
-  return { 'error' => '', amount => $cust_main->balance, %$error };
+  my $amount = $session->{'pkgnum'}
+                 ? $cust_main->balance_pkgnum( $session->{'pkgnum'} )
+                 : $cust_main->balance;
+
+  return { 'error' => '', amount => $amount, %$error };
 }
 
 sub process_payment_order_pkg {
@@ -802,6 +961,7 @@ sub list_svcs {
   foreach my $cust_pkg ( $p->{'ncancelled'} 
                          ? $cust_main->ncancelled_pkgs
                          : $cust_main->unsuspended_pkgs ) {
+    next if $session->{'pkgnum'} && $cust_pkg->pkgnum != $session->{'pkgnum'};
     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
   }
   if ( $p->{'svcdb'} ) {
@@ -817,7 +977,7 @@ sub list_svcs {
   #              @svc_x;
 
   { 
-    #no#'svcnum'   => $session->{'svcnum'},
+    'svcnum'   => $session->{'svcnum'},
     'custnum'  => $custnum,
     'svcs'     => [
       map { 
@@ -910,7 +1070,7 @@ sub list_support_usage {
 sub _list_cdr_usage {
   my($svc_phone, $begin, $end) = @_;
   map [ $_->downstream_csv('format' => 'default') ], #XXX config for format
-      $svc_phone->cust_svc->get_cdrs( 'begin'=>$begin, 'end'=>$end, );
+      $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, );
 }
 
 sub list_cdr_usage {
@@ -1037,8 +1197,9 @@ sub order_pkg {
     my %fields = (
       'svc_acct'     => [ qw( username domsvc _password sec_phrase popnum ) ],
       'svc_domain'   => [ qw( domain ) ],
-      'svc_phone'    => [ qw( phonenum pin sip_password ) ],
+      'svc_phone'    => [ qw( phonenum pin sip_password phone_name ) ],
       'svc_external' => [ qw( id title ) ],
+      'svc_pbx'      => [ qw( id name ) ],
     );
   
     my $svc_x = "FS::$svcdb"->new( {
@@ -1095,7 +1256,9 @@ sub order_pkg {
     $cust_pkg->reexport;
   }
 
-  return { error => '', pkgnum => $cust_pkg->pkgnum };
+  my $svcnum = $svc[0] ? $svc[0]->svcnum : '';
+
+  return { error=>'', pkgnum=>$cust_pkg->pkgnum, svcnum=>$svcnum };
 
 }
 
@@ -1202,7 +1365,7 @@ sub _do_bop_realtime {
 
     my $bill_error =    $cust_main->bill
                      || $cust_main->apply_payments_and_credits
-                     || $cust_main->collect('realtime' => 1);
+                     || $cust_main->realtime_collect;
 
     if (    $cust_main->balance > $old_balance
          && $cust_main->balance > 0
@@ -1293,6 +1456,32 @@ sub order_renew {
 
 }
 
+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'})
@@ -1309,7 +1498,7 @@ sub cancel_pkg {
                                         'pkgnum'  => $pkgnum,   } )
     or return { 'error' => "unknown pkgnum $pkgnum" };
 
-  my $error = $cust_pkg->cancel( 'quiet'=>1 );
+  my $error = $cust_pkg->cancel('quiet' => 1);
   return { 'error' => $error };
 
 }
@@ -1522,6 +1711,43 @@ sub myaccount_passwd {
 
 }
 
+sub create_ticket {
+  my $p = shift;
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  warn "$me create_ticket: initializing ticket system\n" if $DEBUG;
+  FS::TicketSystem->init();
+
+  my $conf = new FS::Conf;
+  my $queue = $p->{'queue'}
+              || $conf->config('ticket_system-selfservice_queueid')
+              || $conf->config('ticket_system-default_queueid');
+
+  warn "$me create_ticket: creating ticket\n" if $DEBUG;
+  my $err_or_ticket = FS::TicketSystem->create_ticket(
+    '', #create RT session based on FS CurrentUser (fs_selfservice)
+    'queue'   => $queue,
+    'custnum' => $custnum,
+    'svcnum'  => $session->{'svcnum'},
+    map { $_ => $p->{$_} } qw( requestor cc subject message mime_type )
+  );
+
+  if ( ref($err_or_ticket) ) {
+    warn "$me create_ticket: sucessful: ". $err_or_ticket->id. "\n"
+      if $DEBUG;
+    return { 'error'     => '',
+             'ticket_id' => $err_or_ticket->id,
+           };
+  } else {
+    warn "$me create_ticket: unsucessful: $err_or_ticket\n"
+      if $DEBUG;
+    return { 'error' => $err_or_ticket };
+  }
+
+
+}
+
 #--
 
 sub _custoragent_session_custnum {
@@ -1546,6 +1772,7 @@ sub _custoragent_session_custnum {
     $custnum = $p->{'custnum'};
 
   } else {
+    $context = 'error';
     return ( 'error' => "Can't resume session" ); #better error message
   }