diff --git a/.github/workflows/qodo-review-comment.yml b/.github/workflows/qodo-review-comment.yml new file mode 100644 index 00000000..e2b8bd02 --- /dev/null +++ b/.github/workflows/qodo-review-comment.yml @@ -0,0 +1,74 @@ +name: Comment on PR + +on: + pull_request: + branches: + - dev + - main + types: [opened, synchronize] + +jobs: + trigger-bot-review: + if: github.event.pull_request.draft == false + name: Trigger Bot Review + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + GITHUB_TOKEN: ${{ secrets.BOT_REVIEW_COMMENT_ACCESS_TOKEN }} + MAIN_BRANCH: main + steps: + - uses: actions/checkout@v2 + + - name: Add comment on PR creation on dev and main + if: false + uses: actions/github-script@v7 + continue-on-error: true + timeout-minutes: 5 + with: + github-token: ${{ env.GITHUB_TOKEN }} + script: | + const comments = ['/describe']; + for (const comment of comments) { + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } + + - name: Add comment on PR creation to trigger bot review + if: github.event.action == 'opened' && github.base_ref == ${{ env.MAIN_BRANCH }} + uses: actions/github-script@v7 + continue-on-error: true + timeout-minutes: 5 + with: + github-token: ${{ env.GITHUB_TOKEN }} + script: | + const comments = ['/describe', '/review', '/improve']; + for (const comment of comments) { + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } + + - name: Add comment on PR update + if: github.event.action == 'synchronize' && github.base_ref == ${{ env.MAIN_BRANCH }} + uses: actions/github-script@v7 + continue-on-error: true + timeout-minutes: 5 + with: + github-token: ${{ env.GITHUB_TOKEN }} + script: | + const comments = ['/review', '/improve']; + for (const comment of comments) { + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } diff --git a/src/main/java/org/fineract/messagegateway/sms/domain/SMSBridge.java b/src/main/java/org/fineract/messagegateway/sms/domain/SMSBridge.java index 18ac3284..1eb90a80 100644 --- a/src/main/java/org/fineract/messagegateway/sms/domain/SMSBridge.java +++ b/src/main/java/org/fineract/messagegateway/sms/domain/SMSBridge.java @@ -167,7 +167,14 @@ public void setSMSBridgeToBridgeConfigs() { public void setSmsBridgeConfig(final Collection bridgeConfigurations) { this.bridgeConfigurations.clear(); - this.bridgeConfigurations = bridgeConfigurations; + + // Add new configurations + if (bridgeConfigurations != null) { + for (SMSBridgeConfig config : bridgeConfigurations) { + config.setSMSBridge(this); + this.bridgeConfigurations.add(config); + } + } } public String generateApiKey() { diff --git a/src/main/java/org/fineract/messagegateway/sms/providers/impl/africastalking/AfricastalkingMessageProvider.java b/src/main/java/org/fineract/messagegateway/sms/providers/impl/africastalking/AfricastalkingMessageProvider.java index d5f7a387..a9c95c5d 100644 --- a/src/main/java/org/fineract/messagegateway/sms/providers/impl/africastalking/AfricastalkingMessageProvider.java +++ b/src/main/java/org/fineract/messagegateway/sms/providers/impl/africastalking/AfricastalkingMessageProvider.java @@ -1,5 +1,6 @@ package org.fineract.messagegateway.sms.providers.impl.africastalking; +import okhttp3.*; import org.fineract.messagegateway.exception.MessageGatewayException; import org.fineract.messagegateway.sms.domain.SMSBridge; import org.fineract.messagegateway.sms.domain.SMSMessage; @@ -11,85 +12,160 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import okhttp3.FormBody; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - import java.io.IOException; +import java.util.Collections; @Service(value = "Africastalking") public class AfricastalkingMessageProvider extends SMSProvider { private static final Logger logger = LoggerFactory.getLogger(AfricastalkingMessageProvider.class); + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private static final String SANDBOX_API_URL = "https://api.sandbox.africastalking.com/version1/messaging"; + private static final String LIVE_API_URL = "https://api.africastalking.com/version1/messaging/bulk"; @Override public void sendMessage(SMSBridge smsBridgeConfig, SMSMessage message) throws MessageGatewayException { try { OkHttpClient client = new OkHttpClient(); - String from = smsBridgeConfig.getPhoneNo(); - logger.info("Sending SMS from: {}", from); - if (from.equals("AFRICASTKNG")) { - from = null; - } String username = smsBridgeConfig.getConfigValue("username"); - String url = "https://api.africastalking.com/version1/messaging"; - if (username.equals("sandbox")) { - url = "https://api.sandbox.africastalking.com/version1/messaging"; - } String mobile = smsBridgeConfig.getCountryCode() + message.getMobileNumber(); - FormBody.Builder requestBodyBuilder = new FormBody.Builder() - .add("username", username) - .add("to", mobile) - .add("message", message.getMessage()) - .add("bulkSMSMode", "1") - .add("enqueue", "0"); - - if (from != null) { - requestBodyBuilder.add("from", from); + String apiKey = smsBridgeConfig.getConfigValue("apiKey"); + + if (username.equals("sandbox")) { + sendMessageSandbox(client, smsBridgeConfig, message, mobile, apiKey); + } else { + sendMessageLive(client, smsBridgeConfig, message, mobile, apiKey); } + } catch (IOException e) { + logger.error("An error occurred while sending the SMS: {}", e.getMessage(), e); + throw new MessageGatewayException("Failed to send SMS. Please check the logs for more details."); + } + } + + private void sendMessageSandbox(OkHttpClient client, SMSBridge smsBridgeConfig, SMSMessage message, String mobile, String apiKey) throws IOException, MessageGatewayException { + String from = smsBridgeConfig.getPhoneNo(); + logger.info("Sending SMS via sandbox from: {}", from); + if (from.equals("AFRICASTKNG")) { + from = null; + } + + FormBody.Builder requestBodyBuilder = new FormBody.Builder() + .add("username", smsBridgeConfig.getConfigValue("username")) + .add("to", mobile) + .add("message", message.getMessage()) + .add("bulkSMSMode", "1") + .add("enqueue", "0"); + + if (from != null) { + requestBodyBuilder.add("from", from); + } + + FormBody requestBody = requestBodyBuilder.build(); + + // Log request details + logRequestDetails(SANDBOX_API_URL, "application/x-www-form-urlencoded", + String.format("username=%s, to=%s, message=%s, bulkSMSMode=1, enqueue=0%s", + smsBridgeConfig.getConfigValue("username"), mobile, message.getMessage(), + from != null ? ", from=" + from : "")); + + // Build and execute request + Request request = buildRequest(SANDBOX_API_URL, apiKey, "application/x-www-form-urlencoded", requestBody); + executeRequest(client, request, message); + } - FormBody requestBody = requestBodyBuilder.build(); - - Request request = new Request.Builder() - .url(url) - .addHeader("apiKey", smsBridgeConfig.getConfigValue("apiKey")) - .addHeader("Content-Type", "application/x-www-form-urlencoded") - .addHeader("Accept", "application/json") - .post(requestBody) - .build(); - Response response = client.newCall(request).execute(); - if (response.isSuccessful()) { - String responseBody = response.body().string(); - logger.info("SMS sent successfully. Response: {}", responseBody); - - // Parse the response body - JSONObject responseJson = new JSONObject(responseBody); - JSONObject smsMessageData = responseJson.getJSONObject("SMSMessageData"); + private void sendMessageLive(OkHttpClient client, SMSBridge smsBridgeConfig, SMSMessage message, String mobile, String apiKey) throws IOException, MessageGatewayException { + String senderId = smsBridgeConfig.getConfigValue("senderId"); + if (senderId == null || senderId.trim().isEmpty()) { + throw new MessageGatewayException("senderId configuration is required for live environment"); + } + + logger.info("Sending SMS via live API with senderId: {}", senderId); + + JSONObject jsonBody = new JSONObject() + .put("username", smsBridgeConfig.getConfigValue("username")) + .put("phoneNumbers", Collections.singletonList(mobile)) + .put("message", message.getMessage()) + .put("senderId", senderId); + + RequestBody requestBody = RequestBody.create(JSON, jsonBody.toString()); + + // Log request details + logRequestDetails(LIVE_API_URL, "application/json", jsonBody.toString()); + + // Build and execute request + Request request = buildRequest(LIVE_API_URL, apiKey, "application/json", requestBody); + executeRequest(client, request, message); + } + + /** + * Builds the HTTP request with appropriate headers + */ + private Request buildRequest(String url, String apiKey, String contentType, RequestBody requestBody) { + return new Request.Builder() + .url(url) + .addHeader("apiKey", apiKey) + .addHeader("Content-Type", contentType) + .addHeader("Accept", "application/json") + .post(requestBody) + .build(); + } + + /** + * Logs request details for debugging + */ + private void logRequestDetails(String url, String contentType, String payload) { + logger.info("Request URL: {}", url); + logger.info("Request Headers: Content-Type: {}, Accept: application/json", contentType); + logger.info("Request Payload: {}", payload); + } + + /** + * Executes the request and processes the response + */ + private void executeRequest(OkHttpClient client, Request request, SMSMessage message) throws IOException, MessageGatewayException { + Response response = client.newCall(request).execute(); + processResponse(response, message); + } + + private void processResponse(Response response, SMSMessage message) throws IOException, MessageGatewayException { + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new MessageGatewayException("Empty response received from Africa's Talking API"); + } + + String responseString = responseBody.string(); + if (response.isSuccessful()) { + logger.info("SMS sent successfully. Response: {}", responseString); + + JSONObject responseJson = new JSONObject(responseString); + JSONObject smsMessageData = responseJson.getJSONObject("SMSMessageData"); + + if (smsMessageData.has("Recipients") && !smsMessageData.isNull("Recipients")) { JSONArray recipients = smsMessageData.getJSONArray("Recipients"); - if (recipients.length() > 0) { + if (!recipients.isEmpty()) { JSONObject recipient = recipients.getJSONObject(0); - // Update the message with external ID and delivery status String messageId = recipient.getString("messageId"); int statusCode = recipient.getInt("statusCode"); + logger.info("Africa's Talking API response - MessageId: {}, StatusCode: {}", messageId, statusCode); SmsMessageStatusType deliveryStatus = AfricastalkingStatus.smsStatus(statusCode); + logger.info("Mapped delivery status: {}", deliveryStatus); message.setExternalId(messageId); message.setDeliveryStatus(deliveryStatus.getValue()); } else { String errorMessage = smsMessageData.getString("Message"); - logger.error("Failed to send SMS. Error message: {}", errorMessage); - throw new MessageGatewayException("Failed to send SMS. Error message: " + errorMessage); + logger.error("Failed to send SMS. Empty recipients array. Error message: {}", errorMessage); + throw new MessageGatewayException("Failed to send SMS. Empty recipients array. Error message: " + errorMessage); } } else { - logger.error("Failed to send SMS. Response code: {}, Response body: {}", response.code(), - response.body().string()); - throw new MessageGatewayException("Failed to send SMS. Please check the logs for more details."); + String errorMessage = smsMessageData.has("Message") ? smsMessageData.getString("Message") : "No Recipients array in response"; + logger.error("Failed to send SMS. Missing Recipients data. Error message: {}", errorMessage); + throw new MessageGatewayException("Failed to send SMS. Missing Recipients data. Error message: " + errorMessage); } - } catch (IOException e) { - logger.error("An error occurred while sending the SMS: {}", e.getMessage(), e); - throw new MessageGatewayException("Failed to send SMS. Please check the logs for more details."); + } else { + logger.error("Failed to send SMS. Response code: {}, Response body: {}", response.code(), responseString); + throw new MessageGatewayException("Failed to send SMS. Response code: " + response.code() + ", Response body: " + responseString); } } diff --git a/src/main/java/org/fineract/messagegateway/sms/providers/impl/africastalking/AfricastalkingStatus.java b/src/main/java/org/fineract/messagegateway/sms/providers/impl/africastalking/AfricastalkingStatus.java index d39d98fc..f3f09abc 100644 --- a/src/main/java/org/fineract/messagegateway/sms/providers/impl/africastalking/AfricastalkingStatus.java +++ b/src/main/java/org/fineract/messagegateway/sms/providers/impl/africastalking/AfricastalkingStatus.java @@ -7,7 +7,7 @@ public static SmsMessageStatusType smsStatus(final Integer AfricastalkingStatus) SmsMessageStatusType smsStatus = SmsMessageStatusType.PENDING; switch (AfricastalkingStatus) { case 100: - smsStatus = SmsMessageStatusType.PENDING; + smsStatus = SmsMessageStatusType.SENT; break; case 101: smsStatus = SmsMessageStatusType.SENT; diff --git a/src/main/java/org/fineract/messagegateway/sms/serialization/SmsBridgeSerializer.java b/src/main/java/org/fineract/messagegateway/sms/serialization/SmsBridgeSerializer.java index 06f6d686..41a7e781 100644 --- a/src/main/java/org/fineract/messagegateway/sms/serialization/SmsBridgeSerializer.java +++ b/src/main/java/org/fineract/messagegateway/sms/serialization/SmsBridgeSerializer.java @@ -153,6 +153,7 @@ private void assembleBridgeConfigParams(final SMSBridge bridge, final JsonArray baseDataValidator.reset().parameter(SmsConstants.configname_paramname).value(configName).notBlank(); baseDataValidator.reset().parameter(SmsConstants.configvalue_paramname).value(configValue).notBlank(); final SMSBridgeConfig config = new SMSBridgeConfig(configName, configValue) ; + config.setSMSBridge(bridge); configs.add(config) ; } bridge.setSmsBridgeConfig(configs) ;