Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable reverse charge for a specific ticket type #1026

Merged
merged 6 commits into from
Oct 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ targetCompatibility=11
systemProp.jdk.tls.client.protocols="TLSv1,TLSv1.1,TLSv1.2"

# https://jitpack.io/#alfio-event/alf.io-public-frontend -> go to commit tab, set the version
alfioPublicFrontendVersion=93f4b52e22
alfioPublicFrontendVersion=5acacec38d
5 changes: 3 additions & 2 deletions src/main/java/alfio/config/DataSourceConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package alfio.config;

import alfio.config.support.ArrayColumnMapper;
import alfio.config.support.EnumTypeColumnMapper;
import alfio.config.support.JSONColumnMapper;
import alfio.config.support.PlatformProvider;
import alfio.job.Jobs;
Expand Down Expand Up @@ -148,12 +149,12 @@ public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSour

@Bean
public List<ColumnMapperFactory> getAdditionalColumnMappers() {
return Arrays.asList(new JSONColumnMapper.Factory(), new ArrayColumnMapper.Factory());
return Arrays.asList(new JSONColumnMapper.Factory(), new ArrayColumnMapper.Factory(), new EnumTypeColumnMapper.Factory());
}

@Bean
public List<ParameterConverter> getAdditionalParameterConverters() {
return Arrays.asList(new JSONColumnMapper.Converter(), new ArrayColumnMapper.Converter());
return Arrays.asList(new JSONColumnMapper.Converter(), new ArrayColumnMapper.Converter(), new EnumTypeColumnMapper.Converter());
}

@Bean
Expand Down
114 changes: 114 additions & 0 deletions src/main/java/alfio/config/support/EnumTypeColumnMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.config.support;

import alfio.model.support.EnumTypeAsString;
import ch.digitalfondue.npjt.mapper.ColumnMapper;
import ch.digitalfondue.npjt.mapper.ColumnMapperFactory;
import ch.digitalfondue.npjt.mapper.ParameterConverter;
import lombok.extern.log4j.Log4j2;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Objects;

@Log4j2
public class EnumTypeColumnMapper extends ColumnMapper {

private static final int ORDER = Integer.MAX_VALUE - 32;

private EnumTypeColumnMapper(String name, Class<?> paramType) {
super(name, paramType);
}

@Override
public Object getObject(ResultSet rs) throws SQLException {
var enumAsString = rs.getString(name);
if (enumAsString != null) {
return parseValue(paramType, enumAsString);
}
return null;
}

private static boolean isSupported(Class<?> paramType, Annotation[] annotations) {
return annotations != null
&& Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType() == EnumTypeAsString.class)
&& Enum.class.isAssignableFrom(paramType);
}

public static class Factory implements ColumnMapperFactory {

@Override
public ColumnMapper build(String name, Class<?> paramType) {
return new EnumTypeColumnMapper(name, paramType);
}

@Override
public int order() {
return ORDER;
}

@Override
public boolean accept(Class<?> paramType, Annotation[] annotations) {
return isSupported(paramType, annotations);
}

@Override
public RowMapper<Object> getSingleColumnRowMapper(Class<Object> clazz) {
return (resultSet, rowNum) -> {
var enumAsString = resultSet.getString(1);
if(enumAsString != null) {
return parseValue(clazz, enumAsString);
}
return null;
};
}
}

public static class Converter implements ParameterConverter {

@Override
public boolean accept(Class<?> parameterType, Annotation[] annotations) {
return isSupported(parameterType, annotations);
}

@Override
public void processParameter(String parameterName, Object arg, Class<?> parameterType, MapSqlParameterSource ps) {
String value = arg != null ? arg.toString() : null;
ps.addValue(parameterName, value);
}

@Override
public int order() {
return ORDER;
}
}

private static Object parseValue(Class<?> clazz, String enumAsString) {
try {
var method = clazz.getMethod("valueOf", String.class);
return method.invoke(null, Objects.requireNonNull(enumAsString));
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new IllegalStateException("unexpected exception while deserializing Enum value", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public static class ReservationInfoOrderSummary {
public ReservationInfoOrderSummary(OrderSummary orderSummary) {
this.summary = orderSummary.getSummary()
.stream()
.map(s -> new ReservationInfoOrderSummaryRow(s.getName(), s.getAmount(), s.getPrice(), s.getSubTotal(), s.getType()))
.map(s -> new ReservationInfoOrderSummaryRow(s.getName(), s.getAmount(), s.getPrice(), s.getSubTotal(), s.getType(), s.getTaxPercentage()))
.collect(Collectors.toList());
this.totalPrice = orderSummary.getTotalPrice();
this.free = orderSummary.getFree();
Expand All @@ -237,6 +237,7 @@ public static class ReservationInfoOrderSummaryRow {
private final String price;
private final String subTotal;
private final SummaryType type;
private final String taxPercentage;
}

@AllArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import alfio.controller.form.ContactAndTicketsForm;
import alfio.controller.form.PaymentForm;
import alfio.controller.form.ReservationCodeForm;
import alfio.controller.form.UpdateTicketOwnerForm;
import alfio.controller.support.CustomBindingResult;
import alfio.controller.support.TemplateProcessor;
import alfio.manager.*;
Expand All @@ -39,18 +38,15 @@
import alfio.manager.system.ConfigurationManager;
import alfio.manager.system.ReservationPriceCalculator;
import alfio.manager.user.PublicUserManager;
import alfio.manager.user.UserManager;
import alfio.model.*;
import alfio.model.TicketCategory.TicketAccessType;
import alfio.model.PurchaseContext.PurchaseContextType;
import alfio.model.extension.AdditionalInfoItem;
import alfio.model.subscription.Subscription;
import alfio.model.subscription.SubscriptionUsageExceeded;
import alfio.model.subscription.SubscriptionUsageExceededForEvent;
import alfio.model.subscription.UsageDetails;
import alfio.model.system.ConfigurationKeys;
import alfio.model.transaction.*;
import alfio.model.user.AdditionalInfoWithLabel;
import alfio.model.user.PublicUserProfile;
import alfio.repository.*;
import alfio.util.*;
import lombok.AllArgsConstructor;
Expand All @@ -62,6 +58,8 @@
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
Expand All @@ -74,18 +72,20 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.security.Principal;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static alfio.model.PriceContainer.VatStatus.*;
import static alfio.model.system.ConfigurationKeys.*;
import static alfio.util.MonetaryUtil.unitToCents;
import static java.util.Objects.requireNonNullElse;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;
import static org.apache.commons.lang3.StringUtils.trimToNull;

Expand All @@ -110,15 +110,12 @@ public class ReservationApiV2Controller {
private final EuVatChecker vatChecker;
private final RecaptchaService recaptchaService;
private final BookingInfoTicketLoader bookingInfoTicketLoader;
private final PromoCodeDiscountRepository promoCodeDiscountRepository;
private final AdditionalServiceItemRepository additionalServiceItemRepository;
private final AdditionalServiceRepository additionalServiceRepository;
private final BillingDocumentManager billingDocumentManager;
private final PurchaseContextManager purchaseContextManager;
private final SubscriptionRepository subscriptionRepository;
private final TicketRepository ticketRepository;
private final UserManager userManager;
private final PublicUserManager publicUserManager;
private final ReverseChargeManager reverseChargeManager;

/**
* Note: now it will return for any states of the reservation.
Expand Down Expand Up @@ -146,7 +143,7 @@ public ResponseEntity<ReservationInfo> getReservationInfo(@PathVariable("reserva
boolean hasPaidSupplement = ticketReservationManager.hasPaidSupplements(reservationId);
//

var ticketsInfo = purchaseContext.event().map(event -> {
var ticketsInfo = purchaseContext.event().filter(e -> !ticketIds.isEmpty()).map(event -> {
var valuesByTicketIds = ticketFieldRepository.findAllValuesByTicketIds(ticketIds)
.stream()
.collect(Collectors.groupingBy(TicketFieldValue::getTicketId));
Expand Down Expand Up @@ -411,7 +408,9 @@ public ResponseEntity<ValidatedResponse<Boolean>> validateToOverview(@PathVariab
ticketReservationRepository.resetVat(reservationId, contactAndTicketsForm.isInvoiceRequested(), purchaseContext.getVatStatus(),
reservation.getSrcPriceCts(), reservationCost.getPriceWithVAT(), reservationCost.getVAT(), Math.abs(reservationCost.getDiscount()), reservation.getCurrencyCode());
if(contactAndTicketsForm.isBusiness()) {
checkAndApplyVATRules(purchaseContext, reservationId, contactAndTicketsForm, bindingResult);
reverseChargeManager.checkAndApplyVATRules(purchaseContext, reservationId, contactAndTicketsForm, bindingResult);
} else if(reservationCost.getPriceWithVAT() > 0) {
reverseChargeManager.resetVat(purchaseContext, reservationId);
}

//persist data
Expand Down Expand Up @@ -506,59 +505,6 @@ private void assignTickets(String eventName, String reservationId, ContactAndTic
}
}

private void checkAndApplyVATRules(PurchaseContext purchaseContext, String reservationId, ContactAndTicketsForm contactAndTicketsForm, BindingResult bindingResult) {
// VAT handling
String country = contactAndTicketsForm.getVatCountryCode();

// validate VAT presence if EU mode is enabled
if (vatChecker.isReverseChargeEnabledFor(purchaseContext) && (country == null || isEUCountry(country))) {
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "vatNr", "error.emptyField");
}

try {
var optionalReservation = ticketReservationRepository.findOptionalReservationById(reservationId);
Optional<VatDetail> vatDetail = optionalReservation
.filter(e -> EnumSet.of(INCLUDED, NOT_INCLUDED).contains(purchaseContext.getVatStatus()))
.filter(e -> vatChecker.isReverseChargeEnabledFor(purchaseContext))
.flatMap(e -> vatChecker.checkVat(contactAndTicketsForm.getVatNr(), country, purchaseContext));


if(vatDetail.isPresent()) {
var vatValidation = vatDetail.get();
if (!vatValidation.isValid()) {
bindingResult.rejectValue("vatNr", "error.STEP_2_INVALID_VAT");
} else {
var reservation = ticketReservationManager.findById(reservationId).orElseThrow();
var currencyCode = reservation.getCurrencyCode();
PriceContainer.VatStatus vatStatus = determineVatStatus(purchaseContext.getVatStatus(), vatValidation.isVatExempt());
updateBillingData(reservationId, contactAndTicketsForm, purchaseContext, country, trimToNull(vatValidation.getVatNr()), reservation, vatStatus);
vatChecker.logSuccessfulValidation(vatValidation, reservationId, purchaseContext.event().map(Event::getId).orElse(null));
}
} else if(optionalReservation.isPresent() && contactAndTicketsForm.isItalyEInvoicingSplitPayment()) {
var reservation = optionalReservation.get();
var vatStatus = purchaseContext.getVatStatus() == INCLUDED ? INCLUDED_NOT_CHARGED : NOT_INCLUDED_NOT_CHARGED;
updateBillingData(reservationId, contactAndTicketsForm, purchaseContext, country, trimToNull(contactAndTicketsForm.getVatNr()), reservation, vatStatus);
}
} catch (IllegalStateException ise) {//vat checker failure
bindingResult.rejectValue("vatNr", "error.vatVIESDown");
}
}

private void updateBillingData(String reservationId, ContactAndTicketsForm contactAndTicketsForm, PurchaseContext purchaseContext, String country, String vatNr, TicketReservation reservation, PriceContainer.VatStatus vatStatus) {
var discount = reservation.getPromoCodeDiscountId() != null ? promoCodeDiscountRepository.findById(reservation.getPromoCodeDiscountId()) : null;
var additionalServiceItems = additionalServiceItemRepository.findByReservationUuid(reservation.getId());
var tickets = ticketReservationManager.findTicketsInReservation(reservation.getId());
var additionalServices = purchaseContext.event().map(event -> additionalServiceRepository.loadAllForEvent(event.getId())).orElse(List.of());
var subscriptions = subscriptionRepository.findSubscriptionsByReservationId(reservationId);
var appliedSubscription = subscriptionRepository.findAppliedSubscriptionByReservationId(reservationId);
var calculator = new ReservationPriceCalculator(reservation.withVatStatus(vatStatus), discount, tickets, additionalServiceItems, additionalServices, purchaseContext, subscriptions, appliedSubscription);
var currencyCode = reservation.getCurrencyCode();
ticketReservationRepository.updateBillingData(vatStatus, reservation.getSrcPriceCts(),
unitToCents(calculator.getFinalPrice(), currencyCode), unitToCents(calculator.getVAT(), currencyCode), unitToCents(calculator.getAppliedDiscount(), currencyCode),
reservation.getCurrencyCode(), vatNr,
country, contactAndTicketsForm.isInvoiceRequested(), reservationId);
}

private Optional<Pair<PurchaseContext, TicketReservation>> getReservation(String reservationId) {
return purchaseContextManager.findByReservationId(reservationId)
.flatMap(purchaseContext -> ticketReservationManager.findById(reservationId)
Expand Down Expand Up @@ -814,18 +760,6 @@ private Map<String, String> formatDateForLocales(PurchaseContext purchaseContext
return res;
}

private boolean isEUCountry(String countryCode) {
return configurationManager.getForSystem(EU_COUNTRIES_LIST).getRequiredValue().contains(countryCode);
}

private static PriceContainer.VatStatus determineVatStatus(PriceContainer.VatStatus current, boolean isVatExempt) {
if(!isVatExempt) {
return current;
}
return current == NOT_INCLUDED ? NOT_INCLUDED_EXEMPT : INCLUDED_EXEMPT;
}


private boolean isCaptchaInvalid(int cost, PaymentProxy paymentMethod, String recaptchaResponse, HttpServletRequest request, Configurable configurable) {
return (cost == 0 || paymentMethod == PaymentProxy.OFFLINE || paymentMethod == PaymentProxy.ON_SITE)
&& configurationManager.isRecaptchaForOfflinePaymentAndFreeEnabled(configurable.getConfigurationLevel())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public static Map<ConfigurationKeys, ConfigurationManager.MaybeConfiguration> co
// required by EuVatChecker.reverseChargeEnabled
ENABLE_EU_VAT_DIRECTIVE,
COUNTRY_OF_BUSINESS,
ENABLE_REVERSE_CHARGE_IN_PERSON,
ENABLE_REVERSE_CHARGE_ONLINE,

DISPLAY_TICKETS_LEFT_INDICATOR,
EVENT_CUSTOM_CSS
Expand Down
Loading