svc_dsl, ikano, on-going implementation, initial commit of freeside-pull-dsl, RT7111
authorlevinse <levinse>
Mon, 29 Nov 2010 22:11:01 +0000 (22:11 +0000)
committerlevinse <levinse>
Mon, 29 Nov 2010 22:11:01 +0000 (22:11 +0000)
FS/FS/Schema.pm
FS/FS/part_export/ikano.pm
FS/FS/svc_dsl.pm
FS/bin/freeside-pull-dsl [new file with mode: 0755]
httemplate/edit/svc_dsl.cgi
httemplate/view/svc_dsl.cgi

index 281ca6b..e482bed 100644 (file)
@@ -1844,8 +1844,8 @@ sub tables_hashref {
        'due_date',     'int', 'NULL',       '', '', '',
         'vendor_order_id',              'varchar', 'NULL', $char_d,  '', '',
         'vendor_qual_id',              'varchar', 'NULL', $char_d,  '', '',
-        'vendor_order_type',   'char', 'NULL',       1,  '', '', 
-        'vendor_order_status',   'char', 'NULL',       1,  '', '', 
+        'vendor_order_type',   'varchar', 'NULL', $char_d,  '', '',
+        'vendor_order_status',   'varchar', 'NULL', $char_d,  '', '',
         'first',              'varchar', 'NULL', $char_d,  '', '',
         'last',              'varchar', 'NULL', $char_d,  '', '',
         'company',              'varchar', 'NULL', $char_d,  '', '',
index 91953e3..444e9b8 100644 (file)
@@ -1,17 +1,15 @@
 package FS::part_export::ikano;
 
-use vars qw(@ISA %info %orderType %orderStatus %loopType $DEBUG $me);
+use vars qw(@ISA %info %loopType $me);
 use Tie::IxHash;
 use Date::Format qw( time2str );
 use Date::Parse qw( str2time );
 use FS::Record qw(qsearch qsearchs dbh);
 use FS::part_export;
 use FS::svc_dsl;
-use FS::dsl_note;
 use Data::Dumper;
 
 @ISA = qw(FS::part_export);
-$DEBUG = 1;
 $me= '[' .  __PACKAGE__ . ']';
 
 tie my %options, 'Tie::IxHash',
@@ -23,6 +21,7 @@ tie my %options, 'Tie::IxHash',
   'check_networks' => { label => 'Check Networks',
                    default => 'ATT,BELLCA',
                    },
+  'debug' => { label => 'Debug Mode',  type => 'checkbox' },
 ;
 
 %info = (
@@ -35,12 +34,6 @@ Requires installation of
 END
 );
     
-%orderType = ( 'N' => 'NEW', 'X' => 'CANCEL', 'C' => 'CHANGE' );
-%orderStatus = ('N' => 'NEW',
-               'P' => 'PENDING',
-               'X' => 'CANCELLED',
-               'C' => 'COMPLETED',
-               'E' => 'ERROR' );
 %loopType = ( '' => 'Line-share', '0' => 'Standalone' );
 
 sub rebless { shift; }
@@ -54,8 +47,8 @@ sub dsl_pull {
 # current assumptions of what won't change (from their side):
 # vendor_order_id, vendor_qual_id, vendor_order_type, pushed, monitored,
 # last_pull, address (from qual), contact info, ProductCustomId
-
     my($self, $svc_dsl) = (shift, shift);
+    $self->loadmod;
     my $result = $self->valid_order($svc_dsl,'pull');
     return $result unless $result eq '';
   
@@ -64,7 +57,8 @@ sub dsl_pull {
     return $result unless ref($result); # scalar (string) is an error
 
     # now we're getting an OrderResponse which should have one Order in it
-    warn "$me pull OrderResponse hash:\n".Dumper($result) if $DEBUG;
+    warn "$me pull OrderResponse hash:\n".Dumper($result) 
+       if $self->option('debug');
   
     return 'Invalid order response' unless defined $result->{'Order'};
     $result = $result->{'Order'};
@@ -84,13 +78,13 @@ sub dsl_pull {
     my $dbh = dbh;
 
     # 1. status 
-    my $new_order_status = $self->orderstatus_long2short($result->{'Status'});
-    return 'Invalid new status' if $new_order_status eq '';
-    if($svc_dsl->vendor_order_status ne $new_order_status) {
-       $svc_dsl->monitored(''
-           if ($new_order_status eq 'X' || $new_order_status eq 'C');
-       $svc_dsl->vendor_order_status($new_order_status);
-    }
+    my $order_status = grep($_ eq $result->{'Status'}, @Net::Ikano::orderStatus)
+                           ? $result->{'Status'} : '';
+    return 'Invalid new status' if $order_status eq '';
+    $svc_dsl->vendor_order_status($order_status
+       if($svc_dsl->vendor_order_status ne $order_status);
+    $svc_dsl->monitored('') 
+           if ($order_status eq 'CANCELLED' || $order_status eq 'COMPLETED');
 
     # 2. fields we don't care much about
     my %justUpdate = ( 'first' => 'FirstName',
@@ -114,9 +108,9 @@ sub dsl_pull {
 # TN may change only if sub changes it and New or Change order in Completed status
        my $tn = $product->{'PhoneNumber'};
        if($tn ne $svc_dsl->phonenum) {
-           if( ($svc_dsl->vendor_order_type eq 'N' 
-               || $svc_dsl->vendor_order_type eq 'C')
-              && $svc_dsl->vendor_order_status eq 'C' ) {
+           if( ($svc_dsl->vendor_order_type eq 'NEW
+               || $svc_dsl->vendor_order_type eq 'CHANGE')
+              && $svc_dsl->vendor_order_status eq 'COMPLETED' ) {
                $svc_dsl->phonenum($tn);
            }
            else { return 'TN has changed in an invalid state'; }
@@ -128,10 +122,10 @@ sub dsl_pull {
            if $product->{'PhoneNumber'} ne 'STANDALONE';
        my $tn = $product->{'VirtualPhoneNumber'};
        if($tn ne $svc_dsl->phonenum) {
-           if( ($svc_dsl->vendor_order_type eq 'N' 
-               || $svc_dsl->vendor_order_type eq 'C')
-             && $svc_dsl->vendor_order_status ne 'C'
-             && $svc_dsl->vendor_order_status ne 'X') {
+           if( ($svc_dsl->vendor_order_type eq 'NEW
+               || $svc_dsl->vendor_order_type eq 'CHANGE')
+             && $svc_dsl->vendor_order_status ne 'COMPLETED'
+             && $svc_dsl->vendor_order_status ne 'CANCELLED') {
                $svc_dsl->phonenum($tn);
            }
            else { return 'TN has changed in an invalid state'; }
@@ -139,39 +133,58 @@ sub dsl_pull {
     }
     
     # 4. desired_due_date - may change if manually changed
-    if($svc_dsl->vendor_order_type eq 'N' 
-           || $svc_dsl->vendor_order_type eq 'C'){
+    if($svc_dsl->vendor_order_type eq 'NEW
+           || $svc_dsl->vendor_order_type eq 'CHANGE'){
        my $f = str2time($product->{'DateToOrder'});
        return 'Invalid DateToOrder' unless $f;
-       $svc_dsl->desired_due_date($f) if $svc_dsl->desired_due_date != $f;
+       $svc_dsl->desired_due_date($f) if $svc_dsl->desired_due_date ne $f;
        # XXX: optionally sync back to start_date or whatever... 
     }
-    elsif($svc_dsl->vendor_order_type eq 'X'){
+    elsif($svc_dsl->vendor_order_type eq 'CANCEL'){
        my $f = str2time($product->{'DateToDisconnect'});
        return 'Invalid DateToDisconnect' unless $f;
-       $svc_dsl->desired_due_date($f) if $svc_dsl->desired_due_date != $f;
+       $svc_dsl->desired_due_date($f) if $svc_dsl->desired_due_date ne $f;
        # XXX: optionally sync back to expire or whatever... 
     }
 
     # 5. due_date
-    if($svc_dsl->vendor_order_type eq 'N' 
-         || $svc_dsl->vendor_order_type eq 'C') {
+    if($svc_dsl->vendor_order_type eq 'NEW
+         || $svc_dsl->vendor_order_type eq 'CHANGE') {
        my $f = str2time($product->{'ActivationDate'});
-       if($svc_dsl->vendor_order_status ne 'N') {
+       if($svc_dsl->vendor_order_status ne 'NEW'
+           && $svc_dsl->vendor_order_status ne 'CANCELLED') {
            return 'Invalid ActivationDate' unless $f;
-           $svc_dsl->due_date($f) if $svc_dsl->due_date != $f;
+           $svc_dsl->due_date($f) if $svc_dsl->due_date ne $f;
        }
     }
     # Ikano API does not implement the proper disconnect date,
     # so we can't do anything about it
 
     # 6. staticips - for now just comma-separate them
-    my @statics = $result->{'StaticIps'};
-# XXX
+    my $tstatics = $result->{'StaticIps'};
+    my @istatics = defined $tstatics ? @$tstatics : ();
+    my $ostatics = $svc_dsl->staticips;
+    my @ostatics = split(',',$ostatics);
+    # more horrible search/sync code below...
+    my $staticsChanged = 0;
+    foreach $istatic ( @istatics ) { # they have, we don't
+       unless ( grep($_ eq $istatic, @ostatics) ) {
+           push @ostatics, $istatic;
+           $staticsChanged = 1;
+       }
+    }
+    for(my $i=0; $i < scalar(@ostatics); $i++) {
+       unless ( grep($_ eq $ostatics[$i], @istatics) ) {
+           splice(@ostatics,$i,1);
+           $i--;
+           $staticsChanged = 1;
+       }
+    }
+    $svc_dsl->staticips(join(',',@ostatics)) if $staticsChanged;
 
     # 7. notes - put them into the common format and compare
     my $tnotes = $result->{'OrderNotes'}; 
-    my @tnotes = @$tnotes;
+    my @tnotes = defined $tnotes ? @$tnotes : ();
     my @inotes = (); # all Ikano OrderNotes as FS::dsl_note objects
     my $notesChanged = 0; 
     foreach $tnote ( @tnotes ) {
@@ -260,38 +273,37 @@ sub loop_type_long { # sub, not a method
     return $loopType{$svc_dsl->loop_type};
 }
 
-sub status_line {
-    my($self,$svc_dsl) = (shift,shift);
-    return "Ikano ".$orderType{$svc_dsl->vendor_order_type}." order #"
-       . $svc_dsl->vendor_order_id . " (Status: " 
-       . $orderStatus{$svc_dsl->vendor_order_status} . ")";
-}
-
 sub ikano_command {
   my( $self, $command, $args ) = @_;
 
-  eval "use Net::Ikano;";
-  die $@ if $@;
+  $self->loadmod;
 
   my $ikano = Net::Ikano->new(
     'keyid' => $self->option('keyid'),
     'username'  => $self->option('username'),
     'password'  => $self->option('password'),
-    'debug'    => 1,
-    #'reqpreviewonly' => 1,
+    'debug'    => $self->option('debug'),
   );
 
   $ikano->$command($args);
 }
 
+sub loadmod {
+  eval "use Net::Ikano;";
+  die $@ if $@;
+}
+
 sub valid_order {
   my( $self, $svc_dsl, $action ) = (shift, shift, shift);
+  $self->loadmod;
   
-  warn "$me valid_order action=$action svc_dsl:\n". Dumper($svc_dsl) if $DEBUG;
+  warn "$me valid_order action=$action svc_dsl:\n". Dumper($svc_dsl)
+       if $self->option('debug');
 
   # common to all order types/status/loop_type
   my $error = !($svc_dsl->desired_due_date
-           &&  defined $orderType{$svc_dsl->vendor_order_type}
+           &&  grep($_ eq $svc_dsl->vendor_order_type, @Net::Ikano::orderType)
            &&  $svc_dsl->first
            &&  $svc_dsl->last
            &&  defined $svc_dsl->loop_type
@@ -307,13 +319,17 @@ sub valid_order {
 
   # now go by order type
   # weird ifs & long lines for readability and ease of understanding - don't change
-  if($svc_dsl->vendor_order_type eq 'N') {
+  if($svc_dsl->vendor_order_type eq 'NEW') {
     if($svc_dsl->pushed) {
-       $error = !($action eq 'pull'
+       $error = !( ($action eq 'pull' || $action eq 'statuschg' 
+                       || $action eq 'delete')
            &&  length($svc_dsl->vendor_order_id) > 0
            &&  length($svc_dsl->vendor_order_status) > 0
                );
        return 'Invalid order data' if $error;
+
+       return 'Phone number required for status change'
+           if ($action eq 'statuschg' && length($svc_dsl->phonenum) < 1);
     }
     else { # unpushed New order - cannot do anything other than push it
        $error = !($action eq 'insert'
@@ -326,9 +342,9 @@ sub valid_order {
        return 'Invalid order data' if $error;
     }
   }
-  elsif($svc_dsl->vendor_order_type eq 'X') {
+  elsif($svc_dsl->vendor_order_type eq 'CANCEL') {
   }
-  elsif($svc_dsl->vendor_order_type eq 'C') {
+  elsif($svc_dsl->vendor_order_type eq 'CHANGE') {
   }
 
  '';
@@ -350,16 +366,11 @@ sub qual2termsid {
     '';
 }
 
-sub orderstatus_long2short {
-    my ($self,$order_status) = (shift,shift);
-    my %rorderStatus = reverse %orderStatus;
-    return $rorderStatus{$order_status} if exists $rorderStatus{$order_status};
-    '';
-}
-
 sub _export_insert {
   my( $self, $svc_dsl ) = (shift, shift);
 
+  $self->loadmod;
+
   my $result = $self->valid_order($svc_dsl,'insert');
   return $result unless $result eq '';
 
@@ -396,18 +407,20 @@ sub _export_insert {
   return $result unless ref($result); # scalar (string) is an error
 
   # now we're getting an OrderResponse which should have one Order in it
-  warn "$me _export_insert OrderResponse hash:\n".Dumper($result) if $DEBUG;
+  warn "$me _export_insert OrderResponse hash:\n".Dumper($result)
+       if $self->option('debug');
   
   return 'Invalid order response' unless defined $result->{'Order'};
   $result = $result->{'Order'};
 
-  return 'No order id or status returned' 
-    unless defined $result->{'Status'} && defined $result->{'OrderId'};
+  return 'No/invalid order id or status returned' 
+    unless defined $result->{'Status'} && defined $result->{'OrderId'}
+       && grep($_ eq $result->{'Status'}, @Net::Ikano::orderStatus);
 
   $svc_dsl->pushed(time);
   $svc_dsl->last_pull((time)+1); 
   $svc_dsl->vendor_order_id($result->{'OrderId'});
-  $svc_dsl->vendor_order_status($self->orderstatus_long2short($result->{'Status'}));
+  $svc_dsl->vendor_order_status($result->{'Status'});
   $svc_dsl->username($result->{'Username'});
   local $FS::svc_Common::noexport_hack = 1;
   local $FS::UID::AutoCommit = 0;
@@ -418,22 +431,85 @@ sub _export_insert {
 
 sub _export_replace {
   my( $self, $new, $old ) = (shift, shift, shift);
+# XXX only supports password changes now, but should return error if 
+# another change is attempted?
+
+  if($new->password ne $old->password) {
+      my $result = $self->valid_order($new,'statuschg');
+      return $result unless $result eq '';
+      
+      $result = $self->ikano_command('PASSWORDCHANGE',
+           { DSLPhoneNumber => $new->phonenum,
+             NewPassword => $new->password,
+           } ); 
+      return $result unless ref($result); # scalar (string) is an error
+
+      return 'Error changing password' unless defined $result->{'Password'}
+       && $result->{'Password'} eq $new->password;
+  }
+
   '';
 }
 
 sub _export_delete {
   my( $self, $svc_dsl ) = (shift, shift);
+  
+  my $result = $self->valid_order($svc_dsl,'delete');
+  return $result unless $result eq '';
+
+  # for now allow an immediate cancel only on New orders in New/Pending status
+  #XXX: add support for Chance and Cancel orders in New/Pending status later
+
+  if($svc_dsl->vendor_order_type eq 'NEW') {
+    if($svc_dsl->vendor_order_status eq 'NEW' 
+           || $svc_dsl->vendor_order_status eq 'PENDING') {
+       my $result = $self->ikano_command('CANCEL', 
+           { OrderId => $svc_dsl->vendor_order_id, } );
+       return $result unless ref($result); # scalar (string) is an error
+
+       return $self->dsl_pull($svc_dsl);
+    }
+    else {
+       return "Cannot cancel a NEW order unless it's in NEW or PENDING status";
+    }
+  }
+  else {
+    return 'Canceling orders other than NEW orders is not currently implemented';
+  }
+
   '';
 }
 
+sub statuschg {
+  my( $self, $svc_dsl, $type ) = (shift, shift, shift);
+
+  my $result = $self->valid_order($svc_dsl,'statuschg');
+  return $result unless $result eq '';
+
+  # get the DSLServiceId
+  $result = $self->ikano_command('CUSTOMERLOOKUP',
+       { PhoneNumber => $svc_dsl->phonenum } ); 
+  return $result unless ref($result); # scalar (string) is an error
+  return 'No DSLServiceId found' unless defined $result->{'DSLServiceId'};
+  my $DSLServiceId = $result->{'DSLServiceId'};
+
+  $result = $self->ikano_command('ACCOUNTSTATUSCHANGE',
+       { DSLPhoneNumber => $svc_dsl->phonenum,
+         DSLServiceId => $DSLServiceId,
+         type => $type,
+       } ); 
+  return $result unless ref($result); # scalar (string) is an error
+  ''; 
+}
+
 sub _export_suspend {
   my( $self, $svc_dsl ) = (shift, shift);
-  '';
+  $self->statuschg($svc_dsl,"SUSPEND");
 }
 
 sub _export_unsuspend {
   my( $self, $svc_dsl ) = (shift, shift);
-  '';
+  $self->statuschg($svc_dsl,"UNSUSPEND");
 }
 
 1;
index 2c30b00..93d3570 100644 (file)
@@ -4,6 +4,8 @@ use strict;
 use vars qw( @ISA $conf $DEBUG $me );
 use FS::Record qw( qsearch qsearchs );
 use FS::svc_Common;
+use FS::dsl_note;
+use FS::qual;
 
 @ISA = qw( FS::svc_Common );
 $DEBUG = 0;
@@ -238,7 +240,7 @@ sub check {
     || $self->ut_numbern('due_date')
     || $self->ut_textn('vendor_order_id')
     || $self->ut_textn('vendor_qual_id')
-    || $self->ut_alpha('vendor_order_type')
+    || $self->ut_alphan('vendor_order_type')
     || $self->ut_alphan('vendor_order_status')
     || $self->ut_text('first')
     || $self->ut_text('last')
diff --git a/FS/bin/freeside-pull-dsl b/FS/bin/freeside-pull-dsl
new file mode 100755 (executable)
index 0000000..d0aa921
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw(qsearch qsearchs dbh);
+use FS::svc_dsl;
+use FS::part_export;
+use Data::Dumper;
+
+&untaint_argv; #what it sounds like  (eww)
+use vars qw(%opt);
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my @monitored = qsearch('svc_dsl', { 'monitored' => 'Y' } );
+foreach my $svc_dsl ( @monitored ) {
+    my @exports = $svc_dsl->part_svc->part_export_dsl_pull;
+    my $svcnum = $svc_dsl->svcnum;
+    warn "either zero or more than one DSL-pulling export attached to svcnum "
+       . "$svcnum, skipping" if ( scalar(@exports) != 1 );
+    my $export = $exports[0];
+    my $error = $export->dsl_pull($svc_dsl); # this will commit to db by default
+    warn "Error pulling DSL svcnum $svcnum: $error" unless $error eq '';
+}
+
+###
+# subroutines
+###
+
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    # Date::Parse
+    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-pull-dsl \n";
+}
+
+###
+# documentation
+###
+
+=head1 NAME
+
+freeside-pull-dsl - Pull DSL order data from telcos/vendors for all monitored DSL orders to update
+
+=head1 SYNOPSIS
+
+  freeside-pull-dsl user
+
+=head1 DESCRIPTION
+
+user - name of an internal Freeside user
+
+This is still a work in progress - in future may add limiting by exportnum or svcpart or other such stuff.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, config.html from the base documentation
+
+=cut
+
index 4fca0cd..79aeb1a 100644 (file)
@@ -113,7 +113,7 @@ my $new_cb = sub {
                  value => $vendor_qual_id,  },
                { field => 'vendor_order_type', 
                  type => 'hidden', 
-                 value => 'N' },
+                 value => 'NEW' },
                { field => 'desired_due_date',
                  type => 'fixed',
                  formatted_value => 
index 14c1eb2..7bbdca1 100644 (file)
@@ -51,12 +51,16 @@ my $svc_cb = sub {
            'last',
            'company'  );
 
+    my $status = '';
     if($export->exporttype eq 'ikano') {
        push @fields, qw ( username password isp_chg isp_prev staticips );
+       $status = "Ikano " . $svc_x->vendor_order_type . " order #"
+               . $svc_x->vendor_order_id . " &nbsp; Status: " 
+               . $svc_x->vendor_order_status;
     }
     # else add any other export-specific stuff here
    
-    $footer = "<B>".$export->status_line($svc_x)."</B>";
+    $footer = "<B>$status</B>";
     $footer .= "<BR><BR><BR><B>Order Notes:</B><BR>".$export->notes_html($svc_x);
 };
 </%init>