Eating our own dogfood

I really like the TransactionManager, it allows me to clearly define a transaction as if it was processed in an assembly line, with each small reusable participant doing its own little part, and the TransactionManager giving useful profiling information.

But now we live in a RESTful world, we need to implement RESTful based services here and there.

A typical REST call implementation looks like this:

@Path("/customers/{customer_id}/wallets/{wallet_id}/credit")
public class WalletCredit extends WalletCreditDebitSupport {
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response credit (
            @Context UriInfo uriInfo,
            @PathParam("customer_id") String customerId,
            @PathParam("wallet_id") String walletId,
            @FormParam("rrn") String rrn,
            @FormParam("detail") String detail,
            @FormParam("amount") BigDecimal amount,
            @FormParam("currency") String currency)
            throws IOException, URISyntaxException, BLException, ISOException
    {
        …
        …
        …

    }
}

so the first reaction is to just start coding the business logic as part of the body of that method. You need to:

  • sanity check the parameters
  • open a database connection
  • create a TranLog record
  • Validate the Customer, Account, etc.
  • create a GL Transaction
  • do some audit logging
  • commit the JDBC transaction

This of course work fine, but hey, we already have code that does that:

  • We have a standard transaction Open participant
  • We usually have a CreateTranLog participant
  • We have a CheckCustomer participant and CheckAccount
  • We have participants that generate GLTransactions
  • And of course, the Close and Debug participants

Problem with a traditional implementation inside that body is that we need to reinvent the wheel, repeat code, add custom profiling or otherwise we wouldn’t know which operations are fast and which ones are slow.

The solution was to eat our own dog food and use the TransactionManager.

So in this jCard case I’ve created a rest_txnmgr.xml that looks like this:

<!DOCTYPE txnmgr [
    <!ENTITY PAN_PATTERN   "^[\d]{10}$">
    <!ENTITY AMOUNT_PATTERN  "[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?">
    <!ENTITY CURRENCY_PATTERN "^\d{1,4}">
    <!ENTITY RRN_PATTERN "^\d{1,12}">
    <!ENTITY DETAIL_PATTERN "^.{0,255}$">
    <!ENTITY WALLET_PATTERN "^[\d]{1,64}$">
    <!ENTITY TEXT50_PATTERN "^[\w\s.\-\']{0,50}$">
    ...
    ...
]>

<txnmgr name='rest-txnmgr' class="org.jpos.transaction.TransactionManager" logger="Q2" realm='rest-txnmgr'>
  <property name="queue" value="JCARD.RESTAPI.TXN" />
  <property name="sessions" value="2" />
  <property name="max-sessions" value="64" />
  <property name="debug" value="true" />

  <participant class="org.jpos.jcard.PrepareContext" logger="Q2" realm="prepare-context" />
  <participant class="org.jpos.jcard.Switch" logger="Q2" realm="Switch">
    <property name="WalletCreate" value="walletcreate close trigger-response" />
    <property name="WalletCredit" value="walletcredit close trigger-response" />
    <property name="WalletDebit" value="walletdebit close trigger-response" />
    <property name="BalanceInquiry" value="balance-inquiry close trigger-response" />
    <property name="MiniStatement" value="mini-statement close trigger-response" />
    …
    ..

</participant>
…
…
     <group name="walletcreate">
   <participant class="org.jpos.jcard.rest.ValidateParams" logger="Q2">
     <mandatory>
       <param name="PAN">&PAN_PATTERN;</param>
       <param name="WALLET_NUMBER">&WALLET_PATTERN;</param>
     </mandatory>
     <optional>
       <!-- no optional fields -->
     </optional>
   </participant>
   <participant class="org.jpos.transaction.Open" logger="Q2" realm="open">
     <property name="checkpoint" value="open" />
     <property name="timeout" value="300" />
   </participant>
   <participant class="org.jpos.jcard.CreateTranLog" logger="Q2"
                realm="create-tranlog">
     <property name="capture-date" value="capture-date" />
     <property name="node"         value="01" />
   </participant>
   <participant class="org.jpos.jcard.CheckCustomer"
                logger="Q2" realm="checkcustomer">
   </participant>
   <participant class="org.jpos.jcard.rest.WalletCreateParticipant" logger="Q2" realm="create-wallet">
     <property name="chart"        value="jcard" />
     <property name="customers-account" value="21" />
     <property name="kid" value="&KID;" />
   </participant>
 </group>
 ...
 ...

</txnmgr>

Now the code looks like this:

@Path("/customers/{customer_id}/wallets/{wallet_id}/credit")
public class WalletCredit extends WalletCreditDebitSupport {
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response credit (
            @Context UriInfo uriInfo,
            @PathParam("customer_id") String customerId,
            @PathParam("wallet_id") String walletId,
            @FormParam("rrn") String rrn,
            @FormParam("detail") String detail,
            @FormParam("amount") BigDecimal amount,
            @FormParam("currency") String currency)
            throws IOException, URISyntaxException, BLException, ISOException
    {
        return process(uriInfo, customerId, walletId, rrn, detail, amount, currency);
    }
}
public abstract class WalletCreditDebitSupport extends RestSupport {
    @SuppressWarnings("unchecked")
    public Response process (
            UriInfo uriInfo,
            String customerId,
            String walletId,
            String rrn,
            String detail,
            BigDecimal amount,
            String currency)
            throws IOException, URISyntaxException, BLException, ISOException
    {
        org.jpos.transaction.Context ctx = new org.jpos.transaction.Context();
        ctx.put(TXNNAME, getClass().getSimpleName());
        ctx.put(PAN, customerId);
        ctx.put(WALLET_NUMBER, walletId);
        ctx.put(CURRENCY, currency);
        ctx.put(RRN, rrn);
        ctx.put(AMOUNT, amount);
        ctx.put(DETAIL, detail);

        int result = queryTxnMgr(ctx);
        Map<String,Object> resp = createResponseMap();
        if (result == TransactionManager.PREPARED) {
            String irc = (String) ctx.get (IRC);
            resp.put("success", TRAN_APPROVED.equals(irc));
            resp.put("balance", ctx.get (LEDGER_BALANCE));
            return Response.ok(toJson(resp), MediaType.APPLICATION_JSON)
                .status(Response.Status.CREATED)
                .location(URI.create(uriInfo.getAbsolutePath().toString())).build();
        } else {
            resp.put("success", false);
            resp.put("message", ctx.getString(TxnConstants.RC));
            return Response.status(Response.Status.BAD_REQUEST).entity(toJson(resp)).build();
        }
    }
}

And the best part of this, is that we get our familiar ‘Debug’ and ‘Trace’ events:

<log realm="rest-txnmgr" at="Wed Oct 09 20:07:01 UYST 2013.489" lifespan="925ms">
<debug>
    rest-txnmgr-0:idle:1
            prepare: org.jpos.jcard.PrepareContext NO_JOIN
            prepare: org.jpos.jcard.Switch READONLY NO_JOIN
        selector: walletcreate close trigger-response
            prepare: org.jpos.jcard.rest.ValidateParams READONLY NO_JOIN
            prepare: org.jpos.transaction.Open READONLY NO_JOIN
            prepare: org.jpos.jcard.CreateTranLog NO_JOIN
            prepare: org.jpos.jcard.CheckCustomer NO_JOIN
            prepare: org.jpos.jcard.rest.WalletCreateParticipant READONLY NO_JOIN
            prepare: org.jpos.transaction.Close READONLY
            prepare: org.jpos.jcard.rest.TriggerResponse READONLY
            prepare: org.jpos.transaction.Debug READONLY
            commit: org.jpos.transaction.Close
            commit: org.jpos.jcard.rest.TriggerResponse
            commit: org.jpos.transaction.Debug
    head=2, tail=2, outstanding=0, active-sessions=2/64, tps=0, peak=0, avg=0.00, elapsed=925ms
    <profiler>
    prepare: org.jpos.jcard.PrepareContext [0.1/0.1]
    prepare: org.jpos.jcard.Switch [0.0/0.2]
    prepare: org.jpos.jcard.rest.ValidateParams [0.2/0.5]
    prepare: org.jpos.transaction.Open [0.7/1.2]
    prepare: org.jpos.jcard.CreateTranLog [2.3/3.6]
    prepare: org.jpos.jcard.CheckCustomer [8.5/12.2]
    prepare: org.jpos.jcard.rest.WalletCreateParticipant [556.5/568.7]
    prepare: org.jpos.transaction.Close [0.1/568.9]
    prepare: org.jpos.jcard.rest.TriggerResponse [0.1/569.1]
    prepare: org.jpos.transaction.Debug [0.1/569.2]
    commit: org.jpos.transaction.Close [344.9/914.2]
    commit: org.jpos.jcard.rest.TriggerResponse [0.6/914.8]
    commit: org.jpos.transaction.Debug [9.8/924.7]
    end [0.9/925.7]
    </profiler>
</debug>
</log>
<log realm="debug" at="Wed Oct 09 20:07:03 UYST 2013.845">
<commit>
    <id>2</id>
    <context>
    <entry key='PAN'>0000000005</entry>
    <entry key='RRN'>000000000001</entry>
    <entry key='LOGEVT'><log realm="" at="Wed Oct 09 20:07:03 UYST 2013.846" lifespan="38ms">
<log>

    <![CDATA[
<transaction id="97" date="20131009200703" post-date="20131009" journal="jcard">
<detail>WalletCredit 2</detail>
<entry account="11.001.00" type="debit" layer="840">
    <amount>100.00</amount>
</entry>
<entry account="21.0000000005.1" type="credit" layer="840">
    <amount>100.00</amount>
</entry>
</transaction>
    ]]>
</log>
</log>
</entry>
    <entry key='WALLET_NUMBER'>0000000005</entry>
    <entry key='CARDHOLDER'>org.jpos.ee.CardHolder@64a5efa3[id=5]</entry>
    <entry key='DB'>org.jpos.ee.DB@63130f0d</entry>
    <entry key='LEDGER_BALANCE'>100.00</entry>
    <entry key='TRANLOG'>org.jpos.ee.TranLog@76250246[id=172]</entry>
    <entry key='AMOUNT'>100.00</entry>
    <entry key='SWITCH'>WalletCredit (walletcredit close trigger-response)</entry>
    <entry key='ACCOUNT'>org.jpos.gl.FinalAccount@20c0fe7c[id=49,code=21.0000000005.1]</entry>
    <entry key='TXNRESULT'>1</entry>
    <entry key='TXNMGR'>rest-txnmgr</entry>
    <entry key='IRC'>0000</entry>
    <entry key='CURRENCY'>840</entry>
    <entry key='DETAIL'>Test wallet credit</entry>
    <entry key='WALLET'>org.jpos.ee.Wallet@71df1c8a[id=1,number=0000000005]</entry>
    <entry key='CAPTURE_DATE'>Wed Oct 09 00:00:00 UYST 2013</entry>
    <entry key='TXNNAME'>WalletCredit</entry>
    <entry key='TIMESTAMP'>Wed Oct 09 20:07:03 UYST 2013</entry>
    <entry key='PROFILER'>
    <profiler>
        prepare-context [0.0/0.0]
        open [1.1/1.2]
        close [533.2/534.4]
        end [10.9/545.4]
    </profiler>
    </entry>
    <entry key='ISSUER'>org.jpos.ee.Issuer@6f59db4f[id=1,name=jcard]</entry>
    </context>
</commit>
</log>
<log realm="rest-txnmgr" at="Wed Oct 09 20:07:03 UYST 2013.864" lifespan="553ms">
<debug>
    rest-txnmgr-0:idle:2
            prepare: org.jpos.jcard.PrepareContext NO_JOIN
            prepare: org.jpos.jcard.Switch READONLY NO_JOIN
        selector: walletcredit close trigger-response
            prepare: org.jpos.jcard.rest.ValidateParams READONLY NO_JOIN
            prepare: org.jpos.transaction.Open READONLY NO_JOIN
            prepare: org.jpos.jcard.CreateTranLog NO_JOIN
            prepare: org.jpos.jcard.CheckCustomer NO_JOIN
            prepare: org.jpos.jcard.CheckWallet READONLY NO_JOIN
            prepare: org.jpos.jcard.rest.WalletTransactionParticipant READONLY NO_JOIN
            prepare: org.jpos.transaction.Close READONLY
            prepare: org.jpos.jcard.rest.TriggerResponse READONLY
            prepare: org.jpos.transaction.Debug READONLY
            commit: org.jpos.transaction.Close
            commit: org.jpos.jcard.rest.TriggerResponse
            commit: org.jpos.transaction.Debug
    head=3, tail=3, outstanding=0, active-sessions=2/64, tps=0, peak=1, avg=0.50, elapsed=553ms
    <profiler>
    prepare: org.jpos.jcard.PrepareContext [0.1/0.1]
    prepare: org.jpos.jcard.Switch [0.1/0.2]
    prepare: org.jpos.jcard.rest.ValidateParams [0.2/0.5]
    prepare: org.jpos.transaction.Open [0.8/1.3]
    prepare: org.jpos.jcard.CreateTranLog [2.4/3.8]
    prepare: org.jpos.jcard.CheckCustomer [8.7/12.5]
    prepare: org.jpos.jcard.CheckWallet [7.3/19.9]
    prepare: org.jpos.jcard.rest.WalletTransactionParticipant [487.2/507.2]
    prepare: org.jpos.transaction.Close [0.1/507.3]
    prepare: org.jpos.jcard.rest.TriggerResponse [0.0/507.4]
    prepare: org.jpos.transaction.Debug [0.0/507.5]
    commit: org.jpos.transaction.Close [27.0/534.5]
    commit: org.jpos.jcard.rest.TriggerResponse [0.1/534.7]
    commit: org.jpos.transaction.Debug [18.3/553.0]
    end [1.0/554.1]
    </profiler>
  </debug>
</log>

Hope you consider using the TransactionManager the next time you need to write a REST API call.