PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.2
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.2
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / API / V2 / Orders_Controller.php

Orders_Controller.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.2, at includes/API/V2/Orders_Controller.php

203 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WCPOS sync read surface.
4 *
5 * @package WCPOS\WooCommercePOS\API\V2
6 */
7
8 namespace WCPOS\WooCommercePOS\API\V2;
9
10 use WCPOS\WooCommercePOS\Sync\Api;
11 use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions;
12 use WCPOS\WooCommercePOS\Sync\Order_Document;
13 use WCPOS\WooCommercePOS\Sync\Order_Pull_Planner;
14 use WCPOS\WooCommercePOS\Sync\Order_Query;
15 use WCPOS\WooCommercePOS\Sync\Order_Serializer;
16 use WCPOS\WooCommercePOS\Sync\Sync_Journal;
17 use WP_REST_Controller;
18 use WP_REST_Request;
19 use WP_REST_Server;
20
21 // phpcs:disable Squiz.Commenting, Generic.Commenting -- Ported lab documentation is preserved verbatim.
22
23 /**
24 * The orders custom-pull lane. Orders ride a checkpointed cursor over the
25 * order rows in the unified journal — the client greedily
26 * drains /orders/pull, coalescing each order to its net state per page (F6
27 * tombstones on the separate delete channel, F8 journal epoch/head for reset
28 * detection). Window/targeted order reads use the plain /orders wc/v3 proxy
29 * (Catalog_Proxy_Controller); this controller owns the cursor lane only.
30 *
31 * The bench-only order lanes (/orders/pull.ndjson, /orders/snapshot.ndjson,
32 * /orders/skeleton) and the sparse order_fields / compression / benchmark
33 * instrumentation stay in the lab — they never ported.
34 */
35 final class Orders_Controller extends WP_REST_Controller {
36 use Endpoint_Permissions;
37
38 public function register_routes(): void {
39 register_rest_route(
40 Api::ROUTE_NAMESPACE,
41 '/orders/pull',
42 array(
43 'methods' => WP_REST_Server::READABLE,
44 'callback' => array( $this, 'pull_orders' ),
45 'permission_callback' => array( $this, 'permissions_check' ),
46 'args' => array(
47 'limit' => array(
48 'default' => 100,
49 'sanitize_callback' => 'absint',
50 ),
51 'updated_at_gmt' => array(
52 'default' => '1970-01-01T00:00:00.000Z',
53 'sanitize_callback' => 'sanitize_text_field',
54 ),
55 'order_id' => array(
56 'default' => 0,
57 'sanitize_callback' => 'absint',
58 ),
59 'sequence' => array(
60 'default' => 0,
61 'sanitize_callback' => 'absint',
62 ),
63 'include_deletes' => array(
64 'default' => false,
65 'type' => 'boolean',
66 'sanitize_callback' => 'rest_sanitize_boolean',
67 ),
68 ),
69 )
70 );
71
72 // Out-of-band admin lane (ADR 0021): populates the gated journal that
73 // feeds /orders/pull. Zero client callers by design — an operator repair
74 // job — and it stays health-gated because its work lives in the gated
75 // table (it cannot cure a broken install, only fail against it).
76 register_rest_route(
77 Api::ROUTE_NAMESPACE,
78 '/orders/index/backfill',
79 array(
80 'methods' => WP_REST_Server::CREATABLE,
81 'callback' => array( $this, 'index_backfill' ),
82 'permission_callback' => array( $this, 'admin_permissions_check' ),
83 'args' => array(
84 'limit' => array(
85 'default' => 50,
86 'sanitize_callback' => 'absint',
87 ),
88 'reset' => array(
89 'default' => '',
90 'sanitize_callback' => 'sanitize_text_field',
91 ),
92 ),
93 )
94 );
95 }
96
97 /**
98 * The checkpointed greedy order pull. Renders the planner's typed decisions
99 * onto the batch response shape; the checkpoint hold-back, coalescing,
100 * tombstone-split and uuid-stop invariants live in the planner.
101 */
102 public function pull_orders( WP_REST_Request $request ) {
103 $limit = max( 1, min( 250, (int) $request->get_param( 'limit' ) ) );
104 $updated_at_gmt = (string) $request->get_param( 'updated_at_gmt' );
105 $order_id = (int) $request->get_param( 'order_id' );
106 $sequence = (int) $request->get_param( 'sequence' );
107 $include_deletes = rest_sanitize_boolean( $request->get_param( 'include_deletes' ) ); // strict: 'false'/'0' ⇒ false
108
109 $query = new Order_Query();
110 $serializer = new Order_Serializer();
111 $change_rows = $query->changes_after_checkpoint( $updated_at_gmt, $order_id, $sequence, $limit + 1 );
112 $has_more = count( $change_rows ) > $limit;
113
114 $planner = new Order_Pull_Planner(
115 array(
116 'updatedAtGmt' => $updated_at_gmt,
117 'orderId' => $order_id,
118 'revision' => '',
119 'sequence' => $sequence,
120 ),
121 $include_deletes
122 );
123 $plan = $planner->plan(
124 $change_rows,
125 $has_more,
126 function ( int $id ) use ( $serializer, $request ): array {
127 return $serializer->serialize_order( $id, $request );
128 },
129 static function ( array $full_payload ): string {
130 return Order_Serializer::canonical_revision( $full_payload );
131 }
132 );
133
134 $documents = array();
135 $deletes = array(); // wooOrderIds of deleted orders (F6) — a SEPARATE channel from documents
136 $response_checkpoint = array(
137 'updatedAtGmt' => $updated_at_gmt,
138 'orderId' => $order_id,
139 'revision' => '',
140 'sequence' => $sequence,
141 );
142
143 foreach ( $plan as $decision ) {
144 if ( 'tombstone' === $decision['type'] ) {
145 $deletes[] = $decision['wooOrderId'];
146 continue;
147 }
148 if ( 'complete' === $decision['type'] ) {
149 $response_checkpoint = $decision['checkpoint'];
150 $has_more = $decision['hasMore'];
151 continue;
152 }
153 $full_payload = $decision['payload'];
154 $documents[] = Order_Document::build(
155 $full_payload,
156 $full_payload,
157 $decision['orderId'],
158 $decision['revision'],
159 $decision['checkpoint'],
160 false,
161 'custom-pull'
162 );
163 }
164
165 $payloads = (array) apply_filters( 'woocommerce_pos_sync_order_pull_payloads', array_column( $documents, 'payload' ), 'orders', $request );
166 foreach ( $payloads as $index => $payload ) {
167 $documents[ $index ]['payload'] = $payload;
168 }
169
170 // Journal epoch + head (F8): the client resyncs from zero when the epoch it stored differs
171 // (a new sequence generation) or when its checkpoint sequence exceeds the head (the
172 // AUTO_INCREMENT space reset beneath it). Cheap: an autoloaded option + a MAX(sequence).
173 $journal = new Sync_Journal();
174
175 return rest_ensure_response(
176 array(
177 'documents' => $documents,
178 'deletes' => $deletes, // wooOrderIds the client resolves + removes (F6); empty unless include_deletes
179 'checkpoint' => $response_checkpoint,
180 'hasMore' => $has_more,
181 'epoch' => $journal->ensure_epoch(), // F8 journal epoch — client resyncs on mismatch
182 'head' => $journal->head_sequence( array( 'order' ) ), // F8 order-lane head — client resyncs if its cursor exceeds it; stream-scoped so catalogue writes don't move it
183 'horizon' => $journal->prune_watermark( array( 'order' ) ),
184 )
185 );
186 }
187
188 /**
189 * Run one bounded chunk of the append-only journal backfill. `reset=1`
190 * clears the persisted backfill cursor so a COMPLETED store can re-run the
191 * append-only backfill; the F8 epoch is untouched.
192 */
193 public function index_backfill( WP_REST_Request $request ) {
194 $raw_limit = $request->get_param( 'limit' );
195 $limit = max( 1, min( 250, (int) ( null === $raw_limit ? 50 : $raw_limit ) ) );
196 $journal = new Sync_Journal();
197 if ( in_array( (string) $request->get_param( 'reset' ), array( '1', 'true' ), true ) ) {
198 $journal->reset_backfill_state();
199 }
200 return rest_ensure_response( $journal->run_backfill_chunk( $limit ) );
201 }
202 }
203