Skip to content

Commit

Permalink
[Brent] Add paye.net integration.
Browse files Browse the repository at this point in the history
  • Loading branch information
dracos committed Oct 25, 2023
1 parent d72f2fe commit 5aab048
Show file tree
Hide file tree
Showing 4 changed files with 440 additions and 17 deletions.
142 changes: 138 additions & 4 deletions perllib/FixMyStreet/Cobrand/Brent.pm
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ with 'FixMyStreet::Roles::Open311Multi';
with 'FixMyStreet::Roles::CobrandOpenUSRN';
with 'FixMyStreet::Roles::CobrandEcho';
with 'FixMyStreet::Roles::SCP';
use Integrations::Paye;

sub council_area_id { return 2488; }
sub council_area { return 'Brent'; }
Expand Down Expand Up @@ -629,19 +630,152 @@ sub admin_templates_external_status_code_hook {
return $code;
}

=head2 Staff payments
If a staff member is making a payment, then instead of using SCP, we redirect
to Paye. We also need to check the completed payment against the same source.
=cut

around waste_cc_get_redirect_url => sub {
my ($orig, $self, $c, $back) = @_;

if ($c->stash->{is_staff}) {
my $payment = Integrations::Paye->new({
config => $self->feature('payment_gateway')
});

my $p = $c->stash->{report};
my $uprn = $p->get_extra_field_value('uprn');

my $amount = $p->get_extra_field_value( 'pro_rata' );
unless ($amount) {
$amount = $p->get_extra_field_value( 'payment' );
}
my $admin_fee = $p->get_extra_field_value('admin_fee');

my $redirect_id = mySociety::AuthToken::random_token();
$p->set_extra_metadata('redirect_id', $redirect_id);
$p->update;

my $backUrl = $c->uri_for_action("/waste/$back", [ $c->stash->{property}{id} ]) . '';
my $address = $c->stash->{property}{address};
my @parts = split ',', $address;

my @items = ({
amount => $amount,
reference => $payment->config->{customer_ref},
description => $p->title,
lineId => $self->waste_cc_payment_line_item_ref($p),
});
if ($admin_fee) {
push @items, {
amount => $admin_fee,
reference => $payment->config->{customer_ref_admin_fee},
description => 'Admin fee',
lineId => $self->waste_cc_payment_admin_fee_line_item_ref($p),
};
}
my $result = $payment->pay({
returnUrl => $c->uri_for('pay_complete', $p->id, $redirect_id ) . '',
backUrl => $backUrl,
ref => $self->waste_cc_payment_sale_ref($p),
request_id => $p->id,
description => $p->title,
name => $p->name,
email => $p->user->email,
uprn => $uprn,
address1 => shift @parts,
address2 => shift @parts,
country => 'UK',
postcode => pop @parts,
items => \@items,
staff => $c->stash->{staff_payments_allowed} eq 'cnp',
});

if ( $result ) {
$c->stash->{xml} = $result;

# GET back
# requestId - should match above
# apnReference - transaction Ref, used later for query
# transactionState - InProgress
# invokeResult/status - Success/...
# invokeResult/redirectUrl - what is says
# invokeResult/errorDetails - what it says
if ( $result->{transactionState} eq 'InProgress' &&
$result->{invokeResult}->{status} eq 'Success' ) {

$p->set_extra_metadata('apnReference', $result->{apnReference});
$p->update;

my $redirect = $result->{invokeResult}->{redirectUrl};
$redirect .= "?apnReference=$result->{apnReference}";
return $redirect;
} else {
# XXX - should this do more?
my $error = $result->{invokeResult}->{status};
$c->stash->{error} = $error;
return undef;
}
} else {
return undef;
}
return;
}

return $self->$orig($c, $back);
};

around garden_cc_check_payment_status => sub {
my ($orig, $self, $c, $p) = @_;

if (my $apn = $p->get_extra_metadata('apnReference')) {
my $payment = Integrations::Paye->new({
config => $self->feature('payment_gateway')
});

my $resp = $payment->query({
request_id => $p->id,
apnReference => $apn,
});

if ($resp->{transactionState} eq 'Complete') {
if ($resp->{paymentResult}->{status} eq 'Success') {
my $auth_details
= $resp->{paymentResult}{paymentDetails}{authDetails};
$p->set_extra_metadata( 'authCode', $auth_details->{authCode} );
$p->set_extra_metadata( 'continuousAuditNumber',
$resp->{paymentResult}{paymentDetails}{payments}{paymentSummary}{continuousAuditNumber} );
$p->update_extra_field({ name => 'payment_method', value => 'csc' });
$p->update;

my $ref = $auth_details->{uniqueAuthId};
return $ref;
} else {
$c->stash->{error} = $resp->{paymentResult}->{status};
return undef;
}
} else {
$c->stash->{error} = $resp->{transactionState};
return undef;
}
}

return $self->$orig($c, $p);
};

=head2 waste_check_staff_payment_permissions
Staff can make payments via entering a PAYE code.
Staff can make payments via a PAYE endpoint.
=cut

sub waste_check_staff_payment_permissions {
my $self = shift;
my $c = $self->{c};

return unless $c->stash->{is_staff};

$c->stash->{staff_payments_allowed} = 'paye';
$c->stash->{staff_payments_allowed} = 'paye-api';
}

=head2 waste_event_state_map
Expand Down
230 changes: 230 additions & 0 deletions perllib/Integrations/Paye.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
=head1 NAME
Integrations::Paye - integration with the Capita paye.net interface
=head1 DESCRIPTION
=cut

package Integrations::Paye;

use Moo;
with 'FixMyStreet::Roles::SOAPIntegration';

use Data::Dumper;
use Sys::Syslog;
use DateTime;
use MIME::Base64;
use Digest::HMAC;
use Crypt::Digest::SHA256;

has config => ( is => 'ro' );

has url => ( is => 'ro' );

has endpoint => (
is => 'lazy',
default => sub {
my $self = shift;
SOAP::Lite->soapversion(1.1);
my $soap = SOAP::Lite->on_action( sub { "$_[0]IPortalService/$_[1]"; } )->proxy($self->config->{paye_url});
$soap->autotype(0);
return $soap;
}
);

has log_open => (
is => 'ro',
lazy => 1,
builder => '_syslog_open',
);

sub _syslog_open {
my $self = shift;
my $ident = $self->config->{log_ident} or return 0;
my $opts = 'pid,ndelay';
my $facility = 'local6';
my $log;
eval {
Sys::Syslog::setlogsock('unix');
openlog($ident, $opts, $facility);
$log = $ident;
};
$log;
}

sub DEMOLISH {
my $self = shift;
closelog() if $self->log_open;
}

sub log {
my ($self, $str) = @_;
$self->log_open or return;
$str = Dumper($str) if ref $str;
syslog('debug', '%s', $str);
}

sub call {
my ($self, $method, @params) = @_;

require SOAP::Lite;
$self->log($method);
$self->log(\@params);
my $res = $self->endpoint->call(
SOAP::Data->name($method)->attr({
#'xmlns:scpbase' => 'http://www.capita-software-services.com/scp/base',
xmlns => 'http://www.capita-software.co.uk/software/pages/payments.aspx?apnPortal',
'xmlns:common' => 'https://support.capita-software.co.uk/selfservice/?commonFoundation',
'xmlns:temp' => 'http://tempuri.org/'
}),
make_soap_structure_with_attr(@params),
);

my $body;
if ( $res ) {
$body = $res->body;
$self->log($body);
} else {
$self->log('No response');
}

return $body;
}

sub credentials {
my ($self, $args) = @_;

# this is UTC
my $ts = DateTime->now->format_cldr('yyyyMMddHHmmss');

my $ref = $args->{ref} . ':' . time;
my $hmac = Digest::HMAC->new(MIME::Base64::decode($self->config->{paye_hmac}), "Crypt::Digest::SHA256");
$hmac->add(join('!', 'CapitaPortal', $self->config->{paye_siteID}, $ref, $ts, 'Original', $self->config->{paye_hmac_id}));

return ixhash(
'common:subject' => ixhash(
'common:subjectType' => 'CapitaPortal',
'common:identifier' => $self->config->{paye_siteID},
'common:systemCode' => 'APN',
),
'common:requestIdentification' => ixhash(
'common:uniqueReference' => $ref,
'common:timeStamp' => $ts,
),
'common:signature' => ixhash(
'common:algorithm' => 'Original',
'common:hmacKeyID' => $self->config->{paye_hmac_id},
'common:digest' => $hmac->b64digest,
)
);
}

sub pay {
my ($self, $args) = @_;

my $credentials = $self->credentials($args);
my $entry_method = $args->{staff} ? 'CNP' : 'ECOM';

for my $field( qw/name address1 address2/ ) {
$args->{$field} = substr($args->{$field}, 0, 50) if $args->{$field};
}

my @items;
my $total = 0;
foreach (@{$args->{items}}) {
push @items, ixhash(
#'scpbase:itemSummary' => ixhash(
# 'scpbase:description' => $_->{description},
#),
'itemDetails' => ixhash(
'fundCode' => $self->config->{paye_fund_code},
'reference' => $_->{reference},
'amountInMinorUnits' => $_->{amount},
'additionalInfo' => ixhash(
'narrative' => $args->{uprn},
'additionalReference' => $_->{lineId},
),
'accountDetails' => {
'name' => {
'surname' => $args->{name},
},
'address' => ixhash(
'address1' => $args->{address1},
'address2' => $args->{address2},
#'country' => $args->{country},
'postcode' => $args->{postcode},
),
$args->{email} ? (
'contact' => {
'email' => $args->{email},
}
) : (),
},
),
$self->config->{scp_vat_code} ? (
'vat' =>ixhash(
'vatCode' => $self->config->{scp_vat_code},
'vatRate' => $self->config->{scp_vat_rate} || 0,
'vatAmountInMinorUnits' => $_->{vat} || 0,
),
) : (),
'lineId' => $_->{lineId},
);
$total += $_->{amount};
}

my $obj = [ 'temp:request' => ixhash(
'credentials' => $credentials,
'login' => ixhash(
'loginName' => $self->config->{paye_username},
'password' => $self->config->{paye_password},
'consortiumCode' => $self->config->{paye_consortiumCode},
'siteId' => $self->config->{paye_siteID},
),
'requestType' => 'PayOnly',
'requestId' => $args->{request_id},
'routing' => ixhash(
'returnUrl' => $args->{returnUrl},
),
'sale' => ixhash(
#'scpbase:saleSummary' => ixhash(
# 'scpbase:description' => $args->{description},
# 'scpbase:amountInMinorUnits' => $total,
# 'scpbase:reference' => $args->{ref},
#),
items => {
item => \@items,
},
),
) ];

my $res = $self->call('temp:Invoke', @$obj);

if ( $res && $res->{InvokeResponse} ) {
$res = $res->{InvokeResponse}{InvokeResult};
}

return $res;
}

sub query {
my ($self, $args) = @_;
my $credentials = $self->credentials({ ref => $args->{request_id} });

my $obj = [ 'temp:request' => ixhash(
'credentials' => $credentials,
'requestId' => $args->{request_id},
'apnReference' => $args->{apnReference},
) ];

my $res = $self->call('temp:Query', @$obj);

if ( $res && $res->{QueryResponse} ) {
$res = $res->{QueryResponse}{QueryResult};
}

return $res;
}

1;
Loading

0 comments on commit 5aab048

Please sign in to comment.