BroadWorks export, phase 1, #25927
[freeside.git] / FS / FS / part_export / broadworks.pm
1 package FS::part_export::broadworks;
2
3 use base qw( FS::part_export );
4 use strict;
5
6 use Tie::IxHash;
7 use FS::Record qw(dbh qsearch qsearchs);
8 use Locale::SubCountry;
9 use BroadWorks::OCI;
10
11 our $me = '[broadworks]';
12 our %client; # exportnum => client object
13 our %expire; # exportnum => timestamp on which to refresh the client
14
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',
21                          default  => 100 },
22   'debug'           => { label => 'Enable debugging',
23                          type  => 'checkbox',
24                        },
25 ;
26
27 # do we need roles for this?
28 # no. cust_main -> group, svc_phone -> pilot/single user, 
29 # phone_device -> access device
30 #
31 # phase 2: svc_pbx -> trunk group, pbx_extension -> trunk user
32
33 our %info = (
34   'svc'      => [qw( svc_phone svc_pbx )], # part_device?
35   'desc'     =>
36     'Provision phone and PBX services to a Broadworks Application Server',
37   'options'  => \%options,
38   'notes'    => <<'END'
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>
46 END
47 );
48
49 sub export_insert {
50   my($self, $svc_x) = (shift, shift);
51
52   my $cust_main = $svc_x->cust_main;
53   my ($groupId, $error) = $self->set_cust_main_Group($cust_main);
54   return $error if $error;
55
56   if ( $svc_x->isa('FS::svc_phone') ) {
57     my $userId;
58     ($userId, $error) = $self->set_svc_phone_User($svc_x, $groupId);
59
60     $error ||= $self->set_sip_authentication($userId, $userId, $svc_x->sip_password);
61
62     return $error if $error;
63
64   } elsif ( $svc_x->isa('FS::svc_pbx') ) {
65     # noop
66   }
67
68   '';
69 }
70
71 sub export_replace {
72   my($self, $svc_new, $svc_old) = @_;
73
74   my $cust_main = $svc_new->cust_main;
75   my ($groupId, $error) = $self->set_cust_main_Group($cust_main);
76   return $error if $error;
77
78   if ( $svc_new->isa('FS::svc_phone') ) {
79     my $oldUserId = $self->userId($svc_old);
80     my $newUserId = $self->userId($svc_new);
81
82     if ( $oldUserId ne $newUserId ) {
83       my ($success, $message) = $self->request(
84         User => 'UserModifyUserIdRequest',
85         userId    => $oldUserId,
86         newUserId => $newUserId
87       );
88       return $message if !$success;
89     }
90
91     if ( $svc_old->phonenum ne $svc_new->phonenum ) {
92       $error ||= $self->release_number($svc_old->phonenum, $groupId);
93     }
94
95     my $userId;
96     ($userId, $error) = $self->set_svc_phone_User($svc_new, $groupId);
97     $error ||= $self->set_sip_authentication($userId, $userId, $svc_new->sip_password);
98
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
105       );
106       # if it fails, we can't really fix it
107       return "$error; unable to reverse user ID change: $message" if !$success;
108     }
109
110     return $error if $error;
111
112   } elsif ( $svc_new->isa('FS::svc_pbx') ) {
113     # noop
114   }
115
116   '';
117 }
118
119 sub export_delete {
120   my ($self, $svc_x) = @_;
121
122   my $cust_main = $svc_x->cust_main;
123   my $groupId = $self->groupId($cust_main);
124
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') ) {
131     # noop
132   }
133
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) '.
140     ' = ? '.
141     ' AND svcnum != ?'.
142     " AND svcpart IN ($svcparts)",
143     $cust_main->custnum,
144     $svc_x->svcnum
145   );
146
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;
152   }
153
154   '';
155 }
156
157 sub export_device_insert {
158   my ($self, $svc_x, $device) = @_;
159
160   if ( $device->count('svcnum = ?', $svc_x->svcnum) > 1 ) {
161     return "This service already has a device.";
162   }
163
164   my $cust_main = $svc_x->cust_main;
165   my $groupId = $self->groupId($cust_main);
166
167   my ($deviceName, $error) = $self->set_device_AccessDevice($device, $groupId);
168   return $error if $error;
169
170   if ( $device->isa('FS::phone_device') ) {
171     return $self->set_endpoint( $self->userId($svc_x), $deviceName);
172   } # else pbx_device, extension_device
173
174   '';
175 }
176
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);
181
182   my $new_deviceName = $self->deviceName($new_device);
183   my $old_deviceName = $self->deviceName($old_device);
184
185   if ($new_deviceName ne $old_deviceName) {
186
187     # do it in this order to switch the service endpoint over to the new 
188     # device.
189     return $self->export_device_insert($svc_x, $new_device)
190         || $self->delete_Device($old_deviceName, $groupId);
191
192   } else { # update in place
193
194     my ($deviceName, $error) = $self->set_device_AccessDevice($new_device, $groupId);
195     return $error if $error;
196
197   }
198 }
199
200 sub export_device_delete {
201   my ($self, $svc_x, $device) = @_;
202
203   if ( $device->isa('FS::phone_device') ) {
204     my $error = $self->set_endpoint( $self->userId($svc_x), '' );
205     return $error if $error;
206   } # else...
207
208   return $self->delete_Device($self->deviceName($device));
209 }
210
211
212 =head2 CREATE-OR-UPDATE METHODS
213
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
217 of two objects:
218 - that object's identifying string or hashref or whatever in Broadworks, and
219 - an error message, if creating the object failed.
220
221 =over 4
222
223 =item set_cust_main_Group CUST_MAIN
224
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
227 and export settings.
228
229 =cut
230
231 sub set_cust_main_Group {
232   my $self = shift;
233   my $cust_main = shift;
234   my $location = $cust_main->ship_location;
235
236   my $LSC = Locale::SubCountry->new($location->country)
237     or return(0, "Invalid country code ".$location->country);
238   my $state_name;
239   if ( $LSC->has_sub_countries ) {
240     $state_name = $LSC->full_name( $location->state );
241   }
242
243   my $groupId = $self->groupId($cust_main);
244   my %group_info = (
245     $self->SPID,
246     groupId           => $groupId,
247     defaultDomain     => $self->option('domain'),
248     userLimit         => $self->option('user_limit'),
249     groupName         => $cust_main->name_short,
250     callingLineIdName => $cust_main->name_short,
251     contact => {
252       contactName     => $cust_main->contact_firstlast,
253       contactNumber   => (   $cust_main->daytime
254                           || $cust_main->night
255                           || $cust_main->mobile
256                           || undef
257                          ),
258       contactEmail    => ( ($cust_main->all_emails)[0] || undef ),
259     },
260     address => {
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,
267     },
268   );
269
270   my ($success, $message) = $self->request('Group' => 'GroupGetRequest14sp7',
271     $self->SPID,
272     groupId => $groupId
273   );
274
275   if ($success) { # update it with the curent params
276
277     ($success, $message) =
278       $self->request('Group' => 'GroupModifyRequest', %group_info);
279
280   } elsif ($message =~ /Group not found/) {
281
282     # create a new group
283     ($success, $message) =
284       $self->request('Group' => 'GroupAddRequest', %group_info);
285
286     if ($success) {
287       # tell the group that its users in general are allowed to use
288       # Authentication
289       ($success, $message) = $self->request(
290         'Group' => 'GroupServiceModifyAuthorizationListRequest',
291         $self->SPID,
292         groupId => $groupId,
293         userServiceAuthorization => {
294           serviceName => 'Authentication',
295           authorizedQuantity => { unlimited => 'true' },
296         },
297       );
298     }
299
300     if ($success) {
301       # tell the group that each new user, specifically, is allowed to 
302       # use Authentication
303       ($success, $message) = $self->request(
304         'Group' => 'GroupNewUserTemplateAssignUserServiceListRequest',
305         $self->SPID,
306         groupId => $groupId,
307         serviceName => 'Authentication',
308       );
309     }
310
311   } # else we somehow failed to fetch the group; throw an error
312
313   if ($success) {
314     return ($groupId, '');
315   } else {
316     return ('', $message);
317   }
318 }
319
320 =item set_svc_phone_User SVC_PHONE, GROUPID
321
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.
325
326 =cut
327
328 sub set_svc_phone_User {
329   my ($self, $svc_phone, $groupId) = @_;
330
331   my $error;
332
333   # make sure the phone number is available
334   $error = $self->assign_number( $svc_phone->phonenum, $groupId );
335
336   my $userId = $self->userId($svc_phone);
337   my $cust_main = $svc_phone->cust_main;
338
339   my ($first, $last);
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);
344   } else {
345     $first = $cust_main->first;
346     $last = $cust_main->last;
347   }
348
349   my %new_user = (
350     $self->SPID,
351     groupId                 => $groupId,
352     userId                  => $userId,
353     lastName                => $last,
354     firstName               => $first,
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,
361   );
362
363   # does the user exist?
364   my ($success, $message) = $self->request(
365     'User' => 'UserGetRequest21',
366     userId => $userId
367   );
368
369   if ( $success ) { # modify in place
370
371     ($success, $message) = $self->request(
372       'User' => 'UserModifyRequest17sp4',
373       %new_user
374     );
375
376   } elsif ( $message =~ /User not found/ ) { # create new
377
378     ($success, $message) = $self->request(
379       'User' => 'UserAddRequest17sp4',
380       %new_user
381     );
382
383   }
384
385   if ($success) {
386     return ($userId, '');
387   } else {
388     return ('', $message);
389   }
390 }
391
392 =item set_device_AccessDevice DEVICE, [ GROUPID ]
393
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>.
398
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.
401
402 =cut
403
404 sub set_device_AccessDevice {
405   my $self = shift;
406   my $device = shift;
407   my $groupId = shift;
408
409   my $deviceName = $self->deviceName($device);
410
411   my $svc_x;
412   if ($device->svcnum) {
413     $svc_x = FS::cust_svc->by_key($device->svcnum)->svc_x;
414   } else {
415     $svc_x = FS::svc_phone->new({}); # returns empty for all fields
416   }
417
418   my $part_device = $device->part_device
419     or return ('', "devicepart ".$device->part_device." not defined" );
420
421   # required fields
422   my %new_device = (
423     $self->SPID,
424     deviceName        => $deviceName,
425     deviceType        => $part_device->title,
426     description       => ($svc_x->title # svc_pbx
427                           || $part_device->devicename), # others
428   );
429
430   # optional fields
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;
433
434   my %find_device = (
435     $self->SPID,
436     deviceName => $deviceName
437   );
438   my $level = 'ServiceProvider';
439
440   if ( $groupId ) {
441     $level = 'Group';
442     $find_device{groupId} = $new_device{groupId} = $groupId;
443   } else {
444     # shouldn't be used in our current design
445     warn "$me creating access device $deviceName at Service Provider level\n";
446   }
447
448   my ($success, $message) = $self->request(
449     $level, $level.'AccessDeviceGetRequest18sp1',
450     %find_device
451   );
452
453   if ( $success ) { # modify in place
454
455     ($success, $message) = $self->request(
456       $level => $level.'AccessDeviceModifyRequest14',
457       %new_device
458     );
459
460   } elsif ( $message =~ /Access Device not found/ ) { # create new
461
462     ($success, $message) = $self->request(
463       $level => $level.'AccessDeviceAddRequest14',
464       %new_device
465     );
466
467   }
468
469   if ($success) {
470     return ($deviceName, '');
471   } else {
472     return ('', $message);
473   }
474 }
475
476 =back
477
478 =head2 PROVISIONING METHODS
479
480 These return an error string on failure, and an empty string on success.
481
482 =over 4
483
484 =item assign_number NUMBER, GROUPID
485
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.
489
490 =cut
491
492 sub assign_number {
493   my ($self, $number, $groupId) = @_;
494   # see if it's already assigned
495   my ($success, $message) = $self->request(
496     Group => 'GroupDnGetAssignmentListRequest18',
497     $self->SPID,
498     groupId           => $groupId,
499     searchCriteriaDn  => {
500       mode  => 'Equal To',
501       value => $number,
502       isCaseInsensitive => 'false',
503     },
504   );
505   return "$message (checking phone number status)" if !$success;
506   my $result = $self->oci_table( $message->{dnTable} );
507   return '' if @$result > 0;
508
509   ($success, $message) = $self->request(
510     Group => 'GroupDnAssignListRequest',
511     $self->SPID,
512     groupId     => $groupId,
513     phoneNumber => $number,
514   );
515
516   $success ? '' : $message;
517 }
518
519 =item release_number NUMBER, GROUPID
520
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
523 does nothing.
524
525 =cut
526
527 sub release_number {
528   my ($self, $number, $groupId) = @_;
529   # see if it's already assigned
530   my ($success, $message) = $self->request(
531     Group => 'GroupDnGetAssignmentListRequest18',
532     $self->SPID,
533     groupId           => $groupId,
534     searchCriteriaDn  => {
535       mode  => 'Equal To',
536       value => $number,
537       isCaseInsensitive => 'false',
538     },
539   );
540   return "$message (checking phone number status)" if !$success;
541   my $result = $self->oci_table( $message->{dnTable} );
542   return '' if @$result == 0;
543
544   ($success, $message) = $self->request(
545     Group => 'GroupDnUnassignListRequest',
546     $self->SPID,
547     groupId     => $groupId,
548     phoneNumber => $number,
549   );
550
551   $success ? '' : $message;
552 }
553
554 =item set_endpoint USERID [, DEVICENAME ]
555
556 Sets the endpoint for communicating with USERID to DEVICENAME. For now, this
557 assumes that all devices are defined at Group level.
558
559 If DEVICENAME is null, the user will be set to have no endpoint.
560
561 =cut
562       
563 # we only support linePort = userId, and no numbered ports
564
565 sub set_endpoint {
566   my ($self, $userId, $deviceName) = @_;
567
568   my $endpoint;
569   if ( length($deviceName) > 0 ) {
570     $endpoint = {
571       accessDeviceEndpoint => {
572         linePort      => $userId,
573         accessDevice  => {
574           deviceLevel => 'Group',
575           deviceName  => $deviceName,
576         },
577       }
578     };
579   } else {
580     $endpoint = undef;
581   }
582   my ($success, $message) = $self->request(
583     User => 'UserModifyRequest17sp4',
584     userId    => $userId,
585     endpoint  => $endpoint,
586   );
587
588   $success ? '' : $message;
589 }
590
591 =item set_sip_authentication USERID, NAME, PASSWORD
592
593 Sets the SIP authentication credentials for USERID to (NAME, PASSWORD).
594
595 =cut
596
597 sub set_sip_authentication {
598   my ($self, $userId, $userName, $password) = @_;
599
600   my ($success, $message) = $self->request(
601     'Services/ServiceAuthentication' => 'UserAuthenticationModifyRequest',
602     userId      => $userId,
603     userName    => $userName,
604     newPassword => $password,
605   );
606
607   $success ? '' : $message;
608 }
609
610 =item delete_group GROUPID
611
612 Deletes the group GROUPID.
613
614 =cut
615
616 sub delete_Group {
617   my ($self, $groupId) = @_;
618
619   my ($success, $message) = $self->request(
620     Group => 'GroupDeleteRequest',
621     $self->SPID,
622     groupId => $groupId
623   );
624   if ( $success or $message =~ /Group not found/ ) {
625     return '';
626   } else {
627     return $message;
628   }
629 }
630
631 =item delete_User USERID
632
633 Deletes the user USERID, and releases its phone number if it has one.
634
635 =cut
636
637 sub delete_User {
638   my ($self, $userId) = @_;
639
640   my ($success, $message) = $self->request(
641     User => 'UserDeleteRequest',
642     userId => $userId
643   );
644   if ($success or $message =~ /User not found/) {
645     return '';
646   } else {
647     return $message;
648   }
649 }
650
651 =item delete_Device DEVICENAME[, GROUPID ]
652
653 Deletes the access device DEVICENAME (from group GROUPID, or from the service
654 provider if there is no GROUPID).
655
656 =cut
657
658 sub delete_Device {
659   my ($self, $deviceName, $groupId) = @_;
660
661   my ($success, $message);
662   if ( $groupId ) {
663     ($success, $message) = $self->request(
664       Group => 'GroupAccessDeviceDeleteRequest',
665       $self->SPID,
666       groupId => $groupId,
667       deviceName => $deviceName,
668     );
669   } else {
670     ($success, $message) = $self->request(
671       ServiceProvider => 'ServiceProviderAccessDeviceDeleteRequest',
672       $self->SPID,
673       deviceName => $deviceName,
674     );
675   }
676   if ( $success or $message =~ /Access Device not found/ ) {
677     return '';
678   } else {
679     return $message;
680   }
681 }
682
683 =back
684
685 =head2 CONVENIENCE METHODS
686
687 =over 4
688
689 =item SPID
690
691 Returns 'serviceProviderId' => the service_provider option. This is commonly
692 needed in request parameters.
693
694 =item groupId CUST_MAIN
695
696 Returns the groupID that goes with the specified customer.
697
698 =item userId SVC_X
699
700 Returns the userId (including domain) that should go with the specified
701 service.
702
703 =item deviceName DEVICE
704
705 Returns the access device name that should go with the specified phone_device
706 or pbx_device.
707
708 =cut
709
710 sub SPID {
711   my $self = shift;
712   my $id = $self->option('service_provider') or die 'service provider not set';
713   'serviceProviderId' => $id
714 }
715
716 sub groupId {
717   my $self = shift;
718   my $cust_main = shift;
719   'cust_main#'.$cust_main->custnum;
720 }
721
722 sub userId {
723   my $self = shift;
724   my $svc = shift;
725   my $userId;
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";
730   }
731   my $domain = $self->option('domain'); # domsvc?
732   $userId .= '@' . $domain if $domain;
733
734   return $userId;
735 }
736
737 sub deviceName {
738   my $self = shift;
739   my $device = shift;
740   $device->mac_addr || ($device->table . '#' . $device->devicenum);
741 }
742
743 =item oci_table HASHREF
744
745 Converts the base OCITable type into an arrayref of hashrefs.
746
747 =cut
748
749 sub oci_table {
750   my $self = shift;
751   my $oci_table = shift;
752   my @colnames = $oci_table->{colHeading};
753   my @data;
754   foreach my $row (@{ $oci_table->{row} }) {
755     my %hash;
756     @hash{@colnames} = @{ $row->{col} };
757     push @data, \%hash;
758   }
759
760   \@data;
761 }
762
763 #################
764 # DID SELECTION #
765 #################
766
767
768
769 ################
770 # CALL DETAILS #
771 ################
772
773 =item import_cdrs START, END
774
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.
778
779 =cut
780
781 ##############
782 # API ACCESS #
783 ##############
784
785 =item request SCOPE, COMMAND, [ ARGUMENTS... ]
786
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.
790
791 On failure of the request (or failure to authenticate), the response message
792 will be a simple scalar containing the error message.
793
794 =cut
795
796 sub request {
797   my $self = shift;
798
799   delete $client{$self->exportnum} if $expire{$self->exportnum} < time;
800   my $client = $client{$self->exportnum};
801   if (!$client) {
802     local $@;
803     eval "use BroadWorks::OCI";
804     die "$me $@" if $@;
805
806     Log::Report::dispatcher('PERL', 'default',
807       mode => ($self->option('debug') ? 'DEBUG' : 'NORMAL')
808     );
809
810     $client = BroadWorks::OCI->new(
811       userId    => $self->option('admin_user'),
812       password  => $self->option('admin_pass'),
813     );
814     my ($success, $message) = $client->login;
815     return ('', $message) if !$success;
816
817     $client{$self->exportnum} = $client; # if login succeeded
818     $expire{$self->exportnum} = time + 120; # hardcoded, yeah
819   }
820   return $client->request(@_);
821 }
822
823 1;