Subversion Repositories SmartDukaan

Rev

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();
    }
}