Skip to content

feat(licenses): add batched child license transitions#2303

Open
johnyeocx wants to merge 2 commits into
license-update-childfrom
license-batch-transitions
Open

feat(licenses): add batched child license transitions#2303
johnyeocx wants to merge 2 commits into
license-update-childfrom
license-batch-transitions

Conversation

@johnyeocx

@johnyeocx johnyeocx commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • compute child product, entitlement, and base-price transitions
  • execute transitions asynchronously with bounded batch orchestration and structured logs
  • add immediate/scheduled integration coverage plus focused unit and benchmark coverage

Stack

Verification

  • server typecheck
  • Vite typecheck
  • immediate non-consumable assignment transition integration test
  • scheduled non-consumable assignment transition integration test

Summary by cubic

Adds batched child license transitions to scale seat, base price, and entitlement repoints during plan swaps. Fixes assignment entitlement cycle alignment to keep Stripe cycles and pool resets in sync.

  • New Features

    • Compute and execute batched child license transitions (customer product, base price, entitlements) with bounded orchestration and statement timeouts, and align assignment entitlement cycles during transitions.
    • Validate limits (active assignments, distinct base prices/entitlements) and list distinct definitions to prevent oversized or ambiguous transitions.
    • Add batch-transition task via @trigger.dev/sdk and new logging helpers (logContextExtras) for clear, structured logs.
  • Refactors

    • Unify license quantity setup for attach/update; omitted quantities carry paid seats to 1:1 successors.
    • Replace the per-seat entitlement repoint trigger with a batched executor; remove repointSeatEntitlementsTask and simplify attach/update error handling and license transition checks.
    • Align Stripe item updates, previews, and scheduled activation flow with immediate/scheduled transitions; improve line item building using projected definitions and unused prepaid computation.

Written for commit 67845cb. Summary will update on new commits.

Review in cubic

@johnyeocx
johnyeocx requested a review from ay-rod as a code owner July 20, 2026 11:00
@entelligence-ai-pr-reviews

Copy link
Copy Markdown
Contributor

Automatic Review Skipped

Too many files for automatic review.

If you would still like a review, you can trigger one manually by commenting:

@entelligence review

@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (115 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
checkout Ignored Ignored Jul 20, 2026 11:37am
landing-page Ignored Ignored Jul 20, 2026 11:37am

Request Review

@capy-ai

capy-ai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

15 issues found across 115 files

Confidence score: 2/5

  • server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionComputeErrors.ts and .../handleUpdateSubscriptionLicenseErrors.ts can let an update commit while child transitions later fail (including negative remaining seats), so users may receive a success response for an invalid subscription state. Reintroduce base-price/assignment-limit and license-pool transition guards in preflight and ensure the request fails atomically before merging.
  • shared/utils/productUtils/entUtils/findEntitlement/findEntitlementSuccessor.ts plus server/src/internal/billing/v2/actions/batchTransition/compute/transitions/computeEntitlementPriceTransitions.ts make successor selection order-dependent and can treat unrelated fixed-price features as replacements, which can apply incorrect grant/usage and pricing transitions. Tighten matching to deterministic definition+interval criteria (with no broad price-identity fallback) before merging.
  • server/src/internal/billing/v2/actions/batchTransition/compute/operations/computeBatchTransitionOperations.ts, .../computeEntitlementPriceOperations.ts, and server/src/internal/billing/v2/compute/customerLicenseTransitions/transitionLicenseBillingPriceRows.ts can drop paid additions/removals, crash on in-place definition changes, and misstate projected quantities when included seats change. Add execution paths for unhandled.added/unhandled.deleted, fix the same-id branch, and recalculate quantities from target definitions before merging.
  • server/src/internal/billing/v2/actions/batchTransition/errors/validateCustomerEntitlementBatchTransitions.ts and server/tests/perf/batchTransitionBench.ts introduce operational risk via unbounded per-transition queries and brittle cleanup that can leave temp schema/connections open on errors. Add bounded/chunked validation concurrency and make cleanup resilient with nested try/finally so teardown always runs before merging.
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="server/src/internal/billing/v2/actions/batchTransition/compute/transitions/computeEntitlementPriceTransitions.ts">

<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/compute/transitions/computeEntitlementPriceTransitions.ts:39">
P1: Unrelated fixed-price features can be classified as a replacement instead of one deletion plus one addition: this call allows the successor helper's price-identity fallback to match any same-interval fixed price before reaching feature matching. That can repoint child assignments and carry usage from one feature to another; requiring a matching entitlement feature for the price-identity fallback (or excluding fixed prices from it) would keep unrelated entitlements separate.</violation>
</file>

<file name="server/src/internal/billing/v2/actions/batchTransition/errors/enforceDistinctEntitlementLimit.ts">

<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/errors/enforceDistinctEntitlementLimit.ts:4">
P3: The entitlement limit guard duplicates `enforceDistinctBasePriceLimit` almost verbatim, so fixes to shared limit behavior can be applied to one helper and missed in the other. A parameterized shared limit-enforcement helper would keep these checks consistent while preserving resource-specific messages.</violation>
</file>

<file name="server/src/internal/billing/v2/execute/addStripeSubscriptionIdToBillingPlan.ts">

<violation number="1" location="server/src/internal/billing/v2/execute/addStripeSubscriptionIdToBillingPlan.ts:31">
P2: Free patched products can now be persisted with this Stripe subscription ID even though they are not billed by it. Applying the same paid-recurring/paid-license eligibility check used for `insertCustomerProducts` would prevent later billing and subscription-link lookups from treating the free product as Stripe-backed.</violation>
</file>

<file name="server/src/internal/billing/v2/actions/batchTransition/errors/validateCustomerEntitlementBatchTransitions.ts">

<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/errors/validateCustomerEntitlementBatchTransitions.ts:15">
P2: Large license pools can make validation submit one database query per child transition at once, exhausting the connection pool and adding avoidable load before the billing request proceeds. A bounded limiter or chunked validation would keep this preflight within the same concurrency budget as batch execution.</violation>
</file>

<file name="server/src/internal/billing/v2/actions/batchTransition/compute/transitions/computeProductTransitions.ts">

<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/compute/transitions/computeProductTransitions.ts:19">
P3: `ProductTransitions.toProduct` is never consumed by any caller, so this new field adds dead data and obscures which transition outputs are actually used. Removing the field and its object property would keep the result contract focused on the computed transitions.</violation>
</file>

<file name="shared/utils/productUtils/entUtils/findEntitlement/findEntitlementSuccessor.ts">

<violation number="1" location="shared/utils/productUtils/entUtils/findEntitlement/findEntitlementSuccessor.ts:65">
P1: When multiple target entitlements share a feature but neither matches at definition or interval precision, this returns an arbitrary target based on array order, which can apply the wrong grant/usage transition. Requiring exactly one match at each precision (or otherwise disambiguating candidates) would avoid silently guessing.</violation>

<violation number="2" location="shared/utils/productUtils/entUtils/findEntitlement/findEntitlementSuccessor.ts:74">
P1: A feature-only match for an earlier source can claim the candidate that is an exact interval/definition successor for a later source, making the result dependent on source ordering and producing incorrect entitlement transitions. Resolving all definition/interval matches before allowing feature fallbacks would preserve the strongest global mapping.</violation>
</file>

<file name="server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionComputeErrors.ts">

<violation number="1" location="server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionComputeErrors.ts:14">
P1: An update can commit and return successfully while its child transition later fails for the base-price or assignment limit, because this compute preflight validates only entitlement counts. The base-price and assignment limits would be safer to validate before the license state is carried and the async task is queued.</violation>
</file>

<file name="server/src/internal/billing/v2/compute/customerLicenseTransitions/computeCustomerLicenseTransitions.ts">

<violation number="1" location="server/src/internal/billing/v2/compute/customerLicenseTransitions/computeCustomerLicenseTransitions.ts:30">
P2: A same-product plan-license definition change can now be treated as a no-op when counters and product prices are unchanged, so the incoming pool keeps the old `plan_license_id` and `repointDefinition` is never executed. Including the outgoing/incoming plan-license identity in the no-op check would preserve these definition transitions.</violation>
</file>

<file name="server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionLicenseErrors.ts">

<violation number="1" location="server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionLicenseErrors.ts:105">
P1: Multi-update can now execute a plan restructure that carries a license pool with negative remaining seats instead of rejecting the request. The removed guard was the only transition validation on the `handleMultiUpdateErrors` path; keeping this validation in a shared path or invoking compute-error validation for multi-update plans would preserve the active-assignment safeguard.</violation>
</file>

<file name="server/src/internal/billing/v2/compute/customerLicenseTransitions/transitionLicenseBillingPriceRows.ts">

<violation number="1" location="server/src/internal/billing/v2/compute/customerLicenseTransitions/transitionLicenseBillingPriceRows.ts:76">
P1: Transitions with a changed included-seat count misstate projected billing quantities when the base price is replaced: the rows retain quantities calculated under the outgoing definition, while `targetQuantity` only affects the add branch. The projection should reconcile the quantity against the incoming included/paid quantity (or preserve enough seat-level information to recompute it) for replace transitions as well.</violation>
</file>

<file name="server/src/internal/billing/v2/actions/batchTransition/compute/operations/computeBatchTransitionOperations.ts">

<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/compute/operations/computeBatchTransitionOperations.ts:47">
P1: Paid entitlement additions and removals are silently dropped here: only `unhandled.transitions` is converted, while `unhandled.added` and `unhandled.deleted` have no execution path. The batch transition should preserve and route these cases to the appropriate fallback/mutation instead of returning a transition that omits them.</violation>
</file>

<file name="server/src/internal/billing/v2/actions/batchTransition/compute/operations/entitlementPriceOperations/computeEntitlementPriceOperations.ts">

<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/compute/operations/entitlementPriceOperations/computeEntitlementPriceOperations.ts:56">
P1: An in-place entitlement-definition change can crash the batch transition instead of applying the balance/unlimited patch: when `fromEntitlement.id === toEntitlement.id` but `entsAreSame` is false, this condition retains that ID and creates a replacement from an ID to itself. The operation should handle same-ID definition changes as an in-place update (or normalize them before constructing a replacement).</violation>
</file>

<file name="server/tests/unit/billing/setup-customer-license-quantity-context.test.ts">

<violation number="1" location="server/tests/unit/billing/setup-customer-license-quantity-context.test.ts:56">
P2: No test coverage for: empty outgoing/incoming licenses (zero matches), absent `fullProduct`/`customerProduct` (falls through to return-only-explicit branch), or multiple outgoing licenses with partial matching. These edge cases are exercised by the production branches but uncovered here.</violation>
</file>

<file name="server/tests/perf/batchTransitionBench.ts">

<violation number="1" location="server/tests/perf/batchTransitionBench.ts:671">
P0: Observation: the finally block can allow a thrown error from `client.end()` to skip the DROP SCHEMA and `bootstrapClient.end()` calls, which can leave a temporary schema and the bootstrap connection open after the benchmark runs. Cause: `client.end()` is invoked in the same finally block before the schema cleanup and bootstrap shutdown, so any exception from ending the client aborts the rest of the cleanup. Recommendation: ensure schema teardown and bootstrap connection shutdown run even if `client.end()` fails. Do this by performing the DROP while the client is still usable (before ending it), and wrapping each termination step in its own try/catch so one failure doesn't prevent the rest. You can also use Promise.allSettled or sequential try/catch blocks to aggregate/log errors while ensuring best-effort cleanup.</violation>
</file>

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

Re-trigger cubic

results.push(result);
console.table([result]);
}
} finally {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P0: Observation: the finally block can allow a thrown error from client.end() to skip the DROP SCHEMA and bootstrapClient.end() calls, which can leave a temporary schema and the bootstrap connection open after the benchmark runs. Cause: client.end() is invoked in the same finally block before the schema cleanup and bootstrap shutdown, so any exception from ending the client aborts the rest of the cleanup. Recommendation: ensure schema teardown and bootstrap connection shutdown run even if client.end() fails. Do this by performing the DROP while the client is still usable (before ending it), and wrapping each termination step in its own try/catch so one failure doesn't prevent the rest. You can also use Promise.allSettled or sequential try/catch blocks to aggregate/log errors while ensuring best-effort cleanup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/tests/perf/batchTransitionBench.ts, line 671:

<comment>Observation: the finally block can allow a thrown error from `client.end()` to skip the DROP SCHEMA and `bootstrapClient.end()` calls, which can leave a temporary schema and the bootstrap connection open after the benchmark runs. Cause: `client.end()` is invoked in the same finally block before the schema cleanup and bootstrap shutdown, so any exception from ending the client aborts the rest of the cleanup. Recommendation: ensure schema teardown and bootstrap connection shutdown run even if `client.end()` fails. Do this by performing the DROP while the client is still usable (before ending it), and wrapping each termination step in its own try/catch so one failure doesn't prevent the rest. You can also use Promise.allSettled or sequential try/catch blocks to aggregate/log errors while ensuring best-effort cleanup.</comment>

<file context>
@@ -0,0 +1,679 @@
+		results.push(result);
+		console.table([result]);
+	}
+} finally {
+	await client.end();
+	await bootstrapDb.execute(
</file context>

});

for (const fromEntitlementPrice of fromEntitlementPrices) {
const toEntitlementPrice = findEntitlementPriceSuccessor({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Unrelated fixed-price features can be classified as a replacement instead of one deletion plus one addition: this call allows the successor helper's price-identity fallback to match any same-interval fixed price before reaching feature matching. That can repoint child assignments and carry usage from one feature to another; requiring a matching entitlement feature for the price-identity fallback (or excluding fixed prices from it) would keep unrelated entitlements separate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/billing/v2/actions/batchTransition/compute/transitions/computeEntitlementPriceTransitions.ts, line 39:

<comment>Unrelated fixed-price features can be classified as a replacement instead of one deletion plus one addition: this call allows the successor helper's price-identity fallback to match any same-interval fixed price before reaching feature matching. That can repoint child assignments and carry usage from one feature to another; requiring a matching entitlement feature for the price-identity fallback (or excluding fixed prices from it) would keep unrelated entitlements separate.</comment>

<file context>
@@ -0,0 +1,72 @@
+	});
+
+	for (const fromEntitlementPrice of fromEntitlementPrices) {
+		const toEntitlementPrice = findEntitlementPriceSuccessor({
+			sourceEntitlementPrice: fromEntitlementPrice,
+			candidateEntitlementPrices: toEntitlementPrices,
</file context>

Comment on lines +65 to +74
const candidate = candidateEntitlements.find(
(candidateEntitlement) =>
!excludedEntitlementIds?.has(candidateEntitlement.id) &&
entitlementsMatchAtPrecision({
sourceEntitlement,
candidateEntitlement,
matchPrecision: currentMatchPrecision,
}),
);
if (candidate) return candidate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: When multiple target entitlements share a feature but neither matches at definition or interval precision, this returns an arbitrary target based on array order, which can apply the wrong grant/usage transition. Requiring exactly one match at each precision (or otherwise disambiguating candidates) would avoid silently guessing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At shared/utils/productUtils/entUtils/findEntitlement/findEntitlementSuccessor.ts, line 65:

<comment>When multiple target entitlements share a feature but neither matches at definition or interval precision, this returns an arbitrary target based on array order, which can apply the wrong grant/usage transition. Requiring exactly one match at each precision (or otherwise disambiguating candidates) would avoid silently guessing.</comment>

<file context>
@@ -1,31 +1,78 @@
+		: ENTITLEMENT_MATCH_PRECISIONS;
+
+	for (const currentMatchPrecision of matchPrecisions) {
+		const candidate = candidateEntitlements.find(
+			(candidateEntitlement) =>
+				!excludedEntitlementIds?.has(candidateEntitlement.id) &&
</file context>
Suggested change
const candidate = candidateEntitlements.find(
(candidateEntitlement) =>
!excludedEntitlementIds?.has(candidateEntitlement.id) &&
entitlementsMatchAtPrecision({
sourceEntitlement,
candidateEntitlement,
matchPrecision: currentMatchPrecision,
}),
);
if (candidate) return candidate;
const matches = candidateEntitlements.filter(
(candidateEntitlement) =>
!excludedEntitlementIds?.has(candidateEntitlement.id) &&
entitlementsMatchAtPrecision({
sourceEntitlement,
candidateEntitlement,
matchPrecision: currentMatchPrecision,
}),
);
if (matches.length === 1) return matches[0];

matchPrecision: currentMatchPrecision,
}),
);
if (candidate) return candidate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: A feature-only match for an earlier source can claim the candidate that is an exact interval/definition successor for a later source, making the result dependent on source ordering and producing incorrect entitlement transitions. Resolving all definition/interval matches before allowing feature fallbacks would preserve the strongest global mapping.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At shared/utils/productUtils/entUtils/findEntitlement/findEntitlementSuccessor.ts, line 74:

<comment>A feature-only match for an earlier source can claim the candidate that is an exact interval/definition successor for a later source, making the result dependent on source ordering and producing incorrect entitlement transitions. Resolving all definition/interval matches before allowing feature fallbacks would preserve the strongest global mapping.</comment>

<file context>
@@ -1,31 +1,78 @@
+					matchPrecision: currentMatchPrecision,
+				}),
+		);
+		if (candidate) return candidate;
+	}
+
</file context>

autumnBillingPlan: AutumnBillingPlan;
}) => {
handleLicenseTransitionErrors({ autumnBillingPlan });
await validateCustomerEntitlementBatchTransitions({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: An update can commit and return successfully while its child transition later fails for the base-price or assignment limit, because this compute preflight validates only entitlement counts. The base-price and assignment limits would be safer to validate before the license state is carried and the async task is queued.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionComputeErrors.ts, line 14:

<comment>An update can commit and return successfully while its child transition later fails for the base-price or assignment limit, because this compute preflight validates only entitlement counts. The base-price and assignment limits would be safer to validate before the license state is carried and the async task is queued.</comment>

<file context>
@@ -0,0 +1,18 @@
+	autumnBillingPlan: AutumnBillingPlan;
+}) => {
+	handleLicenseTransitionErrors({ autumnBillingPlan });
+	await validateCustomerEntitlementBatchTransitions({
+		ctx,
+		transitions: autumnBillingPlan.customerLicenseTransitions,
</file context>

ctx: AutumnContext;
transitions: CustomerLicenseTransition[] | undefined;
}) => {
await Promise.all(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Large license pools can make validation submit one database query per child transition at once, exhausting the connection pool and adding avoidable load before the billing request proceeds. A bounded limiter or chunked validation would keep this preflight within the same concurrency budget as batch execution.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/billing/v2/actions/batchTransition/errors/validateCustomerEntitlementBatchTransitions.ts, line 15:

<comment>Large license pools can make validation submit one database query per child transition at once, exhausting the connection pool and adding avoidable load before the billing request proceeds. A bounded limiter or chunked validation would keep this preflight within the same concurrency budget as batch execution.</comment>

<file context>
@@ -0,0 +1,42 @@
+	ctx: AutumnContext;
+	transitions: CustomerLicenseTransition[] | undefined;
+}) => {
+	await Promise.all(
+		(transitions ?? []).map(async (transition) => {
+			const fromProduct =
</file context>

transition.priceTransitions.length === 0 &&
transition.entitlementTransitions.length === 0 &&
!productTransitions?.basePrice &&
!productTransitions?.customerProduct &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: A same-product plan-license definition change can now be treated as a no-op when counters and product prices are unchanged, so the incoming pool keeps the old plan_license_id and repointDefinition is never executed. Including the outgoing/incoming plan-license identity in the no-op check would preserve these definition transitions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/billing/v2/compute/customerLicenseTransitions/computeCustomerLicenseTransitions.ts, line 30:

<comment>A same-product plan-license definition change can now be treated as a no-op when counters and product prices are unchanged, so the incoming pool keeps the old `plan_license_id` and `repointDefinition` is never executed. Including the outgoing/incoming plan-license identity in the no-op check would preserve these definition transitions.</comment>

<file context>
@@ -3,38 +3,51 @@ import type {
-		transition.priceTransitions.length === 0 &&
-		transition.entitlementTransitions.length === 0 &&
+		!productTransitions?.basePrice &&
+		!productTransitions?.customerProduct &&
+		!hasEntitlementChanges &&
 		updates.linkId === incomingCustomerLicense.link_id &&
</file context>
Suggested change
!productTransitions?.customerProduct &&
!productTransitions?.customerProduct &&
transition.outgoingCustomerLicense.plan_license_id ===
transition.incomingCustomerLicense.plan_license_id &&

@@ -0,0 +1,105 @@
import { describe, expect, test } from "bun:test";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: No test coverage for: empty outgoing/incoming licenses (zero matches), absent fullProduct/customerProduct (falls through to return-only-explicit branch), or multiple outgoing licenses with partial matching. These edge cases are exercised by the production branches but uncovered here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/tests/unit/billing/setup-customer-license-quantity-context.test.ts, line 56:

<comment>No test coverage for: empty outgoing/incoming licenses (zero matches), absent `fullProduct`/`customerProduct` (falls through to return-only-explicit branch), or multiple outgoing licenses with partial matching. These edge cases are exercised by the production branches but uncovered here.</comment>

<file context>
@@ -0,0 +1,105 @@
+		fullProduct: { licenses: incomingLicenses } as FullProduct,
+	});
+
+describe("setupCustomerLicenseQuantityContext", () => {
+	test("carries paid seats to an omitted 1:1 group successor", () => {
+		const outgoingPlanLicense = planLicense({
</file context>

import { ErrCode, RecaseError } from "@autumn/shared";
import { MAX_DISTINCT_ENTITLEMENTS } from "../utils/batchTransitionConstants";

export const enforceDistinctEntitlementLimit = ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The entitlement limit guard duplicates enforceDistinctBasePriceLimit almost verbatim, so fixes to shared limit behavior can be applied to one helper and missed in the other. A parameterized shared limit-enforcement helper would keep these checks consistent while preserving resource-specific messages.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/billing/v2/actions/batchTransition/errors/enforceDistinctEntitlementLimit.ts, line 4:

<comment>The entitlement limit guard duplicates `enforceDistinctBasePriceLimit` almost verbatim, so fixes to shared limit behavior can be applied to one helper and missed in the other. A parameterized shared limit-enforcement helper would keep these checks consistent while preserving resource-specific messages.</comment>

<file context>
@@ -0,0 +1,16 @@
+import { ErrCode, RecaseError } from "@autumn/shared";
+import { MAX_DISTINCT_ENTITLEMENTS } from "../utils/batchTransitionConstants";
+
+export const enforceDistinctEntitlementLimit = ({
+	count,
+}: {
</file context>

basePrice: BasePriceTransition | undefined;
customerProduct: CustomerProductTransition | undefined;
entitlementPrices: ComputedEntitlementPriceTransitions;
toProduct: FullProductWithoutLicenses;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: ProductTransitions.toProduct is never consumed by any caller, so this new field adds dead data and obscures which transition outputs are actually used. Removing the field and its object property would keep the result contract focused on the computed transitions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/billing/v2/actions/batchTransition/compute/transitions/computeProductTransitions.ts, line 19:

<comment>`ProductTransitions.toProduct` is never consumed by any caller, so this new field adds dead data and obscures which transition outputs are actually used. Removing the field and its object property would keep the result contract focused on the computed transitions.</comment>

<file context>
@@ -0,0 +1,39 @@
+	basePrice: BasePriceTransition | undefined;
+	customerProduct: CustomerProductTransition | undefined;
+	entitlementPrices: ComputedEntitlementPriceTransitions;
+	toProduct: FullProductWithoutLicenses;
+};
+
</file context>

@vercel
vercel Bot temporarily deployed to Preview – autumn-vite July 20, 2026 11:37 Inactive
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