From 2f62aad8da7225a3db337f2416ed3c08a3a99add Mon Sep 17 00:00:00 2001 From: sanchit-0 Date: Tue, 6 Aug 2024 16:55:43 +0530 Subject: [PATCH] Create mock Airtel APIs. The response codes in the `application.yml` file have been added from the Airtel documentation. Similarly, the responses returned by the API are based on the Airtel documentation. --- build.gradle | 2 +- .../org/mifos/connector/airtel/AppConfig.java | 14 ++ .../airtel/api/definition/CallBackApi.java | 21 ++ .../implementation/CallBackController.java | 28 +++ .../api/definition/AirtelMockApi.java | 45 ++++ .../implementation/AirtelMockController.java | 197 ++++++++++++++++++ .../mockairtel/utils/TransferStatus.java | 5 + src/main/resources/application.yaml | 25 ++- 8 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/mifos/connector/airtel/AppConfig.java create mode 100644 src/main/java/org/mifos/connector/airtel/api/definition/CallBackApi.java create mode 100644 src/main/java/org/mifos/connector/airtel/api/implementation/CallBackController.java create mode 100644 src/main/java/org/mifos/connector/airtel/mockairtel/api/definition/AirtelMockApi.java create mode 100644 src/main/java/org/mifos/connector/airtel/mockairtel/api/implementation/AirtelMockController.java create mode 100644 src/main/java/org/mifos/connector/airtel/mockairtel/utils/TransferStatus.java diff --git a/build.gradle b/build.gradle index ff39427..fd36523 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'org.mifos:ph-ee-connector-common:1.9.1-SNAPSHOT' + implementation 'org.mifos:ph-ee-connector-common:0.0.0' implementation 'org.json:json:20210307' checkstyle 'com.puppycrawl.tools:checkstyle:10.9.3' checkstyle 'com.github.sevntu-checkstyle:sevntu-checks:1.44.1' diff --git a/src/main/java/org/mifos/connector/airtel/AppConfig.java b/src/main/java/org/mifos/connector/airtel/AppConfig.java new file mode 100644 index 0000000..db182aa --- /dev/null +++ b/src/main/java/org/mifos/connector/airtel/AppConfig.java @@ -0,0 +1,14 @@ +package org.mifos.connector.airtel; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/org/mifos/connector/airtel/api/definition/CallBackApi.java b/src/main/java/org/mifos/connector/airtel/api/definition/CallBackApi.java new file mode 100644 index 0000000..8f13791 --- /dev/null +++ b/src/main/java/org/mifos/connector/airtel/api/definition/CallBackApi.java @@ -0,0 +1,21 @@ +package org.mifos.connector.airtel.api.definition; + +import org.mifos.connector.airtel.api.implementation.CallBackController; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelCallBackRequestDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class CallBackApi { + + @Autowired + CallBackController callBackController; + + @PostMapping("/callback") + public ResponseEntity getCallBack(@RequestBody AirtelCallBackRequestDTO requestBody) { + return callBackController.handleCallBackRequest(requestBody); + } +} diff --git a/src/main/java/org/mifos/connector/airtel/api/implementation/CallBackController.java b/src/main/java/org/mifos/connector/airtel/api/implementation/CallBackController.java new file mode 100644 index 0000000..aea2c73 --- /dev/null +++ b/src/main/java/org/mifos/connector/airtel/api/implementation/CallBackController.java @@ -0,0 +1,28 @@ +package org.mifos.connector.airtel.api.implementation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelCallBackRequestDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +@Service +public class CallBackController { + + @Autowired + ObjectMapper objectMapper; + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + public ResponseEntity handleCallBackRequest(AirtelCallBackRequestDTO requestBody) { + try { + logger.info("CallBack Request: {}", objectMapper.writeValueAsString(requestBody)); + return ResponseEntity.status(HttpStatus.OK).body(requestBody); + } catch (Exception ex) { + throw new RuntimeException("Invalid response!", ex); + } + } +} diff --git a/src/main/java/org/mifos/connector/airtel/mockairtel/api/definition/AirtelMockApi.java b/src/main/java/org/mifos/connector/airtel/mockairtel/api/definition/AirtelMockApi.java new file mode 100644 index 0000000..3453b4c --- /dev/null +++ b/src/main/java/org/mifos/connector/airtel/mockairtel/api/definition/AirtelMockApi.java @@ -0,0 +1,45 @@ +package org.mifos.connector.airtel.mockairtel.api.definition; + +import org.mifos.connector.airtel.mockairtel.api.implementation.AirtelMockController; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelCallBackRequestDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelEnquiryResponseDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelPaymentRequestDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelPaymentResponseDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class AirtelMockApi { + + @Autowired + private AirtelMockController airtelMockController; + + protected Logger logger = LoggerFactory.getLogger(this.getClass()); + + @GetMapping(value = "/standard/v1/payments/{transactionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity airtelTransactionEnquiry(@PathVariable String transactionId) { + return airtelMockController.getTransactionStatus(transactionId); + } + + @PostMapping("/merchant/v2/payments") + public ResponseEntity getAuthorization(@RequestHeader(value = "X-Country") String country, + @RequestHeader(value = "X-Currency") String currency, @RequestHeader(value = "Authorization") String authorization, + @RequestHeader(value = "x-signature") String signature, @RequestHeader(value = "x-key") String key, + @RequestBody AirtelPaymentRequestDTO airtelPaymentRequestDTO) { + return airtelMockController.initiateTransaction(airtelPaymentRequestDTO); + } + + @PostMapping("/sendcallback") + public ResponseEntity sendCallback(@RequestBody String transactionId) { + return airtelMockController.sendCallBack(transactionId); + } +} diff --git a/src/main/java/org/mifos/connector/airtel/mockairtel/api/implementation/AirtelMockController.java b/src/main/java/org/mifos/connector/airtel/mockairtel/api/implementation/AirtelMockController.java new file mode 100644 index 0000000..eaa233d --- /dev/null +++ b/src/main/java/org/mifos/connector/airtel/mockairtel/api/implementation/AirtelMockController.java @@ -0,0 +1,197 @@ +package org.mifos.connector.airtel.mockairtel.api.implementation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.UUID; +import org.mifos.connector.airtel.mockairtel.utils.TransferStatus; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelCallBackRequestDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelCallBackRequestTransactionDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelEnquiryResponseDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelEnquiryResponseDataDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelEnquiryResponseDataTransactionDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelPaymentRequestDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelPaymentResponseDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelPaymentResponseDataDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelPaymentResponseDataTransactionDTO; +import org.mifos.connector.common.mobilemoney.airtel.dto.AirtelResponseStatusDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +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.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class AirtelMockController { + + @Autowired + ObjectMapper objectMapper; + + @Value("${airtel.endpoints.contact-point}") + public String airtelContactPoint; + + @Value("${server.port}") + public String port; + + @Value("${airtel.endpoints.call-back}") + public String callBackEndpoint; + + @Value("${airtel.endpoints.send-call-back}") + public String sendCallBackEndpoint; + + @Value("${mock-airtel.MSISDN_FAILED}") + private String msisdnFailed; + + @Value("${mock-airtel.CLIENT_CORRELATION_ID_SUCCESSFUL}") + private String transactionIdSuccessful; + + @Value("${mock-airtel.CLIENT_CORRELATION_ID_FAILED}") + private String transactionIdFailed; + + @Value("${mock-airtel.SUCCESS_RESPONSE_CODE}") + private String successResponseCode; + + @Value("${mock-airtel.FAILED_RESPONSE_CODE}") + private String failedResponseCode; + + @Value("${mock-airtel.PENDING_RESPONSE_CODE}") + private String pendingResponseCode; + + @Value("${mock-airtel.RESULT_CODE}") + private String resultCode; + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + @Autowired + private RestTemplate restTemplate; + + public ResponseEntity getTransactionStatus(String transactionId) { + String airtelMoneyId = UUID.nameUUIDFromBytes(transactionId.getBytes()).toString().replace("-", ""); + String message; + String status; + String code; + String responseCode; + boolean success; + HttpStatus httpStatus; + + if (transactionId.equals(transactionIdSuccessful)) { + message = TransferStatus.SUCCESS.name(); + status = TransferStatus.TS.name(); + code = HttpStatus.OK.toString(); + responseCode = successResponseCode; + success = true; + httpStatus = HttpStatus.OK; + } + + else if (transactionId.equals(transactionIdFailed)) { + message = TransferStatus.FAILED.name(); + status = TransferStatus.TF.name(); + code = HttpStatus.BAD_REQUEST.toString(); + responseCode = failedResponseCode; + success = false; + httpStatus = HttpStatus.BAD_REQUEST; + } + + else { + message = TransferStatus.IN_PROGRESS.name(); + status = TransferStatus.TIP.name(); + code = HttpStatus.ACCEPTED.toString(); + responseCode = pendingResponseCode; + success = true; + httpStatus = HttpStatus.ACCEPTED; + } + + return airtelEnquiryResponse(airtelMoneyId, transactionId, message, status, code, responseCode, success, httpStatus); + } + + public ResponseEntity initiateTransaction(AirtelPaymentRequestDTO airtelPaymentRequestDTO) { + String message; + String status; + String responseCode; + boolean success; + String code = HttpStatus.OK.name(); + HttpStatus httpStatus = HttpStatus.OK; + + String msisdn = airtelPaymentRequestDTO.getSubscriber().getMsisdn(); + + if (msisdn.equals(msisdnFailed)) { + message = TransferStatus.FAILED.name(); + responseCode = failedResponseCode; + success = false; + AirtelResponseStatusDTO airtelResponseStatusDTO = new AirtelResponseStatusDTO(code, message, resultCode, responseCode, success); + AirtelPaymentResponseDTO responseEntity = new AirtelPaymentResponseDTO(null, airtelResponseStatusDTO); + return ResponseEntity.status(httpStatus).body(responseEntity); + } + + else { + message = TransferStatus.IN_PROGRESS.name(); + status = TransferStatus.IN_PROGRESS.name(); + responseCode = pendingResponseCode; + success = true; + + String url = airtelContactPoint + ":" + port + sendCallBackEndpoint; + + String transactionId = airtelPaymentRequestDTO.getTransaction().getId(); + restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(transactionId, new HttpHeaders()), AirtelCallBackRequestDTO.class); + } + + boolean id = false; + AirtelPaymentResponseDataTransactionDTO airtelPaymentResponseDataTransactionDTO = new AirtelPaymentResponseDataTransactionDTO(id, + status); + AirtelPaymentResponseDataDTO airtelResponseDataDTO = new AirtelPaymentResponseDataDTO(airtelPaymentResponseDataTransactionDTO); + AirtelResponseStatusDTO airtelResponseStatusDTO = new AirtelResponseStatusDTO(code, message, resultCode, responseCode, success); + AirtelPaymentResponseDTO responseEntity = new AirtelPaymentResponseDTO(airtelResponseDataDTO, airtelResponseStatusDTO); + return ResponseEntity.status(httpStatus).body(responseEntity); + } + + private ResponseEntity airtelEnquiryResponse(String airtelMoneyId, String transactionId, String message, + String status, String code, String responseCode, Boolean success, HttpStatus httpStatus) { + String id = transactionId; + + AirtelEnquiryResponseDataTransactionDTO airtelEnquiryResponseDataTransactionDTO = new AirtelEnquiryResponseDataTransactionDTO( + airtelMoneyId, id, message, status); + AirtelEnquiryResponseDataDTO airtelResponseDataDTO = new AirtelEnquiryResponseDataDTO(airtelEnquiryResponseDataTransactionDTO); + + AirtelResponseStatusDTO airtelResponseStatusDTO = new AirtelResponseStatusDTO(code, message, resultCode, responseCode, success); + + AirtelEnquiryResponseDTO responseEntity = new AirtelEnquiryResponseDTO(airtelResponseDataDTO, airtelResponseStatusDTO); + return ResponseEntity.status(httpStatus).body(responseEntity); + } + + @Async + public ResponseEntity sendCallBack(String transactionId) { + String url = airtelContactPoint + ":" + port + callBackEndpoint; + HttpHeaders headers = new HttpHeaders(); + String airtelMoneyId = UUID.nameUUIDFromBytes(transactionId.getBytes()).toString().replace("-", ""); + String statusCode = TransferStatus.TF.name(); + + if (transactionId.equals(transactionIdSuccessful)) { + statusCode = TransferStatus.TS.name(); + } + + AirtelCallBackRequestTransactionDTO transactionDTO = new AirtelCallBackRequestTransactionDTO(); + transactionDTO.setId(transactionId); + transactionDTO.setMessage("Paid amount x"); + transactionDTO.setStatusCode(statusCode); + transactionDTO.setAirtelMoneyId(airtelMoneyId); + + AirtelCallBackRequestDTO callBackRequestDTO = new AirtelCallBackRequestDTO(); + callBackRequestDTO.setTransaction(transactionDTO); + try { + // Sleep for 1 second before sending callback + Thread.sleep(1000); + ResponseEntity result = restTemplate.exchange(url, HttpMethod.POST, + new HttpEntity<>(callBackRequestDTO, headers), AirtelCallBackRequestDTO.class); + HttpStatus statusCode2 = result.getStatusCode(); + logger.info("Response code from sendcallback: {}", statusCode2.value()); + logger.info("Response sendcallback: {}", objectMapper.writeValueAsString(result)); + return result; + } catch (Exception ex) { + throw new RuntimeException("Invalid response!", ex); + } + } +} diff --git a/src/main/java/org/mifos/connector/airtel/mockairtel/utils/TransferStatus.java b/src/main/java/org/mifos/connector/airtel/mockairtel/utils/TransferStatus.java new file mode 100644 index 0000000..f29a267 --- /dev/null +++ b/src/main/java/org/mifos/connector/airtel/mockairtel/utils/TransferStatus.java @@ -0,0 +1,5 @@ +package org.mifos.connector.airtel.mockairtel.utils; + +public enum TransferStatus { + COMPLETED, FAILED, IN_PROGRESS, UNKNOWN, SUCCESS, TS, TF, TIP +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1f976fe..16286a0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,5 +1,8 @@ dfspids: "DFSPID" +server: + port: 8080 + transaction-id-length: -1 timer: "PT45S" @@ -13,15 +16,33 @@ operations: bpmn: flows: - airtel_flow_mifos: "airtel_flow_mifos-{dfspid}" + AIRTEL_FLOW_MIFOS: "airtel_flow_mifos-{dfspid}" ams: groups: - - identifier: "accountid" + - identifier: "account_id" value: "fineract" - identifier: "default" value : "fineract" +airtel: + MAX_RETRY_COUNT: 3 + endpoints: + contact-point: "http://localhost" + airtel-ussd-push: "/merchant/v2/payments/" + airtel-transaction-enquiry: "/standard/v1/payments/" + call-back: "/callback" + send-call-back: "/sendcallback" + +mock-airtel: + FAILED_RESPONSE_CODE: "DP00800001005" + SUCCESS_RESPONSE_CODE: "DP00800001001" + PENDING_RESPONSE_CODE: "DP00800001006" + MSISDN_SUCCESSFUL: "1643344477" + MSISDN_FAILED: "6729461912" + CLIENT_CORRELATION_ID_SUCCESSFUL: "123456" + CLIENT_CORRELATION_ID_FAILED: "1278320" + RESULT_CODE: "ESB000010" logging: level: