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