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 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.
32 This module also provides generic methods for working through the L</PortaOne API>.
38 tie my %options, 'Tie::IxHash',
39 'username' => { label => 'User Name',
41 'password' => { label => 'Password',
43 'port' => { label => 'Port',
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' },
55 'desc' => 'Export customer and service/account to PortaOne',
56 'options' => \%options,
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.
67 my ($self, $svc_phone) = @_;
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;
74 # initialize api session
76 return $self->api_error if $self->api_error;
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
82 # return $self->api_error if $self->api_error;
83 # return "Number is already assigned" if $number_info->{'i_account'};
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,
91 my $i_customer = $customer_info ? $customer_info->{'i_customer'} : undef;
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
97 unless ($i_customer) {
98 $i_customer = $self->api_call('Customer','add_customer',{
100 'name' => $self->option('customer_prefix') . $cust_main->custnum,
101 'iso_4217' => ($conf->config('currency') || 'USD'),
104 return $self->api_error if $self->api_error;
105 return "Error creating customer" unless $i_customer;
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,
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'};
121 # normal case--insert account for this service
122 $i_account = $self->api_call('Account','add_account',{
124 'id' => $self->option('account_prefix') . $svc_phone->svcnum,
125 'i_customer' => $i_customer,
126 'iso_4217' => ($conf->config('currency') || 'USD'),
129 return $self->api_error if $self->api_error;
131 return "Error creating account" unless $i_account;
133 # assign DID to account
135 # update customer, including name
136 $self->api_update_customer($i_customer,$cust_main);
137 return $self->api_error if $self->api_error;
140 return $self->api_logout;
143 sub _export_replace {
144 my ($self, $svc_phone, $svc_phone_old) = @_;
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;
151 # initialize api session
153 return $self->api_error if $self->api_error;
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,
160 my $i_customer = $customer_info ? $customer_info->{'i_customer'} : undef;
162 return "Customer not found in portaone" unless $i_customer;
165 # make sure new did is available, reserve
166 # release old did from account
167 # assign new did to account
169 # update customer info
170 $self->api_update_customer($i_customer,$cust_main);
171 return $self->api_error if $self->api_error;
174 return $self->api_logout();
178 my ($self, $svc_phone) = @_;
182 sub _export_suspend {
183 my ($self, $svc_phone) = @_;
187 sub _export_unsuspend {
188 my ($self, $svc_phone) = @_;
194 These methods allow access to the PortaOne API using the credentials
195 set in the export options.
198 die $export->api_error if $export->api_error;
200 my $customer_info = $export->api_call('Customer','get_customer_info',{
201 'name' => $export->option('customer_prefix') . $cust_main->custnum,
203 die $export->api_error if $export->api_error;
206 die $export->api_error if $export->api_error;
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>.
220 Must run L</api_login> first.
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)) : ();
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) ],
237 if (($response eq '200 OK') || ($response =~ /^500/)) {
239 eval { $result = decode_json($page) };
241 $self->{'__portaone_error'} = "Error decoding json: $@";
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";
250 return $result->{$returnfield};
252 if ($result->{'faultcode'}) {
253 $self->{'__portaone_error'} =
254 "Server returned error during $service/$method: ".$result->{'faultstring'};
258 $self->{'__portaone_error'} =
259 "Bad response from server during $service/$method: $response";
265 Returns the error string set by L</PortaOne API> methods,
266 or a blank string if most recent call produced no errors.
272 return $self->{'__portaone_error'} || '';
277 Initializes an api session using the credentials for this export.
278 Always returns empty. Retrieve error messages using L</api_error>.
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'),
289 return unless $result;
290 $self->{'__portaone_auth_info'} = $result;
296 Ends the current api session established by L</api_login>.
298 For convenience, returns L</api_error>.
304 $self->api_call('Session','logout',$self->{'__portaone_auth_info'});
305 return $self->api_error;
308 =head2 api_update_customer
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>.
316 sub api_update_customer {
317 my ($self,$i_customer,$cust_main) = @_;
318 my $location = $cust_main->bill_location;
320 $self->{'__portaone_error'} = "Could not load customer location";
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
336 $self->{'__portaone_error'} = "Customer updated, but custnum mismatch detected"
337 unless $updated_customer eq $i_customer;
348 jonathan@freeside.biz