Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
package com.spice.profitmandi.service.whatsapp;import org.json.JSONArray;import org.json.JSONObject;import java.util.ArrayList;import java.util.List;/*** Request builders and response parsers for the BotPenguin gateway.** <p>Deliberately static and Spring-free, unlike the equivalent private methods on* {@code CpassWhatsappService}: the nesting rules below are the part most likely to be got wrong, and* keeping them here lets them be tested directly with no context and no mocking.</p>*/public final class BotPenguinPayloads {private BotPenguinPayloads() {}// ------------------------------------------------------------------// Session messages (POST /wa/send-message)// ------------------------------------------------------------------public static JSONObject textMessage(String userName, String waId, String body, List<String> tags) {JSONObject payload = base(userName, waId, "text", tags);payload.put("message", new JSONObject().put("text", body));return payload;}/*** Media session message. The gateway names the URL {@code source} and the caption {@code label} —* not {@code link}/{@code caption} as the template endpoint does.*/public static JSONObject mediaMessage(String userName, String waId, String type, String source,String label, List<String> tags) {JSONObject message = new JSONObject().put("source", source);if (isPresent(label)) {message.put("label", label);}JSONObject payload = base(userName, waId, type, tags);payload.put("message", message);return payload;}private static JSONObject base(String userName, String waId, String type, List<String> tags) {JSONObject payload = new JSONObject();payload.put("userName", userName);payload.put("wa_id", waId);payload.put("type", type);putTags(payload, tags);return payload;}// ------------------------------------------------------------------// Template messages (POST /wa/send-template)// ------------------------------------------------------------------public static JSONObject template(String userName, String waId, String templateId, String assignTo,List<String> tags, List<WhatsappTemplateComponent> components,boolean nameBasedParams) {JSONObject payload = new JSONObject();payload.put("userName", userName);payload.put("wa_id", waId);payload.put("templateId", templateId);// An unrecognised assignTo is a plausible cause of a rejected send, so send it only when set.if (isPresent(assignTo)) {payload.put("assignTo", assignTo);}putTags(payload, tags);payload.put("params", params(components, nameBasedParams));return payload;}private static JSONArray params(List<WhatsappTemplateComponent> components, boolean nameBasedParams) {JSONArray params = new JSONArray();if (components == null) {return params;}for (WhatsappTemplateComponent component : components) {JSONArray parameters = new JSONArray();for (WhatsappTemplateParam param : component.getParameters()) {parameters.put(parameter(param, nameBasedParams));}params.put(new JSONObject().put("type", component.getType()).put("parameters", parameters));}return params;}/*** A text parameter carries a flat {@code text} key; a media parameter carries a nested object named* after its own type and no {@code text} key at all.** <p>{@code parameter_name} is emitted only for name-based templates. Sending it to a positional* template — or omitting it on a name-based one — fails without a useful error, which is why the* template registry carries that flag rather than leaving it to the caller.</p>*/private static JSONObject parameter(WhatsappTemplateParam param, boolean nameBasedParams) {JSONObject json = new JSONObject();json.put("type", param.getType());if (param.isText()) {json.put("text", param.getText());} else {JSONObject media = new JSONObject().put("link", param.getLink());if (isPresent(param.getFileName())) {media.put("filename", param.getFileName());}json.put(param.getType(), media);}if (nameBasedParams && isPresent(param.getParameterName())) {json.put("parameter_name", param.getParameterName());}return json;}private static void putTags(JSONObject payload, List<String> tags) {if (tags == null || tags.isEmpty()) {return;}JSONArray array = new JSONArray();for (String tag : tags) {if (isPresent(tag)) {array.put(tag);}}if (array.length() > 0) {payload.put("tags", array);}}// ------------------------------------------------------------------// Responses// ------------------------------------------------------------------/** Outcome of a send, as read from the gateway body. */public static final class SendResult {private final boolean success;private final String externalId;private final String status;private final String error;SendResult(boolean success, String externalId, String status, String error) {this.success = success;this.externalId = externalId;this.status = status;this.error = error;}public boolean isSuccess() {return success;}public String getExternalId() {return externalId;}public String getStatus() {return status;}public String getError() {return error;}}/*** Reads a send response.** <p>This has to carry the whole burden of deciding success, because the HTTP client used for these* calls returns the response body without inspecting the status code — a 401 arrives looking exactly* like a 200. Treating "the call did not throw" as success would report a delivered invoice to the* store operator while the customer received nothing, and with no delivery webhook nothing would* ever contradict it. So: anything that is not an explicit {@code success: true} is a failure, and a* body we cannot parse is a failure rather than an exception.</p>*/public static SendResult parseSend(String response) {if (!isPresent(response)) {return new SendResult(false, null, null, "empty response");}JSONObject json;try {json = new JSONObject(response);} catch (Exception e) {return new SendResult(false, null, null, "unparseable response: " + truncate(response));}if (!json.optBoolean("success", false)) {return new SendResult(false, null, null, errorText(json, response));}JSONObject data = data(json);if (data == null) {return new SendResult(false, null, null, "success with no data: " + truncate(response));}String externalId = firstNonEmpty(data, "_id", "id", "messageId");String status = data.optString("status", null);if (!isPresent(externalId)) {// Recorded as sent, but nothing to match a future delivery receipt against.return new SendResult(true, null, status, "success with no message id");}return new SendResult(true, externalId, status, null);}/** Parses the approved-template list. Returns an empty list rather than throwing on a bad body. */public static List<BotPenguinTemplateInfo> parseTemplates(String response) {List<BotPenguinTemplateInfo> templates = new ArrayList<>();if (!isPresent(response)) {return templates;}JSONArray rows;try {rows = new JSONObject(response).optJSONArray("data");} catch (Exception e) {return templates;}if (rows == null) {return templates;}for (int i = 0; i < rows.length(); i++) {JSONObject row = rows.optJSONObject(i);if (row == null) {continue;}JSONObject configuration = row.optJSONObject("configuration");if (configuration == null) {configuration = new JSONObject();}templates.add(new BotPenguinTemplateInfo(row.optString("_id", null),row.optString("templateId", null),configuration.optString("name", null),configuration.optString("language", null),configuration.optString("parameterFormat", null),row.optString("status", null)));}return templates;}private static JSONObject data(JSONObject json) {JSONObject data = json.optJSONObject("data");if (data != null) {return data;}JSONArray array = json.optJSONArray("data");if (array != null && array.length() > 0) {return array.optJSONObject(0);}return null;}private static String errorText(JSONObject json, String raw) {String message = firstNonEmpty(json, "message", "error", "errors");return isPresent(message) ? message : truncate(raw);}private static String firstNonEmpty(JSONObject json, String... keys) {for (String key : keys) {if (json.has(key)) {String value = json.optString(key, null);if (isPresent(value)) {return value;}}}return null;}private static String truncate(String value) {if (value == null) {return null;}return value.length() <= 200 ? value : value.substring(0, 200);}private static boolean isPresent(String value) {return value != null && !value.trim().isEmpty();}}