445f0ece87972f73c6a4ef56598702133b0bd819
[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::svc_acct;
13 use FS::svc_domain;
14 use FS::cust_main;
15 use FS::cust_bill;
16 use FS::cust_main_county;
17 use FS::cust_pkg;
18
19 use FS::ClientAPI; #hmm
20 FS::ClientAPI->register_handlers(
21   'MyAccount/login'            => \&login,
22   'MyAccount/customer_info'    => \&customer_info,
23   'MyAccount/edit_info'        => \&edit_info,
24   'MyAccount/invoice'          => \&invoice,
25   'MyAccount/cancel'           => \&cancel,
26   'MyAccount/payment_info'     => \&payment_info,
27   'MyAccount/process_payment'  => \&process_payment,
28   'MyAccount/list_pkgs'        => \&list_pkgs,
29   'MyAccount/order_pkg'        => \&order_pkg,
30   'MyAccount/cancel_pkg'       => \&cancel_pkg,
31   'MyAccount/charge'           => \&charge,
32 );
33
34 use vars qw( @cust_main_editable_fields );
35 @cust_main_editable_fields = qw(
36   first last company address1 address2 city
37     county state zip country daytime night fax
38   ship_first ship_last ship_company ship_address1 ship_address2 ship_city
39     ship_state ship_zip ship_country ship_daytime ship_night ship_fax
40 );
41
42 #store in db?
43 my $cache = new Cache::SharedMemoryCache( {
44    'namespace' => 'FS::ClientAPI::MyAccount',
45 } );
46
47 #false laziness w/FS::ClientAPI::passwd::passwd (needs to handle encrypted pw)
48 sub login {
49   my $p = shift;
50
51   my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
52     or return { error => "Domain not found" };
53
54   my $svc_acct =
55     ( length($p->{'password'}) < 13
56       && qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
57                                  'domsvc'    => $svc_domain->svcnum,
58                                  '_password' => $p->{'password'}     } )
59     )
60     || qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
61                                'domsvc'    => $svc_domain->svcnum,
62                                '_password' => $p->{'password'}     } );
63
64   unless ( $svc_acct ) { return { error => 'Incorrect 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   my $session = $cache->get($p->{'session_id'})
91     or return { 'error' => "Can't resume session" }; #better error message
92
93   my %return;
94
95   my $custnum = $session->{'custnum'};
96
97   if ( $custnum ) { #customer record
98
99     my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
100       or return { 'error' => "unknown custnum $custnum" };
101
102     $return{balance} = $cust_main->balance;
103
104     my @open = map {
105                      {
106                        invnum => $_->invnum,
107                        date   => time2str("%b %o, %Y", $_->_date),
108                        owed   => $_->owed,
109                      };
110                    } $cust_main->open_cust_bill;
111     $return{open_invoices} = \@open;
112
113     my $conf = new FS::Conf;
114     $return{small_custview} =
115       small_custview( $cust_main, $conf->config('defaultcountry') );
116
117     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
118
119     for (@cust_main_editable_fields) {
120       $return{$_} = $cust_main->get($_);
121     }
122
123   } else { #no customer record
124
125     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
126       or die "unknown svcnum";
127     $return{name} = $svc_acct->email;
128
129   }
130
131   return { 'error'          => '',
132            'custnum'        => $custnum,
133            %return,
134          };
135
136 }
137
138 sub edit_info {
139   my $p = shift;
140   my $session = $cache->get($p->{'session_id'})
141     or return { 'error' => "Can't resume session" }; #better error message
142
143   my $custnum = $session->{'custnum'}
144     or return { 'error' => "no customer record" };
145
146   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
147     or return { 'error' => "unknown custnum $custnum" };
148
149   my $new = new FS::cust_main { $cust_main->hash };
150   $new->set( $_ => $p->{$_} )
151     foreach grep { exists $p->{$_} } @cust_main_editable_fields;
152   my $error = $new->replace($cust_main);
153   return { 'error' => $error } if $error;
154   #$cust_main = $new;
155   
156   return { 'error' => '' };
157 }
158
159 sub payment_info {
160   my $p = shift;
161   my $session = $cache->get($p->{'session_id'})
162     or return { 'error' => "Can't resume session" }; #better error message
163
164   my %return;
165
166   my $custnum = $session->{'custnum'};
167
168   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
169     or return { 'error' => "unknown custnum $custnum" };
170
171   $return{balance} = $cust_main->balance;
172
173   $return{payname} = $cust_main->payname
174                      || ( $cust_main->first. ' '. $cust_main->get('last') );
175
176   $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip);
177
178   $return{payby} = $cust_main->payby;
179
180   if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
181     warn $return{card_type} = cardtype($cust_main->payinfo);
182     $return{payinfo} = $cust_main->payinfo;
183
184     if ( $cust_main->paydate  =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #Pg date format
185       @return{'month', 'year'} = ( $2, $1 );
186     } elsif ( $cust_main->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
187       @return{'month', 'year'} = ( $1, $3 );
188     }
189
190   }
191
192   #list all counties/states/countries
193   $return{'cust_main_county'} = 
194       [ map { $_->hashref } qsearch('cust_main_county', {}) ],
195
196   #shortcut for one-country folks
197   my $conf = new FS::Conf;
198   my %states = map { $_->state => 1 }
199                  qsearch('cust_main_county', {
200                    'country' => $conf->config('defaultcountry') || 'US'
201                  } );
202   $return{'states'} = [ sort { $a cmp $b } keys %states ];
203
204   $return{card_types} = {
205     'VISA' => 'VISA card',
206     'MasterCard' => 'MasterCard',
207     'Discover' => 'Discover card',
208     'American Express' => 'American Express card',
209   };
210
211   my $_date = time;
212   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
213
214   return { 'error' => '',
215            %return,
216          };
217
218 };
219
220 sub process_payment {
221   my $p = shift;
222
223   my $session = $cache->get($p->{'session_id'})
224     or return { 'error' => "Can't resume session" }; #better error message
225
226   my %return;
227
228   my $custnum = $session->{'custnum'};
229
230   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
231     or return { 'error' => "unknown custnum $custnum" };
232
233   if ( $p->{'save'} ) {
234     my $new = new FS::cust_main { $cust_main->hash };
235     $new->set( $_ => $p->{$_} )
236       foreach qw( payname address1 address2 city state zip payinfo );
237     $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' );
238     $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
239     my $error = $new->replace($cust_main);
240     return { 'error' => $error } if $error;
241     $cust_main = $new;
242   }
243
244   my $error = $cust_main->realtime_bop( 'CC', $p->{'amount'}, quiet=>1,
245     'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01',
246     map { $_ => $p->{$_} }
247       qw( payname address1 address2 city state zip payinfo paybatch )
248   );
249   return { 'error' => $error } if $error;
250
251   $cust_main->apply_payments;
252
253   return { 'error' => '' };
254
255 }
256
257 sub invoice {
258   my $p = shift;
259   my $session = $cache->get($p->{'session_id'})
260     or return { 'error' => "Can't resume session" }; #better error message
261
262   my $custnum = $session->{'custnum'};
263
264   my $invnum = $p->{'invnum'};
265
266   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
267                                           'custnum' => $custnum } )
268     or return { 'error' => "Can't find invnum" };
269
270   #my %return;
271
272   return { 'error'        => '',
273            'invnum'       => $invnum,
274            'invoice_text' => join('', $cust_bill->print_text ),
275          };
276
277 }
278
279 sub cancel {
280   my $p = shift;
281   my $session = $cache->get($p->{'session_id'})
282     or return { 'error' => "Can't resume session" }; #better error message
283
284   my $custnum = $session->{'custnum'};
285
286   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
287     or return { 'error' => "unknown custnum $custnum" };
288
289   my @errors = $cust_main->cancel( 'quiet'=>1 );
290
291   my $error = scalar(@errors) ? join(' / ', @errors) : '';
292
293   return { 'error' => $error };
294
295 }
296
297 sub list_pkgs {
298   my $p = shift;
299   my $session = $cache->get($p->{'session_id'})
300     or return { 'error' => "Can't resume session" }; #better error message
301
302   my $custnum = $session->{'custnum'};
303
304   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
305     or return { 'error' => "unknown custnum $custnum" };
306
307   return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] };
308
309 }
310
311 sub order_pkg {
312   my $p = shift;
313   my $session = $cache->get($p->{'session_id'})
314     or return { 'error' => "Can't resume session" }; #better error message
315
316   my $custnum = $session->{'custnum'};
317
318   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
319     or return { 'error' => "unknown custnum $custnum" };
320
321   #false laziness w/ClientAPI/Signup.pm
322
323   my $cust_pkg = new FS::cust_pkg ( {
324     'custnum' => $custnum,
325     'pkgpart' => $p->{'pkgpart'},
326   } );
327   my $error = $cust_pkg->check;
328   return { 'error' => $error } if $error;
329
330   my $svc_acct = new FS::svc_acct ( {
331     'svcpart'   => $p->{'svcpart'} || $cust_pkg->part_pkg->svcpart('svc_acct'),
332     map { $_ => $p->{$_} }
333       qw( username _password sec_phrase popnum ),
334   } );
335
336   my @acct_snarf;
337   my $snarfnum = 1;
338   while ( length($p->{"snarf_machine$snarfnum"}) ) {
339     my $acct_snarf = new FS::acct_snarf ( {
340       'machine'   => $p->{"snarf_machine$snarfnum"},
341       'protocol'  => $p->{"snarf_protocol$snarfnum"},
342       'username'  => $p->{"snarf_username$snarfnum"},
343       '_password' => $p->{"snarf_password$snarfnum"},
344     } );
345     $snarfnum++;
346     push @acct_snarf, $acct_snarf;
347   }
348   $svc_acct->child_objects( \@acct_snarf );
349
350   my $y = $svc_acct->setdefault; # arguably should be in new method
351   return { 'error' => $y } if $y && !ref($y);
352
353   $error = $svc_acct->check;
354   return { 'error' => $error } if $error;
355
356   use Tie::RefHash;
357   tie my %hash, 'Tie::RefHash';
358   %hash = ( $cust_pkg => [ $svc_acct ] );
359   #msgcat
360   $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 );
361   return { 'error' => $error } if $error;
362
363   my $conf = new FS::Conf;
364   if ( $conf->exists('signup_server-realtime') ) {
365
366     my $old_balance = $cust_main->balance;
367
368     my $bill_error = $cust_main->bill;
369     $cust_main->apply_payments;
370     $cust_main->apply_credits;
371     $bill_error = $cust_main->collect;
372
373     if ( $cust_main->balance > $old_balance ) {
374       $cust_pkg->cancel('quiet'=>1);
375       return { 'error' => '_decline' };
376     } else {
377       $cust_pkg->reexport;
378     }
379
380   } else {
381     $cust_pkg->reexport;
382   }
383
384   return { error => '' };
385
386 }
387
388 sub cancel_pkg {
389   my $p = shift;
390   my $session = $cache->get($p->{'session_id'})
391     or return { 'error' => "Can't resume session" }; #better error message
392
393   my $custnum = $session->{'custnum'};
394
395   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
396     or return { 'error' => "unknown custnum $custnum" };
397
398   my $pkgnum = $session->{'pkgnum'};
399
400   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
401                                         'pkgnum'  => $pkgnum,   } )
402     or return { 'error' => "unknown pkgnum $pkgnum" };
403
404   my $error = $cust_main->cancel( 'quiet'=>1 );
405   return { 'error' => $error };
406
407 }
408
409 1;
410