/**
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.broadleafcommerce.inventory.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Resource;
import org.broadleafcommerce.common.currency.domain.BroadleafCurrency;
import org.broadleafcommerce.common.persistence.EntityConfiguration;
import org.broadleafcommerce.core.catalog.domain.Product;
import org.broadleafcommerce.core.catalog.domain.Sku;
import org.broadleafcommerce.core.inventory.service.type.InventoryType;
import org.broadleafcommerce.inventory.dao.InventoryDao;
import org.broadleafcommerce.inventory.domain.FulfillmentLocation;
import org.broadleafcommerce.inventory.domain.Inventory;
import org.broadleafcommerce.inventory.exception.ConcurrentInventoryModificationException;
import org.broadleafcommerce.inventory.exception.InventoryUnavailableException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service("blInventoryService")
public class InventoryServiceImpl implements InventoryService {
@Resource(name = "blInventoryDao")
protected InventoryDao inventoryDao;
@Resource(name = "blEntityConfiguration")
protected EntityConfiguration entityConfiguration;
@Override
public boolean isSkuEligibleForInventoryCheck(Sku sku) {
if (sku.getInventoryType() == null
&& (sku.getProduct().getDefaultCategory() == null || sku
.getProduct().getDefaultCategory().getInventoryType() == null)) {
return false;
} else if (InventoryType.NONE.equals(sku.getInventoryType())
|| (sku.getProduct().getDefaultCategory() != null && InventoryType.NONE
.equals(sku.getProduct().getDefaultCategory()
.getInventoryType()))) {
return false;
} else if (InventoryType.BASIC.equals(sku.getInventoryType())
|| (sku.getProduct().getDefaultCategory() != null && InventoryType.BASIC
.equals(sku.getProduct().getDefaultCategory()
.getInventoryType()))) {
return true;
}
return false;
}
@Override
public boolean isQuantityAvailable(Sku sku, Integer quantity) {
// if the sku does not exist or is not active, there is no quantity
// available
if (!sku.isActive()) {
return false;
}
if (!isSkuEligibleForInventoryCheck(sku)) {
// This sku is not eligible for inventory checks, so assume it is
// available
return true;
}
// quantity must be greater than 0
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must be a positive integer");
}
Inventory inventory = inventoryDao
.readInventoryForDefaultFulfillmentLocation(sku);
return inventory != null
&& inventory.getQuantityAvailable() >= quantity;
}
@Override
@Transactional("blTransactionManager")
public boolean isQuantityAvailable(Sku sku, Integer quantity,
FulfillmentLocation fulfillmentLocation) {
// if the sku does not exist or is not active, there is no quantity
// available
if (!sku.isActive()) {
return false;
}
if (!isSkuEligibleForInventoryCheck(sku)) {
// This sku is not eligible for inventory checks, so assume it is
// available
return true;
}
// quantity must be greater than 0
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must be a positive integer");
}
Inventory inventory = inventoryDao.readInventory(sku,
fulfillmentLocation);
return inventory != null
&& inventory.getQuantityAvailable() >= quantity;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, value = "blTransactionManager", rollbackFor = {
InventoryUnavailableException.class,
ConcurrentInventoryModificationException.class })
public void decrementInventory(Map<Sku, Integer> skuInventory)
throws ConcurrentInventoryModificationException,
InventoryUnavailableException {
decrementInventory(skuInventory, null);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, value = "blTransactionManager", rollbackFor = {
InventoryUnavailableException.class,
ConcurrentInventoryModificationException.class })
public void decrementInventory(Map<Sku, Integer> skuInventory,
FulfillmentLocation fulfillmentLocation)
throws ConcurrentInventoryModificationException,
InventoryUnavailableException {
Set<Sku> skus = skuInventory.keySet();
Map<Long, Integer> unavailableInventoryHolder = new HashMap<Long, Integer>();
Map<Sku, Integer> unavailableSkus = new HashMap<Sku, Integer>();
for (Sku sku : skus) {
Integer quantity = skuInventory.get(sku);
/*
* If the inventory type of the sku or category is null or
* InventoryType.NONE, do not adjust inventory
*/
if (!isSkuEligibleForInventoryCheck(sku)) {
// Don't adjust inventory for this Sku
continue;
}
// quantity must not be null
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must not be a positive integer");
}
if (quantity == 0) {
continue;
}
// check available inventory
Inventory inventory = null;
if (fulfillmentLocation != null) {
inventory = inventoryDao.readInventoryForUpdate(sku,
fulfillmentLocation);
} else {
inventory = inventoryDao
.readInventoryForUpdateForDefaultFulfillmentLocation(sku);
}
if (inventory != null) {
Integer quantityAvailable = inventory.getQuantityAvailable();
int qtyToUpdate = quantityAvailable - quantity;
if (qtyToUpdate < 0) {
// there is not enough inventory available
unavailableSkus.put(sku, quantityAvailable);
unavailableInventoryHolder.put(sku.getId(),
quantityAvailable);
} else {
inventory.setQuantityAvailable(qtyToUpdate);
inventoryDao.save(inventory); // this call could throw
// ConcurrentInventoryModificationException
}
} else {
unavailableInventoryHolder.put(sku.getId(), 0);
}
}
if (!unavailableInventoryHolder.isEmpty()) {
String errorMessage = null;
errorMessage = "<h3>有" + unavailableInventoryHolder.size()
+ "个商品库存不足 请修改该商品的购买数量</h3><br/>商品名称为:<br/><br/>";
for (Map.Entry<Sku, Integer> unavaiableSku : unavailableSkus
.entrySet()) {
errorMessage = errorMessage + unavaiableSku.getKey().getName()
+ ";<br/>该商品还有" + unavaiableSku.getValue()
+ "个库存。<br/><br/>";
}
InventoryUnavailableException ex = new InventoryUnavailableException(
errorMessage);
ex.setSkuInventoryAvailable(unavailableInventoryHolder);
throw ex;
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, value = "blTransactionManager", rollbackFor = {
InventoryUnavailableException.class,
ConcurrentInventoryModificationException.class })
public void decrementInventoryOnHand(Map<Sku, Integer> skuInventory)
throws ConcurrentInventoryModificationException,
InventoryUnavailableException {
decrementInventoryOnHand(skuInventory, null);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, value = "blTransactionManager", rollbackFor = {
InventoryUnavailableException.class,
ConcurrentInventoryModificationException.class })
public void decrementInventoryOnHand(Map<Sku, Integer> skuInventory,
FulfillmentLocation fulfillmentLocation)
throws ConcurrentInventoryModificationException,
InventoryUnavailableException {
Set<Sku> skus = skuInventory.keySet();
Map<Long, Integer> unavailableInventoryHolder = new HashMap<Long, Integer>();
for (Sku sku : skus) {
Integer quantity = skuInventory.get(sku);
/*
* If the inventory type of the sku or category is null or
* InventoryType.NONE, do not adjust inventory
*/
if (!isSkuEligibleForInventoryCheck(sku)) {
// Don't check inventory for this Sku
continue;
}
// quantity must not be null
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must not be a positive integer");
}
if (quantity == 0) {
continue;
}
// check available inventory
Inventory inventory = null;
if (fulfillmentLocation != null) {
inventory = inventoryDao.readInventoryForUpdate(sku,
fulfillmentLocation);
} else {
inventory = inventoryDao
.readInventoryForUpdateForDefaultFulfillmentLocation(sku);
}
if (inventory != null) {
Integer quantityOnHand = inventory.getQuantityOnHand();
int qtyToUpdate = quantityOnHand - quantity;
if (qtyToUpdate < 0) {
// there is not enough inventory available
unavailableInventoryHolder.put(sku.getId(), quantityOnHand);
} else {
inventory.setQuantityOnHand(qtyToUpdate);
inventoryDao.save(inventory); // this call could throw
// ConcurrentInventoryModificationException
}
} else {
unavailableInventoryHolder.put(sku.getId(), 0);
}
}
if (!unavailableInventoryHolder.isEmpty()) {
InventoryUnavailableException ex = new InventoryUnavailableException(
"Inventory is unavailable for "
+ unavailableInventoryHolder.size() + " skus");
ex.setSkuInventoryAvailable(unavailableInventoryHolder);
throw ex;
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, value = "blTransactionManager", rollbackFor = {
InventoryUnavailableException.class,
ConcurrentInventoryModificationException.class })
public void incrementInventory(Map<Sku, Integer> skuInventory,
FulfillmentLocation fulfillmentLocation)
throws ConcurrentInventoryModificationException {
Set<Sku> skus = skuInventory.keySet();
for (Sku sku : skus) {
Integer quantity = skuInventory.get(sku);
/*
* If the inventory type of the sku or category is null or
* InventoryType.NONE, do not adjust inventory
*/
if (!isSkuEligibleForInventoryCheck(sku)) {
// Don't adjust inventory for this Sku
continue;
}
// quantity must not be null
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must not be a positive integer");
}
if (quantity == 0) {
continue;
}
Inventory inventory = null;
if (fulfillmentLocation != null) {
inventory = inventoryDao.readInventoryForUpdate(sku,
fulfillmentLocation);
}
if (inventory != null) {
inventory.setQuantityAvailable(inventory.getQuantityAvailable()
+ quantity);
inventoryDao.save(inventory);
} else {
/*
* create a new inventory record if one does not exist
*/
inventory = (Inventory) entityConfiguration
.createEntityInstance(Inventory.class.getName());
inventory.setQuantityAvailable(quantity);
inventory.setQuantityOnHand(quantity);
inventory.setSku(sku);
inventory.setFulfillmentLocation(fulfillmentLocation);
inventoryDao.save(inventory);
}
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, value = "blTransactionManager", rollbackFor = {
InventoryUnavailableException.class,
ConcurrentInventoryModificationException.class })
public void incrementInventory(Map<Sku, Integer> skuInventory)
throws ConcurrentInventoryModificationException {
Set<Sku> skus = skuInventory.keySet();
for (Sku sku : skus) {
Integer quantity = skuInventory.get(sku);
/*
* If the inventory type of the sku or category is null or
* InventoryType.NONE, do not adjust inventory
*/
if (!isSkuEligibleForInventoryCheck(sku)) {
// Don't adjust inventory for this Sku
continue;
}
// quantity must not be null
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must not be a positive integer");
}
if (quantity == 0) {
continue;
}
Inventory inventory = inventoryDao
.readInventoryForUpdateForDefaultFulfillmentLocation(sku);
if (inventory != null) {
inventory.setQuantityAvailable(inventory.getQuantityAvailable()
+ quantity);
inventoryDao.save(inventory);
} else {
throw new IllegalStateException(
"There was a call to InventoryServiceImpl.incrementInventory for a default fulfillment location, but no default "
+ "inventory for the sku: "
+ sku.getId()
+ " could be found!");
}
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, value = "blTransactionManager", rollbackFor = {
InventoryUnavailableException.class,
ConcurrentInventoryModificationException.class })
public void incrementInventoryOnHand(Map<Sku, Integer> skuInventory,
FulfillmentLocation fulfillmentLocation)
throws ConcurrentInventoryModificationException {
Set<Sku> skus = skuInventory.keySet();
for (Sku sku : skus) {
Integer quantity = skuInventory.get(sku);
/*
* If the inventory type of the sku or category is null or
* InventoryType.NONE, do not adjust inventory
*/
if (!isSkuEligibleForInventoryCheck(sku)) {
// Don't adjust inventory for this Sku
continue;
}
// quantity must not be null
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must not be a positive integer");
}
if (quantity == 0) {
continue;
}
Inventory inventory = inventoryDao.readInventoryForUpdate(sku,
fulfillmentLocation);
if (inventory != null) {
inventory.setQuantityOnHand(inventory.getQuantityOnHand()
+ quantity);
inventoryDao.save(inventory);
} else {
/*
* create a new inventory record if one does not exist
*/
inventory = (Inventory) entityConfiguration
.createEntityInstance(Inventory.class.getName());
inventory.setQuantityAvailable(quantity);
inventory.setQuantityOnHand(quantity);
inventory.setSku(sku);
inventory.setFulfillmentLocation(fulfillmentLocation);
inventoryDao.save(inventory);
}
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, value = "blTransactionManager", rollbackFor = {
InventoryUnavailableException.class,
ConcurrentInventoryModificationException.class })
public void incrementInventoryOnHand(Map<Sku, Integer> skuInventory)
throws ConcurrentInventoryModificationException {
Set<Sku> skus = skuInventory.keySet();
for (Sku sku : skus) {
Integer quantity = skuInventory.get(sku);
/*
* If the inventory type of the sku or category is null or
* InventoryType.NONE, do not adjust inventory
*/
if (!isSkuEligibleForInventoryCheck(sku)) {
// Don't adjust inventory for this Sku
continue;
}
// quantity must not be null
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must not be a positive integer");
}
if (quantity == 0) {
continue;
}
Inventory inventory = inventoryDao
.readInventoryForUpdateForDefaultFulfillmentLocation(sku);
if (inventory != null) {
inventory.setQuantityOnHand(inventory.getQuantityOnHand()
+ quantity);
inventoryDao.save(inventory);
} else {
throw new IllegalStateException(
"There was a call to InventoryServiceImpl.incrementInventoryOnHand for a default fulfillment location, but no default "
+ "inventory for the sku: "
+ sku.getId()
+ " could be found!");
}
}
}
@Override
@Transactional(value = "blTransactionManager")
public Inventory readInventory(Sku sku,
FulfillmentLocation fulfillmentLocation) {
return inventoryDao.readInventory(sku, fulfillmentLocation);
}
@Override
@Transactional(value = "blTransactionManager")
public Inventory readInventory(Sku sku) {
return inventoryDao.readInventoryForDefaultFulfillmentLocation(sku);
}
@Override
public List<Inventory> listInventories(Sku sku) {
return inventoryDao.listInventories(sku);
}
@Override
public List<Inventory> listAllInventories(Product product,
List<FulfillmentLocation> fulfillmentLocations) {
return inventoryDao.listAllInventories(product, fulfillmentLocations);
}
@Override
public Map<Product, List<Inventory>> listAllInventories(
List<Product> products, List<FulfillmentLocation> locations) {
if (products == null || products.isEmpty() || locations == null
|| locations.isEmpty())
return Collections.emptyMap();
List<Inventory> inventories = inventoryDao.listAllInventories(products,
locations);
Map<Product, List<Inventory>> ret = new HashMap<Product, List<Inventory>>(
inventories.size());
for (Inventory i : inventories) {
Product p = i.getSku().getProduct();
List<Inventory> list = ret.get(p);
if (list == null)
ret.put(p, list = new ArrayList<Inventory>());
list.add(i);
}
return ret;
}
@Override
@Transactional(value = "blTransactionManager")
public List<Inventory> readInventoryForFulfillmentLocation(
FulfillmentLocation fulfillmentLocation) {
return inventoryDao
.readInventoryForFulfillmentLocation(fulfillmentLocation);
}
@Override
@Transactional(value = "blTransactionManager")
public Inventory save(Inventory inventory)
throws ConcurrentInventoryModificationException {
return inventoryDao.save(inventory);
}
@Override
@Transactional(value = "blTransactionManager")
public List<Sku> readSkusNotAtFulfillmentLocation(
FulfillmentLocation fulfillmentLocation) {
return inventoryDao
.readSkusNotAtFulfillmentLocation(fulfillmentLocation);
}
@Override
public Boolean isAllQuantityAvailable(Sku sku, Integer quantity) {
// if the sku does not exist or is not active, there is no quantity
// available
if (!sku.isActive()) {
return false;
}
if (!isSkuEligibleForInventoryCheck(sku)) {
// This sku is not eligible for inventory checks, so assume it is
// available
return true;
}
// quantity must be greater than 0
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must be a positive integer");
}
Long couont = inventoryDao
.readInventoryForSkuAndQuantity(sku, quantity);
return (couont <= 0);
}
@Override
public List<Inventory> readInventoryForLessThanQuantity(Integer quantity,
Long fulfillmentLocationId) {
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must be a positive integer");
}
return inventoryDao.readInventoryForLessThanQuantity(quantity,
fulfillmentLocationId);
}
@Override
public boolean isInventory(Product product, FulfillmentLocation location) {
if (isSkuEligibleForInventoryCheck(product.getDefaultSku()))
return inventoryDao.countInventory(product, location) > 0;
return true;
}
@Override
public List<Sku> listAllSkus(Product product,
FulfillmentLocation fulfillmentLocation) {
if (isSkuEligibleForInventoryCheck(product.getDefaultSku()))
return inventoryDao.listAllSkus(product, fulfillmentLocation);
List<Sku> skus = product.getSkus();
List<Sku> ret = new ArrayList<Sku>(skus.size() + 1);
ret.add(product.getDefaultSku());
ret.addAll(skus);
return ret;
}
@Override
public Boolean skuIsInFulfillmentLocation(Sku sku,
FulfillmentLocation fulfillmentLocation) {
if (!sku.isActive()) {
return false;
}
if (!isSkuEligibleForInventoryCheck(sku)) {
// This sku is not eligible for inventory checks, so assume it is
// available
return true;
}
return inventoryDao
.skuIsInFulfillmentLocation(sku, fulfillmentLocation);
}
@Override
public List<Product> filterProducts(final List<Product> products,
List<FulfillmentLocation> locs) {
if (products == null || products.isEmpty() || locs == null
|| locs.isEmpty())
return products;
// FIXME isSkuEligibleForInventoryCheck for each product
List<Product> prods = inventoryDao.filterProducts(products, locs);
Collections.sort(prods, new Comparator<Product>() {
@Override
public int compare(Product o1, Product o2) {
return products.indexOf(o1) - products.indexOf(o2);
}
});
return prods;
}
@Override
public List<Sku> filterSkus(final List<Sku> skus, FulfillmentLocation loc) {
if (skus == null || skus.isEmpty())
return skus;
List<Sku> ss = inventoryDao.filterSkus(skus, loc);
Collections.sort(ss, new Comparator<Sku>() {
@Override
public int compare(Sku o1, Sku o2) {
return skus.indexOf(o1) - skus.indexOf(o2);
}
});
return ss;
}
@Override
public void incrementInventory(Inventory inventory, Integer quantity)
throws ConcurrentInventoryModificationException {
// quantity must not be null
if (quantity == null || quantity < 0) {
throw new IllegalArgumentException(
"Quantity must not be a positive integer");
}
Inventory inventorys = inventoryDao.readById(inventory.getId());
if (inventorys != null) {
inventorys.setQuantityOnHand(inventorys.getQuantityOnHand()
+ quantity);
inventorys.setQuantityAvailable(inventorys.getQuantityAvailable()
+ quantity);
inventoryDao.save(inventory);
}
}
@Override
public List<Inventory> findProductsByPriceAndCurrency(BigDecimal amount,
BroadleafCurrency currency,
List<FulfillmentLocation> fulfillmentLocations, int start, int size) {
if (amount.intValue() != amount.floatValue()) {
return inventoryDao.readSkuByPriceAndCurrency(amount, currency,
fulfillmentLocations, start, size);
} else {
return Collections.emptyList();
}
}
}