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