#eval "use Time::Local qw(timelocal timelocal_nocheck);";
eval "use Time::Local qw(timelocal_nocheck);";
}
+use Digest::MD5 qw(md5_base64);
use Date::Format;
#use Date::Manip;
use String::Approx qw(amatch);
-use Business::CreditCard;
+use Business::CreditCard 0.28;
use FS::UID qw( getotaker dbh );
use FS::Record qw( qsearchs qsearch dbdef );
use FS::Misc qw( send_email );
+use FS::Msgcat qw(gettext);
use FS::cust_pkg;
use FS::cust_svc;
use FS::cust_bill;
use FS::cust_bill_event;
use FS::cust_tax_exempt;
use FS::type_pkgs;
-use FS::Msgcat qw(gettext);
+use FS::payment_gateway;
+use FS::agent_payment_gateway;
+use FS::banned_pay;
@ISA = qw( FS::Record );
return $paymask;
}
+=item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
+=item paystart_month - start date month (maestro/solo cards only)
+=item paystart_year - start date year (maestro/solo cards only)
-=item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
+=item payissue - issue number (maestro/solo cards only)
=item payname - name on card or billing name
+=item payip - IP address from which payment information was received
+
=item tax - tax exempt, empty or `Y'
=item otaker - order taker (assigned automatically, see L<FS::UID>)
}
if ( $amount ) {
- $error = $self->insert_prepay($amount, $prepay_identifier);
+ $error = $self->insert_cust_pay_prepay($amount, $prepay_identifier);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "inserting prepayment (transaction rolled back): $error";
my $cust_pay = new FS::cust_pay {
'custnum' => $self->custnum,
- 'paid' => $amount,
+ 'paid' => sprintf('%.2f', $amount),
#'_date' => #date the prepaid card was purchased???
'payby' => 'PREP',
'payinfo' => $payinfo,
} ) ) {
return "Unknown ship_state/ship_county/ship_country: ".
$self->ship_state. "/". $self->ship_county. "/". $self->ship_country
- unless qsearchs('cust_main_county',{
+ unless qsearch('cust_main_county',{
'state' => $self->ship_state,
'county' => $self->ship_county,
'country' => $self->ship_country,
$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
or return "Illegal payby: ". $self->payby;
+ $error = $self->ut_numbern('paystart_month')
+ || $self->ut_numbern('paystart_year')
+ || $self->ut_numbern('payissue')
+ ;
+ return $error if $error;
+
+ if ( $self->payip eq '' ) {
+ $self->payip('');
+ } else {
+ $error = $self->ut_ip('payip');
+ return $error if $error;
+ }
+
# If it is encrypted and the private key is not availaible then we can't
# check the credit card.
$self->payby($1);
- if ( $check_payinfo && ($self->payby eq 'CARD' || $self->payby eq 'DCRD')) {
+ if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
$self->payinfo($payinfo);
validate($payinfo)
or return gettext('invalid_card'); # . ": ". $self->payinfo;
+
return gettext('unknown_card_type')
if cardtype($self->payinfo) eq "Unknown";
+
+ my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
+ return "Banned credit card" if $ban;
+
if ( defined $self->dbdef_table->column('paycvv') ) {
if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
if ( cardtype($self->payinfo) eq 'American Express card' ) {
}
}
- } elsif ($check_payinfo && ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' )) {
+ my $cardtype = cardtype($payinfo);
+ if ( $cardtype =~ /^(Switch|Solo)$/i ) {
+
+ return "Start date or issue number is required for $cardtype cards"
+ unless $self->paystart_month && $self->paystart_year or $self->payissue;
+
+ return "Start month must be between 1 and 12"
+ if $self->paystart_month
+ and $self->paystart_month < 1 || $self->paystart_month > 12;
+
+ return "Start year must be 1990 or later"
+ if $self->paystart_year
+ and $self->paystart_year < 1990;
+
+ return "Issue number must be beween 1 and 99"
+ if $self->payissue
+ and $self->payissue < 1 || $self->payissue > 99;
+
+ } else {
+ $self->paystart_month('');
+ $self->paystart_year('');
+ $self->payissue('');
+ }
+
+ } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/[^\d\@]//g;
$self->payinfo($payinfo);
$self->paycvv('') if $self->dbdef_table->column('paycvv');
+ my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
+ return "Banned ACH account" if $ban;
+
} elsif ( $self->payby eq 'LECB' ) {
my $payinfo = $self->payinfo;
Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
-Available options are: I<quiet>
+Available options are: I<quiet>, I<reasonnum>, and I<ban>
I<quiet> can be set true to supress email cancellation notices.
+# I<reasonnum> can be set to a cancellation reason (see L<FS::cancel_reason>)
+
+I<ban> can be set true to ban this customer's credit card or ACH information,
+if present.
+
Always returns a list: an empty list on success or a list of errors.
=cut
sub cancel {
my $self = shift;
+ my %opt = @_;
+
+ if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
+
+ #should try decryption (we might have the private key)
+ # and if not maybe queue a job for the server that does?
+ return ( "Can't (yet) ban encrypted credit cards" )
+ if $self->is_encrypted($self->payinfo);
+
+ my $ban = new FS::banned_pay $self->_banned_pay_hashref;
+ my $error = $ban->insert;
+ return ( $error ) if $error;
+
+ }
+
grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
}
+sub _banned_pay_hashref {
+ my $self = shift;
+
+ my %payby2ban = (
+ 'CARD' => 'CARD',
+ 'DCRD' => 'CARD',
+ 'CHEK' => 'CHEK',
+ 'DCHK' => 'CHEK'
+ );
+
+ {
+ 'payby' => $payby2ban{$self->payby},
+ 'payinfo' => md5_base64($self->payinfo),
+ #'reason' =>
+ };
+}
+
=item agent
Returns the agent (see L<FS::agent>) for this customer.
$options{'description'} ||= 'Internet services';
- #pre-requisites
- die "Real-time processing not enabled\n"
- unless $conf->exists('business-onlinepayment');
eval "use Business::OnlinePayment";
die $@ if $@;
- #load up config
- my $bop_config = 'business-onlinepayment';
- $bop_config .= '-ach'
- if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
- my ( $processor, $login, $password, $action, @bop_options ) =
- $conf->config($bop_config);
- $action ||= 'normal authorization';
- pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
- die "No real-time processor is enabled - ".
- "did you set the business-onlinepayment configuration value?\n"
- unless $processor;
+ my $payinfo = exists($options{'payinfo'})
+ ? $options{'payinfo'}
+ : $self->payinfo;
- #massage data
+ ###
+ # select a gateway
+ ###
+
+ my $taxclass = '';
+ if ( $options{'invnum'} ) {
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
+ die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
+ my @taxclasses =
+ map { $_->part_pkg->taxclass }
+ grep { $_ }
+ map { $_->cust_pkg }
+ $cust_bill->cust_bill_pkg;
+ unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
+ #different taxclasses
+ $taxclass = $taxclasses[0];
+ }
+ }
+
+ #look for an agent gateway override first
+ my $cardtype;
+ if ( $method eq 'CC' ) {
+ $cardtype = cardtype($payinfo);
+ } elsif ( $method eq 'ECHECK' ) {
+ $cardtype = 'ACH';
+ } else {
+ $cardtype = $method;
+ }
+
+ my $override =
+ qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => $cardtype,
+ taxclass => $taxclass, } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => '',
+ taxclass => $taxclass, } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => $cardtype,
+ taxclass => '', } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => '',
+ taxclass => '', } );
+
+ my $payment_gateway = '';
+ my( $processor, $login, $password, $action, @bop_options );
+ if ( $override ) { #use a payment gateway override
+
+ $payment_gateway = $override->payment_gateway;
+
+ $processor = $payment_gateway->gateway_module;
+ $login = $payment_gateway->gateway_username;
+ $password = $payment_gateway->gateway_password;
+ $action = $payment_gateway->gateway_action;
+ @bop_options = $payment_gateway->options;
+
+ } else { #use the standard settings from the config
+
+ ( $processor, $login, $password, $action, @bop_options ) =
+ $self->default_payment_gateway($method);
+
+ }
+
+ ###
+ # massage data
+ ###
my $address = exists($options{'address1'})
? $options{'address1'}
? $conf->config('business-onlinepayment-email-override')
: $invoicing_list[0];
- my $payinfo = exists($options{'payinfo'})
- ? $options{'payinfo'}
- : $self->payinfo;
-
my %content = ();
if ( $method eq 'CC' ) {
$paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
$content{expiration} = "$2/$1";
- if ( defined $self->dbdef_table->column('paycvv') ) {
- my $paycvv = exists($options{'paycvv'})
- ? $options{'paycvv'}
- : $self->paycvv;
- $content{cvv2} = $self->paycvv
- if length($paycvv);
- }
+ my $paycvv = exists($options{'paycvv'})
+ ? $options{'paycvv'}
+ : $self->paycvv;
+ $content{cvv2} = $self->paycvv
+ if length($paycvv);
+
+ my $paystart_month = exists($options{'paystart_month'})
+ ? $options{'paystart_month'}
+ : $self->paystart_month;
+
+ my $paystart_year = exists($options{'paystart_year'})
+ ? $options{'paystart_year'}
+ : $self->paystart_year;
+
+ $content{card_start} = "$paystart_month/$paystart_year"
+ if $paystart_month && $paystart_year;
+
+ my $payissue = exists($options{'payissue'})
+ ? $options{'payissue'}
+ : $self->payissue;
+ $content{issue_number} = $payissue if $payissue;
+
+ my $payip = exists($options{'payip'})
+ ? $options{'payip'}
+ : $self->payip;
+ $content{customer_ip} = $payip
+ if length($payip);
$content{recurring_billing} = 'YES'
if qsearch('cust_pay', { 'custnum' => $self->custnum,
$content{phone} = $payinfo;
}
- #transaction(s)
+ ###
+ # run transaction(s)
+ ###
my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
}
- #remove paycvv after initial transaction
+ ###
+ # remove paycvv after initial transaction
+ ###
+
#false laziness w/misc/process/payment.cgi - check both to make sure working
# correctly
if ( defined $self->dbdef_table->column('paycvv')
}
}
- #result handling
+ ###
+ # result handling
+ ###
+
if ( $transaction->is_success() ) {
my %method2payby = (
'LEC' => 'LECB',
);
- my $paybatch = "$processor:". $transaction->authorization;
+ my $paybatch = '';
+ if ( $payment_gateway ) { # agent override
+ $paybatch = $payment_gateway->gatewaynum. '-';
+ }
+
+ $paybatch .= "$processor:". $transaction->authorization;
+
$paybatch .= ':'. $transaction->order_number
if $transaction->can('order_number')
&& length($transaction->order_number);
}
+=item default_payment_gateway
+
+=cut
+
+sub default_payment_gateway {
+ my( $self, $method ) = @_;
+
+ die "Real-time processing not enabled\n"
+ unless $conf->exists('business-onlinepayment');
+
+ #load up config
+ my $bop_config = 'business-onlinepayment';
+ $bop_config .= '-ach'
+ if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
+ my ( $processor, $login, $password, $action, @bop_options ) =
+ $conf->config($bop_config);
+ $action ||= 'normal authorization';
+ pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+ die "No real-time processor is enabled - ".
+ "did you set the business-onlinepayment configuration value?\n"
+ unless $processor;
+
+ ( $processor, $login, $password, $action, @bop_options )
+}
+
=item remove_cvv
Removes the I<paycvv> field from the database directly.
warn " $_ => $options{$_}\n" foreach keys %options;
}
- #pre-requisites
- die "Real-time processing not enabled\n"
- unless $conf->exists('business-onlinepayment');
eval "use Business::OnlinePayment";
die $@ if $@;
- #load up config
- my $bop_config = 'business-onlinepayment';
- $bop_config .= '-ach'
- if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
- my ( $processor, $login, $password, $unused_action, @bop_options ) =
- $conf->config($bop_config);
- #$action ||= 'normal authorization';
- pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
- die "No real-time processor is enabled - ".
- "did you set the business-onlinepayment configuration value?\n"
- unless $processor;
+ ###
+ # look up the original payment and optionally a gateway for that payment
+ ###
my $cust_pay = '';
my $amount = $options{'amount'};
- my( $pay_processor, $auth, $order_number ) = ( '', '', '' );
+
+ my( $processor, $login, $password, @bop_options ) ;
+ my( $auth, $order_number ) = ( '', '', '' );
+
if ( $options{'paynum'} ) {
+
warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG;
$cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
or return "Unknown paynum $options{'paynum'}";
$amount ||= $cust_pay->paid;
- $cust_pay->paybatch =~ /^(\w+):([\w-]*)(:(\w+))?$/
+
+ $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-]*)(:([\w\-]+))?$/
or return "Can't parse paybatch for paynum $options{'paynum'}: ".
$cust_pay->paybatch;
- ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 );
- return "processor of payment $options{'paynum'} $pay_processor does not".
- " match current processor $processor"
- unless $pay_processor eq $processor;
+ my $gatewaynum = '';
+ ( $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;
+ $login = $payment_gateway->gateway_username;
+ $password = $payment_gateway->gateway_password;
+ @bop_options = $payment_gateway->options;
+
+ } else { #try the default gateway
+
+ my( $conf_processor, $unused_action );
+ ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
+ $self->default_payment_gateway($method);
+
+ return "processor of payment $options{'paynum'} $processor does not".
+ " match default processor $conf_processor"
+ unless $processor eq $conf_processor;
+
+ }
+
+
+ } else { # didn't specify a paynum, so look for agent gateway overrides
+ # like a normal transaction
+
+ my $cardtype;
+ if ( $method eq 'CC' ) {
+ $cardtype = cardtype($self->payinfo);
+ } elsif ( $method eq 'ECHECK' ) {
+ $cardtype = 'ACH';
+ } else {
+ $cardtype = $method;
+ }
+ my $override =
+ qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => $cardtype,
+ taxclass => '', } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => '',
+ taxclass => '', } );
+
+ if ( $override ) { #use a payment gateway override
+
+ my $payment_gateway = $override->payment_gateway;
+
+ $processor = $payment_gateway->gateway_module;
+ $login = $payment_gateway->gateway_username;
+ $password = $payment_gateway->gateway_password;
+ #$action = $payment_gateway->gateway_action;
+ @bop_options = $payment_gateway->options;
+
+ } else { #use the standard settings from the config
+
+ my $unused_action;
+ ( $processor, $login, $password, $unused_action, @bop_options ) =
+ $self->default_payment_gateway($method);
+
+ }
+
}
return "neither amount nor paynum specified" unless $amount;
sub name {
my $self = shift;
- my $name = $self->get('last'). ', '. $self->first;
+ my $name = $self->contact;
$name = $self->company. " ($name)" if $self->company;
$name;
}
+=item ship_name
+
+Returns a name string for this (service/shipping) contact, either
+"Company (Last, First)" or "Last, First".
+
+=cut
+
+sub ship_name {
+ my $self = shift;
+ if ( $self->get('ship_last') ) {
+ my $name = $self->ship_contact;
+ $name = $self->ship_company. " ($name)" if $self->ship_company;
+ $name;
+ } else {
+ $self->name;
+ }
+}
+
+=item contact
+
+Returns this customer's full (billing) contact name only, "Last, First"
+
+=cut
+
+sub contact {
+ my $self = shift;
+ $self->get('last'). ', '. $self->first;
+}
+
+=item ship_contact
+
+Returns this customer's full (shipping) contact name only, "Last, First"
+
+=cut
+
+sub ship_contact {
+ my $self = shift;
+ $self->get('ship_last')
+ ? $self->get('ship_last'). ', '. $self->ship_first
+ : $self->contact;
+}
+
=item status
Returns a status string for this customer, currently:
=cut
-my $recurring_sql = "
- '0' != ( select freq from part_pkg
- where cust_pkg.pkgpart = part_pkg.pkgpart )
-";
-
sub active_sql { "
0 < ( SELECT COUNT(*) FROM cust_pkg
WHERE cust_pkg.custnum = cust_main.custnum
- AND $recurring_sql
- AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
- AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
+ AND ". FS::cust_pkg->active_sql. "
)
"; }
=cut
+#my $recurring_sql = FS::cust_pkg->recurring_sql;
+my $recurring_sql = "
+ '0' != ( select freq from part_pkg
+ where cust_pkg.pkgpart = part_pkg.pkgpart )
+";
+
sub suspended_sql { susp_sql(@_); }
sub susp_sql { "
0 < ( SELECT COUNT(*) FROM cust_pkg
)
AND 0 = ( SELECT COUNT(*) FROM cust_pkg
WHERE cust_pkg.custnum = cust_main.custnum
- AND $recurring_sql
- AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
- AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
+ AND ". FS::cust_pkg->active_sql. "
)
"; }