Skip to content

Comments

feat: 🎸 volume-based tiered pricing#765

Draft
SirTenzin wants to merge 4 commits intodevfrom
feat/volume-based-pricing-tiers
Draft

feat: 🎸 volume-based tiered pricing#765
SirTenzin wants to merge 4 commits intodevfrom
feat/volume-based-pricing-tiers

Conversation

@SirTenzin
Copy link
Member

@SirTenzin SirTenzin commented Feb 19, 2026


Summary by cubic

Adds volume-based tiered pricing alongside graduated tiers. Users can switch tier mode in the plan editor; volume mode auto-enforces prepaid. Graduated tier amount and overage calculations are centralized in shared utils and all call sites are updated.

  • New Features

    • Added TiersType (graduated, volume) and tiers_type on Price, ProductItem, and DB; included in price/item comparison and initialization.
    • Updated plan item APIs (V1/V0), mappers, and converters to read/write tiers_type end-to-end.
    • Plan editor: shows tier mode selector when multiple tiers exist; auto-switches to prepaid and disables usage-based in volume mode; server validation enforces this.
  • Migration

    • Add prices.tiers_type (text, nullable). Null is treated as graduated.
    • No changes required for existing plans.

Written for commit 9af29ef. Summary will update on new commits.

Greptile Summary

This PR adds support for volume-based tiered pricing alongside the existing graduated pricing model. The changes introduce a new tiers_type field throughout the pricing system to distinguish between the two pricing strategies.

Key Changes:

  • API changes: Added tiers_type field to API schemas, mappers, and conversion functions
  • Improvements: New TiersType enum in usagePriceConfig.ts defines graduated and volume-based pricing types
  • Improvements: Database schema updated to store tiers_type in the prices table
  • Improvements: UI enhancement in EditPlanFeatureSheet.tsx shows tier type selector when multiple tiers exist
  • Bug fixes: Added tiers_type comparison logic to price and item equality checks

Critical Issue:
Three API schema files incorrectly use z.enum(TiersType) instead of z.nativeEnum(TiersType). Since TiersType is a TypeScript enum (not a Zod enum), this will cause runtime validation errors.

Confidence Score: 2/5

  • This PR has critical syntax errors that will cause runtime validation failures
  • The implementation is comprehensive and well-structured across database, backend, and frontend layers. However, three API schema files use z.enum() instead of z.nativeEnum() for the TypeScript enum TiersType, which will cause Zod validation to fail at runtime when clients attempt to create or update products with tiered pricing.
  • Pay close attention to shared/api/products/items/apiPlanItemV1.ts, shared/api/products/items/previousVersions/apiPlanItemV0.ts, and shared/api/products/items/crud/createPlanItemParamsV1.ts - all have syntax errors that must be fixed

Important Files Changed

Filename Overview
shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts Added TiersType enum to define graduated vs volume-based tiered pricing
shared/api/products/items/apiPlanItemV1.ts Added tiers_type to API schema but incorrectly uses z.enum() instead of z.nativeEnum()
shared/api/products/items/previousVersions/apiPlanItemV0.ts Added tiers_type to legacy API schema with same incorrect z.enum() usage
shared/api/products/items/crud/createPlanItemParamsV1.ts Added tiers_type to create params with incorrect z.enum() instead of z.nativeEnum()
vite/src/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet.tsx Added UI selector for choosing graduated vs volume-based pricing, shown conditionally when multiple tiers exist

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User selects pricing tiers in UI] --> B[EditPlanFeatureSheet.tsx]
    B --> C{Multiple tiers?}
    C -->|Yes| D[Show TiersType selector<br/>Graduated or Volume-based]
    C -->|No| E[Show single tier price]
    D --> F[User selects tiers_type]
    F --> G[productItemToPlanItemV1]
    G --> H[API: CreatePlanItemParamsV1]
    H --> I[planItemV0ToProductItem]
    I --> J[ProductItemSchema with tiers_type]
    J --> K[itemToPriceAndEnt]
    K --> L[Price object with tiers_type]
    L --> M[Prices Table in Database]
    M --> N[priceInitUtils: pricesAreSame]
    N --> O{Compare tiers_type}
    O -->|Different| P[Prices are different]
    O -->|Same| Q[Check other fields]
Loading

Last reviewed commit: 5f685cf

@vercel
Copy link

vercel bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
autumn-vite Ready Ready Preview, Comment Feb 20, 2026 6:49pm

Request Review

@use-tusk
Copy link

use-tusk bot commented Feb 19, 2026

⚠️ Additional setup required (5f685cf) View output ↗

Tip

New to Tusk? Learn more here.
Follow the setup instructions so Tusk can start generating tests.


View check history

Commit Status Output Created (UTC)
5f685cf ⚠️ Additional setup required Output Feb 19, 2026 5:15PM

@SirTenzin SirTenzin marked this pull request as draft February 19, 2026 17:15
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

18 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

.object({
amount: z.number().optional(),
tiers: z.array(UsageTierSchema).optional(),
tiers_type: z.enum(TiersType).optional(),
Copy link
Contributor

Choose a reason for hiding this comment

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

TiersType is a TypeScript enum, so must use z.nativeEnum() not z.enum()

Suggested change
tiers_type: z.enum(TiersType).optional(),
tiers_type: z.nativeEnum(TiersType).optional(),
Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/api/products/items/apiPlanItemV1.ts
Line: 36

Comment:
`TiersType` is a TypeScript enum, so must use `z.nativeEnum()` not `z.enum()`

```suggestion
				tiers_type: z.nativeEnum(TiersType).optional(),
```

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

@@ -31,6 +34,7 @@ export const ApiPlanItemV0Schema = z
.object({
amount: z.number().optional(),
tiers: z.array(UsageTierSchema).optional(),
Copy link
Contributor

Choose a reason for hiding this comment

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

TiersType is a TypeScript enum, so must use z.nativeEnum() not z.enum()

Suggested change
tiers: z.array(UsageTierSchema).optional(),
tiers_type: z.nativeEnum(TiersType).optional(),
Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/api/products/items/previousVersions/apiPlanItemV0.ts
Line: 36

Comment:
`TiersType` is a TypeScript enum, so must use `z.nativeEnum()` not `z.enum()`

```suggestion
				tiers_type: z.nativeEnum(TiersType).optional(),
```

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

@@ -26,6 +29,7 @@ export const CreatePlanItemParamsV1Schema = z
.object({
amount: z.number().optional(),
tiers: z.array(UsageTierSchema).optional(),
Copy link
Contributor

Choose a reason for hiding this comment

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

TiersType is a TypeScript enum, so must use z.nativeEnum() not z.enum()

Suggested change
tiers: z.array(UsageTierSchema).optional(),
tiers_type: z.nativeEnum(TiersType).optional(),
Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/api/products/items/crud/createPlanItemParamsV1.ts
Line: 31

Comment:
`TiersType` is a TypeScript enum, so must use `z.nativeEnum()` not `z.enum()`

```suggestion
				tiers_type: z.nativeEnum(TiersType).optional(),
```

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.

1 issue found across 18 files

Confidence score: 4/5

  • This PR looks safe to merge with minimal risk; the only noted issue is a low-severity guideline mismatch.
  • shared/models/productV2Models/productItemModels/productItemModels.ts uses z.nativeEnum, which is discouraged in Zod v4 and could lead to deprecation-related friction later.
  • Pay close attention to shared/models/productV2Models/productItemModels/productItemModels.ts - update enum handling to align with repository guidance.
Prompt for AI agents (all 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:123">
P2: Avoid z.nativeEnum in Zod v4; use z.enum with the TypeScript enum instead to match repository guidance and avoid deprecated API.

(Based on your team's feedback about avoiding z.nativeEnum in Zod v4.) [FEEDBACK_USED]</violation>
</file>

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

"The billing units of the product item (eg $1 for 30 credits).",
}),

tiers_type: z.nativeEnum(TiersType).nullish().meta({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 19, 2026

Choose a reason for hiding this comment

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

P2: Avoid z.nativeEnum in Zod v4; use z.enum with the TypeScript enum instead to match repository guidance and avoid deprecated API.

(Based on your team's feedback about avoiding z.nativeEnum in Zod v4.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At shared/models/productV2Models/productItemModels/productItemModels.ts, line 123:

<comment>Avoid z.nativeEnum in Zod v4; use z.enum with the TypeScript enum instead to match repository guidance and avoid deprecated API.

(Based on your team's feedback about avoiding z.nativeEnum in Zod v4.) </comment>

<file context>
@@ -119,6 +120,11 @@ export const ProductItemSchema = z.object({
 			"The billing units of the product item (eg $1 for 30 credits).",
 	}),
 
+	tiers_type: z.nativeEnum(TiersType).nullish().meta({
+		description:
+			"The type of tiered pricing: graduated or volume-based.",
</file context>
Fix with Cubic

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: 5f685cf9a3

ℹ️ 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".

.object({
amount: z.number().optional(),
tiers: z.array(UsageTierSchema).optional(),
tiers_type: z.enum(TiersType).optional(),

Choose a reason for hiding this comment

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

P1 Badge Propagate tiers_type through V1-to-V0 plan item mapping

This new tiers_type field is accepted by the V1 schema, but the shared planItemV1ToV0 mapper still omits it when constructing price (it only copies amount, tiers, interval, etc.), and both planV1ToV0 and planV1ToProductItems rely on that mapper. In V1 plan/customize flows, requests that set tiers_type: "volume" are therefore downgraded without the field, so the selected tiering mode is lost before persistence.

Useful? React with 👍 / 👎.

Comment on lines +127 to +130
value={item.tiers_type ?? TiersType.Graduated}
onValueChange={(val) =>
setItem({ ...item, tiers_type: val as TiersType })
}

Choose a reason for hiding this comment

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

P1 Badge Wire selected tier mode into Stripe tier pricing params

The editor now lets users choose tiers_type, but downstream Stripe price creation still hardcodes tiers_mode: "graduated" (e.g. in createStripePrepaid.ts, createStripeInArrear.ts, and priceToStripeCreatePriceParams.ts), so selecting “Volume-based” does not change billing behavior. This creates incorrect invoices whenever multi-tier usage pricing is configured as volume-based, because Stripe continues to apply graduated math.

Useful? React with 👍 / 👎.

@@ -72,7 +72,10 @@ export const getCusPriceUsage = ({
const roundedQuantity =
Math.ceil(new Decimal(usage).div(billingUnits).toNumber()) * billingUnits;
Copy link
Member Author

Choose a reason for hiding this comment

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

should probably Decimal().mult this for the sake of it

import { StatusCodes } from "http-status-codes";
import { compareBillingIntervals } from "./priceUtils/priceIntervalUtils.js";

export { getPriceForOverage } from "@autumn/shared";
Copy link
Member Author

Choose a reason for hiding this comment

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

will remove this and make callers directly import shared

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.

1 participant