All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.masterdata.vat.VATManager Maven / Gradle / Ivy

/**
 * Copyright (C) 2014-2017 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.masterdata.vat;

import java.io.InputStream;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.function.Predicate;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillClose;
import javax.annotation.concurrent.NotThreadSafe;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.CollectionHelper;
import com.helger.commons.collection.ext.CommonsArrayList;
import com.helger.commons.collection.ext.CommonsHashMap;
import com.helger.commons.collection.ext.ICommonsList;
import com.helger.commons.collection.ext.ICommonsMap;
import com.helger.commons.collection.ext.ICommonsSet;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.io.IHasInputStream;
import com.helger.commons.io.resource.ClassPathResource;
import com.helger.commons.locale.LocaleHelper;
import com.helger.commons.locale.country.CountryCache;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.StringParser;
import com.helger.commons.string.ToStringGenerator;
import com.helger.datetime.format.PDTFromString;
import com.helger.xml.microdom.IMicroDocument;
import com.helger.xml.microdom.IMicroElement;
import com.helger.xml.microdom.serialize.MicroReader;
import com.helger.xml.microdom.util.MicroHelper;

/**
 * Manages the available VAT types.
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class VATManager implements IVATItemProvider
{
  private static final class SingletonHolder
  {
    static final VATManager s_aInstance = readFromXML (new ClassPathResource ("codelists/vat-data.xml"));
  }

  /** Special VAT item with 0% */
  public static final IVATItem VATTYPE_NONE = new VATItem ("_none_", EVATType.OTHER, BigDecimal.ZERO, false);

  private static final Logger s_aLogger = LoggerFactory.getLogger (VATManager.class);
  private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_DATE;

  // The sources the data comes from
  private final ICommonsList  m_aSources = new CommonsArrayList <> ();

  // Maps from locale to the available VAT data
  private final ICommonsMap  m_aVATItemsPerCountry = new CommonsHashMap <> ();

  // Overall VAT map (ID to item)
  private final ICommonsMap  m_aAllVATItems = new CommonsHashMap <> ();

  public VATManager ()
  {}

  @Nullable
  private static String _getCountryString (@Nullable final Locale aLocale)
  {
    if (aLocale == null)
      return null;

    // Is it "all" or "independent"?
    if (LocaleHelper.isSpecialLocale (aLocale))
      return aLocale.getLanguage ();
    return aLocale.getCountry ().toLowerCase (Locale.US);
  }

  public void initFromXML (@Nonnull final IMicroDocument aDoc)
  {
    ValueEnforcer.notNull (aDoc, "Doc");
    ValueEnforcer.notNull (aDoc.getDocumentElement (), "Doc.DocumentElement");

    m_aSources.clear ();
    m_aVATItemsPerCountry.clear ();
    m_aAllVATItems.clear ();

    final IMicroElement eSources = aDoc.getDocumentElement ().getFirstChildElement ("sources");
    if (eSources != null)
      eSources.forAllChildElements (IMicroElement.filterNamespaceURIAndName (null, "source"), eSource -> {
        final String sSource = eSource.getTextContent ();
        if (StringHelper.hasText (sSource))
          m_aSources.add (sSource);
      });

    for (final IMicroElement eVATTypes : aDoc.getDocumentElement ().getAllChildElements ("vattypes"))
    {
      // Country
      final String sCountry = eVATTypes.getAttributeValue ("country");
      final Locale aCountry = CountryCache.getInstance ().getCountry (sCountry);
      if (m_aVATItemsPerCountry.containsKey (aCountry))
      {
        s_aLogger.warn ("VAT types for country " + aCountry + " have already been defined!");
        continue;
      }
      final String sCountryName = eVATTypes.getAttributeValue ("countryname");

      // zero VAT allowed?
      final String sZeroVATAllowed = eVATTypes.getAttributeValue ("zerovat");
      final boolean bZeroVATAllowed = StringParser.parseBool (sZeroVATAllowed);

      // Internal comment?
      final String sInternalComment = MicroHelper.getChildTextContent (eVATTypes, "comment");

      // read all items
      final VATCountryData aVATCountryData = new VATCountryData (aCountry,
                                                                 bZeroVATAllowed,
                                                                 sCountryName,
                                                                 sInternalComment);
      for (final IMicroElement eVATItem : eVATTypes.getAllChildElements ("item"))
      {
        // item ID
        final String sID = eVATItem.getAttributeValue ("id");
        if (StringHelper.hasNoText (sID))
        {
          s_aLogger.warn ("VAT item in country " + aCountry + " has no ID. Skipping VAT item.");
          continue;
        }
        final String sRealID = _getCountryString (aCountry) + "." + sID;

        // item type
        final String sType = eVATItem.getAttributeValue ("type");
        final EVATType eType = EVATType.getFromIDOrNull (sType);
        if (eType == null)
        {
          s_aLogger.warn ("VAT type '" + sType + "' for VAT item " + sRealID + " is illegal. Skipping VAT item.");
          continue;
        }

        // item percentage
        final String sPercentage = eVATItem.getAttributeValue ("percentage");
        final BigDecimal aPercentage = StringParser.parseBigDecimal (sPercentage, null);
        if (aPercentage == null)
        {
          s_aLogger.warn ("Percentage value '" +
                          sPercentage +
                          "' for VAT item " +
                          sRealID +
                          " is illegal. Skipping VAT item.");
          continue;
        }

        // Deprecated?
        final String sDeprecated = eVATItem.getAttributeValue ("deprecated");
        final boolean bDeprecated = sDeprecated != null && StringParser.parseBool (sDeprecated);

        // Valid from (optional)
        final String sValidFrom = eVATItem.getAttributeValue ("validfrom");
        final LocalDate aValidFrom = PDTFromString.getLocalDateFromString (sValidFrom, DATE_FORMAT);

        // Valid to (optional)
        final String sValidTo = eVATItem.getAttributeValue ("validto");
        final LocalDate aValidTo = PDTFromString.getLocalDateFromString (sValidTo, DATE_FORMAT);

        // build and add item
        final VATItem aVATItem = new VATItem (sRealID, eType, aPercentage, bDeprecated, aValidFrom, aValidTo);
        if (aVATCountryData.addItem (aVATItem).isUnchanged ())
          s_aLogger.warn ("Found duplicate VAT item " + aVATItem + " for country " + aCountry);
        if (m_aAllVATItems.put (sRealID, aVATItem) != null)
          s_aLogger.warn ("Found overall duplicate VAT item " + aVATItem);
      }

      if (aVATCountryData.isEmpty ())
        s_aLogger.warn ("No VAT types for country " + aCountry + " defined!");
      m_aVATItemsPerCountry.put (aCountry, aVATCountryData);
    }
  }

  /**
   * @return A list with all URLs where the data was gathered from. Purely
   *         descriptive. Has no impact on the logic. Never null.
   */
  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList  getAllSources ()
  {
    return m_aSources.getClone ();
  }

  /**
   * @return All countries for which VAT type definitions are present. Never
   *         null.
   */
  @Nonnull
  @ReturnsMutableCopy
  public ICommonsSet  getAllAvailableCountries ()
  {
    return m_aVATItemsPerCountry.copyOfKeySet ();
  }

  /**
   * @return The complete map from locale to VAT country data. Never
   *         null.
   * @since 5.0.5
   */
  @Nonnull
  @ReturnsMutableCopy
  public ICommonsMap  getAllCountryData ()
  {
    return m_aVATItemsPerCountry.getClone ();
  }

  /**
   * Check if zero VAT is allowed for the passed country
   *
   * @param aCountry
   *        The country to be checked.
   * @param bUndefinedValue
   *        The value to be returned, if no VAT data is available for the passed
   *        country
   * @return true or false
   */
  public boolean isZeroVATAllowed (@Nonnull final Locale aCountry, final boolean bUndefinedValue)
  {
    ValueEnforcer.notNull (aCountry, "Country");

    // first get locale specific VAT types
    final VATCountryData aVATCountryData = getVATCountryData (aCountry);
    return aVATCountryData != null ? aVATCountryData.isZeroVATAllowed () : bUndefinedValue;
  }

  /**
   * Get the VAT data of the passed country.
   *
   * @param aLocale
   *        The locale to use. May not be null.
   * @return null if no such country data is present.
   */
  @Nullable
  public VATCountryData getVATCountryData (@Nonnull final Locale aLocale)
  {
    ValueEnforcer.notNull (aLocale, "Locale");
    final Locale aCountry = CountryCache.getInstance ().getCountry (aLocale);
    return m_aVATItemsPerCountry.get (aCountry);
  }

  /**
   * Check if VAT data is available for the provided country.
   *
   * @param aLocale
   *        The locale to check. May be null.
   * @return true if VAT country data is available.
   * @since 5.0.5
   */
  public boolean isVATCountryDataAvailable (@Nullable final Locale aLocale)
  {
    return aLocale != null && getVATCountryData (aLocale) != null;
  }

  /**
   * Get all VAT types matching the given locale (without any fallback!). It
   * contains both the specific definitions and the locale independent
   * definitions.
   *
   * @param aCountry
   *        The locale to use. May not be null.
   * @return A non-null map from ID to the matching VAT item. Also
   *         the deprecated VAT items are returned! VATTYPE_NONE.getID () is
   *         used if zero VAT is allowed
   */
  @ReturnsMutableCopy
  @Nonnull
  public ICommonsMap  getAllVATItemsForCountry (@Nonnull final Locale aCountry)
  {
    ValueEnforcer.notNull (aCountry, "Country");

    final ICommonsMap  ret = new CommonsHashMap <> ();

    // first get locale specific VAT types
    final VATCountryData aVATCountryData = getVATCountryData (aCountry);
    if (aVATCountryData != null)
    {
      if (aVATCountryData.isZeroVATAllowed ())
        ret.put (VATTYPE_NONE.getID (), VATTYPE_NONE);
      ret.putAll (aVATCountryData.getAllItems ());
    }
    return ret;
  }

  @Nullable
  public IVATItem getVATItemOfID (@Nullable final String sID)
  {
    IVATItem ret = m_aAllVATItems.get (sID);
    if (ret == null && VATTYPE_NONE.getID ().equals (sID))
      ret = VATTYPE_NONE;
    return ret;
  }

  @Nullable
  public IVATItem getVATItemOfID (@Nonnull final Locale aCountry, @Nullable final String sID)
  {
    return getVATItemOfID (_getCountryString (aCountry) + "." + sID);
  }

  /**
   * Find a matching VAT item with the passed properties, independent of the
   * country.
   *
   * @param eType
   *        The VAT type to use. May be null resulting in a
   *        null result.
   * @param aPercentage
   *        The percentage to find. May be null resulting in a
   *        null result.
   * @return null if no matching item could be found,
   */
  @Nullable
  public IVATItem findVATItem (@Nullable final EVATType eType, @Nullable final BigDecimal aPercentage)
  {
    if (eType == null || aPercentage == null)
      return null;
    return findFirst (x -> x.getType ().equals (eType) && EqualsHelper.equals (x.getPercentage (), aPercentage));
  }

  /**
   * Find the first matching VAT item.
   *
   * @param aFilter
   *        The filter to be used. May not be null.
   * @return null if no matching item could be found,
   */
  @Nullable
  public IVATItem findFirst (@Nonnull final Predicate  aFilter)
  {
    return CollectionHelper.findFirst (m_aAllVATItems.values (), aFilter);
  }

  /**
   * Find all matching VAT items.
   *
   * @param aFilter
   *        The filter to be used. May not be null.
   * @return A non-null but maybe empty list with all matching
   *         {@link IVATItem} objects.
   * @since 5.0.5
   */
  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList  findAll (@Nonnull final Predicate  aFilter)
  {
    final ICommonsList  ret = new CommonsArrayList <> ();
    CollectionHelper.findAll (m_aAllVATItems.values (), aFilter, ret::add);
    return ret;
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("sources", m_aSources)
                                       .append ("VATItemsPerCountry", m_aVATItemsPerCountry)
                                       .append ("allVATItems", m_aAllVATItems)
                                       .getToString ();
  }

  @Nonnull
  public static VATManager readFromXML (@Nonnull final IHasInputStream aISP)
  {
    ValueEnforcer.notNull (aISP, "InputStreamProvider");

    return readFromXML (aISP.getInputStream ());
  }

  @Nonnull
  public static VATManager readFromXML (@Nonnull @WillClose final InputStream aIS)
  {
    ValueEnforcer.notNull (aIS, "InputStream");

    final IMicroDocument aDoc = MicroReader.readMicroXML (aIS);
    final VATManager ret = new VATManager ();
    ret.initFromXML (aDoc);
    return ret;
  }

  /**
   * @return The default singleton instance. Never null.
   */
  @Nonnull
  public static VATManager getDefaultInstance ()
  {
    return SingletonHolder.s_aInstance;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy