add support for running selfservice server against multiple machines
[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 Cache::SharedMemoryCache; #store in db?
8 use FS::CGI qw(small_custview); #doh
9 use FS::Conf;
10 use FS::Record qw(qsearchs);
11 use FS::svc_acct;
12 use FS::svc_domain;
13 use FS::cust_main;
14 use FS::cust_bill;
15 use FS::cust_pkg;
16
17 use FS::ClientAPI; #hmm
18 FS::ClientAPI->register_handlers(
19   'MyAccount/login'            => \&login,
20   'MyAccount/customer_info'    => \&customer_info,
21   'MyAccount/edit_info'        => \&edit_info,
22   'MyAccount/invoice'          => \&invoice,
23   'MyAccount/cancel'           => \&cancel,
24   'MyAccount/list_pkgs'        => \&list_pkgs,
25   'MyAccount/order_pkg'        => \&order_pkg,
26   'MyAccount/cancel_pkg'       => \&cancel_pkg,
27   'MyAccount/charge'           => \&charge,
28 );
29
30 use vars qw( @cust_main_editable_fields );
31 @cust_main_editable_fields = qw(
32   first last company address1 address2 city
33     county state zip country daytime night fax
34   ship_first ship_last ship_company ship_address1 ship_address2 ship_city
35     ship_state ship_zip ship_country ship_daytime ship_night ship_fax
36 );
37
38 #store in db?
39 my $cache = new Cache::SharedMemoryCache( {
40    'namespace' => 'FS::ClientAPI::MyAccount',
41 } );
42
43 #false laziness w/FS::ClientAPI::passwd::passwd (needs to handle encrypted pw)
44 sub login {
45   my $p = shift;
46
47   my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
48     or return { error => "Domain not found" };
49
50   my $svc_acct =
51     ( length($p->{'password'}) < 13
52       && qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
53                                  'domsvc'    => $svc_domain->svcnum,
54                                  '_password' => $p->{'password'}     } )
55     )
56     || qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
57                                'domsvc'    => $svc_domain->svcnum,
58                                '_password' => $p->{'password'}     } );
59
60   unless ( $svc_acct ) { return { error => 'Incorrect password.' } }
61
62   my $session = {
63     'svcnum' => $svc_acct->svcnum,
64   };
65
66   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
67   if ( $cust_pkg ) {
68     my $cust_main = $cust_pkg->cust_main;
69     $session->{'custnum'} = $cust_main->custnum;
70   }
71
72   my $session_id;
73   do {
74     $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
75   } until ( ! defined $cache->get($session_id) ); #just in case
76
77   $cache->set( $session_id, $session, '1 hour' );
78
79   return { 'error'      => '',
80            'session_id' => $session_id,
81          };
82 }
83
84 sub customer_info {
85   my $p = shift;
86   my $session = $cache->get($p->{'session_id'})
87     or return { 'error' => "Can't resume session" }; #better error message
88
89   my %return;
90
91   my $custnum = $session->{'custnum'};
92
93   if ( $custnum ) { #customer record
94
95     my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
96       or return { 'error' => "unknown custnum $custnum" };
97
98     $return{balance} = $cust_main->balance;
99
100     my @open = map {
101                      {
102                        invnum => $_->invnum,
103                        date   => time2str("%b %o, %Y", $_->_date),
104                        owed   => $_->owed,
105                      };
106                    } $cust_main->open_cust_bill;
107     $return{open_invoices} = \@open;
108
109     my $conf = new FS::Conf;
110     $return{small_custview} =
111       small_custview( $cust_main, $conf->config('defaultcountry') );
112
113     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
114
115     for (@cust_main_editable_fields) {
116       $return{$_} = $cust_main->get($_);
117     }
118
119   } else { #no customer record
120
121     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
122       or die "unknown svcnum";
123     $return{name} = $svc_acct->email;
124
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 invoice {
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 $custnum = $session->{'custnum'};
162
163   my $invnum = $p->{'invnum'};
164
165   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
166                                           'custnum' => $custnum } )
167     or return { 'error' => "Can't find invnum" };
168
169   #my %return;
170
171   return { 'error'        => '',
172            'invnum'       => $invnum,
173            'invoice_text' => join('', $cust_bill->print_text ),
174          };
175
176 }
177
178 sub cancel {
179   my $p = shift;
180   my $session = $cache->get($p->{'session_id'})
181     or return { 'error' => "Can't resume session" }; #better error message
182
183   my $custnum = $session->{'custnum'};
184
185   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
186     or return { 'error' => "unknown custnum $custnum" };
187
188   my @errors = $cust_main->cancel( 'quiet'=>1 );
189
190   my $error = scalar(@errors) ? join(' / ', @errors) : '';
191
192   return { 'error' => $error };
193
194 }
195
196 sub list_pkgs {
197   my $p = shift;
198   my $session = $cache->get($p->{'session_id'})
199     or return { 'error' => "Can't resume session" }; #better error message
200
201   my $custnum = $session->{'custnum'};
202
203   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
204     or return { 'error' => "unknown custnum $custnum" };
205
206   return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] };
207
208 }
209
210 sub order_pkg {
211   my $p = shift;
212   my $session = $cache->get($p->{'session_id'})
213     or return { 'error' => "Can't resume session" }; #better error message
214
215   my $custnum = $session->{'custnum'};
216
217   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
218     or return { 'error' => "unknown custnum $custnum" };
219
220   #false laziness w/ClientAPI/Signup.pm
221
222   my $cust_pkg = new FS::cust_pkg ( {
223     'custnum' => $custnum,
224     'pkgpart' => $p->{'pkgpart'},
225   } );
226   my $error = $cust_pkg->check;
227   return { 'error' => $error } if $error;
228
229   my $svc_acct = new FS::svc_acct ( {
230     'svcpart'   => $p->{'svcpart'} || $cust_pkg->part_pkg->svcpart('svc_acct'),
231     map { $_ => $p->{$_} }
232       qw( username _password sec_phrase popnum ),
233   } );
234
235   my @acct_snarf;
236   my $snarfnum = 1;
237   while ( length($p->{"snarf_machine$snarfnum"}) ) {
238     my $acct_snarf = new FS::acct_snarf ( {
239       'machine'   => $p->{"snarf_machine$snarfnum"},
240       'protocol'  => $p->{"snarf_protocol$snarfnum"},
241       'username'  => $p->{"snarf_username$snarfnum"},
242       '_password' => $p->{"snarf_password$snarfnum"},
243     } );
244     $snarfnum++;
245     push @acct_snarf, $acct_snarf;
246   }
247   $svc_acct->child_objects( \@acct_snarf );
248
249   my $y = $svc_acct->setdefault; # arguably should be in new method
250   return { 'error' => $y } if $y && !ref($y);
251
252   $error = $svc_acct->check;
253   return { 'error' => $error } if $error;
254
255   use Tie::RefHash;
256   tie my %hash, 'Tie::RefHash';
257   %hash = ( $cust_pkg => [ $svc_acct ] );
258   #msgcat
259   $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 );
260   return { 'error' => $error } if $error;
261
262   my $conf = new FS::Conf;
263   if ( $conf->exists('signup_server-realtime') ) {
264
265     my $old_balance = $cust_main->balance;
266
267     my $bill_error = $cust_main->bill;
268     $cust_main->apply_payments;
269     $cust_main->apply_credits;
270     $bill_error = $cust_main->collect;
271
272     if ( $cust_main->balance > $old_balance ) {
273       $cust_pkg->cancel('quiet'=>1);
274       return { 'error' => '_decline' };
275     } else {
276       $cust_pkg->reexport;
277     }
278
279   } else {
280     $cust_pkg->reexport;
281   }
282
283   return { error => '' };
284
285 }
286
287 sub cancel_pkg {
288   my $p = shift;
289   my $session = $cache->get($p->{'session_id'})
290     or return { 'error' => "Can't resume session" }; #better error message
291
292   my $custnum = $session->{'custnum'};
293
294   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
295     or return { 'error' => "unknown custnum $custnum" };
296
297   my $pkgnum = $session->{'pkgnum'};
298
299   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
300                                         'pkgnum'  => $pkgnum,   } )
301     or return { 'error' => "unknown pkgnum $pkgnum" };
302
303   my $error = $cust_main->cancel( 'quiet'=>1 );
304   return { 'error' => $error };
305
306 }
307
308 1;
309