Skip to content

Commit

Permalink
implement #468 switch euvatchecker v2 (#470)
Browse files Browse the repository at this point in the history
* #468 initial vat checker integration

* #468 handle correctly when vies is down
  • Loading branch information
syjer authored and cbellone committed Jun 21, 2018
1 parent 273cec4 commit 9a01f9a
Show file tree
Hide file tree
Showing 14 changed files with 62 additions and 80 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ dependencies {
compile 'com.github.ben-manes.caffeine:caffeine:2.6.1'
compile 'de.danielbechler:java-object-diff:0.95'
compile 'com.github.scribejava:scribejava-core:5.0.0'
compile 'ch.digitalfondue.vatchecker:vatchecker:1.1'

testCompile "org.springframework:spring-test:$springVersion"
testCompile "com.insightfullogic:lambda-behave:0.3"
Expand Down
16 changes: 11 additions & 5 deletions src/main/java/alfio/controller/api/ReservationApiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,17 @@ public ResponseEntity<VatDetail> validateEUVat(@PathVariable("eventName") String
HttpServletRequest request) {

String country = paymentForm.getVatCountryCode();
Optional<Triple<Event, TicketReservation, VatDetail>> vatDetail = eventRepository.findOptionalByShortName(eventName)
.flatMap(e -> ticketReservationRepository.findOptionalReservationById(reservationId).map(r -> Pair.of(e, r)))
.filter(e -> EnumSet.of(INCLUDED, NOT_INCLUDED).contains(e.getKey().getVatStatus()))
.filter(e -> vatChecker.isVatCheckingEnabledFor(e.getKey().getOrganizationId()))
.flatMap(e -> vatChecker.checkVat(paymentForm.getVatNr(), country, e.getKey().getOrganizationId()).map(vd -> Triple.of(e.getLeft(), e.getRight(), vd)));
Optional<Triple<Event, TicketReservation, VatDetail>> vatDetail;
try {
vatDetail = eventRepository.findOptionalByShortName(eventName)
.flatMap(e -> ticketReservationRepository.findOptionalReservationById(reservationId).map(r -> Pair.of(e, r)))
.filter(e -> EnumSet.of(INCLUDED, NOT_INCLUDED).contains(e.getKey().getVatStatus()))
.filter(e -> vatChecker.isVatCheckingEnabledFor(e.getKey().getOrganizationId()))
.flatMap(e -> vatChecker.checkVat(paymentForm.getVatNr(), country, e.getKey().getOrganizationId()).map(vd -> Triple.of(e.getLeft(), e.getRight(), vd)));
} catch (IllegalStateException e) {
return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
}


vatDetail
.filter(t -> t.getRight().isValid())
Expand Down
58 changes: 14 additions & 44 deletions src/main/java/alfio/manager/EuVatChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,16 @@
import alfio.model.system.Configuration;
import alfio.model.system.ConfigurationKeys;
import alfio.repository.AuditingRepository;
import alfio.util.Json;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.digitalfondue.vatchecker.EUVatCheckResponse;
import ch.digitalfondue.vatchecker.EUVatChecker;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
Expand All @@ -55,9 +50,9 @@ public class EuVatChecker {

private final ConfigurationManager configurationManager;
private final AuditingRepository auditingRepository;
private final OkHttpClient client = new OkHttpClient();
private final EUVatChecker client = new EUVatChecker();

private static final Cache<Pair<String, String>, Map<String, String>> validationCache = Caffeine.newBuilder()
private static final Cache<Pair<String, String>, EUVatCheckResponse> validationCache = Caffeine.newBuilder()
.expireAfterWrite(15, TimeUnit.MINUTES)
.build();

Expand All @@ -69,7 +64,7 @@ public Optional<VatDetail> checkVat(String vatNr, String countryCode, int organi
return performCheck(vatNr, countryCode, organizationId).apply(configurationManager, client);
}

static BiFunction<ConfigurationManager, OkHttpClient, Optional<VatDetail>> performCheck(String vatNr, String countryCode, int organizationId) {
static BiFunction<ConfigurationManager, EUVatChecker, Optional<VatDetail>> performCheck(String vatNr, String countryCode, int organizationId) {
return (configurationManager, client) -> {
boolean vatNrNotEmpty = StringUtils.isNotEmpty(vatNr);
boolean validCountryCode = StringUtils.length(StringUtils.trimToNull(countryCode)) == 2;
Expand All @@ -82,8 +77,7 @@ static BiFunction<ConfigurationManager, OkHttpClient, Optional<VatDetail>> perfo
boolean euCountryCode = configurationManager.getRequiredValue(getSystemConfiguration(ConfigurationKeys.EU_COUNTRIES_LIST)).contains(countryCode);

if(euCountryCode && validationEnabled(configurationManager, organizationId)) {
final Pair<String, String> cacheKey = Pair.of(vatNr, countryCode);
Map<String, String> validationResult = validateEUVat(vatNr, countryCode, configurationManager, client);
EUVatCheckResponse validationResult = validateEUVat(vatNr, countryCode, client);
return Optional.ofNullable(validationResult)
.map(r -> getVatDetail(reverseChargeEnabled(configurationManager, organizationId), r, vatNr, countryCode, organizerCountry(configurationManager, organizationId)));
} else {
Expand All @@ -94,42 +88,18 @@ static BiFunction<ConfigurationManager, OkHttpClient, Optional<VatDetail>> perfo
};
}

private static Map<String, String> validateEUVat(String vat, String countryCode, ConfigurationManager configurationManager, OkHttpClient client) {
private static EUVatCheckResponse validateEUVat(String vat, String countryCode, EUVatChecker client) {

if(StringUtils.isEmpty(vat) || StringUtils.length(countryCode) != 2) {
return null;
}

return validationCache.get(Pair.of(vat, countryCode), k -> {
Request request = new Request.Builder()
.url(apiAddress(configurationManager) + "?country=" + countryCode.toUpperCase() + "&number=" + vat)
.get()
.build();
try (Response resp = client.newCall(request).execute()) {
if (resp.isSuccessful()) {
ResponseBody body = resp.body();
return body != null ? Json.fromJson(body.string(), new TypeReference<Map<String, String>>() {}) : null;
} else {
return null;
}
} catch (IOException e) {
log.warn("Error while calling VAT NR check.", e);
return null;
}
});
}

private static VatDetail getVatDetail(boolean reverseChargeEnabled, Map<String, String> response, String vatNr, String countryCode, String organizerCountryCode) {
boolean isValid = isValid(response);
return new VatDetail(vatNr, countryCode, isValid, response.get("name"), response.get("address"), isValid && reverseChargeEnabled && !organizerCountryCode.equals(countryCode));
}

private static boolean isValid(Map<String, String> response) {
return Boolean.parseBoolean(response.get("isValid"));
return validationCache.get(Pair.of(vat, countryCode), k -> client.check(countryCode.toUpperCase(), vat));
}

private static String apiAddress(ConfigurationManager configurationManager) {
return configurationManager.getStringConfigValue(getSystemConfiguration(ConfigurationKeys.EU_VAT_API_ADDRESS), null);
private static VatDetail getVatDetail(boolean reverseChargeEnabled, EUVatCheckResponse response, String vatNr, String countryCode, String organizerCountryCode) {
boolean isValid = response.isValid();
return new VatDetail(vatNr, countryCode, isValid, response.getName(), response.getAddress(), isValid && reverseChargeEnabled && !organizerCountryCode.equals(countryCode));
}

private static String organizerCountry(ConfigurationManager configurationManager, int organizationId) {
Expand All @@ -141,7 +111,7 @@ private static boolean reverseChargeEnabled(ConfigurationManager configurationMa
}

private static boolean validationEnabled(ConfigurationManager configurationManager, int organizationId) {
return StringUtils.isNotEmpty(apiAddress(configurationManager)) && StringUtils.isNotEmpty(organizerCountry(configurationManager, organizationId));
return StringUtils.isNotEmpty(organizerCountry(configurationManager, organizationId));
}

@RequiredArgsConstructor
Expand All @@ -166,8 +136,8 @@ public boolean test(String vatNr) {
return false;
}

Map<String, String> result = validateEUVat(vatNr, organizerCountry, checker.configurationManager, checker.client);
boolean valid = result != null && isValid(result);
EUVatCheckResponse result = validateEUVat(vatNr, organizerCountry, checker.client);
boolean valid = result != null && result.isValid();
if(valid && StringUtils.isNotEmpty(ticketReservationId)) {
Map<String, Object> data = new HashMap<>();
data.put("vatNumber", vatNr);
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/alfio/model/system/ConfigurationKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ public enum ConfigurationKeys {
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),
@Deprecated
EU_VAT_API_ADDRESS("EU VAT API address", true, SettingCategory.INVOICE_EU, ComponentType.TEXT, false, EnumSet.of(SYSTEM), false),

//

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/alfio/util/ErrorsCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,6 @@ public interface ErrorsCode {

String STEP_2_MISSING_ATTENDEE_DATA = "error.STEP_2_MISSING_ATTENDEE_DATA";

String VIES_IS_DOWN = "error.vatVIESDown";

}
8 changes: 6 additions & 2 deletions src/main/java/alfio/util/Validator.java
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,12 @@ public static ValidationResult validateTicketAssignment(UpdateTicketOwnerForm fo
errors.rejectValue(prefixForLambda + "additional['"+fieldConf.getName()+"']", "error."+fieldConf.getName());
}

if(fieldConf.isEuVat() && !vatValidator.test(formValue)) {
errors.rejectValue(prefixForLambda + "additional['"+fieldConf.getName()+"']", ErrorsCode.STEP_2_INVALID_VAT);
try {
if (fieldConf.isEuVat() && !vatValidator.test(formValue)) {
errors.rejectValue(prefixForLambda + "additional['" + fieldConf.getName() + "']", ErrorsCode.STEP_2_INVALID_VAT);
}
} catch (IllegalStateException e) {
errors.rejectValue(prefixForLambda + "additional['" + fieldConf.getName() + "']", ErrorsCode.VIES_IS_DOWN);
}

});
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 @@ -364,4 +364,5 @@ reservation-page.copy-attendee=Copy Contact Data
reservation.add-company-billing-details=Company registration (a {0} number is required)
error.STEP_2_EMPTY_BILLING_ADDRESS=Billing address is mandatory
common.customer-reference=Purchase Order (PO) / Internal Reference
error.STEP_2_INVALID_VAT=The VAT Number is not valid. Please check the input parameter.
error.STEP_2_INVALID_VAT=The VAT Number is not valid. Please check the input parameter.
error.vatVIESDown=An error occurred contacting the EU VIES vat checker. Please try again.
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 @@ -355,3 +355,4 @@ error.STEP_2_EMPTY_BILLING_ADDRESS=Rechnungsadresse erforderlich
reservation-page.copy-attendee=von Kontaktdaten kopieren
common.customer-reference=Referenznummer
error.STEP_2_INVALID_VAT=Die MWST-Nummer ist nicht g\u00FCltig. Bitte pr\u00FCfen Sie die Eingabe.
error.vatVIESDown=[DE] An error occurred contacting the EU VIES vat checker. Please try again.
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 @@ -364,4 +364,5 @@ error.STEP_2_EMPTY_BILLING_ADDRESS=L''adresse de facturation est obligatoire
reservation-page.copy-attendee=copier donn\u00E9es de contact
common.customer-reference=num\u00E9ro de r\u00E9ference
reservation.add-company-billing-details=Achat en tant qu''entreprise (requis le numero {0})
error.STEP_2_INVALID_VAT=Le Nr. TVA n''est pas valide. V\u00E9rifiez le param\u00E8tre entr\u00E9.
error.STEP_2_INVALID_VAT=Le Nr. TVA n''est pas valide. V\u00E9rifiez le param\u00E8tre entr\u00E9.
error.vatVIESDown=[FR] An error occurred contacting the EU VIES vat checker. Please try again.
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 @@ -349,3 +349,4 @@ reservation.add-company-billing-details=Acquisto come azienda (\u00E8 richiesto
error.STEP_2_EMPTY_BILLING_ADDRESS=L''indirizzo di fatturazione \u00E8 obbligatorio
common.customer-reference=Numero di riferimento
error.STEP_2_INVALID_VAT=Il numero IVA immesso non risulta valido.
error.vatVIESDown=[IT] An error occurred contacting the EU VIES vat checker. Please try again.
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 @@ -359,3 +359,4 @@ reservation.add-company-billing-details=[NL] add company billing details (requir
error.STEP_2_EMPTY_BILLING_ADDRESS=[NL] Billing address is mandatory
common.customer-reference=referentienummer
error.STEP_2_INVALID_VAT=Uw BTW nummer is niet geldig. Check nogmaals of uw BTW nummer klopt.
error.vatVIESDown=[NL] An error occurred contacting the EU VIES vat checker. Please try again.
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@
<div class="col-xs-10 text-right">
<span id="validation-result" class="text-danger" data-validation-error-msg="{{#i18n}}reservation-page.vat-validation-error [{{vatTranslation}}]{{/i18n}}"
data-generic-error-msg="{{#i18n}}reservation-page.vat-error [{{vatTranslation}}]{{/i18n}}"
data-vies-down="{{#i18n}}error.vatVIESDown{{/i18n}}"
data-validation-required-msg="{{#i18n}}reservation-page.vat-validation-required [{{vatTranslation}}]{{/i18n}}"></span>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/main/webapp/resources/js/event/reservation-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,8 @@
vatInput.parent('div').addClass('has-error');
if(xhr.status === 400) {
resultContainer.html(resultContainer.attr('data-validation-error-msg'));
} else if (xhr.status === 503) {
resultContainer.html(resultContainer.attr('data-vies-down'));
} else {
resultContainer.html(resultContainer.attr('data-generic-error-msg'));
}
Expand Down
42 changes: 16 additions & 26 deletions src/test/java/alfio/manager/EuVatCheckerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
import alfio.model.VatDetail;
import alfio.model.system.Configuration;
import alfio.model.system.ConfigurationKeys;
import okhttp3.*;
import ch.digitalfondue.vatchecker.EUVatCheckResponse;
import ch.digitalfondue.vatchecker.EUVatChecker;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -40,13 +41,8 @@
@RunWith(MockitoJUnitRunner.class)
public class EuVatCheckerTest {

private static final String OK_RESPONSE = "{\"isValid\": true,\"name\": \"Test Corp.\",\"address\": \"Address\"}";
private static final String KO_RESPONSE = "{\"isValid\": false,\"name\": \"------\",\"address\": \"------\"}";

@Mock
private OkHttpClient client;
@Mock
private Call call;
private EUVatChecker client;
@Mock
private ConfigurationManager configurationManager;

Expand All @@ -55,13 +51,11 @@ 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);
}

@Test
public void performCheckOK() throws IOException {
initResponse(200, OK_RESPONSE);
public void performCheckOK() {
initResponse(true, "Test Corp.", "Address");
Optional<VatDetail> result = EuVatChecker.performCheck("1234", "IE", 1).apply(configurationManager, client);
assertTrue(result.isPresent());
VatDetail vatDetail = result.get();
Expand All @@ -74,8 +68,8 @@ public void performCheckOK() throws IOException {
}

@Test
public void performCheckKO() throws IOException {
initResponse(200, KO_RESPONSE);
public void performCheckKO() {
initResponse(false, "------", "------");
Optional<VatDetail> result = EuVatChecker.performCheck("12345", "IE", 1).apply(configurationManager, client);
assertTrue(result.isPresent());
VatDetail vatDetail = result.get();
Expand All @@ -87,9 +81,9 @@ public void performCheckKO() throws IOException {
assertEquals("------", vatDetail.getAddress());
}

@Test
public void performCheckRequestFailed() throws IOException {
initResponse(404, "");
@Test(expected = IllegalStateException.class)
public void performCheckRequestFailed() {
when(client.check(any(String.class), any(String.class))).thenThrow(new IllegalStateException("from test!"));
Optional<VatDetail> result = EuVatChecker.performCheck("1234", "IE", 1).apply(configurationManager, client);
assertFalse(result.isPresent());
}
Expand All @@ -104,7 +98,6 @@ public void testForeignBusinessVATApplied() {
assertFalse(vatDetail.isVatExempt());
assertEquals("1234", vatDetail.getVatNr());
assertEquals("UK", vatDetail.getCountry());
verify(client, never()).newCall(any());
}

@Test
Expand All @@ -117,16 +110,13 @@ public void testForeignBusinessVATNotApplied() {
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)
.body(ResponseBody.create(MediaType.parse("application/json"), body))
.code(status)
.message("" + status)
.build();
when(call.execute()).thenReturn(response);
private void initResponse(boolean isValid, String name, String address) {
EUVatCheckResponse resp = mock(EUVatCheckResponse.class);
when(resp.isValid()).thenReturn(isValid);
when(resp.getName()).thenReturn(name);
when(resp.getAddress()).thenReturn(address);
when(client.check(any(String.class), any(String.class))).thenReturn(resp);
}
}

0 comments on commit 9a01f9a

Please sign in to comment.