summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/Schema.pm1
-rw-r--r--FS/FS/part_export.pm72
-rw-r--r--FS/FS/part_export/voip_ms.pm648
-rw-r--r--FS/FS/svc_phone.pm14
-rwxr-xr-xhttemplate/browse/part_export.cgi24
-rw-r--r--httemplate/edit/part_export.cgi16
-rw-r--r--httemplate/edit/svc_phone.cgi3
-rw-r--r--httemplate/elements/select-did.html24
-rw-r--r--httemplate/elements/tr-select-sip_server.html48
-rw-r--r--httemplate/view/svc_phone.cgi3
10 files changed, 813 insertions, 40 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 8b362a7..f559564 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -5656,6 +5656,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 7819a7c..f3d9774 100644
--- a/FS/FS/part_export.pm
+++ b/FS/FS/part_export.pm
@@ -535,23 +535,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
@@ -753,6 +736,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 bd35cba..06ce948 100644
--- a/FS/FS/svc_phone.pm
+++ b/FS/FS/svc_phone.pm
@@ -136,6 +136,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
@@ -253,6 +262,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 %>:&nbsp;</TD>
- <TD ALIGN="left" WIDTH="67%"><% encode_entities($opt{$opt}) %></TD>
+ <TD ALIGN="right" VALIGN="top" WIDTH="33%"><% $optname %>:&nbsp;</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;