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 dbh);
13 @ISA = qw(FS::part_export);
15 $me= '[' . __PACKAGE__ . ']';
17 tie my %options, 'Tie::IxHash',
18 'keyid' => { label=>'Ikano keyid' },
19 'username' => { label=>'Ikano username',
22 'password' => { label=>'Ikano password' },
23 'check_networks' => { label => 'Check Networks',
24 default => 'ATT,BELLCA',
30 'desc' => 'Provision DSL to Ikano',
31 'options' => \%options,
33 Requires installation of
34 <a href="http://search.cpan.org/dist/Net-Ikano">Net::Ikano</a> from CPAN.
38 %orderType = ( 'N' => 'NEW', 'X' => 'CANCEL', 'C' => 'CHANGE' );
39 %orderStatus = ('N' => 'NEW',
44 %loopType = ( '' => 'Line-share', '0' => 'Standalone' );
46 sub rebless { shift; }
49 # we distinguish between invalid new data (return error) versus data that
50 # has legitimately changed (may eventually execute hooks; now just update)
51 # if we do add hooks later, we should work on a copy of svc_dsl and pass
52 # the old and new svc_dsl to the hooks so they know what changed
54 # current assumptions of what won't change (from their side):
55 # vendor_order_id, vendor_qual_id, vendor_order_type, pushed, monitored,
56 # last_pull, address (from qual), contact info, ProductCustomId
58 my($self, $svc_dsl) = (shift, shift);
59 my $result = $self->valid_order($svc_dsl,'pull');
60 return $result unless $result eq '';
62 $result = $self->ikano_command('ORDERSTATUS',
63 { OrderId => $svc_dsl->vendor_order_id } );
64 return $result unless ref($result); # scalar (string) is an error
66 # now we're getting an OrderResponse which should have one Order in it
67 warn "$me pull OrderResponse hash:\n".Dumper($result) if $DEBUG;
69 return 'Invalid order response' unless defined $result->{'Order'};
70 $result = $result->{'Order'};
72 return 'No order id or status returned'
73 unless defined $result->{'Status'} && defined $result->{'OrderId'};
75 local $SIG{HUP} = 'IGNORE';
76 local $SIG{INT} = 'IGNORE';
77 local $SIG{QUIT} = 'IGNORE';
78 local $SIG{TERM} = 'IGNORE';
79 local $SIG{TSTP} = 'IGNORE';
80 local $SIG{PIPE} = 'IGNORE';
82 my $oldAutoCommit = $FS::UID::AutoCommit;
83 local $FS::UID::AutoCommit = 0;
87 my $new_order_status = $self->orderstatus_long2short($result->{'Status'});
88 return 'Invalid new status' if $new_order_status eq '';
89 if($svc_dsl->vendor_order_status ne $new_order_status) {
90 $svc_dsl->monitored('')
91 if ($new_order_status eq 'X' || $new_order_status eq 'C');
92 $svc_dsl->vendor_order_status($new_order_status);
95 # 2. fields we don't care much about
96 my %justUpdate = ( 'first' => 'FirstName',
98 'company' => 'CompanyName',
99 'username' => 'Username',
100 'password' => 'Password' );
102 while (($fsf, $ikanof) = each %justUpdate) {
103 $svc_dsl->$fsf($result->{$ikanof})
104 if $result->{$ikanof} ne $svc_dsl->$fsf;
107 # let's look inside the <Product> response element
108 my @product = $result->{'Product'};
109 return 'Invalid number of products on order' if scalar(@product) != 1;
110 my $product = $result->{'Product'}[0];
113 if($svc_dsl->loop_type eq '') { # line-share
114 # TN may change only if sub changes it and New or Change order in Completed status
115 my $tn = $product->{'PhoneNumber'};
116 if($tn ne $svc_dsl->phonenum) {
117 if( ($svc_dsl->vendor_order_type eq 'N'
118 || $svc_dsl->vendor_order_type eq 'C')
119 && $svc_dsl->vendor_order_status eq 'C' ) {
120 $svc_dsl->phonenum($tn);
122 else { return 'TN has changed in an invalid state'; }
125 elsif($svc_dsl->loop_type eq '0') { # dry loop
126 # TN may change only if it's assigned while a New or Change order is in progress
127 return 'Invalid PhoneNumber value for a dry loop'
128 if $product->{'PhoneNumber'} ne 'STANDALONE';
129 my $tn = $product->{'VirtualPhoneNumber'};
130 if($tn ne $svc_dsl->phonenum) {
131 if( ($svc_dsl->vendor_order_type eq 'N'
132 || $svc_dsl->vendor_order_type eq 'C')
133 && $svc_dsl->vendor_order_status ne 'C'
134 && $svc_dsl->vendor_order_status ne 'X') {
135 $svc_dsl->phonenum($tn);
137 else { return 'TN has changed in an invalid state'; }
141 # 4. desired_due_date - may change if manually changed
142 if($svc_dsl->vendor_order_type eq 'N'
143 || $svc_dsl->vendor_order_type eq 'C'){
144 my $f = str2time($product->{'DateToOrder'});
145 return 'Invalid DateToOrder' unless $f;
146 $svc_dsl->desired_due_date($f) if $svc_dsl->desired_due_date != $f;
147 # XXX: optionally sync back to start_date or whatever...
149 elsif($svc_dsl->vendor_order_type eq 'X'){
150 my $f = str2time($product->{'DateToDisconnect'});
151 return 'Invalid DateToDisconnect' unless $f;
152 $svc_dsl->desired_due_date($f) if $svc_dsl->desired_due_date != $f;
153 # XXX: optionally sync back to expire or whatever...
157 if($svc_dsl->vendor_order_type eq 'N'
158 || $svc_dsl->vendor_order_type eq 'C') {
159 my $f = str2time($product->{'ActivationDate'});
160 if($svc_dsl->vendor_order_status ne 'N') {
161 return 'Invalid ActivationDate' unless $f;
162 $svc_dsl->due_date($f) if $svc_dsl->due_date != $f;
165 # Ikano API does not implement the proper disconnect date,
166 # so we can't do anything about it
168 # 6. staticips - for now just comma-separate them
169 my @statics = $result->{'StaticIps'};
172 # 7. notes - put them into the common format and compare
173 my $tnotes = $result->{'OrderNotes'};
174 my @tnotes = @$tnotes;
175 my @inotes = (); # all Ikano OrderNotes as FS::dsl_note objects
176 my $notesChanged = 0;
177 foreach $tnote ( @tnotes ) {
178 my $inote = $self->ikano2fsnote($tnote,$svc_dsl->svcnum);
179 return 'Cannot parse note' unless ref($inote);
180 push @inotes, $inote;
182 my @onotes = $svc_dsl->notes;
183 # assume notes we already have don't change & no notes added from our side
184 # so using the horrible code below just find what we're missing and add it
186 foreach $inote ( @inotes ) {
188 foreach $onote ( @onotes ) {
189 if($onote->date == $inote->date && $onote->note eq $inote->note) {
194 $error = $inote->insert unless ( $found );
196 $dbh->rollback if $oldAutoCommit;
197 return "Cannot add note: $error";
201 $svc_dsl->last_pull((time));
202 local $FS::svc_Common::noexport_hack = 1;
203 $error = $svc_dsl->replace;
205 $dbh->rollback if $oldAutoCommit;
206 return "Cannot update DSL data: $error";
209 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
215 my($self,$n,$svcnum) = (shift,shift,shift);
216 my @ikanoRequired = qw( HighPriority StaffId Date Text CompanyStaffId );
217 return '' unless defined $n->{'HighPriority'}
218 && defined $n->{'StaffId'}
219 && defined $n->{'CompanyStaffId'}
220 && defined $n->{'Date'}
221 && defined $n->{'Text'}
224 $by = "Ikano" if $n->{'CompanyStaffId'} == -1 && $n->{'StaffId'} != -1;
225 $by = "Us" if $n->{'StaffId'} == -1 && $n->{'CompanyStaffId'} != -1;
227 $fsnote = new FS::dsl_note( {
230 'priority' => $n->{'HighPriority'} eq 'false' ? 'N' : 'H',
231 'date' => int(str2time($n->{'Date'})),
232 'note' => $n->{'Text'},
241 my($self,$svc_dsl) = (shift,shift);
242 my $conf = new FS::Conf;
243 my $date_format = $conf->config('date_format') || '%m/%d/%Y';
244 my @notes = $svc_dsl->notes;
245 my $html = '<TABLE border="1" cellspacing="2" cellpadding="2" id="dsl_notes">
246 <TR><TH>Date</TH><TH>By</TH><TH>Priority</TH><TH>Note</TH></TR>';
247 foreach $note ( @notes ) {
249 <TD>".time2str("$date_format %H:%M",$note->date)."</TD>
250 <TD>".$note->by."</TD>
251 <TD>". ($note->priority eq 'N' ? 'Normal' : 'High') ."</TD>
252 <TD>".$note->note."</TD></TR>";
258 sub loop_type_long { # sub, not a method
259 my($svc_dsl) = (shift);
260 return $loopType{$svc_dsl->loop_type};
264 my($self,$svc_dsl) = (shift,shift);
265 return "Ikano ".$orderType{$svc_dsl->vendor_order_type}." order #"
266 . $svc_dsl->vendor_order_id . " (Status: "
267 . $orderStatus{$svc_dsl->vendor_order_status} . ")";
271 my( $self, $command, $args ) = @_;
273 eval "use Net::Ikano;";
276 my $ikano = Net::Ikano->new(
277 'keyid' => $self->option('keyid'),
278 'username' => $self->option('username'),
279 'password' => $self->option('password'),
281 #'reqpreviewonly' => 1,
284 $ikano->$command($args);
288 my( $self, $svc_dsl, $action ) = (shift, shift, shift);
290 warn "$me valid_order action=$action svc_dsl:\n". Dumper($svc_dsl) if $DEBUG;
292 # common to all order types/status/loop_type
293 my $error = !($svc_dsl->desired_due_date
294 && defined $orderType{$svc_dsl->vendor_order_type}
297 && defined $svc_dsl->loop_type
298 && $svc_dsl->vendor_qual_id
300 return 'Missing or invalid order data' if $error;
302 return 'Package does not have an external id configured'
303 if $svc_dsl->cust_svc->cust_pkg->part_pkg->options('externalid',1) eq '';
305 return 'No valid qualification for this order'
306 unless qsearch( 'qual', { 'vendor_qual_id' => $svc_dsl->vendor_qual_id });
308 # now go by order type
309 # weird ifs & long lines for readability and ease of understanding - don't change
310 if($svc_dsl->vendor_order_type eq 'N') {
311 if($svc_dsl->pushed) {
312 $error = !($action eq 'pull'
313 && length($svc_dsl->vendor_order_id) > 0
314 && length($svc_dsl->vendor_order_status) > 0
316 return 'Invalid order data' if $error;
318 else { # unpushed New order - cannot do anything other than push it
319 $error = !($action eq 'insert'
320 && length($svc_dsl->vendor_order_id) < 1
321 && length($svc_dsl->vendor_order_status) < 1
322 && ( ($svc_dsl->phonenum eq '' && $svc_dsl->loop_type eq '0') # dry
323 || ($svc_dsl->phonenum ne '' && $svc_dsl->loop_type eq '') # line-share
326 return 'Invalid order data' if $error;
329 elsif($svc_dsl->vendor_order_type eq 'X') {
331 elsif($svc_dsl->vendor_order_type eq 'C') {
338 my ($self,$vendor_qual_id,$ProductCustomId) = (shift,shift,shift);
339 my $qual = qsearchs( 'qual', { 'vendor_qual_id' => $vendor_qual_id });
340 return '' unless $qual;
341 my %qual_options = $qual->options;
342 while (($optionname, $optionvalue) = each %qual_options) {
343 if ( $optionname =~ /^ikano_Network_(\d+)_ProductGroup_(\d+)_Product_(\d+)_ProductCustomId$/
344 && $optionvalue eq $ProductCustomId ) {
346 my $productgroup = $2;
347 return $qual->option("ikano_Network_".$network."_ProductGroup_".$productgroup."_TermsId");
353 sub orderstatus_long2short {
354 my ($self,$order_status) = (shift,shift);
355 my %rorderStatus = reverse %orderStatus;
356 return $rorderStatus{$order_status} if exists $rorderStatus{$order_status};
361 my( $self, $svc_dsl ) = (shift, shift);
363 my $result = $self->valid_order($svc_dsl,'insert');
364 return $result unless $result eq '';
366 my $isp_chg = $svc_dsl->isp_chg eq 'Y' ? 'YES' : 'NO';
367 my $contactTN = $svc_dsl->cust_svc->cust_pkg->cust_main->daytime;
368 $contactTN =~ s/[^0-9]//g;
370 my $ProductCustomId = $svc_dsl->cust_svc->cust_pkg->part_pkg->option('externalid',1);
374 ProductCustomId => $ProductCustomId,
375 TermsId => $self->qual2termsid($svc_dsl->vendor_qual_id,$ProductCustomId),
376 DSLPhoneNumber => $svc_dsl->loop_type eq '0' ? 'STANDALONE'
377 : $svc_dsl->phonenum,
378 Password => $svc_dsl->password,
379 PrequalId => $svc_dsl->vendor_qual_id,
380 CompanyName => $svc_dsl->company,
381 FirstName => $svc_dsl->first,
382 LastName => $svc_dsl->last,
384 ContactMethod => 'PHONE',
385 ContactPhoneNumber => $contactTN,
386 ContactEmail => 'x@x.xx',
388 DateToOrder => time2str("%Y-%m-%d",$svc_dsl->desired_due_date),
389 RequestClientIP => '127.0.0.1',
390 IspChange => $isp_chg,
391 IspPrevious => $isp_chg eq 'YES' ? $svc_dsl->isp_prev : '',
392 CurrentProvider => $isp_chg eq 'NO' ? $svc_dsl->isp_prev : '',
395 $result = $self->ikano_command('ORDER',$args);
396 return $result unless ref($result); # scalar (string) is an error
398 # now we're getting an OrderResponse which should have one Order in it
399 warn "$me _export_insert OrderResponse hash:\n".Dumper($result) if $DEBUG;
401 return 'Invalid order response' unless defined $result->{'Order'};
402 $result = $result->{'Order'};
404 return 'No order id or status returned'
405 unless defined $result->{'Status'} && defined $result->{'OrderId'};
407 $svc_dsl->pushed(time);
408 $svc_dsl->last_pull((time)+1);
409 $svc_dsl->vendor_order_id($result->{'OrderId'});
410 $svc_dsl->vendor_order_status($self->orderstatus_long2short($result->{'Status'}));
411 $svc_dsl->username($result->{'Username'});
412 local $FS::svc_Common::noexport_hack = 1;
413 local $FS::UID::AutoCommit = 0;
414 $result = $svc_dsl->replace;
415 return 'Error setting DSL fields' if $result;
419 sub _export_replace {
420 my( $self, $new, $old ) = (shift, shift, shift);
425 my( $self, $svc_dsl ) = (shift, shift);
429 sub _export_suspend {
430 my( $self, $svc_dsl ) = (shift, shift);
434 sub _export_unsuspend {
435 my( $self, $svc_dsl ) = (shift, shift);