Skip to content

Commit 0ccddcc

Browse files
committed
fix product duplication bug
1 parent a5fbc05 commit 0ccddcc

File tree

11 files changed

+282
-43
lines changed

11 files changed

+282
-43
lines changed

includes/API/Traits/Uuid_Handler.php

Lines changed: 132 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
use WC_Order_Item;
1010
use WCPOS\WooCommercePOS\Logger;
1111
use WP_User;
12+
use WC_Product;
13+
use WC_Product_Variation;
14+
use WC_Abstract_Order;
15+
use Automattic\WooCommerce\Utilities\OrderUtil;
1216
use function get_user_meta;
1317
use function update_user_meta;
1418

@@ -33,21 +37,25 @@ function ( WC_Meta_Data $meta ) {
3337
$uuids
3438
);
3539

36-
// If there is no uuid, add one, i.e., new product
37-
if ( empty( $uuid_values ) ) {
38-
$object->update_meta_data( '_woocommerce_pos_uuid', $this->create_uuid() );
39-
}
40-
41-
// Check if there's more than one uuid, if so, delete and regenerate
40+
// Check if there's more than one uuid, if so, keep the first and delete the rest.
4241
if ( \count( $uuid_values ) > 1 ) {
43-
foreach ( $uuids as $uuid_meta ) {
44-
$object->delete_meta_data( $uuid_meta->key );
42+
// Keep the first UUID and remove the rest.
43+
for ( $i = 1; $i < count( $uuid_values ); $i++ ) {
44+
$object->delete_meta_data_by_mid( $uuids[ $i ]->id );
4545
}
46+
$uuid_values = array( reset( $uuid_values ) ); // Keep only the first UUID in the array.
47+
}
48+
49+
// Check conditions for updating the UUID.
50+
$should_update_uuid = empty( $uuid_values )
51+
|| ( isset( $uuid_values[0] ) && ! Uuid::isValid( $uuid_values[0] ) )
52+
|| ( isset( $uuid_values[0] ) && $this->uuid_postmeta_exists( $uuid_values[0], $object ) );
53+
54+
if ( $should_update_uuid ) {
4655
$object->update_meta_data( '_woocommerce_pos_uuid', $this->create_uuid() );
4756
}
4857
}
4958

50-
5159
/**
5260
* @param WP_User $user
5361
*
@@ -56,7 +64,21 @@ function ( WC_Meta_Data $meta ) {
5664
private function maybe_add_user_uuid( WP_User $user ): void {
5765
$uuids = get_user_meta( $user->ID, '_woocommerce_pos_uuid', false );
5866

59-
if ( empty( $uuids ) || empty( $uuids[0] ) ) {
67+
// Check if there's more than one uuid, if so, keep the first and delete the rest.
68+
if ( count( $uuids ) > 1 ) {
69+
// Keep the first UUID and remove the rest.
70+
for ( $i = 1; $i < count( $uuids ); $i++ ) {
71+
delete_user_meta( $user->ID, '_woocommerce_pos_uuid', $uuids[ $i ] );
72+
}
73+
$uuids = array( $uuids[0] );
74+
}
75+
76+
// Check conditions for updating the UUID.
77+
$should_update_uuid = empty( $uuids )
78+
|| ( isset( $uuids[0] ) && ! Uuid::isValid( $uuids[0] ) )
79+
|| ( isset( $uuids[0] ) && $this->uuid_usermeta_exists( $uuids[0], $user->ID ) );
80+
81+
if ( $should_update_uuid ) {
6082
update_user_meta( $user->ID, '_woocommerce_pos_uuid', $this->create_uuid() );
6183
}
6284
}
@@ -84,14 +106,30 @@ private function maybe_add_order_item_uuid( WC_Order_Item $item ): void {
84106
*
85107
* @return string
86108
*/
87-
private function get_term_uuid( object $item ): string {
88-
$uuid = get_term_meta( $item->term_id, '_woocommerce_pos_uuid', true );
89-
if ( ! $uuid ) {
90-
$uuid = Uuid::uuid4()->toString();
91-
add_term_meta( $item->term_id, '_woocommerce_pos_uuid', $uuid, true );
109+
private function get_term_uuid( $term ): string {
110+
$uuids = get_term_meta( $term->term_id, '_woocommerce_pos_uuid', false );
111+
112+
// Check if there's more than one uuid, if so, keep the first and delete the rest.
113+
if ( count( $uuids ) > 1 ) {
114+
// Keep the first UUID and remove the rest.
115+
for ( $i = 1; $i < count( $uuids ); $i++ ) {
116+
delete_term_meta( $term->term_id, '_woocommerce_pos_uuid', $uuids[ $i ] );
117+
}
118+
$uuids = array( $uuids[0] );
119+
}
120+
121+
// Check conditions for updating the UUID.
122+
$should_update_uuid = empty( $uuids )
123+
|| ( isset( $uuids[0] ) && ! Uuid::isValid( $uuids[0] ) )
124+
|| ( isset( $uuids[0] ) && $this->uuid_termmeta_exists( $uuids[0], $term->term_id ) );
125+
126+
if ( $should_update_uuid ) {
127+
$uuid = $this->create_uuid();
128+
add_term_meta( $term->term_id, '_woocommerce_pos_uuid', $uuid, true );
129+
return $uuid;
92130
}
93131

94-
return $uuid;
132+
return $uuids[0];
95133
}
96134

97135
/**
@@ -108,4 +146,82 @@ private function create_uuid(): string {
108146
return 'fallback-uuid-' . time();
109147
}
110148
}
149+
150+
/**
151+
* Check if the given UUID is unique.
152+
*
153+
* @param string $uuid The UUID to check.
154+
* @param WC_Data $object The WooCommerce data object.
155+
* @return bool True if unique, false otherwise.
156+
*/
157+
private function uuid_postmeta_exists( string $uuid, WC_Data $object ): bool {
158+
global $wpdb;
159+
160+
if ( $object instanceof WC_Abstract_Order && OrderUtil::custom_orders_table_usage_is_enabled() ) {
161+
// Check the orders meta table.
162+
$result = $wpdb->get_var(
163+
$wpdb->prepare(
164+
"SELECT 1 FROM {$wpdb->prefix}wc_ordermeta WHERE meta_key = '_woocommerce_pos_uuid' AND meta_value = %s AND order_id != %d LIMIT 1",
165+
$uuid,
166+
$object->get_id()
167+
)
168+
);
169+
} else {
170+
// Check the postmeta table.
171+
$result = $wpdb->get_var(
172+
$wpdb->prepare(
173+
"SELECT 1 FROM {$wpdb->postmeta} WHERE meta_key = '_woocommerce_pos_uuid' AND meta_value = %s AND post_id != %d LIMIT 1",
174+
$uuid,
175+
$object->get_id()
176+
)
177+
);
178+
}
179+
180+
// Convert the result to a boolean.
181+
return (bool) $result;
182+
}
183+
184+
/**
185+
* Check if the given UUID already exists for any user.
186+
*
187+
* @param string $uuid The UUID to check.
188+
* @param int $exclude_id The user ID to exclude from the check.
189+
* @return bool True if unique, false otherwise.
190+
*/
191+
private function uuid_usermeta_exists( string $uuid, int $exclude_id ): bool {
192+
global $wpdb;
193+
194+
$result = $wpdb->get_var(
195+
$wpdb->prepare(
196+
"SELECT 1 FROM {$wpdb->usermeta} WHERE meta_key = '_woocommerce_pos_uuid' AND meta_value = %s AND user_id != %d LIMIT 1",
197+
$uuid,
198+
$exclude_id
199+
)
200+
);
201+
202+
// Convert the result to a boolean.
203+
return (bool) $result;
204+
}
205+
206+
/**
207+
* Check if the given UUID already exists for any term.
208+
*
209+
* @param string $uuid The UUID to check.
210+
* @param int $exclude_term_id The term ID to exclude from the check.
211+
* @return bool True if unique, false otherwise.
212+
*/
213+
private function uuid_termmeta_exists( string $uuid, int $exclude_term_id ): bool {
214+
global $wpdb;
215+
216+
$result = $wpdb->get_var(
217+
$wpdb->prepare(
218+
"SELECT 1 FROM {$wpdb->termmeta} WHERE meta_key = '_woocommerce_pos_uuid' AND meta_value = %s AND term_id != %d LIMIT 1",
219+
$uuid,
220+
$exclude_term_id
221+
)
222+
);
223+
224+
// Convert the result to a boolean.
225+
return (bool) $result;
226+
}
111227
}

includes/Admin/Products/List_Products.php

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class List_Products {
2727
private $options;
2828

2929

30-
30+
3131
public function __construct() {
3232
$this->barcode_field = woocommerce_pos_get_settings( 'general', 'barcode_field' );
3333

@@ -43,10 +43,15 @@ public function __construct() {
4343
add_action( 'woocommerce_product_options_sku', array( $this, 'woocommerce_product_options_sku' ) );
4444
add_action( 'woocommerce_process_product_meta', array( $this, 'woocommerce_process_product_meta' ) );
4545
// variations
46-
add_action('woocommerce_product_after_variable_attributes', array(
47-
$this,
48-
'after_variable_attributes_barcode_field',
49-
), 10, 3);
46+
add_action(
47+
'woocommerce_product_after_variable_attributes',
48+
array(
49+
$this,
50+
'after_variable_attributes_barcode_field',
51+
),
52+
10,
53+
3
54+
);
5055
add_action( 'woocommerce_save_product_variation', array( $this, 'save_product_variation_barcode_field' ) );
5156
}
5257

@@ -57,17 +62,30 @@ public function __construct() {
5762
add_action( 'woocommerce_product_bulk_edit_save', array( $this, 'bulk_edit_save' ) );
5863
add_action( 'quick_edit_custom_box', array( $this, 'quick_edit' ), 10, 2 );
5964
add_action( 'manage_product_posts_custom_column', array( $this, 'custom_product_column' ), 10, 2 );
60-
add_action('woocommerce_product_after_variable_attributes', array(
61-
$this,
62-
'after_variable_attributes_pos_only_products',
63-
), 10, 3);
64-
add_action('woocommerce_save_product_variation', array(
65-
$this,
66-
'save_product_variation_pos_only_products',
67-
));
65+
add_action(
66+
'woocommerce_product_after_variable_attributes',
67+
array(
68+
$this,
69+
'after_variable_attributes_pos_only_products',
70+
),
71+
10,
72+
3
73+
);
74+
add_action(
75+
'woocommerce_save_product_variation',
76+
array(
77+
$this,
78+
'save_product_variation_pos_only_products',
79+
)
80+
);
6881
}
82+
83+
add_filter( 'woocommerce_duplicate_product_exclude_meta', array( $this, 'exclude_uuid_meta_on_product_duplicate' ) );
6984
}
7085

86+
/**
87+
*
88+
*/
7189
public function woocommerce_product_options_sku(): void {
7290
woocommerce_wp_text_input(
7391
array(
@@ -228,4 +246,17 @@ public function custom_product_column( $column, $post_id ): void {
228246
echo '<div class="hidden" id="woocommerce_pos_inline_' . $post_id . '" data-visibility="' . $selected . '"></div>';
229247
}
230248
}
249+
250+
/**
251+
* Filter to allow us to exclude meta keys from product duplication..
252+
*
253+
* @param array $exclude_meta The keys to exclude from the duplicate.
254+
* @param array $existing_meta_keys The meta keys that the product already has.
255+
*
256+
* @return array
257+
*/
258+
public function exclude_uuid_meta_on_product_duplicate( array $meta_keys ) {
259+
$meta_keys[] = '_woocommerce_pos_uuid';
260+
return $meta_keys;
261+
}
231262
}

includes/Templates/Frontend.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public function head(): void {
8686
public function footer(): void {
8787
$development = isset( $_ENV['DEVELOPMENT'] ) && $_ENV['DEVELOPMENT'];
8888
$user = wp_get_current_user();
89-
$github_url = 'https://cdn.jsdelivr.net/gh/wcpos/web-bundle@latest/';
89+
$github_url = 'https://cdn.jsdelivr.net/gh/wcpos/web-bundle@1.4/';
9090
$auth_service = Auth::instance();
9191
$stores = array_map(
9292
function ( $store ) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@wcpos/woocommerce-pos",
3-
"version": "1.4.7",
3+
"version": "1.4.8",
44
"description": "A simple front-end for taking WooCommerce orders at the Point of Sale.",
55
"main": "index.js",
66
"workspaces": {

readme.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Contributors: kilbot
33
Tags: cart, e-commerce, ecommerce, inventory, point-of-sale, pos, sales, sell, shop, shopify, store, vend, woocommerce, wordpress-ecommerce
44
Requires at least: 5.6 & WooCommerce 5.3
55
Tested up to: 6.4
6-
Stable tag: 1.4.7
6+
Stable tag: 1.4.8
77
License: GPL-3.0
88
License URI: http://www.gnu.org/licenses/gpl-3.0.html
99

@@ -63,6 +63,9 @@ There is more information on our website at [https://wcpos.com](https://wcpos.co
6363

6464
== Changelog ==
6565

66+
= 1.4.8 - 2024/01/21 =
67+
* Fix: duplicating Products in WC Admin also duplicated POS UUID, which casued problems
68+
6669
= 1.4.7 - 2024/01/18 =
6770
* Bump: web application to version 1.4.1
6871
* Fix: scroll-to-top issue when scrolling data tables

templates/pos.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<title><?php esc_attr_e( 'Point of Sale', 'woocommerce-pos' ); ?> - <?php esc_html( bloginfo( 'name' ) ); ?></title>
1515
<meta charset="utf-8"/>
1616

17-
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover">
17+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
1818
<meta name="theme-color" content="#000000">
1919
<meta name="apple-mobile-web-app-capable" content="yes"/>
2020

@@ -54,7 +54,7 @@
5454
* Matches Expo build
5555

5656
* Extend the react-native-web reset:
57-
* https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/exports/StyleSheet/initialRules.js
57+
* https://necolas.github.io/react-native-web/docs/setup/#root-element
5858
*/
5959
html,
6060
body,
@@ -115,4 +115,3 @@
115115
<?php do_action( 'woocommerce_pos_footer' ); ?>
116116

117117
</html>
118-

templates/receipt.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,6 @@
320320

321321
</section><!-- /.col2-set -->
322322

323-
324-
325323
<?php do_action( 'woocommerce_order_details_after_customer_details', $order ); ?>
326324

327325
</section>
@@ -334,10 +332,6 @@
334332
</div>
335333
<?php } ?>
336334

337-
<div class="footer">
338-
<p><?php esc_html_e( 'Thank you for your purchase!', 'woocommerce' ); ?></p>
339-
<p><?php bloginfo( 'name' ); ?> - <?php bloginfo( 'description' ); ?></p>
340-
</div>
341335
</div>
342336

343337
</body>

tests/includes/API/Test_Customers_Controller.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,4 +524,39 @@ public function test_customer_search_with_excludes(): void {
524524
$this->assertEquals( 1, \count( $data ) );
525525
$this->assertEquals( $customer1->get_id(), $data[0]['id'] );
526526
}
527+
528+
/**
529+
*
530+
*/
531+
public function test_customer_uuid_is_unique(): void {
532+
$uuid = UUID::uuid4()->toString();
533+
$customer1 = CustomerHelper::create_customer();
534+
$customer1->update_meta_data( '_woocommerce_pos_uuid', $uuid );
535+
$customer1->save_meta_data();
536+
$customer2 = CustomerHelper::create_customer();
537+
$customer2->update_meta_data( '_woocommerce_pos_uuid', $uuid );
538+
$customer2->save_meta_data();
539+
540+
$request = $this->wp_rest_get_request( '/wcpos/v1/customers' );
541+
542+
$response = $this->server->dispatch( $request );
543+
$data = $response->get_data();
544+
545+
$this->assertEquals( 200, $response->get_status() );
546+
$this->assertEquals( 2, \count( $data ) );
547+
548+
// pluck uuids from meta_data
549+
$uuids = array();
550+
foreach ( $data as $customer ) {
551+
foreach ( $customer['meta_data'] as $meta ) {
552+
if ( '_woocommerce_pos_uuid' === $meta['key'] ) {
553+
$uuids[] = $meta['value'];
554+
}
555+
}
556+
}
557+
558+
$this->assertEquals( 2, \count( $uuids ) );
559+
$this->assertContains( $uuid, $uuids );
560+
$this->assertEquals( 2, \count( array_unique( $uuids ) ) );
561+
}
527562
}

0 commit comments

Comments
 (0)