11import {
22 type CatalogPropagateTargetParams ,
33 type FullPlanLicense ,
4+ type FullProduct ,
45 type LicenseCustomize ,
56 productKeyToString ,
67 productToProductKey ,
@@ -9,6 +10,7 @@ import type { ProductStatesContext } from "@/internal/catalogV2/actions/updateCa
910import type { UpsertProductPlan } from "@/internal/catalogV2/actions/updateCatalog/types/upsertProductPlan" ;
1011import { activeVersionForPlan } from "@/internal/catalogV2/actions/updateCatalog/utils/productStateUtils/activeVersionForPlan" ;
1112import { findFullProductByInternalId } from "@/internal/catalogV2/actions/updateCatalog/utils/productStateUtils/findFullProductByInternalId" ;
13+ import { versionForSlug } from "@/internal/catalogV2/actions/updateCatalog/utils/productStateUtils/versionForSlug" ;
1214
1315/** Current plan_license links on this row. Minted versions fall back to the clone source. */
1416export const upsertProductPlanToLicenses = ( {
@@ -35,35 +37,81 @@ const childSourceInternalIds = ({
3537 ) ,
3638] ;
3739
38- /** Incoming links on the upserted child plus the demoted pointer (promote). */
40+ const uniqueReverseLinks = ( {
41+ products,
42+ } : {
43+ products : Array < FullProduct | null | undefined > ;
44+ } ) => {
45+ const seen = new Set < string > ( ) ;
46+ return products . flatMap ( ( product ) => {
47+ if ( ! product ) return [ ] ;
48+ return ( product . parent_plan_licenses ?? [ ] ) . filter ( ( link ) => {
49+ const key = productKeyToString ( {
50+ productKey : productToProductKey ( { product : link . product } ) ,
51+ } ) ;
52+ if ( seen . has ( key ) ) return false ;
53+ seen . add ( key ) ;
54+ return true ;
55+ } ) ;
56+ } ) ;
57+ } ;
58+
59+ /** Incoming links on every live version row of this child plan. */
60+ export const reverseLinksOnChildPlan = ( {
61+ planId,
62+ productStatesContext,
63+ } : {
64+ planId : string ;
65+ productStatesContext : ProductStatesContext ;
66+ } ) =>
67+ uniqueReverseLinks ( {
68+ products : productStatesContext . versionsByPlanId [ planId ] ?? [ ] ,
69+ } ) ;
70+
71+ /** Incoming links on one child version row. */
72+ export const reverseLinksOnChildProduct = ( {
73+ planId,
74+ childInternalId,
75+ productStatesContext,
76+ } : {
77+ planId : string ;
78+ childInternalId : string ;
79+ productStatesContext : ProductStatesContext ;
80+ } ) => {
81+ const childRow = ( productStatesContext . versionsByPlanId [ planId ] ?? [ ] ) . find (
82+ ( row ) => row . internal_id === childInternalId ,
83+ ) ;
84+ return uniqueReverseLinks ( { products : [ childRow ] } ) ;
85+ } ;
86+
87+ /** Incoming links on the planned child row, plus the demoted pointer (promote). */
3988export const reverseLinksForChild = ( {
4089 upsert,
4190 productStatesContext,
4291} : {
4392 upsert : UpsertProductPlan ;
4493 productStatesContext : ProductStatesContext ;
4594} ) => {
95+ const plannedInternalId =
96+ upsert . row . currentFullProduct ?. internal_id ??
97+ upsert . row . baseFullProduct ?. internal_id ??
98+ upsert . row . nextFullProduct . internal_id ;
99+ const hydrated = (
100+ productStatesContext . versionsByPlanId [ upsert . row . planId ] ?? [ ]
101+ ) . find ( ( row ) => row . internal_id === plannedInternalId ) ;
46102 const previousActive = upsert . previousActiveInternalId
47103 ? findFullProductByInternalId ( {
48104 internalId : upsert . previousActiveInternalId ,
49105 productStatesContext,
50106 } )
51107 : null ;
52- const seen = new Set < string > ( ) ;
53- return [
54- upsert . row . currentFullProduct ,
55- upsert . row . baseFullProduct ,
56- previousActive ,
57- ] . flatMap ( ( product ) => {
58- if ( ! product ) return [ ] ;
59- return ( product . parent_plan_licenses ?? [ ] ) . filter ( ( link ) => {
60- const key = productKeyToString ( {
61- productKey : productToProductKey ( { product : link . product } ) ,
62- } ) ;
63- if ( seen . has ( key ) ) return false ;
64- seen . add ( key ) ;
65- return true ;
66- } ) ;
108+ return uniqueReverseLinks ( {
109+ products : [
110+ hydrated ,
111+ upsert . row . currentFullProduct ,
112+ upsert . row . baseFullProduct ,
113+ previousActive ,
114+ ] ,
67115 } ) ;
68116} ;
69117
@@ -74,8 +122,14 @@ export const parentLicenseLinkForChild = ({
74122 parent : UpsertProductPlan ;
75123 child : UpsertProductPlan ;
76124} ) : FullPlanLicense | undefined => {
125+ const licenses = upsertProductPlanToLicenses ( { upsert : parent } ) ;
126+ // Adopt mint clones the old row's link; pair by child plan so follow can
127+ // re-anchor onto the child's active row in this batch.
128+ if ( parent . row . source === "license_adopt" ) {
129+ return licenses . find ( ( link ) => link . product . id === child . row . planId ) ;
130+ }
77131 const sourceInternalIds = childSourceInternalIds ( { child } ) ;
78- return upsertProductPlanToLicenses ( { upsert : parent } ) . find (
132+ return licenses . find (
79133 ( link ) =>
80134 link . product . id === child . row . planId &&
81135 ( sourceInternalIds . length === 0 ||
@@ -110,17 +164,25 @@ const propagateTargetMatchesParent = ({
110164 target,
111165 parent,
112166 activeVersion,
167+ productStatesContext,
113168} : {
114169 target : CatalogPropagateTargetParams ;
115170 parent : UpsertProductPlan ;
116171 activeVersion : number | undefined ;
172+ productStatesContext : ProductStatesContext ;
117173} ) : boolean => {
118174 if ( target . plan_id !== parent . row . planId ) return false ;
119175 if ( target . version !== undefined ) return target . version === parent . row . version ;
176+ if ( target . version_slug !== undefined ) {
177+ const version = versionForSlug ( {
178+ planId : target . plan_id ,
179+ versionSlug : target . version_slug ,
180+ productStatesContext,
181+ } ) ;
182+ return version !== undefined && version === parent . row . version ;
183+ }
120184 if ( target . versioning === "all_versions" ) return true ;
121- return (
122- activeVersion !== undefined && parent . row . version === activeVersion
123- ) ;
185+ return activeVersion !== undefined && parent . row . version === activeVersion ;
124186} ;
125187
126188export const childPropagatesToParent = ( {
@@ -137,7 +199,12 @@ export const childPropagatesToParent = ({
137199 productStatesContext,
138200 } ) ;
139201 return ( child . propagate ?. license_parents ?? [ ] ) . some ( ( target ) =>
140- propagateTargetMatchesParent ( { target, parent, activeVersion } ) ,
202+ propagateTargetMatchesParent ( {
203+ target,
204+ parent,
205+ activeVersion,
206+ productStatesContext,
207+ } ) ,
141208 ) ;
142209} ;
143210
@@ -153,18 +220,36 @@ export const movesActivePointer = ({
153220 return nextIsActive && ( mintedNewRow || promotedExisting ) ;
154221} ;
155222
156- /** In- place item writes, or the child taking the pointer. Draft-mint clones do not . */
157- export const childTriggersLicenseRewrite = ( {
223+ /** Pin-lane trigger: in- place item writes only. Mints/promotes leave anchored links alone . */
224+ export const childEditsItemsInPlace = ( {
158225 child,
159226} : {
160227 child : UpsertProductPlan ;
161228} ) : boolean => {
162229 const mintedNewRow = child . row . versioning === "new_version" ;
163230 const childHasItemWrites = child . entitlementPricesPlan != null ;
164- const inPlaceItemWrites = childHasItemWrites && ! mintedNewRow ;
165- return inPlaceItemWrites || movesActivePointer ( { upsert : child } ) ;
231+ return childHasItemWrites && ! mintedNewRow ;
166232} ;
167233
234+ /** Propagate-lane trigger: in-place item writes, or the child taking the pointer. */
235+ export const childTriggersLicenseRewrite = ( {
236+ child,
237+ } : {
238+ child : UpsertProductPlan ;
239+ } ) : boolean =>
240+ childEditsItemsInPlace ( { child } ) || movesActivePointer ( { upsert : child } ) ;
241+
242+ /** In-place follow only reaches the row this link already points at. Mint/promote still move it. */
243+ export const propagateReachesLink = ( {
244+ currentPlanLicense,
245+ child,
246+ } : {
247+ currentPlanLicense : FullPlanLicense ;
248+ child : UpsertProductPlan ;
249+ } ) : boolean =>
250+ movesActivePointer ( { upsert : child } ) ||
251+ ! needsRepoint ( { currentPlanLicense, child } ) ;
252+
168253export const shouldPropagate = ( {
169254 parent,
170255 child,
0 commit comments