@dev.smartpricing/message-composer-utils 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/types/api/aiIntegration.js +7 -0
  2. package/dist/types/api/aiIntegration.js.map +1 -0
  3. package/dist/types/api/apiMessages.js +2 -0
  4. package/dist/types/api/apiMessages.js.map +1 -0
  5. package/dist/types/api/index.js +3 -0
  6. package/dist/types/api/index.js.map +1 -0
  7. package/dist/types/app.js +23 -0
  8. package/dist/types/app.js.map +1 -0
  9. package/dist/types/brand.js +31 -0
  10. package/dist/types/brand.js.map +1 -0
  11. package/dist/types/dynamic-values.js +5 -0
  12. package/dist/types/dynamic-values.js.map +1 -0
  13. package/dist/types/emailblocks.v1.defaults.js +83 -0
  14. package/dist/types/emailblocks.v1.defaults.js.map +1 -0
  15. package/dist/types/emailblocks.v1.js +339 -0
  16. package/dist/types/emailblocks.v1.js.map +1 -0
  17. package/dist/types/index.js +16 -0
  18. package/dist/types/index.js.map +1 -0
  19. package/dist/types/kafka-events.js +2 -0
  20. package/dist/types/kafka-events.js.map +1 -0
  21. package/dist/types/language.js +2 -0
  22. package/dist/types/language.js.map +1 -0
  23. package/dist/types/media.js +6 -0
  24. package/dist/types/media.js.map +1 -0
  25. package/dist/types/message.js +41 -0
  26. package/dist/types/message.js.map +1 -0
  27. package/dist/types/meta-template.js +2 -0
  28. package/dist/types/meta-template.js.map +1 -0
  29. package/dist/types/meta-webhook.js +2 -0
  30. package/dist/types/meta-webhook.js.map +1 -0
  31. package/dist/types/submission-job.js +2 -0
  32. package/dist/types/submission-job.js.map +1 -0
  33. package/dist/types/tag.js +2 -0
  34. package/dist/types/tag.js.map +1 -0
  35. package/dist/types/utils.js +2 -0
  36. package/dist/types/utils.js.map +1 -0
  37. package/dist/types/whatsappblocks.v1.js +24 -0
  38. package/dist/types/whatsappblocks.v1.js.map +1 -0
  39. package/dist/utils/array.js +26 -0
  40. package/dist/utils/array.js.map +1 -0
  41. package/dist/utils/async.js +22 -0
  42. package/dist/utils/async.js.map +1 -0
  43. package/dist/utils/date.js +37 -0
  44. package/dist/utils/date.js.map +1 -0
  45. package/dist/utils/index.js +34 -0
  46. package/dist/utils/index.js.map +1 -0
  47. package/dist/utils/intl.js +37 -0
  48. package/dist/utils/intl.js.map +1 -0
  49. package/dist/utils/meta-status.js +21 -0
  50. package/dist/utils/meta-status.js.map +1 -0
  51. package/dist/utils/number.js +24 -0
  52. package/dist/utils/number.js.map +1 -0
  53. package/dist/utils/object.js +87 -0
  54. package/dist/utils/object.js.map +1 -0
  55. package/dist/utils/string.js +22 -0
  56. package/dist/utils/string.js.map +1 -0
  57. package/dist/utils/templates/brand-cascade.js +116 -0
  58. package/dist/utils/templates/brand-cascade.js.map +1 -0
  59. package/dist/utils/templates/email-compiler.js +126 -0
  60. package/dist/utils/templates/email-compiler.js.map +1 -0
  61. package/dist/utils/templates/email-mjml.js +444 -0
  62. package/dist/utils/templates/email-mjml.js.map +1 -0
  63. package/dist/utils/templates/email.js +580 -0
  64. package/dist/utils/templates/email.js.map +1 -0
  65. package/dist/utils/templates/html-sanitizer.js +95 -0
  66. package/dist/utils/templates/html-sanitizer.js.map +1 -0
  67. package/dist/utils/templates/messages.js +33 -0
  68. package/dist/utils/templates/messages.js.map +1 -0
  69. package/dist/utils/templates/meta.js +246 -0
  70. package/dist/utils/templates/meta.js.map +1 -0
  71. package/dist/utils/templates/tiptap.js +167 -0
  72. package/dist/utils/templates/tiptap.js.map +1 -0
  73. package/dist/utils/templates/whatsapp-compiler.js +168 -0
  74. package/dist/utils/templates/whatsapp-compiler.js.map +1 -0
  75. package/dist/utils/templates/whatsapp.js +329 -0
  76. package/dist/utils/templates/whatsapp.js.map +1 -0
  77. package/dist/utils/validations.js +39 -0
  78. package/dist/utils/validations.js.map +1 -0
  79. package/package.json +43 -0
  80. package/src/types/api/aiIntegration.ts +59 -0
  81. package/src/types/api/apiMessages.ts +22 -0
  82. package/src/types/api/index.ts +2 -0
  83. package/src/types/app.ts +76 -0
  84. package/src/types/brand.ts +80 -0
  85. package/src/types/dynamic-values.ts +47 -0
  86. package/src/types/emailblocks.v1.defaults.ts +123 -0
  87. package/src/types/emailblocks.v1.ts +655 -0
  88. package/src/types/index.ts +15 -0
  89. package/src/types/kafka-events.ts +46 -0
  90. package/src/types/language.ts +9 -0
  91. package/src/types/media.ts +26 -0
  92. package/src/types/message.ts +635 -0
  93. package/src/types/meta-template.ts +50 -0
  94. package/src/types/meta-webhook.ts +32 -0
  95. package/src/types/submission-job.ts +30 -0
  96. package/src/types/tag.ts +8 -0
  97. package/src/types/utils.ts +14 -0
  98. package/src/types/whatsappblocks.v1.ts +102 -0
  99. package/src/utils/array.ts +33 -0
  100. package/src/utils/async.ts +30 -0
  101. package/src/utils/date.ts +39 -0
  102. package/src/utils/index.ts +44 -0
  103. package/src/utils/intl.ts +51 -0
  104. package/src/utils/meta-status.ts +24 -0
  105. package/src/utils/number.ts +36 -0
  106. package/src/utils/object.ts +101 -0
  107. package/src/utils/string.ts +24 -0
  108. package/src/utils/templates/brand-cascade.ts +156 -0
  109. package/src/utils/templates/email-compiler.ts +155 -0
  110. package/src/utils/templates/email-mjml.ts +602 -0
  111. package/src/utils/templates/email.ts +713 -0
  112. package/src/utils/templates/html-sanitizer.ts +121 -0
  113. package/src/utils/templates/messages.ts +44 -0
  114. package/src/utils/templates/meta.ts +336 -0
  115. package/src/utils/templates/tiptap.ts +247 -0
  116. package/src/utils/templates/whatsapp-compiler.ts +212 -0
  117. package/src/utils/templates/whatsapp.ts +408 -0
  118. package/src/utils/validations.ts +49 -0
  119. package/tsconfig.json +10 -0
@@ -0,0 +1,408 @@
1
+ import type {
2
+ MessageFromDb,
3
+ PostEntity,
4
+ WaHeaderTypeV1Url,
5
+ WhatsappMessage,
6
+ } from "../../types/index.js";
7
+ import {
8
+ WA_HEADER_TEXT_MAX_LENGTH,
9
+ WA_BODY_MAX_LENGTH,
10
+ WA_FOOTER_MAX_LENGTH,
11
+ WA_BUTTON_TEXT_MAX_LENGTH,
12
+ } from "../../types/index.js";
13
+ import { generateHTML, generateText } from "@tiptap/core";
14
+ import type { Content, JSONContent } from "@tiptap/core";
15
+ import { shortcodeToEmoji, gitHubEmojis } from "@tiptap/extension-emoji";
16
+ import { getTipTapExtensions } from "./tiptap.js";
17
+ import { isValidPhoneNumber, isValidUrl } from "../validations.js";
18
+
19
+ export function getWhatsappEmpty(
20
+ languageId: string,
21
+ ): PostEntity<WhatsappMessage> {
22
+ return {
23
+ user_id: "",
24
+ status: "draft",
25
+ render_type: "whatsapp_v1",
26
+ body: {
27
+ header: {
28
+ type: "none",
29
+ },
30
+ body: {
31
+ enabled: true,
32
+ content: undefined,
33
+ },
34
+ footer: {
35
+ enabled: false,
36
+ content: "",
37
+ },
38
+ buttons: {
39
+ enabled: false,
40
+ buttons: [],
41
+ },
42
+ },
43
+ metadata: {},
44
+ language_id: languageId,
45
+ message_group_id: "",
46
+ };
47
+ }
48
+
49
+ export interface WhatsappValidationResult {
50
+ valid: boolean;
51
+ errors: string[];
52
+ }
53
+
54
+ /**
55
+ * Validates WhatsApp message compliance with Meta requirements
56
+ * Checks:
57
+ * - Body content exists and not empty
58
+ * - Header text length (max 60 chars)
59
+ * - Body text length (max 1024 chars)
60
+ * - Footer text length (max 60 chars)
61
+ * - Button text length (max 20 chars per button)
62
+ * - Max 10 buttons total
63
+ * - Max 1 link button
64
+ * - Max 1 phone button
65
+ * - Phone numbers are valid E.164 format
66
+ * - Max 1 unsubscribe button
67
+ * - Max 3 quick_reply buttons
68
+ */
69
+ export function isWhatsappValid(
70
+ message: PostEntity<WhatsappMessage> | MessageFromDb,
71
+ ): WhatsappValidationResult {
72
+ const errors: string[] = [];
73
+
74
+ if (message.render_type !== "whatsapp_v1") {
75
+ return {
76
+ valid: false,
77
+ errors: ["editor.validation.error.invalidRenderType"],
78
+ };
79
+ }
80
+
81
+ // Validate header text length (use markdown output to match what Meta receives)
82
+ if (
83
+ message.body.header.type === "text" &&
84
+ message.body.header.content &&
85
+ !isContentEmpty(message.body.header.content)
86
+ ) {
87
+ const headerText = convertToWhatsappMarkdown(message.body.header.content);
88
+ if (headerText.length > WA_HEADER_TEXT_MAX_LENGTH) {
89
+ errors.push("editor.validation.error.headerTooLong");
90
+ }
91
+ }
92
+
93
+ // Validate header media url
94
+ if (["image", "video", "document"].includes(message.body.header.type)) {
95
+ if (
96
+ !("url" in message.body.header) ||
97
+ !(message.body.header as WaHeaderTypeV1Url).url?.trim()
98
+ ) {
99
+ errors.push("editor.validation.error.headerMediaRequired");
100
+ }
101
+ }
102
+
103
+ // Validate body content
104
+ if (!message.body.body.content) {
105
+ errors.push("editor.validation.error.bodyRequired");
106
+ } else if (isContentEmpty(message.body.body.content)) {
107
+ errors.push("editor.validation.error.bodyEmpty");
108
+ } else {
109
+ // Validate body text length (use markdown output to match what Meta receives)
110
+ const bodyText = convertToWhatsappMarkdown(message.body.body.content);
111
+ if (bodyText.length > WA_BODY_MAX_LENGTH) {
112
+ errors.push("editor.validation.error.bodyTooLong");
113
+ }
114
+ }
115
+
116
+ // Validate footer text length
117
+ if (message.body.footer?.enabled && message.body.footer.content) {
118
+ if (message.body.footer.content.length > WA_FOOTER_MAX_LENGTH) {
119
+ errors.push("editor.validation.error.footerTooLong");
120
+ }
121
+ }
122
+
123
+ // Validate buttons if enabled
124
+ if (
125
+ message.body.buttons?.enabled &&
126
+ message.body.buttons.buttons.length > 0
127
+ ) {
128
+ const buttons = message.body.buttons.buttons;
129
+
130
+ // Check max total buttons
131
+ if (buttons.length > 10) {
132
+ errors.push("editor.validation.error.maxButtons");
133
+ }
134
+
135
+ // Count button types
136
+ const linkButtons = buttons.filter((b) => b.type === "link").length;
137
+ const phoneButtons = buttons.filter((b) => b.type === "phone").length;
138
+ const unsubscribeButtons = buttons.filter(
139
+ (b) => b.type === "unsubscribe",
140
+ ).length;
141
+ const quickReplyButtons = buttons.filter(
142
+ (b) => b.type === "quick_reply",
143
+ ).length;
144
+
145
+ // Validate button type limits
146
+ if (linkButtons > 1) {
147
+ errors.push("editor.validation.error.maxLinkButtons");
148
+ }
149
+ if (phoneButtons > 1) {
150
+ errors.push("editor.validation.error.maxPhoneButtons");
151
+ }
152
+ if (unsubscribeButtons > 1) {
153
+ errors.push("editor.validation.error.maxUnsubscribeButtons");
154
+ }
155
+ if (quickReplyButtons > 3) {
156
+ errors.push("editor.validation.error.maxQuickReplyButtons");
157
+ }
158
+
159
+ // Validate button text length, phone numbers, and URLs
160
+ buttons.forEach((button) => {
161
+ // Validate button text
162
+ const buttonText = typeof button.text === "string" ? button.text : "";
163
+ if (!buttonText.trim()) {
164
+ errors.push(`editor.validation.error.buttonTextRequired`);
165
+ } else if (buttonText.length > WA_BUTTON_TEXT_MAX_LENGTH) {
166
+ errors.push(`editor.validation.error.buttonTextTooLong`);
167
+ }
168
+
169
+ if (button.type === "phone") {
170
+ const phone = button.phoneNumber?.trim();
171
+ if (!phone) {
172
+ errors.push(`editor.validation.error.phoneRequired`);
173
+ } else if (!isValidPhoneNumber(phone)) {
174
+ errors.push(`editor.validation.error.phoneInvalid`);
175
+ }
176
+ }
177
+ if (button.type === "link") {
178
+ const url = button.url?.trim();
179
+ if (!url) {
180
+ errors.push(`editor.validation.error.urlRequired`);
181
+ } else if (!isValidUrl(url)) {
182
+ errors.push(`editor.validation.error.urlInvalid`);
183
+ }
184
+ }
185
+ });
186
+ }
187
+
188
+ return {
189
+ valid: errors.length === 0,
190
+ errors,
191
+ };
192
+ }
193
+
194
+ export function isContentEmpty(content: Content): boolean {
195
+ if (content == null) {
196
+ return true;
197
+ }
198
+
199
+ if (typeof content === "string") {
200
+ return content === "";
201
+ }
202
+
203
+ if (Array.isArray(content)) {
204
+ return content.every((item) => isContentEmpty(item));
205
+ }
206
+
207
+ if (!content.content) {
208
+ return true;
209
+ }
210
+
211
+ return content.content.every((item) => !item.content);
212
+ }
213
+
214
+ export function generateWhatsappTextPreview(
215
+ message: PostEntity<WhatsappMessage> | undefined,
216
+ ) {
217
+ if (!message?.body.body.content) return "";
218
+ return generateWhatsappText(message.body.body.content);
219
+ }
220
+
221
+ export function generateWhatsappText(content: JSONContent | undefined) {
222
+ if (!content) return "";
223
+ return generateText(content, getTipTapExtensions({ preset: "whatsapp" }));
224
+ }
225
+ export function generateWhatsappHtml(content: JSONContent | undefined) {
226
+ if (!content) return "";
227
+ return generateHTML(content, getTipTapExtensions({ preset: "whatsapp" }));
228
+ }
229
+
230
+ /**
231
+ * Converts TipTap JSONContent to WhatsApp markdown format
232
+ * Supports: *bold*, _italic_, ~strikethrough~, ```code```
233
+ */
234
+ export function convertToWhatsappMarkdown(
235
+ content: Content | undefined,
236
+ ): string {
237
+ if (!content) return "";
238
+
239
+ function processNode(node: Content): string {
240
+ if (typeof node === "string") {
241
+ return node;
242
+ }
243
+
244
+ if (Array.isArray(node)) {
245
+ return node.map(processNode).join("");
246
+ }
247
+
248
+ if (!node || typeof node !== "object") {
249
+ return "";
250
+ }
251
+
252
+ let text = "";
253
+
254
+ // Process content recursively
255
+ if (node.content) {
256
+ text = node.content.map(processNode).join("");
257
+ }
258
+
259
+ // Handle text nodes with marks
260
+ if (node.type === "text" && node.text) {
261
+ text = node.text;
262
+
263
+ // Apply formatting based on marks
264
+ if (node.marks) {
265
+ for (const mark of node.marks) {
266
+ switch (mark.type) {
267
+ case "bold":
268
+ text = `*${text}*`;
269
+ break;
270
+ case "italic":
271
+ text = `_${text}_`;
272
+ break;
273
+ case "strike":
274
+ text = `~${text}~`;
275
+ break;
276
+ case "code":
277
+ text = `\`\`\`${text}\`\`\``;
278
+ break;
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ // Handle block-level elements
285
+ switch (node.type) {
286
+ case "paragraph":
287
+ return text + "\n";
288
+ case "mention":
289
+ return `{{${node.attrs?.label}}}`;
290
+ case "emoji": {
291
+ const emojiItem = shortcodeToEmoji(node.attrs?.name, gitHubEmojis);
292
+ return emojiItem?.emoji || `:${node.attrs?.name}:`;
293
+ }
294
+ case "hardBreak":
295
+ return "\n";
296
+ case "doc":
297
+ return text.trim();
298
+ default:
299
+ return text;
300
+ }
301
+ }
302
+
303
+ return processNode(content);
304
+ }
305
+
306
+ /**
307
+ * Calculates the translation completion percentage for a WhatsApp message
308
+ * Compares against a base message to determine which fields need translation
309
+ * @param message - The message to calculate percentage for
310
+ * @param baseMessage - The base language message to compare structure against
311
+ * @returns A number between 0-100 representing the completion percentage
312
+ */
313
+ export function calculateWhatsappTranslationPercentage(
314
+ message: PostEntity<WhatsappMessage> | MessageFromDb | undefined,
315
+ baseMessage: PostEntity<WhatsappMessage> | MessageFromDb | undefined,
316
+ ): number {
317
+ if (!message || message.render_type !== "whatsapp_v1") return 0;
318
+ if (!baseMessage || baseMessage.render_type !== "whatsapp_v1") return 0;
319
+
320
+ let totalFields = 0;
321
+ let filledFields = 0;
322
+
323
+ // Body content is always required
324
+ totalFields++;
325
+ if (message.body.body.content && !isContentEmpty(message.body.body.content)) {
326
+ filledFields++;
327
+ }
328
+
329
+ // Check header if it exists in base message
330
+ if (baseMessage.body.header.type === "text") {
331
+ totalFields++;
332
+ if (
333
+ message.body.header.type === "text" &&
334
+ message.body.header.content &&
335
+ !isContentEmpty(message.body.header.content)
336
+ ) {
337
+ filledFields++;
338
+ }
339
+ }
340
+
341
+ // Check media header if it exists in base message
342
+ if (["image", "video", "document"].includes(baseMessage.body.header.type)) {
343
+ totalFields++;
344
+ if (
345
+ message.body.header.type === baseMessage.body.header.type &&
346
+ "url" in message.body.header &&
347
+ message.body.header.url?.trim()
348
+ ) {
349
+ filledFields++;
350
+ }
351
+ }
352
+
353
+ // Check footer if enabled in base message
354
+ if (baseMessage.body.footer.enabled) {
355
+ totalFields++;
356
+ if (
357
+ message.body.footer.enabled &&
358
+ message.body.footer.content &&
359
+ message.body.footer.content.trim() !== ""
360
+ ) {
361
+ filledFields++;
362
+ }
363
+ }
364
+
365
+ // Check buttons if they exist in base message
366
+ if (
367
+ baseMessage.body.buttons?.buttons &&
368
+ baseMessage.body.buttons.buttons.length > 0
369
+ ) {
370
+ for (let i = 0; i < baseMessage.body.buttons.buttons.length; i++) {
371
+ const baseButton = baseMessage.body.buttons.buttons[i];
372
+ const messageButton = message.body.buttons?.buttons?.[i];
373
+
374
+ // Button text
375
+ totalFields++;
376
+ if (messageButton?.text && messageButton.text.trim() !== "") {
377
+ filledFields++;
378
+ }
379
+
380
+ // URL for link buttons
381
+ if (baseButton.type === "link") {
382
+ totalFields++;
383
+ if (
384
+ messageButton?.type === "link" &&
385
+ messageButton.url &&
386
+ messageButton.url.trim() !== ""
387
+ ) {
388
+ filledFields++;
389
+ }
390
+ }
391
+
392
+ // Phone number for phone buttons
393
+ if (baseButton.type === "phone") {
394
+ totalFields++;
395
+ if (
396
+ messageButton?.type === "phone" &&
397
+ messageButton.phoneNumber &&
398
+ messageButton.phoneNumber.trim() !== ""
399
+ ) {
400
+ filledFields++;
401
+ }
402
+ }
403
+ }
404
+ }
405
+
406
+ if (totalFields === 0) return 0;
407
+ return Math.round((filledFields / totalFields) * 100);
408
+ }
@@ -0,0 +1,49 @@
1
+ // Common validation functions
2
+ export const isEmail = (email: string): boolean => {
3
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4
+ return emailRegex.test(email);
5
+ };
6
+
7
+ export const isRequired = (value: unknown): boolean => {
8
+ if (value === null || value === undefined) return false;
9
+ if (typeof value === "string") return value.trim().length > 0;
10
+ if (Array.isArray(value)) return value.length > 0;
11
+ return true;
12
+ };
13
+
14
+ export const minLength = (value: string, min: number): boolean => {
15
+ return value.length >= min;
16
+ };
17
+
18
+ export const maxLength = (value: string, max: number): boolean => {
19
+ return value.length <= max;
20
+ };
21
+
22
+ export const isNumeric = (value: string): boolean => {
23
+ return !isNaN(Number(value)) && !isNaN(parseFloat(value));
24
+ };
25
+
26
+ export const isPositiveNumber = (value: number): boolean => {
27
+ return value > 0;
28
+ };
29
+
30
+ export interface ValidationResult {
31
+ isValid: boolean;
32
+ errors: string[];
33
+ }
34
+
35
+ // WhatsApp Button validations
36
+ export const isValidButtonText = (text: string): boolean => {
37
+ return text.length > 0 && text.length <= 20;
38
+ };
39
+
40
+ export const VALID_PHONE_NUMBER_REGEX = /^\+?[1-9]\d{8,14}$/;
41
+ export const isValidPhoneNumber = (phoneNumber: string): boolean => {
42
+ return VALID_PHONE_NUMBER_REGEX.test(phoneNumber);
43
+ };
44
+
45
+ export const VALID_URL_REGEX =
46
+ /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#={}]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//={}]*)$/;
47
+ export const isValidUrl = (url: string): boolean => {
48
+ return VALID_URL_REGEX.test(url);
49
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": ["@tsconfig/node22/tsconfig.json"],
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "sourceMap": true,
7
+ "lib": ["ESNext", "DOM"]
8
+ },
9
+ "references": []
10
+ }