diff --git a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/api/definition/TransactionApi.java b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/api/definition/TransactionApi.java index cf151b523..f70987fa1 100644 --- a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/api/definition/TransactionApi.java +++ b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/api/definition/TransactionApi.java @@ -1,5 +1,6 @@ package org.mifos.connector.channel.api.definition; +import static org.mifos.connector.channel.camel.config.CamelProperties.CALLBACK_URL; import static org.mifos.connector.channel.camel.config.CamelProperties.CLIENTCORRELATIONID; import static org.mifos.connector.channel.zeebe.ZeebeVariables.TRANSACTION_ID; @@ -25,6 +26,7 @@ public interface TransactionApi { @PostMapping("/channel/transactionRequest") ResponseEntity transaction(@RequestHeader(value = "Platform-TenantId") String tenant, @RequestHeader(value = CLIENTCORRELATIONID, required = false) String correlationId, + @RequestHeader(value = CALLBACK_URL, required = false) String callbackURL, @RequestBody TransactionChannelRequestDTO requestBody) throws JsonProcessingException; @PostMapping("/channel/transaction/{" + TRANSACTION_ID + "}/resolve") diff --git a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/api/implementation/TransactionApiController.java b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/api/implementation/TransactionApiController.java index 7f9bf0054..d61795271 100644 --- a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/api/implementation/TransactionApiController.java +++ b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/api/implementation/TransactionApiController.java @@ -6,9 +6,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.camunda.zeebe.client.api.command.ClientStatusException; import io.grpc.Status; +import java.util.Objects; import org.apache.camel.Exchange; import org.apache.camel.ProducerTemplate; import org.mifos.connector.channel.api.definition.TransactionApi; +import org.mifos.connector.channel.camel.routes.ChannelRouteBuilder; import org.mifos.connector.channel.gsma_api.GsmaP2PResponseDto; import org.mifos.connector.channel.service.ValidateHeaders; import org.mifos.connector.channel.utils.HeaderConstants; @@ -19,6 +21,7 @@ import org.mifos.connector.common.channel.dto.TransactionChannelRequestDTO; import org.mifos.connector.common.exception.ValidationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -30,12 +33,16 @@ public class TransactionApiController implements TransactionApi { ObjectMapper objectMapper; @Autowired private ProducerTemplate producerTemplate; + @Autowired + ChannelRouteBuilder routeBuilder; + @Value("${mtn-transaction-request.tenant}") + private String mtnTenant; @Override - @ValidateHeaders(requiredHeaders = { HeaderConstants.PLATFORM_TENANT_ID, - HeaderConstants.CLIENT_CORRELATION_ID }, validatorClass = HeaderValidator.class, validationFunction = "validateTransactionRequest") - public ResponseEntity transaction(String tenant, String correlationId, TransactionChannelRequestDTO requestBody) - throws JsonProcessingException { + @ValidateHeaders(requiredHeaders = { HeaderConstants.PLATFORM_TENANT_ID, HeaderConstants.CLIENT_CORRELATION_ID, + HeaderConstants.X_Callback_URL }, validatorClass = HeaderValidator.class, validationFunction = "validateTransactionRequest") + public ResponseEntity transaction(String tenant, String correlationId, String callbackURL, + TransactionChannelRequestDTO requestBody) throws JsonProcessingException { try { ChannelValidator.validateTransfer(requestBody); @@ -45,6 +52,13 @@ public ResponseEntity transaction(String tenant, String corr Headers headers = new Headers.HeaderBuilder().addHeader("Platform-TenantId", tenant).addHeader(CLIENTCORRELATIONID, correlationId) .build(); + + if (Objects.equals(tenant, mtnTenant)) { + String transactionId = routeBuilder.mtnTxn(requestBody, mtnTenant, callbackURL); + GsmaP2PResponseDto responseDto = new GsmaP2PResponseDto(transactionId); + return ResponseEntity.status(HttpStatus.ACCEPTED).body(responseDto); + } + Exchange exchange = SpringWrapperUtil.getDefaultWrappedExchange(producerTemplate.getCamelContext(), headers, objectMapper.writeValueAsString(requestBody)); producerTemplate.send("direct:post-transaction-request", exchange); @@ -64,6 +78,5 @@ public void transactionResolve(String requestBody) throws JsonProcessingExceptio Headers headers = new Headers.HeaderBuilder().build(); Exchange exchange = SpringWrapperUtil.getDefaultWrappedExchange(producerTemplate.getCamelContext(), null, requestBody); producerTemplate.send("direct:post-transaction-resolve", exchange); - } } diff --git a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/camel/config/CamelProperties.java b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/camel/config/CamelProperties.java index cb00c7c2b..77daaa254 100644 --- a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/camel/config/CamelProperties.java +++ b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/camel/config/CamelProperties.java @@ -16,5 +16,6 @@ private CamelProperties() {} public static final String PAYEE_DFSP_ID = "X-PayeeDFSP-ID"; public static final String X_CALLBACKURL = "X-CallbackURL"; public static final String COUNTRY = "X-Country"; + public static final String CALLBACK_URL = "X-CallbackURL"; } diff --git a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/camel/routes/ChannelRouteBuilder.java b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/camel/routes/ChannelRouteBuilder.java index c1b340e47..45f8c617f 100644 --- a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/camel/routes/ChannelRouteBuilder.java +++ b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/camel/routes/ChannelRouteBuilder.java @@ -62,6 +62,7 @@ import org.mifos.connector.channel.utils.AMSProps; import org.mifos.connector.channel.utils.AMSUtils; import org.mifos.connector.channel.utils.Constants; +import org.mifos.connector.channel.utils.HeaderConstants; import org.mifos.connector.channel.zeebe.ZeebeProcessStarter; import org.mifos.connector.common.camel.AuthProcessor; import org.mifos.connector.common.camel.AuthProperties; @@ -103,6 +104,7 @@ public class ChannelRouteBuilder extends ErrorHandlerRouteBuilder { private String paymentTransferFlow; private String specialPaymentTransferFlow; private String transactionRequestFlow; + private String mtnTransactionRequestFlow; private String partyRegistration; private String inboundTransactionReqFlow; private String restAuthHost; @@ -127,6 +129,7 @@ public ChannelRouteBuilder(@Value("#{'${dfspids}'.split(',')}") List dfs @Value("${bpmn.flows.payment-transfer}") String paymentTransferFlow, @Value("${bpmn.flows.special-payment-transfer}") String specialPaymentTransferFlow, @Value("${bpmn.flows.transaction-request}") String transactionRequestFlow, + @Value("${bpmn.flows.mtn-transaction-request}") String mtnTransactionRequestFlow, @Value("${bpmn.flows.party-registration}") String partyRegistration, @Value("${bpmn.flows.inboundTransactionReq-flow}") String inboundTransactionReqFlow, @Value("${rest.authorization.host}") String restAuthHost, @Value("${operations.url}") String operationsUrl, @@ -163,6 +166,7 @@ public ChannelRouteBuilder(@Value("#{'${dfspids}'.split(',')}") List dfs this.restAuthHeader = restAuthHeader; this.operationsAuthEnabled = operationsAuthEnabled; this.destinationDfspId = destinationDfspId; + this.mtnTransactionRequestFlow = mtnTransactionRequestFlow; } @Override @@ -374,6 +378,19 @@ private void transferRoutes() { }); } + public String mtnTxn(TransactionChannelRequestDTO requestBody, String tenantId, String callbackURL) throws JsonProcessingException { + Map extraVariables = new HashMap<>(); + String body = objectMapper.writeValueAsString(requestBody); + extraVariables.put(HeaderConstants.X_Callback_URL, callbackURL); + + String tenantSpecificBpmn = mtnTransactionRequestFlow.replace("{dfspid}", tenantId); + + String transactionId = zeebeProcessStarter.startZeebeWorkflow(tenantSpecificBpmn, body, extraVariables); + + logger.debug("transactionId is : {}", transactionId); + return transactionId; + } + public ResponseEntity fetchApibyWorkflowKey(HttpEntity entity, long workflowInstanceKey) { return restTemplate.exchange(operationsUrl + "/transfer/" + workflowInstanceKey, HttpMethod.GET, entity, String.class); } diff --git a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/gsma_api/GsmaP2PResponseDto.java b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/gsma_api/GsmaP2PResponseDto.java index fc5415a25..a1574293b 100644 --- a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/gsma_api/GsmaP2PResponseDto.java +++ b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/gsma_api/GsmaP2PResponseDto.java @@ -1,5 +1,10 @@ package org.mifos.connector.channel.gsma_api; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor public class GsmaP2PResponseDto { public String transactionId; diff --git a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/validator/HeaderValidator.java b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/validator/HeaderValidator.java index 00a984450..9906c0bb2 100644 --- a/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/validator/HeaderValidator.java +++ b/ph-ee-connector-channel/src/main/java/org/mifos/connector/channel/validator/HeaderValidator.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Set; import javax.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.mifos.connector.channel.utils.ChannelValidatorsEnum; import org.mifos.connector.channel.utils.HeaderConstants; import org.mifos.connector.common.channel.dto.PhErrorDTO; @@ -16,6 +17,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +@Slf4j @Component public class HeaderValidator { @@ -25,6 +27,9 @@ public class HeaderValidator { @Value("#{'${default_headers}'.split(',')}") private List defaultHeader; + @Value("${mtn-transaction-request.tenant}") + private String mtnTenant; + private static final String resource = "channelValidator"; public PhErrorDTO validateTransfer(Set requiredHeaders, HttpServletRequest request) { @@ -54,6 +59,16 @@ public PhErrorDTO validateTransactionRequest(Set requiredHeaders, HttpSe request.getHeader(HeaderConstants.PLATFORM_TENANT_ID), ChannelValidatorsEnum.INVALID_PLATFORM_TENANT_ID, 20, ChannelValidatorsEnum.INVALID_PLATFORM_TENANT_ID_LENGTH); + // Checks for X-CallbackURL + if (request.getHeader(HeaderConstants.PLATFORM_TENANT_ID).equals(mtnTenant)) { + validatorBuilder.validateFieldIsNullAndMaxLengthWithFailureCode(resource, HeaderConstants.X_Callback_URL, + request.getHeader(HeaderConstants.X_Callback_URL), ChannelValidatorsEnum.INVALID_X_CALLBACK_URL, 100, + ChannelValidatorsEnum.INVALID_X_CALLBACK_URL_LENGTH); + } else { + validatorBuilder.validateFieldIgnoreNullAndMaxLengthWithFailureCode(resource, HeaderConstants.X_Callback_URL, + request.getHeader(HeaderConstants.X_Callback_URL), 100, ChannelValidatorsEnum.INVALID_X_CALLBACK_URL_LENGTH); + } + return handleValidationErrors(validatorBuilder); } diff --git a/ph-ee-connector-channel/src/main/resources/application.yml b/ph-ee-connector-channel/src/main/resources/application.yml index 86c543f8f..6dbe72aa1 100644 --- a/ph-ee-connector-channel/src/main/resources/application.yml +++ b/ph-ee-connector-channel/src/main/resources/application.yml @@ -56,6 +56,10 @@ bpmn: international-remittance-payee: "international_remittance_payee_process-{dfspid}" international-remittance-payer: "international_remittance_payer_process-{dfspid}" inboundTransactionReq-flow: "{ps}_flow_{ams}-{dfspid}" + mtn-transaction-request: "mtn_payee_transaction_request-{dfspid}" + +mtn-transaction-request: + tenant: "rhino" ams: groups: diff --git a/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/api/MtnCollectionApi.java b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/api/MtnCollectionApi.java new file mode 100644 index 000000000..7de3d5121 --- /dev/null +++ b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/api/MtnCollectionApi.java @@ -0,0 +1,58 @@ +package org.mifos.connector.mockpaymentschema.api; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.mifos.connector.mockpaymentschema.schema.MtnRtpDTO; +import org.mifos.connector.mockpaymentschema.service.MtnCollectionService; +import org.mifos.connector.mockpaymentschema.service.SendCallbackService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Objects; + +@Slf4j +@RestController +@RequestMapping("/mtn/collection") +public class MtnCollectionApi { + + @Autowired + private MtnCollectionService mtnService; + + @Autowired + private SendCallbackService callbackService; + + @Value("${mtn.contactpoint}") + private String mtnContactpoint; + + @Value("${mtn.endpoints.callback}") + private String callbackEndpoint; + + @Autowired + private ObjectMapper objectMapper; + + + @PostMapping("/v1_0/requesttopay") + public ResponseEntity requestToPay(@RequestBody MtnRtpDTO request) throws JsonProcessingException { + ResponseEntity response = mtnService.requestToPay(request); + + String callbackUrl = mtnContactpoint + callbackEndpoint; + callbackService.sendCallback(objectMapper.writeValueAsString(response.getBody()), callbackUrl); + + + return response; + } + + @GetMapping("/v1_0/requesttopay/{referenceId}") + public ResponseEntity getRequestToPayStatus(@PathVariable(name = "referenceId") String referenceId) throws + JsonProcessingException { + ResponseEntity response = mtnService.getRequestToPayStatus(referenceId); + + String callbackUrl = mtnContactpoint + callbackEndpoint; + callbackService.sendCallback(objectMapper.writeValueAsString(response.getBody()), callbackUrl); + + return response; + } +} diff --git a/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/MtnRtpDTO.java b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/MtnRtpDTO.java new file mode 100644 index 000000000..a58e37673 --- /dev/null +++ b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/MtnRtpDTO.java @@ -0,0 +1,15 @@ +package org.mifos.connector.mockpaymentschema.schema; + +import lombok.*; + +@Setter +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MtnRtpDTO { + private String amount; + private String currency; + private String externalId; + private Payer payer; +} diff --git a/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/Payer.java b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/Payer.java new file mode 100644 index 000000000..172db5af9 --- /dev/null +++ b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/Payer.java @@ -0,0 +1,13 @@ +package org.mifos.connector.mockpaymentschema.schema; + +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Payer { + private String partyIdType; + private String partyId; +} \ No newline at end of file diff --git a/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/RequestToPayFailureResponse.java b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/RequestToPayFailureResponse.java new file mode 100644 index 000000000..657f13572 --- /dev/null +++ b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/RequestToPayFailureResponse.java @@ -0,0 +1,17 @@ +package org.mifos.connector.mockpaymentschema.schema; + +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RequestToPayFailureResponse { + private String externalId; + private String amount; + private String currency; + private Payer payer; + private String status; + private String reason; +} diff --git a/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/RequestToPaySuccessResponse.java b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/RequestToPaySuccessResponse.java new file mode 100644 index 000000000..ee2c44425 --- /dev/null +++ b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/schema/RequestToPaySuccessResponse.java @@ -0,0 +1,17 @@ +package org.mifos.connector.mockpaymentschema.schema; + +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RequestToPaySuccessResponse { + private String financialTransactionId; + private String externalId; + private String amount; + private String currency; + private Payer payer; + private String status; +} \ No newline at end of file diff --git a/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/service/MtnCollectionService.java b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/service/MtnCollectionService.java new file mode 100644 index 000000000..d917e2114 --- /dev/null +++ b/ph-ee-connector-mock-payment-schema/src/main/java/org/mifos/connector/mockpaymentschema/service/MtnCollectionService.java @@ -0,0 +1,69 @@ +package org.mifos.connector.mockpaymentschema.service; + + +import org.mifos.connector.mockpaymentschema.schema.MtnRtpDTO; +import org.mifos.connector.mockpaymentschema.schema.Payer; +import org.mifos.connector.mockpaymentschema.schema.RequestToPayFailureResponse; +import org.mifos.connector.mockpaymentschema.schema.RequestToPaySuccessResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class MtnCollectionService { + + @Value("${mtn.payerIdentifier.reject}") + private String rejectPayerId; + + @Value("${mtn.payerIdentifier.delay}") + private String callbackDelayPayerId; + + public ResponseEntity requestToPay(MtnRtpDTO requestBody) { + String partyId = requestBody.getPayer().getPartyIdType(); + if (partyId.equals(rejectPayerId)) { + RequestToPayFailureResponse failureResponse = converterForFailureResponse(requestBody); + return ResponseEntity.status(HttpStatus.ACCEPTED).body((T) failureResponse); + } + RequestToPaySuccessResponse successResponse = converterForSuccessfulResponse(requestBody); + return ResponseEntity.status(HttpStatus.ACCEPTED).body((T) successResponse); + } + + public ResponseEntity getRequestToPayStatus(String referenceId) { + Payer payer = Payer.builder() + .partyIdType("MSISDN") + .partyId(callbackDelayPayerId) + .build(); + + RequestToPaySuccessResponse response = RequestToPaySuccessResponse.builder().payer(payer).build(); + return ResponseEntity.status(HttpStatus.ACCEPTED).body((T) response); + } + + + private RequestToPayFailureResponse converterForFailureResponse(MtnRtpDTO requestToPayDTO) { + + return RequestToPayFailureResponse.builder() + .externalId(requestToPayDTO.getExternalId()) + .amount(requestToPayDTO.getAmount()) + .currency(requestToPayDTO.getCurrency()) + .payer(requestToPayDTO.getPayer()) + .status("FAILED") + .reason("APPROVAL_REJECTED") + .build(); + } + + private RequestToPaySuccessResponse converterForSuccessfulResponse(MtnRtpDTO requestToPayDTO) { + + String financialTransactionId = UUID.randomUUID().toString().replace("-", ""); + return RequestToPaySuccessResponse.builder() + .financialTransactionId(financialTransactionId) + .externalId(requestToPayDTO.getExternalId()) + .amount(requestToPayDTO.getAmount()) + .currency(requestToPayDTO.getCurrency()) + .payer(requestToPayDTO.getPayer()) + .status("SUCCESSFUL") + .build(); + } +} \ No newline at end of file diff --git a/ph-ee-connector-mock-payment-schema/src/main/resources/application.yml b/ph-ee-connector-mock-payment-schema/src/main/resources/application.yml index 3e85667bd..66bc00990 100644 --- a/ph-ee-connector-mock-payment-schema/src/main/resources/application.yml +++ b/ph-ee-connector-mock-payment-schema/src/main/resources/application.yml @@ -42,6 +42,16 @@ async: threshold: amount: 20000 +mtn: + contactpoint: "http://ph-ee-connector-mtn:80" + endpoints: + callback: "/callback" + payerIdentifier: + reject: 46733123451 + delay: 46733123454 + failure: 46733123450 + + management: endpoint: health: diff --git a/ph-ee-env-template/helm/g2p-sandbox/values.yaml b/ph-ee-env-template/helm/g2p-sandbox/values.yaml index f5cb072a2..2295a9229 100644 --- a/ph-ee-env-template/helm/g2p-sandbox/values.yaml +++ b/ph-ee-env-template/helm/g2p-sandbox/values.yaml @@ -595,6 +595,9 @@ ph-ee-engine: enabled: true minio: enabled: true + connector-mtn: + enabled: true + image: docker.io/fynarfin/ph-ee-connector-mtn:latest connector_airtel: enabled: true diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/Chart.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/Chart.yaml new file mode 100644 index 000000000..0071f7358 --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +description: ph-ee-connector-mtn +name: connector-mtn +version: 1.0.0 +appVersion: "1.0.0" \ No newline at end of file diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/clusterrole.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/clusterrole.yaml new file mode 100644 index 000000000..fe1a82b37 --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/clusterrole.yaml @@ -0,0 +1,11 @@ +{{- if .Values.managedServiceAccount }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + # "namespace" omitted since ClusterRoles are not namespaced + name: ph-ee-connector-mtn-c-role +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "watch", "list"] + {{- end -}} diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/clusterrolebinding.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/clusterrolebinding.yaml new file mode 100644 index 000000000..0817592b2 --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/clusterrolebinding.yaml @@ -0,0 +1,15 @@ +{{- if .Values.managedServiceAccount }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ph-ee-connector-mtn-c-role-binding +subjects: + - kind: ServiceAccount + name: ph-ee-connector-mtn # name of your service account + namespace: {{ .Release.Namespace }} # this is the namespace your service account is in +roleRef: # referring to your ClusterRole + kind: ClusterRole + name: ph-ee-connector-mtn-c-role + apiGroup: rbac.authorization.k8s.io + + {{- end -}} diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/deployment.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/deployment.yaml new file mode 100644 index 000000000..5b7a81df0 --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/deployment.yaml @@ -0,0 +1,62 @@ +{{- if .Values.enabled -}} + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ph-ee-connector-mtn + labels: + app: ph-ee-connector-mtn +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + app: ph-ee-connector-mtn + template: + metadata: + labels: + app: ph-ee-connector-mtn + annotations: + {{- if .Values.deployment.annotations }} + {{ toYaml .Values.deployment.annotations | indent 8 }} + {{- end }} +spec: + initContainers: + #During this Pod's initialization, check that zeebe-gateway service is up and running before starting this pod + - name: check-zeebe-gateway-ready + image: busybox:latest + command: [ 'sh', '-c','until nc -vz {{ .Release.Name }}-zeebe-gateway 26500; do echo "Waiting for zeebe-gateway service"; sleep 2; done;' ] + containers: + - name: ph-ee-connector-mtn + image: "{{ .Values.image }}" + ports: + - containerPort: 5000 + imagePullPolicy: "{{ .Values.imagePullPolicy }}" + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: {{.Values.livenessProbe.initialDelaySeconds}} + periodSeconds: {{.Values.livenessProbe.periodSeconds}} + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: {{.Values.readinessProbe.initialDelaySeconds}} + periodSeconds: {{.Values.readinessProbe.periodSeconds}} + resources: + limits: + memory: "{{ .Values.limits.memory }}" + cpu: "{{ .Values.limits.cpu }}" + requests: + memory: "{{ .Values.requests.memory }}" + cpu: "{{ .Values.requests.cpu }}" + env: + - name: "loggingLevelRoot" + value: "{{ .Values.global.loggingLevelRoot }}" + {{- if .Values.extraEnvs | default .Values.deployment.extraEnvs }} + {{ toYaml ( .Values.extraEnvs | default .Values.deployment.extraEnvs ) | indent 10 }} + {{- end }} +envFrom: {{ toYaml ( .Values.envFrom | default .Values.deployment.envFrom ) | nindent 12 }} +securityContext: {{ toYaml ( .Values.podSecurityContext | default .Values.deployment.securityContext ) | nindent 12 }} + + {{- end }} diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/ingress.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/ingress.yaml new file mode 100644 index 000000000..054d387c8 --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/ingress.yaml @@ -0,0 +1,49 @@ +{{- if .Values.ingress.enabled -}} + {{- $pathtype := .Values.ingress.pathtype -}} + {{- $ingressPath := .Values.ingress.path -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ph-ee-connector-mtn + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- with .Values.ingress.annotations }} +annotations: + {{ toYaml . | indent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className | quote }} + {{- end }} + {{- if .Values.ingress.tls }} +tls: + {{- if .ingressPath }} + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- else }} + {{ toYaml .Values.ingress.tls | indent 4 }} + {{- end }} + {{- end}} +rules: + {{- range .Values.ingress.hosts }} +- host: {{ .host }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ $pathtype }} + backend: + service: + name: {{ .backend.service.name}} + port: + number: {{ .backend.service.port.number}} + {{- end }} + {{- end }} + {{- end }} diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/role.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/role.yaml new file mode 100644 index 000000000..f1234f202 --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/role.yaml @@ -0,0 +1,14 @@ +{{- if .Values.managedServiceAccount }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: ph-ee-connector-mtn-role + namespace: {{ .Release.Namespace }} +rules: + - apiGroups: + - "" + resources: + - pods + - configmaps + verbs: ["get", "create", "update"] + {{- end -}} diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/rolebinding.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/rolebinding.yaml new file mode 100644 index 000000000..4c4bba5bf --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/rolebinding.yaml @@ -0,0 +1,15 @@ +{{- if .Values.managedServiceAccount }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: ph-ee-connector-mtn-role-binding + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ph-ee-connector-mtn-role +subjects: + - kind: ServiceAccount + name: ph-ee-connector-mtn + namespace: {{ .Release.Namespace }} + {{- end -}} diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/service.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/service.yaml new file mode 100644 index 000000000..9a1043e48 --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.enabled -}} +apiVersion: {{ .Values.service.apiversion }} +kind: Service +metadata: + labels: + app: ph-ee-connector-mtn + name: ph-ee-connector-mtn +spec: + ports: + - name: port + port: 80 + protocol: TCP + targetPort: 5000 + selector: + app: ph-ee-connector-mtn + sessionAffinity: None + type: ClusterIP + {{- end }} diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/serviceaccount.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/serviceaccount.yaml new file mode 100644 index 000000000..a814a4afc --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.managedServiceAccount }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ph-ee-connector-mtn + annotations: + {{- with .Values.serviceAccountAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + app: ph-ee-connector-mtn + {{- end -}} diff --git a/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/values.yaml b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/values.yaml new file mode 100644 index 000000000..455223821 --- /dev/null +++ b/ph-ee-env-template/helm/ph-ee-engine/connector-mtn/values.yaml @@ -0,0 +1,41 @@ +service: + apiversion: "v1" + +secret: + apiversion: "v1" + +configmap: + apiversion: "v1" + +enabled: false +image: "" +loggingLevelRoot: "INFO" +limits: + cpu: "500m" + memory: "512M" +requests: + cpu: "100m" + memory: "256M" + +ingress: + enabled: false + annotations: {} + pathtype: ImplementationSpecific + hosts: + - host: "" + paths: + - path: / + tls: [] + +deployment: + apiVersion: "apps/v1" + annotations: + deployTime: "{{ .Values.deployTime }}" + +livenessProbe: + initialDelaySeconds: 20 + periodSeconds: 30 + +readinessProbe: + initialDelaySeconds: 20 + periodSeconds: 30 diff --git a/ph-ee-env-template/helm/ph-ee-engine/requirements.yaml b/ph-ee-env-template/helm/ph-ee-engine/requirements.yaml index e5857cb80..b376f7b36 100644 --- a/ph-ee-env-template/helm/ph-ee-engine/requirements.yaml +++ b/ph-ee-env-template/helm/ph-ee-engine/requirements.yaml @@ -128,4 +128,9 @@ dependencies: - name: ph-ee-connector version: 1.0.0 repository: "file://./connector-ph-ee-bulk" - condition: "ph-ee-connector.enabled" \ No newline at end of file + condition: "ph-ee-connector.enabled" + + - name: connector-mtn + version: 1.0.0 + repository: "file://./connector-mtn" + condition: "connector-mtn.enabled" \ No newline at end of file diff --git a/ph-ee-env-template/helm/ph-ee-engine/values.yaml b/ph-ee-env-template/helm/ph-ee-engine/values.yaml index 2c009331b..40addd9f8 100644 --- a/ph-ee-env-template/helm/ph-ee-engine/values.yaml +++ b/ph-ee-env-template/helm/ph-ee-engine/values.yaml @@ -2361,3 +2361,6 @@ minio: post_installation_job: enabled: false + +connector-mtn: + enabled: false diff --git a/ph-ee-importer-rdbms/src/main/resources/application-local.yml b/ph-ee-importer-rdbms/src/main/resources/application-local.yml index e008e74a2..a83621124 100644 --- a/ph-ee-importer-rdbms/src/main/resources/application-local.yml +++ b/ph-ee-importer-rdbms/src/main/resources/application-local.yml @@ -1331,5 +1331,37 @@ transfer: variableName: payerPartyId - field: errorInformation variableName: errorInformation - - + - name: mtn_payee_transaction_request + direction: INCOMING + type: TRANSACTION-REQUEST + transformers: + - field: amount + variableName: channelRequest + jsonPath: $.amount.amount + - field: currency + variableName: channelRequest + jsonPath: $.amount.currency + - field: payeePartyIdType + variableName: channelRequest + jsonPath: $.payee.partyIdInfo.partyIdType + - field: payeePartyId + variableName: channelRequest + jsonPath: $.payee.partyIdInfo.partyIdentifier + - field: payerPartyIdType + variableName: channelRequest + jsonPath: $.payer.partyIdInfo.partyIdType + - field: payerPartyId + variableName: channelRequest + jsonPath: $.payer.partyIdInfo.partyIdentifier + - field: transactionId + variableName: transactionId + - field: currency + variableName: currency + - field: batchId + variableName: batchId + - field: tenantId + variableName: tenantId + - field: errorInformation + variableName: errorInformation + - field: clientCorrelationId + variableName: clientCorrelationId diff --git a/ph-ee-integration-test/src/test/java/org/mifos/integrationtest/cucumber/stepdef/MtnStepDef.java b/ph-ee-integration-test/src/test/java/org/mifos/integrationtest/cucumber/stepdef/MtnStepDef.java new file mode 100644 index 000000000..d60f34273 --- /dev/null +++ b/ph-ee-integration-test/src/test/java/org/mifos/integrationtest/cucumber/stepdef/MtnStepDef.java @@ -0,0 +1,134 @@ +package org.mifos.integrationtest.cucumber.stepdef; + +import static com.github.tomakehurst.wiremock.client.WireMock.getAllServeEvents; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; + +import com.github.tomakehurst.wiremock.stubbing.ServeEvent; +import io.cucumber.core.internal.com.fasterxml.jackson.databind.JsonNode; +import io.cucumber.core.internal.com.fasterxml.jackson.databind.ObjectMapper; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import io.restassured.RestAssured; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.specification.RequestSpecification; +import java.io.IOException; +import java.util.List; +import org.mifos.connector.common.channel.dto.TransactionChannelRequestDTO; +import org.mifos.connector.common.mojaloop.dto.MoneyData; +import org.mifos.connector.common.mojaloop.dto.Party; +import org.mifos.connector.common.mojaloop.type.IdentifierType; +import org.mifos.integrationtest.common.TransactionHelper; +import org.mifos.integrationtest.common.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +public class MtnStepDef extends BaseStepDef { + + @Autowired + MockServerStepDef mockServerStepDef; + + @Autowired + ScenarioScopeState scenarioScopeState; + + Logger logger = LoggerFactory.getLogger(MtnStepDef.class); + + @Value("${tenantconfig.tenants.paymentbb1}") + private String tenant; + + private String tenantHeader = "Platform-TenantId"; + + @Given("I can create a TransactionChannelRequestDTO for MTN Transaction Request with PayerId {string}") + public void iCreateATransactionChannelRequestDTOForMtn(String payerId) { + TransactionHelper transactionHelper = new TransactionHelper(); + Party payer = transactionHelper.partyHelper(IdentifierType.MSISDN, payerId); + Party payee = transactionHelper.partyHelper(IdentifierType.MSISDN, "27710101999"); + MoneyData amount = transactionHelper.amountHelper("100", "SNR"); + TransactionChannelRequestDTO requestDTO = transactionHelper.transactionChannelRequestHelper(payer, payee, amount); + requestDTO.setClientRefId("123"); + ObjectMapper objectMapper = new ObjectMapper(); + + scenarioScopeState.payerIdentifier = payerId; + try { + scenarioScopeState.createTransactionChannelRequestBody = objectMapper.writeValueAsString(requestDTO); + } catch (Exception e) { + logger.error("An Exception occurred", e); + } + } + + @When("I call the transaction request API for MTN with expected status of {int}") + public void iCallTheTransactionRequestAPIForMtnWithExpectedStatusOf(int expectedStatus) { + RequestSpecification requestSpec = Utils.getDefaultSpec(); + + scenarioScopeState.tenant = tenant; + scenarioScopeState.response = RestAssured.given(requestSpec).header("Content-Type", "application/json") + .header(tenantHeader, scenarioScopeState.tenant).header("X-CallbackURL", identityMapperConfig.callbackURL) + .baseUri(channelConnectorConfig.channelConnectorContactPoint).body(scenarioScopeState.createTransactionChannelRequestBody) + .expect().spec(new ResponseSpecBuilder().expectStatusCode(expectedStatus).build()).when() + .post(channelConnectorConfig.transferReqEndpoint).andReturn().asString(); + + logger.info("Transaction Request Response: {}", scenarioScopeState.response); + } + + @When("I call the transaction request API for MTN with expected status of {int} and {string}") + public void iCallTheTransactionRequestAPIForMtnWithExpectedStatusOf(int expectedStatus, String stub) { + RequestSpecification requestSpec = Utils.getDefaultSpec(); + + scenarioScopeState.tenant = tenant; + scenarioScopeState.response = RestAssured.given(requestSpec).header("Content-Type", "application/json") + .header(tenantHeader, scenarioScopeState.tenant).header("X-CallbackURL", identityMapperConfig.callbackURL + stub) + .baseUri(channelConnectorConfig.channelConnectorContactPoint).body(scenarioScopeState.createTransactionChannelRequestBody) + .expect().spec(new ResponseSpecBuilder().expectStatusCode(expectedStatus).build()).when() + .post(channelConnectorConfig.transferReqEndpoint).andReturn().asString(); + + logger.info("Transaction Request Response: {}", scenarioScopeState.response); + } + + @Then("I should be able to extract response body from callback for mtn") + public void iShouldBeAbleToExtractResponseBodyFromCallbackForMtn() { + await().atMost(awaitMost, SECONDS).pollDelay(pollDelay, SECONDS).pollInterval(pollInterval, SECONDS).untilAsserted(() -> { + boolean flag = false; + JsonNode rootNode = null; + + List allServeEvents = getAllServeEvents(); + for (int i = allServeEvents.size() - 1; i >= 0; i--) { + ServeEvent request = allServeEvents.get(i); + if (!(request.getRequest().getBodyAsString()).isEmpty()) { + flag = true; + try { + rootNode = objectMapper.readTree(request.getRequest().getBody()); + logger.info("Rootnode value:" + rootNode); + assertThat(rootNode).isNotNull(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + assertThat(flag).isTrue(); + + if (rootNode.has("financialTransactionId")) { + scenarioScopeState.callbackBody = request.getRequest().getBodyAsString(); + assertThat(scenarioScopeState.callbackBody.contains(scenarioScopeState.payerIdentifier)); + } + } + }); + } + + @And("I should have {string} and {string} in mtn callback response") + public void iShouldHaveAndInResponse(String status, String statusValue) { + assertThat(scenarioScopeState.callbackBody).contains(status); + assertThat(scenarioScopeState.callbackBody).contains(statusValue); + + } + + @Then("I set the header as invalid for mtn") + public void setTenantAsNull() { + scenarioScopeState.tenant = null; + } + +} diff --git a/ph-ee-integration-test/src/test/java/resources/mtn.feature b/ph-ee-integration-test/src/test/java/resources/mtn.feature new file mode 100644 index 000000000..e11dbfed1 --- /dev/null +++ b/ph-ee-integration-test/src/test/java/resources/mtn.feature @@ -0,0 +1,28 @@ + +Feature: Mtn Flow Test + + @gov + Scenario: MTN-001 MTN Flow validation test + Given I can create a TransactionChannelRequestDTO for MTN Transaction Request with PayerId "" + Then I set the header as invalid for mtn + When I call the transaction request API for MTN with expected status of 400 + + @gov + Scenario: MTN-002 Mtn Flow Test for Successful Payment Response + When I can inject MockServer + Then I can start mock server + And I can register the stub with "/transactionRequest" endpoint for "PUT" request with status of 200 + Given I can create a TransactionChannelRequestDTO for MTN Transaction Request with PayerId "875621381" + When I call the transaction request API for MTN with expected status of 202 and "/transactionRequest" + Then I should be able to extract response body from callback for mtn + And I should have "status" and "SUCCESSFUL" in mtn callback response + + @gov + Scenario: MTN-003 Mtn Flow Test for Failed Payment Response + When I can inject MockServer + Then I can start mock server + And I can register the stub with "/transactionRequest" endpoint for "PUT" request with status of 200 + Given I can create a TransactionChannelRequestDTO for MTN Transaction Request with PayerId "46733123451" + When I call the transaction request API for MTN with expected status of 202 and "/transactionRequest" + Then I should be able to extract response body from callback for mtn + And I should have "status" and "FAILED" in mtn callback response \ No newline at end of file