feat(licenses): add batched child license transitions#2303
Conversation
|
Automatic Review Skipped Too many files for automatic review. If you would still like a review, you can trigger one manually by commenting: |
|
Too many files changed for review. ( Bypass the limit by tagging |
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
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. |
There was a problem hiding this comment.
15 issues found across 115 files
Confidence score: 2/5
server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionComputeErrors.tsand.../handleUpdateSubscriptionLicenseErrors.tscan 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.tsplusserver/src/internal/billing/v2/actions/batchTransition/compute/transitions/computeEntitlementPriceTransitions.tsmake 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, andserver/src/internal/billing/v2/compute/customerLicenseTransitions/transitionLicenseBillingPriceRows.tscan drop paid additions/removals, crash on in-place definition changes, and misstate projected quantities when included seats change. Add execution paths forunhandled.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.tsandserver/tests/perf/batchTransitionBench.tsintroduce 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 nestedtry/finallyso 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 { |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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>
| const candidate = candidateEntitlements.find( | ||
| (candidateEntitlement) => | ||
| !excludedEntitlementIds?.has(candidateEntitlement.id) && | ||
| entitlementsMatchAtPrecision({ | ||
| sourceEntitlement, | ||
| candidateEntitlement, | ||
| matchPrecision: currentMatchPrecision, | ||
| }), | ||
| ); | ||
| if (candidate) return candidate; |
There was a problem hiding this comment.
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>
| 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; |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 && |
There was a problem hiding this comment.
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>
| !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"; | |||
There was a problem hiding this comment.
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 = ({ |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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>
Summary
Stack
Verification
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
batch-transitiontask via@trigger.dev/sdkand new logging helpers (logContextExtras) for clear, structured logs.Refactors
repointSeatEntitlementsTaskand simplify attach/update error handling and license transition checks.Written for commit 67845cb. Summary will update on new commits.