Skip to content

fix(products): a product or group named like another gets a slug of its own - #187

Merged
Panelica merged 3 commits into
Panelica:mainfrom
ertugrulkeyvan:fix/product-slug-unique
Oct 12, 2026
Merged

Panelica merged 3 commits into
Panelica:mainfrom
ertugrulkeyvan:fix/product-slug-unique

Conversation

@ertugrulkeyvan

Copy link
Copy Markdown
Contributor

What

A product or group with the same name as an existing one now gets its own slug (vps-s-2), so it can be ordered.

Products and product groups took Str::slug($name) as it came. The order page finds a product by its slug (store/configure/{product:slug}), and products.slug is not unique, so a second product called like an earlier one:

  • with the first visible: its "Order" link opened the first product, at the first product's price;
  • with the first hidden or retired: its link was a 404;
  • in either case: it could not be ordered from the store at all.

We ran into it while drafting a new catalogue next to an old one that had the same plan names. A second product group with the same name is refused by the unique index on product_groups.slug with a 500 (Duplicate entry … product_groups_slug_unique).

After this:

  • New products and groups: a name that is already taken gets -2, -3, and so on.
  • Names a slug cannot spell (only CJK letters, say): these used to get an empty slug. They now get product or group, and a suffix if that is taken too.
  • Installs that already have shared slugs: a migration gives every later product its own. The oldest keeps its slug, so every link already out there still works. The later ones' old links never reached them anyway.
  • Unchanged: renaming a product still keeps its slug, so links do not move.

How

  • App\Support\UniqueSlug::for($model, $name, $fallback, $ignoreId): Str::slug, the fallback when that is empty, and a suffix until no other row has it.
  • Callers:
    • ProductCreator, used by the product screen and the API's AddProduct;
    • ProductController::storeGroup.
  • Migration 2026_10_12_000001: walks the products by id and re-slugs only the ones whose slug is empty or already taken by an older product.

The work is split into three commits: the fix, the migration, and the tests.

Tests

tests/Feature/ProductSlugTest.php, 5 tests:

  • Same-name products: three products named "VPS S" get vps-s, vps-s-2 and vps-s-3.
  • Order page: with the first hidden, the second opens there as itself.
  • CJK name: a name like 专业版 gets product.
  • Same-name groups: two groups named "Shared Hosting" are both created, with shared-hosting and shared-hosting-2.
  • Migration: the oldest of three products sharing vps-s keeps it, the others get -2 and -3, an empty slug is filled, and an unrelated product is untouched.

The first four fail on main.

Full suite: 4133 passed. The one failure is the CurrencyAndBackupTest db backup test, which fails on main too in my local setup.

@Panelica
Panelica merged commit 172ba45 into Panelica:main Oct 12, 2026
1 check passed
@Panelica

Copy link
Copy Markdown
Owner

Thank you, @ertugrulkeyvan, merged. A product or group named like an existing one now gets a slug of its own, so every product in the store can be ordered, and installs that already had shared slugs are put right by the migration while the oldest product keeps its address.

Checked before merging: the four behaviour tests and the migration test fail on main and pass here, a second run of the migration changes nothing, renaming a product still leaves its slug alone, and the full suite passes (4134).

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.

2 participants