Conversation
Comparing `scheduled_date_local` to a WooCommerce meta timestamp, which is UTC, shows an offset that does not exist — it is the site's own offset. It produced a false conclusion while scoping the price filter: a window where `is_on_sale()` says yes and `_price` still holds the regular price. There is none; WooCommerce schedules the flip at the exact boundary.
Frames R-43 / Q-08 before any code. Nothing is decided. Two things correct the R-43 entry, both measured on the test catalogue: its argument that no variable product exists is no longer true (8 of them, plus 2 grouped), and its proposal of price buckets does not follow the platform — WooCommerce filters min/max on an interval per product, with an overlap test. The blocking defect: for a variable or grouped product only the lowest price reaches the index, so a 40-70 filter misses a product sold from 28 to 62. Ten products of seventy-six. Also framed: the scheduled-sale chain does hold (Action Scheduler flips the price, which saves the product, which reindexes — verified on a past sale), the stock of a variation, which a live facet already contradicts, and what FacetWP and the Algolia WooCommerce connector do.
Seven decisions taken with Louis, two deferred with the stock work. The shape follows WooCommerce: a min/max overlap on an interval indexed per product, bounds from facetStats, an on-sale flag frozen at index time like the `onsale` column of wc_product_meta_lookup, and its tax conversion. The derived fields live under a `price` field of the module's own, beside `card` and `facets`. None of them exists in postmeta, so presenting them as metas would mislead the next reader. R-109 closed by AmphiBee/MeiliScout@26eb035: saving a product dropped its metas from the index, silently, because the meta keys were only resolved on the batch paths. Found while checking a scheduled sale.
A variable or grouped product carries one `_price` row per distinct child price. Only the lowest reached the index, so a 40-70 filter missed a product sold from 28 to 62 — ten of seventy-six here — and "price, high to low" ranked it at its cheapest variation. `price.min` and `price.max` are the ends of those rows, which `sync_price()` writes sorted: the same reading `wc_product_meta_lookup` does. They live under a `price` field of the module's own, beside `card` and `facets`, because none of the three exists in `postmeta`. `price.onsale` comes from `is_on_sale()` rather than from the formula of the lookup column. Measured on the catalogue: 20 against 15, and the five it differs on are all variable or grouped — `sync_price()` deletes `_sale_price` from the parent, so the column is wrong exactly where the method is overridden to read the children. Reproducing it would reproduce a defect. Sorting follows: ascending on the low end, descending on the high one. The `allow-plugins` entry arrived with a `composer update`, and it is kept on purpose: the module requires meiliscout, which pulls meilisearch-php, which pulls php-http/discovery, so a standalone `composer install` here needs the consent too.
`Modules/` sits outside the docroot, so the browser reads a copy of the module's CSS and JavaScript. A copy older than its source leaves the page reading rules nobody holds, and nothing said so. `meilifacets:check-assets` names every file that is missing or older than the module's own, and gives the command that republishes them.
…ject asks for A listing can carry a price filter made of a two-handle slider, two number fields, or both — `PriceFilter` takes the parts, one view renders those it was given, in a fixed order. The pair of inputs always travels, shown or hidden: the browser client reads the parameter names and the current bounds from them. Filtering tests the overlap of a product's own interval with the range asked for, the way WooCommerce does, so a variable product is found through any of its variations. Bounds come from the engine's `facetStats` and follow the current filtering; a range the new bounds no longer meet is dropped rather than clamped onto an empty sliver. `NativeFiltering` stops WooCommerce filtering the main query in parallel, which it would otherwise do from the same `$_GET` names with its own tax conversion. `Contract::VERSION` stays at 1. The seven hooks are added, never renamed: an override written against the previous contract keeps working, and refusing to start would cost the whole listing to protect a part the theme never rendered. The rule on the constant now says so.
`check-parameters` refused `min_price` and `max_price`, which are at once the module's own defaults and names WooCommerce reads straight from `$_GET`. Keeping them is the point: a link written for WooCommerce drives the module without translation. The guard now reports them without blocking, and only while the bound still answers to the name. A project that renames one gives up the compatibility and the excuse together: should a taxonomy then take the freed name, it blocks again.
`prix.md` carries the nine decisions the batch took: min/max rather than brackets, the interval indexed per product, bounds from `facetStats`, the derived fields under a `price` key, WooCommerce's tax conversion, and `min_price`/`max_price` as renameable defaults. The register closes R-113 (the price views lived outside `components/`, where the README promises overriding), R-116 (the rule on the contract put adding a hook on the same footing as removing one) and R-117, and opens R-114 and R-115. `configuration.md` documents the three price sub-views with the props each receives, and why the bounds keep WooCommerce's names. `architecture.md` names the version mechanism instead of a literal, and lists the seven price hooks it had been missing. `CLAUDE.md` gains the reason `config/config.php` cannot hold an overridable setting: nwidart merges it over the project's, recursively.
The price half of batch 4 shipped, so R-43 closes on evidence rather than on a claim: a product only matches `price.min <= 28 AND price.max >= 62` if its whole interval is indexed, and one does. The defect the entry named — only the lowest price indexed for a variable product — is gone. Q-08 asked whether to serve simple products early or wait for variations. D-b removed the choice by indexing the interval first, so the filter was right on day one for all 76 products. Availability stays deferred under D-f, per-variation stock under D-g; `lots.md` says which half of batch 4 landed and which acceptance point is still unmet.
…ine's `faceting.maxValuesPerFacet` was left at Meilisearch's default of 100, the only one of the three ceilings that could not be found by reading the module's code. It cuts `facetDistribution` by global count — before a facet narrows to the aisle being looked at, so a value that is rare shop-wide may be the only one that mattered on its page. The module now writes it from `engine.max_facet_values`, carried by `EngineLimits` beside `reachable_hits`, which already travels from the provider down to the indexable. `ResolvedListing::valuesOf()` reports `FacetTruncated` when a distribution stops exactly on that ceiling — the last moment the raw distribution is in hand, since `FacetValues::of()` then narrows and slices it. It reports on 100 as well: between a release and its reindexing the index still cuts where the engine does while the module already asks for more. The threshold comes from the same object that is written to the index, never a second copy.
…nders the page in
…d through the filter
Loubal70
left a comment
There was a problem hiding this comment.
Three findings on the price filter, each checked against the module and the WooCommerce source.
|
|
||
| public function clause(ListingState $state): string | ||
| { | ||
| return FilterExpression::overlapping(PriceTax::excluding($state->price)); |
There was a problem hiding this comment.
Server and browser disagree on the range when a shop stores prices excluding tax and displays them including tax.
PriceTax::excluding() takes the tax off the bounds here, but resources/assets/ts/price/price-query.ts builds its clause from the raw bounds — while its docblock says it runs « the same test the server writes ». Neither side adds the tax to the min/max the slider shows either, which WooCommerce's own price widget does (class-wc-widget-price-filter.php:101-107).
With 20 % tax:
- loading
?max_price=50directly filters on a stored price ≤ 41.67, while setting 50 with the slider filters on ≤ 50 — the same URL lists different products depending on how it was reached; - the slider's top is the tax-free maximum (100), which the server then brings down to 83.33, dropping the most expensive products.
| */ | ||
| final readonly class NativeFiltering | ||
| { | ||
| #[Filter('woocommerce_enable_post_clause_filtering')] |
There was a problem hiding this comment.
This turns WooCommerce's native filtering off on every main product query, not only where a MeiliFacets listing renders.
woocommerce_enable_post_clause_filtering gates price filtering (class-wc-query.php:787) and attribute filtering (src/Internal/ProductAttributesLookup/Filterer.php:70) alike, and nothing here checks that a listing is on the page — ListingPage only tests is_archive() || is_search(). The docblock's « inert on every other listing » does not hold.
A product archive or search page that keeps WooCommerce's loop with its price filter or layered-nav widgets would ignore min_price, max_price and filter_*, with no error.
| $prices = (array) get_post_meta($post->ID, '_price', false); | ||
|
|
||
| if ($prices === []) { |
There was a problem hiding this comment.
A product with no price is indexed at 0.
Saving a product with an empty regular price writes _price = '' (class-wc-product-data-store-cpt.php:876). get_post_meta(..., false) then returns [''], which passes the [] === check, and (float) '' is 0.0 — so « A product with no price has none » is not what happens.
Such a product (« price on request ») matches every range with an upper bound, ?max_price=10 included, and pulls the measured minimum to 0, so the slider always starts at 0. Dropping empty values before the check fixes it.
No description provided.