Skip to content

feat(billing): add monthly billing cycle for Team and Enterprise seats#1609

Merged
jeanduplessis merged 24 commits intomainfrom
team-enterprise-monthly-plan
Mar 31, 2026
Merged

feat(billing): add monthly billing cycle for Team and Enterprise seats#1609
jeanduplessis merged 24 commits intomainfrom
team-enterprise-monthly-plan

Conversation

@jeanduplessis
Copy link
Copy Markdown
Contributor

@jeanduplessis jeanduplessis commented Mar 26, 2026

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

  • All new organizations now default to require_seats: true (migration 0064)
  • Trial middleware and member invitation flows gate on seat availability
  • Seat-change, resubscribe, and billing-cycle-change logic hardened to correctly handle mixed paid/free subscription items (uses unit_amount > 0 on client, KNOWN_SEAT_PRICE_IDS on server)

Membership Audit Tombstones

  • New organization_membership_removals table (migration 0065) tracks who was removed, when, by whom, and their previous role
  • Tombstone records prevent webhook-driven re-addition of intentionally removed members
  • GDPR: softDeleteUser updated to delete/anonymize tombstone records

Enterprise Model Restrictions

  • getEffectiveModelRestrictions() enforces model/provider deny lists only for enterprise orgs
  • Defaults route returns appropriate errors when all models are blocked

KISS Refactoring

  • Replaced 8 individual price constants + inferPlanFromUnitAmount() with SEAT_PRICING lookup table and seatPrice(plan, cycle) helper
  • Extracted shared helpers: canManageBilling(), findPaidSeatItem(), paidSeatQuantity(), detectPendingCycleChange()
  • Unified billingCycleFromDb and billingCycleFromStripeInterval into toBillingCycle()
  • Renamed organizationOwnerProcedureorganizationBillingProcedure (14 files) to reflect actual role permissions
  • Simplified BillingCycleChangeDialog.getPricing() from 28 lines to 5
  • Tightened Stripe error detection: removed loose .includes('already') fallback
  • Removed unused useOrganizationWithMembers dependency from UpgradeTrialDialog

Review Fixes (from automated review pass)

  • Fixed GDPR gap: new organization_membership_removals not in softDeleteUser
  • Fixed billing-cycle detection reading free items instead of paid items
  • Renamed misleading paidSeatCountdefaultSeatCount in resubscribe-defaults API
  • Fixed removed_by not passed in member removal router

Verification

  • pnpm typecheck — passes with zero errors
  • pnpm test — 3343 tests pass; 3 test failures caused by branch changes were identified and fixed:
    • stripe-3ds.test.ts: Updated mock items to include price.id for KNOWN_SEAT_PRICE_IDS
    • organization-subscription-event.test.ts: Mocked retrieveSubscription for H1 guard test
    • defaults/route.test.ts: Set plan: 'enterprise' on mock orgs that test deny-list enforcement
  • Remaining 1 failure (kiloclaw-billing-router.test.ts timeout) is pre-existing/flaky
  • Full manual verification against Stripe test env.
Manual Verification Plan# Manual Verification Plan: Monthly Billing for Teams & Enterprise

Prerequisites (All Journeys)

  • Dev server running (read port from .dev-port)
  • Stripe test mode enabled with test card 4242 4242 4242 4242 (any future expiry, any CVC)
  • Access to the database to verify records
  • Access to Stripe dashboard (test mode) to verify subscription objects

Part A: Teams Subscription Paths

Journey A1: New Org → Teams Monthly Subscription

Pre-setup: None (fresh user).

Steps:

  1. Log in as a new fake user.
  2. Navigate to /organizations/new.
  3. Fill in organization name and submit. You will be redirected to /organizations/{orgId}/welcome?firstTime=1.
  4. Navigate to the subscription page at /organizations/{orgId}/subscription.
  5. Observe the "No Active Subscription" state. Click the upgrade CTA (or trigger the upgrade dialog from a trial banner).
  6. In the Upgrade Dialog:
    • Verify the billing cycle toggle defaults to Annual.
    • Click the Monthly toggle button.
    • Verify the "Save 17%" badge becomes faded/dim.
    • Select the Teams plan card.
    • Verify the price shown is $18/seat/month.
    • Set seat count to 3.
    • Verify the button reads "Purchase Teams Plan".
  7. Click "Purchase Teams Plan". You should be redirected to Stripe Checkout.
  8. On Stripe Checkout:
    • Verify the line item shows 3 seats at $18/seat/month = $54/month.
    • Verify billing address collection is required.
    • Complete payment with test card.
  9. After successful payment, you are redirected back to the app.

End-state validation:

Check Expected
UI: Subscription page Status badge: "Active". Billing Cycle: "Monthly". Seats Used: 1/3. Next Payment: $54.00. Days Until Renewal: ~30.
UI: Quick Actions "Change Seats", "Switch to Annual", "Update Payment Method", "View Payment History", "Cancel Subscription" all visible.
DB: organizations table plan = 'teams', seat_count = 3.
DB: organization_seats_purchases One row: subscription_status = 'active', seat_count = 3, billing_cycle = 'monthly', amount_usd = 5400 (cents).
Stripe Dashboard Subscription active, interval = month, price = Teams Monthly price ID, quantity = 3. Metadata: type=seats, planType=teams.

Journey A2: New Org → Teams Annual Subscription

Pre-setup: None (fresh user).

Steps:

  1. Log in as a new fake user.
  2. Create a new organization via /organizations/new.
  3. Navigate to /organizations/{orgId}/subscription and trigger the upgrade dialog.
  4. In the Upgrade Dialog:
    • Leave the billing cycle toggle on Annual (default).
    • Select the Teams plan card.
    • Verify the price shown is $15/seat/month (billed as $180/seat/year).
    • Set seat count to 5.
  5. Click "Purchase Teams Plan" and complete Stripe Checkout.
    • Verify the Stripe checkout shows 5 seats at $180/seat/year = $900/year.

End-state validation:

Check Expected
UI: Subscription page Billing Cycle: "Yearly". Seats Used: 1/5. Next Payment reflects annual amount.
UI: Quick Actions "Switch to Monthly" button visible.
DB: organizations plan = 'teams', seat_count = 5.
DB: organization_seats_purchases billing_cycle = 'yearly', seat_count = 5, amount_usd = 90000.
Stripe Subscription interval = year, Teams Annual price ID.

Journey A3: Teams Monthly → Switch to Annual Billing

Pre-setup: Complete Journey A1 (active Teams Monthly subscription with 3 seats).

Steps:

  1. Navigate to /organizations/{orgId}/subscription.
  2. In Quick Actions, click "Switch to Annual".
  3. In the Billing Cycle Change Dialog:
    • Verify "Now" row shows: Monthly billing — $18/seat/month.
    • Verify "New" row shows: Annual billing — $15/seat/month.
    • Verify total cost comparison: $54/mo$45/mo (billed as $540/yr).
    • Verify the effective date annotation shows the current period end date.
    • No amber "cost increase" warning should appear (switching to annual is a discount).
  4. Click "Switch to Annual". Observe success toast.

End-state validation:

Check Expected
UI: Subscription page Billing Cycle shows: "Monthly → Yearly". Amber banner: "Switching to Annual billing on {date}." with a "Cancel Change" button. Next Payment shows "Changes at renewal".
UI: Quick Actions "Switch to Annual/Monthly" button is hidden (pending schedule blocks further changes).
Stripe A subscription schedule exists with 2 phases. Phase 1: current monthly price. Phase 2: Teams Annual price, starting at current period end.

Journey A4: Teams Annual → Switch to Monthly Billing

Pre-setup: Complete Journey A2 (active Teams Annual subscription with 5 seats).

Steps:

  1. Navigate to /organizations/{orgId}/subscription.
  2. In Quick Actions, click "Switch to Monthly".
  3. In the Billing Cycle Change Dialog:
    • Verify "Now" row: Annual billing — $15/seat/month.
    • Verify "New" row: Monthly billing — $18/seat/month.
    • Verify total: $75/mo$90/mo.
    • Verify the amber cost warning appears: "Switching to monthly billing will increase your per-seat cost from $15 to $18 per month."
  4. Click "Switch to Monthly". Observe success toast.

End-state validation:

Check Expected
UI Billing Cycle: "Yearly → Monthly". Amber pending-change banner visible.
Stripe Schedule phase 2 uses Teams Monthly price ID.

Journey A5: Cancel Pending Billing Cycle Change (Teams)

Pre-setup: Complete Journey A3 or A4 (pending billing cycle change exists).

Steps:

  1. On the subscription page, locate the amber banner: "Switching to {target} billing on {date}."
  2. Click "Cancel Change".
  3. Observe success toast.

End-state validation:

Check Expected
UI Amber banner gone. Billing Cycle reverts to original value (no arrow). "Switch to Annual/Monthly" button reappears in Quick Actions.
Stripe Subscription schedule released (status = canceled or removed).

Journey A6: Teams Monthly → Cancel → Resubscribe (Preserves Monthly)

Pre-setup: Active Teams Monthly subscription (e.g., from Journey A1).

Steps:

  1. On subscription page, click "Cancel Subscription" in Quick Actions.
  2. In the cancel modal, confirm cancellation.
  3. Observe: amber banner "Subscription will be canceled" with "Stop Pending Cancellation" button. Status still "Active".
  4. Wait for the subscription to reach its period end (or use Stripe dashboard to advance the test clock). Once ended:
    • Page shows red banner: "Subscription has been canceled" with "Resubscribe" button.
  5. Click "Resubscribe". You should be redirected to Stripe Checkout.
  6. On Stripe Checkout, verify:
    • The billing interval is monthly (preserved from previous subscription).
    • Seat count matches the previous paid seat quantity.

End-state validation:

Check Expected
Stripe Checkout Monthly billing pre-selected, correct seat count from ended subscription.
After completing checkout: DB New organization_seats_purchases row with billing_cycle = 'monthly'. Organization seat_count restored.

Journey A7: Teams Monthly → Mid-Subscription Seat Increase

Pre-setup: Active Teams Monthly subscription with 3 seats.

Steps:

  1. On subscription page, click "Change Seats" in Quick Actions.
  2. In the Seat Change Modal:
    • Current seat count shown: 3.
    • Increase to 5.
    • Verify the prorated amount preview.
  3. Confirm the change.

End-state validation:

Check Expected
UI Seats Used: 1/5. Next Payment updated to reflect 5 seats.
DB: organizations seat_count = 5.
DB: organization_seats_purchases New row with seat_count = 5, billing_cycle = 'monthly'.
Stripe Prorated invoice generated. Subscription quantity = 5.

Journey A8: Teams Monthly → Mid-Subscription Seat Decrease

Pre-setup: Active Teams Monthly subscription with 5 seats, 2 seats in use.

Steps:

  1. Click "Change Seats" → decrease to 3.
  2. Confirm the change.

End-state validation:

Check Expected
UI Seats Used: 2/5 (NOT 2/3 — downgrade deferred to next period). Seat Count Change Notification appears: "Next billing cycle your total seats will drop from 5 to 3".
DB: organizations seat_count = 5 (unchanged until next period).
Stripe Subscription quantity = 3 with proration_behavior: none. Change effective at period end.

Part B: Enterprise Subscription Paths

Journey B1: New Org → Enterprise Monthly Subscription

Pre-setup: None (fresh user).

Steps:

  1. Log in as a new fake user and create a new organization.
  2. Navigate to /organizations/{orgId}/subscription and trigger the upgrade dialog.
  3. In the Upgrade Dialog:
    • Click the Monthly toggle.
    • The Enterprise plan card should already be selected (org starts on Enterprise plan, so currentPlan = enterprise and the dialog initializes selectedPlan to currentPlan).
    • Verify the price shown is $72/seat/month.
    • Set seat count to 2.
  4. Click "Purchase Enterprise Plan" and complete Stripe Checkout.
    • Verify Stripe shows: 2 seats at $72/seat/month = $144/month.

End-state validation:

Check Expected
UI: Subscription page Status: "Active". Billing Cycle: "Monthly". Seats Used: 1/2. Next Payment: $144.00.
UI: Quick Actions "Switch to Annual" visible.
DB: organizations plan = 'enterprise', seat_count = 2.
DB: organization_seats_purchases billing_cycle = 'monthly', seat_count = 2, amount_usd = 14400.
Stripe Interval = month, Enterprise Monthly price ID, quantity = 2.

Journey B2: New Org → Enterprise Annual Subscription

Pre-setup: None (fresh user).

Steps:

  1. Log in, create org, trigger upgrade dialog.
  2. Leave billing cycle on Annual (default).
  3. Enterprise plan should be pre-selected.
  4. Verify price: $60/seat/month (billed as $720/seat/year).
  5. Set seat count to 10.
  6. Complete checkout.
    • Verify Stripe shows: 10 seats at $720/seat/year = $7,200/year.

End-state validation:

Check Expected
UI Billing Cycle: "Yearly". Seats Used: 1/10.
DB plan = 'enterprise', seat_count = 10, purchase billing_cycle = 'yearly', amount_usd = 720000.

Journey B3: Enterprise Monthly → Switch to Annual Billing

Pre-setup: Active Enterprise Monthly subscription (e.g., Journey B1, 2 seats).

Steps:

  1. Click "Switch to Annual" in Quick Actions.
  2. In dialog:
    • "Now": Monthly — $72/seat/month
    • "New": Annual — $60/seat/month
    • Total: $144/mo$120/mo (billed $1,440/yr)
    • No cost-increase warning.
  3. Confirm switch.

End-state validation:

Check Expected
UI Billing Cycle: "Monthly → Yearly". Pending change banner with effective date.
Stripe Schedule phase 2: Enterprise Annual price ID.

Journey B4: Enterprise Annual → Switch to Monthly Billing

Pre-setup: Active Enterprise Annual subscription (Journey B2, 10 seats).

Steps:

  1. Click "Switch to Monthly".
  2. In dialog:
    • "Now": Annual — $60/seat/month
    • "New": Monthly — $72/seat/month
    • Total: $600/mo$720/mo
    • Amber warning: "Switching to monthly billing will increase your per-seat cost from $60 to $72 per month."
  3. Confirm.

End-state validation:

Check Expected
UI Billing Cycle: "Yearly → Monthly". Pending change banner.
Stripe Schedule phase 2: Enterprise Monthly price ID.

Journey B5: Cancel Pending Billing Cycle Change (Enterprise)

Pre-setup: Complete Journey B3 or B4.

Identical steps and validation as Journey A5 but for Enterprise pricing.


Journey B6: Enterprise Monthly → Cancel → Resubscribe (Preserves Monthly)

Pre-setup: Active Enterprise Monthly subscription.

Steps:

  1. Cancel subscription via Quick Actions.
  2. After subscription ends (advance test clock or wait), click "Resubscribe".
  3. Verify Stripe Checkout defaults to monthly billing and previous paid seat count.
  4. Complete checkout.

End-state validation:

Check Expected
DB New purchase with billing_cycle = 'monthly'. Org plan = 'enterprise', seat_count restored.

Journey B7: Enterprise Monthly → Mid-Subscription Seat Increase

Pre-setup: Active Enterprise Monthly, 2 seats.

Steps:

  1. "Change Seats" → increase to 5.
  2. Confirm.

End-state validation:

Check Expected
UI Seats Used: 1/5.
DB seat_count = 5. New purchase row.
Stripe Prorated invoice for 3 additional seats at $72/seat/month.

Journey B8: Enterprise Monthly → Mid-Subscription Seat Decrease

Pre-setup: Active Enterprise Monthly, 5 seats, 2 in use.

Steps:

  1. "Change Seats" → decrease to 3.
  2. Confirm.

End-state validation:

Check Expected
UI Seats still 2/5. "Seat Count Change Scheduled" notification: "Next billing cycle your total seats will drop from 5 to 3".
Stripe Quantity = 3, no proration.

Part C: Cross-Plan Journeys (Monthly-Specific)

Journey C1: Enterprise Trial → Choose Teams Monthly at Checkout

Pre-setup: New org (starts on Enterprise trial).

Steps:

  1. Trigger upgrade dialog.
  2. Select Monthly toggle, then select the Teams plan card.
  3. Note the plan card should show "Downgrade" badge on Teams (since current plan is Enterprise).
  4. Verify price is $18/seat/month.
  5. Complete checkout.

End-state validation:

Check Expected
DB: organizations plan = 'teams' (changed from enterprise).
Stripe metadata planType = 'teams'.
UI Enterprise-only settings (model deny lists, provider deny lists) no longer visible in org settings.

Journey C2: Upgrade Dialog Defaults — Verify Resubscribe Seeding

Pre-setup: Org that previously had a Teams Monthly subscription (now ended) with 5 paid seats.

Steps:

  1. Trigger upgrade dialog.
  2. Verify:
    • Billing cycle toggle is pre-set to Monthly (seeded from last ended subscription).
    • Seat count is pre-filled with max(previousPaidSeats, currentUsedSeats).

End-state validation: Visual confirmation only — the dialog correctly seeds from getResubscribeDefaults.


Part D: Access Control Verification (Applies to Both Plans)

Journey D1: Billing Manager Can Manage Monthly Subscription

Pre-setup: Active monthly subscription. Invite a user with billing_manager role.

Steps:

  1. Log in as the billing manager.
  2. Navigate to /organizations/{orgId}/subscription.
  3. Verify Quick Actions are visible: Change Seats, Switch to Annual/Monthly, Cancel Subscription.
  4. Verify the billing manager does NOT count toward seat usage.

Journey D2: Regular Member Cannot Manage Subscription

Pre-setup: Active monthly subscription. Invite a user with member role.

Steps:

  1. Log in as the member.
  2. Navigate to /organizations/{orgId}/subscription.
  3. Verify: Can see subscription status and seat usage, but Quick Actions (Change Seats, Cancel, Change Billing Cycle) are NOT available.

Database Queries for Validation

-- Check organization plan, seat count
SELECT id, name, plan, seat_count, require_seats, free_trial_end_at
FROM organizations WHERE id = '<org_id>';

-- Check seat purchases
SELECT id, subscription_stripe_id, seat_count, amount_usd,
       subscription_status, billing_cycle, starts_at, expires_at
FROM organization_seats_purchases
WHERE organization_id = '<org_id>'
ORDER BY created_at DESC;

-- Check seat usage (members excluding billing managers)
SELECT u.email, om.role
FROM organization_memberships om
JOIN users u ON u.id = om.user_id
WHERE om.organization_id = '<org_id>'
  AND om.removed_at IS NULL;

Summary Matrix

Journey Plan Billing Cycle Action
A1 Teams Monthly New subscription
A2 Teams Annual New subscription
A3 Teams Monthly → Annual Billing cycle change
A4 Teams Annual → Monthly Billing cycle change
A5 Teams Any Cancel pending cycle change
A6 Teams Monthly Cancel → Resubscribe
A7 Teams Monthly Mid-sub seat increase
A8 Teams Monthly Mid-sub seat decrease
B1 Enterprise Monthly New subscription
B2 Enterprise Annual New subscription
B3 Enterprise Monthly → Annual Billing cycle change
B4 Enterprise Annual → Monthly Billing cycle change
B5 Enterprise Any Cancel pending cycle change
B6 Enterprise Monthly Cancel → Resubscribe
B7 Enterprise Monthly Mid-sub seat increase
B8 Enterprise Monthly Mid-sub seat decrease
C1 Enterprise → Teams Monthly Plan downgrade at checkout
C2 Any Monthly Resubscribe default seeding
D1 Any Monthly Billing manager access
D2 Any Monthly Member access denied

Visual Changes

Recording: https://cln.sh/WXNR7wNZ

Reviewer Notes

  • Legacy price constant exports are preserved as derived values from SEAT_PRICING for backward compatibility — no external consumers need updating
  • organizationBillingProcedure rename is purely cosmetic (same ['owner', 'billing_manager'] role check) but touches 14 router files
  • The require_seats DB default change (migration 0064) only affects code paths that omit the column on insert; all production insert paths explicitly set the value
  • toBillingCycle accepts the union of DB ('yearly') and Stripe ('year') values; old function names are retained as typed aliases

@jeanduplessis jeanduplessis force-pushed the team-enterprise-monthly-plan branch from edd2aef to d6b2084 Compare March 28, 2026 18:17
@jeanduplessis jeanduplessis changed the title docs: add monthly billing UI prototype and implementation plan feat(billing): add monthly billing cycle for Team and Enterprise seats Mar 28, 2026
@jeanduplessis jeanduplessis marked this pull request as ready for review March 28, 2026 18:37
Comment thread src/components/organizations/subscription/SubscriptionOverviewCard.tsx Outdated
Comment thread src/routers/organizations/organization-subscription-router.ts
@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot Bot commented Mar 28, 2026

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 4
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
src/routers/organizations/organization-members-router.ts 49 Billing managers can still invite/remove members and promote accounts to owner.

WARNING

File Line Issue
src/routers/organizations/organization-subscription-router.ts 93 Ended subscriptions still surface an Update Payment Method action that the portal endpoint rejects.
src/lib/organizations/organization-seats.ts 199 Stripe lookup failures still fail open in the duplicate-subscription guard.
src/lib/stripe.ts 1293 The cancel fallback still releases an attached schedule after any cancel error, so transient Stripe failures can drop a pending billing-cycle change.
src/routers/organizations/organization-settings-router.ts 187 Billing managers can still edit non-billing organization settings.
Other Observations (not in diff)

None.

Files Reviewed (52 files)
  • src/routers/organizations/organization-members-router.ts - 1 issue persists
  • src/routers/organizations/organization-subscription-router.ts - 1 issue persists
  • src/routers/organizations/organization-settings-router.ts - 1 issue persists
  • src/lib/organizations/organization-seats.ts - 1 issue persists
  • src/lib/stripe.ts - 1 issue persists
  • 47 additional changed non-generated files reviewed - no new issues

Fix these issues in Kilo Cloud


Reviewed by gpt-5.4-20260305 · 5,572,415 tokens

Comment thread src/components/organizations/subscription/SubscriptionOverviewCard.tsx Outdated
Comment thread src/routers/organizations/organization-members-router.ts
Comment thread src/routers/organizations/organization-settings-router.ts
Comment thread src/lib/organizations/organization-seats.ts
Comment thread src/components/organizations/UpgradeTrialDialog.tsx Outdated
Comment thread src/routers/auto-fix/auto-fix-router.ts Outdated
Comment thread src/routers/auto-triage/auto-triage-router.ts Outdated
Comment thread src/lib/stripe.ts
Comment thread src/components/organizations/UpgradeTrialDialog.tsx Outdated
Comment thread src/components/organizations/UpgradeTrialDialog.tsx
Comment thread src/components/organizations/UpgradeTrialDialog.tsx
Comment thread src/routers/organizations/organization-subscription-router.ts
…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
…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
…, 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
…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.
… dynamic plan label

Move metadata from subscriptionSchedules.create() to .update() to comply
with Stripe's from_subscription constraint. Allow ended subscriptions to
be fetched so the resubscribe UI renders. Dynamically set plan title
based on org plan (Enterprise vs Teams). Polish effective-date fallback
copy and button styles.
@jeanduplessis jeanduplessis force-pushed the team-enterprise-monthly-plan branch from ef26a41 to 9f9025c Compare March 30, 2026 14:10
@jeanduplessis jeanduplessis requested a review from jrf0110 March 30, 2026 14:39
@jrf0110
Copy link
Copy Markdown
Contributor

jrf0110 commented Mar 30, 2026

How has this been tested? I don't see any new products in test mode in stripe

Comment thread src/lib/config.server.ts
@jeanduplessis jeanduplessis merged commit 4a2fddb into main Mar 31, 2026
33 checks passed
@jeanduplessis jeanduplessis deleted the team-enterprise-monthly-plan branch March 31, 2026 12:05
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.

3 participants