Skip to content

Release assignments when plans drop licenses - #2348

Merged
charlietlamb merged 7 commits into
devfrom
charlie/release-dropped-license-assignments
Jul 22, 2026
Merged

Release assignments when plans drop licenses#2348
charlietlamb merged 7 commits into
devfrom
charlie/release-dropped-license-assignments

Conversation

@charlietlamb

@charlietlamb charlietlamb commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • release active entity assignments when a plan transition drops their license pool
  • keep exact/group successor assignments intact and continue blocking ambiguous mappings
  • apply the same billing-plan operation across attach, sync/backsync, scheduled activation, expiry, and default activation
  • warn on complete license removal in Attach Product review and list the affected entities without changing billing.preview_attach

Implementation

  • compute emits only dropped customer-license link IDs
  • execute unassigns seats and restores pool capacity with one set-based, retry-safe statement
  • the dashboard derives affected entities from existing license assignment and plan queries

Testing

  • cd server && bunx tsgo --build --noEmit
  • cd vite && bun run ts
  • focused dropped-license integration regression: 1 passed
  • Stripe cancellation/backsync license suite: 2 passed
  • Biome checks passed
  • React Doctor reported no issues

Summary by cubic

Automatically release assigned licenses when a plan drops its license pool, scoped to the exact customer and pools. Restores capacity, preserves exact/group successors, and still blocks ambiguous mappings with clearer guidance.

  • New Features

    • Release and restore capacity across attach, sync/backsync, scheduled/default activation, and expiry.
    • Unify license computation via computeCustomerLicenseChanges and computeCustomerLicenseReleases; execute via shared plan with releaseCustomerLicenseAssignments; remove redundant release flow.
    • Centralize the release action type as CustomerLicenseAssignmentRelease in @autumn/shared; added to AutumnBillingPlan and deduped/merged in mergeAutumnBillingPlans.
    • Show an attach review warning listing entities that will lose licenses on complete removal via useLicenseLossEntities; billing.preview_attach is unchanged.
  • Bug Fixes

    • Scope releases by internalCustomerId and exact pool (id + linkId) so only the intended customer and pool are modified.
    • Clarify the ambiguous-license error with guidance on resolving group conflicts.

Written for commit e44cdb7. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR releases assignments when billing-plan transitions remove license pools. The main changes are:

  • Bug fixes: Compute dropped pools across attach, sync, activation, and expiry flows.
  • Improvements: Release assignments and restore pool capacity with one set-based statement.
  • API changes: Add release instructions to the shared billing-plan model and merge path.
  • Improvements: Show affected entities in the Attach Product review.

Confidence Score: 4/5

The release query and partial-drop warning need fixes before merging.

  • A link collision can release assignments outside the target customer.
  • Partial license removals execute without an Attach Product warning.
  • The main release computation and retry guard otherwise follow the existing billing-plan flow.

server/src/internal/licenses/repos/licenseAssignmentRepo.ts; vite/src/components/forms/attach-v2/components/AttachLicenseLossWarning.tsx

Security Review

The release update is not scoped by customer or organization. A license-link collision can modify assignments and pool capacity belonging to another customer.

Important Files Changed

Filename Overview
server/src/internal/billing/v2/compute/customerLicenseTransitions/computeCustomerLicenseReleases.ts Computes release instructions for outgoing license pools without successors.
server/src/internal/licenses/repos/licenseAssignmentRepo.ts Adds set-based assignment release and capacity restoration without customer scoping.
server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts Runs assignment releases through the shared billing-plan executor.
server/src/internal/billing/v2/utils/billingPlan/mergeAutumnBillingPlans.ts Deduplicates release link IDs when billing plans are combined.
vite/src/components/forms/attach-v2/components/AttachLicenseLossWarning.tsx Adds an affected-entity warning but suppresses it for partial license drops.

Sequence Diagram

sequenceDiagram
    participant Flow as Attach/Sync/Activation
    participant Compute as Plan Compute
    participant Execute as Plan Executor
    participant DB as License Repository
    Flow->>Compute: Outgoing and incoming products
    Compute->>Compute: Match license successors
    Compute-->>Execute: Dropped license link IDs
    Execute->>DB: Release active assignments
    DB->>DB: Clear entity bindings
    DB->>DB: Restore pool capacity
Loading
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/repos/licenseAssignmentRepo.ts:156-161
**Release Query Lacks Customer Scope**

The update selects assignments only by `customer_license_link_id`, while the plan already identifies the customer and the schema does not enforce global uniqueness for this link. If two customers have the same link value, one customer's plan change clears the other customer's assignments and increments the wrong pools.

### Issue 2 of 2
vite/src/components/forms/attach-v2/components/AttachLicenseLossWarning.tsx:40-43
**Partial License Drops Hide Warning**

This returns no affected entities whenever the incoming plan contains any license. If a plan retains license A but drops license B, the backend releases B's assignments while the review screen shows no warning.

Reviews (1): Last reviewed commit: "fix(billing): release assignments for dr..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used:

  • Context used - When generating the key changes section of the sum... (source)

Automatically unassign entity licenses when a plan transition removes their license pool, preserve ambiguous-transition safeguards, and warn affected entities in the attach review.
Comment thread server/src/internal/licenses/repos/licenseAssignmentRepo.ts Outdated
Comment thread vite/src/components/forms/attach-v2/components/AttachLicenseLossWarning.tsx Outdated

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

Added 1 comment

Comment thread vite/src/components/forms/attach-v2/components/AttachLicenseLossWarning.tsx Outdated

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

All reported issues were addressed across 18 files

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.

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

Re-trigger cubic

Comment thread server/src/internal/licenses/repos/licenseAssignmentRepo.ts Outdated
Comment thread server/src/internal/customers/cusProducts/actions/expireAndActivateDefault.ts Outdated
Comment thread vite/src/components/forms/attach-v2/components/AttachLicenseLossWarning.tsx Outdated
Comment thread vite/src/components/forms/attach-v2/components/AttachLicenseLossWarning.tsx Outdated
@vercel

vercel Bot commented Jul 22, 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 22, 2026 11:36am
landing-page Ignored Ignored Jul 22, 2026 11:36am

Request Review

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

Added 1 comment


const keepsAnyLicense =
planLicenses.length + (formValues.addLicenses?.length ?? 0) > 0;
if (keepsAnyLicense) 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.

[🟡 Medium] [🔵 Bug]

The new guard returns no warning whenever the incoming plan still has any license, which hides real assignment loss when a transition drops only some pools (e.g. keeps license A, drops license B). That creates a misleading review step: backend execution still releases dropped assignments, but the UI shows no affected entities. Remove the keepsAnyLicense short-circuit and compute affected entities from previewDiff.outgoingLicenses so partial drops are surfaced.

// vite/src/components/forms/attach-v2/hooks/useLicenseLossEntities.ts
const keepsAnyLicense =
	planLicenses.length + (formValues.addLicenses?.length ?? 0) > 0;
if (keepsAnyLicense) return [];

@vercel
vercel Bot temporarily deployed to Preview – autumn-vite July 22, 2026 11:10 Inactive
@vercel
vercel Bot temporarily deployed to Preview – autumn-vite July 22, 2026 11:19 Inactive
@capy-ai

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

1 issue found across 17 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="vite/src/components/forms/attach-v2/hooks/useLicenseLossEntities.ts">

<violation number="1" location="vite/src/components/forms/attach-v2/hooks/useLicenseLossEntities.ts:21">
P2: useLicenseLossEntities derives outgoingLicenseIds from previewDiff.outgoingLicenses/previewDiff.hasOutgoingPlans without checking previewDiff.isDiffLoading, so right after the user changes product/version/items the warning can briefly reflect the previous selection's outgoing licenses before the diff recomputes. Consider gating on `!previewDiff.isDiffLoading` (return [] while loading) to avoid a stale/incorrect flash of the license-loss warning.</violation>
</file>

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

Re-trigger cubic

planLicenses.length + (formValues.addLicenses?.length ?? 0) > 0;
if (keepsAnyLicense) return [];

const outgoingLicenseIds = new Set(

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: useLicenseLossEntities derives outgoingLicenseIds from previewDiff.outgoingLicenses/previewDiff.hasOutgoingPlans without checking previewDiff.isDiffLoading, so right after the user changes product/version/items the warning can briefly reflect the previous selection's outgoing licenses before the diff recomputes. Consider gating on !previewDiff.isDiffLoading (return [] while loading) to avoid a stale/incorrect flash of the license-loss warning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vite/src/components/forms/attach-v2/hooks/useLicenseLossEntities.ts, line 21:

<comment>useLicenseLossEntities derives outgoingLicenseIds from previewDiff.outgoingLicenses/previewDiff.hasOutgoingPlans without checking previewDiff.isDiffLoading, so right after the user changes product/version/items the warning can briefly reflect the previous selection's outgoing licenses before the diff recomputes. Consider gating on `!previewDiff.isDiffLoading` (return [] while loading) to avoid a stale/incorrect flash of the license-loss warning.</comment>

<file context>
@@ -0,0 +1,45 @@
+		planLicenses.length + (formValues.addLicenses?.length ?? 0) > 0;
+	if (keepsAnyLicense) return [];
+
+	const outgoingLicenseIds = new Set(
+		previewDiff.outgoingLicenses.map((license) => license.license_plan_id),
+	);
</file context>

@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 17 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/src/internal/billing/v2/actions/sync/compute/computeSyncImmediatePhase.ts">

<violation number="1">
P1: Removing the computeCustomerLicenseReleases call from computeSyncImmediatePhase leaves the sync/backsync flow with no mechanism to release active license assignments when a synced plan transition drops a license pool, which contradicts the PR's stated goal of applying the same release/restore logic across attach, sync/backsync, activation, and expiry. Please confirm release logic for sync is now handled elsewhere (e.g. via a shared executeAutumnBillingPlan step) — if not, dropped license pools during Stripe sync will leave stale/active assignments un-released.</violation>
</file>

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

Re-trigger cubic

@charlietlamb
charlietlamb merged commit 5d89832 into dev Jul 22, 2026
15 checks passed
@charlietlamb
charlietlamb deleted the charlie/release-dropped-license-assignments branch July 22, 2026 11:44
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