diff --git a/README.md b/README.md index 293b402..564854f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Put a settings.xml into your ~/.m2 directory with the following content: central Central Repository - http://repo.maven.apache.org/maven2 + https://repo.maven.apache.org/maven2 default false @@ -39,7 +39,7 @@ Put a settings.xml into your ~/.m2 directory with the following content: central Central Repository - http://repo.maven.apache.org/maven2 + https://repo.maven.apache.org/maven2 default false never diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/config/MPesaSettings.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/config/MPesaSettings.java new file mode 100644 index 0000000..5d57bc6 --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/config/MPesaSettings.java @@ -0,0 +1,98 @@ +package org.openmf.psp.mpesa.config; + +import javax.annotation.PostConstruct; +import org.openmf.psp.config.ApplicationSettings; +import org.openmf.psp.config.Binding; +import org.openmf.psp.config.Header; +import org.openmf.psp.config.HubSettings; +import org.openmf.psp.config.Operation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("mpesa-settings") +public class MPesaSettings extends ApplicationSettings { + + private String consumerKey; + private String consumerSecret; + + HubSettings hubSettings; + + MPesaSettings() { + + } + + @Autowired + public MPesaSettings(HubSettings hubSettings) { + this.hubSettings = hubSettings; + } + + @PostConstruct + public void postConstruct() { + postConstruct(hubSettings); + } + + public String getConsumerKey() { + return consumerKey; + } + + public void setConsumerKey(String consumerKey) { + this.consumerKey = consumerKey; + } + + public String getConsumerSecret() { + return consumerSecret; + } + + public void setConsumerSecret(String consumerSecret) { + this.consumerSecret = consumerSecret; + } + + public enum MPesaHeader implements Header { + TENANT("tenant"); // Arbitrary variable, need to confirm + + private final String configName; + + MPesaHeader(String configName) { + this.configName = configName; + } + + @Override + public String getConfigName() { + return configName; + } + } + + public enum MPesaOperation implements Operation { + OAUTH("oauth-token"), + BALANCE("account-balance"); + + private final String configName; + + MPesaOperation(String configName) { + this.configName = configName; + } + + @Override + public String getConfigName() { + return configName; + } + } + + public enum MPesaBinding implements Binding { + BALANCE("balance"); // Arbitrary variable, need to confirm + + private final String configName; + + MPesaBinding(String configName) { + this.configName = configName; + } + + @Override + public String getConfigName() { + return configName; + } + } + +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/AccessTokenResponse.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/AccessTokenResponse.java new file mode 100644 index 0000000..a3f8155 --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/AccessTokenResponse.java @@ -0,0 +1,23 @@ +package org.openmf.psp.mpesa.dto; + +public class AccessTokenResponse { + + String access_token; + String expires_in; + + public String getAccess_token() { + return access_token; + } + + public void setAccess_token(String access_token) { + this.access_token = access_token; + } + + public String getExpires_in() { + return expires_in; + } + + public void setExpires_in(String expires_in) { + this.expires_in = expires_in; + } +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/BalanceRequest.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/BalanceRequest.java new file mode 100644 index 0000000..96895ab --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/BalanceRequest.java @@ -0,0 +1,80 @@ +package org.openmf.psp.mpesa.dto; + +import java.util.Arrays; + +public class BalanceRequest { + + String CommandID; + String PartyA; + String IdentifierType; + String Remarks; + String Initiator; + String SecurityCredential; + String QueueTimeOutURL; + String ResultURL; + + public String getCommandID() { + return CommandID; + } + + public void setCommandID(String commandID) { + CommandID = commandID; + } + + public String getPartyA() { + return PartyA; + } + + public void setPartyA(String partyA) { + PartyA = partyA; + } + + public String getIdentifierType() { + return IdentifierType; + } + + public void setIdentifierType(String identifierType) { + IdentifierType = identifierType; + } + + public String getRemarks() { + return Remarks; + } + + public void setRemarks(String remarks) { + Remarks = remarks; + } + + public String getInitiator() { + return Initiator; + } + + public void setInitiator(String initiator) { + Initiator = initiator; + } + + public String getSecurityCredential() { + return SecurityCredential; + } + + public void setSecurityCredentials(String securityCredential) { + SecurityCredential = securityCredential; + } + + public String getQueueTimeOutURL() { + return QueueTimeOutURL; + } + + public void setQueueTimeOutURL(String queueTimeOutURL) { + QueueTimeOutURL = queueTimeOutURL; + } + + public String getResultURL() { + return ResultURL; + } + + public void setResultURL(String resultURL) { + ResultURL = resultURL; + } + +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ReferenceData.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ReferenceData.java new file mode 100644 index 0000000..9e6570c --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ReferenceData.java @@ -0,0 +1,14 @@ +package org.openmf.psp.mpesa.dto; + +public class ReferenceData { + + public ReferenceItem referenceItem; + + public ReferenceItem getReferenceItem() { + return referenceItem; + } + + public void setReferenceItem(ReferenceItem referenceItem) { + this.referenceItem = referenceItem; + } +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ReferenceItem.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ReferenceItem.java new file mode 100644 index 0000000..6e98ca5 --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ReferenceItem.java @@ -0,0 +1,23 @@ +package org.openmf.psp.mpesa.dto; + +public class ReferenceItem { + + public String key; + public String value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/Result.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/Result.java new file mode 100644 index 0000000..a021c25 --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/Result.java @@ -0,0 +1,77 @@ +package org.openmf.psp.mpesa.dto; + +public class Result { + + public String conversationID; + public String originatorConversationID; + public ReferenceData referenceData; + public Integer resultCode; + public String resultDesc; + public ResultParameters resultParameters; + public Integer resultType; + public String transactionID; + + public String getConversationID() { + return conversationID; + } + + public void setConversationID(String conversationID) { + this.conversationID = conversationID; + } + + public String getOriginatorConversationID() { + return originatorConversationID; + } + + public void setOriginatorConversationID(String originatorConversationID) { + this.originatorConversationID = originatorConversationID; + } + + public ReferenceData getReferenceData() { + return referenceData; + } + + public void setReferenceData(ReferenceData referenceData) { + this.referenceData = referenceData; + } + + public Integer getResultCode() { + return resultCode; + } + + public void setResultCode(Integer resultCode) { + this.resultCode = resultCode; + } + + public String getResultDesc() { + return resultDesc; + } + + public void setResultDesc(String resultDesc) { + this.resultDesc = resultDesc; + } + + public ResultParameters getResultParameters() { + return resultParameters; + } + + public void setResultParameters(ResultParameters resultParameters) { + this.resultParameters = resultParameters; + } + + public Integer getResultType() { + return resultType; + } + + public void setResultType(Integer resultType) { + this.resultType = resultType; + } + + public String getTransactionID() { + return transactionID; + } + + public void setTransactionID(String transactionID) { + this.transactionID = transactionID; + } +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ResultParameter.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ResultParameter.java new file mode 100644 index 0000000..a6294c8 --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ResultParameter.java @@ -0,0 +1,23 @@ +package org.openmf.psp.mpesa.dto; + +public class ResultParameter { + + public String key; + public Integer value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ResultParameters.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ResultParameters.java new file mode 100644 index 0000000..2cc1f7b --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/ResultParameters.java @@ -0,0 +1,16 @@ +package org.openmf.psp.mpesa.dto; + +import java.util.List; + +public class ResultParameters { + + List resultParameter = null; + + public List getResultParameter() { + return resultParameter; + } + + public void setResultParameter(List resultParameter) { + this.resultParameter = resultParameter; + } +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/TransactionResponse.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/TransactionResponse.java new file mode 100644 index 0000000..0cadd36 --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/dto/TransactionResponse.java @@ -0,0 +1,14 @@ +package org.openmf.psp.mpesa.dto; + +public class TransactionResponse { + + public Result result; + + public Result getResult() { + return result; + } + + public void setResult(Result result) { + this.result = result; + } +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/channel/AccountBalance.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/channel/AccountBalance.java new file mode 100644 index 0000000..a100801 --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/channel/AccountBalance.java @@ -0,0 +1,58 @@ +package org.openmf.psp.mpesa.routebuilder.channel; + +import org.apache.camel.CamelContext; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.openmf.psp.config.BindingProperties; +import org.openmf.psp.config.HubSettings; +import org.openmf.psp.config.OperationProperties; +import org.openmf.psp.mpesa.config.MPesaSettings; +import org.openmf.psp.mpesa.dto.BalanceRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AccountBalance extends RouteBuilder { + + private HubSettings hubSettings; + private MPesaSettings mPesaSettings; + + @Autowired + public AccountBalance(CamelContext camelContext, HubSettings hubSettings, MPesaSettings mPesaSetting) { + super(camelContext); + this.hubSettings = hubSettings; + this.mPesaSettings = mPesaSetting; + } + + @Override + public void configure() throws Exception { + + OperationProperties transactionOperation = mPesaSettings.getOperation(MPesaSettings.MPesaOperation.BALANCE); + OperationProperties oauthOperation = mPesaSettings.getOperation(MPesaSettings.MPesaOperation.OAUTH); + String apiTransactionEndpoint = transactionOperation.getUrl(); + String apiOAuthEndpoint = oauthOperation.getUrl(); + + BindingProperties binding = mPesaSettings.getBinding(MPesaSettings.MPesaBinding.BALANCE); + String url = binding.getUrl(); + + String consumerEndpoint = ""; //TODO: Add consumerEndpoint after discussion + + from(consumerEndpoint) + .id("receive-account-balance-check-request") + .log("Account balance check request received") + .streamCaching() + .process(exchange -> { + exchange.setProperty("consumerKey", mPesaSettings.getConsumerKey()); + exchange.setProperty("consumerSecret", mPesaSettings.getConsumerSecret()); + exchange.setProperty("apiOAuthEndpoint", apiOAuthEndpoint); + exchange.setProperty("apiTransactionEndpoint", apiTransactionEndpoint); + exchange.setProperty("transactionType", binding.getName()); + exchange.setProperty("mainBody", exchange.getIn().getBody(String.class)); + }) + + .unmarshal().json(JsonLibrary.Jackson, BalanceRequest.class) + .to("direct:conductTransaction") + ; + + } +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/channel/BaseTransaction.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/channel/BaseTransaction.java new file mode 100644 index 0000000..4932521 --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/channel/BaseTransaction.java @@ -0,0 +1,54 @@ +package org.openmf.psp.mpesa.routebuilder.channel; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.openmf.psp.mpesa.config.MPesaSettings; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BaseTransaction extends RouteBuilder { + + @Override + public void configure() throws Exception { + + from("direct:conductTransaction") + .id("conductTransaction") + .log("Starting transaction") + .to("direct:getAccessToken") + ; + + from("direct:getAccessToken") + .id("getAccessToken") + .log("Initiated Process to get Access Token") + .process("fetchAccessTokenProcessor") + .choice() + .when(exchange -> exchange.getProperty("tokenResponseCode", String.class).equals("200")) + .log("Access Token fetch successful, moving to transaction.") + .to("direct:commitBalanceCheck") + .otherwise() + .log("Access token fetch unsuccessful.") + .to("direct:transactionFailure") + ; + + from("direct:commitBalanceCheck") + .id("commitBalanceCheck") + .log("Committing Balance Check") + .process("postTransactionProcess") + .choice() + .when(exchange -> exchange.getProperty("transactionResponseCode", String.class).equals("200")) + .log("Balance Check was successful") + .to("direct:endTransaction") + .otherwise() + .log("Balance Check failed!") + ; + + from("direct:endTransaction") + .id("endTransaction") + .choice() + .when(exchange -> exchange.getProperty("transactionType", String.class).equals(MPesaSettings.MPesaBinding.BALANCE)) + .marshal().json(JsonLibrary.Jackson) + ; + + } + +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/processor/FetchAccessTokenProcessor.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/processor/FetchAccessTokenProcessor.java new file mode 100644 index 0000000..56968da --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/processor/FetchAccessTokenProcessor.java @@ -0,0 +1,56 @@ +package org.openmf.psp.mpesa.routebuilder.processor; + +import com.sun.org.apache.xerces.internal.impl.dv.util.Base64; +import java.nio.charset.StandardCharsets; +import org.apache.camel.Exchange; +import org.apache.camel.Processor; +import org.openmf.psp.mpesa.dto.AccessTokenResponse; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component("fetchAccessTokenProcessor") +public class FetchAccessTokenProcessor implements Processor { + + RestTemplate restTemplate; + + public FetchAccessTokenProcessor(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + public void process (Exchange exchange) throws Exception { + + String app_key = exchange.getProperty("consumerKey", String.class); + String app_secret = exchange.getProperty("consumerSecret", String.class); + String appKeySecret = app_key + ":" + app_secret; + byte[] bytes = appKeySecret.getBytes(StandardCharsets.ISO_8859_1); + String auth = Base64.encode(bytes); + + String url = exchange.getProperty("apiOAuthEndpoint", String.class); + + HttpHeaders headers = new HttpHeaders(); + headers.set("authorization", "Basic " + auth); + headers.set("cache-control", "no-cache"); + + HttpEntity entity = new HttpEntity<>(headers); + + HttpMethod method = HttpMethod.GET; + + ResponseEntity responseEntity = restTemplate.exchange(url, method, entity, AccessTokenResponse.class); + AccessTokenResponse response = responseEntity.getBody(); + + if (responseEntity.getStatusCode() == HttpStatus.OK && response != null) { + exchange.setProperty("tokenResponseCode", "200"); + exchange.setProperty("access_token", response.getAccess_token()); + } else { + exchange.setProperty("tokenResponseCode", String.valueOf(responseEntity.getStatusCode().value())); + } + + } + +} diff --git a/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/processor/PostTransactionProcessor.java b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/processor/PostTransactionProcessor.java new file mode 100644 index 0000000..817d910 --- /dev/null +++ b/sources/payment-hub/src/main/java/org/openmf/psp/mpesa/routebuilder/processor/PostTransactionProcessor.java @@ -0,0 +1,50 @@ +package org.openmf.psp.mpesa.routebuilder.processor; + +import org.apache.camel.Exchange; +import org.apache.camel.Processor; +import org.openmf.psp.mpesa.dto.TransactionResponse; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component("postTransactionProcessor") +public class PostTransactionProcessor implements Processor { + + RestTemplate restTemplate; + + public PostTransactionProcessor(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + public void process (Exchange exchange) throws Exception { + + String body = exchange.getProperty("mainBody", String.class); + + String url = exchange.getProperty("apiTransactionEndpoint", String.class); + String access_token = exchange.getProperty("access_token", String.class); + + HttpHeaders headers = new HttpHeaders(); + headers.set("authorization", "Bearer " + access_token); + headers.set("content-type", "application/json"); + + HttpEntity entity = new HttpEntity<>(body, headers); + + HttpMethod method = HttpMethod.POST; + + ResponseEntity responseEntity = restTemplate.exchange(url, method, entity, TransactionResponse.class); + TransactionResponse response = responseEntity.getBody(); + + if (responseEntity.getStatusCode() == HttpStatus.OK && response != null) { + exchange.setProperty("transactionResponseCode", "200"); + exchange.getIn().setBody(response); + } else { + exchange.setProperty("transactionResponseCode", String.valueOf(responseEntity.getStatusCode().value())); + } + } + +} diff --git a/work/mpesa/application.yml b/work/mpesa/application.yml new file mode 100644 index 0000000..b0eb173 --- /dev/null +++ b/work/mpesa/application.yml @@ -0,0 +1,100 @@ +debug: true + +spring: + main: + web-application-type: none + +# the name of Camel +camel: + springboot: + name: RestCamel + +iban-settings: + fsp-instance-id-first-index: 4 + fsp-instance-id-length: 4 + fsp-tenant-id-first-index: 8 + fsp-tenant-id-length: 4 + +hub-settings: + instance: in02 + expiration: 30000 + tenants: tn03, tn04 + +channel-settings: + cors-enabled: true + headers: + - name: tenant + key: X-Tenant-Identifier + operations: #hub -> channel + - name: operation-basic-settings + host: https://fineract.mifos.io + tenants: + - name: tn03 + port: 48888 + - name: tn04 + port: 48889 + - name: transfers #post notification + base: interoperation/transfers + - name: response #put async response + base: interoperation/transactions + bindings: #channel -> hub + - name: binding-basic-settings + host: http://0.0.0.0 + port: 80 + - name: parties + base: channel/parties + - name: payment #post payment request + base: channel/transactions #/in01/channel/transactions + - name: status #get status by hub id + base: channel/transactions + - name: client-status #get status by client id + base: channel/transactions/client + +fsp-settings: + ilp-secret: h4on38bsDjKiat2783gnklgafikmeuu5123kpobb7jm99 + auth: + profile: BASIC + encode: NONE + login-class: org.openmf.psp.dto.fsp.LoginFineractXResponseDTO + headers: + - name: user + key: User + - name: tenant + key: Fineract-Platform-TenantId + operations: #hub -> fsp + - name: operation-basic-settings + user: mifos + password: password + host: https://fineract.mifos.io + port: 8443 + - name: auth #login + base: fineract-provider/api/v1/authentication + - name: requests + base: fineract-provider/api/v1/interoperation/requests + - name: transfers + base: fineract-provider/api/v1/interoperation/transfers + +mpesa-settings: + consumer-key: LOyKl0JIxeqwpv7qEUiU4QnKDuuiUPGO + consumer-secret: UirFo80R4UOACqvi + headers: + # TODO: Complete it + operations: #hub -> ott + - name: operation-basic-settings + host: https://sandbox.safaricom.co.ke + - name: oauth-token + base: oauth/v1/generate?grant_type=client_credentials + - name: account-balance + base: mpesa/accountbalance/v1/query + bindings: #ott -> hub + - name: binding-basic-settings + host: http://0.0.0.0 + port: #TODO: Discuss for the port number + - name: balance + host: balance + #TODO: Complete the bindings + +mock-settings: + start-channel-consumers: false + start-switch-consumers: false + start-fsp-consumers: false