<commit_before>/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.ofbiz.shipment.shipment;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.UtilGenerics;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilNumber;
import org.ofbiz.base.util.UtilProperties;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.common.geo.GeoWorker;
import org.ofbiz.entity.Delegator;
import org.ofbiz.entity.GenericEntityException;
import org.ofbiz.entity.GenericValue;
import org.ofbiz.entity.condition.EntityCondition;
import org.ofbiz.entity.condition.EntityOperator;
import org.ofbiz.entity.util.EntityListIterator;
import org.ofbiz.entity.util.EntityQuery;
import org.ofbiz.entity.util.EntityUtil;
import org.ofbiz.party.party.PartyWorker;
import org.ofbiz.product.store.ProductStoreWorker;
import org.ofbiz.service.DispatchContext;
import org.ofbiz.service.GenericServiceException;
import org.ofbiz.service.LocalDispatcher;
import org.ofbiz.service.ModelService;
import org.ofbiz.service.ServiceUtil;

/**
 * ShipmentServices
 */
public class ShipmentServices {

    private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());

    public static final String resource = "ProductUiLabels";
    public static final String resource_error = "OrderErrorUiLabels";
    public static final int decimals = UtilNumber.getBigDecimalScale("order.decimals");
    public static final RoundingMode rounding = UtilNumber.getRoundingMode("order.rounding");
    public static final BigDecimal ZERO = BigDecimal.ZERO.setScale(decimals, rounding);

    public static Map<String, Object> createShipmentEstimate(DispatchContext dctx, Map<String, ? extends Object> context) {
        Map<String, Object> result = new HashMap<String, Object>();
        Delegator delegator = dctx.getDelegator();
        Locale locale = (Locale) context.get("locale");
        List<GenericValue> storeAll = new LinkedList<GenericValue>();
        String productStoreShipMethId = (String)context.get("productStoreShipMethId");

        GenericValue productStoreShipMeth = null;
        try {
            productStoreShipMeth = EntityQuery.use(delegator).from("ProductStoreShipmentMeth").where("productStoreShipMethId", productStoreShipMethId).queryOne();
        } catch (GenericEntityException e) {
            return ServiceUtil.returnError(UtilProperties.getMessage(resource,
                    "ProductStoreShipmentMethodCannotRetrieve",
                    UtilMisc.toMap("productStoreShipMethId", productStoreShipMethId,
                            "errorString", e.toString()), locale));
        }

        // Create the basic entity.
        GenericValue estimate = delegator.makeValue("ShipmentCostEstimate");

        estimate.set("shipmentCostEstimateId", delegator.getNextSeqId("ShipmentCostEstimate"));
        estimate.set("productStoreShipMethId", productStoreShipMethId);
        estimate.set("shipmentMethodTypeId", productStoreShipMeth.getString("shipmentMethodTypeId"));
        estimate.set("carrierPartyId", productStoreShipMeth.getString("partyId"));
        estimate.set("carrierRoleTypeId", "CARRIER");
        estimate.set("productStoreId", productStoreShipMeth.getString("productStoreId"));
        estimate.set("geoIdTo", context.get("toGeo"));
        estimate.set("geoIdFrom", context.get("fromGeo"));
        estimate.set("partyId", context.get("partyId"));
        estimate.set("roleTypeId", context.get("roleTypeId"));
        estimate.set("orderPricePercent", context.get("flatPercent"));
        estimate.set("orderFlatPrice", context.get("flatPrice"));
        estimate.set("orderItemFlatPrice", context.get("flatItemPrice"));
        estimate.set("shippingPricePercent", context.get("shippingPricePercent"));
        estimate.set("productFeatureGroupId", context.get("productFeatureGroupId"));
        estimate.set("oversizeUnit", context.get("oversizeUnit"));
        estimate.set("oversizePrice", context.get("oversizePrice"));
        estimate.set("featurePercent", context.get("featurePercent"));
        estimate.set("featurePrice", context.get("featurePrice"));
        estimate.set("weightBreakId", context.get("weightBreakId"));
        estimate.set("weightUnitPrice", context.get("wprice"));
        estimate.set("weightUomId", context.get("wuom"));
        estimate.set("quantityBreakId", context.get("quantityBreakId"));
        estimate.set("quantityUnitPrice", context.get("qprice"));
        estimate.set("quantityUomId", context.get("quom"));
        estimate.set("priceBreakId", context.get("priceBreakId"));
        estimate.set("priceUnitPrice", context.get("pprice"));
        estimate.set("priceUomId", context.get("puom"));
        storeAll.add(estimate);

        if (!applyQuantityBreak(context, result, storeAll, delegator, estimate, "w", "weight", "Weight")) {
            return result;
        }

        if (!applyQuantityBreak(context, result, storeAll, delegator, estimate, "q", "quantity", "Quantity")) {
            return result;
        }

        if (!applyQuantityBreak(context, result, storeAll, delegator, estimate, "p", "price", "Price")) {
            return result;
        }

        try {
            delegator.storeAll(storeAll);
        } catch (GenericEntityException e) {
            result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_ERROR);
            result.put(ModelService.ERROR_MESSAGE, "Problem reading product features: " + e.toString());
            return result;
        }

        result.put("shipmentCostEstimateId", estimate.get("shipmentCostEstimateId"));
        result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_SUCCESS);
        return result;
    }

    public static Map<String, Object> removeShipmentEstimate(DispatchContext dctx, Map<String, ? extends Object> context) {
        Delegator delegator = dctx.getDelegator();
        String shipmentCostEstimateId = (String) context.get("shipmentCostEstimateId");
        Locale locale = (Locale) context.get("locale");

        GenericValue estimate = null;

        try {
            estimate = EntityQuery.use(delegator).from("ShipmentCostEstimate").where("shipmentCostEstimateId", shipmentCostEstimateId).queryOne();
            estimate.remove();
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(UtilProperties.getMessage(resource,
                    "ProductShipmentCostEstimateRemoveError",
                    UtilMisc.toMap("errorString", e.toString()), locale));
        }
        return ServiceUtil.returnSuccess();
    }

    private static boolean applyQuantityBreak(Map<String, ? extends Object> context, Map<String, Object> result, List<GenericValue> storeAll, Delegator delegator,
                                              GenericValue estimate, String prefix, String breakType, String breakTypeString) {
        BigDecimal min = (BigDecimal) context.get(prefix + "min");
        BigDecimal max = (BigDecimal) context.get(prefix + "max");
        if (min != null || max != null) {
            if (min != null && max != null) {
                if (min.compareTo(max) <= 0 || max.compareTo(BigDecimal.ZERO) == 0) {
                    try {
                        String newSeqId = delegator.getNextSeqId("QuantityBreak");
                        GenericValue weightBreak = delegator.makeValue("QuantityBreak");
                        weightBreak.set("quantityBreakId", newSeqId);
                        weightBreak.set("quantityBreakTypeId", "SHIP_" + breakType.toUpperCase(Locale.getDefault()));
                        weightBreak.set("fromQuantity", min);
                        weightBreak.set("thruQuantity", max);
                        estimate.set(breakType + "BreakId", newSeqId);
                        estimate.set(breakType + "UnitPrice", context.get(prefix + "price"));
                        if (context.containsKey(prefix + "uom")) {
                            estimate.set(breakType + "UomId", context.get(prefix + "uom"));
                        }
                        storeAll.add(0, weightBreak);
                    } catch (Exception e) {
                        Debug.logError(e, module);
                    }
                }
                else {
                    result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_ERROR);
                    result.put(ModelService.ERROR_MESSAGE, "Max " + breakTypeString +
                            " must not be less than Min " + breakTypeString + ".");
                    return false;
                }
            }
            else {
                result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_ERROR);
                result.put(ModelService.ERROR_MESSAGE, breakTypeString+" Span Requires BOTH Fields.");
                return false;
            }
        }
        return true;
    }

    // ShippingEstimate Calc Service
    public static Map<String, Object> calcShipmentCostEstimate(DispatchContext dctx, Map<String, ? extends Object> context) {
        Delegator delegator = dctx.getDelegator();

        // prepare the data
        String productStoreShipMethId = (String) context.get("productStoreShipMethId");
        String productStoreId = (String) context.get("productStoreId");
        String carrierRoleTypeId = (String) context.get("carrierRoleTypeId");
        String carrierPartyId = (String) context.get("carrierPartyId");
        String shipmentMethodTypeId = (String) context.get("shipmentMethodTypeId");
        String shippingContactMechId = (String) context.get("shippingContactMechId");
        String shippingPostalCode = (String) context.get("shippingPostalCode");
        String shippingCountryCode = (String) context.get("shippingCountryCode");

        List<Map<String, Object>> shippableItemInfo = UtilGenerics.checkList(context.get("shippableItemInfo"));
        BigDecimal shippableTotal = (BigDecimal) context.get("shippableTotal");
        BigDecimal shippableQuantity = (BigDecimal) context.get("shippableQuantity");
        BigDecimal shippableWeight = (BigDecimal) context.get("shippableWeight");
        BigDecimal initialEstimateAmt = (BigDecimal) context.get("initialEstimateAmt");
        Locale locale = (Locale) context.get("locale");

        if (shippableTotal == null) {
            shippableTotal = BigDecimal.ZERO;
        }
        if (shippableQuantity == null) {
            shippableQuantity = BigDecimal.ZERO;
        }
        if (shippableWeight == null) {
            shippableWeight = BigDecimal.ZERO;
        }
        if (initialEstimateAmt == null) {
            initialEstimateAmt = BigDecimal.ZERO;
        }

        // get the ShipmentCostEstimate(s)
        Map<String, String> estFields = UtilMisc.toMap("productStoreId", productStoreId, "shipmentMethodTypeId", shipmentMethodTypeId,
                "carrierPartyId", carrierPartyId, "carrierRoleTypeId", carrierRoleTypeId);
        EntityCondition estFieldsCond = EntityCondition.makeCondition(estFields, EntityOperator.AND);

        if (UtilValidate.isNotEmpty(productStoreShipMethId)) {
            // if the productStoreShipMethId field is passed, then also get estimates that have the field set
            List<EntityCondition> condList = UtilMisc.toList(EntityCondition.makeCondition("productStoreShipMethId", EntityOperator.EQUALS, productStoreShipMethId), estFieldsCond);
            estFieldsCond = EntityCondition.makeCondition(condList, EntityOperator.AND);
        }

        Collection<GenericValue> estimates = null;
        try {
            estimates = EntityQuery.use(delegator).from("ShipmentCostEstimate")
                            .where(estFieldsCond)
                            .cache(true)
                            .queryList();
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(UtilProperties.getMessage(resource,
                    "ProductShipmentCostEstimateCannotRetrieve", locale));
        }
        if (estimates == null || estimates.size() < 1) {
            // SCIPIO: 2018-11-09: We must return null if no ShipmentCostEstimate records instead of ZERO so that
            // caller is aware we got nothing (not appropriate to return failure because this is normal and don't want in logs)
            // We will also only log warning if initialEstimateAmt was outright null (because shipping could theoretically be deemed free by other service and give 0$).
//            if (initialEstimateAmt.compareTo(BigDecimal.ZERO) == 0) {
//                Debug.logWarning("No shipping estimates found; the shipping amount returned is 0! Condition used was: " + estFieldsCond + "; Using the passed context: " + context, module);
//            }
//
//            Map<String, Object> respNow = ServiceUtil.returnSuccess();
//            respNow.put("shippingEstimateAmount", BigDecimal.ZERO);
//            return respNow;
            Map<String, Object> respNow = ServiceUtil.returnSuccess(UtilProperties.getMessage(resource, "ProductShipmentCostEstimateCannotFoundForCarrier",
                    UtilMisc.toMap("carrierPartyId", carrierPartyId, "shipmentMethodTypeId", shipmentMethodTypeId), locale));
            if (context.get("initialEstimateAmt") == null) {
                // SCIPIO: TODO: REVIEW: It is strange for this service to be responsible to print this warning, but no better alternative for now
                Debug.logWarning("No entity ShipmentCostEstimate shipping estimates found for condition: " + estFieldsCond + "; using context: " + context, module);
            } else {
                // If we got a initialEstimateAmt (from external services), it is normal for there to be no ShipmentCostEstimate records, so verbose this
                if (Debug.verboseOn()) {
                    Debug.logVerbose("No entity ShipmentCostEstimate shipping estimates found for condition: " + estFieldsCond + "; using context: " + context, module);
                }
            }
            respNow.put("shippingEstimateAmount", null);
            return respNow;
        }

        // Get the PostalAddress
        GenericValue shipAddress = null;
        if (shippingContactMechId != null) {
            try {
                shipAddress = EntityQuery.use(delegator).from("PostalAddress").where("contactMechId", shippingContactMechId).queryOne();
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
                return ServiceUtil.returnError(UtilProperties.getMessage(resource,
                        "ProductShipmentCostEstimateCannotGetShippingAddress", locale));
            }
        } else if (shippingPostalCode != null) {
            String countryGeoId = null;
            try {
                GenericValue countryGeo = EntityQuery.use(delegator).from("Geo")
                        .where("geoTypeId", "COUNTRY", "geoCode", shippingCountryCode)
                        .cache(true)
                        .queryFirst();
                if (countryGeo != null) {
                    countryGeoId = countryGeo.getString("geoId");
                }
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
            shipAddress = delegator.makeValue("PostalAddress");
            shipAddress.set("countryGeoId", countryGeoId);
            shipAddress.set("postalCodeGeoId", shippingPostalCode);
        }
        // Get the possible estimates.
        List<GenericValue> estimateList = new LinkedList<GenericValue>();

        for (GenericValue thisEstimate: estimates) {
            try {
                String toGeo = thisEstimate.getString("geoIdTo");
                if (UtilValidate.isNotEmpty(toGeo) && shipAddress ==null) {
                    // This estimate requires shipping address details. We don't have it so we cannot use this estimate.
                    continue;
                }
                List<GenericValue> toGeoList = GeoWorker.expandGeoGroup(toGeo, delegator);
                // Make sure we have a valid GEOID.
                if (UtilValidate.isEmpty(toGeoList) ||
                        GeoWorker.containsGeo(toGeoList, shipAddress.getString("countryGeoId"), delegator) ||
                        GeoWorker.containsGeo(toGeoList, shipAddress.getString("stateProvinceGeoId"), delegator) ||
                        GeoWorker.containsGeo(toGeoList, shipAddress.getString("postalCodeGeoId"), delegator)) {
                    GenericValue wv = thisEstimate.getRelatedOne("WeightQuantityBreak", false);
                    GenericValue qv = thisEstimate.getRelatedOne("QuantityQuantityBreak", false);
                    GenericValue pv = thisEstimate.getRelatedOne("PriceQuantityBreak", false);
                    if (wv == null && qv == null && pv == null) {
                        estimateList.add(thisEstimate);
                    } else {
                        // Do some testing.
                        boolean useWeight = false;
                        boolean weightValid = false;
                        boolean useQty = false;
                        boolean qtyValid = false;
                        boolean usePrice = false;
                        boolean priceValid = false;

                        if (wv != null) {
                            useWeight = true;
                            BigDecimal min = BigDecimal.ONE.movePointLeft(4);
                            BigDecimal max = BigDecimal.ONE.movePointLeft(4);
                            min = wv.getBigDecimal("fromQuantity");
                            max = wv.getBigDecimal("thruQuantity");
                            if (shippableWeight.compareTo(min) >= 0 && (max.compareTo(BigDecimal.ZERO) == 0 || shippableWeight.compareTo(max) <= 0)) {
                                weightValid = true;
                            }
                        }
                        if (qv != null) {
                            useQty = true;
                            BigDecimal min = BigDecimal.ONE.movePointLeft(4);
                            BigDecimal max = BigDecimal.ONE.movePointLeft(4);
                            min = qv.getBigDecimal("fromQuantity");
                            max = qv.getBigDecimal("thruQuantity");
                            if (shippableQuantity.compareTo(min) >= 0 && (max.compareTo(BigDecimal.ZERO) == 0 || shippableQuantity.compareTo(max) <= 0)) {
                                qtyValid = true;
                            }
                        }
                        if (pv != null) {
                            usePrice = true;
                            BigDecimal min = BigDecimal.ONE.movePointLeft(4);
                            BigDecimal max = BigDecimal.ONE.movePointLeft(4);
                            min = pv.getBigDecimal("fromQuantity");
                            max = pv.getBigDecimal("thruQuantity");
                            if (shippableTotal.compareTo(min) >= 0 && (max.compareTo(BigDecimal.ZERO) == 0 || shippableTotal.compareTo(max) <= 0)) {
                                priceValid = true;
                            }
                        }
                        // Now check the tests.
                        if ((useWeight && weightValid) || (useQty && qtyValid) || (usePrice && priceValid)) {
                            estimateList.add(thisEstimate);
                        }
                    }
                }
            } catch (GenericEntityException e) {
                Debug.logError(e, e.getLocalizedMessage(), module);
            }
        }

        if (estimateList.size() < 1) {
            return ServiceUtil.returnFailure(UtilProperties.getMessage(resource,
                    "ProductShipmentCostEstimateCannotFoundForCarrier",
                    UtilMisc.toMap("carrierPartyId", carrierPartyId,
                            "shipmentMethodTypeId", shipmentMethodTypeId), locale));
        }

        // make the shippable item size/feature objects
        List<BigDecimal> shippableItemSizes = new LinkedList<BigDecimal>();
        Map<String, BigDecimal> shippableFeatureMap = new HashMap<String, BigDecimal>();
        if (shippableItemInfo != null) {
            for (Map<String, Object> itemMap: shippableItemInfo) {
                // add the item sizes
                if (itemMap.containsKey("size")) {
                    BigDecimal itemSize = (BigDecimal) itemMap.get("size");
                    if (itemSize != null) {
                        shippableItemSizes.add(itemSize);
                    }
                }

                // add the feature quantities
                BigDecimal quantity = (BigDecimal) itemMap.get("quantity");
                if (itemMap.containsKey("featureSet")) {
                    Set<String> featureSet = UtilGenerics.checkSet(itemMap.get("featureSet"));
                    if (UtilValidate.isNotEmpty(featureSet)) {
                        for (String featureId: featureSet) {
                            BigDecimal featureQuantity = shippableFeatureMap.get(featureId);
                            if (featureQuantity == null) {
                                featureQuantity = BigDecimal.ZERO;
                            }
                            featureQuantity = featureQuantity.add(quantity);
                            shippableFeatureMap.put(featureId, featureQuantity);
                        }
                    }
                }

            }
        }

        // Calculate priority based on available data.
        double PRIORITY_PARTY = 9;
        double PRIORITY_ROLE = 8;
        double PRIORITY_GEO = 4;
        double PRIORITY_WEIGHT = 1;
        double PRIORITY_QTY = 1;
        double PRIORITY_PRICE = 1;

        int estimateIndex = 0;

        if (estimateList.size() > 1) {
            TreeMap<Integer, GenericValue> estimatePriority = new TreeMap<Integer, GenericValue>();

            for (GenericValue currentEstimate: estimateList) {
                int prioritySum = 0;
                if (UtilValidate.isNotEmpty(currentEstimate.getString("partyId"))) {
                    prioritySum += PRIORITY_PARTY;
                }
                if (UtilValidate.isNotEmpty(currentEstimate.getString("roleTypeId"))) {
                    prioritySum += PRIORITY_ROLE;
                }
                if (UtilValidate.isNotEmpty(currentEstimate.getString("geoIdTo"))) {
                    prioritySum += PRIORITY_GEO;
                }
                if (UtilValidate.isNotEmpty(currentEstimate.getString("weightBreakId"))) {
                    prioritySum += PRIORITY_WEIGHT;
                }
                if (UtilValidate.isNotEmpty(currentEstimate.getString("quantityBreakId"))) {
                    prioritySum += PRIORITY_QTY;<commit_msg>shipment: taking into consideration Geo proximity while calculating estimates priority<commit_after>/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.ofbiz.shipment.shipment;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.UtilGenerics;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilNumber;
import org.ofbiz.base.util.UtilProperties;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.common.geo.GeoWorker;
import org.ofbiz.entity.Delegator;
import org.ofbiz.entity.GenericEntityException;
import org.ofbiz.entity.GenericValue;
import org.ofbiz.entity.condition.EntityCondition;
import org.ofbiz.entity.condition.EntityOperator;
import org.ofbiz.entity.util.EntityListIterator;
import org.ofbiz.entity.util.EntityQuery;
import org.ofbiz.entity.util.EntityUtil;
import org.ofbiz.party.party.PartyWorker;
import org.ofbiz.product.store.ProductStoreWorker;
import org.ofbiz.service.DispatchContext;
import org.ofbiz.service.GenericServiceException;
import org.ofbiz.service.LocalDispatcher;
import org.ofbiz.service.ModelService;
import org.ofbiz.service.ServiceUtil;

/**
 * ShipmentServices
 */
public class ShipmentServices {

    private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());

    public static final String resource = "ProductUiLabels";
    public static final String resource_error = "OrderErrorUiLabels";
    public static final int decimals = UtilNumber.getBigDecimalScale("order.decimals");
    public static final RoundingMode rounding = UtilNumber.getRoundingMode("order.rounding");
    public static final BigDecimal ZERO = BigDecimal.ZERO.setScale(decimals, rounding);

    public static Map<String, Object> createShipmentEstimate(DispatchContext dctx, Map<String, ? extends Object> context) {
        Map<String, Object> result = new HashMap<String, Object>();
        Delegator delegator = dctx.getDelegator();
        Locale locale = (Locale) context.get("locale");
        List<GenericValue> storeAll = new LinkedList<GenericValue>();
        String productStoreShipMethId = (String)context.get("productStoreShipMethId");

        GenericValue productStoreShipMeth = null;
        try {
            productStoreShipMeth = EntityQuery.use(delegator).from("ProductStoreShipmentMeth").where("productStoreShipMethId", productStoreShipMethId).queryOne();
        } catch (GenericEntityException e) {
            return ServiceUtil.returnError(UtilProperties.getMessage(resource,
                    "ProductStoreShipmentMethodCannotRetrieve",
                    UtilMisc.toMap("productStoreShipMethId", productStoreShipMethId,
                            "errorString", e.toString()), locale));
        }

        // Create the basic entity.
        GenericValue estimate = delegator.makeValue("ShipmentCostEstimate");

        estimate.set("shipmentCostEstimateId", delegator.getNextSeqId("ShipmentCostEstimate"));
        estimate.set("productStoreShipMethId", productStoreShipMethId);
        estimate.set("shipmentMethodTypeId", productStoreShipMeth.getString("shipmentMethodTypeId"));
        estimate.set("carrierPartyId", productStoreShipMeth.getString("partyId"));
        estimate.set("carrierRoleTypeId", "CARRIER");
        estimate.set("productStoreId", productStoreShipMeth.getString("productStoreId"));
        estimate.set("geoIdTo", context.get("toGeo"));
        estimate.set("geoIdFrom", context.get("fromGeo"));
        estimate.set("partyId", context.get("partyId"));
        estimate.set("roleTypeId", context.get("roleTypeId"));
        estimate.set("orderPricePercent", context.get("flatPercent"));
        estimate.set("orderFlatPrice", context.get("flatPrice"));
        estimate.set("orderItemFlatPrice", context.get("flatItemPrice"));
        estimate.set("shippingPricePercent", context.get("shippingPricePercent"));
        estimate.set("productFeatureGroupId", context.get("productFeatureGroupId"));
        estimate.set("oversizeUnit", context.get("oversizeUnit"));
        estimate.set("oversizePrice", context.get("oversizePrice"));
        estimate.set("featurePercent", context.get("featurePercent"));
        estimate.set("featurePrice", context.get("featurePrice"));
        estimate.set("weightBreakId", context.get("weightBreakId"));
        estimate.set("weightUnitPrice", context.get("wprice"));
        estimate.set("weightUomId", context.get("wuom"));
        estimate.set("quantityBreakId", context.get("quantityBreakId"));
        estimate.set("quantityUnitPrice", context.get("qprice"));
        estimate.set("quantityUomId", context.get("quom"));
        estimate.set("priceBreakId", context.get("priceBreakId"));
        estimate.set("priceUnitPrice", context.get("pprice"));
        estimate.set("priceUomId", context.get("puom"));
        storeAll.add(estimate);

        if (!applyQuantityBreak(context, result, storeAll, delegator, estimate, "w", "weight", "Weight")) {
            return result;
        }

        if (!applyQuantityBreak(context, result, storeAll, delegator, estimate, "q", "quantity", "Quantity")) {
            return result;
        }

        if (!applyQuantityBreak(context, result, storeAll, delegator, estimate, "p", "price", "Price")) {
            return result;
        }

        try {
            delegator.storeAll(storeAll);
        } catch (GenericEntityException e) {
            result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_ERROR);
            result.put(ModelService.ERROR_MESSAGE, "Problem reading product features: " + e.toString());
            return result;
        }

        result.put("shipmentCostEstimateId", estimate.get("shipmentCostEstimateId"));
        result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_SUCCESS);
        return result;
    }

    public static Map<String, Object> removeShipmentEstimate(DispatchContext dctx, Map<String, ? extends Object> context) {
        Delegator delegator = dctx.getDelegator();
        String shipmentCostEstimateId = (String) context.get("shipmentCostEstimateId");
        Locale locale = (Locale) context.get("locale");

        GenericValue estimate = null;

        try {
            estimate = EntityQuery.use(delegator).from("ShipmentCostEstimate").where("shipmentCostEstimateId", shipmentCostEstimateId).queryOne();
            estimate.remove();
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(UtilProperties.getMessage(resource,
                    "ProductShipmentCostEstimateRemoveError",
                    UtilMisc.toMap("errorString", e.toString()), locale));
        }
        return ServiceUtil.returnSuccess();
    }

    private static boolean applyQuantityBreak(Map<String, ? extends Object> context, Map<String, Object> result, List<GenericValue> storeAll, Delegator delegator,
                                              GenericValue estimate, String prefix, String breakType, String breakTypeString) {
        BigDecimal min = (BigDecimal) context.get(prefix + "min");
        BigDecimal max = (BigDecimal) context.get(prefix + "max");
        if (min != null || max != null) {
            if (min != null && max != null) {
                if (min.compareTo(max) <= 0 || max.compareTo(BigDecimal.ZERO) == 0) {
                    try {
                        String newSeqId = delegator.getNextSeqId("QuantityBreak");
                        GenericValue weightBreak = delegator.makeValue("QuantityBreak");
                        weightBreak.set("quantityBreakId", newSeqId);
                        weightBreak.set("quantityBreakTypeId", "SHIP_" + breakType.toUpperCase(Locale.getDefault()));
                        weightBreak.set("fromQuantity", min);
                        weightBreak.set("thruQuantity", max);
                        estimate.set(breakType + "BreakId", newSeqId);
                        estimate.set(breakType + "UnitPrice", context.get(prefix + "price"));
                        if (context.containsKey(prefix + "uom")) {
                            estimate.set(breakType + "UomId", context.get(prefix + "uom"));
                        }
                        storeAll.add(0, weightBreak);
                    } catch (Exception e) {
                        Debug.logError(e, module);
                    }
                }
                else {
                    result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_ERROR);
                    result.put(ModelService.ERROR_MESSAGE, "Max " + breakTypeString +
                            " must not be less than Min " + breakTypeString + ".");
                    return false;
                }
            }
            else {
                result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_ERROR);
                result.put(ModelService.ERROR_MESSAGE, breakTypeString+" Span Requires BOTH Fields.");
                return false;
            }
        }
        return true;
    }

    // ShippingEstimate Calc Service
    public static Map<String, Object> calcShipmentCostEstimate(DispatchContext dctx, Map<String, ? extends Object> context) {
        Delegator delegator = dctx.getDelegator();

        // prepare the data
        String productStoreShipMethId = (String) context.get("productStoreShipMethId");
        String productStoreId = (String) context.get("productStoreId");
        String carrierRoleTypeId = (String) context.get("carrierRoleTypeId");
        String carrierPartyId = (String) context.get("carrierPartyId");
        String shipmentMethodTypeId = (String) context.get("shipmentMethodTypeId");
        String shippingContactMechId = (String) context.get("shippingContactMechId");
        String shippingPostalCode = (String) context.get("shippingPostalCode");
        String shippingCountryCode = (String) context.get("shippingCountryCode");

        List<Map<String, Object>> shippableItemInfo = UtilGenerics.checkList(context.get("shippableItemInfo"));
        BigDecimal shippableTotal = (BigDecimal) context.get("shippableTotal");
        BigDecimal shippableQuantity = (BigDecimal) context.get("shippableQuantity");
        BigDecimal shippableWeight = (BigDecimal) context.get("shippableWeight");
        BigDecimal initialEstimateAmt = (BigDecimal) context.get("initialEstimateAmt");
        Locale locale = (Locale) context.get("locale");

        if (shippableTotal == null) {
            shippableTotal = BigDecimal.ZERO;
        }
        if (shippableQuantity == null) {
            shippableQuantity = BigDecimal.ZERO;
        }
        if (shippableWeight == null) {
            shippableWeight = BigDecimal.ZERO;
        }
        if (initialEstimateAmt == null) {
            initialEstimateAmt = BigDecimal.ZERO;
        }

        // get the ShipmentCostEstimate(s)
        Map<String, String> estFields = UtilMisc.toMap("productStoreId", productStoreId, "shipmentMethodTypeId", shipmentMethodTypeId,
                "carrierPartyId", carrierPartyId, "carrierRoleTypeId", carrierRoleTypeId);
        EntityCondition estFieldsCond = EntityCondition.makeCondition(estFields, EntityOperator.AND);

        if (UtilValidate.isNotEmpty(productStoreShipMethId)) {
            // if the productStoreShipMethId field is passed, then also get estimates that have the field set
            List<EntityCondition> condList = UtilMisc.toList(EntityCondition.makeCondition("productStoreShipMethId", EntityOperator.EQUALS, productStoreShipMethId), estFieldsCond);
            estFieldsCond = EntityCondition.makeCondition(condList, EntityOperator.AND);
        }

        Collection<GenericValue> estimates = null;
        try {
            estimates = EntityQuery.use(delegator).from("ShipmentCostEstimate")
                            .where(estFieldsCond)
                            .cache(true)
                            .queryList();
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(UtilProperties.getMessage(resource,
                    "ProductShipmentCostEstimateCannotRetrieve", locale));
        }
        if (estimates == null || estimates.size() < 1) {
            // SCIPIO: 2018-11-09: We must return null if no ShipmentCostEstimate records instead of ZERO so that
            // caller is aware we got nothing (not appropriate to return failure because this is normal and don't want in logs)
            // We will also only log warning if initialEstimateAmt was outright null (because shipping could theoretically be deemed free by other service and give 0$).
//            if (initialEstimateAmt.compareTo(BigDecimal.ZERO) == 0) {
//                Debug.logWarning("No shipping estimates found; the shipping amount returned is 0! Condition used was: " + estFieldsCond + "; Using the passed context: " + context, module);
//            }
//
//            Map<String, Object> respNow = ServiceUtil.returnSuccess();
//            respNow.put("shippingEstimateAmount", BigDecimal.ZERO);
//            return respNow;
            Map<String, Object> respNow = ServiceUtil.returnSuccess(UtilProperties.getMessage(resource, "ProductShipmentCostEstimateCannotFoundForCarrier",
                    UtilMisc.toMap("carrierPartyId", carrierPartyId, "shipmentMethodTypeId", shipmentMethodTypeId), locale));
            if (context.get("initialEstimateAmt") == null) {
                // SCIPIO: TODO: REVIEW: It is strange for this service to be responsible to print this warning, but no better alternative for now
                Debug.logWarning("No entity ShipmentCostEstimate shipping estimates found for condition: " + estFieldsCond + "; using context: " + context, module);
            } else {
                // If we got a initialEstimateAmt (from external services), it is normal for there to be no ShipmentCostEstimate records, so verbose this
                if (Debug.verboseOn()) {
                    Debug.logVerbose("No entity ShipmentCostEstimate shipping estimates found for condition: " + estFieldsCond + "; using context: " + context, module);
                }
            }
            respNow.put("shippingEstimateAmount", null);
            return respNow;
        }

        // Get the PostalAddress
        GenericValue shipAddress = null;
        if (shippingContactMechId != null) {
            try {
                shipAddress = EntityQuery.use(delegator).from("PostalAddress").where("contactMechId", shippingContactMechId).queryOne();
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
                return ServiceUtil.returnError(UtilProperties.getMessage(resource,
                        "ProductShipmentCostEstimateCannotGetShippingAddress", locale));
            }
        } else if (shippingPostalCode != null) {
            String countryGeoId = null;
            try {
                GenericValue countryGeo = EntityQuery.use(delegator).from("Geo")
                        .where("geoTypeId", "COUNTRY", "geoCode", shippingCountryCode)
                        .cache(true)
                        .queryFirst();
                if (countryGeo != null) {
                    countryGeoId = countryGeo.getString("geoId");
                }
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
            shipAddress = delegator.makeValue("PostalAddress");
            shipAddress.set("countryGeoId", countryGeoId);
            shipAddress.set("postalCodeGeoId", shippingPostalCode);
        }
        // Get the possible estimates.
        List<GenericValue> estimateList = new LinkedList<GenericValue>();

        for (GenericValue thisEstimate: estimates) {
            try {
                String toGeo = thisEstimate.getString("geoIdTo");
                if (UtilValidate.isNotEmpty(toGeo) && shipAddress ==null) {
                    // This estimate requires shipping address details. We don't have it so we cannot use this estimate.
                    continue;
                }
                List<GenericValue> toGeoList = GeoWorker.expandGeoGroup(toGeo, delegator);
                // Make sure we have a valid GEOID.
                if (UtilValidate.isEmpty(toGeoList) ||
                        GeoWorker.containsGeo(toGeoList, shipAddress.getString("countryGeoId"), delegator) ||
                        GeoWorker.containsGeo(toGeoList, shipAddress.getString("stateProvinceGeoId"), delegator) ||
                        GeoWorker.containsGeo(toGeoList, shipAddress.getString("postalCodeGeoId"), delegator)) {
                    GenericValue wv = thisEstimate.getRelatedOne("WeightQuantityBreak", false);
                    GenericValue qv = thisEstimate.getRelatedOne("QuantityQuantityBreak", false);
                    GenericValue pv = thisEstimate.getRelatedOne("PriceQuantityBreak", false);
                    if (wv == null && qv == null && pv == null) {
                        estimateList.add(thisEstimate);
                    } else {
                        // Do some testing.
                        boolean useWeight = false;
                        boolean weightValid = false;
                        boolean useQty = false;
                        boolean qtyValid = false;
                        boolean usePrice = false;
                        boolean priceValid = false;

                        if (wv != null) {
                            useWeight = true;
                            BigDecimal min = BigDecimal.ONE.movePointLeft(4);
                            BigDecimal max = BigDecimal.ONE.movePointLeft(4);
                            min = wv.getBigDecimal("fromQuantity");
                            max = wv.getBigDecimal("thruQuantity");
                            if (shippableWeight.compareTo(min) >= 0 && (max.compareTo(BigDecimal.ZERO) == 0 || shippableWeight.compareTo(max) <= 0)) {
                                weightValid = true;
                            }
                        }
                        if (qv != null) {
                            useQty = true;
                            BigDecimal min = BigDecimal.ONE.movePointLeft(4);
                            BigDecimal max = BigDecimal.ONE.movePointLeft(4);
                            min = qv.getBigDecimal("fromQuantity");
                            max = qv.getBigDecimal("thruQuantity");
                            if (shippableQuantity.compareTo(min) >= 0 && (max.compareTo(BigDecimal.ZERO) == 0 || shippableQuantity.compareTo(max) <= 0)) {
                                qtyValid = true;
                            }
                        }
                        if (pv != null) {
                            usePrice = true;
                            BigDecimal min = BigDecimal.ONE.movePointLeft(4);
                            BigDecimal max = BigDecimal.ONE.movePointLeft(4);
                            min = pv.getBigDecimal("fromQuantity");
                            max = pv.getBigDecimal("thruQuantity");
                            if (shippableTotal.compareTo(min) >= 0 && (max.compareTo(BigDecimal.ZERO) == 0 || shippableTotal.compareTo(max) <= 0)) {
                                priceValid = true;
                            }
                        }
                        // Now check the tests.
                        if ((useWeight && weightValid) || (useQty && qtyValid) || (usePrice && priceValid)) {
                            estimateList.add(thisEstimate);
                        }
                    }
                }
            } catch (GenericEntityException e) {
                Debug.logError(e, e.getLocalizedMessage(), module);
            }
        }

        if (estimateList.size() < 1) {
            return ServiceUtil.returnFailure(UtilProperties.getMessage(resource,
                    "ProductShipmentCostEstimateCannotFoundForCarrier",
                    UtilMisc.toMap("carrierPartyId", carrierPartyId,
                            "shipmentMethodTypeId", shipmentMethodTypeId), locale));
        }

        // make the shippable item size/feature objects
        List<BigDecimal> shippableItemSizes = new LinkedList<BigDecimal>();
        Map<String, BigDecimal> shippableFeatureMap = new HashMap<String, BigDecimal>();
        if (shippableItemInfo != null) {
            for (Map<String, Object> itemMap: shippableItemInfo) {
                // add the item sizes
                if (itemMap.containsKey("size")) {
                    BigDecimal itemSize = (BigDecimal) itemMap.get("size");
                    if (itemSize != null) {
                        shippableItemSizes.add(itemSize);
                    }
                }

                // add the feature quantities
                BigDecimal quantity = (BigDecimal) itemMap.get("quantity");
                if (itemMap.containsKey("featureSet")) {
                    Set<String> featureSet = UtilGenerics.checkSet(itemMap.get("featureSet"));
                    if (UtilValidate.isNotEmpty(featureSet)) {
                        for (String featureId: featureSet) {
                            BigDecimal featureQuantity = shippableFeatureMap.get(featureId);
                            if (featureQuantity == null) {
                                featureQuantity = BigDecimal.ZERO;
                            }
                            featureQuantity = featureQuantity.add(quantity);
                            shippableFeatureMap.put(featureId, featureQuantity);
                        }
                    }
                }

            }
        }

        // Calculate priority based on available data.
        double PRIORITY_PARTY = 9;
        double PRIORITY_ROLE = 8;
        double PRIORITY_GEO = 4;
        double PRIORITY_WEIGHT = 1;
        double PRIORITY_QTY = 1;
        double PRIORITY_PRICE = 1;

        int estimateIndex = 0;

        if (estimateList.size() > 1) {
            TreeMap<Integer, GenericValue> estimatePriority = new TreeMap<Integer, GenericValue>();

            for (GenericValue currentEstimate: estimateList) {
                int prioritySum = 0;
                if (UtilValidate.isNotEmpty(currentEstimate.getString("partyId"))) {
                    prioritySum += PRIORITY_PARTY;
                }
                if (UtilValidate.isNotEmpty(currentEstimate.getString("roleTypeId"))) {
                    prioritySum += PRIORITY_ROLE;
                }
                if (UtilValidate.isNotEmpty(currentEstimate.getString("geoIdTo"))) {
                    prioritySum += PRIORITY_GEO;
                    try {
                        GenericValue geo = EntityQuery.use(delegator).from("Geo").where("geoId", currentEstimate.getString("geoIdTo")).cache().queryOne();
                        switch (geo.getString("geoTypeId")) {
                            case "CITY":
                            case "COUNTY-CITY":
                                prioritySum += 5;
                                break;
                            case "PROVINCE":
                                prioritySum += 4;
                                break;
                            case "COUNTY":
                                prioritySum += 3;
                                break;
                            case "STATE":
                            case "REGION":
                                prioritySum += 2;
                                break;
                            case "COUNTRY":
                                prioritySum += 1;
                                break;
                        }
                    } catch (GenericEntityException e) {
                        Debug.logError(e.getMessage(), module);
                    }
                }
                if (UtilValidate.isNotEmpty(currentEstimate.getString("weightBreakId"))) {
                    prioritySum += PRIORITY_WEIGHT;
                }
                if (UtilValidate.isNotEmpty(currentEstimate.getString("quantityBreakId"))) {
                    prioritySum += PRIORITY_QTY;
{'fraction_non_alphanumeric': 0.10588545469295044, 'fraction_numerical': 0.0018861036748600633, 'mean_word_length': 2.1541610695323996, 'pattern_counts': {'":': 7, '<': 77, '<?xml version=': 0, '>': 73, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5109095', 'n_tokens_mistral': 12676, 'n_tokens_neox': 12199, 'n_words': 2591}