Skip to content

Fix versioned license pricing in plan dashboard - #2291

Merged
charlietlamb merged 1 commit into
devfrom
charlie/licneses-config-fixes
Jul 17, 2026
Merged

Fix versioned license pricing in plan dashboard#2291
charlietlamb merged 1 commit into
devfrom
charlie/licneses-config-fixes

Conversation

@charlietlamb

@charlietlamb charlietlamb commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • preserve the license plan version returned by plans.get
  • resolve linked license products by public ID and pinned version
  • keep staged license links resolving to the latest available version

Validation

  • bun test tests/views/products/plan/resolve-plan-license-product.test.ts
  • bun run ts
  • pre-commit knip and scoped Vite typecheck

Summary by cubic

Fixes plan dashboard license pricing by resolving linked license products using the plan-pinned version. Staged links still use the latest version so previews stay current.

  • Bug Fixes
    • Preserve license link version from plans.get via VersionedPlanLicense.
    • Resolve linked products by id + version using resolvePlanLicenseProduct and useProductsQuery({ allVersions: true }).
    • Keep staged (pending) links pointing to the latest available version.
    • Add unit test to confirm pinned-version resolution.

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

Review in cubic

Greptile Summary

This PR fixes incorrect pricing shown for versioned license products in the plan dashboard by preserving the version number returned by plans.get and using it to resolve persisted license links to their pinned product version rather than an arbitrary latest entry.

  • Bug fixes | Improvements: usePlanLicensesQuery now maps link.version into a new VersionedPlanLicense type, and useResolvedPlanLicenses passes that version to a new resolvePlanLicenseProduct helper that matches on both id and version — ensuring the dashboard shows the price configured at the time the link was created.
  • Improvements: Staged/pending license links continue to resolve to the latest available version via a licenseById map built from the full allVersions: true product list, which was already a supported query option with its own cache key.
  • Improvements: A targeted unit test is added to verify the version-pinning behaviour of resolvePlanLicenseProduct.

Confidence Score: 4/5

  • The change is a focused, well-scoped fix: it threads the version number from the API response through to product resolution and correctly splits persisted vs. staged license lookup paths.
  • The new resolution logic is correct and the unit test validates the core helper. Two minor style concerns exist: the licenseById map is built with a redundant first pass that makes the !latest branch unreachable, and ResolvedPlanLicense.planLicense is still typed as the base PlanLicense rather than VersionedPlanLicense, hiding the .version field from downstream consumers without a cast.
  • vite/src/views/products/plan/components/plan-licenses/useResolvedPlanLicenses.ts — the redundant map initialization and the interface type narrowing are both worth a second look before merge.

Important Files Changed

Filename Overview
vite/src/hooks/queries/usePlanLicensesQuery.tsx Exports new VersionedPlanLicense type and maps link.version from the API response into the returned plan-license objects; straightforward and correct.
vite/src/views/products/plan/components/plan-licenses/resolvePlanLicenseProduct.ts New helper that finds a ProductV2 by public id and exact version; logic is correct and well-covered by the new unit test.
vite/src/views/products/plan/components/plan-licenses/useResolvedPlanLicenses.ts Switches persisted licenses to version-pinned resolution and staged licenses to latest-version resolution; contains a redundant two-pass map construction where the first pass's result is immediately overwritten by the loop.
vite/tests/views/products/plan/resolve-plan-license-product.test.ts New unit test validating that resolvePlanLicenseProduct picks the pinned version over the latest; happy-path coverage is correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[useResolvedPlanLicenses] --> B{pageCatalogLicenses?}
    B -- Yes --> C[Use server-resolved licenses]
    B -- No --> D[usePlanLicensesQuery returns VersionedPlanLicense array]

    D --> E[resolvePlanLicenseProduct - match by id AND version]
    E --> F[persistedLicenses with version-pinned ProductV2]

    A --> G[useProductsQuery with allVersions true]
    G --> H[licenseById Map - id to latest version ProductV2]

    A --> I[pendingLicenseIds + initialPatches]
    I --> J[stagedIds not in persistedIds]
    J --> K[licenseById.get - latest version ProductV2]
    K --> L[pendingLicenses]

    F --> M[return persistedLicenses + pendingLicenses]
    C --> M
    L --> M
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[useResolvedPlanLicenses] --> B{pageCatalogLicenses?}
    B -- Yes --> C[Use server-resolved licenses]
    B -- No --> D[usePlanLicensesQuery returns VersionedPlanLicense array]

    D --> E[resolvePlanLicenseProduct - match by id AND version]
    E --> F[persistedLicenses with version-pinned ProductV2]

    A --> G[useProductsQuery with allVersions true]
    G --> H[licenseById Map - id to latest version ProductV2]

    A --> I[pendingLicenseIds + initialPatches]
    I --> J[stagedIds not in persistedIds]
    J --> K[licenseById.get - latest version ProductV2]
    K --> L[pendingLicenses]

    F --> M[return persistedLicenses + pendingLicenses]
    C --> M
    L --> M
Loading

Comments Outside Diff (1)

  1. vite/src/views/products/plan/components/plan-licenses/useResolvedPlanLicenses.ts, line 17-21 (link)

    P2 ResolvedPlanLicense.planLicense is still typed as the base PlanLicense, not the narrower VersionedPlanLicense. Persisted licenses are now resolved from fallbackPlanLicenses (type VersionedPlanLicense[]), so the runtime object does carry .version — but it is invisible to callers through this interface. Any downstream component that needs the pinned version for display or comparison would have to cast. Consider widening the type to PlanLicense | VersionedPlanLicense or replacing it with VersionedPlanLicense to make the contract explicit.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: vite/src/views/products/plan/components/plan-licenses/useResolvedPlanLicenses.ts
    Line: 17-21
    
    Comment:
    `ResolvedPlanLicense.planLicense` is still typed as the base `PlanLicense`, not the narrower `VersionedPlanLicense`. Persisted licenses are now resolved from `fallbackPlanLicenses` (type `VersionedPlanLicense[]`), so the runtime object does carry `.version` — but it is invisible to callers through this interface. Any downstream component that needs the pinned version for display or comparison would have to cast. Consider widening the type to `PlanLicense | VersionedPlanLicense` or replacing it with `VersionedPlanLicense` to make the contract explicit.
    
    How can I resolve this? If you propose a fix, please make it concise.
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
vite/src/views/products/plan/components/plan-licenses/useResolvedPlanLicenses.ts:45-53
Redundant two-pass map construction — `new Map(products.map(...))` already populates every ID, so `!latest` in the loop is always `false` (dead code). The first pass builds a map where the last item for each duplicate ID wins arbitrarily; the subsequent loop then fully rewrites it to highest-version-wins anyway. A single loop is both clearer and avoids the intermediate allocation.

```suggestion
		const licenseById = new Map<string, (typeof products)[0]>();
		for (const license of products) {
			const latest = licenseById.get(license.id);
			if (!latest || license.version > latest.version) {
				licenseById.set(license.id, license);
			}
		}
```

### Issue 2 of 2
vite/src/views/products/plan/components/plan-licenses/useResolvedPlanLicenses.ts:17-21
`ResolvedPlanLicense.planLicense` is still typed as the base `PlanLicense`, not the narrower `VersionedPlanLicense`. Persisted licenses are now resolved from `fallbackPlanLicenses` (type `VersionedPlanLicense[]`), so the runtime object does carry `.version` — but it is invisible to callers through this interface. Any downstream component that needs the pinned version for display or comparison would have to cast. Consider widening the type to `PlanLicense | VersionedPlanLicense` or replacing it with `VersionedPlanLicense` to make the contract explicit.

Reviews (1): Last reviewed commit: "fix(dashboard): resolve pinned license p..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

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

@charlietlamb
charlietlamb merged commit d7d52a6 into dev Jul 17, 2026
15 checks passed
@charlietlamb
charlietlamb deleted the charlie/licneses-config-fixes branch July 17, 2026 14:31
Comment on lines 45 to +53
const licenseById = new Map(
products.map((license) => [license.id, license]),
);
for (const license of products) {
const latest = licenseById.get(license.id);
if (!latest || license.version > latest.version) {
licenseById.set(license.id, license);
}
}

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 two-pass map construction — new Map(products.map(...)) already populates every ID, so !latest in the loop is always false (dead code). The first pass builds a map where the last item for each duplicate ID wins arbitrarily; the subsequent loop then fully rewrites it to highest-version-wins anyway. A single loop is both clearer and avoids the intermediate allocation.

Suggested change
const licenseById = new Map(
products.map((license) => [license.id, license]),
);
for (const license of products) {
const latest = licenseById.get(license.id);
if (!latest || license.version > latest.version) {
licenseById.set(license.id, license);
}
}
const licenseById = new Map<string, (typeof products)[0]>();
for (const license of products) {
const latest = licenseById.get(license.id);
if (!latest || license.version > latest.version) {
licenseById.set(license.id, license);
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: vite/src/views/products/plan/components/plan-licenses/useResolvedPlanLicenses.ts
Line: 45-53

Comment:
Redundant two-pass map construction — `new Map(products.map(...))` already populates every ID, so `!latest` in the loop is always `false` (dead code). The first pass builds a map where the last item for each duplicate ID wins arbitrarily; the subsequent loop then fully rewrites it to highest-version-wins anyway. A single loop is both clearer and avoids the intermediate allocation.

```suggestion
		const licenseById = new Map<string, (typeof products)[0]>();
		for (const license of products) {
			const latest = licenseById.get(license.id);
			if (!latest || license.version > latest.version) {
				licenseById.set(license.id, license);
			}
		}
```

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!

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