1 package FS::part_export::portaone;
5 use base qw( FS::part_export );
7 use Date::Format 'time2str';
9 use Net::HTTPS::Any qw(https_post);
17 FS::part_export::portaone
21 PortaOne integration for Freeside
25 This export offers basic svc_phone provisioning for PortaOne.
27 This module also provides generic methods for working through the L</PortaOne API>.
33 tie my %options, 'Tie::IxHash',
34 'username' => { label => 'User Name',
36 'password' => { label => 'Password',
38 'port' => { label => 'Port',
40 'customer_name' => { label => 'Customer Name',
41 default => 'FREESIDE CUST $custnum' },
42 'account_id' => { label => 'Account ID',
43 default => 'SVC$svcnum' },
44 'product_id' => { label => 'Account Product ID' },
45 'debug' => { type => 'checkbox',
46 label => 'Enable debug warnings' },
51 'desc' => 'Export customer and service/account to PortaOne',
52 'options' => \%options,
54 During insert, this will add customers to portaone if they do not yet exist,
55 using the "Customer Name" option with substitutions from the customer record
56 in freeside. If options "Account ID" and "Account Product ID" are also specified,
57 an account will be created for the service and assigned to the customer, using
58 substitutions from the phone service record in freeside for the Account ID.
60 During replace, if a matching account id for the old service can be found,
61 the existing customer and account will be updated. Otherwise, if a matching
62 customer name is found, the info for that customer will be updated.
63 Otherwise, nothing will be updated during replace.
65 Use caution to avoid name/id conflicts when introducing this export to a portaone
66 system with existing customers/accounts.
70 ### NOTE: If we provision DIDs, conflicts with existing data and changes
71 ### to the name/id scheme will be non-issues, as we can load DID by number
72 ### and then load account/customer from there, but provisioning DIDs has
73 ### not yet been implemented....
76 my ($self, $svc_phone) = @_;
78 # load needed info from our end
79 my $cust_main = $svc_phone->cust_main;
80 return "Could not load service customer" unless $cust_main;
81 my $conf = new FS::Conf;
83 # make sure customer name is configured
84 my $customer_name = $self->portaone_customer_name($cust_main);
85 return "No customer name configured, nothing to export"
86 unless $customer_name;
88 # initialize api session
90 return $self->api_error if $self->api_error;
92 # check if customer already exists
93 my $customer_info = $self->api_call('Customer','get_customer_info',{
94 'name' => $customer_name,
96 my $i_customer = $customer_info ? $customer_info->{'i_customer'} : undef;
98 # insert customer (using name => custnum) if customer with that name/custnum doesn't exist
99 # has the possibility of creating duplicates if customer was previously hand-entered,
100 # could check if customer has existing services on our end, load customer from one of those
101 # but...not right now
102 unless ($i_customer) {
103 $i_customer = $self->api_call('Customer','add_customer',{
105 'name' => $customer_name,
106 'iso_4217' => ($conf->config('currency') || 'USD'),
109 return $self->api_error_logout if $self->api_error;
110 unless ($i_customer) {
112 return "Error creating customer";
116 # export account if account id is configured
117 my $account_id = $self->portaone_account_id($svc_phone);
118 my $product_id = $self->option('product_id');
119 if ($account_id && $product_id) {
120 # check if account already exists
121 my $account_info = $self->api_call('Account','get_account_info',{
127 # there shouldn't be any time account already exists on insert,
128 # but if custnum matches, should be safe to run with it
129 unless ($account_info->{'i_customer'} eq $i_customer) {
131 return "Account $account_id already exists";
133 $i_account = $account_info->{'i_account'};
135 # normal case--insert account for this service
136 $i_account = $self->api_call('Account','add_account',{
139 'i_customer' => $i_customer,
140 'iso_4217' => ($conf->config('currency') || 'USD'),
141 'i_product' => $product_id,
142 'activation_date' => time2str("%Y-%m-%d",time),
143 'billing_model' => 1, # '1' for credit, '-1' for debit, could make this an export option
144 'h323_password' => $svc_phone->sip_password,
147 return $self->api_error_logout if $self->api_error;
149 unless ($i_account) {
151 return "Error creating account";
155 # update customer, including name
156 $self->api_update_customer($i_customer,$cust_main);
157 return $self->api_error_logout if $self->api_error;
160 return $self->api_logout;
163 sub _export_replace {
164 my ($self, $svc_phone, $svc_phone_old) = @_;
166 # load needed info from our end
167 my $cust_main = $svc_phone->cust_main;
168 return "Could not load service customer" unless $cust_main;
169 my $conf = new FS::Conf;
171 # initialize api session
173 return $self->api_error if $self->api_error;
175 # if we ever provision DIDs, we should load from DID rather than account
177 # check for existing account
178 my $account_id = $self->portaone_account_id($svc_phone_old);
179 my $account_info = $self->api_call('Account','get_account_info',{
182 my $i_account = $account_info ? $account_info->{'i_account'} : undef;
184 # if account exists, use account customer
187 $i_account = $account_info->{'i_account'};
188 $i_customer = $account_info->{'i_customer'};
189 # if nothing changed, no need to update account
191 if ($account_info->{'i_product'} eq $self->option('product_id'))
192 && ($account_id eq $self->portaone_account_id($svc_phone));
193 # otherwise, check for existing customer
195 my $customer_name = $self->portaone_customer_name($cust_main);
196 my $customer_info = $self->api_call('Customer','get_customer_info',{
197 'name' => $customer_name,
199 $i_customer = $customer_info ? $customer_info->{'i_customer'} : undef;
202 unless ($i_customer) {
204 return "Neither customer nor account found in portaone";
207 # update customer info
208 $self->api_update_customer($i_customer,$cust_main) if $i_customer;
209 return $self->api_error_logout if $self->api_error;
211 # update account info
212 $self->api_update_account($i_account,$svc_phone) if $i_account;
213 return $self->api_error_logout if $self->api_error;
216 return $self->api_logout();
220 my ($self, $svc_phone) = @_;
224 sub _export_suspend {
225 my ($self, $svc_phone) = @_;
229 sub _export_unsuspend {
230 my ($self, $svc_phone) = @_;
236 These methods allow access to the PortaOne API using the credentials
237 set in the export options.
240 die $export->api_error if $export->api_error;
242 my $customer_info = $export->api_call('Customer','get_customer_info',{
243 'name' => $export->portaone_customer_name($cust_main),
245 die $export->api_error_logout if $export->api_error;
247 return $export->api_logout;
253 Accepts I<$service>, I<$method>, I<$params> hashref and optional
254 I<$returnfield>. Places an api call to the specified service
255 and method with the specified params. Returns the decoded json
256 object returned by the api call. If I<$returnfield> is specified,
257 returns only that field of the decoded object, and errors out if
258 that field does not exist. Returns empty on failure; retrieve
259 error messages using L</api_error>.
261 Must run L</api_login> first.
266 my ($self,$service,$method,$params,$returnfield) = @_;
267 $self->{'__portaone_error'} = '';
268 my $auth_info = $self->{'__portaone_auth_info'};
269 my %auth_info = $auth_info ? ('auth_info' => encode_json($auth_info)) : ();
271 print "Calling $service/$method\n" if $self->option('debug');
272 my ( $page, $response, %reply_headers ) = https_post(
273 'host' => $self->machine,
274 'port' => $self->option('port'),
275 'path' => '/rest/'.$service.'/'.$method.'/',
276 'args' => [ %auth_info, 'params' => encode_json($params) ],
278 if (($response eq '200 OK') || ($response =~ /^500/)) {
280 eval { $result = decode_json($page) };
282 $self->{'__portaone_error'} = "Error decoding json: $@";
285 if ($response eq '200 OK') {
286 return $result unless $returnfield;
287 unless (exists $result->{$returnfield}) {
288 $self->{'__portaone_error'} = "Field $returnfield not returned during $service/$method";
291 return $result->{$returnfield};
293 if ($result->{'faultcode'}) {
294 $self->{'__portaone_error'} =
295 "Server returned error during $service/$method: ".$result->{'faultstring'};
299 $self->{'__portaone_error'} =
300 "Bad response from server during $service/$method: $response";
306 Returns the error string set by L</PortaOne API> methods,
307 or a blank string if most recent call produced no errors.
313 return $self->{'__portaone_error'} || '';
316 =head2 api_error_logout
318 Attempts L</api_logout>, but returns L</api_error> message from
319 before logout was attempted. Useful for logging out
320 properly after an error.
324 sub api_error_logout {
326 my $error = $self->api_error;
333 Initializes an api session using the credentials for this export.
334 Always returns empty. Retrieve error messages using L</api_error>.
340 $self->{'__portaone_auth_info'} = undef; # needs to be declared undef for api_call
341 my $result = $self->api_call('Session','login',{
342 'login' => $self->option('username'),
343 'password' => $self->option('password'),
345 return unless $result;
346 $self->{'__portaone_auth_info'} = $result;
352 Ends the current api session established by L</api_login>.
354 For convenience, returns L</api_error>.
360 $self->api_call('Session','logout',$self->{'__portaone_auth_info'});
361 return $self->api_error;
364 =head2 api_update_account
366 Accepts I<$i_account> and I<$svc_phone>. Updates the account
367 specified by I<$i_account> with the current values of I<$svc_phone>
368 (currently only updates account_id.)
369 Always returns empty. Retrieve error messages using L</api_error>.
373 sub api_update_account {
374 my ($self,$i_account,$svc_phone) = @_;
375 my $newid = $self->portaone_account_id($svc_phone);
377 $self->{'__portaone_error'} = "Error loading account id during update_account";
380 my $updated_account = $self->api_call('Account','update_account',{
382 'i_account' => $i_account,
384 'i_product' => $self->option('product_id'),
385 'h323_password' => $svc_phone->sip_password,
388 return if $self->api_error;
389 $self->{'__portaone_error'} = "Account updated, but account id mismatch detected"
390 unless $updated_account eq $i_account; # should never happen
394 =head2 api_update_customer
396 Accepts I<$i_customer> and I<$cust_main>. Updates the customer
397 specified by I<$i_customer> with the current values of I<$cust_main>.
398 Always returns empty. Retrieve error messages using L</api_error>.
402 sub api_update_customer {
403 my ($self,$i_customer,$cust_main) = @_;
404 my $location = $cust_main->bill_location;
406 $self->{'__portaone_error'} = "Could not load customer location";
409 my $newname = $self->portaone_customer_name($cust_main);
411 $self->{'__portaone_error'} = "Error loading customer name during update_customer";
414 my $updated_customer = $self->api_call('Customer','update_customer',{
416 'i_customer' => $i_customer,
418 'companyname' => $cust_main->company,
419 'firstname' => $cust_main->first,
420 'lastname' => $cust_main->last,
421 'baddr1' => $location->address1,
422 'baddr2' => $location->address2,
423 'city' => $location->city,
424 'state' => $location->state,
425 'zip' => $location->zip,
426 'country' => $location->country,
427 # could also add contact phones & email here
430 return if $self->api_error;
431 $self->{'__portaone_error'} = "Customer updated, but custnum mismatch detected"
432 unless $updated_customer eq $i_customer; # should never happen
437 my ($self, $string, @objects) = @_;
438 return '' unless $string;
439 foreach my $object (@objects) {
441 foreach my $field ($object->fields) {
443 my $value = $object->get($field);
444 $string =~ s/\$$field/$value/g;
447 # strip leading/trailing whitespace
453 =head2 portaone_customer_name
455 Accepts I<$cust_main> and returns customer name with substitutions.
459 sub portaone_customer_name {
460 my ($self, $cust_main) = @_;
461 $self->_substitute($self->option('customer_name'),$cust_main);
464 =head2 portaone_account_id
466 Accepts I<$svc_phone> and returns account id with substitutions.
470 sub portaone_account_id {
471 my ($self, $svc_phone) = @_;
472 $self->_substitute($self->option('account_id'),$svc_phone);
482 jonathan@freeside.biz