X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2FClientAPI%2FMyAccount.pm;h=9533d6a7350e67ce07af3fadec497affc1dd16f5;hb=33c725fc1419daeb11b818d12ac6cd7ec499c310;hp=b73442421a0b41e1836d56171610a02313d3e1c7;hpb=07560581aedba753147fcb3f6a5e7c30bdc2f77f;p=freeside.git diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index b73442421..9533d6a73 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -1,21 +1,26 @@ package FS::ClientAPI::MyAccount; +use 5.008; #require 5.8+ for Time::Local 1.05+ use strict; -use vars qw( $cache $DEBUG ); +use vars qw( $cache $DEBUG $me ); use subs qw( _cache _provision ); use Data::Dumper; use Digest::MD5 qw(md5_hex); use Date::Format; use Business::CreditCard; use Time::Duration; +use Time::Local qw(timelocal_nocheck); use FS::UI::Web::small_custview qw(small_custview); #less doh use FS::UI::Web; use FS::UI::bytecount qw( display_bytecount ); use FS::Conf; -use FS::Record qw(qsearch qsearchs); +#use FS::UID qw(dbh); +use FS::Record qw(qsearch qsearchs dbh); use FS::Msgcat qw(gettext); use FS::Misc qw(card_types); +use FS::Misc::DateTime qw(parse_datetime); use FS::ClientAPI_SessionCache; +use FS::cust_svc; use FS::svc_acct; use FS::svc_domain; use FS::svc_phone; @@ -28,27 +33,34 @@ use FS::cust_pkg; use FS::payby; use FS::acct_rt_transaction; use HTML::Entities; +use FS::TicketSystem; +use Text::CSV_XS; +use IO::Scalar; +use Spreadsheet::WriteExcel; $DEBUG = 0; - -#false laziness with FS::cust_main -BEGIN { - eval "use Time::Local;"; - die "Time::Local minimum version 1.05 required with Perl versions before 5.6" - if $] < 5.006 && !defined($Time::Local::VERSION); - eval "use Time::Local qw(timelocal_nocheck);"; -} +$me = '[FS::ClientAPI::MyAccount]'; use vars qw( @cust_main_editable_fields ); @cust_main_editable_fields = qw( first last company address1 address2 city - county state zip country daytime night fax + county state zip country + daytime night fax mobile ship_first ship_last ship_company ship_address1 ship_address2 ship_city - ship_state ship_zip ship_country ship_daytime ship_night ship_fax + ship_state ship_zip ship_country + ship_daytime ship_night ship_fax ship_mobile + locale payby payinfo payname paystart_month paystart_year payissue payip ss paytype paystate stateid stateid_state ); +BEGIN { #preload to reduce time customer_info takes + if ( $FS::TicketSystem::system ) { + warn "$me: initializing ticket system\n" if $DEBUG; + FS::TicketSystem->init(); + } +} + sub _cache { $cache ||= new FS::ClientAPI_SessionCache( { 'namespace' => 'FS::ClientAPI::MyAccount', @@ -56,24 +68,72 @@ sub _cache { } sub skin_info { - #my $p = shift; + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + #return { 'error' => $session } if $context eq 'error'; + + my $agentnum = ''; + if ( $context eq 'customer' ) { + + my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?') + or die dbh->errstr; + + $sth->execute($custnum) or die $sth->errstr; + + $agentnum = $sth->fetchrow_arrayref->[0] + or die "no agentnum for custnum $custnum"; + + #} elsif ( $context eq 'agent' ) { + } elsif ( defined($p->{'agentnum'}) and $p->{'agentnum'} =~ /^(\d+)$/ ) { + $agentnum = $1; + } my $conf = new FS::Conf; - use vars qw($skin_info); #cache for performance. - #agentnum eventually...? but if they're not logged in yet.. ? + #false laziness w/Signup.pm - $skin_info ||= { - 'head' => join("\n", $conf->config('selfservice-head') ), - 'body_header' => join("\n", $conf->config('selfservice-body_header') ), - 'body_footer' => join("\n", $conf->config('selfservice-body_footer') ), - 'body_bgcolor' => scalar( $conf->config('selfservice-body_bgcolor') ), - 'box_bgcolor' => scalar( $conf->config('selfservice-box_bgcolor') ), + my $skin_info_cache_agent = _cache->get("skin_info_cache_agent$agentnum"); - 'company_name' => scalar($conf->config('company_name')), - }; + if ( $skin_info_cache_agent ) { + + warn "$me loading cached skin info for agentnum $agentnum\n" + if $DEBUG > 1; + + } else { + + warn "$me populating skin info cache for agentnum $agentnum\n" + if $DEBUG > 1; + + $skin_info_cache_agent = { + 'agentnum' => $agentnum, + ( map { $_ => scalar( $conf->config($_, $agentnum) ) } + qw( company_name date_format ) ), + ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) } + qw( body_bgcolor box_bgcolor + text_color link_color vlink_color hlink_color alink_color + font title_color title_align title_size menu_bgcolor menu_fontsize + ) + ), + ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) } + qw( menu_skipblanks menu_skipheadings menu_nounderline ) + ), + ( map { $_ => scalar($conf->config_binary("selfservice-$_", $agentnum)) } + qw( title_left_image title_right_image + menu_top_image menu_body_image menu_bottom_image + ) + ), + 'logo' => scalar($conf->config_binary('logo.png', $agentnum )), + ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) } + qw( head body_header body_footer company_address ) ), + }; + + _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent); - $skin_info; + } + + #{ %$skin_info_cache_agent }; + $skin_info_cache_agent; } @@ -83,7 +143,7 @@ sub login_info { my $conf = new FS::Conf; my %info = ( - %{ skin_info() }, + %{ skin_info($p) }, 'phone_login' => $conf->exists('selfservice_server-phone_login'), 'single_domain'=> scalar($conf->config('selfservice_server-single_domain')), ); @@ -126,6 +186,13 @@ sub login { ); return { error => 'User not found.' } unless $svc_acct; + if($conf->exists('selfservice_server-login_svcpart')) { + my @svcpart = $conf->config('selfservice_server-login_svcpart'); + my $svcpart = $svc_acct->cust_svc->svcpart; + return { error => 'Invalid user.' } + unless grep($_ eq $svcpart, @svcpart); + } + return { error => 'Incorrect password.' } unless $svc_acct->check_password($p->{'password'}); @@ -176,9 +243,31 @@ sub logout { my $p = shift; if ( $p->{'session_id'} ) { _cache->remove($p->{'session_id'}); - return { %{ skin_info() }, 'error' => '' }; + return { %{ skin_info($p) }, 'error' => '' }; } else { - return { %{ skin_info() }, 'error' => "Can't resume session" }; #better error message + return { %{ skin_info($p) }, 'error' => "Can't resume session" }; #better error message + } +} + +sub payment_gateway { + # internal use only + # takes a cust_main and a cust_payby entry, returns the payment_gateway + my $conf = new FS::Conf; + my $cust_main = shift; + my $cust_payby = shift; + my $gatewaynum = $conf->config('selfservice-payment_gateway'); + if ( $gatewaynum ) { + my $pg = qsearchs('payment_gateway', { gatewaynum => $gatewaynum }); + die "configured gatewaynum $gatewaynum not found!" if !$pg; + return $pg; + } + else { + return '' if ! FS::payby->realtime($cust_payby); + my $pg = $cust_main->agent->payment_gateway( + 'method' => FS::payby->payby2bop($cust_payby), + 'nofatal' => 1 + ); + return $pg; } } @@ -207,23 +296,24 @@ sub access_info { my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) or return { 'error' => "unknown custnum $custnum" }; - $info->{hide_payment_fields} = - [ - map { return 0 unless FS::payby->realtime($_); - my $pg = $cust_main->agent->payment_gateway( - 'method' => FS::payby->payby2bop($_), - 'nofatal' => 1, - ) or return 0; - $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; - } - @{ $info->{cust_paybys} } + $info->{'hide_payment_fields'} = [ + map { + my $pg = payment_gateway($cust_main, $_); + $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; + } @{ $info->{cust_paybys} } ]; + $info->{'self_suspend_reason'} = + $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum); + + $info->{'edit_ticket_subject'} = + $conf->exists('ticket_system-selfservice_edit_subject') && + $cust_main->edit_subject; + return { %$info, - 'custnum' => $custnum, - 'pkgnum' => $session->{'pkgnum'}, - 'svcnum' => $session->{'svcnum'}, - 'nonprimary' => $session->{'nonprimary'}, + 'custnum' => $custnum, + 'access_pkgnum' => $session->{'pkgnum'}, + 'access_svcnum' => $session->{'svcnum'}, }; } @@ -241,7 +331,12 @@ sub customer_info { }else{ $return{'require_address2'} = ''; } - + + if ( $FS::TicketSystem::system ) { + warn "$me customer_info: initializing ticket system\n" if $DEBUG; + FS::TicketSystem->init(); + } + if ( $custnum ) { #customer record my $search = { 'custnum' => $custnum }; @@ -255,7 +350,25 @@ sub customer_info { $return{balance} = $cust_main->balance; } - $return{tickets} = [ ($cust_main->tickets) ]; + my @tickets = $cust_main->tickets; + # unavoidable false laziness w/ httemplate/view/cust_main/tickets.html + if ( FS::TicketSystem->selfservice_priority ) { + my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1; + $return{tickets} = [ + sort { + ( + ($a->{'_selfservice_priority'} eq '') <=> + ($b->{'_selfservice_priority'} eq '') + ) || + ( $dir * + ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'}) + ) + } @tickets + ]; + } + else { + $return{tickets} = \@tickets; + } unless ( $session->{'pkgnum'} ) { my @open = map { @@ -274,9 +387,8 @@ sub customer_info { ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance ); - warn $return{small_custview}; - $return{name} = $cust_main->first. ' '. $cust_main->get('last'); + $return{ship_name} = $cust_main->ship_first. ' '. $cust_main->get('ship_last'); for (@cust_main_editable_fields) { $return{$_} = $cust_main->get($_); @@ -313,6 +425,17 @@ sub customer_info { $return{support_services} = \@support_services; } + if ( $conf->config('prepayment_discounts-credit_type') ) { + #need to eval? + $return{discount_terms_hash} = { $cust_main->discount_terms_hash }; + } + + if ( $session->{'svcnum'} ) { + my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} }); + $return{'svc_label'} = ($cust_svc->label)[1] if $cust_svc; + $return{'svcnum'} = $session->{'svcnum'}; + } + } elsif ( $session->{'svcnum'} ) { #no customer record my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } ) @@ -332,6 +455,72 @@ sub customer_info { } +sub customer_info_short { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my %return; + + my $conf = new FS::Conf; + + if ( $custnum ) { #customer record + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + $return{small_custview} = + small_custview( $cust_main, + scalar($conf->config('countrydefault')), + 1, ##nobalance + ); + + $return{name} = $cust_main->first. ' '. $cust_main->get('last'); + $return{ship_name} = $cust_main->ship_first. ' '. $cust_main->get('ship_last'); + + $return{payby} = $cust_main->payby; + + #none of these are terribly expensive if we want 'em... + for (@cust_main_editable_fields) { + $return{$_} = $cust_main->get($_); + } + + if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { + $return{payinfo} = $cust_main->paymask; + @return{'month', 'year'} = $cust_main->paydate_monthyear; + } + + $return{'invoicing_list'} = + join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list ); + #$return{'postal_invoicing'} = + # 0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list ); + + if ( $session->{'svcnum'} ) { + my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} }); + $return{'svc_label'} = ($cust_svc->label)[1] if $cust_svc; + $return{'svcnum'} = $session->{'svcnum'}; + } + + } elsif ( $session->{'svcnum'} ) { #no customer record + + #uuh, not supproted yet... die? + return { 'error' => 'customer_info_short not yet supported as agent' }; + + } else { + + return { 'error' => 'Expired session' }; #XXX redirect to login w/this err! + + } + + return { 'error' => '', + 'custnum' => $custnum, + %return, + }; +} + sub edit_info { my $p = shift; my $session = _cache->get($p->{'session_id'}) @@ -366,25 +555,26 @@ sub edit_info { $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' ); - }elsif ( $payby =~ /^(CHEK|DCHK)$/ ) { + } elsif ( $payby =~ /^(CHEK|DCHK)$/ ) { + my $payinfo; $p->{'payinfo1'} =~ /^([\dx]+)$/ or return { 'error' => "illegal account number ". $p->{'payinfo1'} }; my $payinfo1 = $1; - $p->{'payinfo2'} =~ /^([\dx]+)$/ + $p->{'payinfo2'} =~ /^([\dx\.]+)$/ # . turned on by -require-bank-branch? or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} }; my $payinfo2 = $1; $payinfo = $payinfo1. '@'. $payinfo2; - if ( $payinfo eq $cust_main->paymask ) { - $new->payinfo($cust_main->payinfo); - } else { - $new->payinfo($payinfo); - } + $new->payinfo( ($payinfo eq $cust_main->paymask) + ? $cust_main->payinfo + : $payinfo + ); $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' ); - }elsif ( $payby =~ /^(BILL)$/ ) { + } elsif ( $payby =~ /^(BILL)$/ ) { + #no-op } elsif ( $payby ) { #notyet ready return { 'error' => "unknown payby $payby" }; } @@ -414,10 +604,10 @@ sub payment_info { #generic ## + my $conf = new FS::Conf; use vars qw($payment_info); #cache for performance unless ( $payment_info ) { - my $conf = new FS::Conf; my %states = map { $_->state => 1 } qsearch('cust_main_county', { 'country' => $conf->config('countrydefault') || 'US' @@ -452,6 +642,10 @@ sub payment_info { 'show_ss' => $conf->exists('show_ss'), 'show_stateid' => $conf->exists('show_stateid'), 'show_paystate' => $conf->exists('show_bankstate'), + + 'save_unchecked' => $conf->exists('selfservice-save_unchecked'), + + 'credit_card_surcharge_percentage' => $conf->config('credit-card-surcharge-percentage'), }; } @@ -467,16 +661,11 @@ sub payment_info { my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) or return { 'error' => "unknown custnum $custnum" }; - $return{hide_payment_fields} = - [ - map { return 0 unless FS::payby->realtime($_); - my $pg = $cust_main->agent->payment_gateway( - 'method' => FS::payby->payby2bop($_), - 'nofatal' => 1, - ) or return 0; - $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; - } - @{ $return{cust_paybys} } + $return{'hide_payment_fields'} = [ + map { + my $pg = payment_gateway($cust_main, $_); + $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; + } @{ $return{cust_paybys} } ]; $return{balance} = $cust_main->balance; #XXX pkg-balances? @@ -506,6 +695,11 @@ sub payment_info { } + if ( $conf->config('prepayment_discounts-credit_type') ) { + #need to eval? + $return{discount_terms_hash} = { $cust_main->discount_terms_hash }; + } + #doubleclick protection my $_date = time; $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; @@ -514,24 +708,31 @@ sub payment_info { %return, }; -}; +} #some false laziness with httemplate/process/payment.cgi - look there for #ACH and CVV support stuff -sub process_payment { +sub validate_payment { my $p = shift; my $session = _cache->get($p->{'session_id'}) or return { 'error' => "Can't resume session" }; #better error message - my %return; - my $custnum = $session->{'custnum'}; my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) or return { 'error' => "unknown custnum $custnum" }; + $p->{'amount'} =~ /^\s*(\d+(\.\d{2})?)\s*$/ + or return { 'error' => gettext('illegal_amount') }; + my $amount = $1; + return { error => 'Amount must be greater than 0' } unless $amount > 0; + + $p->{'discount_term'} =~ /^\s*(\d*)\s*$/ + or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} }; + my $discount_term = $1; + $p->{'payname'} =~ /^([\w \,\.\-\']+)$/ or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} }; my $payname = $1; @@ -565,18 +766,20 @@ sub process_payment { $payinfo = $p->{'payinfo'}; + #more intelligent mathing will be needed here if you change + #card_masking_method and don't remove existing paymasks $payinfo = $cust_main->payinfo if $cust_main->paymask eq $payinfo; $payinfo =~ s/\D//g; - $payinfo =~ /^(\d{13,16})$/ + $payinfo =~ /^(\d{13,16}|\d{8,9})$/ or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo $payinfo = $1; validate($payinfo) or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo return { 'error' => gettext('unknown_card_type') } - if cardtype($payinfo) eq "Unknown"; + if $payinfo !~ /^99\d{14}$/ && cardtype($payinfo) eq "Unknown"; if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) { if ( cardtype($payinfo) eq 'American Express card' ) { @@ -600,46 +803,189 @@ sub process_payment { 'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ], ); - my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $p->{'amount'}, - 'quiet' => 1, - 'payinfo' => $payinfo, - 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01', - 'payname' => $payname, - 'paybatch' => $paybatch, #this doesn't actually do anything - 'paycvv' => $paycvv, - 'pkgnum' => $session->{'pkgnum'}, - map { $_ => $p->{$_} } @{ $payby2fields{$payby} } + my $card_type = ''; + $card_type = cardtype($payinfo) if $payby eq 'CARD'; + + { + 'cust_main' => $cust_main, #XXX or just custnum?? + 'amount' => $amount, + 'payby' => $payby, + 'payinfo' => $payinfo, + 'paymask' => $cust_main->mask_payinfo( $payby, $payinfo ), + 'card_type' => $card_type, + 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01', + 'paydate_pretty' => $p->{'month'}. ' / '. $p->{'year'}, + 'payname' => $payname, + 'paybatch' => $paybatch, #this doesn't actually do anything + 'paycvv' => $paycvv, + 'payname' => $payname, + 'discount_term' => $discount_term, + 'pkgnum' => $session->{'pkgnum'}, + map { $_ => $p->{$_} } ( @{ $payby2fields{$payby} }, + qw( save auto ), + ) + }; + +} + +sub store_payment { + my $p = shift; + + my $validate = validate_payment($p); + return $validate if $validate->{'error'}; + + my $conf = new FS::Conf; + my $timeout = $conf->config('selfservice-session_timeout') || '1 hour'; #? + _cache->set( 'payment_'.$p->{'session_id'}, $validate, $timeout ); + + +{ map { $_=>$validate->{$_} } + qw( card_type paymask payname paydate_pretty amount ) + }; + +} + +sub process_stored_payment { + my $p = shift; + + my $session_id = $p->{'session_id'}; + + my $payment_info = _cache->get( "payment_$session_id" ) + or return { 'error' => "Can't resume session" }; #better error message + + do_process_payment($payment_info); + +} + +sub process_payment { + my $p = shift; + + my $payment_info = validate_payment($p); + return $payment_info if $payment_info->{'error'}; + + do_process_payment($payment_info); + +} + +sub do_process_payment { + my $validate = shift; + + my $cust_main = $validate->{'cust_main'}; + + my $amount = delete $validate->{'amount'}; + my $paynum = ''; + + my $payby = delete $validate->{'payby'}; + + my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount, + 'quiet' => 1, + 'selfservice' => 1, + 'paynum_ref' => \$paynum, + %$validate, ); return { 'error' => $error } if $error; $cust_main->apply_payments; - if ( $p->{'save'} ) { + if ( $validate->{'save'} ) { my $new = new FS::cust_main { $cust_main->hash }; - if ($payby eq 'CARD' || $payby eq 'DCRD') { - $new->set( $_ => $p->{$_} ) - foreach qw( payinfo payname paystart_month paystart_year payissue payip + if ($validate->{'payby'} eq 'CARD' || $validate->{'payby'} eq 'DCRD') { + $new->set( $_ => $validate->{$_} ) + foreach qw( payname paystart_month paystart_year payissue payip address1 address2 city state zip country ); - $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' ); + $new->set( 'payby' => $validate->{'auto'} ? 'CARD' : 'DCRD' ); } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') { - $new->set( $_ => $p->{$_} ) + $new->set( $_ => $validate->{$_} ) foreach qw( payname payip paytype paystate stateid stateid_state ); - $new->set( 'payinfo' => $payinfo ); - $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' ); + $new->set( 'payby' => $validate->{'auto'} ? 'CHEK' : 'DCHK' ); } - $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' ); + $new->set( 'payinfo' => $cust_main->card_token || $validate->{'payinfo'} ); + $new->set( 'paydate' => $validate->{'paydate'} ); my $error = $new->replace($cust_main); - return { 'error' => $error } if $error; - $cust_main = $new; + if ( $error ) { + #no, this causes customers to process their payments again + #return { 'error' => $error }; + #XXX just warn verosely for now so i can figure out how these happen in + # the first place, eventually should redirect them to the "change + #address" page but indicate the payment did process?? + delete($validate->{'payinfo'}); #don't want to log this! + warn "WARNING: error changing customer info when processing payment (not returning to customer as a processing error): $error\n". + "NEW: ". Dumper($new)."\n". + "OLD: ". Dumper($cust_main)."\n". + "PACKET: ". Dumper($validate)."\n"; + #} else { + #not needed... + #$cust_main = $new; + } } - return { 'error' => '' }; + my $cust_pay = ''; + my $receipt_html = ''; + if ($paynum) { + # currently supported for realtime CC only; send receipt data to SS + $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } ); + if($cust_pay) { + $receipt_html = qq! +
Payment# | +! . $cust_pay->paynum . qq! | +
Date | + +! . + time2str("%a %b %o, %Y %r", $cust_pay->_date) + . qq! | +
Amount | +! . sprintf('%.2f', $cust_pay->paid) . qq! | + +
Payment method | +! . $cust_pay->payby_name .' #'. $cust_pay->paymask + . qq! | +