switch to Digest::SHA on 2.3
[freeside.git] / FS / FS / ClientAPI / Bulk.pm
1 package FS::ClientAPI::Bulk;
2
3 use strict;
4
5 use vars qw( $DEBUG $cache );
6 use Date::Parse;
7 use FS::Record qw( qsearchs );
8 use FS::Conf;
9 use FS::ClientAPI_SessionCache;
10 use FS::cust_main;
11 use FS::cust_pkg;
12 use FS::cust_svc;
13 use FS::svc_acct;
14 use FS::svc_external;
15 use FS::cust_recon;
16 use Data::Dumper;
17
18 $DEBUG = 1;
19
20 sub _cache {
21   $cache ||= new FS::ClientAPI_SessionCache ( {
22                'namespace' => 'FS::ClientAPI::Agent', #yes, share session_ids
23              } );
24 }
25
26 sub _izoom_ftp_row_fixup {
27   my $hash = shift;
28
29   my @addr_fields = qw( address1 address2 city state zip );
30   my @fields = ( qw( agent_custid username _password first last ),
31                  @addr_fields,
32                  map { "ship_$_" } @addr_fields );
33
34   $hash->{$_} =~ s/[&\/\*'"]/_/g foreach @fields;
35
36   #$hash->{action} = '' if $hash->{action} eq 'R'; #unsupported for ftp
37
38   $hash->{refnum} = 1;  #ahem
39   $hash->{country} = 'US';
40   $hash->{ship_country} = 'US';
41   $hash->{payby} = 'LECB';
42   $hash->{payinfo} = $hash->{daytime};
43   $hash->{ship_fax} = '' if ( !$hash->{sms} ||  $hash->{sms} eq 'F' );
44
45   my $has_ship =
46     grep { $hash->{"ship_$_"} &&
47            (! $hash->{$_} || $hash->{"ship_$_"} ne $hash->{$_} )
48          }
49     ( @addr_fields, 'fax' );
50
51   if ( $has_ship )  {
52     foreach ( @addr_fields, qw( first last ) ) {
53       $hash->{"ship_$_"} = $hash->{$_} unless $hash->{"ship_$_"};
54     }
55   }
56     
57   delete $hash->{sms};
58
59   '';
60
61 };
62
63 sub _izoom_ftp_result {
64   my ($hash, $error) = @_;
65   my $cust_main =
66       qsearchs( 'cust_main', { 'agent_custid' => $hash->{agent_custid},
67                                'agentnum'     => $hash->{agentnum}
68                              }
69               );
70
71   my $custnum = $cust_main ? $cust_main->custnum : '';
72   my @response = ( $hash->{action}, $hash->{agent_custid}, $custnum );
73
74   if ( $error ) {
75     push @response, ( 'ERROR', $error );
76   } else {
77     push @response, ( 'OK', 'OK' );
78   }
79
80   join( ',', @response );
81
82 }
83
84 sub _izoom_ftp_badaction {
85   "Invalid action: $_[0] record: @_ ";
86 }
87
88 sub _izoom_soap_row_fixup { _izoom_ftp_row_fixup(@_) };
89
90 sub _izoom_soap_result {
91   my ($hash, $error) = @_;
92
93   if ( $hash->{action} eq 'R' ) {
94     if ( $error ) {
95       return "Please check errors:\n $error"; # odd extra space
96     } else {
97       return join(' ', "Everything ok.", $hash->{pkg}, $hash->{adjourn} );
98     }
99   }
100
101   my $pkg = $hash->{pkg} || $hash->{saved_pkg} || '';
102   if ( $error ) {
103     return join(' ', $hash->{agent_custid}, $error );
104   } else {
105     return join(' ', $hash->{agent_custid}, $pkg, $hash->{adjourn} );
106   }
107
108 }
109
110 sub _izoom_soap_badaction {
111   "Unknown action '$_[13]' ";
112 }
113
114 my %format = (
115   'izoom-ftp'  => {
116                     'fields' => [ qw ( action agent_custid username _password
117                                        daytime ship_fax sms first last
118                                        address1 address2 city state zip
119                                        pkg adjourn ship_address1 ship_address2
120                                        ship_city ship_state ship_zip ) ],
121                     'fixup'  =>  sub { _izoom_ftp_row_fixup(@_) },
122                     'result' =>  sub { _izoom_ftp_result(@_) },
123                     'action' =>  sub { _izoom_ftp_badaction(@_) },
124                   },
125   'izoom-soap' => {
126                     'fields' => [ qw ( agent_custid username _password
127                                        daytime first last address1 address2
128                                        city state zip pkg action adjourn
129                                        ship_fax sms ship_address1 ship_address2
130                                        ship_city ship_state ship_zip ) ],
131                     'fixup'  =>  sub { _izoom_soap_row_fixup(@_) },
132                     'result' =>  sub { _izoom_soap_result(@_) },
133                     'action' =>  sub { _izoom_soap_badaction(@_) },
134                   },
135 );
136
137 sub processrow {
138   my $p = shift;
139
140   my $session = _cache->get($p->{'session_id'})
141     or return { 'error' => "Can't resume session" }; #better error message
142
143   my $conf = new FS::Conf;
144   my $format = $conf->config('selfservice-bulk_format', $session->{agentnum})
145                || 'izoom-soap';
146   my ( @row ) = @{ $p->{row} };
147
148   warn "processrow called with '". join("' '", @row). "'\n" if $DEBUG;
149
150   return { 'error' => "unknown format: $format" }
151     unless exists $format{$format};
152
153   return { 'error' => "Invalid record record length: ". scalar(@row).
154                       "record: @row " #sic
155          }
156     unless scalar(@row) == scalar(@{$format{$format}{fields}});
157
158   my %hash = ( 'agentnum' => $session->{agentnum} );
159   my $error;
160
161   foreach my $field ( @{ $format{ $format }{ fields } } ) {
162     $hash{$field} = shift @row;
163   }
164
165   $error ||= &{ $format{ $format }{ fixup } }( \%hash );
166   
167   # put in the fixup routine?
168   if ( 'R' eq $hash{action} ) {
169     warn "processing reconciliation\n" if $DEBUG;
170     $error ||= process_recon($hash{agentnum}, $hash{agent_custid});
171   } elsif ( 'P' eq $hash{action} ) {
172     #  do nothing
173   } elsif( 'D' eq $hash{action} ) {
174     $hash{promo_pkg} = 'disk-1-'. $session->{agent};
175   } elsif ( 'S' eq $hash{action} ) {
176     $hash{promo_pkg} = 'disk-2-'. $session->{agent};
177     $hash{saved_pkg} = $hash{pkg};
178     $hash{pkg} = '';
179   } else {
180     $error ||= &{ $format{ $format }{ action } }( @row );
181   }
182
183   warn "processing provision\n" if ($DEBUG && !$error && $hash{action} ne 'R');
184   $error ||= provision( %hash ) unless $hash{action} eq 'R';
185
186   my $result =  &{ $format{ $format }{ result } }( \%hash, $error );
187
188   warn "processrow returning '". join("' '", $result, $error). "'\n"
189     if $DEBUG;
190
191   return { 'error' => $error, 'message' => $result };
192
193 }
194
195 sub provision {
196   my %args = ( @_ );
197
198   delete $args{action};
199
200   my $cust_main =
201     qsearchs( 'cust_main',
202               { map { $_ => $args{$_} } qw ( agent_custid agentnum ) },
203             );
204
205   unless ( $cust_main ) {
206     $cust_main = new FS::cust_main { %args };
207     my $error = $cust_main->insert;
208     return $error if $error;
209   }
210
211   my @pkgs = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs;
212   if ( scalar(@pkgs) > 1 ) {
213     return "Invalid account, should not be more then one active package ". #sic
214            "but found: ". scalar(@pkgs). " packages.";
215   }
216
217   my $part_pkg = qsearchs( 'part_pkg', { 'pkg' => $args{pkg} } ) 
218     or return "Unknown pkgpart: $args{pkg}"
219     if $args{pkg};
220
221
222   my $create_package = $args{pkg};        
223   if ( scalar(@pkgs) && $create_package ) {        
224     my $pkg = pop(@pkgs);
225         
226     if ( $part_pkg->pkgpart != $pkg->pkgpart ) {
227       my @cust_bill_pkg = $pkg->cust_bill_pkg();
228       if ( 1 == scalar(@cust_bill_pkg) ) {
229         my $cbp= pop(@cust_bill_pkg);
230         my $cust_bill = $cbp->cust_bill;
231         $cust_bill->delete();  #really?  wouldn't a credit be better?
232       }
233       $pkg->cancel();
234     } else {
235       $create_package = '';
236       $pkg->setfield('adjourn', str2time($args{adjourn}));
237       my $error = $pkg->replace();
238       return $error if $error;
239     }
240   }
241
242   if ( $create_package ) {
243     my $cust_pkg = new FS::cust_pkg ( {
244         'pkgpart' => $part_pkg->pkgpart,
245         'adjourn' => str2time( $args{adjourn} ),
246     } );
247
248     my $svcpart = $part_pkg->svcpart('svc_acct');
249
250     my $svc_acct = new FS::svc_acct ( {
251         'svcpart'   => $svcpart,
252         'username'  => $args{username},
253         '_password' => $args{_password},
254     } );
255
256     my $error = $cust_main->order_pkg( cust_pkg => $cust_pkg,
257                                        svcs     => [ $svc_acct ],
258     );
259     return $error if $error;
260   }
261     
262   if ( $args{promo_pkg} ) {
263     my $part_pkg =
264     qsearchs( 'part_pkg', { 'promo_code' =>  $args{promo_pkg} } )
265       or return "unknown pkgpart: $args{promo_pkg}";
266             
267     my $svcpart = $part_pkg->svcpart('svc_external')
268       or return "unknown svcpart: svc_external";
269
270     my $cust_pkg = new FS::cust_pkg ( {
271       'svcpart' => $svcpart,
272       'pkgpart' => $part_pkg->pkgpart,
273     } );
274
275     my $svc_ext = new FS::svc_external ( { 'svcpart'   => $svcpart } );
276     
277     my $ticket_subject = 'Send setup disk to customer '. $cust_main->custnum;
278     my $error = $cust_main->order_pkg ( cust_pkg       => $cust_pkg,
279                                         svcs           => [ $svc_ext ],
280                                         noexport       => 1,
281                                         ticket_subject => $ticket_subject,
282                                         ticket_queue   => "disk-$args{agentnum}",
283     );
284     return $error if $error;
285   }
286
287   my $error = $cust_main->bill();
288   return $error if $error;
289 }
290
291 sub process_recon {
292   my ( $agentnum, $id ) = @_;
293   my @recs = split /;/, $id;
294   my $err = '';
295   foreach my $rec ( @recs ) {
296     my @record = split /,/, $rec;
297     my $result = process_recon_record(@record, $agentnum);
298     $err .= "$result\n" if $result;
299   }
300   return $err;
301 }
302
303 sub process_recon_record {
304   my ( $agent_custid, $username, $_password, $daytime, $first, $last, $address1, $address2, $city, $state, $zip, $pkg, $adjourn, $agentnum) = @_;
305
306   warn "process_recon_record called with '". join("','", @_). "'\n" if $DEBUG;
307
308   my ($cust_pkg, $package);
309
310   my $cust_main =
311     qsearchs( 'cust_main',
312               { 'agent_custid' => $agent_custid, 'agentnum' => $agentnum },
313             );
314
315   my $comments = '';
316   if ( $cust_main ) {
317     my @cust_pkg = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs;
318     if ( scalar(@cust_pkg) == 1) {
319       $cust_pkg = pop(@cust_pkg);
320       $package = $cust_pkg->part_pkg->pkg;
321       $comments = "$agent_custid wrong package, expected: $pkg found: $package"
322         if ( $pkg ne $package );
323     } else {
324       $comments = "invalid account, should be one active package but found: ".
325                  scalar(@cust_pkg). " packages.";
326     }
327   } else {
328     $comments =
329       "Customer not found agent_custid=$agent_custid, agentnum=$agentnum";
330   }
331
332   my $cust_recon = new FS::cust_recon( {
333     'recondate'     => time,
334     'agentnum'      => $agentnum,
335     'first'         => $first,
336     'last'          => $last,
337     'address1'      => $address1,
338     'address2'      => $address2,
339     'city'          => $city,
340     'state'         => $state,
341     'zip'           => $zip,
342     'custnum'       => $cust_main ? $cust_main->custnum : '', #really?
343     'status'        => $cust_main ? $cust_main->status : '',
344     'pkg'           => $package,
345     'adjourn'       => $cust_pkg ? $cust_pkg->adjourn : '',
346     'agent_custid'  => $agent_custid, # redundant?
347     'agent_pkg'     => $pkg,
348     'agent_adjourn' => str2time($adjourn),
349     'comments'      => $comments,
350   } );
351
352   warn Dumper($cust_recon) if $DEBUG;
353   my $error = $cust_recon->insert;
354   return $error if $error;
355
356   warn "process_recon_record returning $comments\n" if $DEBUG;
357
358   $comments;
359
360 }
361
362 sub check_username {
363   my $p = shift;
364
365   my $session = _cache->get($p->{'session_id'})
366     or return { 'error' => "Can't resume session" }; #better error message
367
368   my $svc_domain = qsearchs( 'svc_domain', { 'domain' => $p->{domain} } )
369     or return { 'error' => 'Unknown domain '. $p->{domain} };
370
371   my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{user},
372                                          'domsvc'   => $svc_domain->svcnum,
373                                        },
374                          );
375
376   return { 'error' => $p->{user}. '@'. $p->{domain}. " alerady in use" } # sic
377     if $svc_acct;
378
379   return { 'error'   => '',
380            'message' => $p->{user}. '@'. $p->{domain}. " is free"
381   };
382 }
383
384 1;