Sunday, November 20, 2016

Lipa Na MPESA Online Checkout - a Java Developer Toolkit & Startup Guide

Introduction 

There is heaps of excitement around digital finance following release of M-PESA API. Mobile payments for C2B online checkout. It's been a year since. As taunted there is expected  developers innovation around this.

I will provide a quick start guide for java developers geared towards integrating existing  Java applications.

Pre-requisites

JDK 7 or latest

Instruction steps


  • Download Full M-PESA API Guide here 
  • Read through Developers Guide C2B_OnlineCheckout_V2.0.doc
  • Generate JAX-WS artifacts . Your client will use this api to access the published  web service. 

Generate JAX-WS artifacts

We will use wsimport tool is used to parse checkout WSDL file and generate required files. 
  • >> mkdir src
  • 
    
    >> wsimport -XadditionalHeaders -clientjar safaricom-lipanampesa-onlinecheckout-wsdl.jar -s src -p ke.co.makara.integration.mpesa.lnmocheckout http://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl
    
  • Check wsimport --help to understand above options; 
Note the package directory for generated artefacts. WSDL is hosted on the lipa na M-PESA endpoint. For this purpose we use the production url. -clientjar option, new in Java 7, simplifies things. You would otherwise have to use -wsdllocation /META-INF/wsdl/Checkout.wsdl option. And copy the wsdl in META-INF after to mitigate the Java 6 wsimport limitation. See limitation below.
  • Optionally create source jar
While inside src folder run command. 


>> jar cvf safaricom-lipanampesa-onlinecheckout-source-wsdl.jar ke/


Next we use the web service artifacts to invoke the web service from a web service client.

Generated artifacts

  1. Service Endpoint Interface (SEI) - LNMOPortType.java
  2. Service class - LnmoCheckoutService.java
  3. If a wsdl:fault is present in the WSDL, an Exception class
  4. Java classes mapped from schema types eg ProcessCheckOutRequest.java
  5. If a wsdl:message is present in WSDL, asynchronous response beans eg ProcessCheckOutResponse.java

Web Service Client

Next we use the web service artifacts to invoke the web service from a web service client.
This can be a servlet invoked from front-end. Out of scope. For simplicity I will create a java class ~ a standalone console

  • Create a java class say LNMOCheckoutTester
  • In the Java client application, create an instance of the LnmoCheckoutService service


LnmoCheckoutService lnmoCheckoutService = new LnmoCheckoutService(); // lina na mpesa online checkout instance

  • The Service class will be created during the build.
  • Obtain a proxy to the service from the service using the getLnmoCheckout() method


LNMOPortType soap = lnmoCheckoutService.getLnmoCheckout();

 The port carries the protocol binding and service endpoint address information. 
  • Configure the service endpoint
Configure the request context properties on the javax.xml.ws.BindingProvider interface


((BindingProvider)soap).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, url + "lnmo_checkout_server.php"); // specifying this property on the RequestContext


JAX-WS provides support for the dynamic invocation of service endpoint operations.
I store the web service url in database and append the wsdl endpoint to this url. This configures the endpoint at runtime. 
  • Compose our client request M-PESA checkout message 
This is the message payload


ProcessCheckOutRequest checkOutRequest = objFactory.createProcessCheckOutRequest();
  checkOutRequest.setMERCHANTTRANSACTIONID("54635469064");
  checkOutRequest.setREFERENCEID("TD346534GH");
  checkOutRequest.setAMOUNT(13300);
  checkOutRequest.setMSISDN("0721XXXXXX");
  checkOutRequest.setENCPARAMS("");
  checkOutRequest.setCALLBACKURL("https://makara.co.ke:8443/odt/checkout");
  checkOutRequest.setCALLBACKMETHOD("GET");
  checkOutRequest.setTIMESTAMP(String.valueOf(date.getTime()));

  • Configure request headers
Follow business rules in Safaricom document to build the password. See attached code.
For String merchantId, String passkey, Date requestTimeStamp; Convert the concatenated string to bytes, Hash the bytes to get arbitary binary data and Convert the binary data to string use base64



CheckOutHeader requestHeader = objFactory.createCheckOutHeader();
  requestHeader.setMERCHANTID(MERCHANT_ID);
  Date timestamp = new Date();
  String encryptedPassword = getEncryptedPassword(MERCHANT_ID, PASSKEY, timestamp);
  requestHeader.setPASSWORD(encryptedPassword.toUpperCase());
  requestHeader.setTIMESTAMP(String.valueOf(timestamp.getTime()));


  • Invoke the service endpoint with the dispatch client
soap.processCheckOut(checkOutRequest, requestHeader);
  • Process the response message from the service as per your business requirement


  ProcessCheckOutResponse checkOutResponse = new ProcessCheckOutResponse();
  checkOutResponse.getRETURNCODE();
  checkOutResponse.getDESCRIPTION();
  checkOutResponse.getTRXID();
  checkOutResponse.getCUSTMSG();


  • Tracing SOAP Traffic.
One of the usual steps involved in the debugging of Web Services applications is to inspect the request and response SOAP messages
Configure client to dump requests and response with JAX-WS 


System.setProperty("com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump", "true");

  • Updated 22/11/2016 : Source code 
 
/*
 * LNMOCheckoutTester.java
 *
 * Nov 20, 2016 Joseph Makara -  Created File to tester Lina Na M-PESA Online checkout
 *
 *
 */
package testMe;

import java.io.*;
import java.security.*;
import java.util.*;
import javax.net.ssl.*;
import javax.xml.ws.*;
import ke.co.makara.integration.mpesa.lnmocheckout.*;
import org.apache.commons.codec.binary.*;

/**
 * @author Joseph Makara
 *
 */
public class LNMOCheckoutTester {

 private static final String PASSKEY = "234fdsghfsg5654dgfhgf6dsfdsafsd43dgfhdgfdgfh74567";
 private static final String MERCHANT_ID = "678fsgd54354";
 private static final String REFERENCE_ID = "";
 private static final String ENDPOINT_URL = "https://safaricom.co.ke/mpesa_online/";
 private static final String CALLBACK_URL = "https://makara.co.ke:8443/odt/checkout";
 private static final String CALLBACK_METHOD = "GET";


 static {
  HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
   @Override
   public boolean verify(String hostname, SSLSession session) {
    if (hostname.equals("safaricom.co.ke")) return true;
    return false;
   }
  });
 }

 /**
  * @param args
  */
 public static void main(String[] args) {

  LNMOPortType soap = outBoundLNMOCheckout(ENDPOINT_URL);

  ObjectFactory objFactory = new ObjectFactory();

  CheckOutHeader requestHeader = objFactory.createCheckOutHeader();
  requestHeader.setMERCHANTID(MERCHANT_ID);
  Date timestamp = new Date();
  String encryptedPassword = getEncryptedPassword(MERCHANT_ID, PASSKEY, timestamp);
  requestHeader.setPASSWORD(encryptedPassword);
  requestHeader.setTIMESTAMP(String.valueOf(timestamp.getTime()));

  ProcessCheckOutRequest checkOutRequest = objFactory.createProcessCheckOutRequest();
  checkOutRequest = processCheckOut(timestamp);

  soap.processCheckOut(checkOutRequest, requestHeader);

  ProcessCheckOutResponse checkOutResponse = new ProcessCheckOutResponse();
  checkOutResponse.getRETURNCODE();
  checkOutResponse.getDESCRIPTION();
  checkOutResponse.getTRXID();
  checkOutResponse.getENCPARAMS();
  checkOutResponse.getCUSTMSG();
 }

 private static ProcessCheckOutRequest processCheckOut(Date date){

  ProcessCheckOutRequest checkOutRequest = new ProcessCheckOutRequest();
  checkOutRequest.setMERCHANTTRANSACTIONID("54635469064");
  checkOutRequest.setREFERENCEID("TD346534GH");
  checkOutRequest.setAMOUNT(3.45);
  checkOutRequest.setMSISDN("0721826284");
  checkOutRequest.setENCPARAMS("");
  checkOutRequest.setCALLBACKURL(CALLBACK_URL);
  checkOutRequest.setCALLBACKMETHOD(CALLBACK_METHOD);
  checkOutRequest.setTIMESTAMP(String.valueOf(date.getTime()));

  return  checkOutRequest;
 }

 /**
  * Convert the concatenated string to bytes
  * Hash the bytes to get arbitary binary data
  * Convert the binary data to string use base64
  *
  * @param merchantId
  * @param passkey
  * @param date
  * @return
  */
 private static String getEncryptedPassword(String merchantId, String passkey, Date date) {
  String encodedPassword = null;
  StringBuilder builder = new StringBuilder(merchantId)
  .append(passkey)
  .append(date.getTime());

  try {
   String sha256 = getSHA256Hash(builder.toString());
   return new String(Base64.encodeBase64(sha256.getBytes("UTF-8")));;
  } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) {
   ex.printStackTrace();
  }

  return encodedPassword;
 }

 private static LNMOPortType outBoundLNMOCheckout(String url) {
  System.setProperty("com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump", "true");
  LnmoCheckoutService lnmoCheckoutService = new LnmoCheckoutService();
  LNMOPortType soap = lnmoCheckoutService.getLnmoCheckout();

  ((BindingProvider)soap).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
   url + "lnmo_checkout_server.php");

  return soap;
 }

 private static String getSHA256Hash(String input) throws NoSuchAlgorithmException {
  MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
  byte[] result = mDigest.digest(input.getBytes());
  StringBuffer sb = new StringBuffer();
  for (int i = 0; i < result.length; i++) {
   sb.append(Integer.toString((result[i] & 0xff) + 0x100, 16).substring(1));
  }
  return sb.toString();
 }

}



Sample Soap Request



[HTTP request - https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php]---
Accept: text/xml, multipart/related
Content-Type: text/xml; charset=utf-8
SOAPAction: ""
User-Agent: JAX-WS RI 2.2.9-b130926.1035 svn-revision#5f6196f2b90e9460065a4c2f4e30e065b245e51e
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
    <S:Header>
        <ns2:CheckOutHeader xmlns:ns2="tns:ns">
            <MERCHANT_ID>F2678987M</MERCHANT_ID>
            <PASSWORD>QQFSOITZB5OJMJTW073/SCWLN5WMDL6LO0FP6DJZ8TQ=</PASSWORD>
            <TIMESTAMP>1479116220855</TIMESTAMP>
        </ns2:CheckOutHeader>
    </S:Header>
    <S:Body>
        <ns2:processCheckOutRequest xmlns:ns2="tns:ns">
            <MERCHANT_TRANSACTION_ID>54635469064</MERCHANT_TRANSACTION_ID>
            <REFERENCE_ID>TD346534GH</REFERENCE_ID>
            <AMOUNT>3.45</AMOUNT>
            <MSISDN>0721826284</MSISDN>
            <ENC_PARAMS></ENC_PARAMS>
            <CALL_BACK_URL>https://makara.co.ke:8443/odt/checkout</CALL_BACK_URL>
            <CALL_BACK_METHOD>lnmo</CALL_BACK_METHOD>
            <TIMESTAMP>1479116220855</TIMESTAMP>
        </ns2:processCheckOutRequest>
    </S:Body>
</S:Envelope>



Attachments

Soap Message Request

source code

client jar

source jar



You will only need to contact Safaricom to get Demo test org details (Merchant ID and PassKey) . No need for VPN setup following the new API

Stock take and Future

  1. There is more Safaricom can do to streamline things, Create proper and complete API Guide ..

This developer guide provides a good head start for any one familiar with web services integration. Instead of ranting about horrible state of things get your hands dirty and do stuff

Community frustrations

Just How Open is Safaricom’s Open API?

Safaricom Integration Nightmare part 2


Java 6 wsimport limitation

The problem with Java 6 wsimport is that the JAX-WS runtime needs to fetch the WSDLs from the endpoint each time a service instance is created, which could incur a network overhead. The WSDL location is saved in the generated artifacts and the JAX-WS runtime fetches the metadata, which is useful if the endpoint policy or the service definition has changed. In the absence of the runtime fetch of the metadata, the clients would need to be regenerated if the endpoint policy or the service definition have changed.

What is new in Java 7 wsimport?

Java 7 supports Java API for XML Web Services (JAX-WS) 2.2.4, which has introduced a new (since JAX-WS 2.2.2) wsimport option called - clientjar as shown in the following sample command:
wsimport -clientjar wsclient.jar
http://example.com/service/hello?WSDL

The -clientjar option fetches the WSDLs and the schemas and packages them with the generated client-side artifacts into a JAR file. By including the generated JAR file in the classpath of the web service client, there is no need to fetch the WSDLs from the endpoint each time a service instance is created, thus saving on network overhead.


5 comments:

Kevin Kepha said...

Good work man.

Augustine mwathi said...

Hi mr Makara, i'm new to android dev. due to that i have been searching through the web on 'how to integrate and implement lipa na mpesa API into android APP i'm working on but got stuck on this level' until i came across your post. i went through the instructions but i didn't understand how the source code and soap code piece up together. and also how client jar and source jar are different and used since both are the same, in terms of files included. I really need to implement this. your advice will be highly appreciated. Thank you

Augustine mwathi said...

following on the above query, i would like ask which library did you use and which one can you advice to use? also where can one find the mpesa API it self not the guide? thank you

Benja Mbuthia said...

Thanx Makara, i wish you can make a step by step video tutorial for this. This is a great work

Unknown said...

Learned a lot of new things from your post!Good creation ,It's amazing blog Core Java Online Course

About Me

Melbourne, VIC, Australia
Bsc. Software Engineering