43b5b065cedb3e6bd0196874e6a286566e96c786
[Net-Ikano.git] / lib / Net / Ikano.pm
1 package Net::Ikano;
2
3 use warnings;
4 use strict;
5 use Net::Ikano::XMLUtil;
6 use LWP::UserAgent;
7 use Data::Dumper;
8
9 =head1 NAME
10
11 Net::Ikano - Interface to Ikano wholesale DSL API
12
13 =head1 VERSION
14
15 Version 0.01
16
17 =cut
18
19 our $VERSION = '0.01';
20
21 our $URL = 'https://orders.value.net/OsirisWebService/XmlApi.aspx';
22
23 our $SCHEMA_ROOT = 'https://orders.value.net/osiriswebservice/schema/v1';
24
25 our $API_VERSION = "1.0";
26
27 our @orderType = qw( NEW CANCEL CHANGE );
28
29 our @orderStatus = qw( NEW PENDING CANCELLED COMPLETED ERROR );
30
31 our $AUTOLOAD;
32
33 =head1 SYNOPSIS
34
35     use Net::Ikano;
36
37     my $ikano = Net::Ikano->new(
38                'keyid' => $your_ikano_api_keyid,
39                'password'  => $your_ikano_admin_user_password,
40                'debug' => 1 # remove this for prod
41                'reqpreviewonly' => 1 # remove this for prod
42                'minimalQualResp' => 1 # on quals, return pairs of ProductCustomId+TermsId only
43                'minimalOrderResp' => 1 # return minimal data on order responses
44                              );
45     
46 =head1 SUPPORTED API METHODS
47
48 =item ORDER
49
50 NOTE: supports orders by ProductCustomId only
51
52 $ikano->ORDER(
53     {
54         orderType => 'NEW',
55         ProductCustomId => 'abc123',
56         TermsId => '123',
57         DSLPhoneNumber => '4167800000',
58         Password => 'abc123',
59         PrequalId => '12345',
60         CompanyName => 'abc co',
61         FirstName => 'first',
62         LastName => 'last',
63         MiddleName => '',
64         ContactMethod => 'PHONE',
65         ContactPhoneNumber => '4167800000',
66         ContactEmail => 'x@x.ca',
67         ContactFax => '',
68         DateToOrder => '2010-11-29',
69         RequestClientIP => '127.0.0.1',
70         IspChange => 'NO',
71         IspPrevious => '',
72         CurrentProvider => '',
73     }
74 );
75
76
77 =item CANCEL
78
79 $i->CANCEL(
80     { OrderId => 555 }
81 );
82
83
84 =item PREQUAL
85
86 $ikano->PREQUAL( {
87     AddressLine1 => '123 Test Rd',
88     AddressUnitType => '', 
89     AddressUnitValue =>  '',
90     AddressCity =>  'Toronto',
91     AddressState => 'ON',
92     ZipCode => 'M6C 2J9', # or 12345
93     Country => 'CA', # or US
94     LocationType => 'R', # or B
95     PhoneNumber => '4167800000',
96     RequestClientIP => '127.0.0.1',
97     CheckNetworks => 'ATT,BELLCA,VER', # either one or command-separated like this
98 } );
99
100
101 =item ORDERSTATUS
102
103 $ikano->ORDERSTATUS( 
104     { OrderId => 1234 }
105 );
106
107
108 =item PASSWORDCHANGE 
109
110 $ikano->PASSWORDCHANGE( {
111             DSLPhoneNumber => '4167800000',
112             NewPassword => 'xxx',
113         } );
114
115
116 =item CUSTOMERLOOKUP
117
118 $ikano->CUSTOMERLOOKUP( { PhoneNumber => '4167800000' } );
119
120
121 =item ACCOUNTSTATUSCHANGE
122
123 $ikano->ACCOUNTSTATUSCHANGE(( {
124             type => 'SUSPEND',
125             DSLPhoneNumber => '4167800000',
126             DSLServiecId => 123,
127         } );
128
129 =cut
130
131 sub new {
132     my ($class,%data) = @_;
133     die "missing keyid and/or password" 
134         unless defined $data{'keyid'} && defined $data{'password'};
135     my $self = { 
136         'keyid' => $data{'keyid'},
137         'password' => $data{'password'},
138         'username' => $data{'username'} ? $data{'username'} : 'admin',
139         'debug' => $data{'debug'} ? $data{'debug'} : 0,
140         'reqpreviewonly' => $data{'reqpreviewonly'} ? $data{'reqpreviewonly'} : 0,
141         };
142     bless $self, $class;
143     return $self;
144 }
145
146
147 sub req_ORDER {
148    my ($self, $args) = (shift, shift);
149
150     return "invalid order data" unless defined $args->{orderType}
151         && defined $args->{ProductCustomId} && defined $args->{DSLPhoneNumber};
152    return "invalid order type ".$args->{orderType}
153     unless grep($_ eq $args->{orderType}, @orderType);
154
155     # XXX: rewrite this uglyness?
156     my @ignoreFields = qw( orderType ProductCustomId );
157     my %orderArgs = ();
158     while ( my ($k,$v) = each(%$args) ) {
159         $orderArgs{$k} = [ $v ] unless grep($_ eq $k,@ignoreFields);
160     }
161
162     return Order => {
163         type => $args->{orderType},
164         %orderArgs,
165         ProductCustomId => [ split(',',$args->{ProductCustomId}) ],
166     };
167 }
168
169 sub resp_ORDER {
170    my ($self, $resphash, $reqhash) = (shift, shift);
171    return "invalid order response" unless defined $resphash->{OrderResponse};
172    return $resphash->{OrderResponse};
173 }
174
175 sub req_CANCEL {
176    my ($self, $args) = (shift, shift);
177
178     return "no order id for cancel" unless defined $args->{OrderId};
179
180     return Cancel => {
181         OrderId => [ $args->{OrderId} ],
182     };
183 }
184
185 sub resp_CANCEL {
186    my ($self, $resphash, $reqhash) = (shift, shift);
187    return "invalid cancel response" unless defined $resphash->{OrderResponse};
188    return $resphash->{OrderResponse};
189 }
190
191 sub req_ORDERSTATUS {
192    my ($self, $args) = (shift, shift);
193
194    return "ORDERSTATUS is supported by OrderId only" 
195     if defined $args->{PhoneNumber} || !defined $args->{OrderId};
196
197     return OrderStatus => {
198         OrderId => [ $args->{OrderId} ],
199     };
200 }
201
202 sub resp_ORDERSTATUS {
203    my ($self, $resphash, $reqhash) = (shift, shift);
204    return "invalid order response" unless defined $resphash->{OrderResponse};
205    return $resphash->{OrderResponse};
206 }
207
208 sub req_ACCOUNTSTATUSCHANGE {
209    my ($self, $args) = (shift, shift);
210    return "invalid account status change request" unless defined $args->{type} 
211     && defined $args->{DSLServiceId} && defined $args->{DSLPhoneNumber};
212
213    return AccountStatusChange => {
214        type => $args->{type},
215         DSLPhoneNumber => [ $args->{DSLPhoneNumber} ],
216         DSLServiceId => [ $args->{DSLServiceId} ],
217     };
218 }
219
220 sub resp_ACCOUNTSTATUSCHANGE {
221    my ($self, $resphash, $reqhash) = (shift, shift);
222     return "invalid account status change response" 
223         unless defined $resphash->{AccountStatusChangeResponse}
224         && defined $resphash->{AccountStatusChangeResponse}->{Customer};
225     return $resphash->{AccountStatusChangeResponse}->{Customer};
226 }
227
228 sub req_CUSTOMERLOOKUP {
229    my ($self, $args) = (shift, shift);
230    return "invalid customer lookup request" unless defined $args->{PhoneNumber};
231    return CustomerLookup => {
232         PhoneNumber => [ $args->{PhoneNumber} ],
233    };
234 }
235
236 sub resp_CUSTOMERLOOKUP {
237    my ($self, $resphash, $reqhash) = (shift, shift);
238    return "invalid customer lookup response" 
239     unless defined $resphash->{CustomerLookupResponse}
240         && defined $resphash->{CustomerLookupResponse}->{Customer};
241    return $resphash->{CustomerLookupResponse}->{Customer};
242 }
243
244 sub req_PASSWORDCHANGE {
245    my ($self, $args) = (shift, shift);
246    return "invalid arguments to PASSWORDCHANGE" 
247         unless defined $args->{DSLPhoneNumber} && defined $args->{NewPassword};
248
249    return PasswordChange => {
250         DSLPhoneNumber => [ $args->{DSLPhoneNumber} ],
251         NewPassword => [ $args->{NewPassword} ],
252    };
253 }
254
255 sub resp_PASSWORDCHANGE {
256    my ($self, $resphash, $reqhash) = (shift, shift);
257    return "invalid change password response"
258       unless defined $resphash->{ChangePasswordResponse}
259           && defined $resphash->{ChangePasswordResponse}->{Customer};
260    $resphash->{ChangePasswordResponse}->{Customer};
261 }
262
263 sub req_PREQUAL {
264    my ($self, $args) = (shift, shift);
265    return PreQual => { 
266         Address =>  [ { ( 
267             map { $_ => [ $args->{$_} ]  }  
268                 qw( AddressLine1 AddressUnitType AddressUnitValue AddressCity 
269                     AddressState ZipCode LocationType Country ) 
270             )  } ],
271         ( map { $_ => [ $args->{$_} ] } qw( PhoneNumber RequestClientIP ) ),
272         CheckNetworks => [ {
273             Network => [ split(',',$args->{CheckNetworks}) ]
274         } ],
275        };
276 }
277
278 sub resp_PREQUAL {
279     my ($self, $resphash, $reqhash) = (shift, shift);
280     return "invalid prequal response" unless defined $resphash->{PreQualResponse};
281     return $resphash->{PreQualResponse};
282 }
283
284 sub AUTOLOAD {
285     my $self = shift;
286    
287     $AUTOLOAD =~ /(^|::)(\w+)$/ or die "invalid AUTOLOAD: $AUTOLOAD";
288     my $cmd = $2;
289     return if $cmd eq 'DESTROY';
290
291     my $reqsub = "req_$cmd";
292     my $respsub = "resp_$cmd";
293     die "invalid request type $cmd" 
294         unless defined &$reqsub && defined &$respsub;
295
296     my $reqargs = shift;
297
298     my $xs = new Net::Ikano::XMLUtil(RootName => undef, SuppressEmpty => 1 );
299     my $reqhash = {
300             OsirisRequest   => {
301                 type    => $cmd,
302                 keyid   => $self->{keyid},
303                 username => $self->{username},
304                 password => $self->{password},
305                 version => $API_VERSION,
306                 xmlns   => "$SCHEMA_ROOT/osirisrequest.xsd",
307                 $self->$reqsub($reqargs),
308             }
309         };
310
311
312     my $reqxml = "<?xml version=\"1.0\"?>\n".$xs->XMLout($reqhash, NoSort => 1);
313    
314     # XXX: validate against their schema to ensure we're not sending invalid XML?
315
316     warn "DEBUG REQUEST\n\tHASH:\n ".Dumper($reqhash)."\n\tXML:\n $reqxml \n\n"
317         if $self->{debug};
318     
319     my $ua = LWP::UserAgent->new;
320
321     return "posting disabled for testing" if $self->{reqpreviewonly};
322
323     my $resp = $ua->post($URL, Content_Type => 'text/xml', Content => $reqxml);
324     return "invalid HTTP response from Ikano: " . $resp->status_line
325         unless $resp->is_success;
326     my $respxml = $resp->decoded_content;
327
328     $xs = new Net::Ikano::XMLUtil(RootName => undef, SuppressEmpty => '',
329         ForceArray => [ 'Address', 'Product', 'StaticIp', 'OrderNotes' ] );
330     my $resphash = $xs->XMLin($respxml);
331
332     warn "DEBUG RESPONSE\n\tHASH:\n ".Dumper($resphash)."\n\tXML:\n $respxml"
333         if $self->{debug};
334
335     # XXX: validate against their schema to ensure they didn't send us invalid XML?
336
337     return "invalid response received from Ikano" 
338         unless defined $resphash->{responseid} && defined $resphash->{version}
339             && defined $resphash->{type};
340
341     return "FAILURE response received from Ikano: " 
342         . $resphash->{FailureResponse}->{FailureMessage} 
343         if $resphash->{type} eq 'FAILURE';
344
345     my $validRespTypes = {
346         'PREQUAL' => qw( PREQUAL ),
347         'ORDERSTATUS' => qw( ORDERSTATUS ),
348         'ORDER' => qw( NEWORDER CHANGEORDER CANCELORDER ),
349         'CANCEL' => qw( ORDERCANCEL ),
350         'PASSWORDCHANGE' => qw( PASSWORDCHANGE ),
351         'ACCOUNTSTATUSCHANGE' => qw( ACCOUNTSTATUSCHANGE ),
352         'CUSTOMERLOOKUP' => qw( CUSTOMERLOOKUP ),
353     };
354
355     return "invalid response type ".$resphash->{type}." for request type $cmd"
356         unless grep( $_ eq $resphash->{type}, $validRespTypes->{$cmd});
357
358     return $self->$respsub($resphash,$reqhash);
359 }
360
361
362 =head1 AUTHOR
363
364 Erik Levinson, C<< <levinse at freeside.biz> >>
365
366 =head1 BUGS
367
368 Please report any bugs or feature requests to C<bug-net-ikano at rt.cpan.org>, or through
369 the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-Ikano>.  I will be notified, and then you'll
370 automatically be notified of progress on your bug as I make changes.
371
372 =head1 SUPPORT
373
374 You can find documentation for this module with the perldoc command.
375
376     perldoc Net::Ikano
377
378 You can also look for information at:
379
380 =over 4
381
382 =item * RT: CPAN's request tracker
383
384 L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net-Ikano>
385
386 =item * AnnoCPAN: Annotated CPAN documentation
387
388 L<http://annocpan.org/dist/Net-Ikano>
389
390 =item * CPAN Ratings
391
392 L<http://cpanratings.perl.org/d/Net-Ikano>
393
394 =item * Search CPAN
395
396 L<http://search.cpan.org/dist/Net-Ikano>
397
398 =back
399
400 =head1 ACKNOWLEDGEMENTS
401
402 This module was developed by Freeside Internet Services, Inc.
403 If you need a complete, open-source web-based application to manage your
404 customers, billing and trouble ticketing, please visit http://freeside.biz/
405
406 =head1 COPYRIGHT & LICENSE
407
408 Copyright 2010 Freeside Internet Services, Inc.
409 All rights reserved.
410
411 This program is free software; you can redistribute it and/or modify it
412 under the same terms as Perl itself.
413
414 =cut
415
416 1;
417