Skip to content

feat(licenses): support child plan propagation#2295

Merged
johnyeocx merged 5 commits into
devfrom
license-update-child
Jul 20, 2026
Merged

feat(licenses): support child plan propagation#2295
johnyeocx merged 5 commits into
devfrom
license-update-child

Conversation

@johnyeocx

@johnyeocx johnyeocx commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • preview linked parent-plan impacts when a child license plan changes and support explicit propagation targets
  • preserve existing customer license configurations across in-place and versioned catalog updates
  • reuse propagation UI for variants and license parents, including conflicts and correct versioning decisions
  • organize license catalog tests and register license, plans CRUD, and Stripe back-sync coverage in core groups

Validation

  • bun test tests/views/products/plan/versioning/preview-has-affected-customers.test.ts
  • focused license-parent conflict integration test
  • bun run ts (vite)
  • bun run ts (server)
  • React Doctor changed-file scan: no issues

Full integration suite was not run.


Summary by cubic

Adds child license plan propagation with preview and explicit parent targets. Preserves customer license configurations across catalog updates, hardens immediate-switch safeguards, and improves attach-target validation and customer item-reference detection.

  • New Features

    • API/Preview
      • plans.preview_update includes license_parents; request with include_license_parents.
      • plans.update accepts update_license_parents to update selected parent plan versions.
      • License links now resolve to the latest child version (removed per-link version pin).
    • Propagation rules
      • Customerless parents update in place; parents with customers copy-on-write; customer snapshots stay pinned.
      • Multiple parent versions stay linked to the newest child; preserved parents remain previewable.
    • Billing safeguards
      • Immediate plan switches pair license pools by same license plan id, or 1:1 by group; ambiguous/dropped pools are rejected.
      • Added attach-target validation to prevent mislinking licenses to the wrong parent.
    • Safety
      • Plan item updates detect customer references via license-item entitlements/prices to choose in-place vs versioned.
    • UI
      • Plan Change Dialog adds a parent-propagation step with conflicts and safe defaults.
      • Customer sheets show license-specific items and assigned entities with search and paging; subscription sheets include license item rows.
    • Tests
      • Added core suites for licenses billing/catalog and plans CRUD; expanded coverage for propagation, previews, transitions, and Stripe back-sync.
  • Migration

    • Use update_license_parents to propagate child license changes to selected parent versions.
    • Request previews with include_license_parents and read license_parents in the response.
    • Stop sending licenses[].version on parent links; links resolve to the latest child.
    • Deleting a license linked to a parent is now rejected; unlink first.

Written for commit 291bcb9. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR adds end-to-end support for child-plan propagation in the license system: when a child (license) plan is edited, its parent plans can now receive a preview of the impact, and the user can explicitly select which parent versions to update alongside the child. It also hardens plan-deletion guards and attach-target validation for license plans.

  • [Improvements, API changes] New update_license_parents parameter and license_parents preview field; the version pin field is removed from PlanLicenseParams.
  • [Improvements] prepareLicenseParentPropagation/applyLicenseParentPropagation snapshot all parent contexts before any writes and apply preserve-vs-propagate semantics; PlanChangeDialog reuses PropagationTargetsStep for both variant and license-parent selection.
  • [Bug fixes] In-place update detection now includes license item references; deletion of a plan with active parent-plan license links is now rejected.

Confidence Score: 4/5

The change is safe to merge. The one structural concern in applyLicenseParentPropagation triggers two updateProduct calls on the same parent when the child is versioned, but the final written state is the intended propagated result.

Most new paths are well-scoped and the propagation snapshot+apply split is sound. The double-update issue wastes a Stripe sync round-trip and leaves the old parent version with a stale rebased customize, but does not corrupt the final state. The full integration suite was not run, which is a gap given the breadth of the change.

server/src/internal/licenses/actions/propagation/applyLicenseParentPropagation.ts — the first-loop guard for selected parents when the child is versioned.

Important Files Changed

Filename Overview
server/src/internal/licenses/actions/propagation/applyLicenseParentPropagation.ts New file orchestrating parent updates after a child-plan change. The double-update path for selected+hasCustomers parents when the child is versioned is the main concern.
server/src/internal/licenses/actions/propagation/prepareLicenseParentPropagation.ts New file fetching and snapshotting all parent contexts before propagation. Logic is straightforward and defensively structured.
server/src/internal/product/actions/previewUpdatePlan/previewAffectedLicenseParents.ts New file computing parent-plan preview impacts. Contains a redundant null filter at the return site; rest of the logic is solid.
server/src/internal/product/actions/updateProduct.ts Adds parent-propagation wiring to the main update flow. Separation between in-place and post-update propagation paths is clean.
server/src/internal/billing/v2/compute/customerLicenseTransitions/matchCustomerLicenseSuccessors.ts New ranked license-pool successor matching (ID-first, then group 1:1). Ambiguity detection is well-documented.
vite/src/views/products/plan/versioning/PlanChangeDialog.tsx Substantial refactor supporting license-parent propagation alongside variants. Default selection and conflict display look correct.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
server/src/internal/licenses/actions/propagation/applyLicenseParentPropagation.ts:80-98
**Double update for `selected + hasCustomers` when child was versioned**

When `childWasVersioned` is `true`, any parent that is both `selected` and `hasCustomers` falls through the first loop (the `continue` only skips `selected && !hasCustomers`) and is also processed in the second loop. This causes two sequential `updateProduct` calls: the first applies `buildPreservedLicenses` in_place (rebased customize) and the second immediately overwrites it with `buildPropagatedLicenses` (original snapshot customize). The first write is wasted and triggers an unnecessary Stripe resource sync and catalog rebase. For latest parents the effect is worse — the first call modifies the old version in place and the second call creates a new version, leaving the old version with a stale rebased customize. The guard in the first loop should likely be `if (target.selected) continue` so all selected parents are handled exclusively by the second loop.

### Issue 2 of 2
server/src/internal/product/actions/previewUpdatePlan/previewAffectedLicenseParents.ts:197-203
**Redundant `!== null` type guard**

`previews` is already typed as `PlanUpdatePreviewLicenseParent[]` — every element is a parsed Zod value, so the `preview !== null` predicate is always `true`. Removing the filter avoids a misleading dead-code guard.

Reviews (1): Last reviewed commit: "test(licenses): 💍 update product item r..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Add license parent preview and propagation controls, preserve customer
configurations across catalog updates, and organize core license catalog
coverage.
@johnyeocx
johnyeocx requested a review from ay-rod as a code owner July 17, 2026 15:44
@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

@capy-ai

capy-ai Bot commented Jul 17, 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.

Mock the license-reference collaborators added to product item updates
so existing atomicity and lock tests exercise their intended paths.
@vercel

vercel Bot commented Jul 17, 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 17, 2026 6:23pm
landing-page Ignored Ignored Jul 17, 2026 6:23pm

Request Review

@vercel
vercel Bot temporarily deployed to Preview – autumn-vite July 17, 2026 15:49 Inactive
Comment on lines +80 to +98
if (childWasVersioned) {
for (const target of prepared.allParents) {
if (target.selected && !target.hasCustomers) continue;
await updateParent({
parent: target.parent,
licenses: buildPreservedLicenses({ target, newChild, newChildPlan }),
versioning: "in_place",
});
}
}

for (const target of prepared.selectedParents) {
if (!childWasVersioned && !target.hasCustomers && !forceVersion) continue;
await updateParent({
parent: target.parent,
licenses: buildPropagatedLicenses({ target, newChild }),
versioning: target.isLatest ? "inherit" : "in_place",
});
}

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 Double update for selected + hasCustomers when child was versioned

When childWasVersioned is true, any parent that is both selected and hasCustomers falls through the first loop (the continue only skips selected && !hasCustomers) and is also processed in the second loop. This causes two sequential updateProduct calls: the first applies buildPreservedLicenses in_place (rebased customize) and the second immediately overwrites it with buildPropagatedLicenses (original snapshot customize). The first write is wasted and triggers an unnecessary Stripe resource sync and catalog rebase. For latest parents the effect is worse — the first call modifies the old version in place and the second call creates a new version, leaving the old version with a stale rebased customize. The guard in the first loop should likely be if (target.selected) continue so all selected parents are handled exclusively by the second loop.

Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/internal/licenses/actions/propagation/applyLicenseParentPropagation.ts
Line: 80-98

Comment:
**Double update for `selected + hasCustomers` when child was versioned**

When `childWasVersioned` is `true`, any parent that is both `selected` and `hasCustomers` falls through the first loop (the `continue` only skips `selected && !hasCustomers`) and is also processed in the second loop. This causes two sequential `updateProduct` calls: the first applies `buildPreservedLicenses` in_place (rebased customize) and the second immediately overwrites it with `buildPropagatedLicenses` (original snapshot customize). The first write is wasted and triggers an unnecessary Stripe resource sync and catalog rebase. For latest parents the effect is worse — the first call modifies the old version in place and the second call creates a new version, leaving the old version with a stale rebased customize. The guard in the first loop should likely be `if (target.selected) continue` so all selected parents are handled exclusively by the second loop.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +197 to +203
editedBasePlan: editedChildPlan,
diff: childDiff,
variantPlan: currentEffectivePlan,
features: ctx.features,
}),
license_changes: [licenseChange],
});

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 Redundant !== null type guard

previews is already typed as PlanUpdatePreviewLicenseParent[] — every element is a parsed Zod value, so the preview !== null predicate is always true. Removing the filter avoids a misleading dead-code guard.

Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/internal/product/actions/previewUpdatePlan/previewAffectedLicenseParents.ts
Line: 197-203

Comment:
**Redundant `!== null` type guard**

`previews` is already typed as `PlanUpdatePreviewLicenseParent[]` — every element is a parsed Zod value, so the `preview !== null` predicate is always `true`. Removing the filter avoids a misleading dead-code guard.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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.

7 issues found and verified against the latest diff

Confidence score: 2/5

  • server/src/internal/product/actions/deleteProduct.ts can allow deleting a license that is still referenced by customized/retired parent-license links, leaving preserved customer configurations pointing at deleted data and risking broken behavior in downstream flows — expand the reference lookup beyond catalog links and block deletion until those references are handled.
  • server/src/internal/catalog/actions/updateCatalog/updateCatalog.ts and vite/src/views/products/plan/versioning/PlanChangeDialog.tsx both risk silently applying less than requested: parent-only propagation targets can be skipped, and metadata-only “all versions” saves can update only the current version — align the update gating and keep the effective all_versions intent in metadata-only submissions before merging.
  • server/src/internal/billing/v2/compute/customerLicenseTransitions/pairCustomerLicensesByLicensePlan.ts can let dangling customer-license rows preempt a valid live-license transition, which may produce incorrect transition outcomes for real customers — filter dead-link rows before successor matching so fallback IDs cannot claim incoming licenses.
  • server/tests/integration/licenses/catalog-update/child-plan/preserve/versioned-child.test.ts currently has an unreachable assertion block due to an early return, so the test passes without validating the behavior it claims to cover — remove the early return and rerun to ensure regressions in child-plan preserve/versioning logic are actually caught.
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/tests/integration/licenses/catalog-update/child-plan/preserve/versioned-child.test.ts">

<violation number="1" location="server/tests/integration/licenses/catalog-update/child-plan/preserve/versioned-child.test.ts:51">
P0: Unreachable `return;` on line 64 bypasses all test assertions — the entire block after it (`getFullLicenseProduct`, `expectLicenseDefinitionCorrect`, `plans.update`, all `expect()` calls) never runs. The test passes trivially regardless of behavior. Remove the early return so the assertions execute.</violation>
</file>

<file name="server/src/internal/product/actions/deleteProduct.ts">

<violation number="1" location="server/src/internal/product/actions/deleteProduct.ts:87">
P1: Deleting a license still succeeds when it is referenced by a customized or retired parent-license link, because this lookup only returns catalog links. Those preserved customer configurations retain the deleted license's internal product ID; include all referencing `planLicenses` rows (or reject deletion when any exists).</violation>
</file>

<file name="server/src/internal/catalog/actions/updateCatalog/updateCatalog.ts">

<violation number="1" location="server/src/internal/catalog/actions/updateCatalog/updateCatalog.ts:255">
P1: Catalog updates that only select license-parent propagation targets are skipped before `updateProduct`, so explicit parent propagation has no effect unless the child also has a plan or variant edit. Treat nonempty `update_license_parents` as work to process, and ensure the no-product-update path in `updateProduct` applies the prepared parent propagation.</violation>
</file>

<file name="vite/src/views/products/plan/versioning/PlanChangeDialog.tsx">

<violation number="1" location="vite/src/views/products/plan/versioning/PlanChangeDialog.tsx:273">
P1: Metadata-only saves now submit an in-place update without `all_versions`, so settings advertised as applying to every version can update only the current version. Preserve the `"all"` effective choice for `isMetadataOnly` so the existing all-version request path runs.</violation>
</file>

<file name="server/src/internal/product/actions/inPlaceUpdateUtils.ts">

<violation number="1" location="server/src/internal/product/actions/inPlaceUpdateUtils.ts:107">
P3: Catalog updates fetch every matching license reference merely to decide whether any exists. A heavily reused item can return thousands of rows; add an existence/`LIMIT 1` repo query here, leaving full ID collection to retirement.</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: Parent review can label a new version when this parent has customers but its propagated change is not versionable. Derive the status from `entry.versionable`, matching the backend preview and strategy logic.</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:31">
P1: A dangling customer-license row can now suppress a valid live-license transition. Filter dead-link rows before calling `matchCustomerLicenseSuccessors`; filtering after matching lets their fallback id claim the incoming pool, so its match is discarded and the live pool is left unpropagated.</violation>
</file>

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

Re-trigger cubic

}),
],
});
return;

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: Unreachable return; on line 64 bypasses all test assertions — the entire block after it (getFullLicenseProduct, expectLicenseDefinitionCorrect, plans.update, all expect() calls) never runs. The test passes trivially regardless of behavior. Remove the early return so the assertions execute.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/tests/integration/licenses/catalog-update/child-plan/preserve/versioned-child.test.ts, line 51:

<comment>Unreachable `return;` on line 64 bypasses all test assertions — the entire block after it (`getFullLicenseProduct`, `expectLicenseDefinitionCorrect`, `plans.update`, all `expect()` calls) never runs. The test passes trivially regardless of behavior. Remove the early return so the assertions execute.</comment>

<file context>
@@ -0,0 +1,125 @@
+				}),
+			],
+		});
+		return;
+
+		const before = await getFullLicenseProduct({
</file context>

orgId: org.id,
env,
}),
planLicenseRepo.listCatalogByLicenseInternalProductIds({

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: Deleting a license still succeeds when it is referenced by a customized or retired parent-license link, because this lookup only returns catalog links. Those preserved customer configurations retain the deleted license's internal product ID; include all referencing planLicenses rows (or reject deletion when any exists).

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 still succeeds when it is referenced by a customized or retired parent-license link, because this lookup only returns catalog links. Those preserved customer configurations retain the deleted license's internal product ID; include all referencing `planLicenses` rows (or reject deletion when any exists).</comment>

<file context>
@@ -71,8 +84,18 @@ export const deleteProduct = async ({
 			orgId: org.id,
 			env,
 		}),
+		planLicenseRepo.listCatalogByLicenseInternalProductIds({
+			db,
+			licenseInternalProductIds: deletionInternalProductIds,
</file context>

Comment thread vite/src/views/products/plan/components/SaveChangesBar.tsx
Comment on lines +31 to +32
outgoingCustomerLicenses: outgoingCustomerProduct.customer_licenses ?? [],
incomingCustomerLicenses: incomingCustomerProduct.customer_licenses ?? [],

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 dangling customer-license row can now suppress a valid live-license transition. Filter dead-link rows before calling matchCustomerLicenseSuccessors; filtering after matching lets their fallback id claim the incoming pool, so its match is discarded and the live pool is left unpropagated.

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 31:

<comment>A dangling customer-license row can now suppress a valid live-license transition. Filter dead-link rows before calling `matchCustomerLicenseSuccessors`; filtering after matching lets their fallback id claim the incoming pool, so its match is discarded and the live pool is left unpropagated.</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>
Suggested change
outgoingCustomerLicenses: outgoingCustomerProduct.customer_licenses ?? [],
incomingCustomerLicenses: incomingCustomerProduct.customer_licenses ?? [],
outgoingCustomerLicenses: (
outgoingCustomerProduct.customer_licenses ?? []
).filter((customerLicense) => customerLicense.planLicense),
incomingCustomerLicenses: (
incomingCustomerProduct.customer_licenses ?? []
).filter((customerLicense) => customerLicense.planLicense),

updates: updateParams,
initialFullProduct: current,
propagateToVariants: update_variant_ids ?? [],
licenseParentUpdates: update_license_parents,

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: Catalog updates that only select license-parent propagation targets are skipped before updateProduct, so explicit parent propagation has no effect unless the child also has a plan or variant edit. Treat nonempty update_license_parents as work to process, and ensure the no-product-update path in updateProduct applies the prepared parent propagation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/catalog/actions/updateCatalog/updateCatalog.ts, line 255:

<comment>Catalog updates that only select license-parent propagation targets are skipped before `updateProduct`, so explicit parent propagation has no effect unless the child also has a plan or variant edit. Treat nonempty `update_license_parents` as work to process, and ensure the no-product-update path in `updateProduct` applies the prepared parent propagation.</comment>

<file context>
@@ -251,6 +252,7 @@ const upsertPlans = async ({
 			updates: updateParams,
 			initialFullProduct: current,
 			propagateToVariants: update_variant_ids ?? [],
+			licenseParentUpdates: update_license_parents,
 			variantUpdates,
 		});
</file context>

const hasVariants = variants.length > 0;
const showVersionStrategy =
!isMetadataOnly && !!preview && previewHasVersionableTargets(preview);
const effectiveVersionChoice = showVersionStrategy ? versionChoice : "update";

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: Metadata-only saves now submit an in-place update without all_versions, so settings advertised as applying to every version can update only the current version. Preserve the "all" effective choice for isMetadataOnly so the existing all-version request path runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vite/src/views/products/plan/versioning/PlanChangeDialog.tsx, line 273:

<comment>Metadata-only saves now submit an in-place update without `all_versions`, so settings advertised as applying to every version can update only the current version. Preserve the `"all"` effective choice for `isMetadataOnly` so the existing all-version request path runs.</comment>

<file context>
@@ -245,28 +268,15 @@ export default function PlanChangeDialog({
 	const hasVariants = variants.length > 0;
+	const showVersionStrategy =
+		!isMetadataOnly && !!preview && previewHasVersionableTargets(preview);
+	const effectiveVersionChoice = showVersionStrategy ? versionChoice : "update";
 	// Only main-plan changes propagate to variants; license-link edits stay on
 	// the selected parent version.
</file context>
Suggested change
const effectiveVersionChoice = showVersionStrategy ? versionChoice : "update";
const effectiveVersionChoice = showVersionStrategy
? versionChoice
: isMetadataOnly
? "all"
: "update";

Comment thread server/src/internal/catalog/actions/catalogPlanPreflight.ts
const targetId = getLicenseParentTargetId(entry);
if (!selectedLicenseParentIdSet.has(targetId)) continue;
const planChanges = entry.license_changes[0]?.plan_changes;
const createsNewVersion = isNewVersion && entry.has_customers;

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: Parent review can label a new version when this parent has customers but its propagated change is not versionable. Derive the status from entry.versionable, matching the backend preview and strategy logic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vite/src/views/products/plan/versioning/buildMigrateTargets.ts, line 143:

<comment>Parent review can label a new version when this parent has customers but its propagated change is not versionable. Derive the status from `entry.versionable`, matching the backend preview and strategy logic.</comment>

<file context>
@@ -0,0 +1,164 @@
+		const targetId = getLicenseParentTargetId(entry);
+		if (!selectedLicenseParentIdSet.has(targetId)) continue;
+		const planChanges = entry.license_changes[0]?.plan_changes;
+		const createsNewVersion = isNewVersion && entry.has_customers;
+		targets.push({
+			id: `license-parent:${targetId}`,
</file context>
Suggested change
const createsNewVersion = isNewVersion && entry.has_customers;
const createsNewVersion = isNewVersion && entry.versionable;

priceIds: currentFullProduct.prices.map((price) => price.id),
priceIds,
}),
licenseItemRepo.listReferencedEntitlementIds({ db, entitlementIds }),

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: Catalog updates fetch every matching license reference merely to decide whether any exists. A heavily reused item can return thousands of rows; add an existence/LIMIT 1 repo query here, leaving full ID collection to retirement.

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/inPlaceUpdateUtils.ts, line 107:

<comment>Catalog updates fetch every matching license reference merely to decide whether any exists. A heavily reused item can return thousands of rows; add an existence/`LIMIT 1` repo query here, leaving full ID collection to retirement.</comment>

<file context>
@@ -85,20 +86,34 @@ export const productItemsHaveCustomerReferences = async ({
-			priceIds: currentFullProduct.prices.map((price) => price.id),
+			priceIds,
 		}),
+		licenseItemRepo.listReferencedEntitlementIds({ db, entitlementIds }),
+		licenseItemRepo.listReferencedPriceIds({ db, priceIds }),
 	]);
</file context>

@vercel
vercel Bot temporarily deployed to Preview – autumn-vite July 17, 2026 18:23 Inactive

@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.

1 issue found across 12 files (changes from recent commits).

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/tests/integration/crud/plans/variants/core-contract.test.ts">

<violation number="1" location="server/tests/integration/crud/plans/variants/core-contract.test.ts:34">
P3: Line 9 of the doc comment lists `preview_update is idempotent, no writes` as a contract item under test, but the corresponding test (section 7) was removed in this diff. Readers scanning the contract list will assume idempotency is still tested.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@@ -31,12 +31,12 @@ import {
type PlanUpdatePreview,

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: Line 9 of the doc comment lists preview_update is idempotent, no writes as a contract item under test, but the corresponding test (section 7) was removed in this diff. Readers scanning the contract list will assume idempotency is still tested.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/tests/integration/crud/plans/variants/core-contract.test.ts, line 34:

<comment>Line 9 of the doc comment lists `preview_update is idempotent, no writes` as a contract item under test, but the corresponding test (section 7) was removed in this diff. Readers scanning the contract list will assume idempotency is still tested.</comment>

<file context>
@@ -31,12 +31,12 @@ import {
 } from "@autumn/shared";
-import { TestFeature, getFeatures } from "@tests/setup/v2Features";
-import ctx from "@tests/utils/testInitUtils/createTestContext.js";
+import { getFeatures, TestFeature } from "@tests/setup/v2Features";
 import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js";
-import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
</file context>

@johnyeocx
johnyeocx merged commit 291bcb9 into dev Jul 20, 2026
16 checks passed
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