| 1 |
<?php |
| 2 |
/** |
| 3 |
* WCPOS sync read surface. |
| 4 |
* |
| 5 |
* @package WCPOS\WooCommercePOS\Sync |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace WCPOS\WooCommercePOS\Sync; |
| 9 |
|
| 10 |
/** |
| 11 |
* Narrows a variable product's advertised child list to what the POS may actually sell. |
| 12 |
* |
| 13 |
* # Why |
| 14 |
* |
| 15 |
* `product.variations[]` is the client's ONLY discovery list for a variable product: the row |
| 16 |
* expansion, the POS variations popover, the "Showing N of M" footer and the idle prefetch walk all |
| 17 |
* read it and nothing else. WooCommerce fills it from `WC_Product_Variable::get_children()`, which |
| 18 |
* returns every child with `post_status IN ( 'publish', 'private' )` and knows nothing about POS |
| 19 |
* visibility. |
| 20 |
* |
| 21 |
* Every other component that answers "which children exist" already narrows the set — the |
| 22 |
* variations endpoint refuses to hydrate a hidden or disabled child, {@see Variable_Price_Range} |
| 23 |
* computes its range from `get_visible_children()`, and the census counts only servable rows. The |
| 24 |
* parent payload was the one that did not, so a store hiding or disabling a variation got four |
| 25 |
* components giving three answers: |
| 26 |
* |
| 27 |
* - the footer renders "Showing 2 of 3" forever, with no way to reach the third; |
| 28 |
* - the popover offers a row that cannot be added; |
| 29 |
* - the row expansion asks for the id, receives a shortfall, prunes it, and asks again on the next |
| 30 |
* expansion — permanent churn; |
| 31 |
* - the prefetch walk spends one request per walk on an id the server will never serve. |
| 32 |
* |
| 33 |
* Filtering here fixes all four at once, because it fixes the fact they all read. |
| 34 |
* |
| 35 |
* # What is filtered |
| 36 |
* |
| 37 |
* Two rules, both WooCommerce's own: |
| 38 |
* |
| 39 |
* - **POS visibility** — `online_only` children, per {@see Pos_Visibility}. Ours, and additive: it |
| 40 |
* changes which records are served, never what a field means. |
| 41 |
* - **Disabled children** — `post_status !== 'publish'`. WooCommerce's Enabled checkbox on the |
| 42 |
* variation metabox writes `post_status = private` when unchecked, and WooCommerce honours that |
| 43 |
* everywhere a customer can reach (`get_visible_children()`, `get_available_variations()`). A |
| 44 |
* cashier must not be offered a variation the owner switched off. |
| 45 |
* |
| 46 |
* # Ordering |
| 47 |
* |
| 48 |
* Registered as a record augmenter at priority 10 — AFTER the revision stamper at 9. The parent's |
| 49 |
* `_rxdb_revision` therefore stays a hash of the bare wc/v3 bytes, which is what the write path |
| 50 |
* recomputes from a bare re-read, so narrowing this list cannot perturb optimistic concurrency. |
| 51 |
* Same ordering {@see Variable_Prices} already relies on when it blanks the parent's price fields. |
| 52 |
* |
| 53 |
* # Why round-tripping the narrowed list is safe |
| 54 |
* |
| 55 |
* A client's product update layers its resident stored payload, so the narrowed array does travel |
| 56 |
* back to wc/v3. `variations` is `readonly => true` in WooCommerce's product schema (v2 and v3), so |
| 57 |
* WooCommerce ignores it on write and no child can be removed by omission. Anything that changes |
| 58 |
* this class must re-verify that property. |
| 59 |
*/ |
| 60 |
final class Variable_Children { |
| 61 |
/** |
| 62 |
* Narrow `variations[]` on a serialized variable product. |
| 63 |
* |
| 64 |
* Non-variable products and payloads without a usable list pass through untouched. The `type` |
| 65 |
* gate comes first so a page of simple products never pays for a lookup. |
| 66 |
* |
| 67 |
* @param mixed $payload Serialized product record. |
| 68 |
* @param null|mixed $object Product object, when the lane has one loaded. |
| 69 |
* @param null|mixed $request Request context. |
| 70 |
*/ |
| 71 |
public static function augment_record( $payload, $object = null, $request = null ) { |
| 72 |
if ( ! \is_array( $payload ) || 'variable' !== ( $payload['type'] ?? '' ) ) { |
| 73 |
return $payload; |
| 74 |
} |
| 75 |
if ( ! isset( $payload['variations'] ) || ! \is_array( $payload['variations'] ) ) { |
| 76 |
return $payload; |
| 77 |
} |
| 78 |
|
| 79 |
$children = array_values( array_filter( array_map( 'intval', $payload['variations'] ) ) ); |
| 80 |
if ( array() === $children ) { |
| 81 |
return $payload; |
| 82 |
} |
| 83 |
|
| 84 |
$payload['variations'] = array_values( |
| 85 |
array_filter( |
| 86 |
( new Pos_Visibility() )->filter_visible_children( $children ), |
| 87 |
static function ( int $id ): bool { |
| 88 |
return 'publish' === get_post_status( $id ); |
| 89 |
} |
| 90 |
) |
| 91 |
); |
| 92 |
|
| 93 |
return $payload; |
| 94 |
} |
| 95 |
} |
| 96 |
|