start
authorMark Wells <mark@freeside.biz>
Fri, 28 Jun 2013 23:36:47 +0000 (16:36 -0700)
committerMark Wells <mark@freeside.biz>
Fri, 28 Jun 2013 23:36:47 +0000 (16:36 -0700)
.gitignore [new file with mode: 0644]
Changes [new file with mode: 0644]
FCMB.pm [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
README [new file with mode: 0644]
faker/ConfirmPayment.cgi [new file with mode: 0755]
faker/MakePayment.aspx [new file with mode: 0755]
faker/UpayTransactionStatus.ashx [new file with mode: 0755]
faker/fcmb.conf [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..9788afa
--- /dev/null
@@ -0,0 +1,6 @@
+blib/
+*.sw?
+Makefile
+Makefile.old
+MYMETA.yml
+pm_to_blib
diff --git a/Changes b/Changes
new file mode 100644 (file)
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 (file)
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<return_url> and I<cancel_url> parameters will be ignored.
+
+=item Set I<username> 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 <mark@freeside.biz>
+
+=head1 SEE ALSO
+
+perl(1). L<Business::OnlineThirdPartyPayment>.
+
+=cut
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
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 (file)
index 0000000..5209b9d
--- /dev/null
@@ -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 <mark@freeside.biz>',
+    'PREREQ_PM'    => { 
+                        'Business::OnlineThirdPartyPayment' => 0.10,
+                        'XML::Simple' => 2,
+                      },
+);
diff --git a/README b/README
new file mode 100644 (file)
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 (executable)
index 0000000..d4be353
--- /dev/null
@@ -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 (executable)
index 0000000..bd29a0d
--- /dev/null
@@ -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!
+<HTML>
+  <HEAD><TITLE>Not FCMB Web Payment</TITLE></HEAD>
+  <BODY><H3>Confirm your payment</H3>
+    <FORM METHOD="POST" ACTION="ConfirmPayment.cgi">
+      <TABLE CELLSPACING=0 STYLE="border: 1px solid">
+        <TR><TD>Order #</TD><TD>!.$transaction{orderId}.qq!</TD></TR>
+        <TR><TD>Product</TD><TD>!.$transaction{prod}.qq!</TD></TR>
+        <TR><TD>Amount </TD><TD>!.$transaction{amt}.qq!</TD></TR>
+        <TR><TD>First Name</TD><TD><INPUT NAME="first"></TD></TR>
+        <TR><TD>Last Name</TD><TD><INPUT NAME="last"></TD></TR>
+      </TABLE><BR>
+      <INPUT TYPE="hidden" name="reference" value="!.$reference.qq!">
+      <INPUT TYPE="submit" name="submit" value="Pay Now">
+      <INPUT TYPE="submit" name="submit" value="Cancel">
+    </FORM>
+  </BODY>
+</HTML>
+!;
+
+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 (executable)
index 0000000..a563cee
--- /dev/null
@@ -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 (file)
index 0000000..f50dd8a
--- /dev/null
@@ -0,0 +1,35 @@
+<IfModule mod_ssl.c>
+<VirtualHost localhost:443>
+       ServerAdmin webmaster@localhost
+
+       DocumentRoot /var/www
+       <Directory />
+               Options FollowSymLinks
+               AllowOverride None
+       </Directory>
+       <Directory /var/www/>
+               Options Indexes FollowSymLinks MultiViews
+               AllowOverride None
+               Order allow,deny
+               allow from all
+       </Directory>
+       <Directory /var/www/customerportal>
+               Options ExecCGI
+               AddHandler cgi-script .cgi .aspx .ashx
+       </Directory>
+
+       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
+
+</VirtualHost>
+</IfModule>