| @@ -7,8 +7,9 @@ | ||
| 7 | 7 | |
| 8 | 8 | namespace WCPOS\WooCommercePOS\Sync; |
| 9 | 9 | |
| 10 | 10 | use Automattic\WooCommerce\Utilities\OrderUtil; |
| 11 | +use WCPOS\WooCommercePOS\Services\Barcode_Field; | |
| 11 | 12 | use WP_REST_Request; |
| 12 | 13 | |
| 13 | 14 | /** |
| 14 | 15 | * THE declaration table for POS collection query behaviour — one Collection Rule per |
| @@ -195,8 +196,13 @@ | ||
| 195 | 196 | * - `posts` => `array( 'posts_orderby' => <wp_posts column> )` for a column the |
| 196 | 197 | * WP_Query `orderby` vocabulary cannot express (rewritten through |
| 197 | 198 | * `posts_orderby`), OR |
| 198 | 199 | * `array( 'meta_key' => ..., 'orderby' => meta_value|meta_value_num )`. |
| 200 | + * - `posts` => `array( 'meta_sort' => array( 'key' => ..., 'numeric' => bool ) )` | |
| 201 | + * a postmeta sort that must NOT filter: applied as a LEFT JOIN through | |
| 202 | + * `posts_clauses`, with rows that have no value for the key ordered | |
| 203 | + * LAST in both directions. Use this for any user-facing column sort — | |
| 204 | + * `meta_key`/`orderby` INNER JOINs and silently drops rows. | |
| 199 | 205 | * |
| 200 | 206 | * Filter row shape: |
| 201 | 207 | * - `meta` => `array( 'key' => <meta key>, 'storage' => <optional storage lock> )` |
| 202 | 208 | * a `meta_query` row on the WC query args (works on both storages). |
| @@ -286,8 +292,34 @@ | ||
| 286 | 292 | ), |
| 287 | 293 | ), |
| 288 | 294 | |
| 289 | 295 | /* |
| 296 | + * The POS grid's SKU / barcode / stock columns, for the product grid and the | |
| 297 | + * variation grid alike — the SAME four rows, from one builder, because the two | |
| 298 | + * surfaces drifted apart once already and a cashier sorting a column expects | |
| 299 | + * the same thing of both. | |
| 300 | + * | |
| 301 | + * A `meta_sort` row sorts on a postmeta value WITHOUT letting the sort decide | |
| 302 | + * which records exist. The obvious encoding — WP_Query's `meta_key` + | |
| 303 | + * `orderby => meta_value` — INNER JOINs `postmeta`, so a record with no row for | |
| 304 | + * that key VANISHES from the result. On a default store the barcode field is | |
| 305 | + * `_global_unique_id`, which most catalogues never populate, so sorting by | |
| 306 | + * barcode returned an EMPTY page; `orderby=sku` silently dropped everything | |
| 307 | + * without a SKU. A sort must never hide a record from a cashier, so these rows | |
| 308 | + * are applied as a LEFT JOIN with the meta-less rows ordered LAST in both | |
| 309 | + * directions (`Collection_Rules_Plan::apply_meta_sort_clauses()`). | |
| 310 | + * | |
| 311 | + * Neither collection is ever HPOS — both are posts on every store — so there is | |
| 312 | + * no `hpos` half to these rows. | |
| 313 | + */ | |
| 314 | + 'products' => array( | |
| 315 | + 'sorts' => self::catalog_meta_sorts(), | |
| 316 | + ), | |
| 317 | + 'variations' => array( | |
| 318 | + 'sorts' => self::catalog_meta_sorts(), | |
| 319 | + ), | |
| 320 | + | |
| 321 | + /* | |
| 290 | 322 | * SORT NAMES ONLY — deliberately no clause bodies. |
| 291 | 323 | * |
| 292 | 324 | * Customers are a `WP_User_Query` over `wp_users`/`wp_usermeta`, a storage |
| 293 | 325 | * this table does not speak: it knows `hpos` and `posts`, and both are ORDER |
| @@ -313,8 +345,56 @@ | ||
| 313 | 345 | ), |
| 314 | 346 | ); |
| 315 | 347 | |
| 316 | 348 | return $rules[ $collection ] ?? array(); |
| 349 | + } | |
| 350 | + | |
| 351 | + /** | |
| 352 | + * The four POS column sorts, shared by `products` and `variations`. | |
| 353 | + * | |
| 354 | + * One builder rather than two copied blocks: these two collections carry the same | |
| 355 | + * cashier-facing columns, and the previous copy-per-controller encoding is exactly how | |
| 356 | + * the variation lane kept a defect the product lane had already fixed. | |
| 357 | + * | |
| 358 | + * @return array<string, array> | |
| 359 | + */ | |
| 360 | + private static function catalog_meta_sorts(): array { | |
| 361 | + return array( | |
| 362 | + 'sku' => array( | |
| 363 | + 'posts' => array( | |
| 364 | + 'meta_sort' => array( 'key' => '_sku' ), | |
| 365 | + ), | |
| 366 | + ), | |
| 367 | + | |
| 368 | + /* | |
| 369 | + * The barcode meta key is a store setting, so the row reads the same accessor | |
| 370 | + * the controllers do rather than hard-coding a key that would drift. | |
| 371 | + */ | |
| 372 | + 'barcode' => array( | |
| 373 | + 'posts' => array( | |
| 374 | + 'meta_sort' => array( 'key' => Barcode_Field::orderby_key() ), | |
| 375 | + ), | |
| 376 | + ), | |
| 377 | + | |
| 378 | + /* | |
| 379 | + * `_stock` is written as NULL for everything that does not manage stock, so this | |
| 380 | + * row needs the same meta-less-last ordering as the rest — it is not a special | |
| 381 | + * case, it was merely the first one noticed. | |
| 382 | + */ | |
| 383 | + 'stock_quantity' => array( | |
| 384 | + 'posts' => array( | |
| 385 | + 'meta_sort' => array( | |
| 386 | + 'key' => '_stock', | |
| 387 | + 'numeric' => true, | |
| 388 | + ), | |
| 389 | + ), | |
| 390 | + ), | |
| 391 | + 'stock_status' => array( | |
| 392 | + 'posts' => array( | |
| 393 | + 'meta_sort' => array( 'key' => '_stock_status' ), | |
| 394 | + ), | |
| 395 | + ), | |
| 396 | + ); | |
| 317 | 397 | } |
| 318 | 398 | |
| 319 | 399 | /** |
| 320 | 400 | * Resolve the storage dialect for a collection when the caller did not name one. |