diff --git a/.changeset/ack-pay-decimals-min-value.md b/.changeset/ack-pay-decimals-min-value.md new file mode 100644 index 0000000..b5ceb6d --- /dev/null +++ b/.changeset/ack-pay-decimals-min-value.md @@ -0,0 +1,7 @@ +--- +"@agentcommercekit/ack-pay": patch +--- + +Reject negative `decimals` in the Valibot payment option schema instead of clamping them to `0`. + +`paymentOptionSchema` used `v.toMinValue(0)`, which silently rewrites negative values. That diverged from the Zod schema (`z.number().int().nonnegative()`) and could hide invalid payment precision. Switch to `v.minValue(0)` so both schemas reject the same inputs. diff --git a/packages/ack-pay/src/schemas/payment-option.test.ts b/packages/ack-pay/src/schemas/payment-option.test.ts index 166b449..4c94181 100644 --- a/packages/ack-pay/src/schemas/payment-option.test.ts +++ b/packages/ack-pay/src/schemas/payment-option.test.ts @@ -35,3 +35,25 @@ describe("paymentOptionSchema amount", () => { }, ) }) + +function acceptsDecimals(decimals: number) { + const value = { ...paymentOption, amount: 10, decimals } + + return { + valibot: v.safeParse(valibotPaymentOptionSchema, value).success, + zod: zodPaymentOptionSchema.safeParse(value).success, + } +} + +describe("paymentOptionSchema decimals", () => { + it.each([0, 2, 18])("accepts non-negative decimals %s", (decimals) => { + expect(acceptsDecimals(decimals)).toEqual({ valibot: true, zod: true }) + }) + + it.each([-1, -6, 1.5])( + "rejects invalid decimals %s instead of clamping", + (decimals) => { + expect(acceptsDecimals(decimals)).toEqual({ valibot: false, zod: false }) + }, + ) +}) diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index 75fc980..ab9ac15 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -17,7 +17,7 @@ export const paymentOptionSchema = v.object({ v.pipe(v.number(), v.integer(), v.gtValue(0)), positiveIntegerString, ]), - decimals: v.pipe(v.number(), v.integer(), v.toMinValue(0)), + decimals: v.pipe(v.number(), v.integer(), v.minValue(0)), currency: v.string(), recipient: v.string(), network: v.optional(v.string()),