Skip to content

fix: 🐛 allow either or both in price.tiers type#876

Merged
johnyeocx merged 3 commits intouseautumn:mainfrom
SirTenzin:fix/at-least-one-tier-type
Mar 4, 2026
Merged

fix: 🐛 allow either or both in price.tiers type#876
johnyeocx merged 3 commits intouseautumn:mainfrom
SirTenzin:fix/at-least-one-tier-type

Conversation

@SirTenzin
Copy link
Member

@SirTenzin SirTenzin commented Mar 4, 2026


Summary by cubic

Allow price.tiers to define amount, flat_amount, or both, with a guard that requires at least one. Missing amount now defaults to 0 across schemas and conversions, and flat_amount nulls are treated as undefined.

  • Bug Fixes

    • PriceTierSchema and UsageTierSchema: amount and flat_amount are optional, add an at-least-one refine, and transform to set amount to 0 when omitted.
    • Server utils and conversions: itemToPriceAndEnt backfills amount with 0; productItemToPlanItemV1 only includes flat_amount when defined; constructItem and tests stop using null for flat_amount.
  • Migration

    • If you send flat_amount: null, remove the field or provide a number.

Written for commit 5245c89. Summary will update on new commits.

Greptile Summary

This PR updates UsageTierSchema and PriceTierSchema to allow tiered pricing where only flat_amount, only amount, or both can be provided — making amount optional with a default of 0 instead of required.

Key changes:

  • Bug fixes — Makes amount optional in tier schemas so that flat-fee-only tiers are now expressible without providing a per-unit rate.
  • Bug fixes — Attempts to add a .refine() guard to UsageTierSchema to enforce that at least one of amount or flat_amount is present; however, the guard is ineffective because amount carries .default(0), which Zod applies before the refinement callback runs. By the time the refine executes, amount is always 0 (not undefined), making the validation condition always true.
  • Bug fixesPriceTierSchema is missing the validation guard that was added to UsageTierSchema, creating an inconsistency where UsageTierSchema (ineffectively) attempts to enforce at-least-one-field validation, while PriceTierSchema does not.

Both shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts and shared/models/productV2Models/productItemModels/productItemModels.ts need attention for consistent and effective validation.

Confidence Score: 2/5

  • PR introduces logic bugs in schema validation that defeat the stated intent of requiring at least one pricing field per tier.
  • Two substantive validation bugs prevent safe merge: (1) The .refine() guard in UsageTierSchema is ineffective because amount's default is applied before the refinement callback runs, making the validation always pass; (2) PriceTierSchema lacks the equivalent guard entirely, creating inconsistent validation behavior. Both schemas should enforce the same at-least-one-field rule, but currently neither does effectively.
  • shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts (refine callback order) and shared/models/productV2Models/productItemModels/productItemModels.ts (missing refine guard)

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Tier Input Object] --> B{Zod Parse}
    B --> C[Apply field defaults\namount = 0 if absent]
    C --> D{.refine check\namount !== undefined\nOR flat_amount !== undefined}
    D -- "Always TRUE\n(amount is 0, not undefined)" --> E[Validation Passes ✓]
    D -- "Never reached" --> F[Validation Fails ✗]

    subgraph UsageTierSchema
        C
        D
    end

    subgraph PriceTierSchema
        G[Apply field defaults\namount = 0 if absent] --> H[No .refine guard]
        H --> I[Validation Passes\neven with empty tier ⚠️]
    end

    B --> G
Loading

Last reviewed commit: 24418db

@vercel
Copy link

vercel bot commented Mar 4, 2026

@SirTenzin is attempting to deploy a commit to the Autumn Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24418dbc42

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export const UsageTierSchema = z
.object({
to: z.number().or(z.literal(Infinite)),
amount: z.number().optional().default(0),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce tier price presence before applying defaults

amount is now declared as z.number().optional().default(0), so the subsequent refine check (amount !== undefined || flat_amount !== undefined) is effectively always true after parsing. That means a tier like { "to": 100 } passes UsageTierSchema even though the schema intends to require at least one pricing field, weakening validation anywhere this shared schema is used for request parsing.

Useful? React with 👍 / 👎.

Comment on lines +22 to +30
.refine(
(z) => {
return z.amount !== undefined || z.flat_amount !== undefined;
},
{
message: "Either amount or flat_amount, or both must be defined",
path: ["amount", "flat_amount"],
},
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .refine() validation will always pass and never catch anything. Because amount is declared as .optional().default(0), Zod applies the default before the refinement runs — so by the time the callback executes, z.amount is always 0, never undefined.

The condition z.amount !== undefined || z.flat_amount !== undefined evaluates to 0 !== undefined || ..., which is always true.

To make this validation meaningful, the default needs to be applied after the refine, not before. Consider this structure:

Suggested change
.refine(
(z) => {
return z.amount !== undefined || z.flat_amount !== undefined;
},
{
message: "Either amount or flat_amount, or both must be defined",
path: ["amount", "flat_amount"],
},
);
export const UsageTierSchema = z
.object({
to: z.number().or(z.literal(Infinite)),
amount: z.number().optional(),
flat_amount: z.number().optional(),
})
.refine(
(val) => val.amount !== undefined || val.flat_amount !== undefined,
{
message: "Either amount or flat_amount, or both must be defined",
path: ["amount", "flat_amount"],
},
)
.transform((val) => ({
...val,
amount: val.amount ?? 0,
}));

This way, the refine callback runs on the raw data (before defaults), and the default is applied afterward via transform.

Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts
Line: 22-30

Comment:
The `.refine()` validation will always pass and never catch anything. Because `amount` is declared as `.optional().default(0)`, Zod applies the default _before_ the refinement runs — so by the time the callback executes, `z.amount` is always `0`, never `undefined`.

The condition `z.amount !== undefined || z.flat_amount !== undefined` evaluates to `0 !== undefined || ...`, which is always `true`.

To make this validation meaningful, the default needs to be applied *after* the refine, not before. Consider this structure:

```suggestion
export const UsageTierSchema = z
  .object({
    to: z.number().or(z.literal(Infinite)),
    amount: z.number().optional(),
    flat_amount: z.number().optional(),
  })
  .refine(
    (val) => val.amount !== undefined || val.flat_amount !== undefined,
    {
      message: "Either amount or flat_amount, or both must be defined",
      path: ["amount", "flat_amount"],
    },
  )
  .transform((val) => ({
    ...val,
    amount: val.amount ?? 0,
  }));
```

This way, the refine callback runs on the raw data (before defaults), and the default is applied afterward via transform.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Additional Comments (1)

shared/models/productV2Models/productItemModels/productItemModels.ts
UsageTierSchema (in usagePriceConfig.ts) includes a .refine() guard to enforce that at least one of amount or flat_amount is provided, but PriceTierSchema here is missing that guard. Both schemas have identical field structures (amount and flat_amount both optional), so they should enforce the same validation rule.

Without the refine, an object like { to: 100 } (with neither field) will pass validation silently, which is inconsistent with UsageTierSchema. Consider adding the same refine guard to ensure consistent validation across both tier schemas.

Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/models/productV2Models/productItemModels/productItemModels.ts
Line: 17-30

Comment:
`UsageTierSchema` (in `usagePriceConfig.ts`) includes a `.refine()` guard to enforce that at least one of `amount` or `flat_amount` is provided, but `PriceTierSchema` here is missing that guard. Both schemas have identical field structures (`amount` and `flat_amount` both optional), so they should enforce the same validation rule.

Without the refine, an object like `{ to: 100 }` (with neither field) will pass validation silently, which is inconsistent with `UsageTierSchema`. Consider adding the same refine guard to ensure consistent validation across both tier schemas.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files

Confidence score: 3/5

  • There is some user-impacting risk: flat_amount in shared/models/productV2Models/productItemModels/productItemModels.ts no longer accepts null, which can break parsing for existing/API tier payloads that still send flat_amount: null.
  • Validation is weakened in shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts because amount defaults to 0, so tiers with neither field explicitly provided may slip through.
  • Pay close attention to shared/models/productV2Models/productItemModels/productItemModels.ts and shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts - parsing/validation changes could affect existing tier payloads.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="shared/models/productV2Models/productItemModels/productItemModels.ts">

<violation number="1" location="shared/models/productV2Models/productItemModels/productItemModels.ts:26">
P1: `flat_amount` no longer accepts `null`, which can break parsing of existing/API tier payloads that still send `flat_amount: null`.</violation>
</file>

<file name="shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts">

<violation number="1" location="shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts:19">
P1: `amount` defaulting to `0` makes the "either amount or flat_amount must be defined" validation ineffective, allowing tiers with neither field explicitly provided.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="shared/models/productV2Models/productItemModels/productItemModels.ts">

<violation number="1" location="shared/models/productV2Models/productItemModels/productItemModels.ts:27">
P2: `flat_amount` is now nullable in `PriceTierSchema`, which introduces an inconsistent tier shape (`number | null | undefined`) versus other tier schemas/types that expect only `number | undefined`. This can allow `null` tiers through validation and cause downstream validation/handling mismatches.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…els.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
@johnyeocx johnyeocx merged commit 0e15779 into useautumn:main Mar 4, 2026
4 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants