API: Total Stock not recalculated. Update only when value changes
-

Affected: 3.0.1 (reproduced), still present after 3.0.2
When updating stock via the public endpoint /?slw-api&…&action=set&item=stock, the plugin should:
- Update _stock_at_{location_id} only if the new value differs from the current one.
- Recalculate and persist the product/variation Total Stock as the sum across locations, and only when a real change occurred.
Currently, the API can trigger recalculation even when the value is unchanged, and the variation’s WC stock quantity may not be refreshed to match the summed location stock.
Expected behavior
- No DB writes or recalculation when the provided value equals the current _stock_at_{location_id}.
- On change, write the meta, then recompute the Total Stock for that specific product/variation and sync WC stock/status (and parent variable product if applicable).
Steps to reproduce
1) Ensure SLW API is enabled and the caller is validated.
2) Call:
/?slw-api&value=<current_value>&action=set&item=stock&product_id=<variation_or_product_id>&location_id=<term_id>&format=json3) Observe that recalculation runs despite no effective change, and/or Total Stock on the variation is not refreshed to the sum.
Proposed fix
- File: wp-content/plugins/stock-locations-for-woocommerce/inc/functions-api.php
- In case ‘set’ → item ‘stock’, update meta only on change; then recalc and sync stock/status for the specific product, and finally sync the parent if it is a variation.
<?php
add_action('init', function(){
if(isset($_GET['slw-api'])){
global $slw_api_valid_keys;
$response = array('response'=>false);
$recalc_required = false;
$data = array('action'=>'','format'=>'');
if(!empty($_GET)){
$received = sanitize_slw_data($_GET);
if ( empty( $slw_api_valid_keys ) || ! is_array( $slw_api_valid_keys ) ) {
$slw_api_valid_keys = [];
}
foreach($received as $k=>$v){
if(array_key_exists($k, $slw_api_valid_keys)){
$data[$k] = $v;
}
}
}
if(get_option('slw_api_status')==true){
$current_source = ($_SERVER['REMOTE_ADDR'].'/'.$_SERVER['SERVER_NAME']);
$validated_requests = get_option('slw_api_request_validated', array());
$validated_requests = (is_array($validated_requests)?$validated_requests:array());
$all_requests = get_option('slw_api_request_sources', array());
$all_requests = (is_array($all_requests)?$all_requests:array());
$all_requests[time()] = $current_source;
$all_requests = array_unique($all_requests);
update_option('slw_api_request_sources', $all_requests);
if(!in_array($current_source, $validated_requests)){
_e('Sorry, you are not allowed to proceed.', 'stock-locations-for-woocommerce');
exit;
}
switch($data['action']){
case 'read':
case 'get':
case 'fetch':
case 'pull':
switch($data['item']){
case 'location':
if($data['id']){
$response = get_term_by('id', $data['id'], 'location');
}else{
$response = slw_get_locations();
}
break;
case 'stock':
if($data['product_id'] && $data['location_id']){
$response['stock_value'] = get_post_meta($data['product_id'], '_stock_at_' . $data['location_id'], true);
}
break;
case 'product':
//pree($data['id']);
if($data['id']){
$response[$data['id']] = wc_get_product($data['id']);
}else{
$products = get_posts( array('post_type'=>'product') );
if(!empty($products)){
foreach($products as $product){ if(!is_object($product)){ continue; }
//pree($product->ID);
$response[$product->ID] = wc_get_product($product->ID);
}
}
}
break;
case 'price':
$response['price'] = get_post_meta($data['product_id'], '_price', true);
break;
}
break;
case 'replace':
case 'update':
case 'put':
case 'set':
switch($data['item']){
case 'location':
if($data['product_id'] && $data['location_id']){
$product_locations = wp_get_object_terms($data['product_id'], 'location' );
$paux = array(intval($data['location_id']));
foreach($product_locations as $termVal) {
if ($termVal -> term_id != $paux[0]) $paux[] = $termVal -> term_id;
}
$response['response'] = wp_set_object_terms($data['product_id'], $paux, 'location');
}
break;
case 'stock':
if($data['product_id'] && $data['location_id'] && $data['value']>=0){
// Update only if value actually changed
$current_value = get_post_meta($data['product_id'], '_stock_at_' . $data['location_id'], true);
if((string)$current_value !== (string)$data['value']){
$product_locations = wp_get_object_terms($data['product_id'], 'location' );
$paux = array(intval($data['location_id']));
foreach($product_locations as $termVal) {
if ($termVal->term_id != $paux[0]) $paux[] = $termVal->term_id;
}
wp_set_object_terms($data['product_id'], $paux, 'location');
$response['response'] = update_post_meta($data['product_id'], '_stock_at_' . $data['location_id'], $data['value']);
$recalc_required = true;
}else{
$response['response'] = false; // no change
}
}
break;
case 'product':
break;
case 'price':
if($data['product_id'] && (isset($data['value']) && is_numeric($data['value']) && $data['value']>=0)){
update_post_meta($data['product_id'], '_regular_price', $data['value']);
$response['response'] = update_post_meta($data['product_id'], '_price', $data['value']);
}
break;
}
break;
}
}else{
_e('Sorry, API is not enabled.', 'stock-locations-for-woocommerce');
exit;
}
if($data['product_id']){
$response['product_id'] = $data['product_id'];
// Recalculate stock totals only if there was an actual change
if($recalc_required){
// 1) Update the stock quantity on the specific product/variation directly
$locations_total = \SLW\SRC\Helpers\SlwProductHelper::get_product_locations_stock_total($data['product_id']);
if($locations_total !== null){
slw_update_product_stock_status($data['product_id'], (int)$locations_total);
}
// 2) Refresh parent variable product (if applicable) via existing aggregator
$recalc_parent_id = $data['product_id'];
$wc_product_obj = wc_get_product($recalc_parent_id);
if($wc_product_obj && $wc_product_obj->is_type('variation')){
$recalc_parent_id = $wc_product_obj->get_parent_id();
}
// Lightweight status/quantity sync for parent
\SLW\SRC\Helpers\SlwProductHelper::update_wc_stock_status($recalc_parent_id, null, true);
}
}
switch($data['format']){
default:
case 'default':
pree($response);
break;
case 'json':
echo json_encode($response);
break;
}
exit;
}
}, 11);Rationale
- Prevents unnecessary DB writes and hook churn when the value is unchanged.
- Ensures the WC stock quantity for the target product/variation immediately reflects the sum across locations.
- Keeps parent variable product’s status/quantity consistent after a variation’s stock change.
You must be logged in to reply to this topic.