feat(licenses): add scalable license transitions - #2306
Conversation
Add license parent preview and propagation controls, preserve customer configurations across catalog updates, and organize core license catalog coverage.
Mock the license-reference collaborators added to product item updates so existing atomicity and lock tests exercise their intended paths.
|
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 |
|
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.
26 issues found across 262 files
Confidence score: 2/5
server/src/internal/product/actions/deleteProduct.tscan still delete customer license pools when a license is tied to a retired/custom catalog link, which creates a concrete data-loss path if merged as-is — extend the delete guard to include retired/customplan_licenserows (or explicitly block deletion when customer references exist) before merging.- Transition logic across
server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerLicenseToStripeItemSpecs.ts,server/src/internal/billing/v2/execute/executeAutumnActions/executeCustomerLicenseTransitions.ts,shared/utils/productUtils/entUtils/findEntitlement/findEntitlementSuccessor.ts, andserver/src/internal/billing/v2/compute/customerLicenseTransitions/pairCustomerLicensesByLicensePlan.tscan mis-pair pools, skip priced seat updates, and drop assigned-seat charges, causing incorrect billing and entitlement assignment state — key projection/matching by stable license IDs, keep a priced-entitlement repoint path, and filter dangling rows before merge. vite/src/components/forms/update-subscription-v2/hooks/useUpdateSubscriptionRequestBody.tsnow emits a combined prepaid + seat-total mutation that the server rejects with 400, so user updates can fail entirely in a common edit flow — split these mutations client-side (or add server support for both fields together) before shipping.- State consistency issues in
server/src/internal/billing/v2/utils/initFullCustomerProduct/initCustomerEntitlement/initCustomerEntitlementFields.ts,server/src/internal/billing/v2/actions/batchTransition/execute/executeCustomerEntitlementOperations.ts,server/src/internal/billing/v2/actions/batchTransition/compute/operations/customerEntitlementCycleOperations/computeCustomerEntitlementCycleOperations.ts, andserver/src/external/stripe/webhookHandlers/handleStripeSubscriptionDeleted/tasks/expireAndActivateCustomerProducts.tscan null entity scope, update the wrong parent product, keep outgoing reset schedules, or leave scheduled add-ons after cancellation — tighten entity-scoped filters/cycle computation and verify webhook cleanup with targeted transition + cancellation integration tests.
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="vite/src/components/forms/update-subscription-v2/hooks/useUpdateSubscriptionRequestBody.ts">
<violation number="1" location="vite/src/components/forms/update-subscription-v2/hooks/useUpdateSubscriptionRequestBody.ts:244">
P1: Updating a prepaid option and a license-seat total in one submission now returns 400 instead of applying either change. Split these mutations or add server-side support before emitting both request fields together.</violation>
</file>
<file name="server/src/internal/products/prices/repos/listDistinctBasePricesByCustomerLicense.ts">
<violation number="1" location="server/src/internal/products/prices/repos/listDistinctBasePricesByCustomerLicense.ts:31">
P2: Transitions can reject valid assigned-seat changes when released pool seats retain more than `MAX_DISTINCT_BASE_PRICES` historical prices. Filter this candidate query to assigned seats so its limit and computed operations match batch execution scope.</violation>
</file>
<file name="vite/src/views/customers2/components/sheets/ReadOnlyPlanItems.tsx">
<violation number="1" location="vite/src/views/customers2/components/sheets/ReadOnlyPlanItems.tsx:52">
P2: Prepaid features with a zero assigned quantity render without a quantity badge, making `0` indistinguishable from no quantity. Render the badge when `prepaidQuantity != null` rather than using its truthiness.</violation>
</file>
<file name="vite/src/views/products/plan/versioning/buildMigrateTargets.ts">
<violation number="1" location="vite/src/views/products/plan/versioning/buildMigrateTargets.ts:143">
P2: The review preview labels historical parent versions with customers as a new version, although parent propagation only versions the latest parent entry. Base this badge/version number on `entry.versionable` so the confirmation accurately reflects the update.</violation>
</file>
<file name="server/src/internal/billing/v2/utils/initFullCustomerProduct/initCustomerEntitlement/initCustomerEntitlementFields.ts">
<violation number="1" location="server/src/internal/billing/v2/utils/initFullCustomerProduct/initCustomerEntitlement/initCustomerEntitlementFields.ts:67">
P1: New license assignments lose their entitlement entity scope because this always writes `null`, even when `internalEntityId` identifies the assigned entity. Preserve the same resolved entity ID used by `initCustomerProduct` so assignment entitlements remain entity-stamped.</violation>
</file>
<file name="server/src/internal/billing/v2/compute/customerLicenseTransitions/pairCustomerLicensesByLicensePlan.ts">
<violation number="1" location="server/src/internal/billing/v2/compute/customerLicenseTransitions/pairCustomerLicensesByLicensePlan.ts:30">
P1: A dead outgoing license can suppress a valid group successor match. Filter rows without `planLicense` before matching, so dangling rows cannot claim an incoming candidate and cause its usage/link state to be lost during this transition.</violation>
</file>
<file name="vite/src/views/customers2/components/sheets/LicenseAssignedEntities.tsx">
<violation number="1" location="vite/src/views/customers2/components/sheets/LicenseAssignedEntities.tsx:102">
P3: Opening a pool can briefly state “No entities assigned” before `list_assignments` returns, even when assignments exist. Surface a loading state until the assignments query completes instead of treating its initial empty array as a confirmed empty result.</violation>
</file>
<file name="vite/src/components/forms/attach-v2/components/AttachPlanSection.tsx">
<violation number="1" location="vite/src/components/forms/attach-v2/components/AttachPlanSection.tsx:79">
P2: A license-only outgoing plan now enables diff mode but hides newly added parent-plan items: the item baseline falls back to the incoming template when `outgoingItems` is empty. Use `hasOutgoingPlans` to retain the empty outgoing baseline for any transition.</violation>
</file>
<file name="server/src/internal/billing/v2/actions/batchTransition/execute/executeCustomerEntitlementOperations.ts">
<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/execute/executeCustomerEntitlementOperations.ts:141">
P1: Adding an entitlement also updates the entity-less parent `customer_product`, rather than only license assignments. Filter add candidates to `seat.internal_entity_id IS NOT NULL`, matching replacement/removal and base-price additions.</violation>
</file>
<file name="server/src/internal/billing/v2/actions/batchTransition/compute/operations/customerEntitlementCycleOperations/computeCustomerEntitlementCycleOperations.ts">
<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/compute/operations/customerEntitlementCycleOperations/computeCustomerEntitlementCycleOperations.ts:26">
P1: A base-price addition combined with an entitlement replacement that changes reset behavior leaves assignments with the outgoing entitlement’s reset schedule. Compute cycle eligibility/anchor/end from the replacement target (including nulling fields when it becomes non-resetting), or apply cycle alignment after replacements.</violation>
</file>
<file name="server/src/internal/product/actions/deleteProduct.ts">
<violation number="1" location="server/src/internal/product/actions/deleteProduct.ts:87">
P0: Deleting a license whose catalog link was retired can still delete customer license pools. Include custom/retired `plan_license` rows in this guard (or separately reject those with customer references) before deleting the product.</violation>
</file>
<file name="server/src/internal/licenses/actions/propagation/prepareLicenseParentPropagation.ts">
<violation number="1" location="server/src/internal/licenses/actions/propagation/prepareLicenseParentPropagation.ts:73">
P2: Updating a child with many license parents can fan out all plan/license resolution work at once, exhausting request/DB capacity before propagation starts. Process `contexts` in bounded chunks or with a concurrency limiter rather than one unbounded `Promise.all`.</violation>
</file>
<file name="vite/src/components/forms/update-subscription-v2/components/EditPlanSection.tsx">
<violation number="1" location="vite/src/components/forms/update-subscription-v2/components/EditPlanSection.tsx:47">
P3: A seat-only license change is saved but remains visually uncustomized: the section title omits its customization indicator and both license/item sections receive `showDiff={false}`. Include staged `licenseQuantities` that differ from `existingLicenseQuantities` when deriving `isCustomized`.</violation>
</file>
<file name="server/src/internal/licenses/repos/planLicenseRepo.ts">
<violation number="1" location="server/src/internal/licenses/repos/planLicenseRepo.ts:120">
P2: Editing a license with many customer-specific links can load and rewrite every snapshot in one request, causing timeouts or transaction exhaustion. Return bounded pages (or enqueue batches) and let the materialization flow process a cursor at a time.</violation>
</file>
<file name="server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerLicenseToStripeItemSpecs.ts">
<violation number="1" location="server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerLicenseToStripeItemSpecs.ts:39">
P1: A transition of one duplicate license pool can drop assigned-seat charges for its untransitioned sibling pool. Track projection state by `customerLicense.id` (or only select projected rows when this pool has them), rather than treating a plan-license ID as a pool-specific projection marker.</violation>
</file>
<file name="server/src/internal/billing/v2/execute/executeAutumnActions/executeCustomerLicenseTransitions.ts">
<violation number="1" location="server/src/internal/billing/v2/execute/executeAutumnActions/executeCustomerLicenseTransitions.ts:54">
P1: Seat entitlement transitions with a price now complete without updating those assigned seats. `batchTransition` explicitly leaves priced entitlement transitions unhandled, so retain a repoint path for them or implement their batch operation before replacing the enqueue.</violation>
</file>
<file name="server/src/external/stripe/webhookHandlers/handleStripeSubscriptionDeleted/tasks/expireAndActivateCustomerProducts.ts">
<violation number="1" location="server/src/external/stripe/webhookHandlers/handleStripeSubscriptionDeleted/tasks/expireAndActivateCustomerProducts.ts:34">
P1: Canceled subscriptions can leave scheduled add-ons behind: this filter skips them, and later cleanup only deletes a scheduled main product. Preserve cleanup for scheduled non-main products (and their schedule state) so they cannot activate after the subscription was deleted.</violation>
</file>
<file name="server/src/internal/billing/v2/actions/batchTransition/execute/sql/deleteCustomerBasePricesBatch.ts">
<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/execute/sql/deleteCustomerBasePricesBatch.ts:29">
P2: A concurrent assignment detach or license-link/status update can leave this batch deleting a base price from a seat that no longer matches this transition. Lock the `seat` rows alongside `customer_price` so the qualifying assignment cannot change between candidate selection and deletion.</violation>
</file>
<file name="shared/utils/productUtils/entUtils/findEntitlement/findEntitlementSuccessor.ts">
<violation number="1" location="shared/utils/productUtils/entUtils/findEntitlement/findEntitlementSuccessor.ts:43">
P1: A feature-only fallback can steal a target that is an exact match for a later entitlement, making transitions depend on entitlement array order. This swaps customer assignments and applies incorrect balance patches when one reset interval changes alongside another unchanged entitlement for the same feature; reserve exact matches globally before lower-precision matching.</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:55">
P1: A changed definition that retains its entitlement ID produces a replacement whose source contains its target ID, causing every such license transition to throw `Batch replacement requires different outgoing and incoming entitlement IDs`. Exclude the incoming ID unconditionally, matching base-price replacement behavior.</violation>
</file>
<file name="vite/src/components/forms/update-subscription-v2/context/UpdateSubscriptionFormProvider.tsx">
<violation number="1" location="vite/src/components/forms/update-subscription-v2/context/UpdateSubscriptionFormProvider.tsx:317">
P2: Adding or removing a paid license still shows “No changes to billing will be made.” `licenseRowHasBillingChanges` ignores added/removed rows because they lack one comparison side; include priced license additions/removals in this predicate so the billing status matches the update.</violation>
</file>
<file name="server/src/internal/billing/v2/actions/batchTransition/execute/sql/repointLicenseCustomerProductsBatch.ts">
<violation number="1" location="server/src/internal/billing/v2/actions/batchTransition/execute/sql/repointLicenseCustomerProductsBatch.ts:37">
P2: Transitioned assignments retain the outgoing public `product_id`, so flows that identify existing customer products by plan ID can treat them as the old license product. Update `product_id` alongside `internal_product_id` using the incoming product definition.</violation>
</file>
<file name="server/src/internal/product/actions/previewUpdatePlan/previewAffectedLicenseParents.ts">
<violation number="1" location="server/src/internal/product/actions/previewUpdatePlan/previewAffectedLicenseParents.ts:128">
P2: Unselected parent previews show propagated `plan_changes` even though the update preserves their effective license plan. Use `currentEffectivePlan` unless this parent target is selected, so `license_changes` matches the eventual preservation rebase.</violation>
<violation number="2" location="server/src/internal/product/actions/previewUpdatePlan/previewAffectedLicenseParents.ts:166">
P2: Unselected latest parents are reported `versionable: true` whenever a child version changes and they have customers, although their preservation update is always in-place. Gate this calculation on selection so migration UI does not offer a version that update execution will not create.</violation>
</file>
<file name="vite/src/components/forms/shared/plan-items/PlanLicensesSummary.tsx">
<violation number="1" location="vite/src/components/forms/shared/plan-items/PlanLicensesSummary.tsx:269">
P1: An alternate-currency-only license price edit is classified as no billing change because `additional_currencies` is omitted from the fingerprint. Include it so updates cannot incorrectly enable the no-billing-change path.</violation>
<violation number="2" location="vite/src/components/forms/shared/plan-items/PlanLicensesSummary.tsx:380">
P2: License cadence-only changes show no pricing transition when the amount is unchanged. Include interval and interval_count in this display condition and render the old/new cadence, matching the parent plan price preview.</violation>
</file>
Note: This PR contains a large number of files. cubic only reviews up to 200 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
Re-trigger cubic
| orgId: org.id, | ||
| env, | ||
| }), | ||
| planLicenseRepo.listCatalogByLicenseInternalProductIds({ |
There was a problem hiding this comment.
P0: Deleting a license whose catalog link was retired can still delete customer license pools. Include custom/retired plan_license rows in this guard (or separately reject those with customer references) before deleting the product.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/product/actions/deleteProduct.ts, line 87:
<comment>Deleting a license whose catalog link was retired can still delete customer license pools. Include custom/retired `plan_license` rows in this guard (or separately reject those with customer references) before deleting the product.</comment>
<file context>
@@ -71,8 +84,18 @@ export const deleteProduct = async ({
orgId: org.id,
env,
}),
+ planLicenseRepo.listCatalogByLicenseInternalProductIds({
+ db,
+ licenseInternalProductIds: deletionInternalProductIds,
</file context>
| return { | ||
| ...base, | ||
| options: options.length > 0 ? options : undefined, | ||
| license_quantities: convertLicenseQuantitiesToParams({ |
There was a problem hiding this comment.
P1: Updating a prepaid option and a license-seat total in one submission now returns 400 instead of applying either change. Split these mutations or add server-side support before emitting both request fields together.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vite/src/components/forms/update-subscription-v2/hooks/useUpdateSubscriptionRequestBody.ts, line 244:
<comment>Updating a prepaid option and a license-seat total in one submission now returns 400 instead of applying either change. Split these mutations or add server-side support before emitting both request fields together.</comment>
<file context>
@@ -219,6 +241,9 @@ export function useUpdateSubscriptionRequestBody({
return {
...base,
options: options.length > 0 ? options : undefined,
+ license_quantities: convertLicenseQuantitiesToParams({
+ licenseQuantities: changedLicenseQuantities,
+ }),
</file context>
| return { | ||
| internal_customer_id: initContext.fullCustomer.internal_id, | ||
| internal_feature_id: entitlement.internal_feature_id, | ||
| internal_entity_id: null, |
There was a problem hiding this comment.
P1: New license assignments lose their entitlement entity scope because this always writes null, even when internalEntityId identifies the assigned entity. Preserve the same resolved entity ID used by initCustomerProduct so assignment entitlements remain entity-stamped.
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/utils/initFullCustomerProduct/initCustomerEntitlement/initCustomerEntitlementFields.ts, line 67:
<comment>New license assignments lose their entitlement entity scope because this always writes `null`, even when `internalEntityId` identifies the assigned entity. Preserve the same resolved entity ID used by `initCustomerProduct` so assignment entitlements remain entity-stamped.</comment>
<file context>
@@ -0,0 +1,85 @@
+ return {
+ internal_customer_id: initContext.fullCustomer.internal_id,
+ internal_feature_id: entitlement.internal_feature_id,
+ internal_entity_id: null,
+ feature_id: entitlement.feature.id,
+ customer_id: initContext.fullCustomer.id,
</file context>
| const { matches } = matchCustomerLicenseSuccessors({ | ||
| outgoingCustomerLicenses: outgoingCustomerProduct.customer_licenses ?? [], | ||
| incomingCustomerLicenses: incomingCustomerProduct.customer_licenses ?? [], | ||
| }); |
There was a problem hiding this comment.
P1: A dead outgoing license can suppress a valid group successor match. Filter rows without planLicense before matching, so dangling rows cannot claim an incoming candidate and cause its usage/link state to be lost during this transition.
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/pairCustomerLicensesByLicensePlan.ts, line 30:
<comment>A dead outgoing license can suppress a valid group successor match. Filter rows without `planLicense` before matching, so dangling rows cannot claim an incoming candidate and cause its usage/link state to be lost during this transition.</comment>
<file context>
@@ -16,60 +17,37 @@ export type CustomerLicensePair = {
- }
-
- return pairs;
+ const { matches } = matchCustomerLicenseSuccessors({
+ outgoingCustomerLicenses: outgoingCustomerProduct.customer_licenses ?? [],
+ incomingCustomerLicenses: incomingCustomerProduct.customer_licenses ?? [],
</file context>
| const { matches } = matchCustomerLicenseSuccessors({ | |
| outgoingCustomerLicenses: outgoingCustomerProduct.customer_licenses ?? [], | |
| incomingCustomerLicenses: incomingCustomerProduct.customer_licenses ?? [], | |
| }); | |
| const { matches } = matchCustomerLicenseSuccessors({ | |
| outgoingCustomerLicenses: ( | |
| outgoingCustomerProduct.customer_licenses ?? [] | |
| ).filter((customerLicense) => customerLicense.planLicense), | |
| incomingCustomerLicenses: ( | |
| incomingCustomerProduct.customer_licenses ?? [] | |
| ).filter((customerLicense) => customerLicense.planLicense), | |
| }); |
| if (operation.type === "add") { | ||
| return { | ||
| replaced: 0, | ||
| added: await executeAddition({ ctx, batchTransition, operation }), |
There was a problem hiding this comment.
P1: Adding an entitlement also updates the entity-less parent customer_product, rather than only license assignments. Filter add candidates to seat.internal_entity_id IS NOT NULL, matching replacement/removal and base-price additions.
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/execute/executeCustomerEntitlementOperations.ts, line 141:
<comment>Adding an entitlement also updates the entity-less parent `customer_product`, rather than only license assignments. Filter add candidates to `seat.internal_entity_id IS NOT NULL`, matching replacement/removal and base-price additions.</comment>
<file context>
@@ -0,0 +1,194 @@
+ if (operation.type === "add") {
+ return {
+ replaced: 0,
+ added: await executeAddition({ ctx, batchTransition, operation }),
+ removed: 0,
+ };
</file context>
| const versionable = | ||
| (isLatest && data.force_version === true) || | ||
| (isLatest && | ||
| !data.disable_version && | ||
| !data.all_versions && | ||
| hasCustomers && | ||
| hasChanges); |
There was a problem hiding this comment.
P2: Unselected latest parents are reported versionable: true whenever a child version changes and they have customers, although their preservation update is always in-place. Gate this calculation on selection so migration UI does not offer a version that update execution will not create.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/product/actions/previewUpdatePlan/previewAffectedLicenseParents.ts, line 166:
<comment>Unselected latest parents are reported `versionable: true` whenever a child version changes and they have customers, although their preservation update is always in-place. Gate this calculation on selection so migration UI does not offer a version that update execution will not create.</comment>
<file context>
@@ -0,0 +1,214 @@
+ const hasChanges =
+ licenseChange.previous_attributes !== null ||
+ licenseChange.plan_changes !== null;
+ const versionable =
+ (isLatest && data.force_version === true) ||
+ (isLatest &&
</file context>
| const versionable = | |
| (isLatest && data.force_version === true) || | |
| (isLatest && | |
| !data.disable_version && | |
| !data.all_versions && | |
| hasCustomers && | |
| hasChanges); | |
| const versionable = | |
| selectedTargets.has( | |
| licenseParentTargetKey({ | |
| planId: parent.id, | |
| version: parent.version, | |
| }), | |
| ) && | |
| ((isLatest && data.force_version) || | |
| (isLatest && | |
| !data.disable_version && | |
| !data.all_versions && | |
| hasCustomers && | |
| hasChanges)); |
| const targetEffectivePlan = currentLink.customized | ||
| ? applyLicenseCustomizeToBasePlan({ | ||
| basePlan: editedChildPlan, | ||
| customize: currentCustomize, | ||
| }) | ||
| : editedChildPlan; |
There was a problem hiding this comment.
P2: Unselected parent previews show propagated plan_changes even though the update preserves their effective license plan. Use currentEffectivePlan unless this parent target is selected, so license_changes matches the eventual preservation rebase.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/product/actions/previewUpdatePlan/previewAffectedLicenseParents.ts, line 128:
<comment>Unselected parent previews show propagated `plan_changes` even though the update preserves their effective license plan. Use `currentEffectivePlan` unless this parent target is selected, so `license_changes` matches the eventual preservation rebase.</comment>
<file context>
@@ -0,0 +1,214 @@
+ from: currentChildPlan,
+ to: currentEffectivePlan,
+ });
+ const targetEffectivePlan = currentLink.customized
+ ? applyLicenseCustomizeToBasePlan({
+ basePlan: editedChildPlan,
</file context>
| const targetEffectivePlan = currentLink.customized | |
| ? applyLicenseCustomizeToBasePlan({ | |
| basePlan: editedChildPlan, | |
| customize: currentCustomize, | |
| }) | |
| : editedChildPlan; | |
| const isSelected = selectedTargets.has( | |
| licenseParentTargetKey({ | |
| planId: parent.id, | |
| version: parent.version, | |
| }), | |
| ); | |
| const targetEffectivePlan = isSelected | |
| ? currentLink.customized | |
| ? applyLicenseCustomizeToBasePlan({ | |
| basePlan: editedChildPlan, | |
| customize: currentCustomize, | |
| }) | |
| : editedChildPlan | |
| : currentEffectivePlan; |
| showDiff && | ||
| !isRemoved && | ||
| row.previousItems !== undefined && | ||
| (previousPrice?.price ?? null) !== (currentPrice?.price ?? null); |
There was a problem hiding this comment.
P2: License cadence-only changes show no pricing transition when the amount is unchanged. Include interval and interval_count in this display condition and render the old/new cadence, matching the parent plan price preview.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vite/src/components/forms/shared/plan-items/PlanLicensesSummary.tsx, line 380:
<comment>License cadence-only changes show no pricing transition when the amount is unchanged. Include interval and interval_count in this display condition and render the old/new cadence, matching the parent plan price preview.</comment>
<file context>
@@ -86,84 +116,312 @@ export const diffPlanLicenses = ({
+ showDiff &&
+ !isRemoved &&
+ row.previousItems !== undefined &&
+ (previousPrice?.price ?? null) !== (currentPrice?.price ?? null);
+
return (
</file context>
| /> | ||
| </div> | ||
| )} | ||
| {filteredAssignments.length === 0 ? ( |
There was a problem hiding this comment.
P3: Opening a pool can briefly state “No entities assigned” before list_assignments returns, even when assignments exist. Surface a loading state until the assignments query completes instead of treating its initial empty array as a confirmed empty result.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vite/src/views/customers2/components/sheets/LicenseAssignedEntities.tsx, line 102:
<comment>Opening a pool can briefly state “No entities assigned” before `list_assignments` returns, even when assignments exist. Surface a loading state until the assignments query completes instead of treating its initial empty array as a confirmed empty result.</comment>
<file context>
@@ -0,0 +1,182 @@
+ />
+ </div>
+ )}
+ {filteredAssignments.length === 0 ? (
+ <p className="text-sm text-tertiary-foreground">
+ {poolAssignments.length === 0
</file context>
| ); | ||
| const licenseQuantityEditor = { | ||
| form, | ||
| quantities: licenseQuantities, |
There was a problem hiding this comment.
P3: A seat-only license change is saved but remains visually uncustomized: the section title omits its customization indicator and both license/item sections receive showDiff={false}. Include staged licenseQuantities that differ from existingLicenseQuantities when deriving isCustomized.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vite/src/components/forms/update-subscription-v2/components/EditPlanSection.tsx, line 47:
<comment>A seat-only license change is saved but remains visually uncustomized: the section title omits its customization indicator and both license/item sections receive `showDiff={false}`. Include staged `licenseQuantities` that differ from `existingLicenseQuantities` when deriving `isCustomized`.</comment>
<file context>
@@ -31,6 +35,26 @@ export function EditPlanSection() {
+ );
+ const licenseQuantityEditor = {
+ form,
+ quantities: licenseQuantities,
+ existingQuantities: existingLicenseQuantities,
+ };
</file context>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
Summary
Validation
Full integration suite was not run for the final UI-only changes.
Summary by cubic
Adds scalable, row-batched license transitions that preserve quantities, billing state, and Stripe linkage across immediate and scheduled plan changes. Also introduces license parent propagation with customization carryover, and polishes previews, Stripe item updates, and validation.
New Features
batchTransitionTask.Bug Fixes
Written for commit 1e1bc59. Summary will update on new commits.