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

Custom vat application #1193

Merged
merged 2 commits into from
Feb 10, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,17 @@ 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()) {

var optionalCustomTaxPolicy = extensionManager.handleCustomTaxPolicy(purchaseContext, reservationId, contactAndTicketsForm, reservationCost);
if (optionalCustomTaxPolicy.isPresent()) {
log.debug("Custom tax policy returned for reservation {}. Applying it.", reservationId);
reverseChargeManager.applyCustomTaxPolicy(
purchaseContext,
optionalCustomTaxPolicy.get(),
reservationId,
contactAndTicketsForm,
bindingResult);
} else if(contactAndTicketsForm.isBusiness()) {
reverseChargeManager.checkAndApplyVATRules(purchaseContext, reservationId, contactAndTicketsForm, bindingResult);
} else if(reservationCost.getPriceWithVAT() > 0) {
reverseChargeManager.resetVat(purchaseContext, reservationId);
Expand Down
31 changes: 27 additions & 4 deletions src/main/java/alfio/manager/ExtensionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package alfio.manager;

import alfio.config.authentication.support.OpenIdAlfioAuthentication;
import alfio.controller.form.ContactAndTicketsForm;
import alfio.extension.ExtensionService;
import alfio.extension.exception.AlfioScriptingException;
import alfio.manager.payment.PaymentSpecification;
Expand All @@ -39,10 +40,7 @@
import alfio.model.user.Organization;
import alfio.model.user.PublicUserProfile;
import alfio.model.user.User;
import alfio.repository.EventRepository;
import alfio.repository.TicketRepository;
import alfio.repository.TicketReservationRepository;
import alfio.repository.TransactionRepository;
import alfio.repository.*;
import alfio.util.ClockProvider;
import alfio.util.EventUtil;
import alfio.util.MonetaryUtil;
Expand All @@ -58,10 +56,13 @@
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static alfio.extension.ExtensionService.toPath;
import static alfio.manager.support.extension.ExtensionEvent.*;
import static alfio.model.PromoCodeDiscount.DiscountType.PERCENTAGE;
import static java.util.stream.Collectors.toSet;

@Component
@AllArgsConstructor
Expand All @@ -86,6 +87,7 @@ public class ExtensionManager {
private final TicketRepository ticketRepository;
private final ConfigurationManager configurationManager;
private final TransactionRepository transactionRepository;
private final TicketCategoryRepository ticketCategoryRepository;


boolean isSupported(ExtensionCapability extensionCapability, PurchaseContext purchaseContext) {
Expand Down Expand Up @@ -537,4 +539,25 @@ public Optional<SubscriptionMetadata> handleSubscriptionAssignmentMetadata(Subsc
context.put("subscriptionDescriptor", descriptor);
return Optional.ofNullable(syncCall(ExtensionEvent.SUBSCRIPTION_ASSIGNED_GENERATE_METADATA, descriptor, context, SubscriptionMetadata.class, false));
}

public Optional<CustomTaxPolicy> handleCustomTaxPolicy(PurchaseContext purchaseContext,
String reservationId,
ContactAndTicketsForm form,
TotalPrice reservationCost) {
if (!purchaseContext.ofType(PurchaseContext.PurchaseContextType.event) || !reservationCost.requiresPayment()) {
return Optional.empty();
}
var event = (Event) purchaseContext;
var categoriesById = ticketCategoryRepository.findCategoriesInReservation(reservationId).stream()
.collect(Collectors.toMap(TicketCategory::getId, Function.identity()));
var ticketInfoById = ticketRepository.findBasicTicketInfoForReservation(event.getId(), reservationId).stream()
.collect(Collectors.toMap(TicketInfo::getTicketUuid, Function.identity()));
var context = new HashMap<String, Object>();
context.put(EVENT, event);
context.put(RESERVATION_ID, reservationId);
context.put("reservationForm", form);
context.put("categoriesById", categoriesById);
context.put("ticketInfoByUuid", ticketInfoById);
return Optional.ofNullable(syncCall(CUSTOM_TAX_POLICY_APPLICATION, event, context, CustomTaxPolicy.class, false));
}
}
117 changes: 97 additions & 20 deletions src/main/java/alfio/manager/ReverseChargeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,32 @@
package alfio.manager;

import alfio.controller.form.ContactAndTicketsForm;
import alfio.controller.support.CustomBindingResult;
import alfio.manager.system.ConfigurationManager;
import alfio.manager.system.ReservationPriceCalculator;
import alfio.model.*;
import alfio.model.decorator.TicketPriceContainer;
import alfio.model.extension.CustomTaxPolicy;
import alfio.repository.*;
import alfio.util.MonetaryUtil;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ValidationUtils;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import static alfio.model.Audit.EntityType.RESERVATION;
import static alfio.model.PriceContainer.VatStatus.*;
import static alfio.model.PriceContainer.VatStatus.NOT_INCLUDED_NOT_CHARGED;
import static alfio.model.system.ConfigurationKeys.*;
Expand All @@ -46,6 +54,7 @@
@Component
public class ReverseChargeManager {

private static final Logger log = LoggerFactory.getLogger(ReverseChargeManager.class);
private final PromoCodeDiscountRepository promoCodeDiscountRepository;
private final AdditionalServiceItemRepository additionalServiceItemRepository;
private final AdditionalServiceRepository additionalServiceRepository;
Expand All @@ -57,6 +66,7 @@ public class ReverseChargeManager {
private final TicketReservationManager ticketReservationManager;
private final TicketRepository ticketRepository;
private final SubscriptionRepository subscriptionRepository;
private final AuditingRepository auditingRepository;



Expand Down Expand Up @@ -111,7 +121,7 @@ public void checkAndApplyVATRules(PurchaseContext purchaseContext,
var currencyCode = reservation.getCurrencyCode();
PriceContainer.VatStatus vatStatus = determineVatStatus(purchaseContext.getVatStatus(), vatValidation.isVatExempt());
// standard case: Reverse Charge is applied to the entire reservation
var discount = reservation.getPromoCodeDiscountId() != null ? promoCodeDiscountRepository.findById(reservation.getPromoCodeDiscountId()) : null;
var discount = getDiscountOrNull(reservation);
if(!isEvent || (reverseChargeOnline && reverseChargeInPerson)) {
if(isEvent) {
var event = purchaseContext.event().orElseThrow();
Expand Down Expand Up @@ -152,11 +162,63 @@ public void checkAndApplyVATRules(PurchaseContext purchaseContext,
}
}

private PromoCodeDiscount getDiscountOrNull(TicketReservation reservation) {
if (reservation.getPromoCodeDiscountId() != null) {
return promoCodeDiscountRepository.findById(reservation.getPromoCodeDiscountId());
}
return null;
}

public void applyCustomTaxPolicy(PurchaseContext purchaseContext,
CustomTaxPolicy customTaxPolicy,
String reservationId,
ContactAndTicketsForm contactAndTicketsForm,
CustomBindingResult bindingResult) {

if (!purchaseContext.ofType(PurchaseContext.PurchaseContextType.event)) {
throw new IllegalStateException("Custom tax policy is only supported for events");
}

var event = (Event) purchaseContext;
// first, validate that categories in CustomTaxPolicy are actually present in the form
var reservation = ticketReservationManager.findById(reservationId).orElseThrow();
var currencyCode = reservation.getCurrencyCode();
var ticketsInReservation = ticketRepository.findTicketsInReservation(reservationId).stream()
.collect(toMap(Ticket::getUuid, Function.identity()));
var ticketIds = ticketsInReservation.keySet();
if (customTaxPolicy.getTicketPolicies().stream().anyMatch(tp -> !ticketIds.contains(tp.getUuid()))) {
log.warn("Error in custom tax policy: some tickets are not included in reservation {}", reservationId);
bindingResult.reject("error.generic");
} else {
// log the received policy to the auditing
auditingRepository.insert(reservationId, null, purchaseContext, Audit.EventType.VAT_CUSTOM_CONFIGURATION_APPLIED, new Date(), RESERVATION, reservationId, List.of(Map.of("policy", customTaxPolicy)));
var priceMapping = customTaxPolicy.getTicketPolicies().stream()
.map(tcp -> toTicketPriceContainer(ticketsInReservation.get(tcp.getUuid()), tcp, getDiscountOrNull(reservation), event))
.collect(Collectors.toList());
updateTicketPrices(priceMapping, currencyCode, event);
// update billing data for the reservation, using the original VatStatus from reservation
updateBillingData(reservationId, contactAndTicketsForm, purchaseContext, reservation.getVatCountryCode(), trimToNull(reservation.getVatNr()), reservation, reservation.getVatStatus());
}
}

private static TicketPriceContainer toTicketPriceContainer(Ticket ticket,
CustomTaxPolicy.TicketTaxPolicy categoryTaxPolicy,
PromoCodeDiscount discount,
Event event) {
return TicketPriceContainer.from(
ticket.withVatStatus(categoryTaxPolicy.getTaxPolicy()),
categoryTaxPolicy.getTaxPolicy(),
event.getVat(),
event.getVatStatus(),
discount
);
}

public void resetVat(PurchaseContext purchaseContext, String reservationId) {
if(purchaseContext.ofType(PurchaseContext.PurchaseContextType.event)) {
var reservation = ticketReservationRepository.findReservationById(reservationId);
var categoriesList = ticketCategoryRepository.findCategoriesInReservation(reservationId);
var discount = reservation.getPromoCodeDiscountId() != null ? promoCodeDiscountRepository.findById(reservation.getPromoCodeDiscountId()) : null;
var discount = getDiscountOrNull(reservation);
var event = purchaseContext.event().orElseThrow();
var priceContainers = mapPriceContainersByCategoryId(categoriesList,
(a) -> true,
Expand Down Expand Up @@ -188,23 +250,38 @@ private void updateTicketPricesByCategory(String reservationId,
Event event,
Map<Integer, TicketCategoryPriceContainer> matchingCategories) {
MapSqlParameterSource[] parameterSources = matchingCategories.entrySet().stream()
.map(entry -> {
var value = entry.getValue();
return new MapSqlParameterSource()
.addValue("reservationId", reservationId)
.addValue("categoryId", entry.getKey())
.addValue("eventId", event.getId())
.addValue("srcPriceCts", value.getSrcPriceCts())
.addValue("finalPriceCts", value.getFinalPriceCts())
.addValue("vatCts", value.getVatCts())
.addValue("discountCts", value.getDiscountCts())
.addValue("currencyCode", currencyCode)
.addValue("vatStatus", vatStatus.name());
}
).toArray(MapSqlParameterSource[]::new);
.map(entry -> buildParameterSourceForPriceUpdate(entry.getValue(), event.getId(), entry.getKey(), currencyCode, vatStatus,
m -> m.addValue("reservationId", reservationId)))
.toArray(MapSqlParameterSource[]::new);
jdbcTemplate.batchUpdate(ticketRepository.updateTicketPriceForCategoryInReservation(), parameterSources);
}

private void updateTicketPrices(List<TicketPriceContainer> ticketPrices, String currencyCode, Event event) {
MapSqlParameterSource[] parameterSources = ticketPrices.stream()
.map(entry -> buildParameterSourceForPriceUpdate(entry, event.getId(), entry.getCategoryId(), currencyCode, entry.getVatStatus(),
m -> m.addValue("uuid", entry.getUuid())))
.toArray(MapSqlParameterSource[]::new);
var updateResult = jdbcTemplate.batchUpdate(ticketRepository.bulkUpdateTicketPrice(), parameterSources);
Validate.isTrue(Arrays.stream(updateResult).allMatch(i -> i == 1), "Error while updating ticket prices");
}

private static MapSqlParameterSource buildParameterSourceForPriceUpdate(PriceContainer value,
int eventId,
int categoryId,
String currencyCode,
PriceContainer.VatStatus vatStatus,
UnaryOperator<MapSqlParameterSource> modifier) {
return modifier.apply(new MapSqlParameterSource()
.addValue("categoryId", categoryId)
.addValue("eventId", eventId)
.addValue("srcPriceCts", value.getSrcPriceCts())
.addValue("finalPriceCts", MonetaryUtil.unitToCents(value.getFinalPrice(), currencyCode))
.addValue("vatCts", MonetaryUtil.unitToCents(value.getVAT(), currencyCode))
.addValue("discountCts", MonetaryUtil.unitToCents(value.getAppliedDiscount(), currencyCode))
.addValue("currencyCode", currencyCode)
.addValue("vatStatus", vatStatus.name()));
}

private Predicate<TicketCategory> findReverseChargeCategory(boolean reverseChargeInPerson, boolean reverseChargeOnline, Event.EventFormat eventFormat) {
return tc -> {
if (eventFormat == Event.EventFormat.HYBRID) {
Expand All @@ -216,7 +293,7 @@ private Predicate<TicketCategory> findReverseChargeCategory(boolean reverseCharg
}

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 discount = getDiscountOrNull(reservation);
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());
Expand Down
Loading