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