From 7b8f5bdbe679ebeab4a03dc3103770dce5cb1b55 Mon Sep 17 00:00:00 2001 From: Celestino Bellone Date: Sat, 19 May 2018 20:33:40 +0200 Subject: [PATCH] #432 Follow EU VAT Rules (update) (cherry picked from commit 5fe797a) --- .../controller/ReservationController.java | 3 -- src/main/java/alfio/manager/EuVatChecker.java | 27 +++++++++++--- .../alfio/model/system/ConfigurationKeys.java | 1 + .../resources/alfio/i18n/public.properties | 3 +- .../resources/alfio/i18n/public_de.properties | 1 + .../resources/alfio/i18n/public_fr.properties | 3 +- .../resources/alfio/i18n/public_it.properties | 1 + .../resources/alfio/i18n/public_nl.properties | 1 + .../templates/event/reservation-page.ms | 8 ++--- .../resources/js/event/reservation-page.js | 2 +- .../java/alfio/manager/EuVatCheckerTest.java | 36 +++++++++++++++++-- 11 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/main/java/alfio/controller/ReservationController.java b/src/main/java/alfio/controller/ReservationController.java index 4b1cd4e38a..69b6808a1e 100644 --- a/src/main/java/alfio/controller/ReservationController.java +++ b/src/main/java/alfio/controller/ReservationController.java @@ -28,7 +28,6 @@ import alfio.model.TicketReservation.TicketReservationStatus; import alfio.model.result.ValidationResult; import alfio.model.system.Configuration; -import alfio.model.system.ConfigurationKeys; import alfio.model.transaction.PaymentProxy; import alfio.model.user.Organization; import alfio.repository.EventRepository; @@ -60,7 +59,6 @@ import java.util.*; import java.util.stream.Collectors; -import static alfio.model.system.Configuration.getSystemConfiguration; import static alfio.model.system.ConfigurationKeys.*; import static java.util.stream.Collectors.toList; @@ -168,7 +166,6 @@ public String showPaymentPage(@PathVariable("eventName") String eventName, .addAttribute("expressCheckoutEnabled", isExpressCheckoutEnabled(event, orderSummary)) .addAttribute("useFirstAndLastName", event.mustUseFirstAndLastName()) .addAttribute("countries", TicketHelper.getLocalizedCountries(locale)) - .addAttribute("euCountries", TicketHelper.getLocalizedEUCountries(locale, configurationManager.getRequiredValue(getSystemConfiguration(ConfigurationKeys.EU_COUNTRIES_LIST)))) .addAttribute("euVatCheckingEnabled", vatChecker.isVatCheckingEnabledFor(event.getOrganizationId())) .addAttribute("invoiceIsAllowed", invoiceAllowed) .addAttribute("vatNrIsLinked", orderSummary.isVatExempt() || paymentForm.getHasVatCountryCode()) diff --git a/src/main/java/alfio/manager/EuVatChecker.java b/src/main/java/alfio/manager/EuVatChecker.java index 737d5ea357..695c821fdf 100644 --- a/src/main/java/alfio/manager/EuVatChecker.java +++ b/src/main/java/alfio/manager/EuVatChecker.java @@ -22,6 +22,7 @@ import alfio.model.system.ConfigurationKeys; import alfio.util.Json; import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.log4j.Log4j2; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -33,8 +34,13 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; +import java.util.function.Supplier; + +import static alfio.model.system.Configuration.getSystemConfiguration; +import static alfio.model.system.ConfigurationKeys.APPLY_VAT_FOREIGN_BUSINESS; @Component +@Log4j2 public class EuVatChecker { private final ConfigurationManager configurationManager; @@ -54,7 +60,16 @@ public Optional checkVat(String vatNr, String countryCode, int organi static BiFunction> performCheck(String vatNr, String countryCode, int organizationId) { return (configurationManager, client) -> { - if(StringUtils.isNotEmpty(vatNr) && StringUtils.length(countryCode) == 2 && checkingEnabled(configurationManager, organizationId)) { + boolean vatNrNotEmpty = StringUtils.isNotEmpty(vatNr); + boolean validCountryCode = StringUtils.length(StringUtils.trimToNull(countryCode)) == 2; + + if(!vatNrNotEmpty || !validCountryCode) { + return Optional.empty(); + } + + boolean euCountryCode = configurationManager.getRequiredValue(getSystemConfiguration(ConfigurationKeys.EU_COUNTRIES_LIST)).contains(countryCode); + + if(euCountryCode && checkingEnabled(configurationManager, organizationId)) { Request request = new Request.Builder() .url(apiAddress(configurationManager) + "?country="+countryCode.toUpperCase()+"&number="+vatNr) .get() @@ -66,10 +81,14 @@ static BiFunction> perfo return Optional.empty(); } } catch (IOException e) { - e.printStackTrace(); + log.warn("Error while calling VAT NR check.", e); + return Optional.empty(); } + } else { + String organizerCountry = organizerCountry(configurationManager, organizationId); + Supplier applyVatToForeignBusiness = () -> configurationManager.getBooleanConfigValue(Configuration.from(organizationId, APPLY_VAT_FOREIGN_BUSINESS), true); + return Optional.of(new VatDetail(vatNr, countryCode, true, "", "", !organizerCountry.equals(countryCode) && !applyVatToForeignBusiness.get())); } - return Optional.empty(); }; } @@ -82,7 +101,7 @@ private static VatDetail getVatDetail(Response resp, String vatNr, String countr } private static String apiAddress(ConfigurationManager configurationManager) { - return configurationManager.getStringConfigValue(Configuration.getSystemConfiguration(ConfigurationKeys.EU_VAT_API_ADDRESS), null); + return configurationManager.getStringConfigValue(getSystemConfiguration(ConfigurationKeys.EU_VAT_API_ADDRESS), null); } private static String organizerCountry(ConfigurationManager configurationManager, int organizationId) { diff --git a/src/main/java/alfio/model/system/ConfigurationKeys.java b/src/main/java/alfio/model/system/ConfigurationKeys.java index cc70c22354..e54543a05a 100644 --- a/src/main/java/alfio/model/system/ConfigurationKeys.java +++ b/src/main/java/alfio/model/system/ConfigurationKeys.java @@ -134,6 +134,7 @@ public enum ConfigurationKeys { INVOICE_NUMBER_PATTERN("Invoice number pattern, example: INVOICE-%d", false, SettingCategory.INVOICE, ComponentType.TEXT, false, EnumSet.of(SYSTEM, ORGANIZATION, EVENT), true), INVOICE_ADDRESS("Invoice address", false, SettingCategory.INVOICE, ComponentType.TEXTAREA, false, EnumSet.of(SYSTEM, ORGANIZATION, EVENT), true), ENABLE_EU_VAT_DIRECTIVE("Enable EU VAT handling for EU Companies", false, SettingCategory.INVOICE_EU, ComponentType.BOOLEAN, false, EnumSet.of(SYSTEM, ORGANIZATION), false), + APPLY_VAT_FOREIGN_BUSINESS("Apply VAT for non-EU B2B customers (default true)", false, SettingCategory.INVOICE_EU, ComponentType.BOOLEAN, false, EnumSet.of(SYSTEM, ORGANIZATION), true), COUNTRY_OF_BUSINESS("The Country where the organizer runs its Business (can differ from event location)", false, SettingCategory.INVOICE_EU, ComponentType.LIST, false, EnumSet.of(SYSTEM, ORGANIZATION), false), EU_COUNTRIES_LIST("EU Countries", true, SettingCategory.INVOICE_EU, ComponentType.LIST, false, EnumSet.of(SYSTEM), false), EU_VAT_API_ADDRESS("EU VAT API address", false, SettingCategory.INVOICE_EU, ComponentType.TEXT, false, EnumSet.of(SYSTEM), false), diff --git a/src/main/resources/alfio/i18n/public.properties b/src/main/resources/alfio/i18n/public.properties index daecd20b70..a8f100adf9 100644 --- a/src/main/resources/alfio/i18n/public.properties +++ b/src/main/resources/alfio/i18n/public.properties @@ -354,4 +354,5 @@ invoice.title=Invoice invoice.refund=This invoice has been updated after the cancellation of one or more tickets and the refund of {0}. reservation-page.privacy.prefix=I have read and agree to the reservation-page.privacy.link.text=privacy policy -reservation-page.privacy.suffix=. \ No newline at end of file +reservation-page.privacy.suffix=. +reservation-page.country.select=Please select the Country \ No newline at end of file diff --git a/src/main/resources/alfio/i18n/public_de.properties b/src/main/resources/alfio/i18n/public_de.properties index 89e9f78a65..1213cf77cc 100644 --- a/src/main/resources/alfio/i18n/public_de.properties +++ b/src/main/resources/alfio/i18n/public_de.properties @@ -325,6 +325,7 @@ reservation-page.i-need-an-invoice=Ich m\u00F6chte eine Rechnung bekommen invoice.validate.vat={0}-Nr. validieren show-event.donations=Spenden reservation-page.country.outside-eu=Au\u00DFerhalb der EU +reservation-page.country.select=Bitte w\u00E4hlen Sie ein Land show-event.mandatoryOneForTicket=Zuschlag, 1 pro Ticket invoice.vat-voided=Mehrwertsteuer nicht inbegriffen, gem\u00E4ss {0} Richtilinien reservation-page.time-for-completion.labels.singular=| Sekunde| Minute| Stunde| Tag| Woche| Monat| Jahr||| diff --git a/src/main/resources/alfio/i18n/public_fr.properties b/src/main/resources/alfio/i18n/public_fr.properties index 389e592bbb..bb8057b1bf 100644 --- a/src/main/resources/alfio/i18n/public_fr.properties +++ b/src/main/resources/alfio/i18n/public_fr.properties @@ -354,4 +354,5 @@ invoice.title=Facture invoice.refund=Cette facture a \u00E9t\u00E9 modifi\u00E9e suite \u00E0 l'annulation d'un ou de plusieurs billets et le remboursement de {0}. reservation-page.privacy.prefix=Vous avez lu et acc\u00E9ptez les reservation-page.privacy.link.text=r\u00E8gles de confidentialit\u00E9 -reservation-page.privacy.suffix=. \ No newline at end of file +reservation-page.privacy.suffix=. +reservation-page.country.select=veuillez s\u00E9lectionner le pays \ No newline at end of file diff --git a/src/main/resources/alfio/i18n/public_it.properties b/src/main/resources/alfio/i18n/public_it.properties index d4506f5dd0..d4c84f726a 100644 --- a/src/main/resources/alfio/i18n/public_it.properties +++ b/src/main/resources/alfio/i18n/public_it.properties @@ -339,3 +339,4 @@ invoice.refund=Questa fattura \u00E8 stata aggiornata dopo la cancellazione di u reservation-page.privacy.prefix=Ho letto ed accetto reservation-page.privacy.link.text=l''informativa sulla privacy reservation-page.privacy.suffix=. +reservation-page.country.select=Seleziona una nazione diff --git a/src/main/resources/alfio/i18n/public_nl.properties b/src/main/resources/alfio/i18n/public_nl.properties index 8c22c55c5c..e74dc355fc 100644 --- a/src/main/resources/alfio/i18n/public_nl.properties +++ b/src/main/resources/alfio/i18n/public_nl.properties @@ -349,3 +349,4 @@ invoice.refund=Dit factuur is geupdate na de annulering van een of meer tickets reservation-page.privacy.prefix=Ik ga akkoord met de reservation-page.privacy.link.text=Privacybeleid reservation-page.privacy.suffix=. +reservation-page.country.select=Selecteer alstublieft een land diff --git a/src/main/webapp/WEB-INF/templates/event/reservation-page.ms b/src/main/webapp/WEB-INF/templates/event/reservation-page.ms index 0fcd369347..6c55392fcc 100644 --- a/src/main/webapp/WEB-INF/templates/event/reservation-page.ms +++ b/src/main/webapp/WEB-INF/templates/event/reservation-page.ms @@ -270,11 +270,11 @@
- + + {{#countries}} - {{/euCountries}} + {{/countries}}
diff --git a/src/main/webapp/resources/js/event/reservation-page.js b/src/main/webapp/resources/js/event/reservation-page.js index 93f3837222..86979bbd7d 100644 --- a/src/main/webapp/resources/js/event/reservation-page.js +++ b/src/main/webapp/resources/js/event/reservation-page.js @@ -313,7 +313,7 @@ } } else { element.find('.field-required').attr('required', false); - $('#billing-address').attr('required', false,).attr('disabled'); + $('#billing-address').attr('required', false).attr('disabled'); element.addClass('hidden'); disableBillingFields(); } diff --git a/src/test/java/alfio/manager/EuVatCheckerTest.java b/src/test/java/alfio/manager/EuVatCheckerTest.java index 8eb6b60ae0..99ecea5a69 100644 --- a/src/test/java/alfio/manager/EuVatCheckerTest.java +++ b/src/test/java/alfio/manager/EuVatCheckerTest.java @@ -31,8 +31,11 @@ import java.util.Optional; import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.when; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class EuVatCheckerTest { @@ -48,8 +51,9 @@ public class EuVatCheckerTest { private ConfigurationManager configurationManager; @Before - public void init() throws IOException { + public void init() { when(configurationManager.getBooleanConfigValue(eq(Configuration.from(1, ConfigurationKeys.ENABLE_EU_VAT_DIRECTIVE)), anyBoolean())).thenReturn(true); + when(configurationManager.getRequiredValue(Configuration.getSystemConfiguration(ConfigurationKeys.EU_COUNTRIES_LIST))).thenReturn("IE"); when(configurationManager.getStringConfigValue(eq(Configuration.from(1, ConfigurationKeys.COUNTRY_OF_BUSINESS)), anyString())).thenReturn("IT"); when(configurationManager.getStringConfigValue(eq(Configuration.getSystemConfiguration(ConfigurationKeys.EU_VAT_API_ADDRESS)), anyString())).thenReturn("http://localhost:8080"); when(client.newCall(any())).thenReturn(call); @@ -90,6 +94,32 @@ public void performCheckRequestFailed() throws IOException { assertFalse(result.isPresent()); } + @Test + public void testForeignBusinessVATApplied() { + when(configurationManager.getBooleanConfigValue(Configuration.from(1, ConfigurationKeys.APPLY_VAT_FOREIGN_BUSINESS), true)).thenReturn(true); + Optional result = EuVatChecker.performCheck("1234", "UK", 1).apply(configurationManager, client); + assertTrue(result.isPresent()); + VatDetail vatDetail = result.get(); + assertTrue(vatDetail.isValid()); + assertFalse(vatDetail.isVatExempt()); + assertEquals("1234", vatDetail.getVatNr()); + assertEquals("UK", vatDetail.getCountry()); + verify(client, never()).newCall(any()); + } + + @Test + public void testForeignBusinessVATNotApplied() { + when(configurationManager.getBooleanConfigValue(Configuration.from(1, ConfigurationKeys.APPLY_VAT_FOREIGN_BUSINESS), true)).thenReturn(false); + Optional result = EuVatChecker.performCheck("1234", "UK", 1).apply(configurationManager, client); + assertTrue(result.isPresent()); + VatDetail vatDetail = result.get(); + assertTrue(vatDetail.isValid()); + assertTrue(vatDetail.isVatExempt()); + assertEquals("1234", vatDetail.getVatNr()); + assertEquals("UK", vatDetail.getCountry()); + verify(client, never()).newCall(any()); + } + private void initResponse(int status, String body) throws IOException { Request request = new Request.Builder().url("http://localhost:8080").get().build(); Response response = new Response.Builder().request(request).protocol(Protocol.HTTP_1_1)