1 package FS::part_export::portaone;
5 use base qw( FS::part_export );
8 use Net::HTTPS::Any qw(https_post);
16 FS::part_export::portaone
20 PortaOne integration for Freeside
24 This export offers basic svc_phone provisioning for PortaOne.
26 This module also provides generic methods for working through the L</PortaOne API>.
32 tie my %options, 'Tie::IxHash',
33 'username' => { label => 'User Name',
35 'password' => { label => 'Password',
37 'port' => { label => 'Port',
39 'account_id' => { label => 'Account ID',
40 default => 'FREESIDE CUST $custnum' },
41 'customer_name' => { label => 'Customer Name',
42 default => 'FREESIDE SVC $svcnum' },
43 'debug' => { type => 'checkbox',
44 label => 'Enable debug warnings' },
49 'desc' => 'Export customer and service/account to PortaOne',
50 'options' => \%options,
52 During insert, this will add customers to portaone if they do not yet exist,
53 using the "Customer Name" option with substitutions from the customer record
54 in freeside. If option "Account ID" is also specified, an account will be
55 created for the service and assigned to the customer, using substitutions
56 from the phone service record in freeside.
58 During replace, if a matching account id for the old service can be found,
59 the existing customer and account will be updated. Otherwise, if a matching
60 customer name is found, the info for that customer will be updated.
61 Otherwise, nothing will be updated during replace.
63 Use caution to avoid name/id conflicts when introducing this export to a portaone
64 system with existing customers/accounts.
68 ### NOTE: If we provision DIDs, conflicts with existing data and changes
69 ### to the name/id scheme will be non-issues, as we can load DID by number
70 ### and then load account/customer from there, but provisioning DIDs has
71 ### not yet been implemented....
74 my ($self, $svc_phone) = @_;
76 # load needed info from our end
77 my $cust_main = $svc_phone->cust_main;
78 return "Could not load service customer" unless $cust_main;
79 my $conf = new FS::Conf;
81 # make sure customer name is configured
82 my $customer_name = $self->portaone_customer_name($cust_main);
83 return "No customer name configured, nothing to export"
84 unless $customer_name;
86 # initialize api session
88 return $self->api_error if $self->api_error;
90 # check if customer already exists
91 my $customer_info = $self->api_call('Customer','get_customer_info',{
92 'name' => $customer_name,
94 my $i_customer = $customer_info ? $customer_info->{'i_customer'} : undef;
96 # insert customer (using name => custnum) if customer with that name/custnum doesn't exist
97 # has the possibility of creating duplicates if customer was previously hand-entered,
98 # could check if customer has existing services on our end, load customer from one of those
100 unless ($i_customer) {
101 $i_customer = $self->api_call('Customer','add_customer',{
103 'name' => $customer_name,
104 'iso_4217' => ($conf->config('currency') || 'USD'),
107 return $self->api_error_logout if $self->api_error;
108 unless ($i_customer) {
110 return "Error creating customer";
114 # export account if account id is configured
115 my $account_id = $self->portaone_account_id($svc_phone);
117 # check if account already exists
118 my $account_info = $self->api_call('Account','get_account_info',{
124 # there shouldn't be any time account already exists on insert,
125 # but if custnum matches, should be safe to run with it
126 unless ($account_info->{'i_customer'} eq $i_customer) {
128 return "Account $account_id already exists";
130 $i_account = $account_info->{'i_account'};
132 # normal case--insert account for this service
133 $i_account = $self->api_call('Account','add_account',{
135 'id' => $self->portaone_account_id($svc_phone),
136 'i_customer' => $i_customer,
137 'iso_4217' => ($conf->config('currency') || 'USD'),
140 return $self->api_error_logout if $self->api_error;
142 unless ($i_account) {
144 return "Error creating account";
148 # update customer, including name
149 $self->api_update_customer($i_customer,$cust_main);
150 return $self->api_error_logout if $self->api_error;
153 return $self->api_logout;
156 sub _export_replace {
157 my ($self, $svc_phone, $svc_phone_old) = @_;
159 # load needed info from our end
160 my $cust_main = $svc_phone->cust_main;
161 return "Could not load service customer" unless $cust_main;
162 my $conf = new FS::Conf;
164 # initialize api session
166 return $self->api_error if $self->api_error;
168 # if we ever provision DIDs, we should load from DID rather than account
170 # check for existing account
171 my $account_id = $self->portaone_account_id($svc_phone_old);
172 my $account_info = $self->api_call('Account','get_account_info',{
175 my $i_account = $account_info ? $account_info->{'i_account'} : undef;
177 # if account exists, use account customer
180 $i_account = $account_info->{'i_account'};
181 $i_customer = $account_info->{'i_customer'};
182 # otherwise, check for existing customer
184 my $customer_name = $self->portaone_customer_name($cust_main);
185 my $customer_info = $self->api_call('Customer','get_customer_info',{
186 'name' => $customer_name,
188 $i_customer = $customer_info ? $customer_info->{'i_customer'} : undef;
191 unless ($i_customer) {
193 return "Neither customer nor account found in portaone";
196 # update customer info
197 $self->api_update_customer($i_customer,$cust_main) if $i_customer;
198 return $self->api_error_logout if $self->api_error;
200 # update account info
201 $self->api_update_account($i_account,$svc_phone) if $i_account;
202 return $self->api_error_logout if $self->api_error;
205 return $self->api_logout();
209 my ($self, $svc_phone) = @_;
213 sub _export_suspend {
214 my ($self, $svc_phone) = @_;
218 sub _export_unsuspend {
219 my ($self, $svc_phone) = @_;
225 These methods allow access to the PortaOne API using the credentials
226 set in the export options.
229 die $export->api_error if $export->api_error;
231 my $customer_info = $export->api_call('Customer','get_customer_info',{
232 'name' => $export->portaone_customer_name($cust_main),
234 die $export->api_error_logout if $export->api_error;
236 return $export->api_logout;
242 Accepts I<$service>, I<$method>, I<$params> hashref and optional
243 I<$returnfield>. Places an api call to the specified service
244 and method with the specified params. Returns the decoded json
245 object returned by the api call. If I<$returnfield> is specified,
246 returns only that field of the decoded object, and errors out if
247 that field does not exist. Returns empty on failure; retrieve
248 error messages using L</api_error>.
250 Must run L</api_login> first.
255 my ($self,$service,$method,$params,$returnfield) = @_;
256 $self->{'__portaone_error'} = '';
257 my $auth_info = $self->{'__portaone_auth_info'};
258 my %auth_info = $auth_info ? ('auth_info' => encode_json($auth_info)) : ();
260 print "Calling $service/$method\n" if $self->option('debug');
261 my ( $page, $response, %reply_headers ) = https_post(
262 'host' => $self->machine,
263 'port' => $self->option('port'),
264 'path' => '/rest/'.$service.'/'.$method.'/',
265 'args' => [ %auth_info, 'params' => encode_json($params) ],
267 if (($response eq '200 OK') || ($response =~ /^500/)) {
269 eval { $result = decode_json($page) };
271 $self->{'__portaone_error'} = "Error decoding json: $@";
274 if ($response eq '200 OK') {
275 return $result unless $returnfield;
276 unless (exists $result->{$returnfield}) {
277 $self->{'__portaone_error'} = "Field $returnfield not returned during $service/$method";
280 return $result->{$returnfield};
282 if ($result->{'faultcode'}) {
283 $self->{'__portaone_error'} =
284 "Server returned error during $service/$method: ".$result->{'faultstring'};
288 $self->{'__portaone_error'} =
289 "Bad response from server during $service/$method: $response";
295 Returns the error string set by L</PortaOne API> methods,
296 or a blank string if most recent call produced no errors.
302 return $self->{'__portaone_error'} || '';
305 =head2 api_error_logout
307 Attempts L</api_logout>, but returns L</api_error> message from
308 before logout was attempted. Useful for logging out
309 properly after an error.
313 sub api_error_logout {
315 my $error = $self->api_error;
322 Initializes an api session using the credentials for this export.
323 Always returns empty. Retrieve error messages using L</api_error>.
329 $self->{'__portaone_auth_info'} = undef; # needs to be declared undef for api_call
330 my $result = $self->api_call('Session','login',{
331 'login' => $self->option('username'),
332 'password' => $self->option('password'),
334 return unless $result;
335 $self->{'__portaone_auth_info'} = $result;
341 Ends the current api session established by L</api_login>.
343 For convenience, returns L</api_error>.
349 $self->api_call('Session','logout',$self->{'__portaone_auth_info'});
350 return $self->api_error;
353 =head2 api_update_account
355 Accepts I<$i_account> and I<$svc_phone>. Updates the account
356 specified by I<$i_account> with the current values of I<$svc_phone>
357 (currently only updates account_id.)
358 Always returns empty. Retrieve error messages using L</api_error>.
362 sub api_update_account {
363 my ($self,$i_account,$svc_phone) = @_;
364 my $newid = $self->portaone_account_id($svc_phone);
366 $self->{'__portaone_error'} = "Error loading account id during update_account";
369 my $updated_account = $self->api_call('Account','update_account',{
371 'i_account' => $i_account,
375 return if $self->api_error;
376 $self->{'__portaone_error'} = "Account updated, but account id mismatch detected"
377 unless $updated_account eq $i_account; # should never happen
381 =head2 api_update_customer
383 Accepts I<$i_customer> and I<$cust_main>. Updates the customer
384 specified by I<$i_customer> with the current values of I<$cust_main>.
385 Always returns empty. Retrieve error messages using L</api_error>.
389 sub api_update_customer {
390 my ($self,$i_customer,$cust_main) = @_;
391 my $location = $cust_main->bill_location;
393 $self->{'__portaone_error'} = "Could not load customer location";
396 my $newname = $self->portaone_customer_name($cust_main);
398 $self->{'__portaone_error'} = "Error loading customer name during update_customer";
401 my $updated_customer = $self->api_call('Customer','update_customer',{
403 'i_customer' => $i_customer,
405 'companyname' => $cust_main->company,
406 'firstname' => $cust_main->first,
407 'lastname' => $cust_main->last,
408 'baddr1' => $location->address1,
409 'baddr2' => $location->address2,
410 'city' => $location->city,
411 'state' => $location->state,
412 'zip' => $location->zip,
413 'country' => $location->country,
414 # could also add contact phones & email here
417 return if $self->api_error;
418 $self->{'__portaone_error'} = "Customer updated, but custnum mismatch detected"
419 unless $updated_customer eq $i_customer; # should never happen
424 my ($self, $string, @objects) = @_;
425 return '' unless $string;
426 foreach my $object (@objects) {
428 foreach my $field ($object->fields) {
430 my $value = $object->get($field);
431 $string =~ s/\$$field/$value/g;
434 # strip leading/trailing whitespace
440 =head2 portaone_customer_name
442 Accepts I<$cust_main> and returns customer name with substitutions.
446 sub portaone_customer_name {
447 my ($self, $cust_main) = @_;
448 $self->_substitute($self->option('customer_name'),$cust_main);
451 =head2 portaone_account_id
453 Accepts I<$svc_phone> and returns account id with substitutions.
457 sub portaone_account_id {
458 my ($self, $svc_phone) = @_;
459 $self->_substitute($self->option('account_id'),$svc_phone);
469 jonathan@freeside.biz