#################### main pod documentation begin ###################
=head1 NAME
Zymonic::Condition::PayerAuthentication - Zymonic Workflow Condition module
=head1 SYNOPSIS
Conditions on PayerAuthentication.
=head1 DESCRIPTION
Will check card number (retrieved via Zymonic::Decryptor) for payer
authentication.
Condition will pass when card is not enrolled, or when PARes has been
sucessfully received after authentication.
Condition will fail when card is enrolled, setting ACS URL and PAReq fields
allowing the user to perform the authentication and try again with PARes.
=head1 USAGE
The following ClassOption Schema should be used:
EncryptedCardNumber is the field to look in to get the encrypted card number
to pass to the decryptor. ACS_URL, PAReq and Issuer_Cert are fields that will be
set with the values a user should user to do the authentication. Will only be
set if card is enrolled.
PARes is the field to look in to get the user's sent PARes value.
Device_Category should be the field which determines if user is using a mobile
device. If user is using a mobile device, also need to pass back PAReq and
Issuer_Cert. ECI, PARes_id and PARes_auth_code are fields that will be set after a
successful PARes has been passed back by the user.
Status is a field which will be updates with the current status of the
authentication. Format is:
"Y" the cardholder completed authentication correctly.
"A" the attempted authentication was recorded.
"U" a system error prevented authentication from completing.
"N" the cardholder did not complete authentication
"" the digital certificate was invalid.
"Q" card not enrolled. (original P, but that is used for pending)
"E" awaiting authentication.
Format may be defined differently by the PayerAuthentication module being used.
Can set in zz_system_options a value with name PayerAuthenticationModule and this
will be used instead of this value.
SkipPayerAuthentication, if Y, will skip payer auth.
=head1 BUGS
NONE
=head1 SUPPORT
As in the license, Zymonic is provided without warranty or support
unless purchased separately, however... If you email
zymonic-support@zednax.com your issue will be noted and may
receive a response.
For security issues, please contact zymonic-security@zednax.com and
someone will respond within 8 working hours.
=head1 AUTHOR
Alex Masidlover et al.
CPAN ID: MODAUTHOR
Zednax Limited
alex.masidlover@zednax.com
http://www.zednax.com
=head1 COPYRIGHT
This program is free software licensed under the...
Zymonic Public License 1.0
The full text of the license can be found in the
LICENSE file included with this module.
Other licenses may be acceptable if including
parts of Zymonic in larger projects, please
contact Zednax for details.
=head1 SEE ALSO
perl(1).
=cut
#################### main pod documentation end ###################
package Zymonic::Condition::PayerAuthentication;
use strict;
BEGIN
{
use Exporter ();
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
$VERSION = 'D1-r7507';
@ISA = qw(Exporter);
#Give a hoot don't pollute, do not export more than needed by default
@EXPORT = qw();
@EXPORT_OK = qw();
%EXPORT_TAGS = ();
}
use base "Zymonic::Condition";
use Zymonic::Decryptor::Client;
use Zymonic::Table;
use Zymonic::Utils qw(get_array debug deep_replace_card_numbers clean);
use Data::Dumper;
use Zymonic;
use Exception::Class (
'Zymonic::Exception::Decryptor' => {
isa => 'Zymonic::Exception',
fields => ['decryptor'],
description => 'Decryptor related exception',
},
'Zymonic::Exception::Decryptor::Condition' => {
isa => 'Zymonic::Exception::Decryptor',
fields => ['decryptor'],
description => 'Decryptor related exception',
},
'Zymonic::Exception::Decryptor::Condition::PayerAuthentication' => {
isa => 'Zymonic::Exception::Decryptor::Condition',
fields => ['decryptor'],
description => 'Decryptor related exception',
},
'Zymonic::Exception::Decryptor::Condition::PayerAuthentication::NoDecryptorResponse' => {
isa => 'Zymonic::Exception::Decryptor::Condition::PayerAuthentication',
fields => [],
description => 'No response received from decryptor',
},
'Zymonic::Exception::Decryptor::Condition::PayerAuthentication::InvalidDecryptorResponse' => {
isa => 'Zymonic::Exception::Decryptor::Condition::PayerAuthentication',
fields => [],
description => 'Payer Authentication response should contain either pares or enrolled',
},
'Zymonic::Exception::Decryptor::Condition::PayerAuthentication::NoEncryptedCardField' => {
isa => 'Zymonic::Exception::Decryptor::Condition::PayerAuthentication',
fields => [],
description => "Unable to find EncryptedCardNumber field",
},
'Zymonic::Exception::Decryptor::Condition::PayerAuthentication::UnableToFindField' => {
isa => 'Zymonic::Exception::Decryptor::Condition::PayerAuthentication',
fields => [],
description => "Unable to find Field",
},
'Zymonic::Exception::Decryptor::Condition::PayerAuthentication::NeedTransactionID' => {
isa => 'Zymonic::Exception::Decryptor::Condition::PayerAuthentication',
fields => [],
description => 'Need Transaction ID to lookup transaction',
},
'Zymonic::Exception::Decryptor::Condition::PayerAuthentication::NeedToken' => {
isa => 'Zymonic::Exception::Decryptor::Condition::PayerAuthentication',
fields => [],
description => 'Need token to lookup Payment Data',
},
);
#################### subroutine header begin ####################
=head2 satisfied
Usage : $condition->satisfied;
Purpose : Evaluates whether the condition is satsified.
Returns : 'PASS' or 'FAIL'.
Argument : nothing
Throws : Zymonic::Exception::Condition
Comment : Passes when not enrolled or when sucessful PARes. Fails otherwise,
i.e. card has yet to be authentications.
See Also :
=cut
#################### subroutine header end ####################
sub satisfied
{
my $self = shift;
my $precheck = shift || '';
# TODO: is this correct? to always pass on precheck
if ($precheck)
{
return 'PASS';
}
my $skip_payer_auth = $self->skip_payer_authentication();
if ( $skip_payer_auth && $skip_payer_auth eq 'Y' )
{
return 'PASS';
}
my $payer_authentication = $self->payer_authentication();
my $encrypted_card_number = $self->encrypted_card_number();
my $pares = $self->pares();
my $transaction_id = $self->transaction_id();
my $device_category = $self->device_category();
my $pareq;
my $issuer_cert;
if ( $device_category == 1 )
{
$pareq = $self->pareq();
$issuer_cert = $self->issuer_cert();
}
my $request = {
messagetype => 'PayerAuthentication',
payer_authentication_type => $payer_authentication,
card_number => 'ENCRYPTED' . $encrypted_card_number . 'ENCRYPTED',
( $pares ? ( pares => $pares ) : () ),
( $issuer_cert ? ( issuer_cert => $issuer_cert ) : () ),
( $device_category ? ( device_category => $device_category ) : () ),
( $issuer_cert ? ( issuer_cert => $issuer_cert ) : () ),
%{ $self->transaction_details($transaction_id) },
%{ $self->payment_data_details($encrypted_card_number) },
# send http headers
http_accept => clean( $ENV{HTTP_ACCEPT}, ':/*;=.+,' ) || '',
http_user_agent => clean( $ENV{HTTP_USER_AGENT}, ':/*.;_()-' ) || '',
};
my $csc = $request->{csc};
$request->{csc} = 'X' x length( $request->{csc} ) if $request->{csc};
debug( 'Payer Authentication request: ' . Dumper( deep_replace_card_numbers($request) ) );
$request->{csc} = $csc;
# call the decryptor to process card authentication
my $response = $self->decryptor_client()->call_decryptor($request);
debug( 'Payer Authentication response: ' . Dumper( deep_replace_card_numbers($response) ) );
Zymonic::Exception::Decryptor::Condition::PayerAuthentication::NoDecryptorResponse->throw(
zname => $self->{zname},
catchable => 'false'
) unless ref($response) && keys %{$response} > 0;
# set the status field from the response
$self->set_field_value( 'Status', $response->{status} ) if $response->{status};
# check for errors and display as session error, fail condition
if ( $response->{error} )
{
# record the error on session
my $session = $self->ancestor('Zymonic::Session');
if ($session)
{
my $process_parent = $self->ancestor('Zymonic::Process');
$session->record_error( $process_parent->{zname} . "." . $self->{zname}, $response->{error} )
if $process_parent;
}
return 'FAIL';
}
elsif ( $response->{pares} )
{
# if pares, set pares fields and pass condition
# check for pares results
if ( defined $response->{ECI} && $response->{pares_id} && $response->{pares_auth_code} )
{
$self->set_field_value( 'ECI', $response->{ECI} );
$self->set_field_value( 'PARes_auth_code', $response->{pares_auth_code} );
$self->set_field_value( 'PARes_id', $response->{pares_id} );
return 'PASS';
}
else
{
# fail if no pares_id or auth_cose
return 'FAIL';
}
}
elsif ( defined $response->{enrolled} )
{
# if enrolled, set fields and fail (ie.e. wait for pares before passing)
if ( $response->{enrolled} )
{
$self->set_field_value( 'ACS_URL', $response->{acs_url} );
$self->set_field_value( 'PAReq', $response->{pareq} );
$self->set_field_value( 'Issuer_Cert', $response->{issuer_cert} );
return 'FAIL';
}
else
{
# not enrolled, check for ECI and PASS
$self->set_field_value( 'ECI', $response->{ECI} ) if defined $response->{ECI};
return 'PASS';
}
}
elsif ( $response->{allow_continue} && $response->{allow_continue} eq 'true' )
{
# an error occurred but user should be allowed to continue with payment
debug("Payer Authentication message: $response->{message}") if $response->{message};
return 'PASS';
}
else
{
Zymonic::Exception::Decryptor::Condition::PayerAuthentication::InvalidDecryptorResponse->throw(
zname => $self->{zname},
catchable => 'false'
);
}
}
################# subroutine header begin ####################
=head2 payer_authentication
Usage : $self->payer_authentication
Purpose : Gets the payer_authentication value from the class options.
Returns : a scalar payer_authentication value
Argument : nothing
Throws : nothing
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub payer_authentication
{
my $self = shift;
# check for system option override
my $pa = $self->{config}->sys_opt('PayerAuthenticationModule');
return ( $pa ? $pa : $self->static_or_field_value('PayerAuthentication') );
}
################# subroutine header begin ####################
=head2 encrypted_card_number
Usage : $self->encrypted_card_number
Purpose : Gets the encrypted_card_number value from the class options.
Returns : a scalar encrypted_card_number value
Argument : nothing
Throws : nothing
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub encrypted_card_number
{
my $self = shift;
return $self->get_field_value(
$self->{xmldef}->{ClassOptions}->{EncryptedCardNumber}->{Field}->{ZName}->{content} );
}
################# subroutine header begin ####################
=head2 pares
Usage : $self->pares
Purpose : Gets the pares value from the class options.
Returns : a scalar pares value
Argument : nothing
Throws : nothing
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub pares
{
my $self = shift;
return $self->get_field_value( $self->{xmldef}->{ClassOptions}->{PARes}->{Field}->{ZName}->{content} ) || '';
}
#################### subroutine header begin ####################
=head2 decyptor_client
Usage : $self->decryptor_client;
Purpose : Returns a handle to a decryptor client
Returns : Zymonic::Decryptor::Client object
Argument : nothing
Throws : nothing
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub decryptor_client
{
my $self = shift;
$self->{decryptor_client} = Zymonic::Decryptor::Client->new(
parent => $self,
config => $self->{config},
db => $self->{DB}
) unless defined( $self->{decryptor_client} );
return $self->{decryptor_client};
}
#################### subroutine header begin ####################
=head2 set_field_value
Usage : $self->set_status($field, $value);
Purpose : If $field is set in ClassOptions, will set field to incoming value.
Returns : nothing
Argument : The value to set
Throws : Zymonic::Exception::Field
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub set_field_value
{
my $self = shift;
my $field = shift;
my $value = shift;
return
unless $field
&& ref( $self->{xmldef}->{ClassOptions}->{$field} )
&& ref( $self->{xmldef}->{ClassOptions}->{$field}->{Field} )
&& ref( $self->{xmldef}->{ClassOptions}->{$field}->{Field}->{ZName} )
&& $self->{xmldef}->{ClassOptions}->{$field}->{Field}->{ZName}->{content}
&& defined $value;
# lookup the field if not already done so
unless ( $self->{"ZZvf_$field"} )
{
my $lookup_zname = $self->{xmldef}->{ClassOptions}->{$field}->{Field}->{ZName}->{content};
my $found_field = $self->get_field($lookup_zname);
$self->{"ZZvf_$field"} = $found_field;
}
# set field with encrypted pan
$self->{"ZZvf_$field"}->set_value_from_other($value);
}
################# subroutine header begin ####################
=head2 skip_payer_authentication
Usage : $self->skip_payer_authentication
Purpose : Gets the skip_payer_authentication value from the class options.
Returns : a scalar skip_payer_authentication value
Argument : nothing
Throws : nothing
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub skip_payer_authentication
{
my $self = shift;
return $self->static_or_field_value( 'SkipPayerAuthentication', 'optional' );
}
#################### subroutine header begin ####################
=head2 transaction_details
Usage : $self->transaction_details($id)
Purpose : Returns the settings from form for the given transaction id.
Returns : A hashref of settings for the specified transaction.
Argument : A transaction id
Throws : Zymonic::Exception::Condition
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub transaction_details
{
my $self = shift;
my $id = shift;
Zymonic::Exception::Decryptor::Condition::PayerAuthentication::NeedTransactionID->throw(
zname => $self->{zname},
catchable => 'true'
) unless $id;
# check in cache
return $self->{transaction_details}->{$id} if $self->{transaction_details}->{$id};
# transaction not in db yet, so get form values
# map into simple hash and add to cache
my $fields = $self->get_field_refs();
$self->{transaction_details}->{$id} = {
map {
my $fo = $self->get_field_object($_);
my $value = $fo->value();
if ( $fo->isa('Zymonic::Field::DecryptorEncrypted') )
{
( $fo->{field} => "ENCRYPTED${value}ENCRYPTED" );
}
else
{
( $fo->{field} => $value );
}
} values %{$fields}
};
# get MID details
my $mid_table = Zymonic::Table->new(
parent => $self,
zname => 'pte_mids',
config => $self->{config},
auth => $self->{auth},
DB => $self->{DB},
);
$mid_table->get_all_records(
{
where_clause => $mid_table->{tableasname} . '.mid = ?',
where_params => [ $self->{transaction_details}->{$id}->{mid} ]
}
);
# set them on transaction
map {
my $fo = $mid_table->get_field_object($_);
my $value = $fo->value();
if ( $fo->isa('Zymonic::Field::DecryptorEncrypted') )
{
$self->{transaction_details}->{$id}->{ $fo->{field} } = "ENCRYPTED${value}ENCRYPTED" if $value;
}
else
{
$self->{transaction_details}->{$id}->{ $fo->{field} } = $value;
}
}
grep { !defined $self->{transaction_details}->{$id}->{ $_->{field} } }
values %{ $mid_table->get_fields( $mid_table->{records}->[0] ) };
# if currency not set on transaction get it from MID
unless ( $self->{transaction_details}->{$id}->{currency} )
{
$self->{transaction_details}->{$id}->{currency} =
$mid_table->get_field_value( 'pte_mids_currency', $mid_table->{records}->[0] );
}
# set transaction_id
$self->{transaction_details}->{$id}->{transaction_id} = $id;
return $self->{transaction_details}->{$id};
}
#################### subroutine header begin ####################
=head2 payment_data_details
Usage : $self->payment_data_details($token)
Purpose : Returns the settings from DB for the given payment_data token.
Returns : A hashref of settings for the specified payment_data.
Argument : A payment_data token
Throws : Zymonic::Exception::Condition
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub payment_data_details
{
my $self = shift;
my $token = shift;
Zymonic::Exception::Decryptor::Condition::PayerAuthentication::NeedToken->throw(
zname => $self->{zname},
catchable => 'true'
) unless $token;
# check in cache
return $self->{payment_data_details}->{$token} if $self->{payment_data_details}->{$token};
# load payment data table
my $payment_data_table = Zymonic::Table->new(
parent => $self,
zname => 'pte_cards',
config => $self->{config},
auth => $self->{auth},
DB => $self->{DB},
filter => { where => 'token = ?', params => [$token] }
);
$payment_data_table->get_all_records();
# map into simple hash and add to cache
$self->{payment_data_details}->{$token} =
{ map { $_->{field} => $_->value }
values %{ $payment_data_table->get_fields( $payment_data_table->{records}->[0], 'objects' ) } };
# set payment_data_id
$self->{payment_data_details}->{$token}->{payment_data_id} = $self->{payment_data_details}->{$token}->{id};
# remove card_number field as it is not in the DB
delete $self->{payment_data_details}->{$token}->{card_number};
return $self->{payment_data_details}->{$token};
}
################# subroutine header begin ####################
=head2 pareq
Usage : $self->pareq
Purpose : Gets the pareq value from the class options.
Returns : a scalar pareq value
Argument : nothing
Throws : nothing
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub pareq
{
my $self = shift;
return $self->get_field_value( $self->{xmldef}->{ClassOptions}->{PAReq}->{Field}->{ZName}->{content} ) || '';
}
################# subroutine header begin ####################
=head2 issuer_cert
Usage : $self->issuer_cert
Purpose : Gets the issuer_cert value from the class options.
Returns : a scalar issuer_cert value
Argument : nothing
Throws : nothing
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub issuer_cert
{
my $self = shift;
return $self->get_field_value( $self->{xmldef}->{ClassOptions}->{Issuer_Cert}->{Field}->{ZName}->{content} ) || '';
}
################# subroutine header begin ####################
=head2 device_category
Usage : $self->device_category
Purpose : Gets the device_category value from the class options.
Returns : a scalar device_category value
Argument : nothing
Throws : nothing
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub device_category
{
my $self = shift;
return $self->get_field_value( $self->{xmldef}->{ClassOptions}->{Device_Category}->{Field}->{ZName}->{content} )
|| '';
}
#################### subroutine header begin ####################
=head2 manual
Usage : $self->manual($manual)
Purpose : Outputs an XML manual for this object
Returns : A hashref to be converted to XML using XML::Simple.
Argument : A Zymonic::Manual object
$schemas - a hashref containing input/output schemas, if present
then schema fragments will be added to the manual
Throws : nothing
Comment :
See Also :
=cut
#################### subroutine header end ####################
sub manual
{
my $self = shift;
my $manual = shift;
my $schemas = shift;
# build any SUPER manual stuff
$self->SUPER::manual( $manual, $schemas );
# add potential error
if ( my $parent_process = $self->ancestor('Zymonic::Process') )
{
$manual->add_error(
{
code => { content => $parent_process->{zname} . "." . $self->{zname}, },
description =>
{ content => '[error code matching error log on server] - [message explaining error cause]' },
}
);
}
}
1;