Skip to content

Commit

Permalink
Implement #573
Browse files Browse the repository at this point in the history
- initial step for implementing the italian e-invoicing: send invoice only to organizer

- only disallow anonymous user to download the invoice when it-e-invoicing is enabled

- fix build

- initial ui work

- persist new data

- fix typo

- expose new data in ticket reservation

- fix build

- fix build, take2

- display information in admin

- first stab at validation+ui/ux

- fix ui/ux

- add missing validation

- fix tests

- PEC address is an e-mail

- add admin modification

- fix test

- add info to confirmation email and invoice
  • Loading branch information
syjer committed Jan 4, 2019
1 parent 67eed0f commit 7c9aaea
Show file tree
Hide file tree
Showing 38 changed files with 532 additions and 61 deletions.
41 changes: 33 additions & 8 deletions src/main/java/alfio/controller/InvoiceReceiptController.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
import alfio.controller.support.TemplateProcessor;
import alfio.manager.FileUploadManager;
import alfio.manager.TicketReservationManager;
import alfio.manager.system.ConfigurationManager;
import alfio.model.Event;
import alfio.model.TicketReservation;
import alfio.repository.EventRepository;
import alfio.util.TemplateManager;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -47,27 +50,49 @@ public class InvoiceReceiptController {
private final TicketReservationManager ticketReservationManager;
private final FileUploadManager fileUploadManager;
private final TemplateManager templateManager;
private final ConfigurationManager configurationManager;

private ResponseEntity<Void> handleReservationWith(String eventName, String reservationId, BiFunction<Event, TicketReservation, ResponseEntity<Void>> with) {
private ResponseEntity<Void> handleReservationWith(String eventName, String reservationId, Authentication authentication,
BiFunction<Event, TicketReservation, ResponseEntity<Void>> with) {
ResponseEntity<Void> notFound = ResponseEntity.notFound().build();
return eventRepository.findOptionalByShortName(eventName).map(event ->
ticketReservationManager.findById(reservationId).map(ticketReservation ->
with.apply(event, ticketReservation)).orElse(notFound)
ResponseEntity<Void> badRequest = ResponseEntity.badRequest().build();



return eventRepository.findOptionalByShortName(eventName).map(event -> {
if(canAccessReceiptOrInvoice(event, authentication)) {
return ticketReservationManager.findById(reservationId).map(ticketReservation -> with.apply(event, ticketReservation)).orElse(notFound);
} else {
return badRequest;
}
}
).orElse(notFound);
}

private boolean canAccessReceiptOrInvoice(Event event, Authentication authentication) {
return configurationManager.canGenerateReceiptOrInvoiceToCustomer(event) || !isAnonymous(authentication);
}


private boolean isAnonymous(Authentication authentication) {
return authentication == null ||
authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).anyMatch("ROLE_ANONYMOUS"::equals);
}

@RequestMapping("/event/{eventName}/reservation/{reservationId}/receipt")
public ResponseEntity<Void> getReceipt(@PathVariable("eventName") String eventName,
@PathVariable("reservationId") String reservationId,
HttpServletResponse response) {
return handleReservationWith(eventName, reservationId, generatePdfFunction(false, response));
HttpServletResponse response,
Authentication authentication) {
return handleReservationWith(eventName, reservationId, authentication, generatePdfFunction(false, response));
}

@RequestMapping("/event/{eventName}/reservation/{reservationId}/invoice")
public ResponseEntity<Void> getInvoice(@PathVariable("eventName") String eventName,
@PathVariable("reservationId") String reservationId,
HttpServletResponse response) {
return handleReservationWith(eventName, reservationId, generatePdfFunction(true, response));
HttpServletResponse response,
Authentication authentication) {
return handleReservationWith(eventName, reservationId, authentication, generatePdfFunction(true, response));
}

private BiFunction<Event, TicketReservation, ResponseEntity<Void>> generatePdfFunction(boolean forInvoice, HttpServletResponse response) {
Expand Down
37 changes: 22 additions & 15 deletions src/main/java/alfio/controller/ReservationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,12 @@
import alfio.model.transaction.PaymentContext;
import alfio.model.transaction.PaymentProxy;
import alfio.model.transaction.PaymentToken;
import alfio.model.user.Organization;
import alfio.repository.EventRepository;
import alfio.repository.TicketFieldRepository;
import alfio.repository.TicketReservationRepository;
import alfio.repository.user.OrganizationRepository;
import alfio.util.ErrorsCode;
import alfio.util.TemplateManager;
import alfio.util.TemplateResource;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -130,7 +128,7 @@ public String showBookingPage(@PathVariable("eventName") String eventName,
//FIXME recaptcha for free orders

boolean invoiceAllowed = configurationManager.hasAllConfigurationsForInvoice(event) || vatChecker.isVatCheckingEnabledFor(event.getOrganizationId());
boolean onlyInvoice = invoiceAllowed && configurationManager.getBooleanConfigValue(partialConfig.apply(ConfigurationKeys.GENERATE_ONLY_INVOICE), false);
boolean onlyInvoice = invoiceAllowed && configurationManager.isInvoiceOnly(event);


ContactAndTicketsForm contactAndTicketsForm = ContactAndTicketsForm.fromExistingReservation(reservation, additionalInfo);
Expand All @@ -150,7 +148,8 @@ public String showBookingPage(@PathVariable("eventName") String eventName,
.addAttribute("vatNrIsLinked", orderSummary.isVatExempt() || contactAndTicketsForm.getHasVatCountryCode())
.addAttribute("attendeeAutocompleteEnabled", ticketsInReservation.size() == 1 && configurationManager.getBooleanConfigValue(partialConfig.apply(ENABLE_ATTENDEE_AUTOCOMPLETE), true))
.addAttribute("billingAddressLabel", invoiceAllowed ? "reservation-page.billing-address" : "reservation-page.receipt-address")
.addAttribute("customerReferenceEnabled", configurationManager.getBooleanConfigValue(partialConfig.apply(ENABLE_CUSTOMER_REFERENCE), false));
.addAttribute("customerReferenceEnabled", configurationManager.getBooleanConfigValue(partialConfig.apply(ENABLE_CUSTOMER_REFERENCE), false))
.addAttribute("enabledItalyEInvoicing", configurationManager.getBooleanConfigValue(partialConfig.apply(ENABLE_ITALY_E_INVOICING), false));

Map<String, Object> modelMap = model.asMap();
modelMap.putIfAbsent("paymentForm", contactAndTicketsForm);
Expand Down Expand Up @@ -223,6 +222,7 @@ public String showConfirmationPage(@PathVariable("eventName") String eventName,
model.addAttribute("pageTitle", "reservation-page-complete.header.title");
model.addAttribute("event", ev);
model.addAttribute("useFirstAndLastName", ev.mustUseFirstAndLastName());
model.addAttribute("userCanDownloadReceiptOrInvoice", configurationManager.canGenerateReceiptOrInvoiceToCustomer(ev));
model.asMap().putIfAbsent("validationResult", ValidationResult.success());
return "/event/reservation-page-complete";
}).orElseGet(() -> redirectReservation(tr, eventName, reservationId));
Expand Down Expand Up @@ -251,8 +251,7 @@ public String validateToOverview(@PathVariable("eventName") String eventName, @P
contactAndTicketsForm.setPostponeAssignment(false);
}

Configuration.ConfigurationPathKey invoiceOnlyKey = Configuration.from(event.getOrganizationId(), event.getId(), ConfigurationKeys.GENERATE_ONLY_INVOICE);
boolean invoiceOnly = configurationManager.getBooleanConfigValue(invoiceOnlyKey, false);
boolean invoiceOnly = configurationManager.isInvoiceOnly(event);

if(invoiceOnly && reservationCost.getPriceWithVAT() > 0) {
//override, that's why we save it
Expand All @@ -272,11 +271,24 @@ public String validateToOverview(@PathVariable("eventName") String eventName, @P
contactAndTicketsForm.getBillingAddressZip(), contactAndTicketsForm.getBillingAddressCity(), contactAndTicketsForm.getVatCountryCode(),
contactAndTicketsForm.getCustomerReference(), contactAndTicketsForm.getVatNr(), contactAndTicketsForm.isInvoiceRequested(),
contactAndTicketsForm.canSkipVatNrCheck(), false);
ticketReservationManager.updateReservationInvoicingAdditionalInformation(reservationId,
new TicketReservationInvoicingAdditionalInfo(
new BillingDetails.ItalianEInvoicing(contactAndTicketsForm.getItalyEInvoicingFiscalCode(),
contactAndTicketsForm.getItalyEInvoicingReferenceType(),
contactAndTicketsForm.getItalyEInvoicingReferenceAddresseeCode(),
contactAndTicketsForm.getItalyEInvoicingReferencePEC())
)
);
assignTickets(event.getShortName(), reservationId, contactAndTicketsForm, bindingResult, request, true, true);
//

boolean italyEInvoicing = configurationManager.getBooleanConfigValue(Configuration.from(event.getOrganizationId(), event.getId(), ENABLE_ITALY_E_INVOICING), false);
Map<ConfigurationKeys, Boolean> formValidationParameters = Collections.singletonMap(ENABLE_ITALY_E_INVOICING, italyEInvoicing);
//
contactAndTicketsForm.validate(bindingResult, event, ticketFieldRepository.findAdditionalFieldsForEvent(event.getId()), new SameCountryValidator(vatChecker, event.getOrganizationId(), event.getId(), reservationId));
contactAndTicketsForm.validate(bindingResult, event,
ticketFieldRepository.findAdditionalFieldsForEvent(event.getId()),
new SameCountryValidator(vatChecker, event.getOrganizationId(), event.getId(), reservationId),
formValidationParameters);
//

if(bindingResult.hasErrors()) {
Expand Down Expand Up @@ -461,6 +473,7 @@ public String showWaitingPaymentPage(@PathVariable("eventName") String eventName

model.addAttribute("expires", ZonedDateTime.ofInstant(ticketReservation.getValidity().toInstant(), ev.getZoneId()));
model.addAttribute("event", ev);
model.addAttribute("userCanDownloadReceiptOrInvoice", configurationManager.canGenerateReceiptOrInvoiceToCustomer(ev));
return "/event/reservation-waiting-for-payment";
}

Expand Down Expand Up @@ -654,14 +667,8 @@ private void sendReservationCompleteEmail(HttpServletRequest request, Event even
}

private void sendReservationCompleteEmailToOrganizer(HttpServletRequest request, Event event, TicketReservation reservation) {

Organization organization = organizationRepository.getById(event.getOrganizationId());
List<String> cc = notificationManager.getCCForEventOrganizer(event);

notificationManager.sendSimpleEmail(event, organization.getEmail(), cc, "Reservation complete " + reservation.getId(), () ->
templateManager.renderTemplate(event, TemplateResource.CONFIRMATION_EMAIL_FOR_ORGANIZER, ticketReservationManager.prepareModelForReservationEmail(event, reservation),
RequestContextUtils.getLocale(request))
);
Locale locale = RequestContextUtils.getLocale(request);
ticketReservationManager.sendReservationCompleteEmailToOrganizer(event, reservation, locale);
}

private boolean isExpressCheckoutEnabled(Event event, OrderSummary orderSummary) {
Expand Down
73 changes: 68 additions & 5 deletions src/main/java/alfio/controller/form/ContactAndTicketsForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@
package alfio.controller.form;

import alfio.manager.EuVatChecker;
import alfio.model.Event;
import alfio.model.TicketFieldConfiguration;
import alfio.model.TicketReservation;
import alfio.model.TicketReservationAdditionalInfo;
import alfio.model.*;
import alfio.model.result.ValidationResult;
import alfio.model.system.ConfigurationKeys;
import alfio.util.ErrorsCode;
import alfio.util.Validator;
import lombok.Data;
Expand Down Expand Up @@ -67,6 +65,14 @@ public class ContactAndTicketsForm implements Serializable {

private Boolean skipVatNr;
private Boolean backFromOverview;
//

// https://github.com/alfio-event/alf.io/issues/573
private String italyEInvoicingFiscalCode;
private BillingDetails.ItalianEInvoicing.ReferenceType italyEInvoicingReferenceType;
private String italyEInvoicingReferenceAddresseeCode;
private String italyEInvoicingReferencePEC;
//

private static void rejectIfOverLength(BindingResult bindingResult, String field, String errorCode,
String value, int maxLength) {
Expand All @@ -77,7 +83,7 @@ private static void rejectIfOverLength(BindingResult bindingResult, String field



public void validate(BindingResult bindingResult, Event event, List<TicketFieldConfiguration> fieldConf, EuVatChecker.SameCountryValidator vatValidator) {
public void validate(BindingResult bindingResult, Event event, List<TicketFieldConfiguration> fieldConf, EuVatChecker.SameCountryValidator vatValidator, Map<ConfigurationKeys, Boolean> formValidationParameters) {



Expand Down Expand Up @@ -129,6 +135,37 @@ public void validate(BindingResult bindingResult, Event event, List<TicketFieldC

}

// https://github.com/alfio-event/alf.io/issues/573
// only for IT and only if enabled!
if (formValidationParameters.getOrDefault(ConfigurationKeys.ENABLE_ITALY_E_INVOICING, false) && "IT".equals(vatCountryCode)) {

// mandatory
ValidationUtils.rejectIfEmpty(bindingResult, "italyEInvoicingFiscalCode", "error.emptyField");
rejectIfOverLength(bindingResult, "italyEInvoicingFiscalCode", "error.tooLong", italyEInvoicingFiscalCode, 256);
//

//
ValidationUtils.rejectIfEmpty(bindingResult, "italyEInvoicingReferenceType", "error.italyEInvoicingReferenceTypeSelectValue");
//
if (BillingDetails.ItalianEInvoicing.ReferenceType.ADDRESSEE_CODE == italyEInvoicingReferenceType) {
ValidationUtils.rejectIfEmpty(bindingResult, "italyEInvoicingReferenceAddresseeCode", "error.emptyField");
italyEInvoicingReferenceAddresseeCode = StringUtils.trim(italyEInvoicingReferenceAddresseeCode);
if (italyEInvoicingReferenceAddresseeCode != null) {
if (italyEInvoicingReferenceAddresseeCode.length() != 7) {
bindingResult.rejectValue("italyEInvoicingReferenceAddresseeCode", "error.lengthMustBe7");
}

if (!StringUtils.isAlphanumeric(italyEInvoicingReferenceAddresseeCode)) {
bindingResult.rejectValue("italyEInvoicingReferenceAddresseeCode", "error.alphanumeric");
}
}
}
if (BillingDetails.ItalianEInvoicing.ReferenceType.PEC == italyEInvoicingReferenceType) {
ValidationUtils.rejectIfEmpty(bindingResult, "italyEInvoicingReferencePEC", "error.emptyField");
}

}

if (email != null && !email.contains("@") && !bindingResult.hasFieldErrors("email")) {
bindingResult.rejectValue("email", ErrorsCode.STEP_2_INVALID_EMAIL);
}
Expand Down Expand Up @@ -177,6 +214,18 @@ public static ContactAndTicketsForm fromExistingReservation(TicketReservation re
form.setBillingAddressZip(additionalInfo.getBillingAddressZip());
form.setBillingAddressCity(additionalInfo.getBillingAddressCity());
form.setSkipVatNr(additionalInfo.getSkipVatNr());

//todo: simplify code, can avoid a level with map
//https://github.com/alfio-event/alf.io/issues/573
Optional.ofNullable(additionalInfo.getInvoicingAdditionalInfo()).ifPresent(i ->
Optional.ofNullable(i.getItalianEInvoicing()).ifPresent(iei -> {
form.setItalyEInvoicingFiscalCode(iei.getFiscalCode());
form.setItalyEInvoicingReferenceType(iei.getReferenceType());
form.setItalyEInvoicingReferenceAddresseeCode(iei.getAddresseeCode());
form.setItalyEInvoicingReferencePEC(iei.getPec());
})
);

return form;
}

Expand All @@ -195,4 +244,18 @@ public boolean canSkipVatNrCheck() {
public boolean isBusiness() {
return StringUtils.isNotBlank(billingAddressCompany) && !canSkipVatNrCheck() && invoiceRequested;
}

// https://github.com/alfio-event/alf.io/issues/573
public boolean getItalyEInvoicingTypeAddresseeCode() {
return italyEInvoicingReferenceType == BillingDetails.ItalianEInvoicing.ReferenceType.ADDRESSEE_CODE;
}

public boolean getItalyEInvoicingTypePEC() {
return italyEInvoicingReferenceType == BillingDetails.ItalianEInvoicing.ReferenceType.PEC;
}

public boolean getItalyEInvoicingTypeNone() {
return italyEInvoicingReferenceType == BillingDetails.ItalianEInvoicing.ReferenceType.NONE;
}
//
}
3 changes: 3 additions & 0 deletions src/main/java/alfio/manager/AdminReservationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import alfio.model.user.User;
import alfio.repository.*;
import alfio.repository.user.UserRepository;
import alfio.util.Json;
import alfio.util.MonetaryUtil;
import alfio.util.TemplateManager;
import alfio.util.TemplateResource;
Expand Down Expand Up @@ -223,6 +224,8 @@ private Result<Boolean> performUpdate(String reservationId, Event event, TicketR
ticketReservationRepository.updateBillingData(r.getVatStatus(), customerData.getVatNr(), customerData.getVatCountryCode(), r.isInvoiceRequested(), reservationId);
}

ticketReservationRepository.updateInvoicingAdditionalInformation(reservationId, Json.toJson(arm.getCustomerData().getInvoicingAdditionalInfo()));

}

if(arm.isUpdateAdvancedBillingOptions() && event.getVatStatus() != PriceContainer.VatStatus.NONE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ private Stream<AdminReservationModification> spread(AdminReservationModification
.map(p -> {
AdminReservationModification.Attendee attendee = p.getLeft();
String language = StringUtils.defaultIfBlank(attendee.getLanguage(), src.getLanguage());
CustomerData cd = new CustomerData(attendee.getFirstName(), attendee.getLastName(), attendee.getEmailAddress(), null, language, null, null, null);

CustomerData cd = new CustomerData(attendee.getFirstName(), attendee.getLastName(), attendee.getEmailAddress(), null, language, null, null, null, null);
return new AdminReservationModification(src.getExpiration(), cd, singletonList(p.getRight()), language, src.isUpdateContactData(), false, null, src.getNotification());
});
}
Expand Down

0 comments on commit 7c9aaea

Please sign in to comment.