Subversion Repositories SmartDukaan

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
37388 vikas 1
package com.spice.profitmandi.service.whatsapp;
2
 
3
import org.json.JSONArray;
4
import org.json.JSONObject;
5
 
6
import java.util.ArrayList;
7
import java.util.List;
8
 
9
/**
10
 * Request builders and response parsers for the BotPenguin gateway.
11
 *
12
 * <p>Deliberately static and Spring-free, unlike the equivalent private methods on
13
 * {@code CpassWhatsappService}: the nesting rules below are the part most likely to be got wrong, and
14
 * keeping them here lets them be tested directly with no context and no mocking.</p>
15
 */
16
public final class BotPenguinPayloads {
17
 
18
    private BotPenguinPayloads() {
19
    }
20
 
21
    // ------------------------------------------------------------------
22
    // Session messages  (POST /wa/send-message)
23
    // ------------------------------------------------------------------
24
 
25
    public static JSONObject textMessage(String userName, String waId, String body, List<String> tags) {
26
        JSONObject payload = base(userName, waId, "text", tags);
27
        payload.put("message", new JSONObject().put("text", body));
28
        return payload;
29
    }
30
 
31
    /**
32
     * Media session message. The gateway names the URL {@code source} and the caption {@code label} —
33
     * not {@code link}/{@code caption} as the template endpoint does.
34
     */
35
    public static JSONObject mediaMessage(String userName, String waId, String type, String source,
36
                                          String label, List<String> tags) {
37
        JSONObject message = new JSONObject().put("source", source);
38
        if (isPresent(label)) {
39
            message.put("label", label);
40
        }
41
        JSONObject payload = base(userName, waId, type, tags);
42
        payload.put("message", message);
43
        return payload;
44
    }
45
 
46
    private static JSONObject base(String userName, String waId, String type, List<String> tags) {
47
        JSONObject payload = new JSONObject();
48
        payload.put("userName", userName);
49
        payload.put("wa_id", waId);
50
        payload.put("type", type);
51
        putTags(payload, tags);
52
        return payload;
53
    }
54
 
55
    // ------------------------------------------------------------------
56
    // Template messages  (POST /wa/send-template)
57
    // ------------------------------------------------------------------
58
 
59
    public static JSONObject template(String userName, String waId, String templateId, String assignTo,
60
                                      List<String> tags, List<WhatsappTemplateComponent> components,
61
                                      boolean nameBasedParams) {
62
        JSONObject payload = new JSONObject();
63
        payload.put("userName", userName);
64
        payload.put("wa_id", waId);
65
        payload.put("templateId", templateId);
66
        // An unrecognised assignTo is a plausible cause of a rejected send, so send it only when set.
67
        if (isPresent(assignTo)) {
68
            payload.put("assignTo", assignTo);
69
        }
70
        putTags(payload, tags);
71
        payload.put("params", params(components, nameBasedParams));
72
        return payload;
73
    }
74
 
75
    private static JSONArray params(List<WhatsappTemplateComponent> components, boolean nameBasedParams) {
76
        JSONArray params = new JSONArray();
77
        if (components == null) {
78
            return params;
79
        }
80
        for (WhatsappTemplateComponent component : components) {
81
            JSONArray parameters = new JSONArray();
82
            for (WhatsappTemplateParam param : component.getParameters()) {
83
                parameters.put(parameter(param, nameBasedParams));
84
            }
85
            params.put(new JSONObject()
86
                    .put("type", component.getType())
87
                    .put("parameters", parameters));
88
        }
89
        return params;
90
    }
91
 
92
    /**
93
     * A text parameter carries a flat {@code text} key; a media parameter carries a nested object named
94
     * after its own type and no {@code text} key at all.
95
     *
96
     * <p>{@code parameter_name} is emitted only for name-based templates. Sending it to a positional
97
     * template — or omitting it on a name-based one — fails without a useful error, which is why the
98
     * template registry carries that flag rather than leaving it to the caller.</p>
99
     */
100
    private static JSONObject parameter(WhatsappTemplateParam param, boolean nameBasedParams) {
101
        JSONObject json = new JSONObject();
102
        json.put("type", param.getType());
103
        if (param.isText()) {
104
            json.put("text", param.getText());
105
        } else {
106
            JSONObject media = new JSONObject().put("link", param.getLink());
107
            if (isPresent(param.getFileName())) {
108
                media.put("filename", param.getFileName());
109
            }
110
            json.put(param.getType(), media);
111
        }
112
        if (nameBasedParams && isPresent(param.getParameterName())) {
113
            json.put("parameter_name", param.getParameterName());
114
        }
115
        return json;
116
    }
117
 
118
    private static void putTags(JSONObject payload, List<String> tags) {
119
        if (tags == null || tags.isEmpty()) {
120
            return;
121
        }
122
        JSONArray array = new JSONArray();
123
        for (String tag : tags) {
124
            if (isPresent(tag)) {
125
                array.put(tag);
126
            }
127
        }
128
        if (array.length() > 0) {
129
            payload.put("tags", array);
130
        }
131
    }
132
 
133
    // ------------------------------------------------------------------
134
    // Responses
135
    // ------------------------------------------------------------------
136
 
137
    /** Outcome of a send, as read from the gateway body. */
138
    public static final class SendResult {
139
        private final boolean success;
140
        private final String externalId;
141
        private final String status;
142
        private final String error;
143
 
144
        SendResult(boolean success, String externalId, String status, String error) {
145
            this.success = success;
146
            this.externalId = externalId;
147
            this.status = status;
148
            this.error = error;
149
        }
150
 
151
        public boolean isSuccess() {
152
            return success;
153
        }
154
 
155
        public String getExternalId() {
156
            return externalId;
157
        }
158
 
159
        public String getStatus() {
160
            return status;
161
        }
162
 
163
        public String getError() {
164
            return error;
165
        }
166
    }
167
 
168
    /**
169
     * Reads a send response.
170
     *
171
     * <p>This has to carry the whole burden of deciding success, because the HTTP client used for these
172
     * calls returns the response body without inspecting the status code — a 401 arrives looking exactly
173
     * like a 200. Treating "the call did not throw" as success would report a delivered invoice to the
174
     * store operator while the customer received nothing, and with no delivery webhook nothing would
175
     * ever contradict it. So: anything that is not an explicit {@code success: true} is a failure, and a
176
     * body we cannot parse is a failure rather than an exception.</p>
177
     */
178
    public static SendResult parseSend(String response) {
179
        if (!isPresent(response)) {
180
            return new SendResult(false, null, null, "empty response");
181
        }
182
        JSONObject json;
183
        try {
184
            json = new JSONObject(response);
185
        } catch (Exception e) {
186
            return new SendResult(false, null, null, "unparseable response: " + truncate(response));
187
        }
188
        if (!json.optBoolean("success", false)) {
189
            return new SendResult(false, null, null, errorText(json, response));
190
        }
191
 
192
        JSONObject data = data(json);
193
        if (data == null) {
194
            return new SendResult(false, null, null, "success with no data: " + truncate(response));
195
        }
196
        String externalId = firstNonEmpty(data, "_id", "id", "messageId");
197
        String status = data.optString("status", null);
198
        if (!isPresent(externalId)) {
199
            // Recorded as sent, but nothing to match a future delivery receipt against.
200
            return new SendResult(true, null, status, "success with no message id");
201
        }
202
        return new SendResult(true, externalId, status, null);
203
    }
204
 
205
    /** Parses the approved-template list. Returns an empty list rather than throwing on a bad body. */
206
    public static List<BotPenguinTemplateInfo> parseTemplates(String response) {
207
        List<BotPenguinTemplateInfo> templates = new ArrayList<>();
208
        if (!isPresent(response)) {
209
            return templates;
210
        }
211
        JSONArray rows;
212
        try {
213
            rows = new JSONObject(response).optJSONArray("data");
214
        } catch (Exception e) {
215
            return templates;
216
        }
217
        if (rows == null) {
218
            return templates;
219
        }
220
        for (int i = 0; i < rows.length(); i++) {
221
            JSONObject row = rows.optJSONObject(i);
222
            if (row == null) {
223
                continue;
224
            }
225
            JSONObject configuration = row.optJSONObject("configuration");
226
            if (configuration == null) {
227
                configuration = new JSONObject();
228
            }
229
            templates.add(new BotPenguinTemplateInfo(
230
                    row.optString("_id", null),
231
                    row.optString("templateId", null),
232
                    configuration.optString("name", null),
233
                    configuration.optString("language", null),
234
                    configuration.optString("parameterFormat", null),
235
                    row.optString("status", null)));
236
        }
237
        return templates;
238
    }
239
 
240
    private static JSONObject data(JSONObject json) {
241
        JSONObject data = json.optJSONObject("data");
242
        if (data != null) {
243
            return data;
244
        }
245
        JSONArray array = json.optJSONArray("data");
246
        if (array != null && array.length() > 0) {
247
            return array.optJSONObject(0);
248
        }
249
        return null;
250
    }
251
 
252
    private static String errorText(JSONObject json, String raw) {
253
        String message = firstNonEmpty(json, "message", "error", "errors");
254
        return isPresent(message) ? message : truncate(raw);
255
    }
256
 
257
    private static String firstNonEmpty(JSONObject json, String... keys) {
258
        for (String key : keys) {
259
            if (json.has(key)) {
260
                String value = json.optString(key, null);
261
                if (isPresent(value)) {
262
                    return value;
263
                }
264
            }
265
        }
266
        return null;
267
    }
268
 
269
    private static String truncate(String value) {
270
        if (value == null) {
271
            return null;
272
        }
273
        return value.length() <= 200 ? value : value.substring(0, 200);
274
    }
275
 
276
    private static boolean isPresent(String value) {
277
        return value != null && !value.trim().isEmpty();
278
    }
279
}