1 package FS::part_export::ikano;
3 use vars qw(@ISA %info %orderType %orderStatus %loopType $DEBUG $me);
5 use Date::Format qw( time2str );
6 use Date::Parse qw( str2time );
7 use FS::Record qw(qsearch qsearchs);
12 @ISA = qw(FS::part_export);
14 $me= '[' . __PACKAGE__ . ']';
16 tie my %options, 'Tie::IxHash',
17 'keyid' => { label=>'Ikano keyid' },
18 'username' => { label=>'Ikano username',
21 'password' => { label=>'Ikano password' },
22 'check_networks' => { label => 'Check Networks',
23 default => 'ATT,BELLCA',
29 'desc' => 'Provision DSL to Ikano',
30 'options' => \%options,
32 Requires installation of
33 <a href="http://search.cpan.org/dist/Net-Ikano">Net::Ikano</a> from CPAN.
37 %orderType = ( 'N' => 'NEW', 'X' => 'CANCEL', 'C' => 'CHANGE' );
38 %orderStatus = ('N' => 'NEW',
43 %loopType = ( '' => 'Line-share', '0' => 'Standalone' );
45 sub rebless { shift; }
48 # we distinguish between invalid new data (return error) versus data that
49 # has legitimately changed (may eventually execute hooks; now just update)
51 my($self, $svc_dsl) = (shift, shift);
52 my $result = $self->valid_order($svc_dsl,'pull');
53 return $result unless $result eq '';
55 $result = $self->ikano_command('ORDERSTATUS',
56 { OrderId => $svc_dsl->vendor_order_id } );
57 return $result unless ref($result); # scalar (string) is an error
59 # now we're getting an OrderResponse which should have one Order in it
60 warn "$me pull OrderResponse hash:\n".Dumper($result) if $DEBUG;
62 return 'Invalid order response' unless defined $result->{'Order'};
63 $result = $result->{'Order'};
65 return 'No order id or status returned'
66 unless defined $result->{'Status'} && defined $result->{'OrderId'};
68 # update this always (except in the above cases), even if error later
69 $svc_dsl->last_pull((time));
70 local $FS::svc_Common::noexport_hack = 1;
71 local $FS::UID::AutoCommit = 1;
72 my $error = $svc_dsl->replace;
73 return 'Error updating last pull time' if $error;
75 # let's compare what we have to what we got back...
77 # current assumptions of what won't change (from their side):
78 # vendor_order_id, vendor_qual_id, vendor_order_type, pushed, monitored,
79 # last_pull, address (from qual), contact info, ProductCustomId
84 my $new_order_status = $self->orderstatus_long2short($result->{'Status'});
85 return 'Invalid new status' if $new_order_status eq '';
86 if($svc_dsl->vendor_order_status ne $new_order_status) {
87 if($new_order_status eq 'X' || $new_order_status eq 'C') {
88 $svc_dsl->monitored('');
90 $svc_dsl->vendor_order_status($new_order_status);
94 # 2. fields we don't care much about
95 my %justUpdate = ( 'first' => 'FirstName',
97 'company' => 'CompanyName',
98 'username' => 'Username',
99 'password' => 'Password' );
101 while (($fsf, $ikanof) = each %justUpdate) {
102 if ( $result->{$ikanof} ne $svc_dsl->$fsf ) {
103 $svc_dsl->$fsf($result->{$ikanof});
108 # let's look inside the <Product> response element
109 my @product = $result->{'Product'};
110 return 'Invalid number of products on order' if scalar(@product) != 1;
111 my $product = $result->{'Product'}[0];
114 if($svc_dsl->loop_type eq '') { # line-share
115 # TN may change only if sub changes it and
116 # New or Change order in Completed status
117 my $tn = $product->{'PhoneNumber'};
118 if($tn ne $svc_dsl->phonenum) {
119 if( ($svc_dsl->vendor_order_type eq 'N'
120 || $svc_dsl->vendor_order_type eq 'C')
121 && $svc_dsl->vendor_order_status eq 'C' ) {
122 $svc_dsl->phonenum($tn);
125 else { return 'TN has changed in an invalid state'; }
128 elsif($svc_dsl->loop_type eq '0') { # dry loop
129 return 'Invalid PhoneNumber value for a dry loop'
130 if $product->{'PhoneNumber'} ne 'STANDALONE';
131 my $tn = $product->{'VirtualPhoneNumber'};
132 if($tn ne $svc_dsl->phonenum) {
133 if( ($svc_dsl->vendor_order_type eq 'N'
134 || $svc_dsl->vendor_order_type eq 'C')
135 && $svc_dsl->vendor_order_status ne 'C'
136 && $svc_dsl->vendor_order_status ne 'X') {
137 $svc_dsl->phonenum($tn);
143 # 4. desired_due_date - may change if manually changed
144 if($svc_dsl->vendor_order_type eq 'N'
145 || $svc_dsl->vendor_order_type eq 'C'){
146 my $f = str2time($product->{'DateToOrder'});
147 return 'Invalid DateToOrder' unless $f;
148 if ( $svc_dsl->desired_due_date != $f ) {
149 $svc_dsl->desired_due_date($f);
152 # XXX: optionally sync back to start_date or whatever...
154 elsif($svc_dsl->vendor_order_type eq 'X'){
155 my $f = str2time($product->{'DateToDisconnect'});
156 return 'Invalid DateToDisconnect' unless $f;
157 if ( $svc_dsl->desired_due_date != $f ) {
158 $svc_dsl->desired_due_date($f);
161 # XXX: optionally sync back to expire or whatever...
165 if($svc_dsl->vendor_order_type eq 'N'
166 || $svc_dsl->vendor_order_type eq 'C') {
167 my $f = str2time($product->{'ActivationDate'});
168 if($svc_dsl->vendor_order_status ne 'N') {
169 return 'Invalid ActivationDate' unless $f;
170 if( $svc_dsl->due_date != $f ) {
171 $svc_dsl->due_date($f);
176 # Ikano API does not implement the proper disconnect date,
177 # so we can't do anything about it
179 # 6. staticips - for now just comma-separate them
180 my @statics = $result->{'StaticIps'};
183 # 7. notes - put them into the common format:
184 # "by" = 0 for Ikano; 1 for ISP
185 # "priority" = 1 for high priority; 0 otherwise
186 my @notes = $result->{'OrderNotes'};
189 if($wechanged) { # you can't check ->modified, the replace above screws it
190 # noexport_hack and AutoCommit are set correctly above
191 $result = $svc_dsl->replace;
192 return 'Error updating DSL data' if $result;
206 sub loop_type_long { # sub, not a method
207 my($svc_dsl) = (shift);
208 return $loopType{$svc_dsl->loop_type};
212 my($self,$svc_dsl) = (shift,shift);
213 return "Ikano ".$orderType{$svc_dsl->vendor_order_type}." order #"
214 . $svc_dsl->vendor_order_id . " (Status: "
215 . $orderStatus{$svc_dsl->vendor_order_status} . ")";
219 my( $self, $command, $args ) = @_;
221 eval "use Net::Ikano;";
224 my $ikano = Net::Ikano->new(
225 'keyid' => $self->option('keyid'),
226 'username' => $self->option('username'),
227 'password' => $self->option('password'),
229 #'reqpreviewonly' => 1,
232 $ikano->$command($args);
236 my( $self, $svc_dsl, $action ) = (shift, shift, shift);
238 warn "$me valid_order action=$action svc_dsl:\n". Dumper($svc_dsl) if $DEBUG;
240 # common to all order types/status/loop_type
241 my $error = !($svc_dsl->desired_due_date
242 && defined $orderType{$svc_dsl->vendor_order_type}
245 && defined $svc_dsl->loop_type
246 && $svc_dsl->vendor_qual_id
248 return 'Missing or invalid order data' if $error;
250 return 'Package does not have an external id configured'
251 if $svc_dsl->cust_svc->cust_pkg->part_pkg->options('externalid',1) eq '';
253 return 'No valid qualification for this order'
254 unless qsearch( 'qual', { 'vendor_qual_id' => $svc_dsl->vendor_qual_id });
256 # now go by order type
257 # weird ifs & long lines for readability and ease of understanding - don't change
258 if($svc_dsl->vendor_order_type eq 'N') {
259 if($svc_dsl->pushed) {
260 $error = !($action eq 'pull'
261 && length($svc_dsl->vendor_order_id) > 0
262 && length($svc_dsl->vendor_order_status) > 0
264 return 'Invalid order data' if $error;
266 else { # unpushed New order - cannot do anything other than push it
267 $error = !($action eq 'insert'
268 && length($svc_dsl->vendor_order_id) < 1
269 && length($svc_dsl->vendor_order_status) < 1
270 && ( ($svc_dsl->phonenum eq '' && $svc_dsl->loop_type eq '0') # dry
271 || ($svc_dsl->phonenum ne '' && $svc_dsl->loop_type eq '') # line-share
274 return 'Invalid order data' if $error;
277 elsif($svc_dsl->vendor_order_type eq 'X') {
279 elsif($svc_dsl->vendor_order_type eq 'C') {
286 my ($self,$vendor_qual_id,$ProductCustomId) = (shift,shift,shift);
287 my $qual = qsearchs( 'qual', { 'vendor_qual_id' => $vendor_qual_id });
288 return '' unless $qual;
289 my %qual_options = $qual->options;
290 while (($optionname, $optionvalue) = each %qual_options) {
291 if ( $optionname =~ /^ikano_Network_(\d+)_ProductGroup_(\d+)_Product_(\d+)_ProductCustomId$/
292 && $optionvalue eq $ProductCustomId ) {
294 my $productgroup = $2;
295 return $qual->option("ikano_Network_".$network."_ProductGroup_".$productgroup."_TermsId");
301 sub orderstatus_long2short {
302 my ($self,$order_status) = (shift,shift);
303 my %rorderStatus = reverse %orderStatus;
304 return $rorderStatus{$order_status} if exists $rorderStatus{$order_status};
309 my( $self, $svc_dsl ) = (shift, shift);
311 my $result = $self->valid_order($svc_dsl,'insert');
312 return $result unless $result eq '';
314 my $isp_chg = $svc_dsl->isp_chg eq 'Y' ? 'YES' : 'NO';
315 my $contactTN = $svc_dsl->cust_svc->cust_pkg->cust_main->daytime;
316 $contactTN =~ s/[^0-9]//g;
318 my $ProductCustomId = $svc_dsl->cust_svc->cust_pkg->part_pkg->option('externalid',1);
322 ProductCustomId => $ProductCustomId,
323 TermsId => $self->qual2termsid($svc_dsl->vendor_qual_id,$ProductCustomId),
324 DSLPhoneNumber => $svc_dsl->loop_type eq '0' ? 'STANDALONE'
325 : $svc_dsl->phonenum,
326 Password => $svc_dsl->password,
327 PrequalId => $svc_dsl->vendor_qual_id,
328 CompanyName => $svc_dsl->company,
329 FirstName => $svc_dsl->first,
330 LastName => $svc_dsl->last,
332 ContactMethod => 'PHONE',
333 ContactPhoneNumber => $contactTN,
334 ContactEmail => 'x@x.xx',
336 DateToOrder => time2str("%Y-%m-%d",$svc_dsl->desired_due_date),
337 RequestClientIP => '127.0.0.1',
338 IspChange => $isp_chg,
339 IspPrevious => $isp_chg eq 'YES' ? $svc_dsl->isp_prev : '',
340 CurrentProvider => $isp_chg eq 'NO' ? $svc_dsl->isp_prev : '',
343 $result = $self->ikano_command('ORDER',$args);
344 return $result unless ref($result); # scalar (string) is an error
346 # now we're getting an OrderResponse which should have one Order in it
347 warn "$me _export_insert OrderResponse hash:\n".Dumper($result) if $DEBUG;
349 return 'Invalid order response' unless defined $result->{'Order'};
350 $result = $result->{'Order'};
352 return 'No order id or status returned'
353 unless defined $result->{'Status'} && defined $result->{'OrderId'};
355 $svc_dsl->pushed(time);
356 $svc_dsl->last_pull((time)+1);
357 $svc_dsl->vendor_order_id($result->{'OrderId'});
358 $svc_dsl->vendor_order_status($self->orderstatus_long2short($result->{'Status'}));
359 $svc_dsl->username($result->{'Username'});
360 local $FS::svc_Common::noexport_hack = 1;
361 local $FS::UID::AutoCommit = 0;
362 $result = $svc_dsl->replace;
363 return 'Error setting DSL fields' if $result;
367 sub _export_replace {
368 my( $self, $new, $old ) = (shift, shift, shift);
373 my( $self, $svc_dsl ) = (shift, shift);
377 sub _export_suspend {
378 my( $self, $svc_dsl ) = (shift, shift);
382 sub _export_unsuspend {
383 my( $self, $svc_dsl ) = (shift, shift);