RT#40056: Export DID's to portaone switch
[freeside.git] / FS / FS / part_export / portaone.pm
1 package FS::part_export::portaone;
2
3 use strict;
4
5 use base qw( FS::part_export );
6
7 use Cpanel::JSON::XS;
8 use Net::HTTPS::Any qw(https_post);
9
10 use FS::Conf;
11
12 =pod
13
14 =head1 NAME
15
16 FS::part_export::portaone
17
18 =head1 SYNOPSIS
19
20 PortaOne integration for Freeside
21
22 =head1 DESCRIPTION
23
24 This export offers basic svc_phone provisioning for PortaOne.
25
26 During insert, this will add customers to portaone if they do not yet exist,
27 using the customer prefix + custnum as the customer name.  An account will
28 be created for the service and assigned to the customer, using account prefix
29 + svcnum as the account id.  During replace, the customer info will be updated
30 if it already exists in the system.
31
32 This module also provides generic methods for working through the L</PortaOne API>.
33
34 =cut
35
36 use vars qw( %info );
37
38 tie my %options, 'Tie::IxHash',
39   'username'         => { label => 'User Name',
40                           default => '' },
41   'password'         => { label => 'Password',
42                           default => '' },
43   'port'             => { label => 'Port',
44                           default => 443 },
45   'account_prefix'   => { label => 'Account ID Prefix',
46                           default => 'FREESIDE_CUST' },
47   'customer_prefix'  => { label => 'Customer Name Prefix',
48                           default => 'FREESIDE_SVC' },
49   'debug'            => { type => 'checkbox',
50                           label => 'Enable debug warnings' },
51 ;
52
53 %info = (
54   'svc'             => 'svc_phone',
55   'desc'            => 'Export customer and service/account to PortaOne',
56   'options'         => \%options,
57   'notes'           => <<'END',
58 During insert, this will add customers to portaone if they do not yet exist,
59 using the customer prefix + custnum as the customer name.  An account will
60 be created for the service and assigned to the customer, using account prefix
61 + svcnum as the account id.  During replace, the customer info will be updated
62 if it already exists in the system.
63 END
64 );
65
66 sub _export_insert {
67   my ($self, $svc_phone) = @_;
68
69   # load needed info from our end
70   my $cust_main = $svc_phone->cust_main;
71   return "Could not load service customer" unless $cust_main;
72   my $conf = new FS::Conf;
73
74   # initialize api session
75   $self->api_login;
76   return $self->api_error if $self->api_error;
77
78   # load DID, abort if it is already assigned
79 #  my $number_info = $self->api_call('DID','get_number_info',{
80 #    'number' => $svc_phone->countrycode . $svc_phone->phonenum
81 #  },'number_info');
82 #  return $self->api_error if $self->api_error;
83 #  return "Number is already assigned" if $number_info->{'i_account'};
84
85   # reserve DID
86
87   # check if customer already exists
88   my $customer_info = $self->api_call('Customer','get_customer_info',{
89     'name' => $self->option('customer_prefix') . $cust_main->custnum,
90   },'customer_info');
91   my $i_customer = $customer_info ? $customer_info->{'i_customer'} : undef;
92
93   # insert customer (using name => custnum) if customer with that name/custnum doesn't exist
94   #   has the possibility of creating duplicates if customer was previously hand-entered,
95   #   could check if customer has existing services on our end, load customer from one of those
96   #   but...not right now
97   unless ($i_customer) {
98     $i_customer = $self->api_call('Customer','add_customer',{
99       'customer_info' => {
100         'name' => $self->option('customer_prefix') . $cust_main->custnum,
101         'iso_4217' => ($conf->config('currency') || 'USD'),
102       }
103     },'i_customer');
104     return $self->api_error if $self->api_error;
105     return "Error creating customer" unless $i_customer;
106   }
107
108   # check if account already exists
109   my $account_info = $self->api_call('Account','get_account_info',{
110     'id' => $self->option('account_prefix') . $svc_phone->svcnum,
111   },'account_info');
112
113   my $i_account;
114   if ($account_info) {
115     # there shouldn't be any time account already exists on insert,
116     # but if custnum & svcnum match, should be safe to run with it
117     return "Account " . $svc_phone->svcnum . " already exists"
118       unless $account_info->{'i_customer'} eq $i_customer;
119     $i_account = $account_info->{'i_account'};
120   } else {
121     # normal case--insert account for this service
122     $i_account = $self->api_call('Account','add_account',{
123       'account_info' => {
124         'id' => $self->option('account_prefix') . $svc_phone->svcnum,
125         'i_customer' => $i_customer,
126         'iso_4217' => ($conf->config('currency') || 'USD'),
127       }
128     },'i_account');
129     return $self->api_error if $self->api_error;
130   }
131   return "Error creating account" unless $i_account;
132
133   # assign DID to account
134
135   # update customer, including name
136   $self->api_update_customer($i_customer,$cust_main);
137   return $self->api_error if $self->api_error;
138
139   # end api session
140   return $self->api_logout;
141 }
142
143 sub _export_replace {
144   my ($self, $svc_phone, $svc_phone_old) = @_;
145
146   # load needed info from our end
147   my $cust_main = $svc_phone->cust_main;
148   return "Could not load service customer" unless $cust_main;
149   my $conf = new FS::Conf;
150
151   # initialize api session
152   $self->api_login;
153   return $self->api_error if $self->api_error;
154
155   # check for existing customer
156   #   should be loading this from DID...
157   my $customer_info = $self->api_call('Customer','get_customer_info',{
158     'name' => $cust_main->custnum,
159   },'customer_info');
160   my $i_customer = $customer_info ? $customer_info->{'i_customer'} : undef;
161
162   return "Customer not found in portaone" unless $i_customer;
163
164   # if did changed
165   #   make sure new did is available, reserve
166   #   release old did from account
167   #   assign new did to account
168
169   # update customer info
170   $self->api_update_customer($i_customer,$cust_main);
171   return $self->api_error if $self->api_error;
172
173   # end api session
174   return $self->api_logout();
175 }
176
177 sub _export_delete {
178   my ($self, $svc_phone) = @_;
179   return '';
180 }
181
182 sub _export_suspend {
183   my ($self, $svc_phone) = @_;
184   return '';
185 }
186
187 sub _export_unsuspend {
188   my ($self, $svc_phone) = @_;
189   return '';
190 }
191
192 =head1 PortaOne API
193
194 These methods allow access to the PortaOne API using the credentials
195 set in the export options.
196
197         $export->api_login;
198         die $export->api_error if $export->api_error;
199
200         my $customer_info = $export->api_call('Customer','get_customer_info',{
201       'name' => $export->option('customer_prefix') . $cust_main->custnum,
202     },'customer_info');
203         die $export->api_error if $export->api_error;
204
205         $export->api_logout;
206         die $export->api_error if $export->api_error;
207
208 =cut
209
210 =head2 api_call
211
212 Accepts I<$service>, I<$method>, I<$params> hashref and optional
213 I<$returnfield>.  Places an api call to the specified service
214 and method with the specified params.  Returns the decoded json
215 object returned by the api call.  If I<$returnfield> is specified,
216 returns only that field of the decoded object, and errors out if
217 that field does not exist.  Returns empty on failure;  retrieve
218 error messages using L</api_error>.
219
220 Must run L</api_login> first.
221
222 =cut
223
224 sub api_call {
225   my ($self,$service,$method,$params,$returnfield) = @_;
226   $self->{'__portaone_error'} = '';
227   my $auth_info = $self->{'__portaone_auth_info'};
228   my %auth_info = $auth_info ? ('auth_info' => encode_json($auth_info)) : ();
229   $params ||= {};
230   print "Calling $service/$method\n" if $self->option('debug');
231   my ( $page, $response, %reply_headers ) = https_post(
232     'host'    => $self->machine,
233     'port'    => $self->option('port'),
234     'path'    => '/rest/'.$service.'/'.$method.'/',
235     'args'    => [ %auth_info, 'params' => encode_json($params) ],
236   );
237   if (($response eq '200 OK') || ($response =~ /^500/)) {
238     my $result;
239     eval { $result = decode_json($page) };
240     unless ($result) {
241       $self->{'__portaone_error'} = "Error decoding json: $@";
242       return;
243     }
244     if ($response eq '200 OK') {
245       return $result unless $returnfield;
246       unless (exists $result->{$returnfield}) {
247         $self->{'__portaone_error'} = "Field $returnfield not returned during $service/$method";
248         return;
249       }
250       return $result->{$returnfield};
251     }
252     if ($result->{'faultcode'}) {
253       $self->{'__portaone_error'} = 
254         "Server returned error during $service/$method: ".$result->{'faultstring'};
255       return;
256     }
257   }
258   $self->{'__portaone_error'} = 
259     "Bad response from server during $service/$method: $response";
260   return;
261 }
262
263 =head2 api_error
264
265 Returns the error string set by L</PortaOne API> methods,
266 or a blank string if most recent call produced no errors.
267
268 =cut
269
270 sub api_error {
271   my $self = shift;
272   return $self->{'__portaone_error'} || '';
273 }
274
275 =head2 api_login
276
277 Initializes an api session using the credentials for this export.
278 Always returns empty.  Retrieve error messages using L</api_error>.
279
280 =cut
281
282 sub api_login {
283   my $self = shift;
284   $self->{'__portaone_auth_info'} = undef;  # needs to be declared undef for api_call
285   my $result = $self->api_call('Session','login',{
286     'login'    => $self->option('username'),
287     'password' => $self->option('password'),
288   });
289   return unless $result;
290   $self->{'__portaone_auth_info'} = $result;
291   return;
292 }
293
294 =head2 api_logout
295
296 Ends the current api session established by L</api_login>.
297
298 For convenience, returns L</api_error>.
299
300 =cut
301
302 sub api_logout {
303   my $self = shift;
304   $self->api_call('Session','logout',$self->{'__portaone_auth_info'});
305   return $self->api_error;
306 }
307
308 =head2 api_update_customer
309
310 Accepts I<$i_customer> and I<$cust_main>.  Updates the customer
311 specified by I<$i_customer> with the current values of I<$cust_main>.
312 Always returns empty.  Retrieve error messages using L</api_error>.
313
314 =cut
315
316 sub api_update_customer {
317   my ($self,$i_customer,$cust_main) = @_;
318   my $location = $cust_main->bill_location;
319   unless ($location) {
320     $self->{'__portaone_error'} = "Could not load customer location";
321     return;
322   }
323   my $updated_customer = $self->api_call('Customer','update_customer',{
324     'i_customer' => $i_customer,
325     'companyname' => $cust_main->company,
326     'firstname' => $cust_main->first,
327     'lastname' => $cust_main->last,
328     'baddr1' => $location->address1,
329     'baddr2' => $location->address2,
330     'city' => $location->city,
331     'state' => $location->state,
332     'zip' => $location->zip,
333     'country' => $location->country,
334     # could also add contact phones & email here
335   },'i_customer');
336   $self->{'__portaone_error'} = "Customer updated, but custnum mismatch detected"
337     unless $updated_customer eq $i_customer;
338   return;
339 }
340
341 =head1 SEE ALSO
342
343 L<FS::part_export>
344
345 =head1 AUTHOR
346
347 Jonathan Prykop 
348 jonathan@freeside.biz
349
350 =cut
351
352 1;
353
354