@Override
  public List<Country> getShipToCountryList(MerchantStore store, Language language)
      throws ServiceException {

    ShippingConfiguration shippingConfiguration = getShippingConfiguration(store);
    ShippingType shippingType = ShippingType.INTERNATIONAL;
    List<String> supportedCountries = new ArrayList<String>();
    if (shippingConfiguration == null) {
      shippingConfiguration = new ShippingConfiguration();
    }

    if (shippingConfiguration.getShippingType() != null) {
      shippingType = shippingConfiguration.getShippingType();
    }

    if (shippingType.name().equals(ShippingType.NATIONAL.name())) {

      supportedCountries.add(store.getCountry().getIsoCode());

    } else {

      MerchantConfiguration configuration =
          merchantConfigurationService.getMerchantConfiguration(SUPPORTED_COUNTRIES, store);

      if (configuration != null) {

        String countries = configuration.getValue();
        if (!StringUtils.isBlank(countries)) {

          Object objRegions = JSONValue.parse(countries);
          JSONArray arrayRegions = (JSONArray) objRegions;
          @SuppressWarnings("rawtypes")
          Iterator i = arrayRegions.iterator();
          while (i.hasNext()) {
            supportedCountries.add((String) i.next());
          }
        }
      }
    }

    return countryService.getCountries(supportedCountries, language);
  }
  @Override
  public List<PackageDetails> getPackagesDetails(
      List<ShippingProduct> products, MerchantStore store) throws ServiceException {

    List<PackageDetails> packages = null;

    ShippingConfiguration shippingConfiguration = this.getShippingConfiguration(store);
    // determine if the system has to use BOX or ITEM
    ShippingPackageType shippingPackageType = ShippingPackageType.ITEM;
    if (shippingConfiguration != null) {
      shippingPackageType = shippingConfiguration.getShippingPackageType();
    }

    if (shippingPackageType.name().equals(ShippingPackageType.BOX.name())) {
      packages = packaging.getBoxPackagesDetails(products, store);
    } else {
      packages = packaging.getItemPackagesDetails(products, store);
    }

    return packages;
  }
  @Override
  public void saveShippingConfiguration(
      ShippingConfiguration shippingConfiguration, MerchantStore store) throws ServiceException {

    MerchantConfiguration configuration =
        merchantConfigurationService.getMerchantConfiguration(
            ShippingConstants.SHIPPING_CONFIGURATION, store);

    if (configuration == null) {
      configuration = new MerchantConfiguration();
      configuration.setMerchantStore(store);
      configuration.setKey(ShippingConstants.SHIPPING_CONFIGURATION);
    }

    String value = shippingConfiguration.toJSONString();
    configuration.setValue(value);
    merchantConfigurationService.saveOrUpdate(configuration);
  }
  @Override
  public ShippingQuote getShippingQuote(
      MerchantStore store, Delivery delivery, List<ShippingProduct> products, Language language)
      throws ServiceException {

    // ShippingConfiguration -> Global configuration of a given store
    // IntegrationConfiguration -> Configuration of a given module
    // IntegrationModule -> The concrete module as defined in integrationmodules.properties

    // delivery without postal code is accepted
    Validate.notNull(store, "MerchantStore must not be null");
    Validate.notNull(delivery, "Delivery must not be null");
    Validate.notEmpty(products, "products must not be empty");
    Validate.notNull(language, "Language must not be null");

    ShippingQuote shippingQuote = new ShippingQuote();
    ShippingQuoteModule shippingQuoteModule = null;

    try {

      if (StringUtils.isBlank(delivery.getPostalCode())) {
        shippingQuote.getWarnings().add("No postal code in delivery address");
        shippingQuote.setShippingReturnCode(ShippingQuote.NO_POSTAL_CODE);
      }

      // get configuration
      ShippingConfiguration shippingConfiguration = getShippingConfiguration(store);
      ShippingType shippingType = ShippingType.INTERNATIONAL;

      /** get shipping origin * */
      ShippingOrigin shippingOrigin = shippingOriginService.getByStore(store);
      if (shippingOrigin == null || !shippingOrigin.isActive()) {
        shippingOrigin = new ShippingOrigin();
        shippingOrigin.setAddress(store.getStoreaddress());
        shippingOrigin.setCity(store.getStorecity());
        shippingOrigin.setCountry(store.getCountry());
        shippingOrigin.setPostalCode(store.getStorepostalcode());
        shippingOrigin.setState(store.getStorestateprovince());
        shippingOrigin.setZone(store.getZone());
      }

      if (shippingConfiguration == null) {
        shippingConfiguration = new ShippingConfiguration();
      }

      if (shippingConfiguration.getShippingType() != null) {
        shippingType = shippingConfiguration.getShippingType();
      }

      // look if customer country code excluded
      Country shipCountry = delivery.getCountry();
      if (shipCountry == null) {
        throw new ServiceException("Delivery country is null");
      }

      // a ship to country is required
      Validate.notNull(shipCountry);
      Validate.notNull(store.getCountry());

      if (shippingType.name().equals(ShippingType.NATIONAL.name())) {
        // customer country must match store country
        if (!shipCountry.getIsoCode().equals(store.getCountry().getIsoCode())) {
          shippingQuote.setShippingReturnCode(
              ShippingQuote.NO_SHIPPING_TO_SELECTED_COUNTRY + " " + shipCountry.getIsoCode());
          return shippingQuote;
        }
      } else if (shippingType.name().equals(ShippingType.INTERNATIONAL.name())) {

        // customer shipping country code must be in accepted list
        List<String> supportedCountries = this.getSupportedCountries(store);
        if (!supportedCountries.contains(shipCountry.getIsoCode())) {
          shippingQuote.setShippingReturnCode(
              ShippingQuote.NO_SHIPPING_TO_SELECTED_COUNTRY + " " + shipCountry.getIsoCode());
          return shippingQuote;
        }
      }

      // must have a shipping module configured
      Map<String, IntegrationConfiguration> modules = this.getShippingModulesConfigured(store);
      if (modules == null) {
        shippingQuote.setShippingReturnCode(ShippingQuote.NO_SHIPPING_MODULE_CONFIGURED);
        return shippingQuote;
      }

      /** uses this module name * */
      String moduleName = null;
      IntegrationConfiguration configuration = null;
      for (String module : modules.keySet()) {
        moduleName = module;
        configuration = modules.get(module);
        // use the first active module
        if (configuration.isActive()) {
          shippingQuoteModule = this.shippingModules.get(module);
          if (shippingQuoteModule instanceof ShippingQuotePrePostProcessModule) {
            shippingQuoteModule = null;
            continue;
          } else {
            break;
          }
        }
      }

      if (shippingQuoteModule == null) {
        shippingQuote.setShippingReturnCode(ShippingQuote.NO_SHIPPING_MODULE_CONFIGURED);
        return shippingQuote;
      }

      /** merchant module configs * */
      List<IntegrationModule> shippingMethods = this.getShippingMethods(store);
      IntegrationModule shippingModule = null;
      for (IntegrationModule mod : shippingMethods) {
        if (mod.getCode().equals(moduleName)) {
          shippingModule = mod;
          break;
        }
      }

      /** general module configs * */
      if (shippingModule == null) {
        shippingQuote.setShippingReturnCode(ShippingQuote.NO_SHIPPING_MODULE_CONFIGURED);
        return shippingQuote;
      }

      // calculate order total
      BigDecimal orderTotal = calculateOrderTotal(products, store);
      List<PackageDetails> packages = this.getPackagesDetails(products, store);

      // free shipping ?
      if (shippingConfiguration.isFreeShippingEnabled()) {
        BigDecimal freeShippingAmount = shippingConfiguration.getOrderTotalFreeShipping();
        if (freeShippingAmount != null) {
          if (orderTotal.doubleValue() > freeShippingAmount.doubleValue()) {
            if (shippingConfiguration.getFreeShippingType() == ShippingType.NATIONAL) {
              if (store.getCountry().getIsoCode().equals(shipCountry.getIsoCode())) {
                shippingQuote.setFreeShipping(true);
                shippingQuote.setFreeShippingAmount(freeShippingAmount);
                return shippingQuote;
              }
            } else { // international all
              shippingQuote.setFreeShipping(true);
              shippingQuote.setFreeShippingAmount(freeShippingAmount);
              return shippingQuote;
            }
          }
        }
      }

      // handling fees
      BigDecimal handlingFees = shippingConfiguration.getHandlingFees();
      if (handlingFees != null) {
        shippingQuote.setHandlingFees(handlingFees);
      }

      // tax basis
      shippingQuote.setApplyTaxOnShipping(shippingConfiguration.isTaxOnShipping());

      Locale locale = languageService.toLocale(language);

      // invoke pre processors
      if (!CollectionUtils.isEmpty(shippingModulePreProcessors)) {
        for (ShippingQuotePrePostProcessModule preProcessor : shippingModulePreProcessors) {
          // System.out.println("Using pre-processor " + preProcessor.getModuleCode());
          preProcessor.prePostProcessShippingQuotes(
              shippingQuote,
              packages,
              orderTotal,
              delivery,
              shippingOrigin,
              store,
              configuration,
              shippingModule,
              shippingConfiguration,
              shippingMethods,
              locale);
          // TODO switch module if required
          if (shippingQuote.getCurrentShippingModule() != null
              && !shippingQuote
                  .getCurrentShippingModule()
                  .getCode()
                  .equals(shippingModule.getCode())) {
            shippingModule = shippingQuote.getCurrentShippingModule();
            moduleName = shippingModule.getCode();
            shippingQuoteModule = this.shippingModules.get(shippingModule.getCode());
            configuration = modules.get(shippingModule.getCode());
          }
        }
      }

      // invoke module
      List<ShippingOption> shippingOptions = null;

      try {
        shippingOptions =
            shippingQuoteModule.getShippingQuotes(
                shippingQuote,
                packages,
                orderTotal,
                delivery,
                shippingOrigin,
                store,
                configuration,
                shippingModule,
                shippingConfiguration,
                locale);
      } catch (Exception e) {
        LOGGER.error("Error while calculating shipping", e);
        merchantLogService.save(
            new MerchantLog(
                store, "Can't process " + shippingModule.getModule() + " -> " + e.getMessage()));
        shippingQuote.setQuoteError(e.getMessage());
        shippingQuote.setShippingReturnCode(ShippingQuote.ERROR);
        return shippingQuote;
      }

      if (shippingOptions == null && !StringUtils.isBlank(delivery.getPostalCode())) {
        shippingQuote.setShippingReturnCode(ShippingQuote.NO_SHIPPING_TO_SELECTED_COUNTRY);
      }

      shippingQuote.setShippingModuleCode(moduleName);

      // filter shipping options
      ShippingOptionPriceType shippingOptionPriceType =
          shippingConfiguration.getShippingOptionPriceType();
      ShippingOption selectedOption = null;

      if (shippingOptions != null) {

        for (ShippingOption option : shippingOptions) {
          if (selectedOption == null) {
            selectedOption = option;
          }
          // set price text
          String priceText = pricingService.getDisplayAmount(option.getOptionPrice(), store);
          option.setOptionPriceText(priceText);
          option.setShippingModuleCode(moduleName);

          if (StringUtils.isBlank(option.getOptionName())) {

            String countryName = delivery.getCountry().getName();
            if (countryName == null) {
              Map<String, Country> deliveryCountries = countryService.getCountriesMap(language);
              Country dCountry =
                  (Country) deliveryCountries.get(delivery.getCountry().getIsoCode());
              if (dCountry != null) {
                countryName = dCountry.getName();
              } else {
                countryName = delivery.getCountry().getIsoCode();
              }
            }
            option.setOptionName(countryName);
          }

          if (shippingOptionPriceType.name().equals(ShippingOptionPriceType.HIGHEST.name())) {

            if (option.getOptionPrice().longValue() > selectedOption.getOptionPrice().longValue()) {
              selectedOption = option;
            }
          }

          if (shippingOptionPriceType.name().equals(ShippingOptionPriceType.LEAST.name())) {

            if (option.getOptionPrice().longValue() < selectedOption.getOptionPrice().longValue()) {
              selectedOption = option;
            }
          }

          if (shippingOptionPriceType.name().equals(ShippingOptionPriceType.ALL.name())) {

            if (option.getOptionPrice().longValue() < selectedOption.getOptionPrice().longValue()) {
              selectedOption = option;
            }
          }
        }

        shippingQuote.setSelectedShippingOption(selectedOption);

        if (selectedOption != null
            && !shippingOptionPriceType.name().equals(ShippingOptionPriceType.ALL.name())) {
          shippingOptions = new ArrayList<ShippingOption>();
          shippingOptions.add(selectedOption);
        }
      }

      /** set final delivery address * */
      shippingQuote.setDeliveryAddress(delivery);

      shippingQuote.setShippingOptions(shippingOptions);

      /** post processors * */
      // invoke pre processors
      if (!CollectionUtils.isEmpty(shippingModulePostProcessors)) {
        for (ShippingQuotePrePostProcessModule postProcessor : shippingModulePostProcessors) {
          // get module info

          // get module configuration
          IntegrationConfiguration integrationConfiguration =
              modules.get(postProcessor.getModuleCode());

          IntegrationModule postProcessModule = null;
          for (IntegrationModule mod : shippingMethods) {
            if (mod.getCode().equals(postProcessor.getModuleCode())) {
              postProcessModule = mod;
              break;
            }
          }

          IntegrationModule module = postProcessModule;
          postProcessor.prePostProcessShippingQuotes(
              shippingQuote,
              packages,
              orderTotal,
              delivery,
              shippingOrigin,
              store,
              integrationConfiguration,
              module,
              shippingConfiguration,
              shippingMethods,
              locale);
        }
      }

    } catch (Exception e) {
      throw new ServiceException(e);
    }

    return shippingQuote;
  }