From 472f07098c2e10ac025b132df098f4b51c14adb1 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 28 Jun 2013 16:36:47 -0700 Subject: [PATCH] start --- .gitignore | 6 ++ Changes | 4 + FCMB.pm | 168 +++++++++++++++++++++++++++++++++++++++ MANIFEST | 9 +++ Makefile.PL | 12 +++ README | 28 +++++++ faker/ConfirmPayment.cgi | 45 +++++++++++ faker/MakePayment.aspx | 43 ++++++++++ faker/UpayTransactionStatus.ashx | 55 +++++++++++++ faker/fcmb.conf | 35 ++++++++ 10 files changed, 405 insertions(+) create mode 100644 .gitignore create mode 100644 Changes create mode 100644 FCMB.pm create mode 100644 MANIFEST create mode 100644 Makefile.PL create mode 100644 README create mode 100755 faker/ConfirmPayment.cgi create mode 100755 faker/MakePayment.aspx create mode 100755 faker/UpayTransactionStatus.ashx create mode 100644 faker/fcmb.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9788afa --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +blib/ +*.sw? +Makefile +Makefile.old +MYMETA.yml +pm_to_blib diff --git a/Changes b/Changes new file mode 100644 index 0000000..5c0def9 --- /dev/null +++ b/Changes @@ -0,0 +1,4 @@ +Revision history for Business-OnlineThirdPartyPayment-FCMB + +0.01 unreleased + Initial version diff --git a/FCMB.pm b/FCMB.pm new file mode 100644 index 0000000..2321d09 --- /dev/null +++ b/FCMB.pm @@ -0,0 +1,168 @@ +package Business::OnlineThirdPartyPayment::FCMB; + +use strict; +use base 'Business::OnlineThirdPartyPayment'; + +use strict; +use LWP; +use URI; +use Data::Dumper; +use Date::Format 'time2str'; +use XML::LibXML; + +our $VERSION = '0.01'; +our $ENDPOINT_SANDBOX = 'localhost'; +our $ENDPOINT_LIVE = 'fcmbwebpay.firstcitygrouponline.com'; + +our $DEBUG = 3; + +# ISO 4217 currency codes (the relevant ones) +our %ALPHA_TO_NUM = ( + USD => 840, + UKP => 826, + NGN => 566, +); + +sub set_defaults { + my $self = shift; + my %args = @_; + $self->build_subs(qw(username password host)); + if ( $args{debug} ) { + $DEBUG = $args{debug}; + } +} + +sub create { + my $self = shift; + my %content = @_; + + my %params; + $params{'orderId'} = time2str('%Y%m%d%H%M%S', time) . '-' . + sprintf('%06d', int(rand(1000000))); + $params{'mercId'} = $self->username + or die "FCMB merchant ID (username) must be configured\n"; + $params{'currCode'} = $ALPHA_TO_NUM{$content{currency}}; + $params{'prod'} = $content{'description'}; + $params{'email'} = $content{'email'}; + $params{'amt'} = $content{'amount'}; + + my $host = $self->host; + if ( $self->test_transaction ) { + $host ||= $ENDPOINT_SANDBOX; + } else { + $host ||= $ENDPOINT_LIVE; + } + my $url = 'https://' . $host . + '/customerportal/MerchantServices/MakePayment.aspx'; + + warn Dumper \%params if $DEBUG > 2; + + # we don't post to the url ourselves, just give it to the user + $self->is_success(1); + $self->redirect($url); + $self->post_params(\%params); + + $self->token( $params{'orderId'} ); +} + +sub execute { + my $self = shift; + my %params = @_; + +# URL looks like +# http://merchantA.com/Success?OrderID=988676&TransactionReference=8765678998989779 + warn Dumper(\%params) if $DEBUG > 2; + + if ($params{'OrderID'}) { + $self->token($params{'OrderID'}); + } else { + die 'No order ID returned from processor'; + } + if ($params{'TransactionReference'}) { + $self->order_number($params{'TransactionReference'}); + } else { + die 'No transaction reference returned from processor'; + } + + my $host = $self->host; + if ( $self->test_transaction ) { + $host ||= $ENDPOINT_SANDBOX; + $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0; + } else { + $host ||= $ENDPOINT_LIVE; + } + my $url = 'https://' . $host . + '/customerportal/MerchantServices/UpayTransactionStatus.ashx'; + my $ua = LWP::UserAgent->new; + warn "Querying transaction status at $url\n" if $DEBUG; + my $response = $ua->get($url, + 'MERCHANT_ID' => $self->username, + 'ORDER_ID' => $self->token + ); + + if ( $response->is_success ) { + + local $@; + my $parser = XML::LibXML->new; + my $doc = eval { $parser->parse_string($response->content) }; + if ( $@ ) { + die "malformed response to transaction status request: $@\n". + ($DEBUG ? ("\n\n".$response->content) : ''); + } + my $root = $doc->documentElement; + my %hash = map { $_->nodeName => $_->textContent } + $root->nonBlankChildNodes; + + warn Dumper \%hash if $DEBUG > 2; + if ( $hash{StatusCode} == 0 ) { + $self->is_success(1); + $self->authorization($hash{PaymentRef}); + } else { + $self->is_success(0); + $self->error_message($hash{Status} . ' - ' . $hash{ResponseDescription}); + } + } else { + die "No confirmation received: ".$response->status_line; + } +} + +1; +__END__ + +=head1 NAME + +Business::OnlineThirdPartyPayment::FCMB + +=head1 DESCRIPTION + +Business::OnlineThirdPartyPayment interface to the First City Monument Bank +(Nigeria) web-based payment processing system. + +=head1 NOTES + +=over 4 + +=item FCMB requires the callback URL to be configured statically; +I and I parameters will be ignored. + +=item Set I to your merchant ID (5 digits). No password is needed. + +=item The only required transaction fields are currency (NGN, USD, or UKP), +amount, description, and email. + +=item The 'faker' directory contains a simple mock-up of the FCMB interface +for testing. This was created from the documentation, without reference +to the real FCMB system, and is NOT known to be accurate. + +=back + +=head1 AUTHOR + +Mark Wells + +=head1 SEE ALSO + +perl(1). L. + +=cut + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..ceb4aa8 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,9 @@ +Makefile.PL +MANIFEST +README +FCMB.pm +Changes +faker/UpayTransactionStatus.ashx +faker/fcmb.conf +faker/ConfirmPayment.cgi +faker/MakePayment.aspx diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..5209b9d --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,12 @@ +use ExtUtils::MakeMaker; +# See lib/ExtUtils/MakeMaker.pm for details of how to influence +# the contents of the Makefile that is written. +WriteMakefile( + 'NAME' => 'Business::OnlineThirdPartyPayment::FCMB', + 'VERSION_FROM' => 'FCMB.pm', # finds $VERSION + 'AUTHOR' => 'Mark Wells ', + 'PREREQ_PM' => { + 'Business::OnlineThirdPartyPayment' => 0.10, + 'XML::Simple' => 2, + }, +); diff --git a/README b/README new file mode 100644 index 0000000..762b50f --- /dev/null +++ b/README @@ -0,0 +1,28 @@ +Business-OnlineThirdPartyPayment-FCMB is a +Business::OnlineThirdPartyPayment module for interfacing with +the First City Monument Bank WebPay system. + +INSTALLATION + +To install this module, run the following commands: + + perl Makefile.PL + make + make test + make install + +SUPPORT AND DOCUMENTATION + +After installing, you can find documentation for this module with the +perldoc command. + + perldoc Business::OnlineThirdPartyPayment::FCMB + +COPYRIGHT AND LICENCE + +Copyright (C) 2013 Mark Wells +Copyright (C) 2013 Freeside Internet Services, Inc. + +This program is free software; you can redistribute it and/or modify it +under the same terms as Perl itself. + diff --git a/faker/ConfirmPayment.cgi b/faker/ConfirmPayment.cgi new file mode 100755 index 0000000..d4be353 --- /dev/null +++ b/faker/ConfirmPayment.cgi @@ -0,0 +1,45 @@ +#!/usr/bin/perl -T + +my $LANDING_URL = + "http://localhost:2080/selfservice.cgi?action=finish_thirdparty_payment"; + +use CGI; +use URI; +use Date::Format 'time2str'; +use Cache::FileCache; +use strict; + +my $cache = Cache::FileCache->new( + { cache_root => '/tmp', namespace => 'FCMB-Faker' } +); + +my $landing = URI->new($LANDING_URL); + +my $cgi = CGI->new; +my $reference = $cgi->param('reference'); +my $txn = $cache->get($reference); + +if ( $cgi->param('submit') eq 'Cancel' ) { + $txn->{status} = 3; #canceled + $landing->query_form(_cancel => 1); +} else { + + # some information captured from the customer + # (in Real Life this would also be their credit card/bank account number) + $txn->{first} = $cgi->param('first'); + $txn->{last} = $cgi->param('last'); + # set status = the last digit of cents + # (0 = success) + my $cents = ($txn->{amt} - int($txn->{amt})) * 100; + $txn->{status} = $cents % 10; + $txn->{date} = time2str('%Y-%m-%d', time); + + $landing->query_form( + $landing->query_form, + 'OrderID' => $txn->{orderId}, + 'TransactionReference' => $reference + ); +} +# update the cache +$cache->set($reference => $txn); +print $cgi->redirect($landing); diff --git a/faker/MakePayment.aspx b/faker/MakePayment.aspx new file mode 100755 index 0000000..bd29a0d --- /dev/null +++ b/faker/MakePayment.aspx @@ -0,0 +1,43 @@ +#!/usr/bin/perl -T + +use CGI; +use Cache::FileCache; +use strict; + +my $cache = Cache::FileCache->new( + { cache_root => '/tmp', namespace => 'FCMB-Faker' } +); +my $cgi = CGI->new; +my %transaction = map { $_ => ($cgi->param($_) || '') } + qw( mercId currCode amt orderId prod email ); + +my $reference = sprintf('%06d%04d', $transaction{mercId}, int(rand(10000))); +$transaction{reference} = $reference; +$transaction{status} = 2; #pending + +$cache->set($reference, \%transaction); + +my $content = qq! + + Not FCMB Web Payment +

Confirm your payment

+
+ + + + + + +
Order #!.$transaction{orderId}.qq!
Product!.$transaction{prod}.qq!
Amount !.$transaction{amt}.qq!
First Name
Last Name

+ + + +
+ + +!; + +print $cgi->header('text/html', + 'Content-Length' => length($content)); +print $content; + diff --git a/faker/UpayTransactionStatus.ashx b/faker/UpayTransactionStatus.ashx new file mode 100755 index 0000000..a563cee --- /dev/null +++ b/faker/UpayTransactionStatus.ashx @@ -0,0 +1,55 @@ +#!/usr/bin/perl -T + +use CGI; +use Cache::FileCache; +use strict; +use XML::LibXML; + +my $cache = Cache::FileCache->new( + { cache_root => '/tmp', namespace => 'FCMB-Faker' } +); + +my @status = ( + 'Successful', 'Failed', 'Pending', 'Cancelled', 'Not Processed', + 'Invalid Merchant', 'Inactive Merchant', 'Inactive Order ID', + 'Duplicate Order ID', 'Invalid Amount' +); + +my $cgi = CGI->new; +my $oid = $cgi->param('ORDER_ID'); + +# inefficient, but this is not production code, so who cares? +my ($txn) = grep { $_->{orderId} eq $oid } + map { $cache->get($_) } $cache->get_keys; +my @out; +if ($txn) { + @out = ( + MerchantID => $txn->{mercId}, + OrderID => $txn->{orderId}, + StatusCode => $txn->{status}, + Status => $status[$txn->{status}], + Amount => sprintf('%.2f', $txn->{amt}), + Date => $txn->{date}, + TransactionRef => $txn->{reference}, + PaymentRef => sprintf('%06d', rand(1000000)), + ResponseCode => sprintf('%02d', rand(100)), + ResponseDescription => 'response description', + CurrencyCode => $txn->{currCode}, + ); +} else { + @out = ( Status => 'Invalid Order ID', StatusCode => '07' ); +} +my $doc = XML::LibXML::Document->new; +my $root = $doc->createElement('UPay'); +$doc->setDocumentElement($root); +while (@out) { + my $name = shift @out; + my $value = shift @out; + my $node = $doc->createElement($name); + $node->appendChild( XML::LibXML::Text->new($value) ); + $root->appendChild($node); +} + +my $content = $doc->toString; +print $cgi->header('text/xml'); +print $content; diff --git a/faker/fcmb.conf b/faker/fcmb.conf new file mode 100644 index 0000000..f50dd8a --- /dev/null +++ b/faker/fcmb.conf @@ -0,0 +1,35 @@ + + + ServerAdmin webmaster@localhost + + DocumentRoot /var/www + + Options FollowSymLinks + AllowOverride None + + + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Order allow,deny + allow from all + + + Options ExecCGI + AddHandler cgi-script .cgi .aspx .ashx + + + ErrorLog ${APACHE_LOG_DIR}/fcmb_error.log + + CustomLog ${APACHE_LOG_DIR}/fcmb_access.log combined + + SSLEngine on + SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem + SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key + + BrowserMatch "MSIE [2-6]" \ + nokeepalive ssl-unclean-shutdown \ + downgrade-1.0 force-response-1.0 + BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown + + + -- 2.11.0