fix(billing): filter non-seat products from seat count calculation#1060
fix(billing): filter non-seat products from seat count calculation#1060kilo-code-bot[bot] wants to merge 2 commits intomainfrom
Conversation
Seat counting was summing quantities from ALL subscription line items, including non-seat products like KiloPass and KiloClaw. This caused inflated seat counts when subscriptions contained mixed product types. Filter line items by product ID (teams/enterprise) before summing quantities and amounts so only seat products contribute to the count.
| // When a subscription has multiple prices for Kilo Teams (e.g., paid seats at one price | ||
| // and free seats at another), Stripe stores them as separate line items. | ||
| const seatCount = lineItems.reduce((total, item) => total + (item.quantity ?? 0), 0); | ||
| const seatLineItems = lineItems.filter(isSeatLineItem); |
There was a problem hiding this comment.
WARNING: Billing period still comes from the unfiltered line items
After filtering out non-seat products here, starts_at and expires_at are still derived from lineItems[0] below. If a non-seat add-on is listed first, the purchase record can be written with the wrong period, which then breaks the later max starts_at logic and can apply an outdated seat count. Read the billing period from the filtered seat line items instead.
Code Review SummaryStatus: 1 Issue Found | Recommendation: Address before merge Fix these issues in Kilo Cloud Overview
Issue Details (click to expand)No new inline-commentable issues. Other Observations (not in diff)Issues found in unchanged code that cannot receive inline comments:
Files Reviewed (2 files)
Reviewed by gpt-5.4-20260305 · 369,158 tokens |
…red items The billing period (starts_at/expires_at) was still derived from lineItems[0] after seat filtering was introduced. If a non-seat add-on appeared first in the list, the purchase record could be written with the wrong period, breaking the max-starts_at seat-count selection logic. Now reads current_period_start/end from the first filtered seat line item instead.
Review follow-upProduct ID question (item 1)Confirmed: comparing against product IDs is correct. The Stripe API returns Reference: Stripe SubscriptionItem API docs — Billing period fix (item 2)Addressed the review comment: Fix (5d21daa): Moved the |
Summary
handleSubscriptionEventInternalwas summing quantities from ALL Stripe subscription line items, including non-seat products (KiloPass, KiloClaw, add-ons). This inflated seat counts when subscriptions contained mixed product types.isSeatLineItemfilter that checks each line item'sprice.productagainst the known seat product IDs (STRIPE_TEAMS_SUBSCRIPTION_PRODUCT_IDandSTRIPE_ENTERPRISE_SUBSCRIPTION_PRODUCT_ID). BothseatCountandamountUsdnow only aggregate from seat-related line items.Verification
pnpm typecheck— passes with no errorshandleSubscriptionEventandhandleSubscriptionEventInternalto confirm no downstream impactVisual Changes
N/A
Reviewer Notes
SEAT_PRODUCT_IDSset is built at module load time from env vars, with.filter(Boolean)to handle cases where an env var might be empty.isSeatLineItemcheckstypeof productId !== 'string'to handle the case where Stripe returns an expandedProductobject rather than a string ID — in that case, the item is conservatively excluded. In practice, subscription items always have string product IDs unless explicitly expanded.firstLineItem(used for period start/end dates) still uses the first item from ALL line items, not just seat items. This is intentional — all items in a subscription share the same billing period.