@dev.smartpricing/message-composer-utils 4.3.2-fix.1 → 4.5.1-next.0

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 (63) hide show
  1. package/dist/schemas/brand.js +2 -0
  2. package/dist/schemas/brand.js.map +1 -1
  3. package/dist/schemas/dynamic-values.js +6 -1
  4. package/dist/schemas/dynamic-values.js.map +1 -1
  5. package/dist/schemas/index.js +1 -0
  6. package/dist/schemas/index.js.map +1 -1
  7. package/dist/schemas/message-compilation.js +2 -0
  8. package/dist/schemas/message-compilation.js.map +1 -1
  9. package/dist/schemas/message-group.js +33 -6
  10. package/dist/schemas/message-group.js.map +1 -1
  11. package/dist/schemas/message-group.test.js +14 -0
  12. package/dist/schemas/message-group.test.js.map +1 -0
  13. package/dist/schemas/meta-integration.js +20 -0
  14. package/dist/schemas/meta-integration.js.map +1 -1
  15. package/dist/schemas/meta-integration.test.js +18 -0
  16. package/dist/schemas/meta-integration.test.js.map +1 -0
  17. package/dist/schemas/onboarding.js +3 -1
  18. package/dist/schemas/onboarding.js.map +1 -1
  19. package/dist/schemas/organization.js +31 -0
  20. package/dist/schemas/organization.js.map +1 -1
  21. package/dist/schemas/organization.test.js +86 -0
  22. package/dist/schemas/organization.test.js.map +1 -0
  23. package/dist/schemas/pagination.js +40 -0
  24. package/dist/schemas/pagination.js.map +1 -0
  25. package/dist/schemas/pagination.test.js +82 -0
  26. package/dist/schemas/pagination.test.js.map +1 -0
  27. package/dist/schemas/tracking.js +2 -0
  28. package/dist/schemas/tracking.js.map +1 -1
  29. package/dist/types/app.js.map +1 -1
  30. package/dist/types/index.js +2 -0
  31. package/dist/types/index.js.map +1 -1
  32. package/dist/types/message.js +24 -0
  33. package/dist/types/message.js.map +1 -1
  34. package/dist/types/organization.js +2 -0
  35. package/dist/types/organization.js.map +1 -0
  36. package/dist/types/pagination.js +2 -0
  37. package/dist/types/pagination.js.map +1 -0
  38. package/dist/utils/templates/whatsapp-compiler.test.js +241 -0
  39. package/dist/utils/templates/whatsapp-compiler.test.js.map +1 -0
  40. package/package.json +1 -1
  41. package/src/schemas/brand.ts +2 -0
  42. package/src/schemas/dynamic-values.ts +9 -2
  43. package/src/schemas/index.ts +1 -0
  44. package/src/schemas/message-compilation.ts +2 -0
  45. package/src/schemas/message-group.test.ts +20 -0
  46. package/src/schemas/message-group.ts +45 -6
  47. package/src/schemas/meta-integration.test.ts +20 -0
  48. package/src/schemas/meta-integration.ts +30 -1
  49. package/src/schemas/onboarding.ts +3 -0
  50. package/src/schemas/organization.test.ts +143 -0
  51. package/src/schemas/organization.ts +39 -0
  52. package/src/schemas/pagination.test.ts +101 -0
  53. package/src/schemas/pagination.ts +49 -0
  54. package/src/schemas/tracking.ts +2 -0
  55. package/src/types/api/apiMessages.ts +5 -0
  56. package/src/types/app.ts +9 -23
  57. package/src/types/index.ts +2 -0
  58. package/src/types/kafka-events.ts +7 -1
  59. package/src/types/message.ts +98 -0
  60. package/src/types/organization.ts +19 -0
  61. package/src/types/pagination.ts +36 -0
  62. package/src/types/submission-job.ts +5 -0
  63. package/src/utils/templates/whatsapp-compiler.test.ts +282 -0
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ organizationBodySchema,
4
+ organizationFilterQuerySchema,
5
+ organizationLookupQuerySchema,
6
+ } from "./organization.js";
7
+ import { listBrandsQuerySchema } from "./brand.js";
8
+ import { dynamicValuesQuerySchema } from "./dynamic-values.js";
9
+ import { trackingBodySchema } from "./tracking.js";
10
+ import {
11
+ createMessageGroupBodySchema,
12
+ duplicateMessageGroupBodySchema,
13
+ listMessageGroupsQuerySchema,
14
+ } from "./message-group.js";
15
+
16
+ const ORGANIZATION = "3f0c1a52-8f0e-4c53-9d58-0f6f6f1f2a77";
17
+ const NOT_UUIDS = ["", "42", "hotel-uno", `${ORGANIZATION}x`, 42, null];
18
+
19
+ describe("organizationFilterQuerySchema", () => {
20
+ it("accepts a UUID", () => {
21
+ expect(
22
+ organizationFilterQuerySchema.parse({ organization_id: ORGANIZATION }),
23
+ ).toEqual({ organization_id: ORGANIZATION });
24
+ });
25
+
26
+ it("is optional: a list without it spans the Reachable Organizations", () => {
27
+ expect(organizationFilterQuerySchema.parse({})).toEqual({});
28
+ });
29
+
30
+ it.each(NOT_UUIDS)("rejects %j", (organization_id) => {
31
+ expect(
32
+ organizationFilterQuerySchema.safeParse({ organization_id }).success,
33
+ ).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe("organizationBodySchema", () => {
38
+ it("accepts a UUID", () => {
39
+ expect(
40
+ organizationBodySchema.parse({ organization_id: ORGANIZATION }),
41
+ ).toEqual({ organization_id: ORGANIZATION });
42
+ });
43
+
44
+ it.each(NOT_UUIDS)("rejects %j", (organization_id) => {
45
+ expect(organizationBodySchema.safeParse({ organization_id }).success).toBe(
46
+ false,
47
+ );
48
+ });
49
+ });
50
+
51
+ describe("Message Group schemas carry the Organization", () => {
52
+ it("the list query keeps a UUID filter and rejects anything else", () => {
53
+ expect(
54
+ listMessageGroupsQuerySchema.parse({ organization_id: ORGANIZATION })
55
+ .organization_id,
56
+ ).toBe(ORGANIZATION);
57
+ expect(
58
+ listMessageGroupsQuerySchema.safeParse({ organization_id: "hotel-uno" })
59
+ .success,
60
+ ).toBe(false);
61
+ });
62
+
63
+ it("the create body keeps a UUID Organization and rejects anything else", () => {
64
+ const body = { name: "Welcome", category: "utility", product: "chat" };
65
+ expect(
66
+ createMessageGroupBodySchema.parse({
67
+ ...body,
68
+ organization_id: ORGANIZATION,
69
+ }).organization_id,
70
+ ).toBe(ORGANIZATION);
71
+ expect(
72
+ createMessageGroupBodySchema.safeParse({
73
+ ...body,
74
+ organization_id: "hotel-uno",
75
+ }).success,
76
+ ).toBe(false);
77
+ });
78
+
79
+ it("the duplicate body keeps a UUID Organization and rejects anything else", () => {
80
+ expect(
81
+ duplicateMessageGroupBodySchema.parse({ organization_id: ORGANIZATION }),
82
+ ).toEqual({ organization_id: ORGANIZATION });
83
+ expect(
84
+ duplicateMessageGroupBodySchema.safeParse({
85
+ organization_id: "hotel-uno",
86
+ }).success,
87
+ ).toBe(false);
88
+ });
89
+ });
90
+
91
+ describe("organizationLookupQuerySchema", () => {
92
+ it("accepts a UUID", () => {
93
+ expect(
94
+ organizationLookupQuerySchema.parse({ organization_id: ORGANIZATION }),
95
+ ).toEqual({ organization_id: ORGANIZATION });
96
+ });
97
+
98
+ it.each(NOT_UUIDS)("rejects %j", (organization_id) => {
99
+ expect(
100
+ organizationLookupQuerySchema.safeParse({ organization_id }).success,
101
+ ).toBe(false);
102
+ });
103
+ });
104
+
105
+ describe("Brand, dynamic values and tracking schemas carry the Organization", () => {
106
+ it("the Brand list query keeps a UUID filter and rejects anything else", () => {
107
+ expect(
108
+ listBrandsQuerySchema.parse({ organization_id: ORGANIZATION })
109
+ .organization_id,
110
+ ).toBe(ORGANIZATION);
111
+ expect(
112
+ listBrandsQuerySchema.safeParse({ organization_id: "hotel-uno" }).success,
113
+ ).toBe(false);
114
+ });
115
+
116
+ it("the dynamic values query keeps a UUID Organization and rejects anything else", () => {
117
+ const query = { product: "connect" };
118
+ expect(
119
+ dynamicValuesQuerySchema.parse({
120
+ ...query,
121
+ organization_id: ORGANIZATION,
122
+ }).organization_id,
123
+ ).toBe(ORGANIZATION);
124
+ expect(
125
+ dynamicValuesQuerySchema.safeParse({
126
+ ...query,
127
+ organization_id: "hotel-uno",
128
+ }).success,
129
+ ).toBe(false);
130
+ });
131
+
132
+ it("the tracking body keeps a UUID Organization and rejects anything else", () => {
133
+ const body = { eventName: "brand_created" };
134
+ expect(
135
+ trackingBodySchema.parse({ ...body, organization_id: ORGANIZATION })
136
+ .organization_id,
137
+ ).toBe(ORGANIZATION);
138
+ expect(
139
+ trackingBodySchema.safeParse({ ...body, organization_id: "hotel-uno" })
140
+ .success,
141
+ ).toBe(false);
142
+ });
143
+ });
@@ -1,4 +1,43 @@
1
1
  import { z } from "zod";
2
+ import { ReachableOrganization } from "../types/index.js";
3
+
4
+ export const reachableOrganizationSchema = z.object({
5
+ id: z.string(),
6
+ name: z.string(),
7
+ }) satisfies z.ZodType<ReachableOrganization>;
8
+
9
+ /** GET /organizations: the user's Reachable Organizations (ADR 0014). */
10
+ export const reachableOrganizationListResponseSchema = z.array(
11
+ reachableOrganizationSchema,
12
+ );
13
+
14
+ /**
15
+ * Optional Organization filter of a list route (ADR 0014). Without it the
16
+ * list spans the user's Reachable Organizations; a value that is not
17
+ * reachable yields no data.
18
+ */
19
+ export const organizationFilterQuerySchema = z.object({
20
+ organization_id: z.uuid().optional(),
21
+ });
22
+
23
+ /**
24
+ * Organization a resource is created in (ADR 0014). Optional only while the
25
+ * deprecated `sm-organization-id` header is accepted in its place: a request
26
+ * with neither is rejected by the route.
27
+ */
28
+ export const organizationBodySchema = z.object({
29
+ organization_id: z.uuid().optional(),
30
+ });
31
+
32
+ /**
33
+ * Organization of a lookup route: one that reads Organization-scoped data
34
+ * without listing or creating a resource (ADR 0014). Optional only while the
35
+ * deprecated `sm-organization-id` header is accepted in its place: a request
36
+ * with neither is rejected by the route.
37
+ */
38
+ export const organizationLookupQuerySchema = z.object({
39
+ organization_id: z.uuid().optional(),
40
+ });
2
41
 
3
42
  /**
4
43
  * GET /organization/data passes through OrganizationData from Smartconnect —
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { z } from "zod";
3
+ import {
4
+ paginationQuerySchema,
5
+ paginatedResponseSchema,
6
+ } from "./pagination.js";
7
+
8
+ describe("paginationQuerySchema", () => {
9
+ it("defaults to page 1 and 25 per page", () => {
10
+ expect(paginationQuerySchema.parse({})).toEqual({ page: 1, per_page: 25 });
11
+ });
12
+
13
+ it("coerces query-string numbers", () => {
14
+ expect(paginationQuerySchema.parse({ page: "3", per_page: "50" })).toEqual({
15
+ page: 3,
16
+ per_page: 50,
17
+ });
18
+ });
19
+
20
+ it("accepts the bounds 1 and 100 for per_page", () => {
21
+ expect(paginationQuerySchema.parse({ per_page: "1" }).per_page).toBe(1);
22
+ expect(paginationQuerySchema.parse({ per_page: "100" }).per_page).toBe(100);
23
+ });
24
+
25
+ it("rejects page 0, per_page 0 and per_page 101", () => {
26
+ expect(paginationQuerySchema.safeParse({ page: "0" }).success).toBe(false);
27
+ expect(paginationQuerySchema.safeParse({ per_page: "0" }).success).toBe(
28
+ false,
29
+ );
30
+ expect(paginationQuerySchema.safeParse({ per_page: "101" }).success).toBe(
31
+ false,
32
+ );
33
+ });
34
+
35
+ it("rejects non-integer values", () => {
36
+ expect(paginationQuerySchema.safeParse({ page: "1.5" }).success).toBe(
37
+ false,
38
+ );
39
+ expect(paginationQuerySchema.safeParse({ page: "abc" }).success).toBe(
40
+ false,
41
+ );
42
+ });
43
+ });
44
+
45
+ describe("paginatedResponseSchema", () => {
46
+ const schema = paginatedResponseSchema(z.object({ id: z.string() }));
47
+
48
+ it("accepts the Laravel resource-collection envelope", () => {
49
+ const envelope = {
50
+ data: [{ id: "a" }],
51
+ links: { first: "/x?page=1", last: "/x?page=1", prev: null, next: null },
52
+ meta: {
53
+ current_page: 1,
54
+ from: 1,
55
+ last_page: 1,
56
+ path: "/x",
57
+ per_page: 25,
58
+ to: 1,
59
+ total: 1,
60
+ },
61
+ };
62
+ expect(schema.parse(envelope)).toEqual(envelope);
63
+ });
64
+
65
+ it("allows null from/to on an empty page and has no meta.links", () => {
66
+ const parsed = schema.parse({
67
+ data: [],
68
+ links: { first: "/x?page=1", last: "/x?page=1", prev: null, next: null },
69
+ meta: {
70
+ current_page: 1,
71
+ from: null,
72
+ last_page: 1,
73
+ path: "/x",
74
+ per_page: 25,
75
+ to: null,
76
+ total: 0,
77
+ links: [{ url: null, label: "x", active: false }],
78
+ },
79
+ });
80
+ expect(parsed.meta.from).toBeNull();
81
+ expect(parsed.meta.to).toBeNull();
82
+ expect("links" in parsed.meta).toBe(false);
83
+ });
84
+
85
+ it("validates items with the given schema", () => {
86
+ const result = schema.safeParse({
87
+ data: [{ id: 1 }],
88
+ links: { first: "/x?page=1", last: "/x?page=1", prev: null, next: null },
89
+ meta: {
90
+ current_page: 1,
91
+ from: 1,
92
+ last_page: 1,
93
+ path: "/x",
94
+ per_page: 25,
95
+ to: 1,
96
+ total: 1,
97
+ },
98
+ });
99
+ expect(result.success).toBe(false);
100
+ });
101
+ });
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import type {
3
+ PaginationLinks,
4
+ PaginationMeta,
5
+ PaginationQuery,
6
+ } from "../types/index.js";
7
+
8
+ export const PAGINATION_DEFAULT_PER_PAGE = 25;
9
+ export const PAGINATION_MAX_PER_PAGE = 100;
10
+
11
+ /** Laravel-style offset pagination query params (ADR 0012). */
12
+ export const paginationQuerySchema = z.object({
13
+ page: z.coerce.number().int().min(1).default(1),
14
+ per_page: z.coerce
15
+ .number()
16
+ .int()
17
+ .min(1)
18
+ .max(PAGINATION_MAX_PER_PAGE)
19
+ .default(PAGINATION_DEFAULT_PER_PAGE),
20
+ }) satisfies z.ZodType<PaginationQuery, unknown>;
21
+
22
+ export const paginationLinksSchema = z.object({
23
+ first: z.string(),
24
+ last: z.string(),
25
+ prev: z.string().nullable(),
26
+ next: z.string().nullable(),
27
+ }) satisfies z.ZodType<unknown, PaginationLinks>;
28
+
29
+ export const paginationMetaSchema = z.object({
30
+ current_page: z.number().int(),
31
+ from: z.number().int().nullable(),
32
+ last_page: z.number().int(),
33
+ path: z.string(),
34
+ per_page: z.number().int(),
35
+ to: z.number().int().nullable(),
36
+ total: z.number().int(),
37
+ }) satisfies z.ZodType<unknown, PaginationMeta>;
38
+
39
+ /**
40
+ * Laravel API resource-collection envelope around `item`:
41
+ * `{ data, links: { first, last, prev, next }, meta: { … } }`, no `meta.links`.
42
+ */
43
+ export function paginatedResponseSchema<T extends z.ZodType>(item: T) {
44
+ return z.object({
45
+ data: z.array(item),
46
+ links: paginationLinksSchema,
47
+ meta: paginationMetaSchema,
48
+ });
49
+ }
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
+ import { organizationBodySchema } from "./organization.js";
2
3
 
3
4
  export const trackingBodySchema = z.object({
4
5
  eventName: z.string().min(1, "eventName is required"),
5
6
  properties: z.record(z.string(), z.unknown()).optional(),
7
+ ...organizationBodySchema.shape,
6
8
  });
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  MessageFromDb,
3
3
  MessageGroupFromDb,
4
+ MessageGroupProduct,
4
5
  MessageGroupVisibility,
5
6
  MessageStatus,
6
7
  RenderType,
@@ -12,8 +13,12 @@ export type MessageGroupWithMessages = MessageGroupFromDb & {
12
13
  };
13
14
 
14
15
  export interface DuplicateMessageGroupBody {
16
+ /** Organization the copy is created in (ADR 0014). */
17
+ organization_id?: string;
15
18
  name?: string;
16
19
  visibility?: MessageGroupVisibility;
20
+ /** Defaults to the source group's Product. */
21
+ product?: MessageGroupProduct;
17
22
  brand_id?: string | null;
18
23
  }
19
24
 
package/src/types/app.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { MessageGroupProduct } from "./message.js";
2
+
1
3
  export const errorCodes = {
2
4
  INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR",
3
5
  UNAUTHORIZED: "UNAUTHORIZED",
@@ -32,37 +34,21 @@ export interface AppError {
32
34
  }
33
35
 
34
36
  export interface AppContextConfig {
35
- /** @deprecated No longer used — auth is cookie-based via platform layer */
36
- authenticationToken?: string;
37
- product?: "connect" | "chat" | "pms" | "pricing" | "browser";
38
- strategy?: "embed" | "native";
37
+ /** The Product the host app embeds the composer in (ADR 0011). Required. */
38
+ product: MessageGroupProduct;
39
39
  channels?: Array<"email" | "whatsapp">;
40
40
  languages?: string[];
41
41
  mainLanguage?: string;
42
+ /**
43
+ * The WABA the Host works on. Only a preselection when the user picks the
44
+ * WABA of a Message Group, valid if it is one of the Organization's
45
+ * (ADR 0015).
46
+ */
42
47
  metaWabaId?: string;
43
- metaAccessToken?: string;
44
48
  organization_id?: string;
45
49
  default_tags?: string[];
46
50
  }
47
51
 
48
- export interface AppContextState extends AppContextConfig {
49
- initializationSource:
50
- | "query"
51
- | "postMessage"
52
- | "cookies"
53
- | "injection"
54
- | null;
55
- }
56
-
57
- export interface ContextSource {
58
- name: string;
59
- priority: number;
60
- extract: () => Promise<Partial<AppContextConfig>>;
61
- watch?: (
62
- callback: (context: Partial<AppContextConfig>) => void,
63
- ) => () => void;
64
- }
65
-
66
52
  export type ParentNotificationType =
67
53
  | "TEMPLATE_SAVED_DRAFT"
68
54
  | "TEMPLATE_SAVED"
@@ -13,3 +13,5 @@ export * from "./submission-job.js";
13
13
  export * from "./kafka-events.js";
14
14
  export * from "./dynamic-values.js";
15
15
  export * from "./brand.js";
16
+ export * from "./organization.js";
17
+ export * from "./pagination.js";
@@ -1,10 +1,16 @@
1
- import type { MessageStatus, MessageGroupCategory } from "./message.js";
1
+ import type {
2
+ MessageStatus,
3
+ MessageGroupCategory,
4
+ MessageGroupProduct,
5
+ } from "./message.js";
2
6
 
3
7
  export interface KafkaEventBase {
4
8
  event_id: string;
5
9
  event_type: string;
6
10
  timestamp: string;
7
11
  message_group_id: string;
12
+ /** Product of the Message Group the event is about (ADR 0011). */
13
+ product: MessageGroupProduct;
8
14
  organization_id?: string | null;
9
15
  user_id: string;
10
16
  }
@@ -8,6 +8,7 @@ import type {
8
8
  ButtonBlockV1,
9
9
  } from "./whatsappblocks.v1.js";
10
10
  import type { EmailBlock, EmailGlobalSettings } from "./emailblocks.v1.js";
11
+ import type { PaginationQuery, SortOrder } from "./pagination.js";
11
12
  import {
12
13
  brandDefaultEmailSettingsSchema,
13
14
  brandDefaultButtonSettingsSchema,
@@ -79,13 +80,67 @@ export type MessageGroupCategory = (typeof MESSAGE_GROUP_CATEGORIES)[number];
79
80
  export const MESSAGE_GROUP_VISIBILITY = ["public", "internal"] as const;
80
81
  export type MessageGroupVisibility = (typeof MESSAGE_GROUP_VISIBILITY)[number];
81
82
 
83
+ /** `visibility` filter of the list route; `internal` and `all` are admin-only. */
84
+ export const MESSAGE_GROUP_LIST_VISIBILITY = [
85
+ ...MESSAGE_GROUP_VISIBILITY,
86
+ "all",
87
+ ] as const;
88
+ export type MessageGroupListVisibility =
89
+ (typeof MESSAGE_GROUP_LIST_VISIBILITY)[number];
90
+
91
+ /** Sortable columns of the list route (ADR 0012); aggregates are not sortable. */
92
+ export const MESSAGE_GROUP_LIST_SORT = [
93
+ "name",
94
+ "category",
95
+ "created_at",
96
+ "updated_at",
97
+ ] as const;
98
+ export type MessageGroupListSort = (typeof MESSAGE_GROUP_LIST_SORT)[number];
99
+
100
+ /**
101
+ * Parsed query of `GET /message-groups` (ADR 0012): what the route handler
102
+ * receives after defaults. The layer sends any subset of these as query params.
103
+ */
104
+ export interface ListMessageGroupsQuery extends PaginationQuery {
105
+ /** Narrows the list to one Reachable Organization (ADR 0014). */
106
+ organization_id?: string;
107
+ search?: string;
108
+ category?: MessageGroupCategory;
109
+ status?: MessageStatus;
110
+ product?: MessageGroupProduct;
111
+ visibility: MessageGroupListVisibility;
112
+ render_type?: RenderType;
113
+ /** Narrows the list to the Message Groups on one WABA (ADR 0015). */
114
+ waba_id?: string;
115
+ sort: MessageGroupListSort;
116
+ order: SortOrder;
117
+ }
118
+
119
+ /**
120
+ * The Smartness application a Message Group is authored for (ADR 0011).
121
+ * Anything outside this list is not a Product and is rejected on write.
122
+ */
123
+ export const MESSAGE_GROUP_PRODUCTS = [
124
+ "connect",
125
+ "chat",
126
+ "pms",
127
+ "pricing",
128
+ ] as const;
129
+ export type MessageGroupProduct = (typeof MESSAGE_GROUP_PRODUCTS)[number];
130
+
82
131
  export interface MessageGroup extends DbEntity {
83
132
  id: string;
84
133
  name: string;
85
134
  category: MessageGroupCategory;
135
+ product: MessageGroupProduct;
86
136
  visibility?: MessageGroupVisibility | null;
87
137
  brand_id?: string | null;
88
138
  organization_id?: string | null;
139
+ /**
140
+ * The WABA the Message Group is submitted to: set at its first submission
141
+ * and fixed from then on, absent until then (ADR 0015).
142
+ */
143
+ waba_id?: string | null;
89
144
  user_id: string;
90
145
  }
91
146
 
@@ -154,6 +209,8 @@ export const brandCascadeDataSchema = z.object({
154
209
  * Request body for previewing a message with inline data (no DB lookups).
155
210
  */
156
211
  export const previewMessageRequestSchema = z.object({
212
+ /** Organization the preview is compiled for (ADR 0014). */
213
+ organization_id: z.uuid().optional(),
157
214
  message: z
158
215
  .object({
159
216
  render_type: z.literal("email_v1"),
@@ -203,6 +260,9 @@ export interface CompileAndSendRequest extends CompileMessageRequest {
203
260
  * Request body for testing email rendering
204
261
  */
205
262
  export interface TestEmailRequest {
263
+ /** Organization the test email is sent for (ADR 0014) */
264
+ organization_id?: string;
265
+
206
266
  /** Email message configuration */
207
267
  message: PostEntity<EmailMessage>;
208
268
 
@@ -311,6 +371,40 @@ interface WhatsAppTemplateMessage extends WhatsAppMessageBase {
311
371
  */
312
372
  export type WhatsAppSendPayload = WhatsAppTemplateMessage;
313
373
 
374
+ /**
375
+ * Sendable Message (WhatsApp): the content object a host hands to its own
376
+ * send pipeline. `meta.data` is the compiled Meta template object (name,
377
+ * language, components) and `meta.template` the translated Meta template the
378
+ * bubble is rendered from.
379
+ */
380
+ export interface WhatsappSendableMessage {
381
+ type: "WhatsappTemplateMessage";
382
+ text: string;
383
+ meta: {
384
+ data: WhatsAppTemplateMessage["template"];
385
+ template: MetaTemplate;
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Email Sendable Message: the content object the email compile-and-send path
391
+ * publishes (Smartchat `HtmlMessage`), returned by `/compile` as `message`.
392
+ */
393
+ export interface EmailSendableMessage {
394
+ type: "HtmlMessage";
395
+ skipTranslation: true;
396
+ /** The compiled HTML, as the message API expects it in both fields. */
397
+ text: string;
398
+ rawHtml: string;
399
+ subject: string;
400
+ }
401
+
402
+ /**
403
+ * Sendable Message: what `/compile` returns as `message` and what
404
+ * compile-and-send publishes, one member per channel.
405
+ */
406
+ export type SendableMessage = WhatsappSendableMessage | EmailSendableMessage;
407
+
314
408
  /**
315
409
  * Response for compile endpoint (WhatsApp)
316
410
  */
@@ -319,6 +413,8 @@ export interface CompileWhatsAppResponse {
319
413
  language: string;
320
414
  template_name: string;
321
415
  payload: Omit<WhatsAppTemplateMessage, "to">;
416
+ /** The Sendable Message, ready for the host's send pipeline. */
417
+ message: SendableMessage;
322
418
  parameters_used: string[];
323
419
  missingParameters?: string[];
324
420
  }
@@ -332,6 +428,8 @@ export interface CompileEmailResponse {
332
428
  subject: string;
333
429
  html: string;
334
430
  plaintext?: string;
431
+ /** The Sendable Message, ready for the host's send pipeline. */
432
+ message: EmailSendableMessage;
335
433
  parameters_used: string[];
336
434
  missingParameters?: string[];
337
435
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * An Organization the logged-in user has a role on, directly or through one of
3
+ * its properties (Reachable Organization, ADR 0014).
4
+ */
5
+ export interface ReachableOrganization {
6
+ id: string;
7
+ name: string;
8
+ }
9
+
10
+ /**
11
+ * A WABA of an Organization. An Organization has none, one or several
12
+ * (ADR 0015). `phone_numbers` are the numbers connected to it, possibly none:
13
+ * a WABA is a valid submission target before its first number.
14
+ */
15
+ export interface OrganizationWaba {
16
+ id: string;
17
+ name: string;
18
+ phone_numbers: string[];
19
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Laravel API resource-collection envelope for paginated lists (ADR 0012).
3
+ * Links are relative (path + query, no host); `meta.links` is omitted.
4
+ */
5
+ export interface PaginationLinks {
6
+ first: string;
7
+ last: string;
8
+ prev: string | null;
9
+ next: string | null;
10
+ }
11
+
12
+ export interface PaginationMeta {
13
+ current_page: number;
14
+ /** 1-based index of the first item on this page; null on an empty page. */
15
+ from: number | null;
16
+ last_page: number;
17
+ path: string;
18
+ per_page: number;
19
+ /** 1-based index of the last item on this page; null on an empty page. */
20
+ to: number | null;
21
+ total: number;
22
+ }
23
+
24
+ export interface PaginatedResponse<T> {
25
+ data: T[];
26
+ links: PaginationLinks;
27
+ meta: PaginationMeta;
28
+ }
29
+
30
+ export interface PaginationQuery {
31
+ page: number;
32
+ per_page: number;
33
+ }
34
+
35
+ export const SORT_ORDER = ["asc", "desc"] as const;
36
+ export type SortOrder = (typeof SORT_ORDER)[number];
@@ -29,3 +29,8 @@ export interface SubmissionJobResponse {
29
29
  status: "queued";
30
30
  message: string;
31
31
  }
32
+
33
+ /** Body of the public submit routes: the WABA to submit to (ADR 0015). */
34
+ export interface SubmitToWabaBody {
35
+ waba_id?: string;
36
+ }