Add support for VAT rate periodization (#17642)

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Jacob Laursen 2024-10-29 11:54:42 +01:00 committed by Ciprian Pascu
parent 7074c938d3
commit f64349261c
10 changed files with 943 additions and 199 deletions

View File

@ -53,6 +53,9 @@ logger.info "Price incl. VAT: #{price_incl_vat}"
The functionality of this `TransformationService` can also be used in a `Profile` on an `ItemChannelLink`.
This is the most powerful usage since VAT will be added without providing any explicit country code, percentage or configuration.
Time series are supported when using this Profile, including applying VAT rates accurately based on the specific date and time of each state, even as new VAT rates come into effect.
This ensures that the correct VAT rate is applied for historical, current, or future data points, reflecting any changes in VAT regulations that occur over time.
To use this, an `.items` file can be configured as follows:
```java

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.transform.vat.internal;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.transform.vat.internal.model.VATCountry;
import org.openhab.transform.vat.internal.model.VATPeriod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
/**
* The {@link RateProvider} class provides VAT rates for different
* countries in different periods of time.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class RateProvider {
private static final String RESOURCE_NAME = "/vat_rates.yaml";
private final Logger logger = LoggerFactory.getLogger(RateProvider.class);
private final Map<String, List<VATPeriod>> rateMap = getMap();
public @Nullable BigDecimal getPercentage(String country) {
return getPercentage(country, Instant.now());
}
public @Nullable BigDecimal getPercentage(String country, Instant time) {
List<VATPeriod> vatPeriods = rateMap.get(country);
if (vatPeriods == null) {
return null;
}
for (VATPeriod vatPeriod : vatPeriods) {
if (!time.isBefore(vatPeriod.start()) && time.isBefore(vatPeriod.end())) {
return vatPeriod.percentage();
}
}
logger.warn("No VAT rate for country {} valid at {}. This is a bug, please report", country, time);
return null;
}
private Map<String, List<VATPeriod>> getMap() {
HashMap<String, List<VATPeriod>> rateMap = new HashMap<>();
Collection<VATCountry> rates = parseResource();
for (VATCountry rate : rates) {
rateMap.put(rate.country(), rate.vatPeriod());
}
return rateMap;
}
private Collection<VATCountry> parseResource() {
try (InputStream inputStream = RateProvider.class.getResourceAsStream(RESOURCE_NAME)) {
if (inputStream == null) {
throw new IllegalStateException("VAT resource not found");
}
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
mapper.registerModule(new JavaTimeModule());
return mapper.readValue(inputStream,
mapper.getTypeFactory().constructCollectionType(List.class, VATCountry.class));
} catch (IOException e) {
throw new IllegalStateException("VAT resource could not be read and parsed", e);
}
}
}

View File

@ -12,8 +12,6 @@
*/
package org.openhab.transform.vat.internal;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.transform.TransformationService;
@ -29,177 +27,4 @@ public class VATTransformationConstants {
public static final ProfileTypeUID PROFILE_TYPE_UID = new ProfileTypeUID(
TransformationService.TRANSFORM_PROFILE_SCOPE, "VAT");
public static final Map<String, String> RATES = Map.ofEntries(
// European Union countries
Map.entry("AT", "20"), // Austria
Map.entry("BE", "21"), // Belgium
Map.entry("BG", "20"), // Bulgaria
Map.entry("HR", "25"), // Croatia
Map.entry("CY", "19"), // Cyprus
Map.entry("CZ", "21"), // Czech Republic
Map.entry("DK", "25"), // Denmark
Map.entry("EE", "20"), // Estonia
Map.entry("FI", "24"), // Finland
Map.entry("FR", "20"), // France
Map.entry("DE", "19"), // Germany
Map.entry("GR", "24"), // Greece
Map.entry("HU", "27"), // Hungary
Map.entry("IE", "23"), // Ireland
Map.entry("IT", "22"), // Italy
Map.entry("LV", "21"), // Latvia
Map.entry("LT", "21"), // Lithuania
Map.entry("LU", "17"), // Luxembourg
Map.entry("MT", "18"), // Malta
Map.entry("NL", "21"), // Netherlands
Map.entry("PL", "23"), // Poland
Map.entry("PT", "23"), // Portugal
Map.entry("RO", "19"), // Romania
Map.entry("SK", "20"), // Slovakia
Map.entry("SI", "22"), // Slovenia
Map.entry("ES", "21"), // Spain
Map.entry("SE", "25"), // Sweden
// Non-European Union countries
Map.entry("AL", "20"), // Albania
Map.entry("DZ", "19"), // Algeria
Map.entry("AD", "4.5"), // Andorra
Map.entry("AO", "14"), // Angola
Map.entry("AG", "15"), // Antigua and Barbuda
Map.entry("AR", "21"), // Argentina
Map.entry("AM", "20"), // Armenia
Map.entry("AU", "10"), // Australia
Map.entry("AZ", "18"), // Azerbaijan
Map.entry("BS", "12"), // Bahamas
Map.entry("BH", "10"), // Bahrain
Map.entry("BD", "15"), // Bangladesh
Map.entry("BB", "17.5"), // Barbados
Map.entry("BY", "20"), // Belarus
Map.entry("BZ", "12.5"), // Belize
Map.entry("BJ", "18"), // Benin
Map.entry("BO", "13"), // Bolivia
Map.entry("BA", "17"), // Bosnia and Herzegovina
Map.entry("BW", "12"), // Botswana
Map.entry("BR", "20"), // Brazil
Map.entry("BF", "18"), // Burkina Faso
Map.entry("BI", "18"), // Burundi
Map.entry("KH", "10"), // Cambodia
Map.entry("CM", "19.25"), // Cameroon
Map.entry("CA", "5"), // Canada
Map.entry("CV", "15"), // Cape Verde
Map.entry("CF", "19"), // Central African Republic
Map.entry("TD", "18"), // Chad
Map.entry("CL", "19"), // Chile
Map.entry("CN", "13"), // China
Map.entry("CO", "19"), // Colombia
Map.entry("CR", "13"), // Costa Rica
Map.entry("CD", "16"), // Democratic Republic of the Congo
Map.entry("DM", "15"), // Dominica
Map.entry("DO", "18"), // Dominican Republic
Map.entry("EC", "12"), // Ecuador
Map.entry("EG", "14"), // Egypt
Map.entry("SV", "13"), // El Salvador
Map.entry("GQ", "15"), // Equatorial Guinea
Map.entry("ET", "15"), // Ethiopia
Map.entry("FO", "25"), // Faroe Islands
Map.entry("FJ", "15"), // Fiji
Map.entry("GA", "18"), // Gabon
Map.entry("GM", "15"), // Gambia
Map.entry("GE", "18"), // Georgia
Map.entry("GH", "15"), // Ghana
Map.entry("GD", "15"), // Grenada
Map.entry("GT", "12"), // Guatemala
Map.entry("GN", "18"), // Guinea
Map.entry("GW", "15"), // Guinea-Bissau
Map.entry("GY", "16"), // Guyana
Map.entry("HT", "10"), // Haiti
Map.entry("HN", "15"), // Honduras
Map.entry("IS", "24"), // Iceland
Map.entry("IN", "5.5"), // India
Map.entry("ID", "11"), // Indonesia
Map.entry("IR", "9"), // Iran
Map.entry("IM", "20"), // Isle of Man
Map.entry("IL", "17"), // Israel
Map.entry("CI", "18"), // Ivory Coast
Map.entry("JM", "12.5"), // Jamaica
Map.entry("JP", "10"), // Japan
Map.entry("JE", "5"), // Jersey
Map.entry("JO", "16"), // Jordan
Map.entry("KZ", "12"), // Kazakhstan
Map.entry("KE", "16"), // Kenya
Map.entry("KG", "20"), // Kyrgyzstan
Map.entry("LA", "10"), // Laos
Map.entry("LB", "11"), // Lebanon
Map.entry("LS", "14"), // Lesotho
Map.entry("LI", "7.7"), // Liechtenstein
Map.entry("MG", "20"), // Madagascar
Map.entry("MW", "16.5"), // Malawi
Map.entry("MY", "6"), // Malaysia
Map.entry("MV", "6"), // Maldives
Map.entry("ML", "18"), // Mali
Map.entry("MR", "14"), // Mauritania
Map.entry("MU", "15"), // Mauritius
Map.entry("MX", "16"), // Mexico
Map.entry("MD", "20"), // Moldova
Map.entry("MC", "19.6"), // Monaco
Map.entry("MN", "10"), // Mongolia
Map.entry("ME", "21"), // Montenegro
Map.entry("MA", "20"), // Morocco
Map.entry("MZ", "17"), // Mozambique
Map.entry("NA", "15"), // Namibia
Map.entry("NP", "13"), // Nepal
Map.entry("NZ", "15"), // New Zealand
Map.entry("NI", "15"), // Nicaragua
Map.entry("NE", "19"), // Niger
Map.entry("NG", "7.5"), // Nigeria
Map.entry("NU", "5"), // Niue
Map.entry("MK", "18"), // North Macedonia
Map.entry("NO", "25"), // Norway
Map.entry("PK", "17"), // Pakistan
Map.entry("PW", "10"), // Palau
Map.entry("PS", "16"), // Palestine
Map.entry("PA", "7"), // Panama
Map.entry("PG", "10"), // Papua New Guinea
Map.entry("PY", "10"), // Paraguay
Map.entry("PE", "18"), // Peru
Map.entry("PH", "12"), // Philippines
Map.entry("CG", "16"), // Republic of Congo
Map.entry("RU", "20"), // Russia
Map.entry("RW", "18"), // Rwanda
Map.entry("KN", "17"), // Saint Kitts and Nevis
Map.entry("VC", "15"), // Saint Vincent and the Grenadines
Map.entry("WS", "15"), // Samoa
Map.entry("SA", "15"), // Saudi Arabia
Map.entry("SN", "18"), // Senegal
Map.entry("RS", "20"), // Serbia
Map.entry("SC", "15"), // Seychelles
Map.entry("SL", "15"), // Sierra Leone
Map.entry("SG", "8"), // Singapore
Map.entry("ZA", "15"), // South Africa
Map.entry("KR", "10"), // South Korea
Map.entry("LK", "12"), // Sri Lanka
Map.entry("SD", "17"), // Sudan
Map.entry("CH", "7.7"), // Switzerland
Map.entry("TW", "5"), // Taiwan
Map.entry("TJ", "20"), // Tajikistan
Map.entry("TZ", "18"), // Tanzania
Map.entry("TH", "10"), // Thailand
Map.entry("TG", "18"), // Togo
Map.entry("TO", "15"), // Tonga
Map.entry("TT", "12.5"), // Trinidad and Tobago
Map.entry("TN", "18"), // Tunisia
Map.entry("TR", "18"), // Turkey
Map.entry("TM", "15"), // Turkmenistan
Map.entry("UG", "18"), // Uganda
Map.entry("UA", "20"), // Ukraine
Map.entry("AE", "5"), // United Arab Emirates
Map.entry("GB", "20"), // United Kingdom
Map.entry("UY", ""), // Uruguay
Map.entry("UZ", "12"), // Uzbekistan
Map.entry("VU", "13"), // Vanuatu
Map.entry("VN", "10"), // Vietnam
Map.entry("VE", "12"), // Venezuela
Map.entry("ZM", "16"), // Zambia
Map.entry("ZW", "15") // Zimbabwe
);
}

View File

@ -12,8 +12,6 @@
*/
package org.openhab.transform.vat.internal;
import static org.openhab.transform.vat.internal.VATTransformationConstants.*;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -36,6 +34,7 @@ import org.slf4j.LoggerFactory;
public class VATTransformationService implements TransformationService {
private final Logger logger = LoggerFactory.getLogger(VATTransformationService.class);
private final RateProvider rateProvider = new RateProvider();
@Override
public @Nullable String transform(String valueString, String sourceString) throws TransformationException {
@ -53,12 +52,11 @@ public class VATTransformationService implements TransformationService {
try {
value = new BigDecimal(valueString);
} catch (NumberFormatException e) {
String rate = RATES.get(valueString);
if (rate == null) {
value = rateProvider.getPercentage(valueString);
if (value == null) {
logger.warn("Input value '{}' could not be converted to a valid number or country code", valueString);
throw new TransformationException("VAT Transformation can only be used with numeric inputs", e);
}
value = new BigDecimal(rate);
}
return addVAT(source, value).toString();

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.transform.vat.internal.model;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* DTO representing a country with VAT rates in different validity periods.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public record VATCountry(String country, @JsonProperty("vatPeriod") List<VATPeriod> vatPeriod) {
@Override
public String toString() {
return "CountryVAT{country='" + country + "', period=" + vatPeriod + '}';
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.transform.vat.internal.model;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* DTO representing a VAT rate in a specific validity period.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public record VATPeriod(Instant start, Instant end, BigDecimal percentage) {
@Override
public Instant start() {
return Objects.isNull(start) ? Instant.MIN : start;
}
@Override
public Instant end() {
return Objects.isNull(end) ? Instant.MAX : end;
}
@Override
public String toString() {
return "VATPeriod{start='" + start() + "', end='" + end() + "', percentage=" + percentage + '}';
}
}

View File

@ -15,6 +15,7 @@ package org.openhab.transform.vat.internal.profile;
import static org.openhab.transform.vat.internal.VATTransformationConstants.*;
import java.math.BigDecimal;
import java.time.Instant;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.LocaleProvider;
@ -32,6 +33,7 @@ import org.openhab.core.types.State;
import org.openhab.core.types.TimeSeries;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
import org.openhab.transform.vat.internal.RateProvider;
import org.openhab.transform.vat.internal.config.VATConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -49,15 +51,17 @@ public class VATTransformationProfile implements TimeSeriesProfile {
private final ProfileCallback callback;
private final TransformationService service;
private final LocaleProvider localeProvider;
private VATConfig configuration;
private final RateProvider rateProvider;
private final VATConfig configuration;
public VATTransformationProfile(final ProfileCallback callback, final TransformationService service,
final ProfileContext context, LocaleProvider localeProvider) {
final ProfileContext context, final LocaleProvider localeProvider, final RateProvider rateProvider) {
this.callback = callback;
this.service = service;
this.localeProvider = localeProvider;
this.configuration = context.getConfiguration().as(VATConfig.class);
this.rateProvider = rateProvider;
configuration = context.getConfiguration().as(VATConfig.class);
}
@Override
@ -76,25 +80,25 @@ public class VATTransformationProfile implements TimeSeriesProfile {
@Override
public void onCommandFromHandler(Command command) {
callback.sendCommand((Command) transformState(command));
callback.sendCommand((Command) transformState(command, Instant.now()));
}
@Override
public void onStateUpdateFromHandler(State state) {
callback.sendUpdate((State) transformState(state));
callback.sendUpdate((State) transformState(state, Instant.now()));
}
@Override
public void onTimeSeriesFromHandler(TimeSeries timeSeries) {
TimeSeries transformedTimeSeries = new TimeSeries(timeSeries.getPolicy());
timeSeries.getStates()
.forEach(entry -> transformedTimeSeries.add(entry.timestamp(), (State) transformState(entry.state())));
timeSeries.getStates().forEach(entry -> transformedTimeSeries.add(entry.timestamp(),
(State) transformState(entry.state(), entry.timestamp())));
callback.sendTimeSeries(transformedTimeSeries);
}
private Type transformState(Type state) {
private Type transformState(Type state, Instant time) {
String result = state.toFullString();
String percentage = getVATPercentage();
String percentage = getVATPercentage(time);
try {
result = TransformationHelper.transform(service, percentage, "%s", result);
} catch (TransformationException e) {
@ -110,23 +114,24 @@ public class VATTransformationProfile implements TimeSeriesProfile {
} else if (state instanceof UnDefType) {
resultType = UnDefType.valueOf(result);
}
logger.debug("Transformed '{}' into '{}'", state, resultType);
logger.debug("Transformed '{}' into '{}' at {}", state, resultType, time);
}
return resultType;
}
private String getVATPercentage() {
private String getVATPercentage(Instant time) {
if (!configuration.percentage.isBlank()) {
return getOverriddenVAT();
}
String country = localeProvider.getLocale().getCountry();
String rate = RATES.get(country);
BigDecimal rate = rateProvider.getPercentage(country, time);
if (rate == null) {
logger.warn("No VAT rate for country {}", country);
logger.warn("No VAT rate for country {} at {}", country, time);
return "0";
}
return rate;
return rate.toString();
}
private String getOverriddenVAT() {

View File

@ -37,6 +37,7 @@ import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.thing.profiles.i18n.ProfileTypeI18nLocalizationService;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.util.BundleResolver;
import org.openhab.transform.vat.internal.RateProvider;
import org.osgi.framework.Bundle;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@ -52,6 +53,7 @@ import org.osgi.service.component.annotations.Reference;
public class VATTransformationProfileFactory implements ProfileFactory, ProfileTypeProvider {
private final LocaleProvider localeProvider;
private final RateProvider rateProvider = new RateProvider();
private final ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService;
private final Map<LocalizedKey, ProfileType> localizedProfileTypeCache = new ConcurrentHashMap<>();
private final Bundle bundle;
@ -76,7 +78,8 @@ public class VATTransformationProfileFactory implements ProfileFactory, ProfileT
@Override
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
ProfileContext profileContext) {
return new VATTransformationProfile(callback, transformationService, profileContext, localeProvider);
return new VATTransformationProfile(callback, transformationService, profileContext, localeProvider,
rateProvider);
}
private ProfileType createLocalizedProfileType(ProfileType profileType, @Nullable Locale locale) {
@ -101,11 +104,11 @@ public class VATTransformationProfileFactory implements ProfileFactory, ProfileT
}
@Reference(target = "(openhab.transform=VAT)")
public void addTransformationService(TransformationService service) {
this.transformationService = service;
public void addTransformationService(TransformationService transformationService) {
this.transformationService = transformationService;
}
public void removeTransformationService(TransformationService service) {
public void removeTransformationService(TransformationService transformationService) {
this.transformationService = null;
}
}

View File

@ -0,0 +1,680 @@
# European Union countries
# Austria
- country: "AT"
vatPeriod:
- percentage: 20
# Belgium
- country: "BE"
vatPeriod:
- percentage: 21
# Bulgaria
- country: "BG"
vatPeriod:
- percentage: 20
# Croatia
- country: "HR"
vatPeriod:
- percentage: 25
# Cyprus
- country: "CY"
vatPeriod:
- percentage: 19
# Czech Republic
- country: "CZ"
vatPeriod:
- percentage: 21
# Denmark
- country: "DK"
vatPeriod:
- percentage: 25
# Estonia
- country: "EE"
vatPeriod:
- percentage: 20
# Finland
- country: "FI"
vatPeriod:
- percentage: 24
# France
- country: "FR"
vatPeriod:
- percentage: 20
# Germany
- country: "DE"
vatPeriod:
- percentage: 19
# Greece
- country: "GR"
vatPeriod:
- percentage: 24
# Hungary
- country: "HU"
vatPeriod:
- percentage: 27
# Ireland
- country: "IE"
vatPeriod:
- percentage: 23
# Italy
- country: "IT"
vatPeriod:
- percentage: 22
# Latvia
- country: "LV"
vatPeriod:
- percentage: 21
# Lithuania
- country: "LT"
vatPeriod:
- percentage: 21
# Luxembourg
- country: "LU"
vatPeriod:
- percentage: 17
# Malta
- country: "MT"
vatPeriod:
- percentage: 18
# Netherlands
- country: "NL"
vatPeriod:
- percentage: 21
# Poland
- country: "PL"
vatPeriod:
- percentage: 23
# Portugal
- country: "PT"
vatPeriod:
- percentage: 23
# Romania
- country: "RO"
vatPeriod:
- percentage: 19
# Slovakia
- country: "SK"
vatPeriod:
- percentage: 20
# Slovenia
- country: "SI"
vatPeriod:
- percentage: 22
# Spain
- country: "ES"
vatPeriod:
- percentage: 21
# Sweden
- country: "SE"
vatPeriod:
- percentage: 25
# Non-European Union countries
# Albania
- country: "AL"
vatPeriod:
- percentage: 20
# Algeria
- country: "DZ"
vatPeriod:
- percentage: 19
# Andorra
- country: "AD"
vatPeriod:
- percentage: 4.5
# Angola
- country: "AO"
vatPeriod:
- percentage: 14
# Antigua and Barbuda
- country: "AG"
vatPeriod:
- percentage: 15
# Argentina
- country: "AR"
vatPeriod:
- percentage: 21
# Armenia
- country: "AM"
vatPeriod:
- percentage: 20
# Australia
- country: "AU"
vatPeriod:
- percentage: 10
# Azerbaijan
- country: "AZ"
vatPeriod:
- percentage: 18
# Bahamas
- country: "BS"
vatPeriod:
- percentage: 12
# Bahrain
- country: "BH"
vatPeriod:
- percentage: 10
# Bangladesh
- country: "BD"
vatPeriod:
- percentage: 15
# Barbados
- country: "BB"
vatPeriod:
- percentage: 17.5
# Belarus
- country: "BY"
vatPeriod:
- percentage: 20
# Belize
- country: "BZ"
vatPeriod:
- percentage: 12.5
# Benin
- country: "BJ"
vatPeriod:
- percentage: 18
# Bolivia
- country: "BO"
vatPeriod:
- percentage: 13
# Bosnia and Herzegovina
- country: "BA"
vatPeriod:
- percentage: 17
# Botswana
- country: "BW"
vatPeriod:
- percentage: 12
# Brazil
- country: "BR"
vatPeriod:
- percentage: 20
# Burkina Faso
- country: "BF"
vatPeriod:
- percentage: 18
# Burundi
- country: "BI"
vatPeriod:
- percentage: 18
# Cambodia
- country: "KH"
vatPeriod:
- percentage: 10
# Cameroon
- country: "CM"
vatPeriod:
- percentage: 19.25
# Canada
- country: "CA"
vatPeriod:
- percentage: 5
# Cape Verde
- country: "CV"
vatPeriod:
- percentage: 15
# Central African Republic
- country: "CF"
vatPeriod:
- percentage: 19
# Chad
- country: "TD"
vatPeriod:
- percentage: 18
# Chile
- country: "CL"
vatPeriod:
- percentage: 19
# China
- country: "CN"
vatPeriod:
- percentage: 13
# Colombia
- country: "CO"
vatPeriod:
- percentage: 19
# Costa Rica
- country: "CR"
vatPeriod:
- percentage: 13
# Democratic Republic of the Congo
- country: "CD"
vatPeriod:
- percentage: 16
# Dominica
- country: "DM"
vatPeriod:
- percentage: 15
# Dominican Republic
- country: "DO"
vatPeriod:
- percentage: 18
# Ecuador
- country: "EC"
vatPeriod:
- percentage: 12
# Egypt
- country: "EG"
vatPeriod:
- percentage: 14
# El Salvador
- country: "SV"
vatPeriod:
- percentage: 13
# Equatorial Guinea
- country: "GQ"
vatPeriod:
- percentage: 15
# Ethiopia
- country: "ET"
vatPeriod:
- percentage: 15
# Faroe Islands
- country: "FO"
vatPeriod:
- percentage: 25
# Fiji
- country: "FJ"
vatPeriod:
- percentage: 15
# Gabon
- country: "GA"
vatPeriod:
- percentage: 18
# Gambia
- country: "GM"
vatPeriod:
- percentage: 15
# Georgia
- country: "GE"
vatPeriod:
- percentage: 18
# Ghana
- country: "GH"
vatPeriod:
- percentage: 15
# Grenada
- country: "GD"
vatPeriod:
- percentage: 15
# Guatemala
- country: "GT"
vatPeriod:
- percentage: 12
# Guinea
- country: "GN"
vatPeriod:
- percentage: 18
# Guinea-Bissau
- country: "GW"
vatPeriod:
- percentage: 15
# Guyana
- country: "GY"
vatPeriod:
- percentage: 16
# Haiti
- country: "HT"
vatPeriod:
- percentage: 10
# Honduras
- country: "HN"
vatPeriod:
- percentage: 15
# Iceland
- country: "IS"
vatPeriod:
- percentage: 24
# India
- country: "IN"
vatPeriod:
- percentage: 5.5
# Indonesia
- country: "ID"
vatPeriod:
- start: null
end: 2024-12-31T17:00:00Z
percentage: 11
- start: 2024-12-31T17:00:00Z
end: null
percentage: 12
# Iran
- country: "IR"
vatPeriod:
- percentage: 9
# Isle of Man
- country: "IM"
vatPeriod:
- percentage: 20
# Israel
- country: "IL"
vatPeriod:
- start: null
end: 2024-12-31T22:00:00Z
percentage: 17
- start: 2024-12-31T22:00:00Z
end: null
percentage: 18
# Ivory Coast
- country: "CI"
vatPeriod:
- percentage: 18
# Jamaica
- country: "JM"
vatPeriod:
- percentage: 12.5
# Japan
- country: "JP"
vatPeriod:
- percentage: 10
# Jersey
- country: "JE"
vatPeriod:
- percentage: 5
# Jordan
- country: "JO"
vatPeriod:
- percentage: 16
# Kazakhstan
- country: "KZ"
vatPeriod:
- percentage: 12
# Kenya
- country: "KE"
vatPeriod:
- percentage: 16
# Kyrgyzstan
- country: "KG"
vatPeriod:
- percentage: 20
# Laos
- country: "LA"
vatPeriod:
- percentage: 10
# Lebanon
- country: "LB"
vatPeriod:
- percentage: 11
# Lesotho
- country: "LS"
vatPeriod:
- percentage: 14
# Liechtenstein
- country: "LI"
vatPeriod:
- percentage: 7.7
# Madagascar
- country: "MG"
vatPeriod:
- percentage: 20
# Malawi
- country: "MW"
vatPeriod:
- percentage: 16.5
# Malaysia
- country: "MY"
vatPeriod:
- percentage: 6
# Maldives
- country: "MV"
vatPeriod:
- percentage: 6
# Mali
- country: "ML"
vatPeriod:
- percentage: 18
# Mauritania
- country: "MR"
vatPeriod:
- percentage: 14
# Mauritius
- country: "MU"
vatPeriod:
- percentage: 15
# Mexico
- country: "MX"
vatPeriod:
- percentage: 16
# Moldova
- country: "MD"
vatPeriod:
- percentage: 20
# Monaco
- country: "MC"
vatPeriod:
- percentage: 19.6
# Mongolia
- country: "MN"
vatPeriod:
- percentage: 10
# Montenegro
- country: "ME"
vatPeriod:
- percentage: 21
# Morocco
- country: "MA"
vatPeriod:
- percentage: 20
# Mozambique
- country: "MZ"
vatPeriod:
- percentage: 17
# Namibia
- country: "NA"
vatPeriod:
- percentage: 15
# Nepal
- country: "NP"
vatPeriod:
- percentage: 13
# New Zealand
- country: "NZ"
vatPeriod:
- percentage: 15
# Nicaragua
- country: "NI"
vatPeriod:
- percentage: 15
# Niger
- country: "NE"
vatPeriod:
- percentage: 19
# Nigeria
- country: "NG"
vatPeriod:
- percentage: 7.5
# Niue
- country: "NU"
vatPeriod:
- percentage: 5
# North Macedonia
- country: "MK"
vatPeriod:
- percentage: 18
# Norway
- country: "NO"
vatPeriod:
- percentage: 25
# Pakistan
- country: "PK"
vatPeriod:
- percentage: 17
# Palau
- country: "PW"
vatPeriod:
- percentage: 10
# Palestine
- country: "PS"
vatPeriod:
- percentage: 16
# Panama
- country: "PA"
vatPeriod:
- percentage: 7
# Papua New Guinea
- country: "PG"
vatPeriod:
- percentage: 10
# Paraguay
- country: "PY"
vatPeriod:
- percentage: 10
# Peru
- country: "PE"
vatPeriod:
- percentage: 18
# Philippines
- country: "PH"
vatPeriod:
- percentage: 12
# Republic of Congo
- country: "CG"
vatPeriod:
- percentage: 16
# Russia
- country: "RU"
vatPeriod:
- percentage: 20
# Rwanda
- country: "RW"
vatPeriod:
- percentage: 18
# Saint Kitts and Nevis
- country: "KN"
vatPeriod:
- percentage: 17
# Saint Vincent and the Grenadines
- country: "VC"
vatPeriod:
- percentage: 15
# Samoa
- country: "WS"
vatPeriod:
- percentage: 15
# Saudi Arabia
- country: "SA"
vatPeriod:
- percentage: 15
# Senegal
- country: "SN"
vatPeriod:
- percentage: 18
# Serbia
- country: "RS"
vatPeriod:
- percentage: 20
# Seychelles
- country: "SC"
vatPeriod:
- percentage: 15
# Sierra Leone
- country: "SL"
vatPeriod:
- percentage: 15
# Singapore
- country: "SG"
vatPeriod:
- percentage: 8
# South Africa
- country: "ZA"
vatPeriod:
- percentage: 15
# South Korea
- country: "KR"
vatPeriod:
- percentage: 10
# Sri Lanka
- country: "LK"
vatPeriod:
- percentage: 12
# Sudan
- country: "SD"
vatPeriod:
- percentage: 17
# Switzerland
- country: "CH"
vatPeriod:
- percentage: 7.7
# Taiwan
- country: "TW"
vatPeriod:
- percentage: 5
# Tajikistan
- country: "TJ"
vatPeriod:
- percentage: 20
# Tanzania
- country: "TZ"
vatPeriod:
- percentage: 18
# Thailand
- country: "TH"
vatPeriod:
- percentage: 10
# Togo
- country: "TG"
vatPeriod:
- percentage: 18
# Tonga
- country: "TO"
vatPeriod:
- percentage: 15
# Trinidad and Tobago
- country: "TT"
vatPeriod:
- percentage: 12.5
# Tunisia
- country: "TN"
vatPeriod:
- percentage: 18
# Turkey
- country: "TR"
vatPeriod:
- percentage: 18
# Turkmenistan
- country: "TM"
vatPeriod:
- percentage: 15
# Uganda
- country: "UG"
vatPeriod:
- percentage: 18
# Ukraine
- country: "UA"
vatPeriod:
- percentage: 20
# United Arab Emirates
- country: "AE"
vatPeriod:
- percentage: 5
# United Kingdom
- country: "GB"
vatPeriod:
- percentage: 20
# Uruguay
- country: "UY"
vatPeriod:
- percentage: 22
# Uzbekistan
- country: "UZ"
vatPeriod:
- percentage: 12
# Vanuatu
- country: "VU"
vatPeriod:
- percentage: 13
# Vietnam
- country: "VN"
vatPeriod:
- percentage: 10
# Venezuela
- country: "VE"
vatPeriod:
- percentage: 12
# Zambia
- country: "ZM"
vatPeriod:
- percentage: 16
# Zimbabwe
- country: "ZW"
vatPeriod:
- percentage: 15

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.transform.vat.internal;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link RateProvider}.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class RateProviderTest {
@Test
void getPercentageWhenNoPeriods() {
RateProvider rateProvider = new RateProvider();
Instant time = LocalDateTime.of(2024, 10, 27, 22, 5, 0).atZone(ZoneId.of("Europe/Copenhagen")).toInstant();
@Nullable
BigDecimal actual = rateProvider.getPercentage("DK", time);
assertThat(actual, is(equalTo(new BigDecimal(25))));
}
@Test
void getPercentageJustBeforeNewRateComesIntoEffect() {
RateProvider rateProvider = new RateProvider();
Instant time = LocalDateTime.of(2025, 1, 1, 0, 0, 0).minusNanos(1).atZone(ZoneId.of("Asia/Jerusalem"))
.toInstant();
@Nullable
BigDecimal actual = rateProvider.getPercentage("IL", time);
assertThat(actual, is(equalTo(new BigDecimal(17))));
}
@Test
void getPercentageAtMomentOfNewRateComingIntoEffect() {
RateProvider rateProvider = new RateProvider();
Instant time = LocalDateTime.of(2025, 1, 1, 0, 0, 0).atZone(ZoneId.of("Asia/Jerusalem")).toInstant();
@Nullable
BigDecimal actual = rateProvider.getPercentage("IL", time);
assertThat(actual, is(equalTo(new BigDecimal(18))));
}
}