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