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