Skip to content

feat(den): add simulated org seat billing#2010

Draft
benjaminshafii wants to merge 4 commits into
devfrom
feat/den-billing-products
Draft

feat(den): add simulated org seat billing#2010
benjaminshafii wants to merge 4 commits into
devfrom
feat/den-billing-products

Conversation

@benjaminshafii
Copy link
Copy Markdown
Member

Summary

  • add separate org_seats billing product alongside existing inference billing
  • add provider/feature flags for disabled, simulated, and Stripe billing modes
  • add simulated billing APIs/UI so purchase, upgrade, failed-payment, and cancel states can be tested without Stripe
  • add centralized org seat entitlement checks behind FEATURE_BILLING_ENFORCEMENT
  • split the Den billing page into Team Seats and OpenWork Models cards

Verification

Daytona Flow Covered

  • signed into seeded Acme Robotics org
  • purchased Team Seats in simulated mode
  • enabled OpenWork Models in simulated mode
  • upgraded Team Seats quantity
  • canceled OpenWork Models
  • canceled Team Seats

Flags

  • OPENWORK_BILLING_PROVIDER=disabled|simulated|stripe
  • FEATURE_BILLING_ORG_SEATS=false by default
  • FEATURE_BILLING_INFERENCE=true by default
  • FEATURE_BILLING_ENFORCEMENT=false by default

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 29, 2026

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

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview, Comment May 29, 2026 11:59pm
openwork-den Ready Ready Preview, Comment May 29, 2026 11:59pm
openwork-den-worker-proxy Ready Ready Preview, Comment May 29, 2026 11:59pm
openwork-landing Ready Ready Preview, Comment, Open in v0 May 29, 2026 11:59pm

@benjaminshafii benjaminshafii marked this pull request as draft May 30, 2026 00:01
Copy link
Copy Markdown

@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 9 files

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="ee/apps/den-web/app/(den)/dashboard/_components/billing-dashboard-screen.tsx">

<violation number="1" location="ee/apps/den-web/app/(den)/dashboard/_components/billing-dashboard-screen.tsx:236">
P2: Loading state won't display on the simulated-mode button because the `actionBusy` key set by `updateSimulated` (`"org_seats:active"`) doesn't match the key checked in `loading` (`"org-seats:portal"`). The spinner never activates in simulated mode.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

<input className="mt-2 w-full rounded-[14px] border border-gray-200 px-3 py-2 text-[14px]" type="number" min={minSeats} max={500} value={seatQuantity} onChange={(event) => setSeatQuantity(Number(event.target.value))} />
</label>
<div className="flex flex-wrap justify-end gap-3">
{orgSeats.hasActiveSubscription ? <DenButton disabled={!isOwner || !orgSeats.configured} loading={actionBusy === "org-seats:portal"} onClick={() => simulated ? void updateSimulated("org_seats", "active", seatQuantity) : void openUrlFromResponse("/v1/billing/org-seats/portal", { method: "POST" }, "org-seats:portal")}>{simulated ? "Update simulated seats" : "Manage seats"}</DenButton> : null}
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 30, 2026

Choose a reason for hiding this comment

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

P2: Loading state won't display on the simulated-mode button because the actionBusy key set by updateSimulated ("org_seats:active") doesn't match the key checked in loading ("org-seats:portal"). The spinner never activates in simulated mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ee/apps/den-web/app/(den)/dashboard/_components/billing-dashboard-screen.tsx, line 236:

<comment>Loading state won't display on the simulated-mode button because the `actionBusy` key set by `updateSimulated` (`"org_seats:active"`) doesn't match the key checked in `loading` (`"org-seats:portal"`). The spinner never activates in simulated mode.</comment>

<file context>
@@ -70,170 +95,185 @@ function parsePolarBilling(payload: unknown): PolarBilling | null {
+              <input className="mt-2 w-full rounded-[14px] border border-gray-200 px-3 py-2 text-[14px]" type="number" min={minSeats} max={500} value={seatQuantity} onChange={(event) => setSeatQuantity(Number(event.target.value))} />
+            </label>
+            <div className="flex flex-wrap justify-end gap-3">
+              {orgSeats.hasActiveSubscription ? <DenButton disabled={!isOwner || !orgSeats.configured} loading={actionBusy === "org-seats:portal"} onClick={() => simulated ? void updateSimulated("org_seats", "active", seatQuantity) : void openUrlFromResponse("/v1/billing/org-seats/portal", { method: "POST" }, "org-seats:portal")}>{simulated ? "Update simulated seats" : "Manage seats"}</DenButton> : null}
+              {!orgSeats.hasActiveSubscription ? <DenButton disabled={!isOwner || !orgSeats.configured} loading={actionBusy === "org-seats:checkout"} onClick={() => void openUrlFromResponse("/v1/billing/org-seats/checkout", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ quantity: seatQuantity }) }, "org-seats:checkout")}>Buy Team Seats</DenButton> : null}
+              {simulated && orgSeats.hasActiveSubscription ? <DenButton variant="secondary" disabled={!isOwner} loading={actionBusy === "org_seats:canceled"} onClick={() => void updateSimulated("org_seats", "canceled", seatQuantity)}>Cancel simulated seats</DenButton> : null}
</file context>
Suggested change
{orgSeats.hasActiveSubscription ? <DenButton disabled={!isOwner || !orgSeats.configured} loading={actionBusy === "org-seats:portal"} onClick={() => simulated ? void updateSimulated("org_seats", "active", seatQuantity) : void openUrlFromResponse("/v1/billing/org-seats/portal", { method: "POST" }, "org-seats:portal")}>{simulated ? "Update simulated seats" : "Manage seats"}</DenButton> : null}
{orgSeats.hasActiveSubscription ? <DenButton disabled={!isOwner || !orgSeats.configured} loading={actionBusy === "org-seats:portal" || actionBusy === "org_seats:active"} onClick={() => simulated ? void updateSimulated("org_seats", "active", seatQuantity) : void openUrlFromResponse("/v1/billing/org-seats/portal", { method: "POST" }, "org-seats:portal")}>{simulated ? "Update simulated seats" : "Manage seats"}</DenButton> : null}
Fix with Cubic

@benjaminshafii
Copy link
Copy Markdown
Member Author

Added clearer Daytona recordings for the billing UX:

These are using simulated billing mode, so the UI/seat state is exercised end-to-end without Stripe credentials.

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