#################### 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.
FailureUSerMessage is the error message that should be displayed to the user
when payer authentication fails. If not set will default to:
"Payer Authentication failed. Please try again or use a different payment method."
=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...
Alfresco 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;
#################### 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}, ':/*.;_()-' ) || '',
};
debug( 'Payer Authentication request: ' . Dumper( deep_replace_card_numbers($request) ) );
# 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::Condition->throw(
error => 'No response received from decryptor',
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 user error flag, flag on self and debug any error messages
if ( $response->{user_error} )
{
debug("Payer Authentication error: $response->{error}")
if $response->{error};
$self->{user_error} = $self->static_or_field_value( 'FailureUserMessage', $self->{other_fields}, 'optional' )
|| 'Payer Authentication failed. Please try again or use a different payment method.';
# 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}, $self->{user_error} )
if $process_parent;
}
return 'FAIL';
}
elsif ( $response->{error} )
{
# any errors without user_error flag should through system errors
Zymonic::Exception::Condition->throw(
error => 'Error from decryptor: ' . $response->{error},
zname => $self->{zname},
catchable => 'false'
);
}
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::Condition->throw(
error => 'Payer Authentication response should contain either pares or enrolled',
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', $self->{other_fields} ) );
}
################# 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->{encrypted_card_number_field}->value if $self->{encrypted_card_number_field};
# other fields already set prior to calling
my $ecn_ref =
$self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{EncryptedCardNumber}->{Field}->{ZName}->{content} };
Zymonic::Exception::Condition->throw(
error => "Unable to find EncryptedCardNumber field: "
. $self->{xmldef}->{ClassOptions}->{EncryptedCardNumber}->{Field}->{ZName}->{content},
zname => $self->{zname},
catchable => 'false'
) unless $ecn_ref;
$self->{encrypted_card_number_field} = $ecn_ref->{parent}->get_object($ecn_ref);
return $self->{encrypted_card_number_field}->value;
}
################# 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->{pares_field}->value if $self->{pares_field};
my $p_ref = $self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{PARes}->{Field}->{ZName}->{content} };
return '' unless $p_ref;
$self->{pares_field} = $p_ref->{parent}->get_object($p_ref);
return $self->{pares_field}->value || '';
}
#################### 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 $field_def = $self->{other_fields}->{$lookup_zname};
my $found_field = $field_def->{parent}->get_object($field_def) if $field_def;
Zymonic::Exception::Field->throw(
error => "Unable to find specified $field Field: $lookup_zname",
zname => $self->{zname},
catchable => 'false'
) unless $found_field;
$self->{"ZZvf_$field"} = $found_field;
}
# set field with encrypted pan
$self->{"ZZvf_$field"}->value($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', $self->{other_fields}, '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::Condition->throw(
zname => $self->{zname},
error => 'Need Transaction ID to lookup transaction',
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 (from other_fields)
# map into simple hash and add to cache
$self->{transaction_details}->{$id} = {
map {
$_->value( $_->raw_value, $_->{fromdb} ) unless defined $_->value;
$_->{field} => $_->value
}
map { $_->{parent}->get_object($_) } values %{ $self->{other_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 {
$_->value( $_->raw_value, $_->{fromdb} ) unless defined $_->value;
$self->{transaction_details}->{$id}->{ $_->{field} } = $_->value
unless defined $self->{transaction_details}->{$id}->{ $_->{field} }
}
map { $mid_table->get_object($_) } values( %{ $mid_table->{records}->[0]->{fields} } );
# if currency not set on transaction get it from MID
unless ( $self->{transaction_details}->{$id}->{currency} )
{
my $crf_ref = $mid_table->{records}->[0]->{fields}->{pte_mids_currency};
my $crf_field = $mid_table->get_object($crf_ref) if $crf_ref;
if ($crf_field)
{
$crf_field->value( $crf_field->raw_value, $crf_field->{fromdb} ) unless defined $crf_field->value;
$self->{transaction_details}->{$id}->{currency} = $crf_field->value;
}
}
# 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::Condition->throw(
zname => $self->{zname},
error => 'Need token to lookup Payment Data',
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 {
$_->value( $_->raw_value, $_->{fromdb} ) unless defined $_->value;
$_->{field} => $_->value
}
map { $payment_data_table->get_object($_) } values( %{ $payment_data_table->{records}->[0]->{fields} } )
};
# 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->{pareq_field}->value if $self->{pareq_field};
my $p_ref = $self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{PAReq}->{Field}->{ZName}->{content} };
return '' unless $p_ref;
$self->{pareq_field} = $p_ref->{parent}->get_object($p_ref);
return $self->{pareq_field}->value || '';
}
################# 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->{issuer_cert_field}->value if $self->{issuer_cert_field};
my $p_ref = $self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{issuer_cert}->{Field}->{ZName}->{content} };
return '' unless $p_ref;
$self->{issuer_cert_field} = $p_ref->{parent}->get_object($p_ref);
return $self->{issuer_cert_field}->value || '';
}
################# 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->{device_category_field}->value if $self->{device_category_field};
my $p_ref =
$self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{device_category}->{Field}->{ZName}->{content} };
return '' unless $p_ref;
$self->{device_category_field} = $p_ref->{parent}->get_object($p_ref);
return $self->{device_category_field}->value || '';
}
#################### 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 => $self->static_or_field_value( 'FailureUserMessage', $self->{other_fields}, 'optional' )
|| 'Payer Authentication failed. Please try again or use a different payment method.'
},
}
);
}
}
1;