com.buffalokiwi.aerodrome.jet.products.JetAPIProduct.java Source code

Java tutorial

Introduction

Here is the source code for com.buffalokiwi.aerodrome.jet.products.JetAPIProduct.java

Source

/**
 * This file is part of the Aerodrome package, and is subject to the 
 * terms and conditions defined in file 'LICENSE', which is part 
 * of this source code package.
 *
 * Copyright (c) 2016 All Rights Reserved, John T. Quinn III,
 * <johnquinn3@gmail.com>
 *
 * THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
 * PARTICULAR PURPOSE.
 */

package com.buffalokiwi.aerodrome.jet.products;

import com.buffalokiwi.aerodrome.jet.reports.SkuSalesDataRec;
import com.buffalokiwi.aerodrome.jet.JetAPI;
import com.buffalokiwi.api.APIException;
import com.buffalokiwi.api.APILog;
import com.buffalokiwi.api.IAPIHttpClient;
import com.buffalokiwi.aerodrome.jet.IJetAPIResponse;
import com.buffalokiwi.aerodrome.jet.JetConfig;
import com.buffalokiwi.aerodrome.jet.JetException;
import com.buffalokiwi.aerodrome.jet.Utils;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObjectBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Handles working with the Jet Products API
 *
 * @author John Quinn
 */
public class JetAPIProduct extends JetAPI implements IJetAPIProduct {
    /**
     * The log 
     */
    private static final Log LOG = LogFactory.getLog(JetAPIProduct.class);

    /**
     * Create a new JetProduct instance
     * @param client The http client 
     * @param conf Configuration 
     */
    public JetAPIProduct(final IAPIHttpClient client, final JetConfig conf) {
        super(client, conf);
    }

    @Override
    public boolean addProductSku(final ProductRec product) throws APIException, JetException, ValidateException {
        product.validate();
        sendPutProductSku(product);
        return true;
    }

    /**
     * Add a product to the Jet catalog
     * @param product Product to add
     * @return Success
     * @throws JetException if there is an error from the jet api
     * @throws APIException if there is some sort of error with the api 
     * library itself. A network issue, etc.
     * @throws ValidateException if the product fails pre-submit validation
     */
    @Override
    public boolean addProduct(final ProductRec product) throws APIException, JetException, ValidateException {
        product.validate();

        //..Add Sku    
        sendPutProductSku(product);

        //..Add an image
        // sendPutProductImage( product );

        //..Add the price
        sendPutProductPrice(product);

        //..Add some inventory
        sendPutProductInventory(product);

        sendPutProductShippingExceptions(product.getMerchantSku(), product.getShippingExceptionNodes());

        sendPutReturnsException(product.getMerchantSku(), product.getAllReturnLocationIds());

        //..pointless.
        return true;
    }

    /**
     * Add/update a product within the jet catalog.
     * Using original, modified will only send data to the appropriate endpoints.
     * This can be used to help reduce the chances of having a product be sent
     * back into the "under review" state.
     * @param original Original and unmodified product
     * @param modified Product modifications being sent 
     * @return Success
     * @throws JetException if there is an error from the jet api
     * @throws APIException if there is some sort of error with the api 
     * library itself. A network issue, etc.
     * @throws ValidateException if the product fails pre-submit validation
     */
    @Override
    public boolean addProduct(final ProductRec original, final ProductRec modified)
            throws APIException, JetException, ValidateException {
        if (original == null || modified == null)
            throw new IllegalArgumentException("original and modified must not be null");

        modified.validate();

        //..Diff the records
        final ProductDiff comp = new ProductDiff(original, modified);

        if (comp.shouldUpdateAll() || comp.shouldUpdateSku())
            return addProduct(modified);

        if (comp.shouldUpdatePrice()) {
            //..Add the price
            sendPutProductPrice(modified);
        }

        if (comp.shouldUpdateInventory()) {
            //..Add some inventory
            sendPutProductInventory(modified);
        }

        if (comp.shouldUpdateShippingExceptions()) {
            sendPutProductShippingExceptions(modified.getMerchantSku(), modified.getShippingExceptionNodes());
        }

        if (comp.shouldUpdateReturnsExceptions()) {
            sendPutReturnsException(modified.getMerchantSku(), modified.getAllReturnLocationIds());
        }

        if (comp.shouldUpdateVariations()) {
            sendPutProductVariation(modified.getVariations());
        }

        return false;
    }

    /**
     * Adds a product sku.
     * Part of a multi-part operation.
     * This will call merchant-skus/{sku-id}
     *
     * @param product product data
     * @return success
     * @throws APIException
     * @throws JetException
     */
    @Override
    public IJetAPIResponse sendPutProductSku(final ProductRec product) throws APIException, JetException {
        APILog.info(LOG, "Sending ", product.getMerchantSku());
        final IJetAPIResponse response = put(config.getAddProductURL(product.getMerchantSku()),
                product.toJSON().toString(), getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * Adds image url's
     * @param product product data
     * @return success
     * @throws APIException
     * @throws JetException
     * @deprecated Removed from Jet 
     */
    @Override
    public IJetAPIResponse sendPutProductImage(final ProductRec product) throws APIException, JetException {
        APILog.info(LOG, "Sending", product.getMerchantSku(), "image");

        final IJetAPIResponse response = put(config.getAddProductImageUrl(product.getMerchantSku()),
                product.toImageJson().toString(), getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * Adds product price data
     * @param product
     * @return
     * @throws APIException
     * @throws JetException
     */
    @Override
    public IJetAPIResponse sendPutProductPrice(final ProductRec product) throws APIException, JetException {
        APILog.info(LOG, "Sending", product.getMerchantSku(), "price");

        final IJetAPIResponse response = put(config.getAddProductPriceUrl(product.getMerchantSku()),
                product.toPriceJson().toString(), getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * Send product price data
     * @param sku merchant sku
     * @param price price data
     * @return response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendPutProductPrice(final String sku, final ProductPriceRec price)
            throws APIException, JetException {
        if (sku == null || sku.isEmpty())
            throw new IllegalArgumentException("sku can't be null or empty");

        APILog.info(LOG, "Sending", sku, "price");

        final IJetAPIResponse response = put(config.getAddProductPriceUrl(sku), price.toJSON().toString(),
                getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * Adds product quantity and inventory data
     * @param product product data
     * @return success
     * @throws APIException
     * @throws JetException
     */
    @Override
    public IJetAPIResponse sendPutProductInventory(final ProductRec product) throws JetException, APIException {
        return sendPutProductInventory(product.getMerchantSku(), product.getfNodeInventory());
    }

    /**
     * Adds product quantity and inventory data
     * @param product product data
     * @return success
     * @throws APIException
     * @throws JetException
     */
    @Override
    public IJetAPIResponse sendPutProductInventory(final String sku, final List<FNodeInventoryRec> nodes)
            throws JetException, APIException {
        Utils.checkNull(sku, "sku");
        Utils.checkNull(nodes, "nodes");

        APILog.info(LOG, "Sending", sku, "inventory");

        final JsonObjectBuilder o = Json.createObjectBuilder();

        if (!nodes.isEmpty()) {
            final JsonArrayBuilder a = Json.createArrayBuilder();
            nodes.forEach(v -> a.add(v.toJSON()));
            o.add("fulfillment_nodes", a.build());
        }

        final IJetAPIResponse response = patch(config.getAddProductInventoryUrl(sku), o.build().toString(),
                getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * The variation request is used to create a variation-type relationship 
     * between several SKUs. To use this request, one must have already uploaded 
     * all the SKUs in question ; they should then choose one "parent" SKU and 
     * make the variation request to that SKU, adding as "children" any SKUs they 
     * want considered part of the relationship.
     * To denote the particular variation refinements, one must have uploaded one 
     * or more attributes in the product call for all the SKUs in question; 
     * finally, they are expected to list these attributes in the variation 
     * request.
     * 
     * @param group data to send 
     * @return response from jet 
     * @throws APIException if there's a problem 
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendPutProductVariation(final ProductVariationGroupRec group)
            throws APIException, JetException

    {
        if (group == null)
            throw new IllegalArgumentException("group cannot be null");

        APILog.info(LOG, "Sending", group.getParentSku(), "variations");

        final IJetAPIResponse response = put(config.getAddProductVariationUrl(group.getParentSku()),
                group.toJSON().toString(), getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * Send shipping exceptions to jet 
     * @param sku Sku 
     * @param nodes Filfillment nodes 
     * @return
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendPutProductShippingExceptions(final String sku, final List<FNodeShippingRec> nodes)
            throws APIException, JetException {
        checkSku(sku);

        if (nodes == null)
            throw new IllegalArgumentException("nodes cannot be null");

        APILog.info(LOG, "Sending", sku, "shipping exceptions");

        final JsonArrayBuilder b = Json.createArrayBuilder();
        for (final FNodeShippingRec node : nodes) {
            b.add(node.toJSON());
        }

        final JsonObjectBuilder o = Json.createObjectBuilder();
        o.add("fulfillment_nodes", b);

        final IJetAPIResponse response = put(config.getAddProductShipExceptionUrl(sku), o.build().toString(),
                getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * The returns exceptions call is used to set up specific methods that will 
     * overwrite your default settings on a fulfillment node level for returns. 
     * This exception will be used to determine how and to where a product is 
     * returned unless the merchant specifies otherwise in the Ship Order message. 
     * 
     * @param sku Product SKU to modify 
     * @param hashes A list of md5 hashes - Each hash is the ID of the returns 
     * node that was created on partner.jet.com under fulfillment settings.
     * 
     * Must be a valid return node ID set up by the merchant
     * 
     * @return response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendPutReturnsException(final String sku, final List<String> hashes)
            throws APIException, JetException {
        checkSku(sku);

        if (hashes == null)
            throw new IllegalArgumentException("hashes cannot be null");

        final JsonArrayBuilder b = Json.createArrayBuilder();
        for (final String s : hashes) {
            b.add(s);
        }

        APILog.info(LOG, "Sending", sku, "returns exceptions");

        final IJetAPIResponse res = put(config.getProductReturnsExceptionUrl(sku),
                Json.createObjectBuilder().add("return_location_ids", b.build()).build().toString(),
                getJSONHeaderBuilder().build());

        return res;

    }

    /**
     * Archive a product sku.
     * 
     * Archiving a SKU allows the retailer to "deactivate" a SKU from the catalog. 
     * At any point in time, a retailer may decide to "reactivate" the SKU
     * @param sku
     * @param isArchived Indicates whether the specified SKU is archived.
      'true' - SKU is inactive
      'false' - SKU is potentially sellable
     * @return
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendPutArchiveSku(final String sku, final boolean isArchived)
            throws APIException, JetException {
        checkSku(sku);

        APILog.info(LOG, "Sending archive sku:", sku);

        final IJetAPIResponse response = put(config.getArchiveSkuURL(sku),
                Json.createObjectBuilder().add("is_archived", isArchived).build().toString(),
                getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * At Jet, the price the retailer sets is not the same as the price the 
     * customer pays. The price set for a SKU will be the price the retailer 
     * gets paid for selling the products. However, the price that is set will 
     * influence how competitive your product offer matches up compared to other 
     * product offers for the same SKU.
     * 
     * @param sku Product sku 
     * @return API response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendGetProductPrice(final String sku) throws APIException, JetException {
        checkSku(sku);

        APILog.info(LOG, "Sending GET product price for sku:", sku);

        final IJetAPIResponse response = get(config.getGetProductPriceURL(sku), getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * At Jet, the price the retailer sets is not the same as the price the 
     * customer pays. The price set for a SKU will be the price the retailer 
     * gets paid for selling the products. However, the price that is set will 
     * influence how competitive your product offer matches up compared to other 
     * product offers for the same SKU.
     * 
     * @param sku Product sku 
     * @return API response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public ProductPriceRec getProductPrice(final String sku) throws APIException, JetException {
        return ProductPriceRec.fromJSON(sendGetProductPrice(sku).getJsonObject());
    }

    /**
     * Retrieve a single product by sku.
     * Any information about the SKU that was previously uploaded (price, 
     * inventory, shipping exception) will show up here
     * @param sku Product Sku
     * @return response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendGetProductSku(final String sku) throws APIException, JetException {
        checkSku(sku);

        APILog.info(LOG, "Retrieving ", sku);

        return get(config.getGetProductURL(sku), getPlainHeaderBuilder().build());
    }

    /**
     * Retrieve product data
     * @param sku Sku to retrieve
     * @return jet product data
     * @throws APIException
     * @throws JetException
     */
    @Override
    public ProductRec getProduct(final String sku) throws APIException, JetException {
        return ProductRec.fromJSON(sendGetProductSku(sku).getJsonObject());
    }

    /**
     * Retrieve product data, pricing, variations, returns exceptions and 
     * shipping exceptions 
     * @param sku product sku 
     * @return Product data 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public ProductRec getFullProduct(final String sku) throws APIException, JetException {
        final ProductRec.Builder b = getProduct(sku).toBuilder();
        try {
            ProductPriceRec p = getProductPrice(sku);
            b.setfNodePrices(p.getFulfillmentNodes());
        } catch (Exception e) {
            APILog.error(LOG, e, "Failed to retrieve product prices for", sku);
        }

        try {
            ProductInventoryRec i = getProductInventory(sku);
            b.setfNodeInventory(i.getNodes());
        } catch (Exception e) {
            APILog.error(LOG, e, "Failed to retrieve product inventory for", sku);
        }

        b.setVariations(getProductVariations(sku));
        b.getReturnsExceptions().add(getReturnsExceptions(sku));
        b.setShippingExceptionNodes(getShippingExceptions(sku));

        return b.build();
    }

    /**
     * Retrieve product inventory by sku.
     * The inventory returned from this endpoint represents the number in the 
     * feed, not the quantity that is currently sellable on Jet.com
     * 
     * @param sku Product sku
     * @return api response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendGetProductInventory(final String sku) throws APIException, JetException {
        checkSku(sku);

        APILog.info(LOG, "Sending GET product inventory for sku:", sku);

        final IJetAPIResponse response = get(config.getGetProductInventoryURL(sku), getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * Retrieve product inventory by sku.
     * The inventory returned from this endpoint represents the number in the 
     * feed, not the quantity that is currently sellable on Jet.com
     * 
     * @param sku Product sku
     * @return api response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public ProductInventoryRec getProductInventory(final String sku) throws APIException, JetException {
        try {
            return ProductInventoryRec.fromJSON(sendGetProductInventory(sku).getJsonObject());
        } catch (ParseException e) {
            APILog.error(LOG, "Failed to parse Jet Fulfillment Node lastUpdate Date:", e.getMessage());
            throw new JetException("getProductPrice result was successful, but "
                    + "Fulfillment node had an invalid lastUpdate date", e);
        }
    }

    /**
     * Retrieve product shipping exceptions by sku.
     * The shipping exceptions call is used to set up specific methods and costs 
     * for individual SKUs that will override your default settings, with the 
     * ability to drill down to the fulfillment node level.
     * 
     * @param sku Product sku 
     * @return api response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendGetProductShippingExceptions(final String sku) throws APIException, JetException {
        checkSku(sku);

        APILog.info(LOG, "Sending GET product shipping exceptions for sku:", sku);

        final IJetAPIResponse response = get(config.getGetShippingExceptionURL(sku),
                getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * Retrieve product variations exceptions by sku.
     * 
     * @param sku Product sku 
     * @return api response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendGetProductVariations(final String sku) throws APIException, JetException {
        checkSku(sku);

        APILog.info(LOG, "Sending GET product variations for sku:", sku);

        final IJetAPIResponse response = get(config.getGetProductVariationURL(sku), getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * Retrieve product variations exceptions by sku.
     * 
     * @param sku Product sku 
     * @return api response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public ProductVariationGroupRec getProductVariations(final String sku) throws APIException, JetException {
        checkSku(sku);

        try {
            return ProductVariationGroupRec.fromJSON(sku, sendGetProductVariations(sku).getJsonObject());
        } catch (ClassCastException e) {
            APILog.error(LOG, "Failed to convert variation_refinements or children_skus to a List");
            throw new JetException(e.getMessage(), e);
        }
    }

    /**
     * Retrieve a set of product shipping exceptions.
     * @param sku Sku
     * @return exceptions 
     * @throws APIException 
     * @throws JetException
     */
    @Override
    public List<FNodeShippingRec> getShippingExceptions(final String sku) throws APIException, JetException {
        checkSku(sku);

        final JsonArray nodes = sendGetProductShippingExceptions(sku).getJsonObject()
                .getJsonArray("fulfillment_nodes");

        final List<FNodeShippingRec> out = new ArrayList<>();

        if (nodes == null)
            return out;

        for (int i = 0; i < nodes.size(); i++) {
            out.add(FNodeShippingRec.fromJSON(nodes.getJsonObject(i)));
        }

        return out;

    }

    /**
     * Retrieve product returns exceptions by sku.
     * 
     * @param sku Product sku 
     * @return api response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendGetProductReturnsExceptions(final String sku) throws APIException, JetException {
        checkSku(sku);

        APILog.info(LOG, "Sending GET product returns exceptions for sku:", sku);

        final IJetAPIResponse response = get(config.getGetReturnsExceptionURL(sku), getJSONHeaderBuilder().build());

        return response;
    }

    /**
     * Retrieve product returns exceptions by sku.
     * 
     * @param sku Product sku 
     * @return api response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public ReturnsExceptionRec getReturnsExceptions(final String sku) throws APIException, JetException {
        checkSku(sku);

        return ReturnsExceptionRec.fromJSON(sendGetProductReturnsExceptions(sku).getJsonObject());
    }

    /**
     * This call allows you visibility into the total number of SKUs you have 
     * uploaded. Alternatively, the Partner Portal allows you to download a 
     * CSV file of all SKUs.
     * @param offset The first SKU # you wish to appear in the return
     * @param limit The last SKU # you wish to appear in the return
     * @return api response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendGetSkuList(final int offset, final int limit) throws APIException, JetException {
        if (offset < 0)
            throw new IllegalArgumentException("offset cannot be less than zero");
        else if (limit < 1)
            throw new IllegalArgumentException("limit cannot be less than one");

        APILog.info(LOG, "Sending GET sku list at (", String.valueOf(offset), ".", String.valueOf(limit), ")");

        return get(config.getSkuListURL(offset, limit), getJSONHeaderBuilder().build());
    }

    /**
     * This call allows you visibility into the total number of SKUs you have 
     * uploaded. Alternatively, the Partner Portal allows you to download a 
     * CSV file of all SKUs.
     * @param offset The first SKU # you wish to appear in the return
     * @param limit The last SKU # you wish to appear in the return
     * @return api response 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public List<String> getSkuList(final int offset, final int limit) throws APIException, JetException {
        //..This shouldn't be able to throw a NullPointerException, need to write tests.....
        return jsonArrayToTokenList(sendGetSkuList(offset, limit).getJsonObject().getJsonArray("sku_urls"), false);
    }

    /**
     * Get sales data.
     *  
     * Analyze how your individual product price (item and shipping price) compares 
     * to the lowest individual product prices from the marketplace. These prices 
     * are only provided for SKUs that have the status Available for Sale?. If a 
     * best price does not change, then the last_update time also will not change. 
     * If your inventory is zero, then these prices will not continue to be updated 
     * and will be stale. Note: It may take up to 24 hours to reflect any price 
     * updates from you and the marketplace.
     * 
     * Product pricing is one factor that Jet uses to determine which retailer wins 
     * a basket order. Jet determines what orders retailers will win based on the 
     * the product prices of all products in the order, base commission on those 
     * items as well as commission adjustments set via the Rules Engine. Commission 
     * adjustments set via the Rules Engine can be very effective in optimizing 
     * your win rate and profitability at the order level without having to have 
     * the absolute lowest item and shipping prices.
     * @param sku Product sku 
     * @return data 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public IJetAPIResponse sendGetSkuSalesData(final String sku) throws APIException, JetException {
        checkSku(sku);

        return get(config.getSalesDataBySkuURL(sku), getJSONHeaderBuilder().build());
    }

    /**
     * Get sales data.
     *  
     * Analyze how your individual product price (item and shipping price) compares 
     * to the lowest individual product prices from the marketplace. These prices 
     * are only provided for SKUs that have the status Available for Sale?. If a 
     * best price does not change, then the last_update time also will not change. 
     * If your inventory is zero, then these prices will not continue to be updated 
     * and will be stale. Note: It may take up to 24 hours to reflect any price 
     * updates from you and the marketplace.
     * 
     * Product pricing is one factor that Jet uses to determine which retailer wins 
     * a basket order. Jet determines what orders retailers will win based on the 
     * the product prices of all products in the order, base commission on those 
     * items as well as commission adjustments set via the Rules Engine. Commission 
     * adjustments set via the Rules Engine can be very effective in optimizing 
     * your win rate and profitability at the order level without having to have 
     * the absolute lowest item and shipping prices.
     * @param sku Product sku 
     * @return data 
     * @throws APIException
     * @throws JetException 
     */
    @Override
    public SkuSalesDataRec getSkuSalesData(final String sku) throws APIException, JetException {
        return SkuSalesDataRec.fromJSON(sku, sendGetSkuSalesData(sku).getJsonObject());
    }

    /**
     * Archive a product sku.
     *
     * Archiving a SKU allows the retailer to "deactivate" a SKU from the catalog.
     * At any point in time, a retailer may decide to "reactivate" the SKU
     * @param sku
     * @param isArchived Indicates whether the specified SKU is archived.
    'true' - SKU is inactive
    'false' - SKU is potentially sellable
     * @return
     * @throws APIException
     * @throws JetException
     */
    @Override
    public boolean archiveSku(final String sku, final boolean isArchived) throws APIException, JetException {
        return sendPutArchiveSku(sku, isArchived).isSuccess();
    }

    /**
     * Adds image url's
     * @param product product data
     * @return success
     * @throws APIException
     * @throws JetException
     * @deprecated Removed from Jet 
     */
    @Override
    public boolean setProductImages(final ProductRec product) throws APIException, JetException {
        return sendPutProductImage(product).isSuccess();
    }

    /**
     * Adds product quantity and inventory data
     * @param product product data
     * @return success
     * @throws APIException
     * @throws JetException
     */
    @Override
    public boolean setProductInventory(final ProductRec product) throws APIException, JetException {
        return sendPutProductInventory(product).isSuccess();
    }

    /**
     * Adds product price data
     * @param product
     * @return
     * @throws APIException
     * @throws JetException
     */
    @Override
    public boolean setProductPrice(final ProductRec product) throws APIException, JetException {
        return sendPutProductPrice(product).isSuccess();
    }

    /**
     * Send shipping exceptions to jet
     * @param sku Sku
     * @param nodes Filfillment nodes
     * @return
     * @throws APIException
     * @throws JetException
     */
    @Override
    public boolean setProductShippingExceptions(final String sku, final List<FNodeShippingRec> nodes)
            throws APIException, JetException {
        return sendPutProductShippingExceptions(sku, nodes).isSuccess();
    }

    /**
     * Adds a product sku.
     * Part of a multi-part operation.
     * This will call merchant-skus/{sku-id}
     *
     * @param product product data
     * @return success
     * @throws APIException
     * @throws JetException
     */
    @Override
    public boolean setProductSku(final ProductRec product) throws APIException, JetException {
        return sendPutProductSku(product).isSuccess();
    }

    /**
     * The variation request is used to create a variation-type relationship
     * between several SKUs. To use this request, one must have already uploaded
     * all the SKUs in question ; they should then choose one "parent" SKU and
     * make the variation request to that SKU, adding as "children" any SKUs they
     * want considered part of the relationship.
     * To denote the particular variation refinements, one must have uploaded one
     * or more attributes in the product call for all the SKUs in question;
     * finally, they are expected to list these attributes in the variation
     * request.
     *
     * @param group data to send
     * @return response from jet
     * @throws APIException if there's a problem
     * @throws JetException
     */
    @Override
    public boolean setProductVariations(final ProductVariationGroupRec group) throws APIException, JetException {
        return sendPutProductVariation(group).isSuccess();
    }

    /**
     * The returns exceptions call is used to set up specific methods that will
     * overwrite your default settings on a fulfillment node level for returns.
     * This exception will be used to determine how and to where a product is
     * returned unless the merchant specifies otherwise in the Ship Order message.
     *
     * @param sku Product SKU to modify
     * @param hashes A list of md5 hashes - Each hash is the ID of the returns
     * node that was created on partner.jet.com under fulfillment settings.
     *
     * Must be a valid return node ID set up by the merchant
     *
     * @return response
     * @throws APIException
     * @throws JetException
     */
    @Override
    public boolean setReturnsException(final String sku, final List<String> hashes)
            throws APIException, JetException {
        return sendPutReturnsException(sku, hashes).isSuccess();
    }

    /**
     * Simply checks sku for null/empty.
     * If true, then throw an exception
     * @param sku Product sku
     * @throws IllegalArgumentException if sku is null/empty 
     */
    private void checkSku(final String sku) throws IllegalArgumentException {
        if (sku == null || sku.isEmpty())
            throw new IllegalArgumentException("sku cannot be null or empty");
    }
}