PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
1.10.20 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 All 164 releases
← All changes | includes/Sync/Config_Fingerprint.php +119 -4 1.10.0 → 1.10.20 View file →
@@ -105,8 +105,89 @@
105 105 return array_keys( Collections::with( 'fingerprint' ) );
106 106 }
107 107
108 108 /**
109 + * The PAYLOAD CONTRACT version per collection — bump when the SHAPE of a served record changes.
110 + *
111 + * # Why this is a representation setting
112 + *
113 + * ADR 0006 built this signal for "a global setting change alters the served representation of
114 + * MANY records without bumping any record's `date_modified`". A PLUGIN UPGRADE that changes a
115 + * payload's shape is the same event, and the three tiers are blind to it in exactly the same
116 + * way: tier 1 writes no journal row (no save hook fires), tier 2's digest is derived from the
117 + * raw DB row and does not move, and tier 3 is only ever reached from a tier-2 mismatch. Without
118 + * a signal here, a client that synced a record under the old shape keeps it INDEFINITELY.
119 + *
120 + * 1.10.0 shipped variations serialized through the PRODUCTS controller — an `images` array
121 + * instead of the singular `image`, and `get_name()` (the generated post title, which
122 + * `generate_product_title()` collapses to just the parent name at 3+ attributes) instead of
123 + * `wc_get_formatted_variation()`. A client can be taught to read either image shape, but a
124 + * collapsed name is indistinguishable from a correct one, so tolerance cannot repair it. Only a
125 + * re-pull can, and only this signal asks for one.
126 + *
127 + * # Why a version rather than a hash of the payload
128 + *
129 + * The shape is a property of the CODE, not of the store's data or settings, so there is nothing
130 + * live to recompute it from — the honest form is a constant a human bumps in the same commit
131 + * that changes the shape. Deliberately NOT an option or a filter: nobody but us can change what
132 + * we serve (see the constants-not-env-vars rule in the repo's agent context).
133 + *
134 + * # Skipped-release caveat for the collections #1756 phase 1 added
135 + *
136 + * A client cold-adopts a fingerprint key it has never stored, so a contract bump for one of
137 + * the six phase-1 collections only reaches tills whose server passed through a release that
138 + * served the key at the OLD version first — a server upgrade that skips straight past phase 1
139 + * cold-adopts at the new version with no re-pull. Between phase 1 and the 1.11.0 protocol
140 + * gate this is moot (recipe changes are batched AT the gate, whose forced resync covers
141 + * them); if a pre-gate bump for one of the six is ever needed, it needs a first-seen
142 + * migration protocol first (#1756 phase 2/3 territory — see the issue).
143 + *
144 + * ADR 0036 extends that rule to ANY collection serving-recipe change: serializer shape, digest
145 + * formula key sets (DIGESTED_META_KEYS / CUSTOMER_DIGESTED_META_KEYS in Digest_Index), or the
146 + * augmentation set. Bump that collection's version IN THE SAME COMMIT; the fingerprint move is
147 + * what triggers the client re-pull that a silent formula change never did.
148 + *
149 + * # Safety against an un-upgraded store
150 + *
151 + * A store still on the old plugin never moves this value, so its clients see no change and
152 + * re-fetch nothing. That is what lets the migration ship with no version gate and no bespoke
153 + * purge lane: the failure mode a gate would defend against is structurally absent.
154 + *
155 + * @var array<string, int>
156 + */
157 + /**
158 + * The contract version every collection starts at, and the value that must NOT appear in a
159 + * fingerprint — see representation_settings().
160 + *
161 + * @var int
162 + */
163 + private const BASELINE_CONTRACT_VERSION = 1;
164 +
165 + private const PAYLOAD_CONTRACT_VERSION = array(
166 + // products: product serialization + DIGESTED_META_KEYS formula + barcode augmentation.
167 + 'products' => 1,
168 + // 2 (1.10.1): variations are serialized through WC_REST_Product_Variations_Controller
169 + // instead of the products controller — singular `image`, `wc_get_formatted_variation()`
170 + // `name`, and no product-only fields. See the 1.10.1 variations spec, S1/S6.
171 + // variations: variation serialization + shared DIGESTED_META_KEYS formula + barcode augmentation.
172 + 'variations' => 2,
173 + // orders: order serialization + the HPOS/CPT order digest formula.
174 + 'orders' => 1,
175 + // customers: customer serialization + CUSTOMER_DIGESTED_META_KEYS formula.
176 + 'customers' => 1,
177 + // categories: product-category term serialization + its augmentation set.
178 + 'categories' => 1,
179 + // brands: product-brand term serialization + its augmentation set.
180 + 'brands' => 1,
181 + // tags: product-tag term serialization + its augmentation set.
182 + 'tags' => 1,
183 + // coupons: coupon serialization + its augmentation set.
184 + 'coupons' => 1,
185 + // tax_rates: tax-rate serialization + its augmentation set.
186 + 'tax_rates' => 1,
187 + );
188 +
189 + /**
109 190 * The canonicalized representation-affecting settings for a collection.
110 191 * DELIBERATELY A SUPERSET: this set must GROW as new representation settings
111 192 * are added, because an omitted setting would silently miss its config
112 193 * change. ksort() is mandatory (ADR 0006 "Canonicalization discipline") — an
@@ -122,8 +203,26 @@
122 203 // this array.
123 204 $settings = array();
124 205 }
125 206
207 + /*
208 + * The served SHAPE is as much a part of the representation as the settings that fill it —
209 + * see PAYLOAD_CONTRACT_VERSION.
210 + *
211 + * Added ONLY above the baseline. Adding it unconditionally would change the serialization
212 + * of every collection still at version 1 — `{"barcode_field":"_sku"}` becomes
213 + * `{"barcode_field":"_sku","payload_contract":1}`, and tax_rates' `[]` becomes an object —
214 + * so their fingerprints would move too. A fingerprint move marks the collection stale, so
215 + * upgrading would trigger a full PRODUCTS catalogue re-fetch and a tax-rate refresh on every
216 + * active till, for a shape that did not change. Omitting the key at the baseline keeps
217 + * version-1 serialization byte-identical, so only a collection whose contract actually moved
218 + * is re-pulled. A future bump anywhere makes the key appear, which is itself the change.
219 + */
220 + $contract = self::payload_contract_version( $collection );
221 + if ( self::BASELINE_CONTRACT_VERSION < $contract ) {
222 + $settings['payload_contract'] = $contract;
223 + }
224 +
126 225 ksort( $settings );
127 226
128 227 return $settings;
129 228 }
@@ -128,8 +227,19 @@
128 227 return $settings;
129 228 }
130 229
131 230 /**
231 + * This collection's payload contract version. Unknown collections report 1 rather than 0, so a
232 + * collection added to the registry without a deliberate entry starts from the same baseline as
233 + * every other unbumped one instead of silently reading as "older than everything".
234 + *
235 + * @param string $collection Collection name.
236 + */
237 + public static function payload_contract_version( string $collection ): int {
238 + return self::PAYLOAD_CONTRACT_VERSION[ $collection ] ?? self::BASELINE_CONTRACT_VERSION;
239 + }
240 +
241 + /**
132 242 * The per-collection fingerprint — md5 over the canonical settings JSON.
133 243 */
134 244 public function fingerprint( string $collection ): string {
135 245 return md5( (string) wp_json_encode( $this->representation_settings( $collection ) ) );
@@ -192,11 +302,13 @@
192 302 *
193 303 * Deletes by EXACT key over the known COLLECTIONS rather than a
194 304 * `LIKE 'woocommerce_pos_sync_config_fp_%'` scan: the namespace is a closed set, so the
195 305 * exact-key form needs no $wpdb query and cannot collide with a future option that
196 - * happens to share the prefix. Only the barcode collections were ever written, but
197 - * sweeping the full COLLECTIONS set costs one extra no-op delete and catches a
198 - * stray row from any revision.
306 + * happens to share the prefix. Only the barcode collections were ever written, so
307 + * sweeping the now-universal membership (nine collections since #1756) costs a
308 + * handful of no-op deletes on a fresh install and nothing on an already-swept one
309 + * (the CLEANUP_VERSION latch above), while catching a stray row from any revision
310 + * that DID write one.
199 311 *
200 312 * Idempotent and correctness-neutral: the endpoint recomputes the fingerprint from
201 313 * live options as the sole source of truth, so removing these rows cannot change a
202 314 * served value.
@@ -209,9 +321,12 @@
209 321 foreach ( self::collections() as $collection ) {
210 322 delete_option( self::LEGACY_PROACTIVE_OPTION_PREFIX . $collection );
211 323 }
212 324
213 - update_option( self::CLEANUP_VERSION_OPTION, self::CLEANUP_VERSION, false );
325 + // Autoloaded: the Init constructor reads this latch on every request.
326 + // Existing rows from older releases are flipped by
327 + // Activator::autoload_request_latches() on upgrade.
328 + update_option( self::CLEANUP_VERSION_OPTION, self::CLEANUP_VERSION, true );
214 329 }
215 330
216 331 /** Collections whose served representation depends on the barcode setting. */
217 332 /**