1 package FS::ClientAPI::PaymentOnly;
3 use 5.008; #require 5.8+ for Time::Local 1.05+
5 use vars qw( $cache $DEBUG $me );
6 use subs qw( _cache _provision );
7 use FS::ClientAPI_SessionCache;
11 use Digest::MD5 qw(md5_hex);
12 use Digest::SHA qw(sha512_hex);
15 use Time::Local qw(timelocal_nocheck);
16 use Business::CreditCard 0.35;
19 use Spreadsheet::WriteExcel;
20 use OLE::Storage_Lite;
21 use FS::UI::Web::small_custview qw(small_custview); #less doh
23 use FS::UI::bytecount qw( display_bytecount );
26 use FS::Record qw(qsearch qsearchs dbh);
27 use FS::Msgcat qw(gettext);
28 use FS::Misc qw(card_types money_pretty);
29 use FS::Misc::DateTime qw(parse_datetime);
31 use FS::ClientAPI_SessionCache;
43 use FS::legacy_cust_bill;
44 use FS::cust_main_county;
48 use FS::acct_rt_transaction;
52 use FS::cust_location;
56 $me = '[FS::ClientAPI::PaymentOnly]';
59 $cache ||= new FS::ClientAPI_SessionCache( {
60 'namespace' => 'FS::ClientAPI::PaymentOnly',
64 sub payment_only_skin_info {
67 my($context, $session, $custnum) = _custoragent_session_custnum($p);
68 #return { 'error' => $session } if $context eq 'error';
71 if ( $context eq 'customer' && $custnum ) {
73 my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?')
76 $sth->execute($custnum) or die $sth->errstr;
78 $agentnum = $sth->fetchrow_arrayref->[0]
79 or die "no agentnum for custnum $custnum";
81 #} elsif ( $context eq 'agent' ) {
82 } elsif ( defined($p->{'agentnum'}) and $p->{'agentnum'} =~ /^(\d+)$/ ) {
85 $p->{'agentnum'} = $agentnum;
87 my $conf = new FS::Conf;
89 #false laziness w/Signup.pm
91 my $skin_info_cache_agent = _cache->get("skin_info_cache_agent$agentnum");
93 if ( $skin_info_cache_agent ) {
95 warn "$me loading cached skin info for agentnum $agentnum\n"
100 warn "$me populating skin info cache for agentnum $agentnum\n"
103 $skin_info_cache_agent = {
104 'agentnum' => $agentnum,
105 ( map { $_ => scalar( $conf->config($_, $agentnum) ) }
106 qw( company_name date_format ) ),
107 ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) }
108 qw( body_bgcolor box_bgcolor stripe1_bgcolor stripe2_bgcolor
109 text_color link_color vlink_color hlink_color alink_color
110 font title_color title_align title_size menu_bgcolor menu_fontsize
113 'menu_disable' => [ $conf->config('selfservice-menu_disable',$agentnum) ],
114 ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) }
115 qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo enable_payment_without_balance )
117 ( map { $_ => scalar($conf->config_binary("selfservice-$_", $agentnum)) }
118 qw( title_left_image title_right_image
119 menu_top_image menu_body_image menu_bottom_image
122 'logo' => scalar($conf->config_binary('logo.png', $agentnum )),
123 ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) }
124 qw( head body_header body_footer company_address ) ),
125 'money_char' => $conf->config("money_char") || '$',
126 'menu' => 'payment_only_payment.php Make Payment
128 payment_only_logout.php Logout
132 _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent);
136 #{ %$skin_info_cache_agent };
137 $skin_info_cache_agent;
144 my $conf = new FS::Conf;
150 return { error => 'MAC address empty '.$p->{'mac'} }
153 my $mac_address = $p->{'mac'};
154 $mac_address =~ s/[\:\,\-\. ]//g;
155 $mac_address =~ tr/[a-z]/[A-Z/;
157 my $svc_broadband = qsearchs( 'svc_broadband', { 'mac_addr' => $mac_address } );
158 return { error => 'MAC address not found $mac_address '.$p->{'mac'} }
159 unless $svc_broadband;
160 $svc_x = $svc_broadband;
164 $session->{'svcnum'} = $svc_x->svcnum;
166 my $cust_svc = $svc_x->cust_svc;
167 my $cust_pkg = $cust_svc->cust_pkg;
169 $cust_main = $cust_pkg->cust_main;
170 $session->{'custnum'} = $cust_main->custnum;
171 if ( $conf->exists('pkg-balances') ) {
172 my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
173 $cust_main->ncancelled_pkgs;
174 $session->{'pkgnum'} = $cust_pkg->pkgnum
175 if scalar(@cust_pkg) > 1;
179 #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
180 #return { error => 'Only primary user may log in.' }
181 # if $conf->exists('selfservice_server-primary_only')
182 # && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
183 my $part_pkg = $cust_pkg->part_pkg;
184 return { error => 'Only primary user may log in.' }
185 if $conf->exists('selfservice_server-primary_only')
186 && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
190 return { error => "No Service Found with Mac Address ".$p->{'mac'} };
193 ## get account information
194 my ($cust_payby_card) = $cust_main->cust_payby('CARD', 'DCRD');
195 if ($cust_payby_card) {
196 $session->{'CARD'} = $cust_payby_card->custpaybynum;
198 my ($cust_payby_check) = $cust_main->cust_payby('CHEK', 'DCHK');
199 if ($cust_payby_check) {
200 $session->{'CHEK'} = $cust_payby_check->custpaybynum;
205 $session_id = sha512_hex(time(). {}. rand(). $$)
206 } until ( ! defined _cache->get($session_id) ); #just in case
208 my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
209 _cache->set( $session_id, $session, $timeout );
211 return { 'error' => '',
212 'session_id' => $session_id,
219 my $skin_info = skin_info($p);
220 if ( $p->{'session_id'} ) {
221 _cache->remove($p->{'session_id'});
222 return { %$skin_info, 'error' => '' };
224 return { %$skin_info, 'error' => "Can't resume session" }; #better error message
228 sub get_mac_address {
231 ## access radius exports acct tables to get mac
232 my @part_export = ();
234 qsearch( 'part_export', { 'exporttype' => 'sqlradius' } ),
235 qsearch( 'part_export', { 'exporttype' => 'sqlradius_withdomain' } ),
236 qsearch( 'part_export', { 'exporttype' => 'broadband_sqlradius' } ),
240 foreach my $part_export (@part_export) {
241 push @sessions, ( @{ $part_export->usage_sessions( {
243 'session_status' => 'open',
247 return { 'mac_address' => $sessions[0]->{'callingstationid'}, };
250 sub payment_only_payment_info {
252 my $session = _cache->get($p->{'session_id'})
253 or return { 'error' => "Can't resume session today" }; #better error message
255 my $custnum = $session->{'custnum'};
256 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
257 or return { 'error' => "unknown custnum $custnum" };
260 'balance' => $cust_main->balance,
263 #doubleclick protection
265 $payment_info->{'payunique'} = "webui-PaymentOnly-$_date-$$-". rand() * 2**32; #new
266 $payment_info->{'paybatch'} = $payment_info->{'payunique'}; #back compat
268 if ($session->{'CARD'}) {
269 my $card_payby = qsearchs('cust_payby', { 'custpaybynum' => $session->{'CARD'} });
271 $payment_info->{'CARD'} = $session->{'CARD'};
272 $payment_info->{'card_mask'} = $card_payby->paymask;
273 $payment_info->{'card_type'} = $card_payby->paycardtype;
277 if ($session->{'CHEK'}) {
278 my $check_payby = qsearchs('cust_payby', { 'custpaybynum' => $session->{'CHEK'} });
280 my ($payaccount, $payaba) = split /\@/, $check_payby->paymask;
281 $payment_info->{'CHEK'} = $session->{'CHEK'};
282 $payment_info->{'check_mask'} = $payaccount;
283 $payment_info->{'check_type'} = $check_payby->paytype;
287 return $payment_info;
291 sub payment_only_process_payment {
294 my $payment_info = _validate_payment($p);
295 return $payment_info if $payment_info->{'error'};
297 FS::ClientAPI::MyAccount::do_process_payment($payment_info);
302 sub _validate_payment {
305 my $session = _cache->get($p->{'session_id'})
306 or return { 'error' => "Can't resume session" }; #better error message
308 my $custnum = $session->{'custnum'};
310 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
311 or return { 'error' => "unknown custnum $custnum" };
313 $p->{'amount'} =~ /^\s*(\d+(\.\d{2})?)\s*$/
314 or return { 'error' => gettext('illegal_amount') };
316 return { error => 'Amount must be greater than 0' } unless $amount > 0;
318 #false laziness w/tr-amount_fee.html, but we don't want selfservice users
319 #changing the hidden form values
320 my $conf = new FS::Conf;
321 my $fee_display = $conf->config('selfservice_process-display') || 'add';
322 my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum);
323 my $fee_skip_first = $conf->exists('selfservice_process-skip_first');
324 if ( $fee_display eq 'add'
326 and ! $fee_skip_first || scalar($cust_main->cust_pay)
329 my $fee_pkg = qsearchs('part_pkg', { pkgpart=>$fee_pkgpart } );
330 $amount = sprintf('%.2f', $amount + $fee_pkg->option('setup_fee') );
333 #$p->{'payby'} ||= 'CARD';
334 $p->{'payby'} =~ /^([A-Z]{4})$/
335 or return { 'error' => "illegal_payby " . $p->{'payby'} };
338 ## get info from custpaybynum.
339 my $custpayby = qsearchs('cust_payby', { custpaybynum => $session->{$p->{'payby'}} } )
340 or return { 'error' => 'No payment information found' };
342 $p->{'discount_term'} =~ /^\s*(\d*)\s*$/
343 or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} };
344 my $discount_term = $1;
346 $p->{'payunique'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
347 or return { 'error' => gettext('illegal_text'). " payunique: ". $p->{'payunique'} };
350 $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
351 or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
354 $payunique = $paybatch if ! length($payunique) && length($paybatch);
355 my $payname = $custpayby->payname;
357 #false laziness w/process/payment.cgi
358 my $payinfo = $custpayby->payinfo;
361 my $replace_cust_payby;
363 if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
365 my ($payinfo1, $payinfo2) = split /\@/, $payinfo;
366 $payinfo1 =~ /^([\dx]+)$/
367 or return { 'error' => "illegal account number " };
368 $payinfo2 =~ /^([\dx]+)$/
369 or return { 'error' => "illegal ABA/routing number " };
371 } elsif ( $payby eq 'CARD' || $payby eq 'DCRD' ) {
374 $payinfo =~ /^(\d{13,19}|\d{8,9})$/
375 or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
379 or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
380 return { 'error' => gettext('unknown_card_type') }
381 if !$cust_main->tokenized($payinfo) && cardtype($payinfo) eq "Unknown";
383 if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) {
384 if ( cardtype($payinfo) eq 'American Express card' ) {
385 $p->{'paycvv'} =~ /^\s*(\d{4})\s*$/
386 or return { 'error' => "CVV2 (CID) for American Express cards is four digits." };
389 $p->{'paycvv'} =~ /^\s*(\d{3})\s*$/
390 or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
393 } elsif ( $conf->exists('selfservice-onfile_require_cvv') ) {
394 return { 'error' => 'CVV2 is required' };
395 } elsif ( !$onfile && $conf->exists('selfservice-require_cvv') ) {
396 return { 'error' => 'CVV2 is required' };
400 die "unknown payby $payby";
403 $p->{$_} = $cust_main->bill_location->get($_)
404 for qw(address1 address2 city state zip);
407 'CARD' => [ qw( paystart_month paystart_year payissue payip
408 address1 address2 city state zip country ) ],
409 'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ],
413 $card_type = cardtype($payinfo) if $payby eq 'CARD';
415 my ($year, $month, $day) = split /-/, $custpayby->{Hash}->{paydate};
418 'cust_main' => $cust_main, #XXX or just custnum??
419 'amount' => sprintf('%.2f', $amount),
421 'payinfo' => $payinfo,
422 'paymask' => $custpayby->paymask,
423 'card_type' => $card_type,
424 'paydate' => $custpayby->paydate,
425 'paydate_pretty' => $month. ' / '. $year,
428 'payname' => $custpayby->{HASH}->{payname},
429 'payunique' => $payunique,
430 'paybatch' => $paybatch,
432 'payname' => $payname,
433 'discount_term' => $discount_term,
434 'pkgnum' => $session->{'pkgnum'},
435 map { $_ => $p->{$_} } ( @{ $payby2fields{$payby} } )
442 sub _custoragent_session_custnum {
445 my($context, $session, $custnum);
446 if ( $p->{'session_id'} ) {
448 $context = 'customer';
449 $session = _cache->get($p->{'session_id'})
450 or return ( 'error' => "Can't resume session" ); #better error message
451 $custnum = $session->{'custnum'};
453 } elsif ( $p->{'agent_session_id'} ) {
456 my $agent_cache = new FS::ClientAPI_SessionCache( {
457 'namespace' => 'FS::ClientAPI::Agent',
459 $session = $agent_cache->get($p->{'agent_session_id'})
460 or return ( 'error' => "Can't resume session" ); #better error message
461 $custnum = $p->{'custnum'};
465 return ( 'error' => "Can't resume session" ); #better error message
468 ($context, $session, $custnum);