X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_pay.pm;h=78c09a3d02cba56d423a336c2128a7091e904ce2;hb=6eae8e86517298d4de53d9754487d53b4f29765d;hp=a7d69901fe991056024ccbb44bd6992a4391ccc5;hpb=550685eff557af23e242c545d6a9e27a7ef44f23;p=freeside.git diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index a7d69901f..78c09a3d0 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -1,20 +1,22 @@ package FS::cust_pay; use strict; -use vars qw( @ISA $conf $unsuspendauto $ignore_noapply ); +use vars qw( @ISA $conf $unsuspendauto $ignore_noapply @encrypted_fields ); use Date::Format; use Business::CreditCard; use Text::Template; use FS::Misc qw(send_email); use FS::Record qw( dbh qsearch qsearchs ); +use FS::payby; use FS::cust_main_Mixin; +use FS::payinfo_Mixin; use FS::cust_bill; use FS::cust_bill_pay; use FS::cust_pay_refund; use FS::cust_main; use FS::cust_pay_void; -@ISA = qw( FS::cust_main_Mixin FS::Record ); +@ISA = qw(FS::Record FS::cust_main_Mixin FS::payinfo_Mixin ); $ignore_noapply = 0; @@ -24,6 +26,8 @@ FS::UID->install_callback( sub { $unsuspendauto = $conf->exists('unsuspendauto'); } ); +@encrypted_fields = ('payinfo'); + =head1 NAME FS::cust_pay - Object methods for cust_pay objects @@ -60,12 +64,15 @@ currently supported: =item _date - specified as a UNIX timestamp; see L. Also see L and L for conversion functions. -=item payby - `CARD' (credit cards), `CHEK' (electronic check/ACH), -`LECB' (phone bill billing), `BILL' (billing), or `COMP' (free) +=item payby - Payment Type (See L for valid payby values) + +=item payinfo - Payment Information (See L for data format) + +=item paymask - Masked payinfo (See L for how this works) -=item payinfo - card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively +=item paybatch - text field for tracking card processing or other batch grouping -=item paybatch - text field for tracking card processing +=item payunique - Optional unique identifer to prevent duplicate transactions. =item closed - books closed flag, empty or `Y' @@ -95,12 +102,15 @@ Adds this payment to the database. For backwards-compatibility and convenience, if the additional field invnum is defined, an FS::cust_bill_pay record for the full amount of the payment -will be created. In this case, custnum is optional. +will be created. In this case, custnum is optional. An hash of optional +arguments may be passed. Currently "manual" is supported. If true, a +payment receipt is sent instead of a statement when 'payment_receipt_email' +configuration option is set. =cut sub insert { - my $self = shift; + my ($self, %options) = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -113,8 +123,9 @@ sub insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; + my $cust_bill; if ( $self->invnum ) { - my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } ) + $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } ) or do { $dbh->rollback if $oldAutoCommit; return "Unknown cust_bill.invnum: ". $self->invnum; @@ -154,17 +165,6 @@ sub insert { } } - if ( $self->paybatch =~ /^webui-/ ) { - my @cust_pay = qsearch('cust_pay', { - 'custnum' => $self->custnum, - 'paybatch' => $self->paybatch, - } ); - if ( scalar(@cust_pay) > 1 ) { - $dbh->rollback if $oldAutoCommit; - return "a payment with webui token ". $self->paybatch. " already exists"; - } - } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; #false laziness w/ cust_credit::insert @@ -185,27 +185,36 @@ sub insert { && grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list ) { - my $receipt_template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ], - ) or do { - warn "can't create payment receipt template: $Text::Template::ERROR"; - return ''; - }; + $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though? - my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list; + my $error; + if ( ( exists($options{'manual'}) && $options{'manual'} ) + || ! $conf->exists('invoice_html_statement') + || ! $cust_bill + ) { - my $payby = $self->payby; - my $payinfo = $self->payinfo; - $payby =~ s/^BILL$/Check/ if $payinfo; - $payinfo = $self->payinfo_masked if $payby eq 'CARD' || $payby eq 'CHEK'; - $payby =~ s/^CHEK$/Electronic check/; + my $receipt_template = new Text::Template ( + TYPE => 'ARRAY', + SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ], + ) or do { + warn "can't create payment receipt template: $Text::Template::ERROR"; + return ''; + }; - my $error = send_email( - 'from' => $conf->config('invoice_from'), #??? well as good as any - 'to' => \@invoicing_list, - 'subject' => 'Payment receipt', - 'body' => [ $receipt_template->fill_in( HASH => { + my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } + $cust_main->invoicing_list; + + my $payby = $self->payby; + my $payinfo = $self->payinfo; + $payby =~ s/^BILL$/Check/ if $payinfo; + $payinfo = $self->paymask if $payby eq 'CARD' || $payby eq 'CHEK'; + $payby =~ s/^CHEK$/Electronic check/; + + $error = send_email( + 'from' => $conf->config('invoice_from'), #??? well as good as any + 'to' => \@invoicing_list, + 'subject' => 'Payment receipt', + 'body' => [ $receipt_template->fill_in( HASH => { 'date' => time2str("%a %B %o, %Y", $self->_date), 'name' => $cust_main->name, 'paynum' => $self->paynum, @@ -213,10 +222,24 @@ sub insert { 'payby' => ucfirst(lc($payby)), 'payinfo' => $payinfo, 'balance' => $cust_main->balance, - } ) ], - ); + } ) ], + ); + + } else { + + my $queue = new FS::queue { + 'paynum' => $self->paynum, + 'job' => 'FS::cust_bill::queueable_email', + }; + $error = $queue->insert( + 'invnum' => $cust_bill->invnum, + 'template' => 'statement', + ); + + } + if ( $error ) { - warn "can't send payment receipt: $error"; + warn "can't send payment receipt/statement: $error"; } } @@ -270,12 +293,14 @@ sub void { =item delete -Deletes this payment and all associated applications (see L), -unless the closed flag is set. In most cases, you want to use the void -method instead to leave a record of the deleted payment. +Unless the closed flag is set, deletes this payment and all associated +applications (see L and L). In most +cases, you want to use the void method instead to leave a record of the +deleted payment. =cut +# very similar to FS::cust_credit::delete sub delete { my $self = shift; return "Can't delete closed payment" if $self->closed =~ /^Y/i; @@ -323,7 +348,7 @@ sub delete { 'paid: $'. sprintf("%.2f", $self->paid). "\n", 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n", 'payby: '. $self->payby. "\n", - 'payinfo: '. $self->payinfo. "\n", + 'payinfo: '. $self->paymask. "\n", 'paybatch: '. $self->paybatch. "\n", ], ); @@ -343,7 +368,16 @@ sub delete { =item replace OLD_RECORD -You probably shouldn't modify payments... +You can, but probably shouldn't modify payments... + +=cut + +sub replace { + #return "Can't modify payment!" + my $self = shift; + return "Can't modify closed payment" if $self->closed =~ /^Y/i; + $self->SUPER::replace(@_); +} =item check @@ -361,7 +395,9 @@ sub check { || $self->ut_money('paid') || $self->ut_numbern('_date') || $self->ut_textn('paybatch') + || $self->ut_textn('payunique') || $self->ut_enum('closed', [ '', 'Y' ]) + || $self->payinfo_check() ; return $error if $error; @@ -373,27 +409,14 @@ sub check { $self->_date(time) unless $self->_date; - $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREP)$/ or return "Illegal payby"; - $self->payby($1); - - #false laziness with cust_refund::check - if ( $self->payby eq 'CARD' ) { - my $payinfo = $self->payinfo; - $payinfo =~ s/\D//g; - $self->payinfo($payinfo); - if ( $self->payinfo ) { - $self->payinfo =~ /^(\d{13,16})$/ - or return "Illegal (mistyped?) credit card number (payinfo)"; - $self->payinfo($1); - validate($self->payinfo) or return "Illegal credit card number"; - return "Unknown card type" if cardtype($self->payinfo) eq "Unknown"; - } else { - $self->payinfo('N/A'); - } - - } else { - $error = $self->ut_textn('payinfo'); - return $error if $error; + # UNIQUE index should catch this too, without race conditions, but this + # should give a better error message the other 99.9% of the time... + if ( length($self->payunique) + && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) { + #well, it *could* be a better error message + return "duplicate transaction". + " - a payment with unique identifer ". $self->payunique. + " already exists"; } $self->SUPER::check; @@ -435,7 +458,7 @@ sub batch_insert { my $errors = 0; my @errors = map { - my $error = $_->insert; + my $error = $_->insert( 'manual' => 1 ); if ( $error ) { $errors++; } else { @@ -526,33 +549,109 @@ sub cust_main { qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); } -=item payinfo_masked +=item payby_name -Returns a "masked" payinfo field with all but the last four characters replaced -by 'x'es. Useful for displaying credit cards. +Returns a name for the payby field. =cut -sub payinfo_masked { +sub payby_name { my $self = shift; - #some false laziness w/cust_main::paymask - if ( $self->payby eq 'CARD' ) { - my $payinfo = $self->payinfo; - 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4)); - } elsif ( $self->payby eq 'CHEK' ) { - my( $account, $aba ) = split('@', $self->payinfo ); - 'x'x(length($account)-2). substr($account,(length($account)-2)). "@". $aba; - } else { - $self->payinfo; + FS::payby->shortname( $self->payby ); +} + +=item gatewaynum + +Returns a gatewaynum for the processing gateway. + +=item processor + +Returns a name for the processing gateway. + +=item authorization + +Returns a name for the processing gateway. + +=item order_number + +Returns a name for the processing gateway. + +=cut + +sub gatewaynum { shift->_parse_paybatch->{'gatewaynum'}; } +sub processor { shift->_parse_paybatch->{'processor'}; } +sub authorization { shift->_parse_paybatch->{'authorization'}; } +sub order_number { shift->_parse_paybatch->{'order_number'}; } + +#sucks that this stuff is in paybatch like this in the first place, +#but at least other code can start to use new field names +#(code nicked from FS::cust_main::realtime_refund_bop) +sub _parse_paybatch { + my $self = shift; + + $self->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/ + or return {}; + #"Can't parse paybatch for paynum $options{'paynum'}: ". + # $cust_pay->paybatch; + + my( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 ); + + if ( $gatewaynum ) { #gateway for the payment to be refunded + + my $payment_gateway = + qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } ); + + die "payment gateway $gatewaynum not found" #? + unless $payment_gateway; + + $processor = $payment_gateway->gateway_module; + } + + { + 'gatewaynum' => $gatewaynum, + 'processor' => $processor, + 'authorization' => $auth, + 'order_number' => $order_number, + }; + +} + +=back + +=head1 CLASS METHODS + +=over 4 + +=item unapplied_sql + +Returns an SQL fragment to retreive the unapplied amount. + +=cut + +sub unapplied_sql { + #my $class = shift; + + "paid + - COALESCE( + ( SELECT SUM(amount) FROM cust_bill_pay + WHERE cust_pay.paynum = cust_bill_pay.paynum ) + ,0 + ) + - COALESCE( + ( SELECT SUM(amount) FROM cust_pay_refund + WHERE cust_pay.paynum = cust_pay_refund.paynum ) + ,0 + ) + "; + } =back =head1 BUGS -Delete and replace methods. payinfo_masked false laziness with cust_main.pm -and cust_refund.pm +Delete and replace methods. =head1 SEE ALSO