fix(sync): push stock immediately on new product creation

ProductSyncService now calls pushInitialStock() after creating a new
product in PrestaShop, so stock is correct without waiting for the next
StockSyncService run. Failures are caught and logged as warnings — the
product mapping is saved as SYNCED regardless, and StockSyncService will
correct any discrepancy on its next scheduled run.

Also applies the PS 8.2.5 false-instead-of-empty-array workaround to
getStockAvailableForProduct(), consistent with getProductByReference().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
luklpz 2026-05-18 18:27:17 +02:00
parent a5feff6a1a
commit e501d83c73
2 changed files with 49 additions and 12 deletions

View File

@ -156,21 +156,30 @@ public class PrestashopClient {
* @param productId PrestaShop product internal ID * @param productId PrestaShop product internal ID
*/ */
public Optional<PrestashopStockAvailableDto> getStockAvailableForProduct(Integer productId) { public Optional<PrestashopStockAvailableDto> getStockAvailableForProduct(Integer productId) {
PrestashopStockAvailableDto.ListResponse response = restClient.get() try {
.uri(u -> u.path("/stock_availables") PrestashopStockAvailableDto.ListResponse response = restClient.get()
.queryParam("ws_key", wsKey) .uri(u -> u.path("/stock_availables")
.queryParam("output_format", "JSON") .queryParam("ws_key", wsKey)
.queryParam("display", "full") .queryParam("output_format", "JSON")
.queryParam("filter[id_product]", productId) .queryParam("display", "full")
.queryParam("filter[id_product_attribute]", "0") .queryParam("filter[id_product]", productId)
.build()) .queryParam("filter[id_product_attribute]", "0")
.retrieve() .build())
.body(PrestashopStockAvailableDto.ListResponse.class); .retrieve()
.body(PrestashopStockAvailableDto.ListResponse.class);
if (response == null || response.stockAvailables() == null || response.stockAvailables().isEmpty()) { if (response == null || response.stockAvailables() == null || response.stockAvailables().isEmpty()) {
return Optional.empty();
}
return Optional.of(response.stockAvailables().get(0));
} catch (PrestashopApiException e) {
if (e.getStatusCode() != null && e.getStatusCode().value() == 404) return Optional.empty();
throw e;
} catch (HttpMessageConversionException | RestClientException e) {
// PS 8.x returns {"stock_availables": false} instead of [] when filter has no results
log.debug("PS returned unparseable body for stock_available productId={}, treating as not found: {}", productId, e.getMessage());
return Optional.empty(); return Optional.empty();
} }
return Optional.of(response.stockAvailables().get(0));
} }
/** Updates the stock quantity for a stock_available record. */ /** Updates the stock quantity for a stock_available record. */

View File

@ -6,6 +6,7 @@ import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrP
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.exception.DolibarrApiException; import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.exception.DolibarrApiException;
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.PrestashopClient; import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.PrestashopClient;
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto.PrestashopProductDto; import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto.PrestashopProductDto;
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto.PrestashopStockAvailableDto;
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.exception.PrestashopApiException; import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.exception.PrestashopApiException;
import com.teterialosjuanjos.tfg.sync_service.mapping.*; import com.teterialosjuanjos.tfg.sync_service.mapping.*;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -110,10 +111,37 @@ public class ProductSyncService {
"Product created in PS but not found via GET (reference=" + sku + ")"); "Product created in PS but not found via GET (reference=" + sku + ")");
} }
upsertMapping(sku, dolProduct.id(), created.id()); upsertMapping(sku, dolProduct.id(), created.id());
pushInitialStock(dolProduct, created.id(), sku);
log.debug("ProductSync: created PS product reference={} id={}", sku, created.id()); log.debug("ProductSync: created PS product reference={} id={}", sku, created.id());
} }
} }
/**
* Pushes Dolibarr stock to the PS stock_available record immediately after product creation.
* Skipped when stockReel is null or zero (PS default is already 0).
* Failures are logged as warnings but do not abort the product sync — StockSyncService
* will correct any discrepancy on its next run.
*/
private void pushInitialStock(DolibarrProductDto dolProduct, Integer psProductId, String sku) {
if (dolProduct.stockReel() == null || dolProduct.stockReel() == 0) return;
int qty = dolProduct.stockReel().intValue();
try {
Optional<PrestashopStockAvailableDto> stockOpt =
prestashopClient.getStockAvailableForProduct(psProductId);
if (stockOpt.isEmpty()) {
log.warn("ProductSync: no stock_available found for new product SKU={}, stock will sync on next StockSync run", sku);
return;
}
PrestashopStockAvailableDto current = stockOpt.get();
prestashopClient.updateStockAvailable(current.id(),
new PrestashopStockAvailableDto(current.id(), current.idProduct(),
current.idProductAttribute(), String.valueOf(qty)));
log.debug("ProductSync: initial stock pushed SKU={} qty={}", sku, qty);
} catch (PrestashopApiException | RestClientException | HttpMessageConversionException e) {
log.warn("ProductSync: could not push initial stock for SKU={}: {}", sku, e.getMessage());
}
}
private void upsertMapping(String sku, Integer dolibarrId, Integer prestashopId) { private void upsertMapping(String sku, Integer dolibarrId, Integer prestashopId) {
ProductMapping mapping = productMappingRepository.findBySku(sku) ProductMapping mapping = productMappingRepository.findBySku(sku)
.orElseGet(() -> ProductMapping.builder().sku(sku).build()); .orElseGet(() -> ProductMapping.builder().sku(sku).build());