Skip to content

Commit

Permalink
#432 Follow EU VAT Rules (update)
Browse files Browse the repository at this point in the history
(cherry picked from commit 5fe797a)
  • Loading branch information
cbellone committed May 19, 2018
1 parent 8e422af commit 7b8f5bd
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 17 deletions.
3 changes: 0 additions & 3 deletions src/main/java/alfio/controller/ReservationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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())
Expand Down
27 changes: 23 additions & 4 deletions src/main/java/alfio/manager/EuVatChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -54,7 +60,16 @@ public Optional<VatDetail> checkVat(String vatNr, String countryCode, int organi

static BiFunction<ConfigurationManager, OkHttpClient, Optional<VatDetail>> 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()
Expand All @@ -66,10 +81,14 @@ static BiFunction<ConfigurationManager, OkHttpClient, Optional<VatDetail>> 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<Boolean> 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();
};
}

Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/main/java/alfio/model/system/ConfigurationKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/alfio/i18n/public.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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=.
reservation-page.privacy.suffix=.
reservation-page.country.select=Please select the Country
1 change: 1 addition & 0 deletions src/main/resources/alfio/i18n/public_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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|||
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/alfio/i18n/public_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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=.
reservation-page.privacy.suffix=.
reservation-page.country.select=veuillez s\u00E9lectionner le pays
1 change: 1 addition & 0 deletions src/main/resources/alfio/i18n/public_it.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/main/resources/alfio/i18n/public_nl.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions src/main/webapp/WEB-INF/templates/event/reservation-page.ms
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,11 @@
<div class="col-xs-12 col-sm-4">
<div class="form-group">
<label for="vatCountry">{{#i18n}}reservation-page-complete.country{{/i18n}}</label>
<select name="vatCountryCode" id="vatCountry" value="{{paymentForm.vatCountryCode}}" class="form-control">
<option value="">{{#i18n}}reservation-page.country.outside-eu{{/i18n}}</option>
{{#euCountries}}
<select name="vatCountryCode" id="vatCountry" value="{{paymentForm.vatCountryCode}}" class="form-control field-required">
<option value="">{{#i18n}}reservation-page.country.select{{/i18n}}</option>
{{#countries}}
<option value="{{left}}">{{right}}</option>
{{/euCountries}}
{{/countries}}
</select>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/main/webapp/resources/js/event/reservation-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
36 changes: 33 additions & 3 deletions src/test/java/alfio/manager/EuVatCheckerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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<VatDetail> 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<VatDetail> 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)
Expand Down

0 comments on commit 7b8f5bd

Please sign in to comment.