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>
45 <P>Each phone service must have a device linked before it will be functional.
46 Until then, authentication will be denied.</P>
51 my($self, $svc_x) = (shift, shift);
53 my $cust_main = $svc_x->cust_main;
54 my ($groupId, $error) = $self->set_cust_main_Group($cust_main);
55 return $error if $error;
57 if ( $svc_x->isa('FS::svc_phone') ) {
59 ($userId, $error) = $self->set_svc_phone_User($svc_x, $groupId);
61 $error ||= $self->set_sip_authentication($userId, $userId, $svc_x->sip_password);
63 return $error if $error;
65 } elsif ( $svc_x->isa('FS::svc_pbx') ) {
73 my($self, $svc_new, $svc_old) = @_;
75 my $cust_main = $svc_new->cust_main;
76 my ($groupId, $error) = $self->set_cust_main_Group($cust_main);
77 return $error if $error;
79 if ( $svc_new->isa('FS::svc_phone') ) {
80 my $oldUserId = $self->userId($svc_old);
81 my $newUserId = $self->userId($svc_new);
83 if ( $oldUserId ne $newUserId ) {
84 my ($success, $message) = $self->request(
85 User => 'UserModifyUserIdRequest',
87 newUserId => $newUserId
89 return $message if !$success;
91 if ( my $device = qsearchs('phone_device', { svcnum => $svc_new->svcnum }) ) {
92 # there's a Line/Port configured for the device, and it also needs to be renamed.
93 $error ||= $self->set_endpoint( $newUserId, $self->deviceName($device) );
97 if ( $svc_old->phonenum ne $svc_new->phonenum ) {
98 $error ||= $self->release_number($svc_old->phonenum, $groupId);
102 ($userId, $error) = $self->set_svc_phone_User($svc_new, $groupId);
103 $error ||= $self->set_sip_authentication($userId, $userId, $svc_new->sip_password);
105 if ($error and $oldUserId ne $newUserId) {
106 # rename it back, then
107 my ($success, $message) = $self->request(
108 User => 'UserModifyUserIdRequest',
109 userId => $newUserId,
110 newUserId => $oldUserId
112 # if it fails, we can't really fix it
113 return "$error; unable to reverse user ID change: $message" if !$success;
116 return $error if $error;
118 } elsif ( $svc_new->isa('FS::svc_pbx') ) {
126 my ($self, $svc_x) = @_;
128 my $cust_main = $svc_x->cust_main;
129 my $groupId = $self->groupId($cust_main);
131 if ( $svc_x->isa('FS::svc_phone') ) {
132 my $userId = $self->userId($svc_x);
133 my $error = $self->delete_User($userId)
134 || $self->release_number($svc_x->phonenum, $groupId);
135 return $error if $error;
136 } elsif ( $svc_x->isa('FS::svc_pbx') ) {
140 # find whether the customer still has any services on this platform
141 # (other than the one being deleted)
142 my %svcparts = map { $_->svcpart => 1 } $self->export_svc;
143 my $svcparts = join(',', keys %svcparts);
144 my $num_svcs = FS::cust_svc->count(
145 '(select custnum from cust_pkg where cust_pkg.pkgnum = cust_svc.pkgnum) '.
148 " AND svcpart IN ($svcparts)",
153 if ( $num_svcs == 0 ) {
154 warn "$me removed last service for group $groupId; deleting group.\n";
155 my $error = $self->delete_Group($groupId);
156 warn "$me error deleting group: $error\n" if $error;
157 return "$error (removing customer group)" if $error;
163 sub export_device_insert {
164 my ($self, $svc_x, $device) = @_;
166 if ( $FS::svc_Common::noexport_hack ) {
167 carp 'export_device_insert() suppressed by noexport_hack'
168 if $self->option('debug');
172 if ( $device->count('svcnum = ?', $svc_x->svcnum) > 1 ) {
173 return "This service already has a device.";
176 my $cust_main = $svc_x->cust_main;
177 my $groupId = $self->groupId($cust_main);
179 my ($deviceName, $error) = $self->set_device_AccessDevice($device, $groupId);
180 return $error if $error;
182 if ( $device->isa('FS::phone_device') ) {
183 return $self->set_endpoint( $self->userId($svc_x), $deviceName);
184 } # else pbx_device, extension_device
189 sub export_device_replace {
190 my ($self, $svc_x, $new_device, $old_device) = @_;
192 if ( $FS::svc_Common::noexport_hack ) {
193 carp 'export_device_replace() suppressed by noexport_hack'
194 if $self->option('debug');
198 my $cust_main = $svc_x->cust_main;
199 my $groupId = $self->groupId($cust_main);
201 my $new_deviceName = $self->deviceName($new_device);
202 my $old_deviceName = $self->deviceName($old_device);
204 if ($new_deviceName ne $old_deviceName) {
206 # do it in this order to switch the service endpoint over to the new
208 return $self->export_device_insert($svc_x, $new_device)
209 || $self->delete_Device($old_deviceName, $groupId);
211 } else { # update in place
213 my ($deviceName, $error) = $self->set_device_AccessDevice($new_device, $groupId);
214 return $error if $error;
219 sub export_device_delete {
220 my ($self, $svc_x, $device) = @_;
222 if ( $FS::svc_Common::noexport_hack ) {
223 carp 'export_device_delete() suppressed by noexport_hack'
224 if $self->option('debug');
228 if ( $device->isa('FS::phone_device') ) {
229 my $error = $self->set_endpoint( $self->userId($svc_x), '' );
230 return $error if $error;
233 return $self->delete_Device($self->deviceName($device));
237 =head2 CREATE-OR-UPDATE METHODS
239 These take a Freeside object that can be exported to the Broadworks system,
240 determine if it already has been exported, and if so, update it to match the
241 Freeside object. If it's not already there, they create it. They return a list
243 - that object's identifying string or hashref or whatever in Broadworks, and
244 - an error message, if creating the object failed.
248 =item set_cust_main_Group CUST_MAIN
250 Takes a L<FS::cust_main>, creates a Group for the customer, and returns a
251 GroupId. If the Group exists, it will be updated with the current customer
256 sub set_cust_main_Group {
258 my $cust_main = shift;
259 my $location = $cust_main->ship_location;
261 my $LSC = Locale::SubCountry->new($location->country)
262 or return(0, "Invalid country code ".$location->country);
264 if ( $LSC->has_sub_countries ) {
265 $state_name = $LSC->full_name( $location->state );
268 my $groupId = $self->groupId($cust_main);
272 defaultDomain => $self->option('domain'),
273 userLimit => $self->option('user_limit'),
274 groupName => $cust_main->name_short,
275 callingLineIdName => $cust_main->name_short,
277 contactName => $cust_main->contact_firstlast,
278 contactNumber => ( $cust_main->daytime
280 || $cust_main->mobile
283 contactEmail => ( ($cust_main->all_emails)[0] || undef ),
286 addressLine1 => $location->address1,
287 addressLine2 => ($location->address2 || undef),
288 city => $location->city,
289 stateOrProvince => $state_name,
290 zipOrPostalCode => $location->zip,
291 country => $location->country,
295 my ($success, $message) = $self->request('Group' => 'GroupGetRequest14sp7',
300 if ($success) { # update it with the curent params
302 ($success, $message) =
303 $self->request('Group' => 'GroupModifyRequest', %group_info);
305 } elsif ($message =~ /Group not found/) {
308 ($success, $message) =
309 $self->request('Group' => 'GroupAddRequest', %group_info);
312 # tell the group that its users in general are allowed to use
314 ($success, $message) = $self->request(
315 'Group' => 'GroupServiceModifyAuthorizationListRequest',
318 userServiceAuthorization => {
319 serviceName => 'Authentication',
320 authorizedQuantity => { unlimited => 'true' },
326 # tell the group that each new user, specifically, is allowed to
328 ($success, $message) = $self->request(
329 'Group' => 'GroupNewUserTemplateAssignUserServiceListRequest',
332 serviceName => 'Authentication',
336 } # else we somehow failed to fetch the group; throw an error
339 return ($groupId, '');
341 return ('', $message);
345 =item set_svc_phone_User SVC_PHONE, GROUPID
347 Creates a User object corresponding to this svc_phone, in the specified
348 group. If the User already exists, updates the record with the current
349 customer name (or phone name), phone number, and access device.
353 sub set_svc_phone_User {
354 my ($self, $svc_phone, $groupId) = @_;
358 # make sure the phone number is available
359 $error = $self->assign_number( $svc_phone->phonenum, $groupId );
361 my $userId = $self->userId($svc_phone);
362 my $cust_main = $svc_phone->cust_main;
365 if ($svc_phone->phone_name =~ /,/) {
366 ($last, $first) = split(/,\s*/, $svc_phone->phone_name);
367 } elsif ($svc_phone->phone_name =~ / /) {
368 ($first, $last) = split(/ +/, $svc_phone->phone_name, 2);
370 $first = $cust_main->first;
371 $last = $cust_main->last;
380 callingLineIdLastName => $last,
381 callingLineIdFirstName => $first,
382 password => $svc_phone->sip_password,
383 # not supported: nameDialingName; Hiragana names
384 phoneNumber => $svc_phone->phonenum,
385 callingLinePhoneNumber => $svc_phone->phonenum,
388 # does the user exist?
389 my ($success, $message) = $self->request(
390 'User' => 'UserGetRequest21',
394 if ( $success ) { # modify in place
396 ($success, $message) = $self->request(
397 'User' => 'UserModifyRequest17sp4',
401 } elsif ( $message =~ /User not found/ ) { # create new
403 ($success, $message) = $self->request(
404 'User' => 'UserAddRequest17sp4',
411 return ($userId, '');
413 return ('', $message);
417 =item set_device_AccessDevice DEVICE, [ GROUPID ]
419 Creates/updates an Access Device Profile. This is a record for a
420 I<specific physical device> that can send/receive calls. (Not to be confused
421 with an "Access Device Endpoint", which is a I<port> on such a device.) DEVICE
422 can be any record with a foreign key to L<FS::part_device>.
424 If GROUPID is specified, this device profile will be created at the Group
425 level in that group; otherwise it will be a ServiceProvider level record.
429 sub set_device_AccessDevice {
434 my $deviceName = $self->deviceName($device);
437 if ($device->svcnum) {
438 $svc_x = FS::cust_svc->by_key($device->svcnum)->svc_x;
440 $svc_x = FS::svc_phone->new({}); # returns empty for all fields
443 my $part_device = $device->part_device
444 or return ('', "devicepart ".$device->part_device." not defined" );
449 deviceName => $deviceName,
450 deviceType => $part_device->title,
451 description => ($svc_x->title # svc_pbx
452 || $part_device->devicename), # others
456 $new_device{netAddress} = $svc_x->ip_addr if $svc_x->ip_addr; # svc_pbx only
457 $new_device{macAddress} = $device->mac_addr if $device->mac_addr;
461 deviceName => $deviceName
463 my $level = 'ServiceProvider';
467 $find_device{groupId} = $new_device{groupId} = $groupId;
469 # shouldn't be used in our current design
470 warn "$me creating access device $deviceName at Service Provider level\n";
473 my ($success, $message) = $self->request(
474 $level, $level.'AccessDeviceGetRequest18sp1',
478 if ( $success ) { # modify in place
480 ($success, $message) = $self->request(
481 $level => $level.'AccessDeviceModifyRequest14',
485 } elsif ( $message =~ /Access Device not found/ ) { # create new
487 ($success, $message) = $self->request(
488 $level => $level.'AccessDeviceAddRequest14',
495 return ($deviceName, '');
497 return ('', $message);
503 =head2 PROVISIONING METHODS
505 These return an error string on failure, and an empty string on success.
509 =item assign_number NUMBER, GROUPID
511 Assigns a phone number to a group. If it's assigned to a different group or
512 doesn't belong to the service provider, this will fail. If it's already
513 assigned to I<this> group, it will do nothing and return success.
518 my ($self, $number, $groupId) = @_;
519 # see if it's already assigned
520 my ($success, $message) = $self->request(
521 Group => 'GroupDnGetAssignmentListRequest18',
524 searchCriteriaDn => {
527 isCaseInsensitive => 'false',
530 return "$message (checking phone number status)" if !$success;
531 my $result = $self->oci_table( $message->{dnTable} );
532 return '' if @$result > 0;
534 ($success, $message) = $self->request(
535 Group => 'GroupDnAssignListRequest',
538 phoneNumber => $number,
541 $success ? '' : $message;
544 =item release_number NUMBER, GROUPID
546 Unassigns a phone number from a group. If it's assigned to a user in the
547 group then this will fail. If it's not assigned to the group at all, this
553 my ($self, $number, $groupId) = @_;
554 # see if it's already assigned
555 my ($success, $message) = $self->request(
556 Group => 'GroupDnGetAssignmentListRequest18',
559 searchCriteriaDn => {
562 isCaseInsensitive => 'false',
565 return "$message (checking phone number status)" if !$success;
566 my $result = $self->oci_table( $message->{dnTable} );
567 return '' if @$result == 0;
569 ($success, $message) = $self->request(
570 Group => 'GroupDnUnassignListRequest',
573 phoneNumber => $number,
576 $success ? '' : $message;
579 =item set_endpoint USERID [, DEVICENAME ]
581 Sets the endpoint for communicating with USERID to DEVICENAME. For now, this
582 assumes that all devices are defined at Group level.
584 If DEVICENAME is null, the user will be set to have no endpoint.
588 # we only support linePort = userId, and no numbered ports
591 my ($self, $userId, $deviceName) = @_;
594 if ( length($deviceName) > 0 ) {
596 accessDeviceEndpoint => {
599 deviceLevel => 'Group',
600 deviceName => $deviceName,
607 my ($success, $message) = $self->request(
608 User => 'UserModifyRequest17sp4',
610 endpoint => $endpoint,
613 $success ? '' : $message;
616 =item set_sip_authentication USERID, NAME, PASSWORD
618 Sets the SIP authentication credentials for USERID to (NAME, PASSWORD).
622 sub set_sip_authentication {
623 my ($self, $userId, $userName, $password) = @_;
625 my ($success, $message) = $self->request(
626 'Services/ServiceAuthentication' => 'UserAuthenticationModifyRequest',
628 userName => $userName,
629 newPassword => $password,
632 $success ? '' : $message;
635 =item delete_group GROUPID
637 Deletes the group GROUPID.
642 my ($self, $groupId) = @_;
644 my ($success, $message) = $self->request(
645 Group => 'GroupDeleteRequest',
649 if ( $success or $message =~ /Group not found/ ) {
656 =item delete_User USERID
658 Deletes the user USERID, and releases its phone number if it has one.
663 my ($self, $userId) = @_;
665 my ($success, $message) = $self->request(
666 User => 'UserDeleteRequest',
669 if ($success or $message =~ /User not found/) {
676 =item delete_Device DEVICENAME[, GROUPID ]
678 Deletes the access device DEVICENAME (from group GROUPID, or from the service
679 provider if there is no GROUPID).
684 my ($self, $deviceName, $groupId) = @_;
686 my ($success, $message);
688 ($success, $message) = $self->request(
689 Group => 'GroupAccessDeviceDeleteRequest',
692 deviceName => $deviceName,
695 ($success, $message) = $self->request(
696 ServiceProvider => 'ServiceProviderAccessDeviceDeleteRequest',
698 deviceName => $deviceName,
701 if ( $success or $message =~ /Access Device not found/ ) {
710 =head2 CONVENIENCE METHODS
716 Returns 'serviceProviderId' => the service_provider option. This is commonly
717 needed in request parameters.
719 =item groupId CUST_MAIN
721 Returns the groupID that goes with the specified customer.
725 Returns the userId (including domain) that should go with the specified
728 =item deviceName DEVICE
730 Returns the access device name that should go with the specified phone_device
737 my $id = $self->option('service_provider') or die 'service provider not set';
738 'serviceProviderId' => $id
743 my $cust_main = shift;
744 'cust_main#'.$cust_main->custnum;
751 if ($svc->phonenum) {
752 $userId = $svc->phonenum;
753 } else { # pbx_extension needs one of these
754 die "can't determine userId for non-svc_phone service";
756 my $domain = $self->option('domain'); # domsvc?
757 $userId .= '@' . $domain if $domain;
765 $device->mac_addr || ($device->table . '#' . $device->devicenum);
768 =item oci_table HASHREF
770 Converts the base OCITable type into an arrayref of hashrefs.
776 my $oci_table = shift;
777 my @colnames = $oci_table->{colHeading};
779 foreach my $row (@{ $oci_table->{row} }) {
781 @hash{@colnames} = @{ $row->{col} };
798 =item import_cdrs START, END
800 Retrieves CDRs for calls in the date range from START to END and inserts them
801 as a new CDR batch. On success, returns a new cdr_batch object. On failure,
802 returns an error message. If there are no new CDRs, returns nothing.
810 =item request SCOPE, COMMAND, [ ARGUMENTS... ]
812 Wrapper for L<BroadWorks::OCI/request>. The client object will be cached.
813 Returns two values: a flag, true or false, indicating success of the request,
814 and the decoded response message as a hashref.
816 On failure of the request (or failure to authenticate), the response message
817 will be a simple scalar containing the error message.
824 delete $client{$self->exportnum} if $expire{$self->exportnum} < time;
825 my $client = $client{$self->exportnum};
828 eval "use BroadWorks::OCI";
831 Log::Report::dispatcher('PERL', 'default',
832 mode => ($self->option('debug') ? 'DEBUG' : 'NORMAL')
835 $client = BroadWorks::OCI->new(
836 userId => $self->option('admin_user'),
837 password => $self->option('admin_pass'),
839 my ($success, $message) = $client->login;
840 return ('', $message) if !$success;
842 $client{$self->exportnum} = $client; # if login succeeded
843 $expire{$self->exportnum} = time + 120; # hardcoded, yeah
845 return $client->request(@_);