| @@ -12,8 +12,9 @@ | ||
| 12 | 12 | use WCPOS\WooCommercePOS\Sync\Order_Document; |
| 13 | 13 | use WCPOS\WooCommercePOS\Sync\Order_Pull_Planner; |
| 14 | 14 | use WCPOS\WooCommercePOS\Sync\Order_Query; |
| 15 | 15 | use WCPOS\WooCommercePOS\Sync\Order_Serializer; |
| 16 | +use WCPOS\WooCommercePOS\Sync\Pos_Uuid; | |
| 16 | 17 | use WCPOS\WooCommercePOS\Sync\Sync_Journal; |
| 17 | 18 | use WP_REST_Controller; |
| 18 | 19 | use WP_REST_Request; |
| 19 | 20 | use WP_REST_Server; |
| @@ -109,9 +110,41 @@ | ||
| 109 | 110 | $query = new Order_Query(); |
| 110 | 111 | $serializer = new Order_Serializer(); |
| 111 | 112 | $change_rows = $query->changes_after_checkpoint( $updated_at_gmt, $order_id, $sequence, $limit + 1 ); |
| 112 | 113 | $has_more = count( $change_rows ) > $limit; |
| 114 | + // The limit+1 probe row is a paging sentinel the planner pops before serving — | |
| 115 | + // it is not part of this page, so the filter must not see (or drop) its id. | |
| 116 | + $page_rows = $has_more ? array_slice( $change_rows, 0, $limit ) : $change_rows; | |
| 117 | + $ids = array_map( 'intval', array_column( $page_rows, 'order_id' ) ); | |
| 113 | 118 | |
| 119 | + /** | |
| 120 | + * Filters the order IDs eligible for the custom pull lane. | |
| 121 | + * | |
| 122 | + * The interim hook-parity seam for order-scoping plugins on a lane that | |
| 123 | + * bypasses `woocommerce_rest_orders_prepare_object_query`. It retires with | |
| 124 | + * the lane at the 1.11.0 protocol boundary (ADR 0035, #1748). | |
| 125 | + * | |
| 126 | + * The contract, precisely: | |
| 127 | + * - NARROW ONLY. Return the subset of `$ids` to serve; ids added by the | |
| 128 | + * filter are ignored by construction. | |
| 129 | + * - BE DETERMINISTIC for a given client. The checkpoint advances PAST an | |
| 130 | + * excluded row and its journal entry is never re-offered to that client, | |
| 131 | + * so exclusion is permanent per-checkpoint: a filter whose answer | |
| 132 | + * changes between pages corrupts what the till holds, and a scope that | |
| 133 | + * later WIDENS only reaches clients after a full resync. | |
| 134 | + * - Exclusion does not tombstone. A copy the till already holds stays | |
| 135 | + * until a real delete tombstones it (deleted rows bypass this filter, | |
| 136 | + * so tombstones for excluded orders still flow — which is the desired | |
| 137 | + * "drop it" signal for a scoped-out order). | |
| 138 | + * | |
| 139 | + * @since 1.10.3 | |
| 140 | + * | |
| 141 | + * @param int[] $ids Candidate order IDs in this pull page. | |
| 142 | + * @param WP_REST_Request $request Pull request. | |
| 143 | + */ | |
| 144 | + $allowed = array_map( 'intval', (array) apply_filters( 'woocommerce_pos_order_pull_ids', $ids, $request ) ); | |
| 145 | + $allowed = array_flip( $allowed ); // O(1) membership for 250-row pages. | |
| 146 | + | |
| 114 | 147 | $planner = new Order_Pull_Planner( |
| 115 | 148 | array( |
| 116 | 149 | 'updatedAtGmt' => $updated_at_gmt, |
| 117 | 150 | 'orderId' => $order_id, |
| @@ -122,10 +155,29 @@ | ||
| 122 | 155 | ); |
| 123 | 156 | $plan = $planner->plan( |
| 124 | 157 | $change_rows, |
| 125 | 158 | $has_more, |
| 126 | - function ( int $id ) use ( $serializer, $request ): array { | |
| 127 | - return $serializer->serialize_order( $id, $request ); | |
| 159 | + function ( int $id ) use ( $serializer, $request, $allowed ): array { | |
| 160 | + // Narrow inside serialization: removing change rows would leave a | |
| 161 | + // fully filtered page unable to advance, so the client would loop forever. | |
| 162 | + if ( ! isset( $allowed[ $id ] ) ) { | |
| 163 | + return array(); | |
| 164 | + } | |
| 165 | + $order = wc_get_order( $id ); | |
| 166 | + $had_uuid = $order && '' !== (string) $order->get_meta( Pos_Uuid::META_KEY ); | |
| 167 | + $payload = $serializer->serialize_order( $id, $request ); | |
| 168 | + if ( ! $had_uuid && array() !== $payload ) { | |
| 169 | + /* | |
| 170 | + * First serialization of an unstamped order MINTS its identity: the | |
| 171 | + * uuid save advances the stored date_updated_gmt AFTER this payload | |
| 172 | + * captured the pre-mint date. Hashing that payload would serve a | |
| 173 | + * revision stale the moment it leaves — the client's next push | |
| 174 | + * false-409s against a fresh re-read. Serialize again from the | |
| 175 | + * settled order (a pure read now: the identity exists). | |
| 176 | + */ | |
| 177 | + $payload = $serializer->serialize_order( $id, $request ); | |
| 178 | + } | |
| 179 | + return $payload; | |
| 128 | 180 | }, |
| 129 | 181 | static function ( array $full_payload ): string { |
| 130 | 182 | return Order_Serializer::canonical_revision( $full_payload ); |
| 131 | 183 | } |