FCHub Memberships — plan-side product picker hides private/draft products
Plugin: FCHub Memberships 1.4.6
Environment: WordPress 7.0.x, FluentCart, single site
Type: UX inconsistency rather than a hard bug — the plan side and the product side disagree about which products can be linked.
Summary
In the plan editor, the "Link product" picker only ever offers products with post\\\_status = publish. Products saved as private (or draft) never appear, so from the plan side they look un-linkable.
They aren't. Adding the same link from the other direction — FluentCart → product → Integrations → Memberships feed — works perfectly on a private product, and produces exactly the same wp\\\_fct\\\_product\\\_meta row. PlanProductLinkService::linkProduct() itself performs no status check at all; it will link any product ID it is given.
So the restriction exists only in the picker's query, and only on one of the two routes to the same outcome.
Where it comes from
app/Domain/Plan/PlanProductLinkService.php → searchProducts():
$where = 'p.post\\\_type = %s AND p.post\\\_status = %s';
$params = \\\['fluent-products', 'publish'];
linkProduct(), a few methods above, looks the product up with no status condition:
"SELECT ID as id, post\\\_title as title FROM {$postsTable} WHERE ID = %d"
Why it matters
Private is a legitimate, well-supported FluentCart product state — an unlisted product that is fully purchasable by direct link. It's the natural shape for a back-catalogue: sold from a curated page or an email, deliberately kept out of the public shop index.
On our site that's most of the catalogue — 30+ private products, each with its own membership plan. Every one of them is invisible in the plan editor's picker, which reads as "you cannot link this", when in fact the product-side route links it without complaint. The disconnect costs more time than the limitation would.
Suggested fix
Widen the query to include private (and arguably draft, since a draft product can be linked ahead of launch):
$where = 'p.post\\\_type = %s AND p.post\\\_status IN (%s, %s)';
$params = \\\['fluent-products', 'publish', 'private'];
Optionally return post\\\_status in the payload so the picker can render a small "Private" / "Draft" badge — the admin then sees the state rather than an absence, which is the part that currently misleads.
A filter on the status list would also do it, and would let sites decide for themselves:
$statuses = apply\\\_filters('fchub\\\_memberships/linkable\\\_product\\\_statuses', \\\['publish', 'private']);
Verification on our install
30+ fluent-products at post\\\_status = private; none appear in the plan-side picker.
Products 302, 304, 306, 308 (private) and 310, 346 (draft) each carry a working memberships integration feed, added from the product side — proof that private products link and grant correctly.
Nothing in linkProduct(), the feed writer, or MembershipAccessIntegration consults post status at any point.
Reported by Brett Chitty
FCHub Memberships — plan-side product picker hides private/draft products
Plugin: FCHub Memberships 1.4.6
Environment: WordPress 7.0.x, FluentCart, single site
Type: UX inconsistency rather than a hard bug — the plan side and the product side disagree about which products can be linked.
Summary
In the plan editor, the "Link product" picker only ever offers products with
post\\\_status = publish. Products saved as private (or draft) never appear, so from the plan side they look un-linkable.They aren't. Adding the same link from the other direction — FluentCart → product → Integrations → Memberships feed — works perfectly on a private product, and produces exactly the same
wp\\\_fct\\\_product\\\_metarow.PlanProductLinkService::linkProduct()itself performs no status check at all; it will link any product ID it is given.So the restriction exists only in the picker's query, and only on one of the two routes to the same outcome.
Where it comes from
app/Domain/Plan/PlanProductLinkService.php→searchProducts():linkProduct(), a few methods above, looks the product up with no status condition:"SELECT ID as id, post\\\_title as title FROM {$postsTable} WHERE ID = %d"Why it matters
Private is a legitimate, well-supported FluentCart product state — an unlisted product that is fully purchasable by direct link. It's the natural shape for a back-catalogue: sold from a curated page or an email, deliberately kept out of the public shop index.
On our site that's most of the catalogue — 30+ private products, each with its own membership plan. Every one of them is invisible in the plan editor's picker, which reads as "you cannot link this", when in fact the product-side route links it without complaint. The disconnect costs more time than the limitation would.
Suggested fix
Widen the query to include private (and arguably draft, since a draft product can be linked ahead of launch):
Optionally return
post\\\_statusin the payload so the picker can render a small "Private" / "Draft" badge — the admin then sees the state rather than an absence, which is the part that currently misleads.A filter on the status list would also do it, and would let sites decide for themselves:
Verification on our install
30+
fluent-productsatpost\\\_status = private; none appear in the plan-side picker.Products 302, 304, 306, 308 (private) and 310, 346 (draft) each carry a working
membershipsintegration feed, added from the product side — proof that private products link and grant correctly.Nothing in
linkProduct(), the feed writer, orMembershipAccessIntegrationconsults post status at any point.Reported by Brett Chitty