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;
11 our $me = '[broadworks]';
12 our %client; # exportnum => client object
13 our %expire; # exportnum => timestamp on which to refresh the client
15 tie my %options, 'Tie::IxHash',
16 'service_provider'=> { label => 'Service Provider ID' },
17 'admin_user' => { label => 'Administrative user ID' },
18 'admin_pass' => { label => 'Administrative password' },
19 'domain' => { label => 'Domain' },
20 'user_limit' => { label => 'Maximum users per customer',
22 'debug' => { label => 'Enable debugging',
27 # do we need roles for this?
28 # no. cust_main -> group, svc_phone -> pilot/single user,
29 # phone_device -> access device
31 # phase 2: svc_pbx -> trunk group, pbx_extension -> trunk user
34 'svc' => [qw( svc_phone svc_pbx )], # part_device?
36 'Provision phone and PBX services to a Broadworks Application Server',
37 'options' => \%options,
39 <P>Export to <b>BroadWorks Application Server</b>.</P>
40 <P>In the simple case where one IP phone corresponds to one public phone
41 number, this requires a svc_phone definition and a part_device. The "title"
42 field ("external name") of the part_device must be one of the access device
43 type names recognized by BroadWorks, such as "Polycom Soundpoint IP 550",
44 "SNOM 320", or "Generic SIP Phone".</P>
50 my($self, $svc_x) = (shift, shift);
52 my $cust_main = $svc_x->cust_main;
53 my ($groupId, $error) = $self->set_cust_main_Group($cust_main);
54 return $error if $error;
56 if ( $svc_x->isa('FS::svc_phone') ) {
58 ($userId, $error) = $self->set_svc_phone_User($svc_x, $groupId);
60 $error ||= $self->set_sip_authentication($userId, $userId, $svc_x->sip_password);
62 return $error if $error;
64 } elsif ( $svc_x->isa('FS::svc_pbx') ) {
72 my($self, $svc_new, $svc_old) = @_;
74 my $cust_main = $svc_new->cust_main;
75 my ($groupId, $error) = $self->set_cust_main_Group($cust_main);
76 return $error if $error;
78 if ( $svc_new->isa('FS::svc_phone') ) {
79 my $oldUserId = $self->userId($svc_old);
80 my $newUserId = $self->userId($svc_new);
82 if ( $oldUserId ne $newUserId ) {
83 my ($success, $message) = $self->request(
84 User => 'UserModifyUserIdRequest',
86 newUserId => $newUserId
88 return $message if !$success;
91 if ( $svc_old->phonenum ne $svc_new->phonenum ) {
92 $error ||= $self->release_number($svc_old->phonenum, $groupId);
96 ($userId, $error) = $self->set_svc_phone_User($svc_new, $groupId);
97 $error ||= $self->set_sip_authentication($userId, $userId, $svc_new->sip_password);
99 if ($error and $oldUserId ne $newUserId) {
100 # rename it back, then
101 my ($success, $message) = $self->request(
102 User => 'UserModifyUserIdRequest',
103 userId => $newUserId,
104 newUserId => $oldUserId
106 # if it fails, we can't really fix it
107 return "$error; unable to reverse user ID change: $message" if !$success;
110 return $error if $error;
112 } elsif ( $svc_new->isa('FS::svc_pbx') ) {
120 my ($self, $svc_x) = @_;
122 my $cust_main = $svc_x->cust_main;
123 my $groupId = $self->groupId($cust_main);
125 if ( $svc_x->isa('FS::svc_phone') ) {
126 my $userId = $self->userId($svc_x);
127 my $error = $self->delete_User($userId)
128 || $self->release_number($svc_x->phonenum, $groupId);
129 return $error if $error;
130 } elsif ( $svc_x->isa('FS::svc_pbx') ) {
134 # find whether the customer still has any services on this platform
135 # (other than the one being deleted)
136 my %svcparts = map { $_->svcpart => 1 } $self->export_svc;
137 my $svcparts = join(',', keys %svcparts);
138 my $num_svcs = FS::cust_svc->count(
139 '(select custnum from cust_pkg where cust_pkg.pkgnum = cust_svc.pkgnum) '.
142 " AND svcpart IN ($svcparts)",
147 if ( $num_svcs == 0 ) {
148 warn "$me removed last service for group $groupId; deleting group.\n";
149 my $error = $self->delete_Group($groupId);
150 warn "$me error deleting group: $error\n" if $error;
151 return "$error (removing customer group)" if $error;
157 sub export_device_insert {
158 my ($self, $svc_x, $device) = @_;
160 if ( $device->count('svcnum = ?', $svc_x->svcnum) > 1 ) {
161 return "This service already has a device.";
164 my $cust_main = $svc_x->cust_main;
165 my $groupId = $self->groupId($cust_main);
167 my ($deviceName, $error) = $self->set_device_AccessDevice($device, $groupId);
168 return $error if $error;
170 if ( $device->isa('FS::phone_device') ) {
171 return $self->set_endpoint( $self->userId($svc_x), $deviceName);
172 } # else pbx_device, extension_device
177 sub export_device_replace {
178 my ($self, $svc_x, $new_device, $old_device) = @_;
179 my $cust_main = $svc_x->cust_main;
180 my $groupId = $self->groupId($cust_main);
182 my $new_deviceName = $self->deviceName($new_device);
183 my $old_deviceName = $self->deviceName($old_device);
185 if ($new_deviceName ne $old_deviceName) {
187 # do it in this order to switch the service endpoint over to the new
189 return $self->export_device_insert($svc_x, $new_device)
190 || $self->delete_Device($old_deviceName, $groupId);
192 } else { # update in place
194 my ($deviceName, $error) = $self->set_device_AccessDevice($new_device, $groupId);
195 return $error if $error;
200 sub export_device_delete {
201 my ($self, $svc_x, $device) = @_;
203 if ( $device->isa('FS::phone_device') ) {
204 my $error = $self->set_endpoint( $self->userId($svc_x), '' );
205 return $error if $error;
208 return $self->delete_Device($self->deviceName($device));
212 =head2 CREATE-OR-UPDATE METHODS
214 These take a Freeside object that can be exported to the Broadworks system,
215 determine if it already has been exported, and if so, update it to match the
216 Freeside object. If it's not already there, they create it. They return a list
218 - that object's identifying string or hashref or whatever in Broadworks, and
219 - an error message, if creating the object failed.
223 =item set_cust_main_Group CUST_MAIN
225 Takes a L<FS::cust_main>, creates a Group for the customer, and returns a
226 GroupId. If the Group exists, it will be updated with the current customer
231 sub set_cust_main_Group {
233 my $cust_main = shift;
234 my $location = $cust_main->ship_location;
236 my $LSC = Locale::SubCountry->new($location->country)
237 or return(0, "Invalid country code ".$location->country);
239 if ( $LSC->has_sub_countries ) {
240 $state_name = $LSC->full_name( $location->state );
243 my $groupId = $self->groupId($cust_main);
247 defaultDomain => $self->option('domain'),
248 userLimit => $self->option('user_limit'),
249 groupName => $cust_main->name_short,
250 callingLineIdName => $cust_main->name_short,
252 contactName => $cust_main->contact_firstlast,
253 contactNumber => ( $cust_main->daytime
255 || $cust_main->mobile
258 contactEmail => ( ($cust_main->all_emails)[0] || undef ),
261 addressLine1 => $location->address1,
262 addressLine2 => ($location->address2 || undef),
263 city => $location->city,
264 stateOrProvince => $state_name,
265 zipOrPostalCode => $location->zip,
266 country => $location->country,
270 my ($success, $message) = $self->request('Group' => 'GroupGetRequest14sp7',
275 if ($success) { # update it with the curent params
277 ($success, $message) =
278 $self->request('Group' => 'GroupModifyRequest', %group_info);
280 } elsif ($message =~ /Group not found/) {
283 ($success, $message) =
284 $self->request('Group' => 'GroupAddRequest', %group_info);
287 # tell the group that its users in general are allowed to use
289 ($success, $message) = $self->request(
290 'Group' => 'GroupServiceModifyAuthorizationListRequest',
293 userServiceAuthorization => {
294 serviceName => 'Authentication',
295 authorizedQuantity => { unlimited => 'true' },
301 # tell the group that each new user, specifically, is allowed to
303 ($success, $message) = $self->request(
304 'Group' => 'GroupNewUserTemplateAssignUserServiceListRequest',
307 serviceName => 'Authentication',
311 } # else we somehow failed to fetch the group; throw an error
314 return ($groupId, '');
316 return ('', $message);
320 =item set_svc_phone_User SVC_PHONE, GROUPID
322 Creates a User object corresponding to this svc_phone, in the specified
323 group. If the User already exists, updates the record with the current
324 customer name (or phone name), phone number, and access device.
328 sub set_svc_phone_User {
329 my ($self, $svc_phone, $groupId) = @_;
333 # make sure the phone number is available
334 $error = $self->assign_number( $svc_phone->phonenum, $groupId );
336 my $userId = $self->userId($svc_phone);
337 my $cust_main = $svc_phone->cust_main;
340 if ($svc_phone->phone_name =~ /,/) {
341 ($last, $first) = split(/,\s*/, $svc_phone->phone_name);
342 } elsif ($svc_phone->phone_name =~ / /) {
343 ($first, $last) = split(/ +/, $svc_phone->phone_name, 2);
345 $first = $cust_main->first;
346 $last = $cust_main->last;
355 callingLineIdLastName => $last,
356 callingLineIdFirstName => $first,
357 password => $svc_phone->sip_password,
358 # not supported: nameDialingName; Hiragana names
359 phoneNumber => $svc_phone->phonenum,
360 callingLinePhoneNumber => $svc_phone->phonenum,
363 # does the user exist?
364 my ($success, $message) = $self->request(
365 'User' => 'UserGetRequest21',
369 if ( $success ) { # modify in place
371 ($success, $message) = $self->request(
372 'User' => 'UserModifyRequest17sp4',
376 } elsif ( $message =~ /User not found/ ) { # create new
378 ($success, $message) = $self->request(
379 'User' => 'UserAddRequest17sp4',
386 return ($userId, '');
388 return ('', $message);
392 =item set_device_AccessDevice DEVICE, [ GROUPID ]
394 Creates/updates an Access Device Profile. This is a record for a
395 I<specific physical device> that can send/receive calls. (Not to be confused
396 with an "Access Device Endpoint", which is a I<port> on such a device.) DEVICE
397 can be any record with a foreign key to L<FS::part_device>.
399 If GROUPID is specified, this device profile will be created at the Group
400 level in that group; otherwise it will be a ServiceProvider level record.
404 sub set_device_AccessDevice {
409 my $deviceName = $self->deviceName($device);
412 if ($device->svcnum) {
413 $svc_x = FS::cust_svc->by_key($device->svcnum)->svc_x;
415 $svc_x = FS::svc_phone->new({}); # returns empty for all fields
418 my $part_device = $device->part_device
419 or return ('', "devicepart ".$device->part_device." not defined" );
424 deviceName => $deviceName,
425 deviceType => $part_device->title,
426 description => ($svc_x->title # svc_pbx
427 || $part_device->devicename), # others
431 $new_device{netAddress} = $svc_x->ip_addr if $svc_x->ip_addr; # svc_pbx only
432 $new_device{macAddress} = $device->mac_addr if $device->mac_addr;
436 deviceName => $deviceName
438 my $level = 'ServiceProvider';
442 $find_device{groupId} = $new_device{groupId} = $groupId;
444 # shouldn't be used in our current design
445 warn "$me creating access device $deviceName at Service Provider level\n";
448 my ($success, $message) = $self->request(
449 $level, $level.'AccessDeviceGetRequest18sp1',
453 if ( $success ) { # modify in place
455 ($success, $message) = $self->request(
456 $level => $level.'AccessDeviceModifyRequest14',
460 } elsif ( $message =~ /Access Device not found/ ) { # create new
462 ($success, $message) = $self->request(
463 $level => $level.'AccessDeviceAddRequest14',
470 return ($deviceName, '');
472 return ('', $message);
478 =head2 PROVISIONING METHODS
480 These return an error string on failure, and an empty string on success.
484 =item assign_number NUMBER, GROUPID
486 Assigns a phone number to a group. If it's assigned to a different group or
487 doesn't belong to the service provider, this will fail. If it's already
488 assigned to I<this> group, it will do nothing and return success.
493 my ($self, $number, $groupId) = @_;
494 # see if it's already assigned
495 my ($success, $message) = $self->request(
496 Group => 'GroupDnGetAssignmentListRequest18',
499 searchCriteriaDn => {
502 isCaseInsensitive => 'false',
505 return "$message (checking phone number status)" if !$success;
506 my $result = $self->oci_table( $message->{dnTable} );
507 return '' if @$result > 0;
509 ($success, $message) = $self->request(
510 Group => 'GroupDnAssignListRequest',
513 phoneNumber => $number,
516 $success ? '' : $message;
519 =item release_number NUMBER, GROUPID
521 Unassigns a phone number from a group. If it's assigned to a user in the
522 group then this will fail. If it's not assigned to the group at all, this
528 my ($self, $number, $groupId) = @_;
529 # see if it's already assigned
530 my ($success, $message) = $self->request(
531 Group => 'GroupDnGetAssignmentListRequest18',
534 searchCriteriaDn => {
537 isCaseInsensitive => 'false',
540 return "$message (checking phone number status)" if !$success;
541 my $result = $self->oci_table( $message->{dnTable} );
542 return '' if @$result == 0;
544 ($success, $message) = $self->request(
545 Group => 'GroupDnUnassignListRequest',
548 phoneNumber => $number,
551 $success ? '' : $message;
554 =item set_endpoint USERID [, DEVICENAME ]
556 Sets the endpoint for communicating with USERID to DEVICENAME. For now, this
557 assumes that all devices are defined at Group level.
559 If DEVICENAME is null, the user will be set to have no endpoint.
563 # we only support linePort = userId, and no numbered ports
566 my ($self, $userId, $deviceName) = @_;
569 if ( length($deviceName) > 0 ) {
571 accessDeviceEndpoint => {
574 deviceLevel => 'Group',
575 deviceName => $deviceName,
582 my ($success, $message) = $self->request(
583 User => 'UserModifyRequest17sp4',
585 endpoint => $endpoint,
588 $success ? '' : $message;
591 =item set_sip_authentication USERID, NAME, PASSWORD
593 Sets the SIP authentication credentials for USERID to (NAME, PASSWORD).
597 sub set_sip_authentication {
598 my ($self, $userId, $userName, $password) = @_;
600 my ($success, $message) = $self->request(
601 'Services/ServiceAuthentication' => 'UserAuthenticationModifyRequest',
603 userName => $userName,
604 newPassword => $password,
607 $success ? '' : $message;
610 =item delete_group GROUPID
612 Deletes the group GROUPID.
617 my ($self, $groupId) = @_;
619 my ($success, $message) = $self->request(
620 Group => 'GroupDeleteRequest',
624 if ( $success or $message =~ /Group not found/ ) {
631 =item delete_User USERID
633 Deletes the user USERID, and releases its phone number if it has one.
638 my ($self, $userId) = @_;
640 my ($success, $message) = $self->request(
641 User => 'UserDeleteRequest',
644 if ($success or $message =~ /User not found/) {
651 =item delete_Device DEVICENAME[, GROUPID ]
653 Deletes the access device DEVICENAME (from group GROUPID, or from the service
654 provider if there is no GROUPID).
659 my ($self, $deviceName, $groupId) = @_;
661 my ($success, $message);
663 ($success, $message) = $self->request(
664 Group => 'GroupAccessDeviceDeleteRequest',
667 deviceName => $deviceName,
670 ($success, $message) = $self->request(
671 ServiceProvider => 'ServiceProviderAccessDeviceDeleteRequest',
673 deviceName => $deviceName,
676 if ( $success or $message =~ /Access Device not found/ ) {
685 =head2 CONVENIENCE METHODS
691 Returns 'serviceProviderId' => the service_provider option. This is commonly
692 needed in request parameters.
694 =item groupId CUST_MAIN
696 Returns the groupID that goes with the specified customer.
700 Returns the userId (including domain) that should go with the specified
703 =item deviceName DEVICE
705 Returns the access device name that should go with the specified phone_device
712 my $id = $self->option('service_provider') or die 'service provider not set';
713 'serviceProviderId' => $id
718 my $cust_main = shift;
719 'cust_main#'.$cust_main->custnum;
726 if ($svc->phonenum) {
727 $userId = $svc->phonenum;
728 } else { # pbx_extension needs one of these
729 die "can't determine userId for non-svc_phone service";
731 my $domain = $self->option('domain'); # domsvc?
732 $userId .= '@' . $domain if $domain;
740 $device->mac_addr || ($device->table . '#' . $device->devicenum);
743 =item oci_table HASHREF
745 Converts the base OCITable type into an arrayref of hashrefs.
751 my $oci_table = shift;
752 my @colnames = $oci_table->{colHeading};
754 foreach my $row (@{ $oci_table->{row} }) {
756 @hash{@colnames} = @{ $row->{col} };
773 =item import_cdrs START, END
775 Retrieves CDRs for calls in the date range from START to END and inserts them
776 as a new CDR batch. On success, returns a new cdr_batch object. On failure,
777 returns an error message. If there are no new CDRs, returns nothing.
785 =item request SCOPE, COMMAND, [ ARGUMENTS... ]
787 Wrapper for L<BroadWorks::OCI/request>. The client object will be cached.
788 Returns two values: a flag, true or false, indicating success of the request,
789 and the decoded response message as a hashref.
791 On failure of the request (or failure to authenticate), the response message
792 will be a simple scalar containing the error message.
799 delete $client{$self->exportnum} if $expire{$self->exportnum} < time;
800 my $client = $client{$self->exportnum};
803 eval "use BroadWorks::OCI";
806 Log::Report::dispatcher('PERL', 'default',
807 mode => ($self->option('debug') ? 'DEBUG' : 'NORMAL')
810 $client = BroadWorks::OCI->new(
811 userId => $self->option('admin_user'),
812 password => $self->option('admin_pass'),
814 my ($success, $message) = $client->login;
815 return ('', $message) if !$success;
817 $client{$self->exportnum} = $client; # if login succeeded
818 $expire{$self->exportnum} = time + 120; # hardcoded, yeah
820 return $client->request(@_);