[freeside-commits] branch FREESIDE_3_BRANCH updated. a52edcc909e5873a2c8790ce33b03917d6e1d29c

Mark Wells mark at 420.am
Sun Dec 28 23:27:56 PST 2014


The branch, FREESIDE_3_BRANCH has been updated
       via  a52edcc909e5873a2c8790ce33b03917d6e1d29c (commit)
       via  7e07d384748a5d0c5307fd711e4af520bf3b3802 (commit)
       via  7e6ab4562bb5490ca82422d91b6389fd844ea6ff (commit)
      from  0fb66f68bcb98f9bec34a2110692d1215c0efee8 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit a52edcc909e5873a2c8790ce33b03917d6e1d29c
Author: Mark Wells <mark at freeside.biz>
Date:   Sun Dec 28 23:26:26 2014 -0800

    voip.ms export, #31834

diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index c2d4f31..fd69037 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -4004,6 +4004,7 @@ sub tables_hashref {
         'e911_class',                    'char', 'NULL',       1, '', '',
         'e911_type',                     'char', 'NULL',       1, '', '', 
         'circuit_svcnum',                 'int', 'NULL',      '', '', '',
+        'sip_server',                 'varchar', 'NULL', $char_d, '', '',
       ],
       'primary_key' => 'svcnum',
       'unique' => [ [ 'sms_carrierid', 'sms_account'] ],
diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm
index 15588ea..bdbecff 100644
--- a/FS/FS/part_export.pm
+++ b/FS/FS/part_export.pm
@@ -546,23 +546,6 @@ sub default_export_machine {
   die "no default export hostname for export ".$self->exportnum;
 }
 
-=item svc_role SVC_X
-
-Returns the role that SVC_X occupies with respect to this export, if any.
-This is part of the part_svc's export configuration.
-
-=cut
-
-sub svc_role {
-  my $self = shift;
-  my $svc_x = shift;
-  my $cust_svc = $svc_x->cust_svc or return '';
-  my $export_svc = qsearchs('export_svc', { exportnum => $self->exportnum,
-                                            svcpart   => $cust_svc->svcpart })
-                   or return '';
-  $export_svc->role;
-} 
-
 #these should probably all go away, just let the subclasses define em
 
 =item export_insert SVC_OBJECT
@@ -763,6 +746,61 @@ sub get_dids_npa_select   { 1; }
 # change the phone number for a service. if false, then they can't (have to
 # reprovision completely).
 
+=item svc_role SVC
+
+Returns the role that SVC occupies with respect to this export, if any.
+This is part of the part_svc's export configuration.
+
+=cut
+
+sub svc_role {
+  my $self = shift;
+  my $svc_x = shift;
+  my $cust_svc = $svc_x->cust_svc or return '';
+  my $export_svc = qsearchs('export_svc', { exportnum => $self->exportnum,
+                                            svcpart   => $cust_svc->svcpart })
+                   or return '';
+  $export_svc->role;
+} 
+
+=item svc_with_role { SVC | PKGNUM }, ROLE
+
+Given a svc_* object SVC or pkgnum PKG, and a role name ROLE, finds the
+service(s) in the same package that are linked to this export with ROLE.
+
+=cut
+
+sub svc_with_role {
+  my $self = shift;
+  my $svc_or_pkgnum = shift;
+  my $role = shift; 
+  my $pkgnum;
+  if ( ref $svc_or_pkgnum ) {
+    $pkgnum = $svc_or_pkgnum->cust_svc->pkgnum or return '';
+  } else {
+    $pkgnum = $svc_or_pkgnum;
+  }
+  my $role_info = $self->info->{roles}->{$role}
+    or die "role '$role' does not exist for export '".$self->exporttype."'\n";
+  my $svcdb = $role_info->{svcdb};
+
+  my @svcs = qsearch({
+    'table'     =>  $svcdb,
+    'addl_from' =>  ' JOIN cust_svc USING (svcnum)' .
+                    ' JOIN export_svc USING (svcpart)',
+    'extra_sql' =>  " WHERE cust_svc.pkgnum = $pkgnum" .
+                    " AND export_svc.exportnum = ".$self->exportnum .
+                    " AND export_svc.role = '$role'",
+  });               
+  if ( $role_info->{multiple} ) {
+    return @svcs;
+  } else {
+    if ( @svcs > 1 ) {
+      warn "multiple $role services in pkgnum $pkgnum; returning the first one.\n";
+    }
+    return $svcs[0];
+  }
+}
 
 =back
 
diff --git a/FS/FS/part_export/voip_ms.pm b/FS/FS/part_export/voip_ms.pm
new file mode 100644
index 0000000..44ce908
--- /dev/null
+++ b/FS/FS/part_export/voip_ms.pm
@@ -0,0 +1,648 @@
+package FS::part_export::voip_ms;
+
+use base qw( FS::part_export );
+use strict;
+
+use Tie::IxHash;
+use LWP::UserAgent;
+use URI;
+use URI::Escape;
+use JSON;
+use HTTP::Request::Common;
+use Cache::FileCache;
+
+our $me = '[voip.ms]';
+our $DEBUG = 2;
+our $base_url = 'https://voip.ms/api/v1/rest.php';
+
+# cache cities and provinces
+our $CACHE; # a FileCache; their API is not as quick as I'd like
+our $cache_timeout = 86400; # seconds
+
+tie my %options, 'Tie::IxHash',
+  'account'         => { label => 'Main account ID' },
+  'username'        => { label => 'API username', },
+  'password'        => { label => 'API password', },
+  'debug'           => { label => 'Enable debugging', type => 'checkbox', value => 1 },
+  # could dynamically pull this from the API...
+  'protocol'        => {
+    label             => 'Protocol',
+    type              => 'select',
+    options           => [ 1, 3 ],
+    option_labels     => { 1 => 'SIP', 3 => 'IAX' },
+  },
+  'auth_type'       => {
+    label             => 'Authorization type',
+    type              => 'select',
+    options           => [ 1, 2 ],
+    option_labels     => { 1 => 'User/Password', 2 => 'Static IP' },
+  },
+  'billing_type'    => {
+    label             => 'DID billing mode',
+    type              => 'select',
+    options           => [ 1, 2 ],
+    option_labels     => { 1 => 'Per minute', 2 => 'Flat rate' },
+  },
+  'device_type'     => {
+    label             => 'Device type',
+    type              => 'select',
+    options           => [ 1, 2 ],
+    option_labels     => { 1 => 'IP PBX, e.g. Asterisk',
+                           2 => 'IP phone or softphone',
+                         },
+  },
+  'canada_routing'    => {
+    label             => 'Canada routing policy',
+    type              => 'select',
+    options           => [ 1, 2 ],
+    option_labels     => { 1 => 'Value (lowest price)',
+                           2 => 'Premium (highest quality)'
+                         },
+  },
+  'international_route' => { # yes, 'route'
+    label             => 'International routing policy',
+    type              => 'select',
+    options           => [ 0, 1, 2 ],
+    option_labels     => { 0 => 'Disable international calls',
+                           1 => 'Value (lowest price)',
+                           2 => 'Premium (highest quality)'
+                         },
+  },
+  'cnam_lookup' => {
+    label             => 'Enable CNAM lookup on incoming calls',
+    type              => 'checkbox',
+  },
+
+;
+
+tie my %roles, 'Tie::IxHash',
+  'subacct'       => {  label     => 'SIP client',
+                        svcdb     => 'svc_acct',
+                     },
+  'did'           => {  label     => 'DID',
+                        svcdb     => 'svc_phone',
+                        multiple  => 1,
+                     },
+;
+
+our %info = (
+  'svc'      => [qw( svc_acct svc_phone )],
+  'desc'     =>
+    'Provision subaccounts and DIDs to voip.ms wholesale',
+  'options'  => \%options,
+  'roles'    => \%roles,
+  'no_machine' => 1,
+  'notes'    => <<'END'
+<P>Export to <b>voip.ms</b> hosted PBX service.</P>
+<P>This requires two service definitions to be configured on the same package:
+  <OL>
+    <LI>An account service for the subaccount (the "login" used by the 
+    customer's PBX or IP phone, and the call routing service). This should
+    be attached to the export in the "subacct" role. If you are using 
+    password authentication, the <i>username</i> and <i>_password</i> will 
+    be used to authenticate to voip.ms. If you are using static IP 
+    authentication, the <i>slipip</I> (IP address) field should be set to 
+    the address.</LI>
+    <LI>A phone service for a DID, attached to the export in the DID role.
+    You must select a server for the "SIP Host" field. Calls from this DID
+    will be routed to the customer via that server.</LI>
+  </OL>
+</P>
+<P>Export options:
+  <UL>
+    <LI>Main account ID: the numeric ID for the master account. 
+    Subaccount usernames will be prefixed with this number and an underscore,
+    so if you create a subaccount in Freeside with a username of "myuser", 
+    the SIP device will have to authenticate as something like 
+    "123456_myuser".</LI>
+    <LI>API username/password: your API login; see 
+    <a href="https://www.voip.ms/m/api.php">this page</a> to configure it
+    if you haven't done so yet.</LI>
+    <LI>Enable debugging: writes all traffic with the API server to the log.
+    This includes passwords.</LI>
+  </UL>
+  The other options correspond to options in either the subaccount or DID 
+  configuration menu in the voip.ms portal; see documentation there for 
+  details.
+</P>
+END
+);
+
+sub export_insert {
+  my($self, $svc_x) = (shift, shift);
+
+  my $role = $self->svc_role($svc_x);
+  if ( $role eq 'subacct' ) {
+
+    my $error = $self->insert_subacct($svc_x);
+    return "$me $error" if $error;
+
+    my @existing_dids = ( $self->svc_with_role($svc_x, 'did') );
+
+    foreach my $svc_phone (@existing_dids) {
+      $error = $self->insert_did($svc_phone, $svc_x);
+      return "$me $error ordering DID ".$svc_phone->phonenum
+        if $error;
+    }
+
+  } elsif ( $role eq 'did' ) {
+
+    my $svc_acct = $self->svc_with_role($svc_x, 'subacct');
+    return if !$svc_acct;
+ 
+    my $error = $self->insert_did($svc_x, $svc_acct);
+    return "$me $error" if $error;
+
+  }
+  '';
+}
+
+sub export_replace {
+  my ($self, $svc_new, $svc_old) = @_;
+  my $role = $self->svc_role($svc_new);
+  my $error;
+  if ( $role eq 'subacct' ) {
+    $error = $self->replace_subacct($svc_new, $svc_old);
+  } elsif ( $role eq 'did' ) {
+    $error = $self->replace_did($svc_new, $svc_old);
+  }
+  return "$me $error" if $error;
+  '';
+}
+
+sub export_delete {
+  my ($self, $svc_x) = (shift, shift);
+  my $role = $self->svc_role($svc_x);
+  if ( $role eq 'subacct' ) {
+
+    my @existing_dids = ( $self->svc_with_role($svc_x, 'did') );
+
+    my $error;
+    foreach my $svc_phone (@existing_dids) {
+      $error = $self->delete_did($svc_phone);
+      return "$me $error canceling DID ".$svc_phone->phonenum
+        if $error;
+    }
+
+    $error = $self->delete_subacct($svc_x);
+    return "$me $error" if $error;
+
+  } elsif ( $role eq 'did' ) {
+
+    my $svc_acct = $self->svc_with_role($svc_x, 'subacct');
+    return if !$svc_acct;
+ 
+    my $error = $self->delete_did($svc_x);
+    return "$me $error" if $error;
+
+  }
+  '';
+}
+
+sub export_suspend {
+  my $self = shift;
+  my $svc_x = shift;
+  my $role = $self->svc_role($svc_x);
+  return if $role ne 'subacct'; # can't suspend DIDs directly
+
+  my $error = $self->replace_subacct($svc_x, $svc_x); # will disable it
+  return "$me $error" if $error;
+  '';
+}
+
+sub export_unsuspend {
+  my $self = shift;
+  my $svc_x = shift;
+  my $role = $self->svc_role($svc_x);
+  return if $role ne 'subacct'; # can't suspend DIDs directly
+
+  $svc_x->set('unsuspended', 1); # hack to tell replace_subacct to do it
+  my $error = $self->replace_subacct($svc_x, $svc_x); #same
+  return "$me $error" if $error;
+  '';
+}
+
+
+sub insert_subacct {
+  my ($self, $svc_acct) = @_;
+  my $method = 'createSubAccount';
+  my $content = $self->subacct_content($svc_acct);
+
+  my $result = $self->api_request($method, $content);
+  if ( $result->{status} ne 'success' ) {
+    return $result->{status}; # or look up the error message string?
+  }
+
+  # result includes the account ID and the full username, but we don't
+  # really need to keep those; we can look them up later
+  '';
+}
+
+sub insert_did {
+  my ($self, $svc_phone, $svc_acct) = @_;
+  my $method = 'orderDID';
+  my $content = $self->did_content($svc_phone, $svc_acct);
+  my $result = $self->api_request($method, $content);
+  if ( $result->{status} ne 'success' ) {
+    return $result->{status}; # or look up the error message string?
+  }
+  '';
+}
+
+sub delete_subacct {
+  my ($self, $svc_acct) = @_;
+  my $account = $self->option('account') . '_' . $svc_acct->username;
+
+  my $id = $self->subacct_id($svc_acct);
+  if ( $id =~ /\D/ ) {
+
+    return $id; # it's an error
+
+  } elsif ( $id eq '' ) {
+
+    return ''; # account doesn't exist, don't need to delete
+
+  } # else it's numeric
+
+  warn "$me deleting account $account with ID $id\n" if $DEBUG;
+  my $result = $self->api_request('delSubAccount', { id => $id });
+  if ( $result->{status} ne 'success' ) {
+    return $result->{status};
+  }
+  '';
+}
+
+sub delete_did {
+  my ($self, $svc_phone) = @_;
+  my $phonenum = $svc_phone->phonenum;
+
+  my $result = $self->api_request('cancelDID', { did => $phonenum });
+  if ( $result->{status} ne 'success' and $result->{status} ne 'invalid_did' )
+  {
+    return $result->{status};
+  }
+  '';
+}
+
+sub replace_subacct {
+  my ($self, $svc_new, $svc_old) = @_;
+  if ( $svc_new->username ne $svc_old->username ) {
+    return "can't change account username; delete and recreate the account instead";
+  }
+  
+  my $id = $self->subacct_id($svc_new);
+  if ( $id =~ /\D/ ) {
+
+    return $id;
+
+  } elsif ( $id eq '' ) {
+
+    # account doesn't exist; provision it anew
+    return $self->insert_subacct($svc_new);
+
+  }
+
+  my $content = $self->subacct_content($svc_new);
+  delete $content->{username};
+  $content->{id} = $id;
+
+  my $result = $self->api_request('setSubAccount', $content);
+  if ( $result->{status} ne 'success' ) {
+    return $result->{status};
+  }
+
+  '';
+}
+
+sub replace_did {
+  my ($self, $svc_new, $svc_old) = @_;
+  if ( $svc_new->phonenum ne $svc_old->phonenum ) {
+    return "can't change DID phone number";
+  }
+  # check that there's a subacct set up
+  my $svc_acct = $self->svc_with_role($svc_new, 'subacct')
+    or return '';
+
+  # check for the existing DID
+  my $result = $self->api_request('getDIDsInfo',
+    { did => $svc_new->phonenum }
+  );
+  if ( $result->{status} eq 'invalid_did' ) {
+
+    # provision the DID
+    return $self->insert_did($svc_new, $svc_acct);
+
+  } elsif ( $result->{status} ne 'success' ) {
+
+    return $result->{status};
+
+  }
+
+  my $existing = $result->{dids}[0];
+
+  my $content = $self->did_content($svc_new, $svc_acct);
+  if ( $content->{billing_type} == $existing->{billing_type} ) {
+    delete $content->{billing_type}; # confuses the server otherwise
+  }
+  $result = $self->api_request('setDIDInfo', $content);
+  if ( $result->{status} ne 'success' ) {
+    return $result->{status};
+  }
+
+  return '';
+}
+
+#######################
+# CONVENIENCE METHODS #
+#######################
+
+sub subacct_id {
+  my ($self, $svc_acct) = @_;
+  my $account = $self->option('account') . '_' . $svc_acct->username;
+
+  # look up the subaccount's numeric ID
+  my $result = $self->api_request('getSubAccounts', { account => $account });
+  if ( $result->{status} eq 'invalid_account' ) {
+    return '';
+  } elsif ( $result->{status} ne 'success' ) {
+    return "$result->{status} looking up account ID";
+  } else {
+    return $result->{accounts}[0]{id};
+  }
+}
+
+sub subacct_content {
+  my ($self, $svc_acct) = @_;
+
+  my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+
+  my $desc = $svc_acct->finger || $svc_acct->username;
+  my $intl = $self->option('international_route');
+  my $lockintl = 0;
+  if ($intl == 0) {
+    $intl = 1; # can't send zero
+    $lockintl = 1;
+  }
+
+  my %auth;
+  if ( $cust_pkg and $cust_pkg->susp > 0 and !$svc_acct->get('unsuspended') ) {
+    # we can't explicitly suspend their account, so just set its password to 
+    # a partially random string that satisfies the password rules
+    # (we still have their real password in the svc_acct record)
+    %auth = ( auth_type => 1,
+              password  => sprintf('Suspend-%08d', int(rand(100000000)) ),
+            );
+  } else {
+    %auth = ( auth_type => $self->option('auth_type'),
+              password  => $svc_acct->_password,
+              ip        => $svc_acct->slipip,
+            );
+  }
+  return {
+    username            => $svc_acct->username,
+    description         => $desc,
+    %auth,
+    device_type         => $self->option('device_type'),
+    canada_routing      => $self->option('canada_routing'),
+    lock_international  => $lockintl,
+    international_route => $intl,
+    # sensible defaults for these
+    music_on_hold       => 'default', # silence
+    allowed_codecs      => 'ulaw;g729;gsm',
+    dtmf_mode           => 'AUTO',
+    nat                 => 'yes',
+  };
+}
+
+sub did_content {
+  my ($self, $svc_phone, $svc_acct) = @_;
+
+  my $account = $self->option('account') . '_' . $svc_acct->username;
+  my $phonenum = $svc_phone->phonenum;
+  # look up POP number (for some reason this is assigned per DID...)
+  my $sip_server = $svc_phone->sip_server
+    or return "SIP server required";
+  my $popnum = $self->cache('server_popnum')->{ $svc_phone->sip_server }
+    or return "SIP server '$sip_server' is unknown";
+  return {
+    did                 => $phonenum,
+    routing             => "account:$account",
+    # secondary routing options (failovers, voicemail) are outside our 
+    # scope here
+    # though we could support them using the "forwarddst" field?
+    pop                 => $popnum,
+    dialtime            => 60, # sensible default, add an option if needed
+    cnam                => ($self->option('cnam_lookup') ? 1 : 0),
+    note                => $svc_phone->phone_name,
+    billing_type        => $self->option('billing_type'),
+  };
+}
+
+#################
+# DID SELECTION #
+#################
+
+sub get_dids_npa_select { 0 } # all Canadian VoIP providers seem to have this
+
+sub get_dids {
+  my $self = shift;
+  my %opt = @_;
+
+  my ($exportnum) = $self->exportnum =~ /^(\d+)$/;
+
+  if ( $opt{'region'} ) {
+
+    # return numbers (probably shouldn't cache this)
+    my ($ratecenter, $province) = $opt{'region'} =~ /^(.*), (..)$/;
+    my $country = $self->cache('province_country')->{ $province };
+    my $result;
+    if ( $country eq 'CAN' ) {
+      $result = $self->api_insist('getDIDsCAN',
+                                  { province => $province,
+                                    ratecenter => $ratecenter
+                                  }
+                                 );
+    } elsif ( $country eq 'USA' ) {
+      $result = $self->api_insist('getDIDsUSA',
+                                  { state => $province,
+                                    ratecenter => $ratecenter
+                                  }
+                                 );
+    }
+    my @return = map { $_->{did} } @{ $result->{dids} };
+    return \@return;
+  } else {
+
+    if ( $opt{'state'} ) {
+      my $province = $opt{'state'};
+
+      # cache() will refresh the cache if necessary, and die on failure.
+      # default here is only in case someone gives us a state that
+      # doesn't exist.
+      return $self->cache('province_city', $province) || [];
+
+    } else {
+
+      # return a list of provinces
+      return [
+        @{ $self->cache('country_province')->{CAN} },
+        @{ $self->cache('country_province')->{USA} },
+      ];
+    }
+  }
+}
+
+sub get_sip_servers {
+  my $self = shift;
+  return [ sort keys %{ $self->cache('server_popnum') } ];
+}
+
+sub cache {
+  my $self = shift;
+  my $element = shift or return;
+  my $province = shift;
+
+  $CACHE ||= Cache::FileCache->new({
+    'cache_root' => $FS::UID::cache_dir.'/cache'.$FS::UID::datasrc,
+    'namespace'  => __PACKAGE__,
+    'default_expires_in' => $cache_timeout,
+  });
+
+  if ( $element eq 'province_city' ) {
+    $element .= ".$province";
+  }
+  return $CACHE->get($element) || $self->reload_cache($element);
+}
+
+sub reload_cache {
+  my $self = shift;
+  my $element = shift;
+  if ( $element eq 'province_country' or $element eq 'country_province' ) {
+    # populate provinces/states
+
+    my %province_country;
+    my %country_province = ( CAN => [], USA => [] );
+
+    my $result = $self->api_insist('getProvinces');
+    foreach my $province (map { $_->{province} } @{ $result->{provinces} }) {
+      $province_country{$province} = 'CAN';
+      push @{ $country_province{CAN} }, $province;
+    }
+
+    $result = $self->api_insist('getStates');
+    foreach my $state (map { $_->{state} } @{ $result->{states} }) {
+      $province_country{$state} = 'USA';
+      push @{ $country_province{USA} }, $state;
+    }
+
+    $CACHE->set('province_country', \%province_country);
+    $CACHE->set('country_province', \%country_province);
+    return $CACHE->get($element);
+
+  } elsif ( $element eq 'server_popnum' ) {
+
+    my $result = $self->api_insist('getServersInfo');
+    my %server_popnum;
+    foreach (@{ $result->{servers} }) {
+      $server_popnum{ $_->{server_hostname} } = $_->{server_pop};
+    }
+
+    $CACHE->set('server_popnum', \%server_popnum);
+    return \%server_popnum;
+
+  } elsif ( $element =~ /^province_city\.(\w+)$/ ) {
+
+    my $province = $1;
+
+    # then get the ratecenters for that province
+    my $country = $self->cache('province_country')->{$province};
+    my @ratecenters;
+
+    if ( $country eq 'CAN' ) {
+
+      my $result = $self->api_insist('getRateCentersCAN',
+                                   { province => $province });
+
+      foreach (@{ $result->{ratecenters} }) {
+        my $ratecenter = $_->{ratecenter} . ", $province"; # disambiguate
+        push @ratecenters, $ratecenter;
+      }
+
+    } elsif ( $country eq 'USA' ) {
+
+      my $result = $self->api_insist('getRateCentersUSA',
+                                   { state => $province });
+      foreach (@{ $result->{ratecenters} }) {
+        my $ratecenter = $_->{ratecenter} . ", $province";
+        push @ratecenters, $ratecenter;
+      }
+
+    }
+
+    $CACHE->set($element, \@ratecenters);
+    return \@ratecenters;
+
+  } else {
+    return;
+  }
+}
+
+##############
+# API ACCESS #
+##############
+
+=item api_request METHOD, CONTENT
+
+Makes a REST request with method name METHOD, and POST content CONTENT (as
+a hashref).
+
+=cut
+
+sub api_request {
+  my $self = shift;
+  my ($method, $content) = @_;
+  $DEBUG ||= 1 if $self->option('debug');
+  my $url = URI->new($base_url);
+  $url->query_form(
+    'method'        => $method,
+    'api_username'  => $self->option('username'),
+    'api_password'  => $self->option('password'),
+    %$content
+  );
+
+  my $request = GET($url,
+    'Accept'        => 'text/json',
+  );
+
+  warn "$me $method\n" . $request->as_string ."\n" if $DEBUG;
+  my $ua = LWP::UserAgent->new;
+  my $response = $ua->request($request);
+  warn "$me received\n" . $response->as_string ."\n" if $DEBUG;
+  if ( !$response->is_success ) {
+    return { status => $response->content };
+  }
+
+  return decode_json($response->content);
+}
+
+=item api_insist METHOD, CONTENT
+
+Exactly like L</api_request>, but if the returned "status" is not "success",
+throws an exception.
+
+=cut
+
+sub api_insist {
+  my $self = shift;
+  my $method = $_[0];
+  my $result = $self->api_request(@_);
+  if ( $result->{status} eq 'success' ) {
+    return $result;
+  } elsif ( $result->{status} ) {
+    die "$me $method: $result->{status}\n";
+  } else {
+    die "$me $method: no status returned\n";
+  }
+}
+
+1;
diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm
index 56104ba..46b2311 100644
--- a/FS/FS/svc_phone.pm
+++ b/FS/FS/svc_phone.pm
@@ -134,6 +134,15 @@ Class of Service for E911 service (per the NENA 2.1 standard).
 
 Type of Service for E911 service.
 
+=item circuit_svcnum
+
+The L<FS::svc_circuit> record for the physical circuit that transports this
+phone line.
+
+=item sip_server
+
+The hostname of the SIP server that this phone number is routed to.
+
 =back
 
 =head1 METHODS
@@ -251,6 +260,10 @@ sub table_info {
                                 disable_inventory => 1,
                                 multiple => 1,
                         },
+        'sip_server'  => {
+                                label => 'SIP Host',
+                                %dis2,
+                         },
     },
   };
 }
@@ -548,6 +561,7 @@ sub check {
 				'native', 'portin-reject', 'portout-reject'])
     || $self->ut_enumn('portable', ['','Y'])
     || $self->ut_textn('lnp_reject_reason')
+    || $self->ut_domainn('sip_server')
   ;
   return $error if $error;
 
diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi
index 876633a..1f835d7 100755
--- a/httemplate/browse/part_export.cgi
+++ b/httemplate/browse/part_export.cgi
@@ -60,21 +60,25 @@ function part_export_areyousure(href) {
 %         my %opt = $part_export->options;
 %         my $defs = $part_export->info->{options};
 %         my %multiples;
-%         foreach my $opt (keys %$defs) { # is a Tie::IxHash
-%           my $group = $defs->{$opt}->{multiple};
+%         foreach my $optname (keys %$defs) { # is a Tie::IxHash
+%           my $def = $defs->{$optname};
+%           my $group = $def->{multiple};
 %           if ( $group ) {
-%             my @values = split("\n", $opt{$opt});
+%             my @values = split("\n", $opt{$optname});
 %             $multiples{$group} ||= [];
-%             push @{ $multiples{$group} }, [ $opt, @values ] if @values;
-%             delete $opt{$opt};
-%           } elsif (length($opt{$opt})) { # the normal case
-%#         foreach my $opt ( keys %opt ) { 
+%             push @{ $multiples{$group} }, [ $optname, @values ] if @values;
+%             delete $opt{$optname};
+%           } elsif (length($opt{$optname})) { # the normal case
+%             my $value = $opt{$optname};
+%             if ( $def->{option_labels} ) {
+%               $value = $def->{option_labels}->{$value} || $value;
+%             }
   
             <TR>
-              <TD ALIGN="right" VALIGN="top" WIDTH="33%"><% $opt %>: </TD>
-              <TD ALIGN="left" WIDTH="67%"><% encode_entities($opt{$opt}) %></TD>
+              <TD ALIGN="right" VALIGN="top" WIDTH="33%"><% $optname %>: </TD>
+              <TD ALIGN="left" WIDTH="67%"><% encode_entities($value) %></TD>
             </TR>
-%             delete $opt{$opt};
+%             delete $opt{$optname};
 %           }
 %         }
 %         # now any that are somehow not in the options list
diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi
index 2897cf3..0e53e29 100644
--- a/httemplate/edit/part_export.cgi
+++ b/httemplate/edit/part_export.cgi
@@ -201,6 +201,15 @@ my $widget = new HTML::Widgets::SelectLayers(
         $html .= qq!<TR><TD ALIGN="right">$label</TD><TD>!;
       }
       if ( $type eq 'select' ) {
+
+        # 'select' options can specify options one of two ways:
+        # the "preferred" way:
+        #   options: arrayref of allowed option values
+        #   option_labels: hashref of option value => label
+        # OR the weird and semi-deprecated way:
+        #   option_values: coderef to return a list of allowed option values
+        #   option_label: coderef to take an option value and return its label
+
         my $size = defined($optinfo->{size}) ? " SIZE=" . $optinfo->{size} : '';
         my $multi = ($optinfo->{multi} || $optinfo->{multiple})
                       ? ' MULTIPLE' : '';
@@ -218,10 +227,15 @@ my $widget = new HTML::Widgets::SelectLayers(
           #} else {
             my $selected = ($multi ? grep {$_ eq $select_option} @values : $select_option eq $value ) ? ' SELECTED' : '';
             my $label = $select_option;
-            if (defined($optinfo->{option_label})) {
+            if ( defined $optinfo->{option_label} ) {
               my $labelsub = $optinfo->{option_label};
               $label = &$labelsub($select_option);
+            } elsif ( defined $optinfo->{option_labels} ) {
+              if (exists $optinfo->{option_labels}->{$select_option}) {
+                $label = $optinfo->{option_labels}->{$select_option};
+              }
             }
+    
             $html .= qq!<OPTION VALUE="$select_option"$selected>!.
                      qq!$label</OPTION>!;
           #}
diff --git a/httemplate/edit/svc_phone.cgi b/httemplate/edit/svc_phone.cgi
index f9c0d40..f1471e2 100644
--- a/httemplate/edit/svc_phone.cgi
+++ b/httemplate/edit/svc_phone.cgi
@@ -132,6 +132,9 @@ my $begin_callback = sub {
              value   => 'Carrier Information',
              colspan => 8,
            },
+           { field => 'sip_server',
+             type  => 'select-sip_server',
+           },
            { field => 'sms_carrierid',
              label => 'SMS Carrier',
              type  => 'select-cdr_carrier',
diff --git a/httemplate/elements/select-did.html b/httemplate/elements/select-did.html
index c396031..8a91d7a 100644
--- a/httemplate/elements/select-did.html
+++ b/httemplate/elements/select-did.html
@@ -81,18 +81,18 @@ Example:
 %       # if/when other folks need an areacode-less DID selector that goes
 %       # directly from state to region
 
-        <TD VALIGN="top">
-          <% include('/elements/select.html',
-                       'field'    => 'phonenum_state',
-                       'id'       => 'phonenum_state',
-                       'options'  => [ '', @{ $export->get_dids } ],
-                       'labels'   => { '' => 'Select province' },
-                       'onchange' => 'phonenum_state_changed(this);',
-                       'disabled' => ( $manual_checked ? 1 : 0 ),
-                    )
-          %>
-          <BR><FONT SIZE="-1" ID="phonenum_state_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Province</FONT>
-        </TD>
+          <TD VALIGN="top">
+            <% include('/elements/select.html',
+                         'field'    => 'phonenum_state',
+                         'id'       => 'phonenum_state',
+                         'options'  => [ '', @{ $export->get_dids } ],
+                         'labels'   => { '' => 'Select province' },
+                         'onchange' => 'phonenum_state_changed(this);',
+                         'disabled' => ( $manual_checked ? 1 : 0 ),
+                      )
+            %>
+            <BR><FONT SIZE="-1" ID="phonenum_state_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Province</FONT>
+          </TD>
 
           <TD VALIGN="top">
             <% include('/elements/select-region.html',
diff --git a/httemplate/elements/tr-select-sip_server.html b/httemplate/elements/tr-select-sip_server.html
new file mode 100644
index 0000000..8df1b62
--- /dev/null
+++ b/httemplate/elements/tr-select-sip_server.html
@@ -0,0 +1,48 @@
+% if ( $columnflag eq 'F' ) {
+<& fixed.html, %opt &>
+% } elsif ( $use_selector ) {
+%   my $servers = $exports[0]->get_sip_servers;
+%   # pretty simple selector, they're all just hostnames/IP addresses
+<& tr-select.html,
+    %opt,
+    options     => $servers,
+&>
+% } else {
+<& tr-input-text.html, %opt &>
+% }
+</TR>
+
+<%init>
+
+my %opt = @_;
+my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+
+$opt{'field'} ||= 'sip_server';
+
+#false laziness w/select-did.html
+#XXX make sure this comes through on errors too
+my $svcpart  = $opt{'svcpart'}
+            || $opt{'object'}->svcpart
+            || $opt{'object'}->cust_svc->svcpart;
+
+my $part_svc = qsearchs('part_svc', { 'svcpart'=>$svcpart } );
+die "unknown svcpart $svcpart" unless $part_svc;
+
+my $columnflag;
+my $psc = $part_svc->part_svc_column($opt{'field'});
+if ( $psc ) {
+  $columnflag = $psc->columnflag;
+}
+
+my @exports = $part_svc->part_export_did;
+if ( scalar(@exports) > 1 ) {
+  die "more than one DID-providing export attached to svcpart $svcpart";
+}
+
+my $use_selector = 0;
+
+if ( $exports[0] and $exports[0]->can('get_sip_servers') ) {
+  $use_selector = 1;
+}
+
+</%init>
diff --git a/httemplate/view/svc_phone.cgi b/httemplate/view/svc_phone.cgi
index 1c0fb39..aca4129 100644
--- a/httemplate/view/svc_phone.cgi
+++ b/httemplate/view/svc_phone.cgi
@@ -19,6 +19,7 @@ my %labels = map { $_ =>  ( ref($fields->{$_})
 my @fields = qw( countrycode phonenum sim_imsi );
 push @fields, 'domain' if $conf->exists('svc_phone-domain');
 push @fields, qw( pbx_title );
+$labels{pbx_title} = 'PBX';
 
 if ( $conf->exists('showpasswords') ) {
   push @fields, qw( sip_password );
@@ -58,6 +59,8 @@ push @fields, { field => 'circuit_label',
                 link => [ $p.'view/svc_circuit.html?', 'circuit_svcnum' ]
               };
 
+push @fields, 'sip_server';
+
 my $html_foot = sub {
   my $svc_phone = shift;
 

commit 7e07d384748a5d0c5307fd711e4af520bf3b3802
Author: Mark Wells <mark at freeside.biz>
Date:   Sun Dec 28 23:26:11 2014 -0800

    documentation nit

diff --git a/FS/FS/part_export/thinktel.pm b/FS/FS/part_export/thinktel.pm
index 4a28649..d208523 100644
--- a/FS/FS/part_export/thinktel.pm
+++ b/FS/FS/part_export/thinktel.pm
@@ -66,9 +66,11 @@ tie my %roles, 'Tie::IxHash',
                },
   'did'     => {  label     => 'DID',
                   svcdb     => 'svc_phone',
+                  multiple  => 1,
                },
   'gateway' => {  label     => 'SIP gateway',
-                  svcdb     => 'svc_pbx'
+                  svcdb     => 'svc_pbx',
+                  multiple  => 1,
                },
 ;
 
@@ -86,7 +88,7 @@ our %info = (
     <LI>A phone service for the SIP trunk. This should be attached to the 
     export in the "trunk" role. Usually there will be only one of these
     per package. The <I>max_simultaneous</i> field of this service will set 
-    the channel limit on the trunk. The I<sip_password> will be used for
+    the channel limit on the trunk. The <i>sip_password</i> will be used for
     all gateways.</LI>
     <LI>A phone service for a DID. This should be attached in the "did" role.
     DIDs should have no properties other than the number and the E911 
@@ -103,40 +105,6 @@ our %info = (
 END
 );
 
-=item svc_with_role { SVC | PKGNUM }, ROLE
-
-Finds the service(s) in the same package as SVC (or the package PKGNUM) that 
-are linked to the export in ROLE (trunk, gateway, or did).
-
-=cut
-
-sub svc_with_role {
-  my $self = shift;
-  my $svc_or_pkgnum = shift;
-  my $role = shift;
-  my $pkgnum;
-  if ( ref $svc_or_pkgnum ) {
-    $pkgnum = $svc_or_pkgnum->cust_svc->pkgnum or return '';
-  } else {
-    $pkgnum = $svc_or_pkgnum;
-  }
-  my $svcdb = ($role eq 'gateway' ? 'svc_pbx' : 'svc_phone');
-  my @svcs = qsearch({
-    'table'     =>  $svcdb,
-    'addl_from' =>  ' JOIN cust_svc USING (svcnum)' .
-                    ' JOIN export_svc USING (svcpart)',
-    'extra_sql' =>  " WHERE cust_svc.pkgnum = $pkgnum" .
-                    " AND export_svc.exportnum = ".$self->exportnum .
-                    " AND export_svc.role = '$role'",
-  });
-  if ( $role eq 'trunk' ) {
-    warn "$me more than one trunk service in pkgnum $pkgnum.\n" if @svcs > 1;
-    return $svcs[0];
-  } else {
-    return @svcs;
-  }
-}
-
 sub check_svc { # check the service for validity
   my($self, $svc_x) = (shift, shift);
   my $role = $self->svc_role($svc_x)

commit 7e6ab4562bb5490ca82422d91b6389fd844ea6ff
Author: Mark Wells <mark at freeside.biz>
Date:   Sun Dec 28 23:24:37 2014 -0800

    debug

diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm
index bfa5651..0bef7bc 100644
--- a/FS/FS/part_svc.pm
+++ b/FS/FS/part_svc.pm
@@ -13,7 +13,7 @@ use FS::part_svc_class;
 
 @ISA = qw(FS::Record);
 
-$DEBUG = 1;
+$DEBUG = 0;
 
 =head1 NAME
 

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Schema.pm                                    |    1 +
 FS/FS/part_export.pm                               |   72 ++-
 FS/FS/part_export/thinktel.pm                      |   40 +-
 FS/FS/part_export/voip_ms.pm                       |  648 ++++++++++++++++++++
 FS/FS/part_svc.pm                                  |    2 +-
 FS/FS/svc_phone.pm                                 |   14 +
 httemplate/browse/part_export.cgi                  |   24 +-
 httemplate/edit/part_export.cgi                    |   16 +-
 httemplate/edit/svc_phone.cgi                      |    3 +
 httemplate/elements/select-did.html                |   24 +-
 ...r-select-did.html => tr-select-sip_server.html} |   39 +-
 httemplate/view/svc_phone.cgi                      |    3 +
 12 files changed, 792 insertions(+), 94 deletions(-)
 create mode 100644 FS/FS/part_export/voip_ms.pm
 copy httemplate/elements/{tr-select-did.html => tr-select-sip_server.html} (52%)




More information about the freeside-commits mailing list