Skip to content

Fix: cascade CatalogId to descendants and products on cross-catalog category move (cut & paste)#882

Open
yuskithedeveloper wants to merge 4 commits into
devfrom
fix/cut-paste-between-catalogs
Open

Fix: cascade CatalogId to descendants and products on cross-catalog category move (cut & paste)#882
yuskithedeveloper wants to merge 4 commits into
devfrom
fix/cut-paste-between-catalogs

Conversation

@yuskithedeveloper
Copy link
Copy Markdown
Contributor

Description

Problem

Moving a category to a different catalog via the admin UI's Cut/Paste
(POST /api/catalog/listentries/move) leaves the database in an
inconsistent state: the selected Category.CatalogId is updated, but
nothing else is. Specifically:

  • Subcategories keep their original CatalogId, so an entire
    subtree ends up belonging to a different catalog than its root.
  • Products under the moved subtree keep their original CatalogId,
    so Item.CatalogId no longer matches Category.CatalogId.

This breaks any feature that filters by CatalogId (search, indexing,
catalog-scoped property/dictionary lookups).

Cause

CategoryMover.PrepareMoveAsync only iterates entries explicitly
listed in ListEntriesMoveRequest.ListEntries (the user's selection
in the UI) and mutates CatalogId/ParentId on those rows alone.
There is no traversal of the moved subtree, and ProductMover only
touches products that were directly listed in the request — products
located inside a moved category are invisible to it.

Fix

CategoryMover now cascades the catalog change. The change is
contained to a single file; controller, request DTO, and
ProductMover are unchanged.

  • PrepareMoveAsync captures the source CatalogId of each
    cross-catalog root, then loads each source catalog's category set
    once via ICategorySearchService and BFS-walks a parent→children
    map to collect all descendants. Descendants get the target
    CatalogId and are appended to the returned list.
  • ConfirmMoveAsync saves all categories (roots + descendants)
    and then cascades products. The target CatalogId and category ids
    are derived from the entities themselves — no instance fields, no
    changes to the base mover signature.
  • CascadeProductsAsync pages through products by CategoryIds
    with SearchInVariations = true so variations are updated as flat
    results, then saves each batch via IItemService.SaveChangesAsync
    (which raises the existing domain events that trigger reindexing).

Same-catalog reparents are unchanged in behavior: no descendants are
added in Prepare, and the product cascade in Confirm is a single
bounded search whose per-product CatalogId filter yields no writes.

Behavior considerations

  • CategoryMover gains three new dependencies (ICategorySearchService,
    IProductSearchService, IItemService). DI resolves them
    automatically; no module wiring change is needed.
  • For very large catalogs, a cross-catalog move now does proportionally
    more work inside the HTTP request. The existing endpoint stays
    synchronous; converting moves to a background job is left as a
    follow-up.
  • Indexing is not triggered synchronously — IItemService.SaveChangesAsync
    raises the usual CatalogProductChangedEvent and reindexing happens
    through the existing pipeline.

References

QA-test:

Jira-link:

Artifact URL:

@vc-ci
Copy link
Copy Markdown
Contributor

vc-ci commented May 7, 2026

Review task created: https://virtocommerce.atlassian.net/browse/VCST-5082

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 8, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants