Skip to main content

· 10 min read
Alejandro Revilla

/by apr/

As a follow-up to the initial post about jCard and jPTS, I'd like to explain what is jCard after all. jCard is an interface between the ISO-8583 world and a double-entry accounting system. -- or better yet -- jCard is an interface between the ISO-8583 v2003 based jPOS-CMF world and a multi-layer, multi-currency general purpose double-entry accounting system (miniGL). Here is an the initial ER diagram of the core components that we used at design time, although somehow changed to support customer's requirements, it still gives you the idea of how the pieces fit together: You can see here that an Issuer has CardProducts which in turn has Cards. A Card is our handle to the CardHolder which in turn can have multiple GL Accounts (think Checking, Savings, Stored Value accounts). Imagine this little chart of accounts for a pre-paid card: As seen from jCard's perspective, a deposit transaction will debit our 'Received money' account (an asset) and credit the Cardholder's SV account (it's a liability now for us). So a deposit transaction will look like this: USD 100 goes to the 'Received Money' account and 100 to the Cardholder's account. If you enlarge the picture, you'll see a little '840' number to the right of the account code, i.e. 11.001.00840, that's our layer (in this case the accounting USD layer, 840 is the ISO-4217 currency number). Here is a purchase transaction that involve some fees, followed by a reversal: You can see we just change who we owe to, instead of owing this money to the cardholder, we owe it now to the acquirer (or merchant/branch, depending on the situation). The ISO-8583 side honors the jPOS CMF, so this purchase transaction looks like this:

This particular CardProduct has a flat fee of USD 3.50 plus a 3.25% for CashAdvance transactions, this is configured like this:

mysql> select * from cardproduct_fees where id = 1;
+----+--------+----------------------+
| id | fee | type |
+----+--------+----------------------+
| 1 | 3.2500 | CashAdvance.%.840 |
| 1 | 3.5000 | CashAdvance.flat.840 |
+----+--------+----------------------+

that's why we ended-up charging USD 24.65. For those of you familiar with jPOS, looking at its TransactionManager main configuration may give you an idea of what we are talking about here:

So the previous transaction was a 200.01 (MTI=200, Processing code 013000), so we processed the following groups:

  • financial
  • prepareresponse
  • logit
  • sendresponse

The financial group looks like this: First, we perform some sanity checks, we verify that the mandatory fields are present, we allow some optional fields too.

Then we create a TranLog record (that's our master transaction log file)

We check that the card exists, it's valid, belongs to a CardHolder, etc. We do this in a PCI compliant way, that 'KID' configuration there is the DUKPT BDK Key ID.

The terminal has to be valid too:

And so is the acquirer (we'll have to pay them at some point)

We know the Card, so we know the CardHolder. We know the CardHolder has accounts, and based on the processing code (data element 3), we choose which account to use (checking, saving, credit, stored value, loyalty, whatever)

It can happen under certain scenarios (mostly due to small network outages) that we could receive a reversal for a given transaction before or almost at the same time as the transaction itself, so we check if this transaction was previously reversed:

Then, based on the CardProduct, we can perform multiple velocity checks:

And now we are ready to generate the GL transaction and compute the balances after that.

Here is the source code for org.jpos.jcard.Financial

public class Financial extends Authorization {
public int prepare (long id, Serializable context) {
return super.prepare (id, context);
}
protected short getLayerOffset() {
// financial transactions goes to the main layer for the
// given currency
return 0;
}
protected String getTransactionName() {
return "Financial";
}
}

Pretty simple, huh? Please pay attention to that 'getLayerOffset()' method that returns 0. The Authorization participant is slightly more complex:

public class Authorization extends JCardTxnSupport {
public static final String FEE_PREFIX = "CashAdvance";

public Authorization() {
super();
}
public int prepare (long id, Serializable context) {
Context ctx = (Context) context;
try {
ctx.checkPoint ("authorization-start");
TranLog tl = (TranLog) ctx.get (TRANLOG);
GLSession gls = getGLSession(ctx);
ISOMsg m = (ISOMsg) ctx.get (REQUEST);
Card card = (Card) ctx.get (CARD);
CardHolder cardHolder = (CardHolder) ctx.get (CARDHOLDER);
Issuer issuer = (Issuer) ctx.get (ISSUER);

String accountType = ctx.getString (PCODE\_ACCOUNT\_TYPE);
BigDecimal amount = (BigDecimal) ctx.get (AMOUNT);
BigDecimal acquirerFee = getAcquirerFee (m);
BigDecimal issuerFee = ZERO;

assertNotNull (issuer, INVALID_REQUEST, "Invalid Issuer");
assertNotNull (card, INVALID_REQUEST, "Invalid Card");
assertNotNull (card.getCardProduct(), INVALID_REQUEST, "Invalid CardProduct");
assertNotNull (
card.getCardProduct().getIssuedAccount(), INVALID_REQUEST,
"Invalid CardProduct Issued Account"
);
assertNotNull (cardHolder, INVALID_REQUEST, "Invalid CardHolder");
assertNotNull (amount, INVALID_AMOUNT);
assertFalse (ZERO.equals (amount), INVALID_AMOUNT, "Zero amount not valid");
assertNotNull (accountType,
INVALID_REQUEST, "Invalid processing code"
);
assertFalse(REFUND\_ACCOUNT\_TYPE.equals (accountType),
INVALID_REQUEST, "Refund account not allowed"
);
String acctid = accountType+"."+ctx.getString(CURRENCY);
FinalAccount acct = (FinalAccount)
cardHolder.getAccounts().get (acctid);

assertNotNull (acct,
ACCOUNT\_NOT\_FOUND,
"Account type '"+acctid+"' is not defined");

Journal journal = issuer.getJournal();
assertNotNull (
journal, SYSERR_DB,
"Journal not found for issuer " + issuer.getId() + " ("
\+ issuer.getName() + ")"
);

ctx.checkPoint ("authorization-pre-lock-journal");
gls.lock (journal, acct);
ctx.checkPoint ("authorization-post-lock-journal");

short currency = getCurrency (ctx.getString(CURRENCY));
short\[\] realAndPending = new short\[\] {
currency, (short) (currency + PENDING_OFFSET)
};
BigDecimal balance = gls.getBalance (journal, acct, realAndPending);
ctx.checkPoint ("authorization-compute-balance");
BigDecimal amountPlusFees = amount.add(acquirerFee);

ctx.put (ACCOUNT, acct);
String surchargeDescription = null;
if (amountPlusFees.compareTo (balance) > 0) {
BigDecimal creditLine = gls.getBalance (
journal, acct,
new short\[\] { (short)(currency+CREDIT_OFFSET) }
);
ctx.checkPoint ("authorization-get-credit-line");
if (acct.isDebit())
creditLine = creditLine.negate();

StringBuilder sb = new StringBuilder();
issuerFee = issuerFee.add(calcSurcharge (ctx, card, amount, FEE_PREFIX, sb));
if (sb.length() > 0)
surchargeDescription = sb.toString();
amountPlusFees = amountPlusFees.add (issuerFee);
if (!isForcePost() && amountPlusFees.compareTo
(balance.add(creditLine)) >= 0)
{
throw new BLException (NOT\_SUFFICIENT\_FUNDS,
"Credit line is " + creditLine
+", issuer fee=" + issuerFee);
}
}
Acquirer acquirer = (Acquirer) ctx.get (ACQUIRER);

GLTransaction txn = new GLTransaction (
getTransactionName() + " " + Long.toString(id)
);
txn.setPostDate ((Date) ctx.get (CAPTURE_DATE));
txn.createDebit (
acct, amountPlusFees, null,
(short) (currency + getLayerOffset())
);
txn.createCredit (
acquirer.getTransactionAccount(), amount,
null, (short) (currency + getLayerOffset())
);
if (!ZERO.equals(issuerFee)) {
txn.createCredit (
card.getCardProduct().getFeeAccount(), issuerFee,
surchargeDescription, (short) (currency + getLayerOffset())
);
ctx.put (ISSUER_FEE, issuerFee);
}
if (!ZERO.equals(acquirerFee)) {
txn.createCredit (
acquirer.getFeeAccount(), acquirerFee,
null, (short) (currency + getLayerOffset())
);
ctx.put (ACQUIRER_FEE, acquirerFee);
}
gls.post (journal, txn);
ctx.checkPoint ("authorization-post-transaction");
tl.setGlTransaction (txn);
ctx.put (RC, TRAN_APPROVED);
ctx.put (APPROVAL_NUMBER, getRandomAuthNumber());
return PREPARED | NO_JOIN;
} catch (ObjectNotFoundException e) {
ctx.put (RC, CARD\_NOT\_FOUND);
} catch (GLException e) {
ctx.put (RC, NOT\_SUFFICIENT\_FUNDS);
ctx.put (EXTRC, e.getMessage());
} catch (BLException e) {
ctx.put (RC, e.getMessage ());
if (e.getDetail() != null)
ctx.put (EXTRC, e.getDetail());
} catch (Throwable t) {
ctx.log (t);
} finally {
checkPoint (ctx);
}
return ABORTED;
}
public void commit (long id, Serializable context) { }
public void abort (long id, Serializable context) { }

protected short getLayerOffset() {
return PENDING_OFFSET;
}
protected String getTransactionName() {
return "Authorization";
}
protected BigDecimal calcSurcharge
(Context ctx, Card card, BigDecimal amount, StringBuilder detail)
{
BigDecimal issuerFee = ZERO;
Map fees = card.getCardProduct().getFees();
BigDecimal flatFee = fees.get(
FEE\_PREFIX + FEE\_FLAT + ctx.getString(CURRENCY)
);
BigDecimal percentageFee = fees.get(
FEE\_PREFIX + FEE\_PERCENTAGE + ctx.getString(CURRENCY)
);
if (flatFee != null) {
issuerFee = issuerFee.add (
flatFee.setScale(2, ROUNDING_MODE)
);
detail.append ("Surcharge: $");
detail.append (flatFee);
}
if (percentageFee != null) {
issuerFee = issuerFee.add (
amount.multiply(
percentageFee.movePointLeft(2)
).setScale(2, ROUNDING_MODE)
);
if (flatFee != null)
detail.append ('+');
else
detail.append ("Surcharge: ");
detail.append (percentageFee);
detail.append ('%');
}
return issuerFee;
}
protected boolean isForcePost () {
return false;
}
}

In this case getLayerOffset() returns PENDING_LAYER (a constant set to 1000). So what happens here is that when we perform an authorization, we impact the PENDING layer (i.e. 1840 if the transaction was in USD) instead of the ACCOUNTING layer (840). When we compute the USD ACCOUNTING BALANCE, we check for transactions on the 840 layer, but when we check for the available balance, we take into account layer 840 merged with layer 1840 (this is why miniGL has layers). So to recap, this is a financial transaction (MTI 200): And here is an interesting sequence:

  • Pre-Auth USD 100
  • Void of the preauth
  • Reversal of the previous void (authorization becomes active again, if available funds permit)
  • Partial completion for USD 90

If you pay attention to the layers involved, you can see that the pre-auth works on layer 1840 while the completion works on 840, the accounting layer (offset=0). We use that layers scheme to handle overdrafts and credit account (offset is 2000), so for a credit account, we pick the balances from 840,1840,2840 (provided the transaction is performed in USD). jCard was developed in paralell with the jPOS CMF and the jCard selftest facility, based on jPOS-EE's clientsimulator. Whenever we touch a single line of it, we automatically run an extensive set of transactions that gives us some confidence that we are not introducing any major issue, i.e:

    77: Card 6009330000000020 - savings balance is 102     \[OK\] 50ms.
78: Card 6009330000000020 - $1 from checking to savings with $0.50 fee \[OK\] 126ms.
79: Card 6009330000000020 - savings balance is 104 \[OK\] 48ms.
80: $95.51 from checking to savings with no fee (NSF) \[OK\] 78ms.
81: $95.01 from checking to savings with $0.50 fee (NSF) \[OK\] 75ms.
82: $95.00 from checking to savings with $0.50 fee - GOOD \[OK\] 70ms.
83: Card 6009330000000020 - savings balance is now 199 \[OK\] 111ms.
84: Reverse previous transfer \[OK\] 57ms.
85: Reverse previous transfer (repeat) \[OK\] 70ms.
86: savings balance after reverse is 104 \[OK\] 58ms.
87: Withdrawal $20 from credit account with 0.50 fee \[OK\] 85ms.
88: credit balance check \[OK\] 59ms.
89: Reverse withdrawal \[OK\] 57ms.
90: credit balance check - should be back to 1000 \[OK\] 50ms.
91: $100.00 from credit to checking with $0.75 fee - GOOD \[OK\] 138ms.
92: Reverse transfer \[OK\] 52ms.
93: credit balance check - should be back to 1000 \[OK\] 57ms.
94: POS Purchase $20 from credit account with 0.50 fee to be voided \[OK\] 107ms.
95: Void previous transaction \[OK\] 51ms.
96: Void repeat \[OK\] 24ms.
97: Auth for $100 from savings account, no fees \[OK\] 82ms.
98: Void completed auth \[OK\] 51ms.
99: Void repeat \[OK\] 29ms.
100: Invalid completion for USD 90.00 (previously voided) \[OK\] 34ms.
101: Reverse void \[OK\] 56ms.
102: Reverse void retransmission \[OK\] 32ms.
103: completion for USD 90.00 (void has been reversed) \[OK\] 48ms.
104: completion for USD 90.00 retransmission \[OK\] 38ms.
105: check savings balance - should be $14.00 \[OK\] 110ms.
106: Refund for $16 \[OK\] 70ms.
107: Reverse previous refund \[OK\] 47ms.
108: refund reversed, balance back to 0.00 \[OK\] 67ms.

I hope that this post can give you an idea of what jCard is and why we are sometimes quiet on the blog, we are having fun developing all this.

· One min read
Alejandro Revilla

We've migrated our issue tracking system to YouTrack. I have reviewed old issues and migrated outstanding ones to the new system. The old system will remain online for a while, but will be decommissioned at some point during 2010. Some of the issues there are kind of trivial to fix, I hope some of you --specially new jPOS developers-- could contribute some time to provide patches. Implementing features and bug fixes is the best way to learn jPOS (or any other OpenSource project), so PLEASE HELP!

· One min read
Alejandro Revilla

As of jPOS 1.6.5 r2845, we added a new input-space property to the TransactionManager. With this minor addition, you can easily distribute the load among multiple Q2 instances running in the same or different machines. I'm a true believer in long-lived lightweight Q2 based components. Now thanks to our very simple Space interface and Bela Ban's awesome JGroups that allowed us to implement the ReplicatedSpace, creating this kind of jPOS clustered setup is now feasible.

· One min read
Alejandro Revilla

In addition to the must-have buy-it-now jPOS Programmer's Guide, I've been working in a new document, the jPOS Project Guide. I thought it would be a good idea to share the source code of that document, which uses DITA, so may be we can learn DITA together by editing this document. You can find it in the new 'jpos6/doc' directory (r2834) This is an experiment, the document will be there for a couple of months, if there are no significant contributions from the community, I will remove it and continue working alone which gives me more flexibility. If that happens, the PDF version will continue to be available, though. Patches and additions are welcome!

· 2 min read
Alejandro Revilla

When we started to log messages using a very simple XML format a long time ago, and then created the XMLPackager to support that message format, we never thought that this was going to be so heavily used, we frequently find ISO-8583 interchanges based on this format, implemented in different languages by different vendors in different countries. I've recently been involved in the deployment of an ISO/Bridge system at a financial institution in one African country connected to another institution at another African country. When I asked for the ISO-8583 specs in order to configure ISO/Bridge, I was surprised to see that the specs where XML based, and basically our spec. I hope we start seeing this with the jPOS CMF soon. The existing format looks like this:

Starting in jPOS 1.6.5 r2817, you can optionally use:

   0800
000001
301

Or any combination, i.e:

   301  

This is particularly useful in some implementations that use XML content as part of the ISO-8583 payload, in the past, we had to expand it using XML entities such as < and > that where not nice looking in the logs. With the new addition, we can:

  301
< ?xml version='1.0' encoding='UTF-8' ?>
29110001
I love XML tags

(you have to add a CDATA block around your inner XML) At response time, we wanted to make it as backward compatible as possible, so we only use this mode if we detect that the field content is XML.

· One min read
Alejandro Revilla

docos Quick cross-post just to tell that I can assert this is quite true:

"Alejandro has told me the same thing – that when he does projects not related to OLS, he misses our documentation and attention to detail."

Andy's "Master Project Workbook" is a time saver, everything is there. Everything. Original post here.

· 2 min read
Alejandro Revilla

About a year ago I've blogged about jCard and jPTS (the jPOS-EE based Card Management System / Stored Value Engine and the jPOS Transaction Switch). As part of the project, we defined what we called our IMF (internal message format) based on the ISO-8583 v2003 standard. jPTS After several deployments and mapping exercises to different source (SS) and destination stations (DS), we believe that this extra message normalization layer was a good thing to do. jPTS Mapping We get to see jPOS based systems connected to other jPOS based systems all the time, which is a good thing and makes things easier (when the connecting parties don't see their channel and packager configurations as some kind of secret sauce and get to kindly exchange them), but the packager/channel is just the low level plumbing, a lot of effort has to be done on the higher levels in order to get them to play nicely with each other. Our jCard system already has a quite complete set of self tests that use this format, same goes for jPTS. As part of this message normalization effort we plan to morph our internal tests into a TCK. Andy and Dave from Online Strategies are already using a similar format and are keen to work together into merging the differences. Mark is also #noded for this effort :) We are releasing this work under a Creative Commons Attribution-Share Alike 3.0 License and everybody is welcome to participate. You can get the PDF version here and you are very welcome to actively collaborate with the DocBook version (http://github/ar/jPOS-CMF). This is by no means a perfect spec, there's a lot of work to be done, but instead of reinventing a new spec, or coding to someone else's proprietary spec, we encourage you to take this one as yours. You can even change the cover page (as long as you retain some due credit). Every new project will require some tweaks and addition to the spec and TCK, and we are here to make that happen fast. Feel free to contact us or send us a tweet.

· 2 min read
Alejandro Revilla

/by apr on why-oh-why/

What's wrong with Payment gateways?

I've seen this problem coming a long time ago; in my country there's a local credit card association called POS2000. After a long long time they've settled on a single ISO-8583 v1987 based protocol that is a mix of several POS and host based protocols where every acquirer managed to bastardize the standard a little bit in order to make the POS implementor's life a little bit worse.

They did a pretty good job (at bastardizing us) and made jPOS quite better (we have now support for esoteric stuff such as the dynamic packagers that change the way we deal with a message for different message types, etc.).

But that's nothing compared to web based payment gateways.

The first craptastic work I've got to see was presented by a local credit card acquirer/issuer.

You know, the regular redirect-to-the-acquirer-site thing with OK and DECLINE URLs which were available in hidden fields, as if a malicious user couldn't read the page source code and hand-craft the OK-url to continue shopping. Come on! More acquirers followed the trend and provided similar pathetic implementations that very few merchants and less users actually use; but things get worst, thanks to the mouse and some visual tools, there's a whole new group of people writing SOAP based services. OMG!!

We are dealing with quite a few now, to support credit card transactions, prepaid top up applications, etc.

Decades of hard work by large financial institutions working on good standards go down the toilet, it's easier to "invent" an API than read an ISO spec and do the right thing. We are working now with a pre-paid phone company and you get to see things like no reversals, and one-second time-window for voids (come on!), and the results code are usually SQL syntax errors displayed to the client.

A similar thing happens with language-specific APIs, vendors think that developers are stupid and not capable enough to deal with a well specified protocol, so they create an API that is usually a bad and closed source implementation of a bad proprietary wire protocol.

Serenity now!

· One min read
Alejandro Revilla

As a follow-up to the previous post about vdmspace, I've implemented a simple SpaceWriter TransactionParticipant that can be used to log all transaction contexts to the highly reliable VoldemortSpace (vdm:space). It can be added to your transaction flow like this:

The code looks like this:

package org.jpos.transaction;

import java.io.Serializable;
import org.jpos.space.Space;
import org.jpos.space.SpaceFactory;
import org.jpos.core.Configurable;
import org.jpos.core.Configuration;
import org.jpos.core.ConfigurationException;

public class SpaceWriter implements AbortParticipant, Configurable {
Space sp;
String prefix;
Configuration cfg;
public int prepare (long id, Serializable o) {
return PREPARED | READONLY;
}
public int prepareForAbort (long id, Serializable o) {
return PREPARED | READONLY;
}
public void commit (long id, Serializable o) {
outContext (id, (Context) o);
}
public void abort (long id, Serializable o) {
outContext (id, (Context) o);
}
private void outContext (long id, Context ctx) {
if (sp == null)
sp = SpaceFactory.getSpace (cfg.get ("space", null));
sp.out (prefix + Long.toString (id), ctx);
}
public void setConfiguration (Configuration cfg) {
this.cfg = cfg;
prefix = cfg.get ("prefix", "");
}
}

You can add this participant to the very end of your transaction flow. All persistent entries in your Context would get stored.