feat(billing): add monthly billing cycle for Team and Enterprise seats#1609
Open
jeanduplessis wants to merge 21 commits intomainfrom
Open
feat(billing): add monthly billing cycle for Team and Enterprise seats#1609jeanduplessis wants to merge 21 commits intomainfrom
jeanduplessis wants to merge 21 commits intomainfrom
Conversation
…criptions The credit-on-subscription-creation feature was already disabled via ENABLE_ORG_CREATION_FREE_CREDITS=false. Remove the feature flag, the credit-granting block inside handleSubscriptionEventInternal, and all associated imports, test helpers, and tests.
Add monthly billing option for team/enterprise seats (/ per month) alongside existing annual pricing. Align spec structure with kiloclaw-billing spec: add Role of This Document, Definitions, Changelog sections, and wrap long lines for readability.
Interactive HTML prototype covering all monthly billing UI changes: - Upgrade dialog with Monthly/Annual toggle and dynamic pricing - Subscription overview cards for monthly and annual contexts - Billing cycle change confirmation with Now/New comparison layout - Seat change modals with live +/- controls, cost preview, validation - Pending cycle change banner with cancel action Deployed to https://monthly-billing-prototype.pages.dev/monthly-billing.html
…se seats Implement monthly billing alongside existing annual billing per the spec changelog 2026-03-26. Users now select monthly or annual billing at checkout, with cycle-specific Stripe prices resolved from env vars. Backend: - Add pricing constants for both cycles (Teams /, Enterprise /) - Add BillingCycle type with DB/API/Stripe boundary mapping - Replace product.default_price lookup with explicit getPriceIdForPlanAndCycle - Populate billing_cycle column from Stripe subscription interval - Add changeBillingCycle/cancelBillingCycleChange endpoints via Stripe subscription schedules (takes effect at renewal, no proration) - Expand schedule on subscription retrieval for pending change detection Frontend: - Add Monthly/Annual pill toggle with Save 17% badge to UpgradeTrialDialog - Dynamic PlanCard billing label (Billed monthly/Billed annually) - Switch to Monthly/Annual button in SubscriptionQuickActions - BillingCycleChangeDialog with cost comparison and effective date - Pending cycle change banner in SubscriptionOverviewCard with cancel Tests: - 3 tests for billing_cycle tracking (monthly, yearly, null fallback) - 2 tests for billingCycle schema validation on checkout endpoint
edd2aef to
d6b2084
Compare
…le-change, and seat preview - Preserve billing cycle when resubscribing ended monthly orgs - Map all subscription items in cycle-change schedule phases - Release orphaned schedules when phase update fails - Infer pending cycle from current interval (phase prices aren't expanded) - Use billing-cycle-aware seat rates in change-seat cost preview
src/components/organizations/subscription/SubscriptionOverviewCard.tsx
Outdated
Show resolved
Hide resolved
Contributor
Code Review SummaryStatus: 5 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)CRITICAL
WARNING
Other Observations (not in diff)Issues found in unchanged code that cannot receive inline comments:
Files Reviewed (1 file)
Fix these issues in Kilo Cloud Reviewed by gpt-5.4-20260305 · 203,885 tokens |
…, and UI parity - Identify paid seat item by known price IDs instead of assuming items[0] - Preserve subscription-level discounts (promotion codes) in schedule phases - Allow billing managers to stop cancellation and resubscribe - Show pending cycle and renewal info in subscription overview cards - Sum all items' quantities when resubscribing ended subscriptions - Verify schedule has 2 phases before releasing in cancelBillingCycleChange
src/components/organizations/subscription/SubscriptionOverviewCard.tsx
Outdated
Show resolved
Hide resolved
…ems[0] When a subscription has mixed paid/free seat items, the free-seat item can sort first. The pendingCycleChange detection now compares ALL phase2 prices against the current subscription's price set, and derives the billing interval from the first item with a recurring interval. Also consolidates all items[0] interval lookups into a single currentBillingInterval computed value.
…splay - Add getPlanForPriceId() to derive plan from the live Stripe price ID instead of org.plan which can drift from the actual subscription - Show 'Changes at renewal' instead of stale dollar amount when a billing cycle change is pending (phase 2 prices aren't expanded) - Sum all items for next-payment display in the non-pending case too
…ge dialog - Filter to paid items (unit_amount > 0) when computing resubscribe seat count to avoid converting free seats into paid ones - Derive seat count and interval from paid seat item in QuickActions - Add inferPlanFromUnitAmount() to derive plan tier from Stripe price instead of org.plan which can drift from the live subscription
…refactors - Enforce require_seats on all new orgs; add seat-usage gating to trial middleware and member invitations - Add organization_membership_removals table for audit/tombstone tracking of removed members - Add model/provider deny-list enforcement for enterprise orgs - Harden billing-cycle detection, resubscribe defaults, and seat-change logic to correctly handle mixed paid/free subscription items - GDPR: integrate membership removals into softDeleteUser - KISS: consolidate price constants into SEAT_PRICING lookup table, extract shared subscription helpers (canManageBilling, findPaidSeatItem, paidSeatQuantity), rename organizationOwnerProcedure to organizationBillingProcedure, unify billing-cycle converters, extract detectPendingCycleChange, tighten Stripe error detection
… and cancel safety - Derive paid-only seat count from Stripe when prefilling resubscribe checkout, falling back to DB total if retrieval fails (P1) - Extract billing cycle from paid seat line item (unit_amount > 0) instead of items[0] which may be a free promo item (P2) - Sort ended purchases by expires_at instead of created_at so resubscribe picks the correct subscription regardless of webhook delivery order (P3) - Defer schedule release until cancel succeeds to avoid silently dropping a pending billing-cycle change on transient errors (P4) - Store actual Stripe subscription status instead of collapsing all non-ended states to active - Consolidate repeated inline as-casts in SubscriptionOverviewCard - Remove stale config comment, simplify trial-utils docblock, precompute effectiveDate in SubscriptionQuickActions
… disable agents - Clamp resubscribe default seat count to at least current usedSeats so orgs that grew while unsubscribed aren't sent to checkout with fewer seats than they need - Use organizationBillingProcedure (no trial gate) for auto-fix toggleAgent and auto-triage saveConfig; enforce trial only when enabling so hard-expired orgs can still disable running agents
The useEffect that seeds resubscribe checkout now waits for both resubscribeDefaults and seatUsage queries to resolve before setting the seat count, ensuring the clamp against current usage is always accurate regardless of query resolution order.
…e choice Guard the resubscribe-defaults seeding effect with a useRef flag so it only runs once. Later seatUsage query refetches no longer trigger setBillingCycle, which was overriding the user's manual cycle toggle.
User-facing billing cycle and seat count controls now mark the seededFromDefaults ref, so the one-time seeding effect is skipped if the user interacted with the form before queries resolved.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR implements seat-based billing enforcement, membership audit tracking, and enterprise model restrictions, then applies a comprehensive KISS refactoring pass across the billing/subscription codebase.
Seat Billing Enforcement
require_seats: true(migration 0064)unit_amount > 0on client,KNOWN_SEAT_PRICE_IDSon server)Membership Audit Tombstones
organization_membership_removalstable (migration 0065) tracks who was removed, when, by whom, and their previous rolesoftDeleteUserupdated to delete/anonymize tombstone recordsEnterprise Model Restrictions
getEffectiveModelRestrictions()enforces model/provider deny lists only for enterprise orgsKISS Refactoring
inferPlanFromUnitAmount()withSEAT_PRICINGlookup table andseatPrice(plan, cycle)helpercanManageBilling(),findPaidSeatItem(),paidSeatQuantity(),detectPendingCycleChange()billingCycleFromDbandbillingCycleFromStripeIntervalintotoBillingCycle()organizationOwnerProcedure→organizationBillingProcedure(14 files) to reflect actual role permissionsBillingCycleChangeDialog.getPricing()from 28 lines to 5.includes('already')fallbackuseOrganizationWithMembersdependency fromUpgradeTrialDialogReview Fixes (from automated review pass)
organization_membership_removalsnot insoftDeleteUserpaidSeatCount→defaultSeatCountin resubscribe-defaults APIremoved_bynot passed in member removal routerVerification
pnpm typecheck— passes with zero errorspnpm test— 3343 tests pass; 3 test failures caused by branch changes were identified and fixed:stripe-3ds.test.ts: Updated mock items to includeprice.idforKNOWN_SEAT_PRICE_IDSorganization-subscription-event.test.ts: MockedretrieveSubscriptionfor H1 guard testdefaults/route.test.ts: Setplan: 'enterprise'on mock orgs that test deny-list enforcementkiloclaw-billing-router.test.tstimeout) is pre-existing/flakyVisual Changes
N/A
Reviewer Notes
SEAT_PRICINGfor backward compatibility — no external consumers need updatingorganizationBillingProcedurerename is purely cosmetic (same['owner', 'billing_manager']role check) but touches 14 router filesrequire_seatsDB default change (migration 0064) only affects code paths that omit the column on insert; all production insert paths explicitly set the valuetoBillingCycleaccepts the union of DB ('yearly') and Stripe ('year') values; old function names are retained as typed aliases