Skip to content

fix: support versioned Mobbin catalog sync - #2315

Merged
charlietlamb merged 2 commits into
devfrom
charlie/mobbin-catalog-minimal
Jul 20, 2026
Merged

fix: support versioned Mobbin catalog sync#2315
charlietlamb merged 2 commits into
devfrom
charlie/mobbin-catalog-minimal

Conversation

@charlietlamb

@charlietlamb charlietlamb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Minimal Mobbin catalog changes only.

  • preserve explicit plan versions in catalog updates
  • validate sequential version targets and dependency ordering
  • preserve matched Stripe plan versions during sync
  • tolerate missing entitlements during catalog preview

Excluded the CLI deletion-check changes; they are unrelated to the versioned import path.


Summary by cubic

Adds support for explicit, versioned Mobbin catalog sync, including renames via new_plan_id. Preserves provided versions, enforces sequential versioning (even across renames), and orders updates by dependencies to keep license links consistent.

  • New Features

    • Preserve explicit plan versions in preview and update; create next version when targeting an existing plan.
    • Validate and sort by target id (new_plan_id when present): fresh plans must start at 1; subsequent versions must be latest + 1; process earlier versions and dependencies first across renames.
    • Keep parent license references pinned to specified child plan versions; added tests for version gaps and renamed-plan sequencing.
  • Bug Fixes

    • Tolerate missing product entitlements in preview to prevent errors.

Written for commit 5611d87. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR extends the Mobbin catalog sync path to correctly handle batches of plans that carry explicit version numbers, fixing a sort-order bug and adding sequential-version validation so the system can reliably create v1, v2, … in a single API call.

  • [Bug fixes] sortCatalogPlansByDependencies now treats version N-1 as an implicit dependency of version N, ensuring lower versions are always written before higher ones in the same batch (the old order placed child@2 before child@1).
  • [Improvements] validateCatalogPlanVersionTargets enforces that new versioned plans must be exactly latestVersion + 1, rejecting gaps (e.g. jumping straight to v2 on a fresh plan); validation fires on both the preview and the update routes.
  • [Bug fixes] upsertPlans now detects when a versioned plan doesn't yet exist at the requested version and routes through updateProduct({ force_version: true }) instead of createProduct, preserving matched Stripe plan prices from the existing latest version.
  • [Bug fixes] previewUpdateCatalog defensively wraps product.entitlements access with ?? [] to tolerate products that arrive without an entitlements array during catalog preview.

Confidence Score: 4/5

Safe to merge; changes are self-contained to the catalog versioned-import path and are backed by both a new integration test and an updated unit test.

The core logic in validateCatalogPlanVersionTargets and sortCatalogPlansByDependencies is sound: the topological sort's new implicit v→(v-1) edge guarantees lower versions are processed first, and the sequential-version check correctly simulates already-created versions within the same batch. The upsertPlans versioned path correctly falls back from createProduct to updateProduct(force_version:true) when the plan_id already exists at another version. The only mild concern is that the new !current && latest branch in upsertPlans issues a second DB query per new versioned plan, which is acceptable but worth monitoring under large catalog syncs.

updateCatalog.ts — the new versioned-plan branch omits propagateToVariants; this is likely intentional for catalog sync but worth confirming if update_variant_ids is ever used alongside explicit version numbers in the Mobbin import.

Important Files Changed

Filename Overview
server/src/internal/catalog/actions/catalogPlanDependencies.ts Adds implicit previous-version dependency edge to the topological sort (so v1 always precedes v2) and introduces validateCatalogPlanVersionTargets to enforce sequential version numbering; logic is correct and well-tested.
server/src/internal/catalog/actions/catalogPlanPreflight.ts Wires validateCatalogPlanVersionTargets into the preflight and preserves explicit plan.version when computing the virtual version for new plans; change is consistent with the new versioned sync path.
server/src/internal/catalog/actions/updateCatalog/updateCatalog.ts Adds a second ProductService.getFull call to locate the latest existing version when a versioned plan is not found at the specified version, then delegates to updateProduct with force_version:true to create the new version; correct but adds an extra DB round-trip per new versioned plan.
server/src/internal/catalog/actions/previewUpdateCatalog/previewCatalogPlanUpdate.ts Stops ignoring the version field for new plans in the preview; uses version ?? 1 so preview output matches what updateCatalog will actually persist.
server/src/internal/catalog/actions/previewUpdateCatalog/previewUpdateCatalog.ts Adds null-safe fallback (entitlements ?? []) so that catalog preview no longer crashes on products that arrive without an entitlements array.
server/tests/integration/licenses/catalog-update/license-catalog-response.test.ts New integration test verifies end-to-end versioned catalog creation with pinned license dependencies submitted in reverse order, and asserts that version gaps are rejected by both preview and update routes.
server/tests/unit/catalog/catalog-plan-dependencies.test.ts Corrects expected sort output from the buggy ["child@2","parent@0","child@1"] to the correct ["child@1","child@2","parent@0"], and adds a unit test for the new version-gap rejection.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant previewUpdateCatalog
    participant preflightCatalogPlans
    participant validateCatalogPlanVersionTargets
    participant sortCatalogPlansByDependencies
    participant upsertPlans
    participant ProductService

    Client->>previewUpdateCatalog: POST /catalog.preview_update
    previewUpdateCatalog->>preflightCatalogPlans: activePlans, previews
    preflightCatalogPlans->>sortCatalogPlansByDependencies: cycle detection (v1 implicit dep of v2)
    preflightCatalogPlans->>validateCatalogPlanVersionTargets: plans, persistedProducts
    Note over validateCatalogPlanVersionTargets: enforces version = latestVersion+1
    validateCatalogPlanVersionTargets-->>preflightCatalogPlans: OK or RecaseError(400)
    preflightCatalogPlans-->>previewUpdateCatalog: license previews attached
    previewUpdateCatalog-->>Client: CatalogPreviewUpdateResponse

    Client->>previewUpdateCatalog: POST /catalog.update
    previewUpdateCatalog->>preflightCatalogPlans: preflight (validation runs again)
    previewUpdateCatalog->>upsertPlans: sorted activePlans
    loop each plan in dependency order
        upsertPlans->>ProductService: "getFull(plan_id, version) -> current"
        alt version specified AND current is null
            upsertPlans->>ProductService: "getFull(plan_id, no version) -> latest"
            alt latest exists
                upsertPlans->>ProductService: updateProduct(force_version:true)
            else brand new plan
                upsertPlans->>ProductService: createProduct
            end
        else current exists
            upsertPlans->>ProductService: updateProduct(version, ...)
        end
    end
    upsertPlans-->>Client: CatalogUpdateResponse
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"}}}%%
sequenceDiagram
    participant Client
    participant previewUpdateCatalog
    participant preflightCatalogPlans
    participant validateCatalogPlanVersionTargets
    participant sortCatalogPlansByDependencies
    participant upsertPlans
    participant ProductService

    Client->>previewUpdateCatalog: POST /catalog.preview_update
    previewUpdateCatalog->>preflightCatalogPlans: activePlans, previews
    preflightCatalogPlans->>sortCatalogPlansByDependencies: cycle detection (v1 implicit dep of v2)
    preflightCatalogPlans->>validateCatalogPlanVersionTargets: plans, persistedProducts
    Note over validateCatalogPlanVersionTargets: enforces version = latestVersion+1
    validateCatalogPlanVersionTargets-->>preflightCatalogPlans: OK or RecaseError(400)
    preflightCatalogPlans-->>previewUpdateCatalog: license previews attached
    previewUpdateCatalog-->>Client: CatalogPreviewUpdateResponse

    Client->>previewUpdateCatalog: POST /catalog.update
    previewUpdateCatalog->>preflightCatalogPlans: preflight (validation runs again)
    previewUpdateCatalog->>upsertPlans: sorted activePlans
    loop each plan in dependency order
        upsertPlans->>ProductService: "getFull(plan_id, version) -> current"
        alt version specified AND current is null
            upsertPlans->>ProductService: "getFull(plan_id, no version) -> latest"
            alt latest exists
                upsertPlans->>ProductService: updateProduct(force_version:true)
            else brand new plan
                upsertPlans->>ProductService: createProduct
            end
        else current exists
            upsertPlans->>ProductService: updateProduct(version, ...)
        end
    end
    upsertPlans-->>Client: CatalogUpdateResponse
Loading

Reviews (1): Last reviewed commit: "fix(catalog): tolerate missing product e..." | Re-trigger Greptile

@vercel

vercel Bot commented Jul 20, 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 20, 2026 7:13pm
landing-page Ignored Ignored Jul 20, 2026 7:13pm

Request Review

@vercel
vercel Bot temporarily deployed to Preview – autumn-vite July 20, 2026 18:38 Inactive
@capy-ai

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

All reported issues were addressed across 7 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/catalog/actions/catalogPlanDependencies.ts Outdated
@vercel
vercel Bot temporarily deployed to Preview – autumn-vite July 20, 2026 19:13 Inactive

@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 2 files (changes from recent commits).

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

Re-trigger cubic

Comment thread server/src/internal/catalog/actions/catalogPlanDependencies.ts
@charlietlamb
charlietlamb force-pushed the charlie/mobbin-catalog-minimal branch from 0a750f4 to 5611d87 Compare July 20, 2026 19:17
@charlietlamb
charlietlamb merged commit 3824b97 into dev Jul 20, 2026
12 checks passed
@charlietlamb
charlietlamb deleted the charlie/mobbin-catalog-minimal branch July 20, 2026 19:20
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