1 package FS::part_export::broadworks;
3 use base qw( FS::part_export );
7 use FS::Record qw(dbh qsearch qsearchs);
8 use Locale::SubCountry;
10 our $me = '[broadworks]';
11 our %client; # exportnum => client object
12 our %expire; # exportnum => timestamp on which to refresh the client
14 tie my %options, 'Tie::IxHash',
15 'service_provider'=> { label => 'Service Provider ID' },
16 'admin_user' => { label => 'Administrative user ID' },
17 'admin_pass' => { label => 'Administrative password' },
18 'domain' => { label => 'Domain' },
19 'user_limit' => { label => 'Maximum users per customer',
21 'debug' => { label => 'Enable debugging',
26 # do we need roles for this?
27 # no. cust_main -> group, svc_phone -> pilot/single user,
28 # phone_device -> access device
30 # phase 2: svc_pbx -> trunk group, pbx_extension -> trunk user
33 'svc' => [qw( svc_phone svc_pbx )], # part_device?
35 'Provision phone and PBX services to a Broadworks Application Server',
36 'options' => \%options,
38 <P>Export to <b>BroadWorks Application Server</b>.</P>
39 <P>In the simple case where one IP phone corresponds to one public phone
40 number, this requires a svc_phone definition and a part_device. The "title"
41 field ("external name") of the part_device must be one of the access device
42 type names recognized by BroadWorks, such as "Polycom Soundpoint IP 550",
43 "SNOM 320", or "Generic SIP Phone".</P>
49 my($self, $svc_x) = (shift, shift);
51 my $cust_main = $svc_x->cust_main;
52 my ($groupId, $error) = $self->set_cust_main_Group($cust_main);
53 return $error if $error;
55 if ( $svc_x->isa('FS::svc_phone') ) {
57 ($userId, $error) = $self->set_svc_phone_User($svc_x, $groupId);
59 $error ||= $self->set_sip_authentication($userId, $userId, $svc_x->sip_password);
61 return $error if $error;
63 } elsif ( $svc_x->isa('FS::svc_pbx') ) {
71 my($self, $svc_new, $svc_old) = @_;
73 my $cust_main = $svc_new->cust_main;
74 my ($groupId, $error) = $self->set_cust_main_Group($cust_main);
75 return $error if $error;
77 if ( $svc_new->isa('FS::svc_phone') ) {
78 my $oldUserId = $self->userId($svc_old);
79 my $newUserId = $self->userId($svc_new);
81 if ( $oldUserId ne $newUserId ) {
82 my ($success, $message) = $self->request(
83 User => 'UserModifyUserIdRequest',
85 newUserId => $newUserId
87 return $message if !$success;
90 if ( $svc_old->phonenum ne $svc_new->phonenum ) {
91 $error ||= $self->release_number($svc_old->phonenum, $groupId);
95 ($userId, $error) = $self->set_svc_phone_User($svc_new, $groupId);
96 $error ||= $self->set_sip_authentication($userId, $userId, $svc_new->sip_password);
98 if ($error and $oldUserId ne $newUserId) {
99 # rename it back, then
100 my ($success, $message) = $self->request(
101 User => 'UserModifyUserIdRequest',
102 userId => $newUserId,
103 newUserId => $oldUserId
105 # if it fails, we can't really fix it
106 return "$error; unable to reverse user ID change: $message" if !$success;
109 return $error if $error;
111 } elsif ( $svc_new->isa('FS::svc_pbx') ) {
119 my ($self, $svc_x) = @_;
121 my $cust_main = $svc_x->cust_main;
122 my $groupId = $self->groupId($cust_main);
124 if ( $svc_x->isa('FS::svc_phone') ) {
125 my $userId = $self->userId($svc_x);
126 my $error = $self->delete_User($userId)
127 || $self->release_number($svc_x->phonenum, $groupId);
128 return $error if $error;
129 } elsif ( $svc_x->isa('FS::svc_pbx') ) {
133 # find whether the customer still has any services on this platform
134 # (other than the one being deleted)
135 my %svcparts = map { $_->svcpart => 1 } $self->export_svc;
136 my $svcparts = join(',', keys %svcparts);
137 my $num_svcs = FS::cust_svc->count(
138 '(select custnum from cust_pkg where cust_pkg.pkgnum = cust_svc.pkgnum) '.
141 " AND svcpart IN ($svcparts)",
146 if ( $num_svcs == 0 ) {
147 warn "$me removed last service for group $groupId; deleting group.\n";
148 my $error = $self->delete_Group($groupId);
149 warn "$me error deleting group: $error\n" if $error;
150 return "$error (removing customer group)" if $error;
156 sub export_device_insert {
157 my ($self, $svc_x, $device) = @_;
159 if ( $device->count('svcnum = ?', $svc_x->svcnum) > 1 ) {
160 return "This service already has a device.";
163 my $cust_main = $svc_x->cust_main;
164 my $groupId = $self->groupId($cust_main);
166 my ($deviceName, $error) = $self->set_device_AccessDevice($device, $groupId);
167 return $error if $error;
169 if ( $device->isa('FS::phone_device') ) {
170 return $self->set_endpoint( $self->userId($svc_x), $deviceName);
171 } # else pbx_device, extension_device
176 sub export_device_replace {
177 my ($self, $svc_x, $new_device, $old_device) = @_;
178 my $cust_main = $svc_x->cust_main;
179 my $groupId = $self->groupId($cust_main);
181 my $new_deviceName = $self->deviceName($new_device);
182 my $old_deviceName = $self->deviceName($old_device);
184 if ($new_deviceName ne $old_deviceName) {
186 # do it in this order to switch the service endpoint over to the new
188 return $self->export_device_insert($svc_x, $new_device)
189 || $self->delete_Device($old_deviceName, $groupId);
191 } else { # update in place
193 my ($deviceName, $error) = $self->set_device_AccessDevice($new_device, $groupId);
194 return $error if $error;
199 sub export_device_delete {
200 my ($self, $svc_x, $device) = @_;
202 if ( $device->isa('FS::phone_device') ) {
203 my $error = $self->set_endpoint( $self->userId($svc_x), '' );
204 return $error if $error;
207 return $self->delete_Device($self->deviceName($device));
211 =head2 CREATE-OR-UPDATE METHODS
213 These take a Freeside object that can be exported to the Broadworks system,
214 determine if it already has been exported, and if so, update it to match the
215 Freeside object. If it's not already there, they create it. They return a list
217 - that object's identifying string or hashref or whatever in Broadworks, and
218 - an error message, if creating the object failed.
222 =item set_cust_main_Group CUST_MAIN
224 Takes a L<FS::cust_main>, creates a Group for the customer, and returns a
225 GroupId. If the Group exists, it will be updated with the current customer
230 sub set_cust_main_Group {
232 my $cust_main = shift;
233 my $location = $cust_main->ship_location;
235 my $LSC = Locale::SubCountry->new($location->country)
236 or return(0, "Invalid country code ".$location->country);
238 if ( $LSC->has_sub_countries ) {
239 $state_name = $LSC->full_name( $location->state );
242 my $groupId = $self->groupId($cust_main);
246 defaultDomain => $self->option('domain'),
247 userLimit => $self->option('user_limit'),
248 groupName => $cust_main->name_short,
249 callingLineIdName => $cust_main->name_short,
251 contactName => $cust_main->contact_firstlast,
252 contactNumber => ( $cust_main->daytime
254 || $cust_main->mobile
257 contactEmail => ( ($cust_main->all_emails)[0] || undef ),
260 addressLine1 => $location->address1,
261 addressLine2 => ($location->address2 || undef),
262 city => $location->city,
263 stateOrProvince => $state_name,
264 zipOrPostalCode => $location->zip,
265 country => $location->country,
269 my ($success, $message) = $self->request('Group' => 'GroupGetRequest14sp7',
274 if ($success) { # update it with the curent params
276 ($success, $message) =
277 $self->request('Group' => 'GroupModifyRequest', %group_info);
279 } elsif ($message =~ /Group not found/) {
282 ($success, $message) =
283 $self->request('Group' => 'GroupAddRequest', %group_info);
286 # tell the group that its users in general are allowed to use
288 ($success, $message) = $self->request(
289 'Group' => 'GroupServiceModifyAuthorizationListRequest',
292 userServiceAuthorization => {
293 serviceName => 'Authentication',
294 authorizedQuantity => { unlimited => 'true' },
300 # tell the group that each new user, specifically, is allowed to
302 ($success, $message) = $self->request(
303 'Group' => 'GroupNewUserTemplateAssignUserServiceListRequest',
306 serviceName => 'Authentication',
310 } # else we somehow failed to fetch the group; throw an error
313 return ($groupId, '');
315 return ('', $message);
319 =item set_svc_phone_User SVC_PHONE, GROUPID
321 Creates a User object corresponding to this svc_phone, in the specified
322 group. If the User already exists, updates the record with the current
323 customer name (or phone name), phone number, and access device.
327 sub set_svc_phone_User {
328 my ($self, $svc_phone, $groupId) = @_;
332 # make sure the phone number is available
333 $error = $self->assign_number( $svc_phone->phonenum, $groupId );
335 my $userId = $self->userId($svc_phone);
336 my $cust_main = $svc_phone->cust_main;
339 if ($svc_phone->phone_name =~ /,/) {
340 ($last, $first) = split(/,\s*/, $svc_phone->phone_name);
341 } elsif ($svc_phone->phone_name =~ / /) {
342 ($first, $last) = split(/ +/, $svc_phone->phone_name, 2);
344 $first = $cust_main->first;
345 $last = $cust_main->last;
354 callingLineIdLastName => $last,
355 callingLineIdFirstName => $first,
356 password => $svc_phone->sip_password,
357 # not supported: nameDialingName; Hiragana names
358 phoneNumber => $svc_phone->phonenum,
359 callingLinePhoneNumber => $svc_phone->phonenum,
362 # does the user exist?
363 my ($success, $message) = $self->request(
364 'User' => 'UserGetRequest21',
368 if ( $success ) { # modify in place
370 ($success, $message) = $self->request(
371 'User' => 'UserModifyRequest17sp4',
375 } elsif ( $message =~ /User not found/ ) { # create new
377 ($success, $message) = $self->request(
378 'User' => 'UserAddRequest17sp4',
385 return ($userId, '');
387 return ('', $message);
391 =item set_device_AccessDevice DEVICE, [ GROUPID ]
393 Creates/updates an Access Device Profile. This is a record for a
394 I<specific physical device> that can send/receive calls. (Not to be confused
395 with an "Access Device Endpoint", which is a I<port> on such a device.) DEVICE
396 can be any record with a foreign key to L<FS::part_device>.
398 If GROUPID is specified, this device profile will be created at the Group
399 level in that group; otherwise it will be a ServiceProvider level record.
403 sub set_device_AccessDevice {
408 my $deviceName = $self->deviceName($device);
411 if ($device->svcnum) {
412 $svc_x = FS::cust_svc->by_key($device->svcnum)->svc_x;
414 $svc_x = FS::svc_phone->new({}); # returns empty for all fields
417 my $part_device = $device->part_device
418 or return ('', "devicepart ".$device->part_device." not defined" );
423 deviceName => $deviceName,
424 deviceType => $part_device->title,
425 description => ($svc_x->title # svc_pbx
426 || $part_device->devicename), # others
430 $new_device{netAddress} = $svc_x->ip_addr if $svc_x->ip_addr; # svc_pbx only
431 $new_device{macAddress} = $device->mac_addr if $device->mac_addr;
435 deviceName => $deviceName
437 my $level = 'ServiceProvider';
441 $find_device{groupId} = $new_device{groupId} = $groupId;
443 # shouldn't be used in our current design
444 warn "$me creating access device $deviceName at Service Provider level\n";
447 my ($success, $message) = $self->request(
448 $level, $level.'AccessDeviceGetRequest18sp1',
452 if ( $success ) { # modify in place
454 ($success, $message) = $self->request(
455 $level => $level.'AccessDeviceModifyRequest14',
459 } elsif ( $message =~ /Access Device not found/ ) { # create new
461 ($success, $message) = $self->request(
462 $level => $level.'AccessDeviceAddRequest14',
469 return ($deviceName, '');
471 return ('', $message);
477 =head2 PROVISIONING METHODS
479 These return an error string on failure, and an empty string on success.
483 =item assign_number NUMBER, GROUPID
485 Assigns a phone number to a group. If it's assigned to a different group or
486 doesn't belong to the service provider, this will fail. If it's already
487 assigned to I<this> group, it will do nothing and return success.
492 my ($self, $number, $groupId) = @_;
493 # see if it's already assigned
494 my ($success, $message) = $self->request(
495 Group => 'GroupDnGetAssignmentListRequest18',
498 searchCriteriaDn => {
501 isCaseInsensitive => 'false',
504 return "$message (checking phone number status)" if !$success;
505 my $result = $self->oci_table( $message->{dnTable} );
506 return '' if @$result > 0;
508 ($success, $message) = $self->request(
509 Group => 'GroupDnAssignListRequest',
512 phoneNumber => $number,
515 $success ? '' : $message;
518 =item release_number NUMBER, GROUPID
520 Unassigns a phone number from a group. If it's assigned to a user in the
521 group then this will fail. If it's not assigned to the group at all, this
527 my ($self, $number, $groupId) = @_;
528 # see if it's already assigned
529 my ($success, $message) = $self->request(
530 Group => 'GroupDnGetAssignmentListRequest18',
533 searchCriteriaDn => {
536 isCaseInsensitive => 'false',
539 return "$message (checking phone number status)" if !$success;
540 my $result = $self->oci_table( $message->{dnTable} );
541 return '' if @$result == 0;
543 ($success, $message) = $self->request(
544 Group => 'GroupDnUnassignListRequest',
547 phoneNumber => $number,
550 $success ? '' : $message;
553 =item set_endpoint USERID [, DEVICENAME ]
555 Sets the endpoint for communicating with USERID to DEVICENAME. For now, this
556 assumes that all devices are defined at Group level.
558 If DEVICENAME is null, the user will be set to have no endpoint.
562 # we only support linePort = userId, and no numbered ports
565 my ($self, $userId, $deviceName) = @_;
568 if ( length($deviceName) > 0 ) {
570 accessDeviceEndpoint => {
573 deviceLevel => 'Group',
574 deviceName => $deviceName,
581 my ($success, $message) = $self->request(
582 User => 'UserModifyRequest17sp4',
584 endpoint => $endpoint,
587 $success ? '' : $message;
590 =item set_sip_authentication USERID, NAME, PASSWORD
592 Sets the SIP authentication credentials for USERID to (NAME, PASSWORD).
596 sub set_sip_authentication {
597 my ($self, $userId, $userName, $password) = @_;
599 my ($success, $message) = $self->request(
600 'Services/ServiceAuthentication' => 'UserAuthenticationModifyRequest',
602 userName => $userName,
603 newPassword => $password,
606 $success ? '' : $message;
609 =item delete_group GROUPID
611 Deletes the group GROUPID.
616 my ($self, $groupId) = @_;
618 my ($success, $message) = $self->request(
619 Group => 'GroupDeleteRequest',
623 if ( $success or $message =~ /Group not found/ ) {
630 =item delete_User USERID
632 Deletes the user USERID, and releases its phone number if it has one.
637 my ($self, $userId) = @_;
639 my ($success, $message) = $self->request(
640 User => 'UserDeleteRequest',
643 if ($success or $message =~ /User not found/) {
650 =item delete_Device DEVICENAME[, GROUPID ]
652 Deletes the access device DEVICENAME (from group GROUPID, or from the service
653 provider if there is no GROUPID).
658 my ($self, $deviceName, $groupId) = @_;
660 my ($success, $message);
662 ($success, $message) = $self->request(
663 Group => 'GroupAccessDeviceDeleteRequest',
666 deviceName => $deviceName,
669 ($success, $message) = $self->request(
670 ServiceProvider => 'ServiceProviderAccessDeviceDeleteRequest',
672 deviceName => $deviceName,
675 if ( $success or $message =~ /Access Device not found/ ) {
684 =head2 CONVENIENCE METHODS
690 Returns 'serviceProviderId' => the service_provider option. This is commonly
691 needed in request parameters.
693 =item groupId CUST_MAIN
695 Returns the groupID that goes with the specified customer.
699 Returns the userId (including domain) that should go with the specified
702 =item deviceName DEVICE
704 Returns the access device name that should go with the specified phone_device
711 my $id = $self->option('service_provider') or die 'service provider not set';
712 'serviceProviderId' => $id
717 my $cust_main = shift;
718 'cust_main#'.$cust_main->custnum;
725 if ($svc->phonenum) {
726 $userId = $svc->phonenum;
727 } else { # pbx_extension needs one of these
728 die "can't determine userId for non-svc_phone service";
730 my $domain = $self->option('domain'); # domsvc?
731 $userId .= '@' . $domain if $domain;
739 $device->mac_addr || ($device->table . '#' . $device->devicenum);
742 =item oci_table HASHREF
744 Converts the base OCITable type into an arrayref of hashrefs.
750 my $oci_table = shift;
751 my @colnames = $oci_table->{colHeading};
753 foreach my $row (@{ $oci_table->{row} }) {
755 @hash{@colnames} = @{ $row->{col} };
772 =item import_cdrs START, END
774 Retrieves CDRs for calls in the date range from START to END and inserts them
775 as a new CDR batch. On success, returns a new cdr_batch object. On failure,
776 returns an error message. If there are no new CDRs, returns nothing.
784 =item request SCOPE, COMMAND, [ ARGUMENTS... ]
786 Wrapper for L<BroadWorks::OCI/request>. The client object will be cached.
787 Returns two values: a flag, true or false, indicating success of the request,
788 and the decoded response message as a hashref.
790 On failure of the request (or failure to authenticate), the response message
791 will be a simple scalar containing the error message.
798 delete $client{$self->exportnum} if $expire{$self->exportnum} < time;
799 my $client = $client{$self->exportnum};
802 eval "use BroadWorks::OCI";
805 Log::Report::dispatcher('PERL', 'default',
806 mode => ($self->option('debug') ? 'DEBUG' : 'NORMAL')
809 $client = BroadWorks::OCI->new(
810 userId => $self->option('admin_user'),
811 password => $self->option('admin_pass'),
813 my ($success, $message) = $client->login;
814 return ('', $message) if !$success;
816 $client{$self->exportnum} = $client; # if login succeeded
817 $expire{$self->exportnum} = time + 120; # hardcoded, yeah
819 return $client->request(@_);