Add Switch dependency (closes: rt.cpan.org #110112)
[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.02
16
17 =cut
18
19 our $VERSION = '0.02';
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', 'Network', '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     return "invalid response type ".$resphash->{type}." for request type $cmd"
354         unless ( $cmd eq $resphash->{type} 
355                 || ($cmd eq 'ORDER' && $resphash->{type} =~ /(NEW|CHANGE|CANCEL)ORDER/ )
356                 || ($cmd eq "CANCEL" && $resphash->{type} eq "ORDERCANCEL")
357              );
358
359     return $self->$respsub($resphash,$reqhash);
360 }
361
362
363 =head1 AUTHOR
364
365 Original Author: Erik Levinson
366
367 Current Maintainer: Ivan Kohler C<< <ivan-ikano@freeside.biz> >>
368
369 =head1 BUGS
370
371 Please report any bugs or feature requests to C<bug-net-ikano at rt.cpan.org>, or through
372 the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-Ikano>.  I will be notified, and then you'll
373 automatically be notified of progress on your bug as I make changes.
374
375 =head1 SUPPORT
376
377 You can find documentation for this module with the perldoc command.
378
379     perldoc Net::Ikano
380
381 You can also look for information at:
382
383 =over 4
384
385 =item * RT: CPAN's request tracker
386
387 L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net-Ikano>
388
389 =item * AnnoCPAN: Annotated CPAN documentation
390
391 L<http://annocpan.org/dist/Net-Ikano>
392
393 =item * CPAN Ratings
394
395 L<http://cpanratings.perl.org/d/Net-Ikano>
396
397 =item * Search CPAN
398
399 L<http://search.cpan.org/dist/Net-Ikano>
400
401 =back
402
403 =head1 COPYRIGHT & LICENSE
404
405 Copyright 2010-2011 Freeside Internet Services, Inc.
406 All rights reserved.
407
408 This program is free software; you can redistribute it and/or modify it
409 under the same terms as Perl itself.
410
411 =head1 ADVERTISEMENT
412
413 Need a complete, open-source back-office and customer self-service solution?
414 The Freeside software includes support for Ikano integration,
415 invoicing, credit card and electronic check processing, integrated trouble
416 ticketing, and customer signup and self-service web interfaces.
417
418 http://freeside.biz/freeside/
419
420 =cut
421
422 1;
423