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>
44 <P>Each phone service must have a device linked before it will be functional.
45 Until then, authentication will be denied.</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;
90 if ( my $device = qsearchs('phone_device', { svcnum => $svc_new->svcnum }) ) {
91 # there's a Line/Port configured for the device, and it also needs to be renamed.
92 $error ||= $self->set_endpoint( $newUserId, $self->deviceName($device) );
96 if ( $svc_old->phonenum ne $svc_new->phonenum ) {
97 $error ||= $self->release_number($svc_old->phonenum, $groupId);
101 ($userId, $error) = $self->set_svc_phone_User($svc_new, $groupId);
102 $error ||= $self->set_sip_authentication($userId, $userId, $svc_new->sip_password);
104 if ($error and $oldUserId ne $newUserId) {
105 # rename it back, then
106 my ($success, $message) = $self->request(
107 User => 'UserModifyUserIdRequest',
108 userId => $newUserId,
109 newUserId => $oldUserId
111 # if it fails, we can't really fix it
112 return "$error; unable to reverse user ID change: $message" if !$success;
115 return $error if $error;
117 } elsif ( $svc_new->isa('FS::svc_pbx') ) {
125 my ($self, $svc_x) = @_;
127 my $cust_main = $svc_x->cust_main;
128 my $groupId = $self->groupId($cust_main);
130 if ( $svc_x->isa('FS::svc_phone') ) {
131 my $userId = $self->userId($svc_x);
132 my $error = $self->delete_User($userId)
133 || $self->release_number($svc_x->phonenum, $groupId);
134 return $error if $error;
135 } elsif ( $svc_x->isa('FS::svc_pbx') ) {
139 # find whether the customer still has any services on this platform
140 # (other than the one being deleted)
141 my %svcparts = map { $_->svcpart => 1 } $self->export_svc;
142 my $svcparts = join(',', keys %svcparts);
143 my $num_svcs = FS::cust_svc->count(
144 '(select custnum from cust_pkg where cust_pkg.pkgnum = cust_svc.pkgnum) '.
147 " AND svcpart IN ($svcparts)",
152 if ( $num_svcs == 0 ) {
153 warn "$me removed last service for group $groupId; deleting group.\n";
154 my $error = $self->delete_Group($groupId);
155 warn "$me error deleting group: $error\n" if $error;
156 return "$error (removing customer group)" if $error;
162 sub export_device_insert {
163 my ($self, $svc_x, $device) = @_;
165 if ( $device->count('svcnum = ?', $svc_x->svcnum) > 1 ) {
166 return "This service already has a device.";
169 my $cust_main = $svc_x->cust_main;
170 my $groupId = $self->groupId($cust_main);
172 my ($deviceName, $error) = $self->set_device_AccessDevice($device, $groupId);
173 return $error if $error;
175 if ( $device->isa('FS::phone_device') ) {
176 return $self->set_endpoint( $self->userId($svc_x), $deviceName);
177 } # else pbx_device, extension_device
182 sub export_device_replace {
183 my ($self, $svc_x, $new_device, $old_device) = @_;
184 my $cust_main = $svc_x->cust_main;
185 my $groupId = $self->groupId($cust_main);
187 my $new_deviceName = $self->deviceName($new_device);
188 my $old_deviceName = $self->deviceName($old_device);
190 if ($new_deviceName ne $old_deviceName) {
192 # do it in this order to switch the service endpoint over to the new
194 return $self->export_device_insert($svc_x, $new_device)
195 || $self->delete_Device($old_deviceName, $groupId);
197 } else { # update in place
199 my ($deviceName, $error) = $self->set_device_AccessDevice($new_device, $groupId);
200 return $error if $error;
205 sub export_device_delete {
206 my ($self, $svc_x, $device) = @_;
208 if ( $device->isa('FS::phone_device') ) {
209 my $error = $self->set_endpoint( $self->userId($svc_x), '' );
210 return $error if $error;
213 return $self->delete_Device($self->deviceName($device));
217 =head2 CREATE-OR-UPDATE METHODS
219 These take a Freeside object that can be exported to the Broadworks system,
220 determine if it already has been exported, and if so, update it to match the
221 Freeside object. If it's not already there, they create it. They return a list
223 - that object's identifying string or hashref or whatever in Broadworks, and
224 - an error message, if creating the object failed.
228 =item set_cust_main_Group CUST_MAIN
230 Takes a L<FS::cust_main>, creates a Group for the customer, and returns a
231 GroupId. If the Group exists, it will be updated with the current customer
236 sub set_cust_main_Group {
238 my $cust_main = shift;
239 my $location = $cust_main->ship_location;
241 my $LSC = Locale::SubCountry->new($location->country)
242 or return(0, "Invalid country code ".$location->country);
244 if ( $LSC->has_sub_countries ) {
245 $state_name = $LSC->full_name( $location->state );
248 my $groupId = $self->groupId($cust_main);
252 defaultDomain => $self->option('domain'),
253 userLimit => $self->option('user_limit'),
254 groupName => $cust_main->name_short,
255 callingLineIdName => $cust_main->name_short,
257 contactName => $cust_main->contact_firstlast,
258 contactNumber => ( $cust_main->daytime
260 || $cust_main->mobile
263 contactEmail => ( ($cust_main->all_emails)[0] || undef ),
266 addressLine1 => $location->address1,
267 addressLine2 => ($location->address2 || undef),
268 city => $location->city,
269 stateOrProvince => $state_name,
270 zipOrPostalCode => $location->zip,
271 country => $location->country,
275 my ($success, $message) = $self->request('Group' => 'GroupGetRequest14sp7',
280 if ($success) { # update it with the curent params
282 ($success, $message) =
283 $self->request('Group' => 'GroupModifyRequest', %group_info);
285 } elsif ($message =~ /Group not found/) {
288 ($success, $message) =
289 $self->request('Group' => 'GroupAddRequest', %group_info);
292 # tell the group that its users in general are allowed to use
294 ($success, $message) = $self->request(
295 'Group' => 'GroupServiceModifyAuthorizationListRequest',
298 userServiceAuthorization => {
299 serviceName => 'Authentication',
300 authorizedQuantity => { unlimited => 'true' },
306 # tell the group that each new user, specifically, is allowed to
308 ($success, $message) = $self->request(
309 'Group' => 'GroupNewUserTemplateAssignUserServiceListRequest',
312 serviceName => 'Authentication',
316 } # else we somehow failed to fetch the group; throw an error
319 return ($groupId, '');
321 return ('', $message);
325 =item set_svc_phone_User SVC_PHONE, GROUPID
327 Creates a User object corresponding to this svc_phone, in the specified
328 group. If the User already exists, updates the record with the current
329 customer name (or phone name), phone number, and access device.
333 sub set_svc_phone_User {
334 my ($self, $svc_phone, $groupId) = @_;
338 # make sure the phone number is available
339 $error = $self->assign_number( $svc_phone->phonenum, $groupId );
341 my $userId = $self->userId($svc_phone);
342 my $cust_main = $svc_phone->cust_main;
345 if ($svc_phone->phone_name =~ /,/) {
346 ($last, $first) = split(/,\s*/, $svc_phone->phone_name);
347 } elsif ($svc_phone->phone_name =~ / /) {
348 ($first, $last) = split(/ +/, $svc_phone->phone_name, 2);
350 $first = $cust_main->first;
351 $last = $cust_main->last;
360 callingLineIdLastName => $last,
361 callingLineIdFirstName => $first,
362 password => $svc_phone->sip_password,
363 # not supported: nameDialingName; Hiragana names
364 phoneNumber => $svc_phone->phonenum,
365 callingLinePhoneNumber => $svc_phone->phonenum,
368 # does the user exist?
369 my ($success, $message) = $self->request(
370 'User' => 'UserGetRequest21',
374 if ( $success ) { # modify in place
376 ($success, $message) = $self->request(
377 'User' => 'UserModifyRequest17sp4',
381 } elsif ( $message =~ /User not found/ ) { # create new
383 ($success, $message) = $self->request(
384 'User' => 'UserAddRequest17sp4',
391 return ($userId, '');
393 return ('', $message);
397 =item set_device_AccessDevice DEVICE, [ GROUPID ]
399 Creates/updates an Access Device Profile. This is a record for a
400 I<specific physical device> that can send/receive calls. (Not to be confused
401 with an "Access Device Endpoint", which is a I<port> on such a device.) DEVICE
402 can be any record with a foreign key to L<FS::part_device>.
404 If GROUPID is specified, this device profile will be created at the Group
405 level in that group; otherwise it will be a ServiceProvider level record.
409 sub set_device_AccessDevice {
414 my $deviceName = $self->deviceName($device);
417 if ($device->svcnum) {
418 $svc_x = FS::cust_svc->by_key($device->svcnum)->svc_x;
420 $svc_x = FS::svc_phone->new({}); # returns empty for all fields
423 my $part_device = $device->part_device
424 or return ('', "devicepart ".$device->part_device." not defined" );
429 deviceName => $deviceName,
430 deviceType => $part_device->title,
431 description => ($svc_x->title # svc_pbx
432 || $part_device->devicename), # others
436 $new_device{netAddress} = $svc_x->ip_addr if $svc_x->ip_addr; # svc_pbx only
437 $new_device{macAddress} = $device->mac_addr if $device->mac_addr;
441 deviceName => $deviceName
443 my $level = 'ServiceProvider';
447 $find_device{groupId} = $new_device{groupId} = $groupId;
449 # shouldn't be used in our current design
450 warn "$me creating access device $deviceName at Service Provider level\n";
453 my ($success, $message) = $self->request(
454 $level, $level.'AccessDeviceGetRequest18sp1',
458 if ( $success ) { # modify in place
460 ($success, $message) = $self->request(
461 $level => $level.'AccessDeviceModifyRequest14',
465 } elsif ( $message =~ /Access Device not found/ ) { # create new
467 ($success, $message) = $self->request(
468 $level => $level.'AccessDeviceAddRequest14',
475 return ($deviceName, '');
477 return ('', $message);
483 =head2 PROVISIONING METHODS
485 These return an error string on failure, and an empty string on success.
489 =item assign_number NUMBER, GROUPID
491 Assigns a phone number to a group. If it's assigned to a different group or
492 doesn't belong to the service provider, this will fail. If it's already
493 assigned to I<this> group, it will do nothing and return success.
498 my ($self, $number, $groupId) = @_;
499 # see if it's already assigned
500 my ($success, $message) = $self->request(
501 Group => 'GroupDnGetAssignmentListRequest18',
504 searchCriteriaDn => {
507 isCaseInsensitive => 'false',
510 return "$message (checking phone number status)" if !$success;
511 my $result = $self->oci_table( $message->{dnTable} );
512 return '' if @$result > 0;
514 ($success, $message) = $self->request(
515 Group => 'GroupDnAssignListRequest',
518 phoneNumber => $number,
521 $success ? '' : $message;
524 =item release_number NUMBER, GROUPID
526 Unassigns a phone number from a group. If it's assigned to a user in the
527 group then this will fail. If it's not assigned to the group at all, this
533 my ($self, $number, $groupId) = @_;
534 # see if it's already assigned
535 my ($success, $message) = $self->request(
536 Group => 'GroupDnGetAssignmentListRequest18',
539 searchCriteriaDn => {
542 isCaseInsensitive => 'false',
545 return "$message (checking phone number status)" if !$success;
546 my $result = $self->oci_table( $message->{dnTable} );
547 return '' if @$result == 0;
549 ($success, $message) = $self->request(
550 Group => 'GroupDnUnassignListRequest',
553 phoneNumber => $number,
556 $success ? '' : $message;
559 =item set_endpoint USERID [, DEVICENAME ]
561 Sets the endpoint for communicating with USERID to DEVICENAME. For now, this
562 assumes that all devices are defined at Group level.
564 If DEVICENAME is null, the user will be set to have no endpoint.
568 # we only support linePort = userId, and no numbered ports
571 my ($self, $userId, $deviceName) = @_;
574 if ( length($deviceName) > 0 ) {
576 accessDeviceEndpoint => {
579 deviceLevel => 'Group',
580 deviceName => $deviceName,
587 my ($success, $message) = $self->request(
588 User => 'UserModifyRequest17sp4',
590 endpoint => $endpoint,
593 $success ? '' : $message;
596 =item set_sip_authentication USERID, NAME, PASSWORD
598 Sets the SIP authentication credentials for USERID to (NAME, PASSWORD).
602 sub set_sip_authentication {
603 my ($self, $userId, $userName, $password) = @_;
605 my ($success, $message) = $self->request(
606 'Services/ServiceAuthentication' => 'UserAuthenticationModifyRequest',
608 userName => $userName,
609 newPassword => $password,
612 $success ? '' : $message;
615 =item delete_group GROUPID
617 Deletes the group GROUPID.
622 my ($self, $groupId) = @_;
624 my ($success, $message) = $self->request(
625 Group => 'GroupDeleteRequest',
629 if ( $success or $message =~ /Group not found/ ) {
636 =item delete_User USERID
638 Deletes the user USERID, and releases its phone number if it has one.
643 my ($self, $userId) = @_;
645 my ($success, $message) = $self->request(
646 User => 'UserDeleteRequest',
649 if ($success or $message =~ /User not found/) {
656 =item delete_Device DEVICENAME[, GROUPID ]
658 Deletes the access device DEVICENAME (from group GROUPID, or from the service
659 provider if there is no GROUPID).
664 my ($self, $deviceName, $groupId) = @_;
666 my ($success, $message);
668 ($success, $message) = $self->request(
669 Group => 'GroupAccessDeviceDeleteRequest',
672 deviceName => $deviceName,
675 ($success, $message) = $self->request(
676 ServiceProvider => 'ServiceProviderAccessDeviceDeleteRequest',
678 deviceName => $deviceName,
681 if ( $success or $message =~ /Access Device not found/ ) {
690 =head2 CONVENIENCE METHODS
696 Returns 'serviceProviderId' => the service_provider option. This is commonly
697 needed in request parameters.
699 =item groupId CUST_MAIN
701 Returns the groupID that goes with the specified customer.
705 Returns the userId (including domain) that should go with the specified
708 =item deviceName DEVICE
710 Returns the access device name that should go with the specified phone_device
717 my $id = $self->option('service_provider') or die 'service provider not set';
718 'serviceProviderId' => $id
723 my $cust_main = shift;
724 'cust_main#'.$cust_main->custnum;
731 if ($svc->phonenum) {
732 $userId = $svc->phonenum;
733 } else { # pbx_extension needs one of these
734 die "can't determine userId for non-svc_phone service";
736 my $domain = $self->option('domain'); # domsvc?
737 $userId .= '@' . $domain if $domain;
745 $device->mac_addr || ($device->table . '#' . $device->devicenum);
748 =item oci_table HASHREF
750 Converts the base OCITable type into an arrayref of hashrefs.
756 my $oci_table = shift;
757 my @colnames = $oci_table->{colHeading};
759 foreach my $row (@{ $oci_table->{row} }) {
761 @hash{@colnames} = @{ $row->{col} };
778 =item import_cdrs START, END
780 Retrieves CDRs for calls in the date range from START to END and inserts them
781 as a new CDR batch. On success, returns a new cdr_batch object. On failure,
782 returns an error message. If there are no new CDRs, returns nothing.
790 =item request SCOPE, COMMAND, [ ARGUMENTS... ]
792 Wrapper for L<BroadWorks::OCI/request>. The client object will be cached.
793 Returns two values: a flag, true or false, indicating success of the request,
794 and the decoded response message as a hashref.
796 On failure of the request (or failure to authenticate), the response message
797 will be a simple scalar containing the error message.
804 delete $client{$self->exportnum} if $expire{$self->exportnum} < time;
805 my $client = $client{$self->exportnum};
808 eval "use BroadWorks::OCI";
811 Log::Report::dispatcher('PERL', 'default',
812 mode => ($self->option('debug') ? 'DEBUG' : 'NORMAL')
815 $client = BroadWorks::OCI->new(
816 userId => $self->option('admin_user'),
817 password => $self->option('admin_pass'),
819 my ($success, $message) = $client->login;
820 return ('', $message) if !$success;
822 $client{$self->exportnum} = $client; # if login succeeded
823 $expire{$self->exportnum} = time + 120; # hardcoded, yeah
825 return $client->request(@_);