-
Notifications
You must be signed in to change notification settings - Fork 245
fix(licenses): reset transition usage unless carry-over is enabled #3255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| import { | ||
| type CarryOverUsages, | ||
| type EntitlementWithFeature, | ||
| featureUtils, | ||
| getStartingBalance, | ||
| isBooleanEntitlement, | ||
| isUnlimitedEntitlement, | ||
|
|
@@ -32,14 +34,35 @@ export const computeCustomerEntitlementInitialState = ({ | |
| }; | ||
| }; | ||
|
|
||
| /** Mirrors attach: allocated usage always carries, `carry_from_previous` | ||
| * carries its own entitlement, and the param carries listed consumables. */ | ||
| export const shouldCarryOverUsage = ({ | ||
| toEntitlement, | ||
| carryOverUsages, | ||
| }: { | ||
| toEntitlement: EntitlementWithFeature; | ||
| carryOverUsages: CarryOverUsages; | ||
| }): boolean => { | ||
| if (featureUtils.isAllocated(toEntitlement.feature)) return true; | ||
| if (toEntitlement.carry_from_previous) return true; | ||
| if (!carryOverUsages?.enabled) return false; | ||
| if (!carryOverUsages.feature_ids) return true; | ||
| return carryOverUsages.feature_ids.includes(toEntitlement.feature.id); | ||
| }; | ||
|
|
||
| const computeBalancePatch = ({ | ||
| fromInitialState, | ||
| toInitialState, | ||
| carryUsage, | ||
| }: { | ||
| fromInitialState: CustomerEntitlementInitialState; | ||
| toInitialState: CustomerEntitlementInitialState; | ||
| carryUsage: boolean; | ||
| }): CustomerEntitlementBalancePatch | undefined => { | ||
| if (fromInitialState.tracksBalance && toInitialState.tracksBalance) { | ||
| if (!carryUsage) { | ||
| return { type: "set", amount: toInitialState.granted }; | ||
| } | ||
| const amount = new Decimal(toInitialState.granted).sub( | ||
| fromInitialState.granted, | ||
| ); | ||
|
|
@@ -57,9 +80,11 @@ const computeBalancePatch = ({ | |
| export const computeCustomerEntitlementPatch = ({ | ||
| fromEntitlement, | ||
| toEntitlement, | ||
| carryOverUsages, | ||
| }: { | ||
| fromEntitlement: EntitlementWithFeature; | ||
| toEntitlement: EntitlementWithFeature; | ||
| carryOverUsages?: CarryOverUsages; | ||
| }): CustomerEntitlementPatch => { | ||
| if ( | ||
| isBooleanEntitlement({ entitlement: fromEntitlement }) || | ||
|
|
@@ -75,7 +100,11 @@ export const computeCustomerEntitlementPatch = ({ | |
| entitlement: toEntitlement, | ||
| }); | ||
| const patch: CustomerEntitlementPatch = {}; | ||
| const balance = computeBalancePatch({ fromInitialState, toInitialState }); | ||
| const balance = computeBalancePatch({ | ||
| fromInitialState, | ||
| toInitialState, | ||
| carryUsage: shouldCarryOverUsage({ toEntitlement, carryOverUsages }), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: When the migration Prompt for AI agents |
||
| }); | ||
|
|
||
| if (balance) patch.balance = balance; | ||
| if (fromInitialState.unlimited !== toInitialState.unlimited) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,14 @@ | ||
| import { | ||
| type CarryOverUsages, | ||
| EntInterval, | ||
| type EntitlementPrice, | ||
| type EntitlementWithFeature, | ||
| entToPooledBalanceIdentity, | ||
| entsAreSame, | ||
| entsHaveSamePooledIdentity, | ||
| entToPooledBalanceIdentity, | ||
| type InitCustomerEntitlementContext, | ||
| type InitFullCustomerProductOptions, | ||
| isBooleanEntitlement, | ||
| PooledBalanceResetMode, | ||
| } from "@autumn/shared"; | ||
| import { initCustomerEntitlementFields } from "@/internal/billing/v2/utils/initFullCustomerProduct/initCustomerEntitlement/initCustomerEntitlementFields"; | ||
|
|
@@ -23,6 +25,7 @@ import type { | |
| import { | ||
| computeCustomerEntitlementInitialState, | ||
| computeCustomerEntitlementPatch, | ||
| shouldCarryOverUsage, | ||
| } from "./computeCustomerEntitlementPatch"; | ||
|
|
||
| const findCandidateEntitlementIds = ({ | ||
|
|
@@ -47,9 +50,11 @@ const findCandidateEntitlementIds = ({ | |
| const computeReplaceOperation = ({ | ||
| candidateOutgoingEntitlements, | ||
| transition, | ||
| carryOverUsages, | ||
| }: { | ||
| candidateOutgoingEntitlements: EntitlementWithFeature[]; | ||
| transition: EntitlementPriceTransition; | ||
| carryOverUsages?: CarryOverUsages; | ||
| }): ReplaceEntitlementPriceOperation | undefined => { | ||
| const { fromEntitlementPrice, toEntitlementPrice } = transition; | ||
| const fromEntitlement = fromEntitlementPrice.entitlement; | ||
|
|
@@ -62,15 +67,19 @@ const computeReplaceOperation = ({ | |
| (entitlementId) => | ||
| !definitionsAreSame || entitlementId !== toEntitlement.id, | ||
| ); | ||
| if (fromEntitlementIds.length === 0) return undefined; | ||
| if (fromEntitlementIds.length === 0) { | ||
| if (!definitionsAreSame) return undefined; | ||
| fromEntitlementIds.push(toEntitlement.id); | ||
| } | ||
|
|
||
| const fromIsPooled = fromEntitlement.pooled === true; | ||
| const toIsPooled = toEntitlement.pooled === true; | ||
| const isPooledReplace = fromIsPooled && toIsPooled; | ||
| const customerEntitlementPatch = computeCustomerEntitlementPatch({ | ||
| fromEntitlement, | ||
| toEntitlement, | ||
| carryOverUsages, | ||
| }); | ||
| const fromIsPooled = fromEntitlement.pooled === true; | ||
| const toIsPooled = toEntitlement.pooled === true; | ||
| const isPooledReplace = fromIsPooled && toIsPooled; | ||
| if (!isPooledReplace) { | ||
| return { | ||
| type: "replace", | ||
|
|
@@ -82,10 +91,15 @@ const computeReplaceOperation = ({ | |
| }; | ||
| } | ||
|
|
||
| const incrementAmount = | ||
| const pooledContributionPatch = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When a pooled transition carries usage and the incoming grant equals the outgoing grant, Prompt for AI agents |
||
| customerEntitlementPatch.balance?.type === "increment" | ||
| ? customerEntitlementPatch.balance.amount | ||
| : 0; | ||
| ? customerEntitlementPatch.balance | ||
| : { | ||
| type: "set" as const, | ||
| amount: computeCustomerEntitlementInitialState({ | ||
| entitlement: toEntitlement, | ||
| }).granted, | ||
| }; | ||
|
|
||
| return { | ||
| type: "replace", | ||
|
|
@@ -96,7 +110,7 @@ const computeReplaceOperation = ({ | |
| customerEntitlementPatch: { | ||
| unlimited: customerEntitlementPatch.unlimited, | ||
| }, | ||
| pooledContributionPatch: { type: "increment", amount: incrementAmount }, | ||
| pooledContributionPatch, | ||
| }; | ||
| }; | ||
|
|
||
|
|
@@ -196,11 +210,13 @@ const computeTransitionOperations = ({ | |
| transition, | ||
| initContext, | ||
| initOptions, | ||
| carryOverUsages, | ||
| }: { | ||
| candidateOutgoingEntitlements: EntitlementWithFeature[]; | ||
| transition: EntitlementPriceTransition; | ||
| initContext: InitCustomerEntitlementContext; | ||
| initOptions: InitFullCustomerProductOptions; | ||
| carryOverUsages?: CarryOverUsages; | ||
| }): EntitlementPriceOperation[] => { | ||
| const fromEntitlement = transition.fromEntitlementPrice.entitlement; | ||
| const toEntitlement = transition.toEntitlementPrice.entitlement; | ||
|
|
@@ -215,6 +231,7 @@ const computeTransitionOperations = ({ | |
| const operation = computeReplaceOperation({ | ||
| candidateOutgoingEntitlements, | ||
| transition, | ||
| carryOverUsages, | ||
| }); | ||
| return operation ? [operation] : []; | ||
| } | ||
|
|
@@ -244,18 +261,21 @@ export const computeEntitlementPriceOperations = ({ | |
| entitlementPriceTransitions, | ||
| customerEntitlementInitContext, | ||
| customerEntitlementInitOptions, | ||
| carryOverUsages, | ||
| }: { | ||
| candidateOutgoingEntitlements: EntitlementWithFeature[]; | ||
| entitlementPriceTransitions: ComputedEntitlementPriceTransitions; | ||
| customerEntitlementInitContext: InitCustomerEntitlementContext; | ||
| customerEntitlementInitOptions: InitFullCustomerProductOptions; | ||
| carryOverUsages?: CarryOverUsages; | ||
| }): { | ||
| operations: EntitlementPriceOperation[]; | ||
| unhandled: ComputedEntitlementPriceTransitions; | ||
| } => { | ||
| const operations: EntitlementPriceOperation[] = []; | ||
| const unhandled: ComputedEntitlementPriceTransitions = { | ||
| transitions: [], | ||
| retained: [], | ||
| added: [], | ||
| deleted: [], | ||
| }; | ||
|
|
@@ -275,10 +295,44 @@ export const computeEntitlementPriceOperations = ({ | |
| transition, | ||
| initContext: customerEntitlementInitContext, | ||
| initOptions: customerEntitlementInitOptions, | ||
| carryOverUsages, | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| for (const transition of entitlementPriceTransitions.retained) { | ||
| if ( | ||
| isBooleanEntitlement({ | ||
| entitlement: transition.toEntitlementPrice.entitlement, | ||
| }) | ||
| ) { | ||
| continue; | ||
| } | ||
| if ( | ||
| hasPrice(transition.fromEntitlementPrice) || | ||
| hasPrice(transition.toEntitlementPrice) | ||
| ) { | ||
| unhandled.retained.push(transition); | ||
| continue; | ||
| } | ||
|
|
||
| if ( | ||
| shouldCarryOverUsage({ | ||
| toEntitlement: transition.toEntitlementPrice.entitlement, | ||
| carryOverUsages, | ||
| }) | ||
| ) { | ||
| continue; | ||
| } | ||
|
|
||
| const operation = computeReplaceOperation({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: When a license has multiple IDs for the same retained definition, this reset omits rows already using Prompt for AI agents |
||
| candidateOutgoingEntitlements, | ||
| transition, | ||
| carryOverUsages, | ||
| }); | ||
| if (operation) operations.push(operation); | ||
| } | ||
|
|
||
| for (const entitlementPrice of entitlementPriceTransitions.added) { | ||
| if (hasPrice(entitlementPrice)) { | ||
| unhandled.added.push(entitlementPrice); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,8 @@ export type EntitlementPriceTransition = { | |
|
|
||
| export type ComputedEntitlementPriceTransitions = { | ||
| transitions: EntitlementPriceTransition[]; | ||
| /** Same-definition survivors. They still need a replace when usage resets. */ | ||
| retained: EntitlementPriceTransition[]; | ||
| added: EntitlementPrice[]; | ||
| deleted: EntitlementPrice[]; | ||
| }; | ||
|
|
@@ -53,6 +55,7 @@ export const computeEntitlementPriceTransitions = ({ | |
| } | ||
|
|
||
| const transitions: EntitlementPriceTransition[] = []; | ||
| const retained: EntitlementPriceTransition[] = []; | ||
| const deleted: EntitlementPrice[] = []; | ||
| fromEntitlementPrices.forEach((fromEntitlementPrice, fromIndex) => { | ||
| const toEntitlementPrice = matchedByFromIndex.get(fromIndex); | ||
|
|
@@ -73,13 +76,16 @@ export const computeEntitlementPriceTransitions = ({ | |
| }) | ||
| ) { | ||
| transitions.push({ fromEntitlementPrice, toEntitlementPrice }); | ||
| return; | ||
| } | ||
|
|
||
| retained.push({ fromEntitlementPrice, toEntitlementPrice }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Same-definition survivors now land in Prompt for AI agents |
||
| }); | ||
|
|
||
| const added = toEntitlementPrices.filter( | ||
| (toEntitlementPrice) => | ||
| !claimedToEntitlementIds.has(toEntitlementPrice.entitlement.id), | ||
| ); | ||
|
|
||
| return { transitions, added, deleted }; | ||
| return { transitions, retained, added, deleted }; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Retained-only product changes are still discarded before
batchTransitionruns, so their usage is not reset. Includeretainedin the upstream no-op/transition detection as well.Prompt for AI agents