agent interface
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
1 package FS::ClientAPI::MyAccount;
2
3 use strict;
4 use vars qw($cache);
5 use Digest::MD5 qw(md5_hex);
6 use Date::Format;
7 use Business::CreditCard;
8 use Cache::SharedMemoryCache; #store in db?
9 use FS::CGI qw(small_custview); #doh
10 use FS::Conf;
11 use FS::Record qw(qsearch qsearchs);
12 use FS::Msgcat qw(gettext);
13 use FS::svc_acct;
14 use FS::svc_domain;
15 use FS::svc_external;
16 use FS::part_svc;
17 use FS::cust_main;
18 use FS::cust_bill;
19 use FS::cust_main_county;
20 use FS::cust_pkg;
21
22 use FS::ClientAPI; #hmm
23 FS::ClientAPI->register_handlers(
24   'MyAccount/login'            => \&login,
25   'MyAccount/customer_info'    => \&customer_info,
26   'MyAccount/edit_info'        => \&edit_info,
27   'MyAccount/invoice'          => \&invoice,
28   'MyAccount/list_invoices'    => \&list_invoices,
29   'MyAccount/cancel'           => \&cancel,
30   'MyAccount/payment_info'     => \&payment_info,
31   'MyAccount/process_payment'  => \&process_payment,
32   'MyAccount/list_pkgs'        => \&list_pkgs,
33   'MyAccount/order_pkg'        => \&order_pkg,
34   'MyAccount/cancel_pkg'       => \&cancel_pkg,
35   'MyAccount/charge'           => \&charge,
36 );
37
38 use vars qw( @cust_main_editable_fields );
39 @cust_main_editable_fields = qw(
40   first last company address1 address2 city
41     county state zip country daytime night fax
42   ship_first ship_last ship_company ship_address1 ship_address2 ship_city
43     ship_state ship_zip ship_country ship_daytime ship_night ship_fax
44   payby payinfo payname
45 );
46
47 #store in db?
48 my $cache = new Cache::SharedMemoryCache( {
49    'namespace' => 'FS::ClientAPI::MyAccount',
50 } );
51
52 #false laziness w/FS::ClientAPI::passwd::passwd
53 sub login {
54   my $p = shift;
55
56   my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
57     or return { error => 'Domain '. $p->{'domain'}. ' not found' };
58
59   my $svc_acct = qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
60                                          'domsvc'    => $svc_domain->svcnum, }
61                          );
62   return { error => 'User not found.' } unless $svc_acct;
63   return { error => 'Incorrect password.' }
64     unless $svc_acct->check_password($p->{'password'});
65
66   my $session = {
67     'svcnum' => $svc_acct->svcnum,
68   };
69
70   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
71   if ( $cust_pkg ) {
72     my $cust_main = $cust_pkg->cust_main;
73     $session->{'custnum'} = $cust_main->custnum;
74   }
75
76   my $session_id;
77   do {
78     $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
79   } until ( ! defined $cache->get($session_id) ); #just in case
80
81   $cache->set( $session_id, $session, '1 hour' );
82
83   return { 'error'      => '',
84            'session_id' => $session_id,
85          };
86 }
87
88 sub customer_info {
89   my $p = shift;
90
91   my($session, $custnum, $context);
92   if ( $p->{'session_id'} ) {
93     $context = 'customer';
94     $session = $cache->get($p->{'session_id'})
95       or return { 'error' => "Can't resume session" }; #better error message
96     $custnum = $session->{'custnum'};
97   } elsif ( $p->{'agent_session_id'} ) {
98     $context = 'agent';
99     my $agent_cache = new Cache::SharedMemoryCache( {
100       'namespace' => 'FS::ClientAPI::Agent',
101     } );
102     $session = $agent_cache->get($p->{'agent_session_id'})
103       or return { 'error' => "Can't resume session" }; #better error message
104     $custnum = $p->{'custnum'};
105   } else {
106     return { 'error' => "Can't resume session" }; #better error message
107   }
108
109   my %return;
110   if ( $custnum ) { #customer record
111
112     my $search = { 'custnum' => $custnum };
113     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
114     my $cust_main = qsearchs('cust_main', $search )
115       or return { 'error' => "unknown custnum $custnum" };
116
117     $return{balance} = $cust_main->balance;
118
119     my @open = map {
120                      {
121                        invnum => $_->invnum,
122                        date   => time2str("%b %o, %Y", $_->_date),
123                        owed   => $_->owed,
124                      };
125                    } $cust_main->open_cust_bill;
126     $return{open_invoices} = \@open;
127
128     my $conf = new FS::Conf;
129     $return{small_custview} =
130       small_custview( $cust_main, $conf->config('defaultcountry') );
131
132     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
133
134     for (@cust_main_editable_fields) {
135       $return{$_} = $cust_main->get($_);
136     }
137
138     if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
139       $return{payinfo} = $cust_main->payinfo_masked;
140       @return{'month', 'year'} = $cust_main->paydate_monthyear;
141     }
142
143     $return{'invoicing_list'} =
144       join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
145     $return{'postal_invoicing'} =
146       0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
147
148   } else { #no customer record
149
150     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
151       or die "unknown svcnum";
152     $return{name} = $svc_acct->email;
153
154   }
155
156   return { 'error'          => '',
157            'custnum'        => $custnum,
158            %return,
159          };
160
161 }
162
163 sub edit_info {
164   my $p = shift;
165   my $session = $cache->get($p->{'session_id'})
166     or return { 'error' => "Can't resume session" }; #better error message
167
168   my $custnum = $session->{'custnum'}
169     or return { 'error' => "no customer record" };
170
171   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
172     or return { 'error' => "unknown custnum $custnum" };
173
174   my $new = new FS::cust_main { $cust_main->hash };
175   $new->set( $_ => $p->{$_} )
176     foreach grep { exists $p->{$_} } @cust_main_editable_fields;
177
178   if ( $p->{'payby'} =~ /^(CARD|DCRD)$/ ) {
179     $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
180     if ( $new->payinfo eq $cust_main->payinfo_masked ) {
181       $new->payinfo($cust_main->payinfo);
182     } else {
183       $new->paycvv($p->{'paycvv'});
184     }
185   }
186
187   my @invoicing_list;
188   if ( exists $p->{'invoicing_list'} || exists $p->{'postal_invoicing'} ) {
189     #false laziness with httemplate/edit/process/cust_main.cgi
190     @invoicing_list = split( /\s*\,\s*/, $p->{'invoicing_list'} );
191     push @invoicing_list, 'POST' if $p->{'postal_invoicing'};
192   } else {
193     @invoicing_list = $cust_main->invoicing_list;
194   }
195
196   my $error = $new->replace($cust_main, \@invoicing_list);
197   return { 'error' => $error } if $error;
198   #$cust_main = $new;
199   
200   return { 'error' => '' };
201 }
202
203 sub payment_info {
204   my $p = shift;
205   my $session = $cache->get($p->{'session_id'})
206     or return { 'error' => "Can't resume session" }; #better error message
207
208   my %return;
209
210   my $custnum = $session->{'custnum'};
211
212   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
213     or return { 'error' => "unknown custnum $custnum" };
214
215   $return{balance} = $cust_main->balance;
216
217   $return{payname} = $cust_main->payname
218                      || ( $cust_main->first. ' '. $cust_main->get('last') );
219
220   $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip);
221
222   $return{payby} = $cust_main->payby;
223
224   if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
225     #warn $return{card_type} = cardtype($cust_main->payinfo);
226     $return{payinfo} = $cust_main->payinfo;
227
228     @return{'month', 'year'} = $cust_main->paydate_monthyear;
229
230   }
231
232   #list all counties/states/countries
233   $return{'cust_main_county'} = 
234       [ map { $_->hashref } qsearch('cust_main_county', {}) ],
235
236   #shortcut for one-country folks
237   my $conf = new FS::Conf;
238   my %states = map { $_->state => 1 }
239                  qsearch('cust_main_county', {
240                    'country' => $conf->config('defaultcountry') || 'US'
241                  } );
242   $return{'states'} = [ sort { $a cmp $b } keys %states ];
243
244   $return{card_types} = {
245     'VISA' => 'VISA card',
246     'MasterCard' => 'MasterCard',
247     'Discover' => 'Discover card',
248     'American Express' => 'American Express card',
249   };
250
251   my $_date = time;
252   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
253
254   return { 'error' => '',
255            %return,
256          };
257
258 };
259
260 #some false laziness with httemplate/process/payment.cgi - look there for
261 #ACH and CVV support stuff
262 sub process_payment {
263
264   my $p = shift;
265
266   my $session = $cache->get($p->{'session_id'})
267     or return { 'error' => "Can't resume session" }; #better error message
268
269   my %return;
270
271   my $custnum = $session->{'custnum'};
272
273   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
274     or return { 'error' => "unknown custnum $custnum" };
275
276   $p->{'payname'} =~ /^([\w \,\.\-\']+)$/
277     or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
278   my $payname = $1;
279
280   $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
281     or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
282   my $paybatch = $1;
283
284   my $payinfo;
285   my $paycvv = '';
286   #if ( $payby eq 'CHEK' ) {
287   #
288   #  $p->{'payinfo1'} =~ /^(\d+)$/
289   #    or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
290   #  my $payinfo1 = $1;
291   #   $p->{'payinfo2'} =~ /^(\d+)$/
292   #    or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
293   #  my $payinfo2 = $1;
294   #  $payinfo = $payinfo1. '@'. $payinfo2;
295   # 
296   #} elsif ( $payby eq 'CARD' ) {
297    
298     $payinfo = $p->{'payinfo'};
299     $payinfo =~ s/\D//g;
300     $payinfo =~ /^(\d{13,16})$/
301       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
302     $payinfo = $1;
303     validate($payinfo)
304       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
305     return { 'error' => gettext('unknown_card_type') }
306       if cardtype($payinfo) eq "Unknown";
307
308     if ( defined $cust_main->dbdef_table->column('paycvv') ) {
309       if ( length($p->{'paycvv'} ) ) {
310         if ( cardtype($payinfo) eq 'American Express card' ) {
311           $p->{'paycvv'} =~ /^(\d{4})$/
312             or return { 'error' => "CVV2 (CID) for American Express cards is four digits." };
313           $paycvv = $1;
314         } else {
315           $p->{'paycvv'} =~ /^(\d{3})$/
316             or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
317           $paycvv = $1;
318         }
319       }
320     }
321   
322   #} else {
323   #  die "unknown payby $payby";
324   #}
325
326   my $error = $cust_main->realtime_bop( 'CC', $p->{'amount'},
327     'quiet'    => 1,
328     'payinfo'  => $payinfo,
329     'paydate'  => $p->{'year'}. '-'. $p->{'month'}. '-01',
330     'payname'  => $payname,
331     'paybatch' => $paybatch,
332     'paycvv'   => $paycvv,
333     map { $_ => $p->{$_} } qw( address1 address2 city state zip )
334   );
335   return { 'error' => $error } if $error;
336
337   $cust_main->apply_payments;
338
339   if ( $p->{'save'} ) {
340     my $new = new FS::cust_main { $cust_main->hash };
341     $new->set( $_ => $p->{$_} )
342       foreach qw( payname address1 address2 city state zip payinfo );
343     $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' );
344     $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
345     my $error = $new->replace($cust_main);
346     return { 'error' => $error } if $error;
347     $cust_main = $new;
348   }
349
350   return { 'error' => '' };
351
352 }
353
354 sub invoice {
355   my $p = shift;
356   my $session = $cache->get($p->{'session_id'})
357     or return { 'error' => "Can't resume session" }; #better error message
358
359   my $custnum = $session->{'custnum'};
360
361   my $invnum = $p->{'invnum'};
362
363   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
364                                           'custnum' => $custnum } )
365     or return { 'error' => "Can't find invnum" };
366
367   #my %return;
368
369   return { 'error'        => '',
370            'invnum'       => $invnum,
371            'invoice_text' => join('', $cust_bill->print_text ),
372          };
373
374 }
375
376 sub list_invoices {
377   my $p = shift;
378   my $session = $cache->get($p->{'session_id'})
379     or return { 'error' => "Can't resume session" }; #better error message
380
381   my $custnum = $session->{'custnum'};
382
383   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
384     or return { 'error' => "unknown custnum $custnum" };
385
386   my @cust_bill = $cust_main->cust_bill;
387
388   return  { 'error'       => '',
389             'invoices'    =>  [ map { { 'invnum' => $_->invnum,
390                                         '_date'  => $_->_date,
391                                       }
392                                     } @cust_bill
393                               ]
394           };
395 }
396
397 sub cancel {
398   my $p = shift;
399   my $session = $cache->get($p->{'session_id'})
400     or return { 'error' => "Can't resume session" }; #better error message
401
402   my $custnum = $session->{'custnum'};
403
404   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
405     or return { 'error' => "unknown custnum $custnum" };
406
407   my @errors = $cust_main->cancel( 'quiet'=>1 );
408
409   my $error = scalar(@errors) ? join(' / ', @errors) : '';
410
411   return { 'error' => $error };
412
413 }
414
415 sub list_pkgs {
416   my $p = shift;
417   my $session = $cache->get($p->{'session_id'})
418     or return { 'error' => "Can't resume session" }; #better error message
419
420   my $custnum = $session->{'custnum'};
421
422   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
423     or return { 'error' => "unknown custnum $custnum" };
424
425   return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] };
426
427 }
428
429 sub order_pkg {
430   my $p = shift;
431
432   my($session, $custnum, $context);
433
434   if ( $p->{'session_id'} ) {
435     $context = 'customer';
436     $session = $cache->get($p->{'session_id'})
437       or return { 'error' => "Can't resume session" }; #better error message
438     $custnum = $session->{'custnum'};
439   } elsif ( $p->{'agent_session_id'} ) {
440     $context = 'agent';
441     my $agent_cache = new Cache::SharedMemoryCache( {
442       'namespace' => 'FS::ClientAPI::Agent',
443     } );
444     $session = $agent_cache->get($p->{'agent_session_id'})
445       or return { 'error' => "Can't resume session" }; #better error message
446     $custnum = $p->{'custnum'};
447   } else {
448     return { 'error' => "Can't resume session" }; #better error message
449   }
450
451   my $search = { 'custnum' => $custnum };
452   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
453
454   my $cust_main = qsearchs('cust_main', $search )
455     or return { 'error' => "unknown custnum $custnum" };
456
457   #false laziness w/ClientAPI/Signup.pm
458
459   my $cust_pkg = new FS::cust_pkg ( {
460     'custnum' => $custnum,
461     'pkgpart' => $p->{'pkgpart'},
462   } );
463   my $error = $cust_pkg->check;
464   return { 'error' => $error } if $error;
465
466   my @svc = ();
467   unless ( $p->{'svcpart'} eq 'none' ) {
468
469     my $svcdb;
470     my $svcpart = '';
471     if ( $p->{'svcpart'} =~ /^(\d+)$/ ) {
472       $svcpart = $1;
473       my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
474       return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc;
475       $svcdb = $part_svc->svcdb;
476     } else {
477       $svcdb = 'svc_acct';
478     }
479     $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb);
480
481     my %fields = (
482       'svc_acct'     => [ qw( username _password sec_phrase popnum ) ],
483       'svc_domain'   => [ qw( domain ) ],
484       'svc_external' => [ qw( id title ) ],
485     );
486   
487     my $svc_x = "FS::$svcdb"->new( {
488       'svcpart'   => $svcpart,
489       map { $_ => $p->{$_} } @{$fields{$svcdb}}
490     } );
491     
492     if ( $svcdb eq 'svc_acct' ) {
493       my @acct_snarf;
494       my $snarfnum = 1;
495       while ( length($p->{"snarf_machine$snarfnum"}) ) {
496         my $acct_snarf = new FS::acct_snarf ( {
497           'machine'   => $p->{"snarf_machine$snarfnum"},
498           'protocol'  => $p->{"snarf_protocol$snarfnum"},
499           'username'  => $p->{"snarf_username$snarfnum"},
500           '_password' => $p->{"snarf_password$snarfnum"},
501         } );
502         $snarfnum++;
503         push @acct_snarf, $acct_snarf;
504       }
505       $svc_x->child_objects( \@acct_snarf );
506     }
507     
508     my $y = $svc_x->setdefault; # arguably should be in new method
509     return { 'error' => $y } if $y && !ref($y);
510   
511     $error = $svc_x->check;
512     return { 'error' => $error } if $error;
513
514     push @svc, $svc_x;
515
516   }
517
518   use Tie::RefHash;
519   tie my %hash, 'Tie::RefHash';
520   %hash = ( $cust_pkg => \@svc );
521   #msgcat
522   $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 );
523   return { 'error' => $error } if $error;
524
525   my $conf = new FS::Conf;
526   if ( $conf->exists('signup_server-realtime') ) {
527
528     my $old_balance = $cust_main->balance;
529
530     my $bill_error = $cust_main->bill;
531     $cust_main->apply_payments;
532     $cust_main->apply_credits;
533     $bill_error = $cust_main->collect;
534
535     if ( $cust_main->balance > $old_balance
536          && $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ) {
537       $cust_pkg->cancel('quiet'=>1);
538       return { 'error' => '_decline' };
539     } else {
540       $cust_pkg->reexport;
541     }
542
543   } else {
544     $cust_pkg->reexport;
545   }
546
547   return { error => '', pkgnum => $cust_pkg->pkgnum };
548
549 }
550
551 sub cancel_pkg {
552   my $p = shift;
553   my $session = $cache->get($p->{'session_id'})
554     or return { 'error' => "Can't resume session" }; #better error message
555
556   my $custnum = $session->{'custnum'};
557
558   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
559     or return { 'error' => "unknown custnum $custnum" };
560
561   my $pkgnum = $p->{'pkgnum'};
562
563   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
564                                         'pkgnum'  => $pkgnum,   } )
565     or return { 'error' => "unknown pkgnum $pkgnum" };
566
567   my $error = $cust_pkg->cancel( 'quiet'=>1 );
568   return { 'error' => $error };
569
570 }
571
572 1;
573