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