Conversation
solaris007
left a comment
There was a problem hiding this comment.
Hey @ravverma,
Thanks for the fix. Four specialists reviewed this PR with a focus on backwards compatibility and cross-repo impact. The directional change is right, but there are several issues we want addressed before this ships to consumers.
Strengths
- Clean positive rewrite of the guard at
packages/spacecat-shared-tier-client/src/tier-client.js:153. Much easier to read than the prior double negative. - Intent-revealing comment at lines 151-152 naming the four tiers.
- Existing upgrade-to-PAID tests (PLG -> PAID, PRE_ONBOARD -> PAID, FREE_TRIAL -> PAID) still pass and correctly assert the new semantics.
- Symmetric test updates for PLG -> FREE_TRIAL and PRE_ONBOARD -> PLG no-ops; pre-existing enum gap flagged honestly in the PR body.
- Tier validation at
tier-client.js:140still throws on unknown strings, so the guard does not have to defend against garbage input.
Issues
Critical (Must Fix)
-
tier-client.test.jscreateEntitlement describe block: zero tests withcurrentTier = PAID. The entire invariant this PR enforces ("protect PAID from being overwritten") is unverified. Add PAID -> PAID, PAID -> PLG, PAID -> FREE_TRIAL, PAID -> PRE_ONBOARD; each assertssetTier/saveare not called andgetTier()still returnsPAID. -
tier-client.js:153-156silent no-op with misleading return. A caller invokingcreateEntitlement('FREE_TRIAL')on a PLG org now receivesexisting.entitlementwhosegetTier() === 'PLG', with no log, no error, no flag. Callers in spacecat-api-service (plg-onboarding,llmo-onboarding) and spacecat-fulfillment-worker cannot detect their request was ignored. Fix: addthis.log.warn({ orgId, currentTier, requestedTier }, 'Tier change skipped; createEntitlement only upgrades to PAID')in anelsebranch, or return{ entitlement, siteEnrollment, tierChanged: boolean }. -
tier-client.js:131-137stale JSDoc. Still says "If entitlement exists with different tier, updates the tier." That is no longer true. Replace with the actual rule (upgrade-to-PAID only; all other targets are ignored when an entitlement exists) so consumers do not have to read source. -
Semver:
fix:commit on a behavior change to a published shared library. Conventional Commits + semantic-release will treat this as a patch. Consumers upgrading without noticing will silently lose the non-PAID-override behavior they may have been relying on. Promote tofeat!:(or include aBREAKING CHANGE:footer) and bump major on publish. -
Downgrade / subscription-end path unspecified. With this change, PAID -> FREE_TRIAL and PAID -> PRE_ONBOARD are silent no-ops here. If any fulfillment flow ever ends a paid subscription through
createEntitlement, customers retain paid entitlement indefinitely (revenue-leakage and over-entitlement risk). Either document where subscription downgrade lives, or close the gap before merging.
Important (Should Fix)
-
Caller audit across dependent repos before release. Search
createEntitlement(usage inspacecat-api-service(PLG/LLMO onboarding controllers, Slack onboarding actions),spacecat-fulfillment-worker(aem-sites + llmo handlers),spacecat-audit-worker,spacecat-import-worker. Any caller that was relying on non-PAID upgrades (e.g. PRE_ONBOARD -> PLG on onboarding completion) is now a silent prod regression. Post the list in this PR before merge. -
tier-client.js:153hard-coded=== PAIDcouples the library to today's tier ladder. When ENTERPRISE or any other tier arrives, the predicate is wrong. Consider tier ordinal rank (targetRank > currentRank) or an explicit transition-policy map. Four tiers make this look cheap; at six it costs every call site. -
Transition policy lives in the wrong layer. Encoding tier-transition business rules in a shared client library forces every consumer to bump and redeploy when policy changes. Long-term, this belongs in the entitlement/data service with the client being a thin wrapper. This PR widens existing debt rather than creating it, but worth tracking.
-
packages/spacecat-shared-tier-client/README.md(around lines 119 and 178) needs updating. The canonical example still showscreateEntitlement('FREE_TRIAL')with no mention of the upgrade-only rule. Add a transition-rules table so consumers do not have to read source or tests. -
Test matrix beyond the PAID row is incomplete. 4 tiers = 16 transition cells; the suite covers about 6. Suggest replacing the existing two renamed tests with a parameterised
forEachover the full 16-cell transition table so future tier additions (e.g. ENTERPRISE) are a one-row change. -
PRE_ONBOARD enum gap promised but not delivered. The PR body documents PRE_ONBOARD as a supported tier in the behavior matrix, but
@mysticat/data-service-typesdoes not yet expose it, socreateEntitlement('PRE_ONBOARD')throwsInvalid tierattier-client.js:140-142. Documentation and runtime behavior diverge until the enum lands.
Minor (Nice to Have)
tier-client.js:153- extractisUpgradeToPaid(currentTier, tier)so the branch reads self-documenting and is testable in isolation.tier-client.js:151-152- drop the inline "Tiers: PRE_ONBOARD, PLG, FREE_TRIAL, PAID" comment. The enum is the source of truth and this list will rot.- Consider splitting
createEntitlementintoensureEntitlement(tier)(for new) andupgradeToPaid()(for transitions). The method name currently does triple duty (create / idempotent get / PAID-only upgrade). - Add a null/undefined
currentTiertest case for defensive coverage.
Recommendations
Documentation gap in mysticat-architecture. We checked. The only tier doc is products/aso/tier.md, which is about operational tier config (scan_frequency, max_pages, goal_facts, priority). The entitlement tier enum (PRE_ONBOARD / PLG / FREE_TRIAL / PAID) and transition rules are not documented anywhere. This PR encodes an architectural decision ("only upgrades to PAID are material; other transitions are no-ops in the shared client") as a one-line inline comment. Concrete proposal:
- Create
mysticat-architecture/platform/concepts/entitlement-tier.mddocumenting the enum, the transition state machine, the authoritative owner (TierClient today, data service future), and the consumer list. Frontmatterscope: platform, status: decided. - Create
mysticat-architecture/platform/decisions/NNN-entitlement-tier-transitions.mdas the ADR recording this decision. - Rename
products/aso/tier.mdtooperational-tier.md(or move toplatform/concepts/operational-tier.md) to end the name collision with the entitlement tier concept. - Cross-link from the tier-client README to the architecture doc.
Per DOCUMENTATION-GUIDE.md criterion "contract shared across repos", platform/ is the correct tier for this.
Beyond documentation: consider an integration test in at least one consumer repo once the version bumps, to confirm the new no-op does not break onboarding or fulfillment flows.
Assessment
Ready to merge? No, with fixes.
Reasoning: The logic is right, but the PR ships a silent behavior change on a shared library under a patch-level commit, with stale JSDoc, zero tests verifying the PAID invariant, no observability on the skipped-tier path, and an unspecified downgrade story. Address the five Critical items (tests with currentTier = PAID, log or signal the no-op, fix the JSDoc, promote to feat!:, resolve the downgrade path), audit callers, and file the architecture docs. Tier-ladder extensibility can be a tracked follow-up.
Next Steps
- Land the five Critical items in this PR (PAID-as-current tests, log/return-signal on skip, JSDoc, commit type, downgrade-path decision).
- Run the caller audit and document results in the PR body.
- Update the README transition-rules example.
- File the mysticat-architecture docs (concept + ADR + rename) as a separate PR.
- Minor items are optional.
| if (currentTier !== tier && currentTier !== ENTITLEMENT_TIERS.PAID) { | ||
| // Only upgrade the tier when the customer is becoming PAID; otherwise keep as-is. | ||
| // Tiers: PRE_ONBOARD, PLG, FREE_TRIAL, PAID. | ||
| if (tier === ENTITLEMENT_TIERS.PAID && currentTier !== ENTITLEMENT_TIERS.PAID) { |
There was a problem hiding this comment.
Critical: silent no-op with misleading return.
When this guard is false, the method still returns existing.entitlement from below, whose getTier() does NOT match the tier argument the caller passed. Callers in spacecat-api-service (PLG/LLMO onboarding) and spacecat-fulfillment-worker have no way to detect their requested tier was ignored: no log, no error, no flag on the return object.
Fix (pick one):
- Add
this.log.warn({ orgId, currentTier, requestedTier: tier }, 'Tier change skipped; createEntitlement only upgrades to PAID')in anelsebranch, OR - Return
{ entitlement, siteEnrollment, tierChanged: boolean }so callers can branch on it.
Important (separate finding, same line): the hard-coded === ENTITLEMENT_TIERS.PAID couples the library to today's 4-tier ladder. When ENTERPRISE or any variant is added, this predicate is wrong at every call site. Prefer a tier-ordinal comparison or an explicit transition-policy map.
| }); | ||
|
|
||
| it('should upgrade PLG entitlement to FREE_TRIAL (PLG is not PAID, so tier updates)', async () => { | ||
| it('should NOT change tier when upgrading PLG to FREE_TRIAL (only PAID upgrades are applied)', async () => { |
There was a problem hiding this comment.
Critical: the createEntitlement describe block has zero tests with currentTier = PAID.
The entire invariant this PR enforces ("protect PAID from being overwritten") is unverified. Please add, each asserting setTier/save are NOT called and the returned entitlement's getTier() still returns PAID:
- PAID -> PAID
- PAID -> PLG
- PAID -> FREE_TRIAL
- PAID -> PRE_ONBOARD
Also Important: consider replacing these two renamed tests with a parameterised forEach over the full 16-cell transition table (4 tiers x 4 targets). Future tier additions (e.g. ENTERPRISE) become a one-row change instead of N new it blocks.
Minor: the stale JSDoc at tier-client.js:131-137 still says "If entitlement exists with different tier, updates the tier" and needs updating to reflect the PAID-only rule.
Summary
Update
TierClient.createEntitlementso an existing entitlement's tier is only overridden when the customer is becoming PAID. Previously, any mismatch between the current and requested tier (as long as the current wasn't PAID) would trigger an override — which now doesn't fit with new additional tiers, we have four tiers now:PRE_ONBOARD,PLG,FREE_TRIAL,PAID.Changes
packages/spacecat-shared-tier-client/src/tier-client.jscreateEntitlement: change the upgrade guard fromcurrentTier !== tier && currentTier !== PAIDtotier === PAID && currentTier !== PAID.packages/spacecat-shared-tier-client/test/tier-client.test.jsPLG → FREE_TRIALtest to assertsetTier/saveare NOT called.PRE_ONBOARD → PLGtest to assertsetTier/saveare NOT called.PAIDtests continue to pass unchanged.Behavior Matrix
Test Plan
npm test -w packages/spacecat-shared-tier-client— 77 passing, 100% line/branch/function coverage ontier-client.js.PAIDmutate the entitlement; all other transitions leave the existing entitlement untouched.spacecat-api-service,spacecat-audit-worker,spacecat-import-worker) to be re-validated after publish.Notes
should create entitlement with PRE_ONBOARD tier when nothing exists) fails both before and after this change because@mysticat/data-service-typesdoes not yet exposePRE_ONBOARDin theENTITLEMENT_TIERenum. Out of scope for this PR; tracked separately.