Skip to content

Commit 11f15d2

Browse files
committed
fix(capi): share value and currency rules and reject oversized values
1 parent 8ee6867 commit 11f15d2

4 files changed

Lines changed: 82 additions & 39 deletions

File tree

packages/business/src/meta-conversions/schema.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,15 @@ import {
1414
defaultMetaCapiActionSource,
1515
metaCapiActionSourceSchema,
1616
metaCapiContentTypeSchema,
17+
metaCapiCurrencySchema,
1718
metaCapiEventNameSchema,
19+
metaCapiValueSchema,
1820
} from "@chatbotx.io/utils/meta-capi"
1921
import { z } from "zod"
2022
import { splitContentIds } from "./event-input"
2123

2224
const capiDatasetIdSchema = z.string().trim().regex(/^\d+$/)
2325
const capiAccessTokenSchema = z.string().trim().min(1)
24-
// Value must already be canonical `\d+(\.\d+)?` after `trim()` — no
25-
// destructive normalization (no comma/dot stripping: "12,50" is ambiguous
26-
// between 12.50 and 1250). An unresolved `{{...}}` template that leaked past
27-
// the worker's variable resolution is rejected here, not persisted.
28-
const capiEventValueSchema = z
29-
.string()
30-
.trim()
31-
.regex(/^\d+(\.\d+)?$/, "Value must be a plain number such as 19.99")
32-
const capiEventCurrencySchema = z
33-
.string()
34-
.trim()
35-
.toUpperCase()
36-
.pipe(
37-
z
38-
.string()
39-
.regex(
40-
/^[A-Z]{3}$/,
41-
"Currency must be a 3-letter ISO 4217 code such as USD",
42-
),
43-
)
44-
4526
/**
4627
* Business-boundary input for enqueuing a Meta CAPI event, shared by the
4728
* flow-step handler and the trigger executor — both resolve any
@@ -69,8 +50,8 @@ export const enqueueEventInput = withMetaCapiEventRefinements(
6950
splitContentIds,
7051
z.array(z.string().min(1)).min(1).optional(),
7152
),
72-
value: capiEventValueSchema.optional(),
73-
currency: capiEventCurrencySchema.optional(),
53+
value: metaCapiValueSchema.optional(),
54+
currency: metaCapiCurrencySchema.optional(),
7455
contentCategory: z.string().trim().min(1).max(200).optional(),
7556
contentName: z.string().trim().min(1).max(200).optional(),
7657
occurredAt: z.date().optional(),

packages/flow-config/src/steps/send-meta-capi-event.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
metaCapiActionSourceSchema,
88
metaCapiBusinessMessagingEventNames,
99
metaCapiContentTypeSchema,
10+
metaCapiCurrencySchema,
1011
metaCapiEventNameSchema,
12+
metaCapiValueSchema,
1113
} from "@chatbotx.io/utils/meta-capi"
1214
import { containsVariablePlaceholder } from "@chatbotx.io/utils/variables"
1315
import { z } from "zod"
@@ -50,17 +52,6 @@ export const templateOrStatic = <TOutput>(
5052
return z.NEVER
5153
})
5254

53-
const metaCapiValueStaticSchema = z
54-
.string()
55-
.trim()
56-
.regex(/^\d+(\.\d+)?$/)
57-
58-
const metaCapiCurrencyStaticSchema = z
59-
.string()
60-
.trim()
61-
.toUpperCase()
62-
.pipe(z.string().regex(/^[A-Z]{3}$/))
63-
6455
const metaCapiContentIdsStaticSchema = z.string().trim().min(1)
6556

6657
/**
@@ -72,10 +63,10 @@ const optionalTemplateOrStatic = <TOutput>(
7263
staticSchema: z.ZodType<TOutput, string>,
7364
) => z.preprocess(blankToUndefined, templateOrStatic(staticSchema).optional())
7465

75-
const metaCapiValueSchema = optionalTemplateOrStatic(metaCapiValueStaticSchema)
66+
const metaCapiValueFieldSchema = optionalTemplateOrStatic(metaCapiValueSchema)
7667

77-
const metaCapiCurrencySchema = optionalTemplateOrStatic(
78-
metaCapiCurrencyStaticSchema,
68+
const metaCapiCurrencyFieldSchema = optionalTemplateOrStatic(
69+
metaCapiCurrencySchema,
7970
)
8071

8172
// Comma-separated Meta `content_ids` (e.g. "123,456" or "{{a}},{{b}}"); split
@@ -198,8 +189,8 @@ export const metaCapiEventFieldsSchema = z.object({
198189
actionSource: metaCapiActionSourceSchema.default(defaultMetaCapiActionSource),
199190
contentType: metaCapiContentTypeSchema.optional(),
200191
contentIds: metaCapiContentIdsSchema,
201-
value: metaCapiValueSchema,
202-
currency: metaCapiCurrencySchema,
192+
value: metaCapiValueFieldSchema,
193+
currency: metaCapiCurrencyFieldSchema,
203194
contentCategory: metaCapiContentTextSchema,
204195
contentName: metaCapiContentTextSchema,
205196
})

packages/utils/__tests__/meta-capi.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
metaCapiActionSourceValues,
66
metaCapiBusinessMessagingEventNames,
77
metaCapiContentTypeValues,
8+
metaCapiCurrencySchema,
89
metaCapiEventNameSchema,
10+
metaCapiValueSchema,
911
metaPixelStandardEventNames,
1012
} from "../src/meta-capi"
1113

@@ -70,3 +72,40 @@ describe("metaCapiContentTypeValues", () => {
7072
expect(metaCapiContentTypeValues).toEqual(["product", "product_group"])
7173
})
7274
})
75+
76+
describe("metaCapiValueSchema / metaCapiCurrencySchema", () => {
77+
test.each([
78+
"19.99",
79+
" 250 ",
80+
"0",
81+
"9007199254740991",
82+
])("accepts plain decimal %j", (input) => {
83+
expect(metaCapiValueSchema.safeParse(input).success).toBe(true)
84+
})
85+
86+
test.each([
87+
"12,50",
88+
"1e5",
89+
"-5",
90+
"abc",
91+
"{{amount}}",
92+
"",
93+
"1.",
94+
".5",
95+
])("rejects non-canonical value %j", (input) => {
96+
expect(metaCapiValueSchema.safeParse(input).success).toBe(false)
97+
})
98+
99+
test("rejects a value that would not survive Number() intact", () => {
100+
expect(metaCapiValueSchema.safeParse("9007199254740992").success).toBe(
101+
false,
102+
)
103+
expect(metaCapiValueSchema.safeParse("1".repeat(400)).success).toBe(false)
104+
})
105+
106+
test("currency is upper-cased and must be a 3-letter code", () => {
107+
expect(metaCapiCurrencySchema.parse(" usd ")).toBe("USD")
108+
expect(metaCapiCurrencySchema.safeParse("US").success).toBe(false)
109+
expect(metaCapiCurrencySchema.safeParse("USDT").success).toBe(false)
110+
})
111+
})

packages/utils/src/meta-capi.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,38 @@ export type MetaCapiActionSource = z.infer<typeof metaCapiActionSourceSchema>
156156
export const defaultMetaCapiActionSource: MetaCapiActionSource =
157157
"business_messaging"
158158

159+
const PLAIN_DECIMAL_PATTERN = /^\d+(\.\d+)?$/
160+
const ISO_4217_PATTERN = /^[A-Z]{3}$/
161+
162+
/**
163+
* Meta CAPI `custom_data.value`: a canonical plain decimal, already trimmed,
164+
* with no locale normalisation ("12,50" is ambiguous between 12.50 and 1250)
165+
* and small enough to survive `Number()` exactly — a digit string long
166+
* enough to overflow to `Infinity` would otherwise serialise as `null`.
167+
*/
168+
export const metaCapiValueSchema = z
169+
.string()
170+
.trim()
171+
.regex(PLAIN_DECIMAL_PATTERN, "Value must be a plain number such as 19.99")
172+
.refine(
173+
(value) => Number(value) <= Number.MAX_SAFE_INTEGER,
174+
"Value is too large",
175+
)
176+
177+
/** Meta CAPI `custom_data.currency`: a 3-letter ISO 4217 code, upper-cased. */
178+
export const metaCapiCurrencySchema = z
179+
.string()
180+
.trim()
181+
.toUpperCase()
182+
.pipe(
183+
z
184+
.string()
185+
.regex(
186+
ISO_4217_PATTERN,
187+
"Currency must be a 3-letter ISO 4217 code such as USD",
188+
),
189+
)
190+
159191
/**
160192
* Meta CAPI `custom_data.content_type` values
161193
* (https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/custom-data).

0 commit comments

Comments
 (0)