Skip to content

Commit

Permalink
#476 - generate and save credit note
Browse files Browse the repository at this point in the history
  • Loading branch information
cbellone committed Dec 16, 2018
1 parent 36d2a11 commit c95f2db
Show file tree
Hide file tree
Showing 17 changed files with 319 additions and 77 deletions.
18 changes: 2 additions & 16 deletions src/main/java/alfio/controller/InvoiceReceiptController.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@
import alfio.manager.FileUploadManager;
import alfio.manager.TicketReservationManager;
import alfio.model.Event;
import alfio.model.OrderSummary;
import alfio.model.TicketReservation;
import alfio.repository.EventRepository;
import alfio.util.Json;
import alfio.util.TemplateManager;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand All @@ -33,13 +31,14 @@
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;

import static alfio.util.FileUtil.sendPdf;

@Controller
@RequiredArgsConstructor
public class InvoiceReceiptController {
Expand All @@ -57,19 +56,6 @@ private ResponseEntity<Void> handleReservationWith(String eventName, String rese
).orElse(notFound);
}

private static boolean sendPdf(byte[] res, HttpServletResponse response, String eventName, String reservationId, String type) {
return Optional.ofNullable(res).map(pdf -> {
try {
response.setHeader("Content-Disposition", "attachment; filename=\"" + type+ "-" + eventName + "-" + reservationId + ".pdf\"");
response.setContentType("application/pdf");
response.getOutputStream().write(pdf);
return true;
} catch(IOException e) {
return false;
}
}).orElse(false);
}

@RequestMapping("/event/{eventName}/reservation/{reservationId}/receipt")
public ResponseEntity<Void> getReceipt(@PathVariable("eventName") String eventName,
@PathVariable("reservationId") String reservationId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import alfio.controller.api.support.PageAndContent;
import alfio.manager.AdminReservationManager;
import alfio.manager.EventManager;
import alfio.manager.FileUploadManager;
import alfio.manager.TicketReservationManager;
import alfio.model.*;
import alfio.model.modification.AdminReservationModification;
Expand All @@ -30,8 +31,10 @@
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.security.Principal;
import java.util.Collections;
Expand All @@ -40,6 +43,8 @@
import java.util.Map;
import java.util.stream.Collectors;

import static alfio.util.FileUtil.sendPdf;

@RequestMapping("/admin/api/reservation")
@RestController
@AllArgsConstructor
Expand All @@ -49,6 +54,7 @@ public class AdminReservationApiController {
private final EventManager eventManager;
private final EventRepository eventRepository;
private final TicketReservationManager ticketReservationManager;
private final FileUploadManager fileUploadManager;

@RequestMapping(value = "/event/{eventName}/new", method = RequestMethod.POST)
public Result<String> createNew(@PathVariable("eventName") String eventName, @RequestBody AdminReservationModification reservation, Principal principal) {
Expand Down Expand Up @@ -97,6 +103,52 @@ public Result<List<Audit>> getAudit(@PathVariable("eventName") String eventName,
return adminReservationManager.getAudit(eventName, reservationId, principal.getName());
}

@GetMapping("/event/{eventName}/{reservationId}/billing-documents")
public Result<List<BillingDocument>> getBillingDocuments(@PathVariable("eventName") String eventName, @PathVariable("reservationId") String reservationId, Principal principal) {
return adminReservationManager.getBillingDocuments(eventName, reservationId, principal.getName());
}

@DeleteMapping("/event/{eventName}/{reservationId}/billing-document/{documentId}")
public ResponseEntity<Boolean> invalidateBillingDocument(@PathVariable("eventName") String eventName,
@PathVariable("reservationId") String reservationId,
@PathVariable("documentId") long documentId,
Principal principal) {
Result<Boolean> invalidateResult = adminReservationManager.invalidateBillingDocument(eventName, reservationId, documentId, principal.getName());
if(invalidateResult.isSuccess()) {
return ResponseEntity.ok(invalidateResult.getData());
} else {
return ResponseEntity.badRequest().build();
}
}

@PutMapping("/event/{eventName}/{reservationId}/billing-document/{documentId}/restore")
public ResponseEntity<Boolean> restoreBillingDocument(@PathVariable("eventName") String eventName,
@PathVariable("reservationId") String reservationId,
@PathVariable("documentId") long documentId,
Principal principal) {
Result<Boolean> restoreResult = adminReservationManager.restoreBillingDocument(eventName, reservationId, documentId, principal.getName());
if(restoreResult.isSuccess()) {
return ResponseEntity.ok(restoreResult.getData());
} else {
return ResponseEntity.badRequest().build();
}
}

@GetMapping("/event/{eventName}/{reservationId}/billing-document/{documentId}")
public ResponseEntity<Void> getBillingDocument(@PathVariable("eventName") String eventName,
@PathVariable("reservationId") String reservationId,
@PathVariable("documentId") long documentId,
Principal principal,
HttpServletResponse response) {
Result<Boolean> result = adminReservationManager.getSingleBillingDocumentAsPdf(eventName, reservationId, documentId, principal.getName())
.map(res -> sendPdf(res.getRight(), response, eventName, reservationId, res.getLeft().getType().toString().toLowerCase()));
if(result.isSuccess()) {
return ResponseEntity.ok(null);
} else {
return ResponseEntity.notFound().build();
}
}

@RequestMapping(value = "/event/{eventName}/{reservationId}", method = RequestMethod.GET)
public Result<TicketReservationDescriptor> loadReservation(@PathVariable("eventName") String eventName, @PathVariable("reservationId") String reservationId, Principal principal) {
return adminReservationManager.loadReservation(eventName, reservationId, principal.getName())
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/alfio/controller/support/TemplateProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,18 @@ private static Optional<byte[]> buildReceiptOrInvoicePdf(Event event, FileUpload
}
}

public static Optional<byte[]> buildBillingDocumentPdf(BillingDocument.Type documentType,Event event, FileUploadManager fileUploadManager, Locale language, TemplateManager templateManager, Map<String, Object> model) {
switch (documentType) {
case INVOICE:
return buildInvoicePdf(event, fileUploadManager, language, templateManager, model);
case RECEIPT:
return buildReceiptPdf(event, fileUploadManager, language, templateManager, model);
case CREDIT_NOTE:
return buildCreditNotePdf(event, fileUploadManager, language, templateManager, model);
default:
throw new IllegalStateException(documentType + " not supported");
}
}

public static Optional<byte[]> buildReceiptPdf(Event event, FileUploadManager fileUploadManager, Locale language, TemplateManager templateManager, Map<String, Object> model) {
return buildReceiptOrInvoicePdf(event, fileUploadManager, language, templateManager, model, TemplateResource.RECEIPT_PDF);
Expand Down
41 changes: 39 additions & 2 deletions src/main/java/alfio/manager/AdminReservationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package alfio.manager;

import alfio.controller.support.TemplateProcessor;
import alfio.manager.support.DuplicateReferenceException;
import alfio.model.*;
import alfio.model.TicketReservation.TicketReservationStatus;
Expand All @@ -34,7 +35,6 @@
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 @@ -98,6 +98,8 @@ public class AdminReservationManager {
private final AuditingRepository auditingRepository;
private final UserRepository userRepository;
private final ExtensionManager extensionManager;
private final BillingDocumentRepository billingDocumentRepository;
private final FileUploadManager fileUploadManager;

//the following methods have an explicit transaction handling, therefore the @Transactional annotation is not helpful here
public Result<Triple<TicketReservation, List<Ticket>, Event>> confirmReservation(String eventName, String reservationId, String username) {
Expand Down Expand Up @@ -535,11 +537,46 @@ public void removeTickets(String eventName, String reservationId, List<Integer>
});
}

@Transactional
@Transactional(readOnly = true)
public Result<List<Audit>> getAudit(String eventName, String reservationId, String username) {
return loadReservation(eventName, reservationId, username).map((res) -> auditingRepository.findAllForReservation(reservationId));
}

@Transactional(readOnly = true)
public Result<List<BillingDocument>> getBillingDocuments(String eventName, String reservationId, String username) {
return loadReservation(eventName, reservationId, username).map((res) -> billingDocumentRepository.findAllByReservationId(reservationId));
}

@Transactional(readOnly = true)
public Result<Pair<BillingDocument, byte[]>> getSingleBillingDocumentAsPdf(String eventName, String reservationId, long documentId, String username) {
return loadReservation(eventName, reservationId, username).map(res -> {
BillingDocument billingDocument = billingDocumentRepository.findById(documentId, reservationId).orElseThrow(IllegalArgumentException::new);
Function<Map<String, Object>, Optional<byte[]>> pdfGenerator = model -> TemplateProcessor.buildBillingDocumentPdf(billingDocument.getType(), res.getRight(), fileUploadManager, new Locale(res.getLeft().getUserLanguage()), templateManager, model);
Map<String, Object> billingModel = billingDocument.getModel();
return Pair.of(billingDocument, pdfGenerator.apply(billingModel).orElse(null));
});
}

@Transactional
public Result<Boolean> invalidateBillingDocument(String eventName, String reservationId, long documentId, String username) {
return updateBillingDocumentStatus(eventName, reservationId, documentId, username, BillingDocument.Status.NOT_VALID, Audit.EventType.BILLING_DOCUMENT_INVALIDATED);
}

@Transactional
public Result<Boolean> restoreBillingDocument(String eventName, String reservationId, long documentId, String username) {
return updateBillingDocumentStatus(eventName, reservationId, documentId, username, BillingDocument.Status.VALID, Audit.EventType.BILLING_DOCUMENT_RESTORED);
}

private Result<Boolean> updateBillingDocumentStatus(String eventName, String reservationId, long documentId, String username, BillingDocument.Status status, Audit.EventType eventType) {
return loadReservation(eventName, reservationId, username).map(res -> {
Integer userId = userRepository.findIdByUserName(username).orElse(null);
auditingRepository.insert(reservationId, userId, res.getRight().getId(), eventType, new Date(), RESERVATION, String.valueOf(documentId));
return billingDocumentRepository.updateStatus(documentId, status, reservationId) == 1;
});


}

@Transactional
public Result<TransactionAndPaymentInfo> getPaymentInfo(String eventName, String reservationId, String username) {
return loadReservation(eventName, reservationId, username)
Expand Down
42 changes: 21 additions & 21 deletions src/main/java/alfio/manager/TicketReservationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@
import java.util.stream.Stream;

import static alfio.model.Audit.EntityType.RESERVATION;
import static alfio.model.BillingDocument.Type.INVOICE;
import static alfio.model.BillingDocument.Type.RECEIPT;
import static alfio.model.BillingDocument.Type.*;
import static alfio.model.PromoCodeDiscount.categoriesOrNull;
import static alfio.model.TicketReservation.TicketReservationStatus.*;
import static alfio.model.system.ConfigurationKeys.*;
Expand Down Expand Up @@ -525,7 +524,8 @@ public void sendConfirmationEmail(Event event, TicketReservation ticketReservati
Map<String, Object> reservationEmailModel = prepareModelForReservationEmail(event, ticketReservation);
List<Mailer.Attachment> attachments = new ArrayList<>(1);
if(mustGenerateBillingDocument(summary, ticketReservation)) { //#459 - include PDF invoice in reservation email
attachments = generateBillingDocumentAttachment(event, ticketReservation, language, getOrCreateBillingDocumentModel(event, ticketReservation, null));
BillingDocument.Type type = ticketReservation.getHasInvoiceNumber() ? INVOICE : RECEIPT;
attachments = generateBillingDocumentAttachment(event, ticketReservation, language, getOrCreateBillingDocumentModel(event, ticketReservation, null), type);
}

notificationManager.sendSimpleEmail(event, ticketReservation.getEmail(), getReservationEmailSubject(event, language, "reservation-email-subject", getShortReservationID(event, reservationId)),
Expand All @@ -536,30 +536,25 @@ private static boolean mustGenerateBillingDocument(OrderSummary summary, TicketR
return !summary.getFree() && (!summary.getNotYetPaid() || (summary.getWaitingForPayment() && ticketReservation.isInvoiceRequested()));
}

private static List<Mailer.Attachment> generateBillingDocumentAttachment(Event event,
TicketReservation ticketReservation,
Locale language,
Map<String, Object> billingDocumentModel) {
return generateBillingDocumentAttachment(event, ticketReservation, language, billingDocumentModel, false);
}

private static List<Mailer.Attachment> generateBillingDocumentAttachment(Event event,
TicketReservation ticketReservation,
Locale language,
Map<String, Object> billingDocumentModel,
boolean creditNote) {
BillingDocument.Type documentType) {
Map<String, String> model = new HashMap<>();
model.put("reservationId", ticketReservation.getId());
model.put("eventId", Integer.toString(event.getId()));
model.put("language", Json.toJson(language));
model.put("reservationEmailModel", Json.toJson(billingDocumentModel));
boolean hasInvoiceNumber = ticketReservation.getHasInvoiceNumber();
if(hasInvoiceNumber && creditNote) {
return Collections.singletonList(new Mailer.Attachment("credit-note.pdf", null, "application/pdf", model, Mailer.AttachmentIdentifier.CREDIT_NOTE_PDF));
} else if(hasInvoiceNumber) {
return Collections.singletonList(new Mailer.Attachment("invoice.pdf", null, "application/pdf", model, Mailer.AttachmentIdentifier.INVOICE_PDF));
} else {
return Collections.singletonList(new Mailer.Attachment("receipt.pdf", null, "application/pdf", model, Mailer.AttachmentIdentifier.RECEIPT_PDF));
model.put("reservationEmailModel", Json.toJson(billingDocumentModel));//ticketReservation.getHasInvoiceNumber()
switch (documentType) {
case INVOICE:
return Collections.singletonList(new Mailer.Attachment("invoice.pdf", null, "application/pdf", model, Mailer.AttachmentIdentifier.INVOICE_PDF));
case RECEIPT:
return Collections.singletonList(new Mailer.Attachment("receipt.pdf", null, "application/pdf", model, Mailer.AttachmentIdentifier.RECEIPT_PDF));
case CREDIT_NOTE:
return Collections.singletonList(new Mailer.Attachment("credit-note.pdf", null, "application/pdf", model, Mailer.AttachmentIdentifier.CREDIT_NOTE_PDF));
default:
throw new IllegalStateException(documentType+" is not supported");
}
}

Expand Down Expand Up @@ -593,11 +588,12 @@ void issueCreditNoteForReservation(Event event, String reservationId, String use
ticketReservationRepository.updateReservationStatus(reservationId, TicketReservationStatus.CREDIT_NOTE_ISSUED.toString());
auditingRepository.insert(reservationId, userRepository.nullSafeFindIdByUserName(username).orElse(null), event.getId(), Audit.EventType.CREDIT_NOTE_ISSUED, new Date(), RESERVATION, reservationId);
Map<String, Object> model = prepareModelForReservationEmail(event, reservation);
Map<String, Object> billingDocumentModel = createBillingDocumentModel(event, reservation, username, BillingDocument.Type.CREDIT_NOTE);
notificationManager.sendSimpleEmail(event,
reservation.getEmail(),
getReservationEmailSubject(event, getReservationLocale(reservation), "credit-note-issued-email-subject", reservation.getId()),
() -> templateManager.renderTemplate(event, TemplateResource.CREDIT_NOTE_ISSUED_EMAIL, model, getReservationLocale(reservation)),
generateBillingDocumentAttachment(event, reservation, getReservationLocale(reservation), model, true)
generateBillingDocumentAttachment(event, reservation, getReservationLocale(reservation), billingDocumentModel, CREDIT_NOTE)
);
}

Expand All @@ -621,6 +617,10 @@ void ensureBillingDocumentIsPresent(Event event, TicketReservation reservation,

@Transactional
Map<String, Object> createBillingDocumentModel(Event event, TicketReservation reservation, String username) {
return createBillingDocumentModel(event, reservation, username, reservation.getHasInvoiceNumber() ? INVOICE : RECEIPT);
}

private Map<String, Object> createBillingDocumentModel(Event event, TicketReservation reservation, String username, BillingDocument.Type type) {
Optional<String> vat = getVAT(event);
String existingModel = reservation.getInvoiceModel();
boolean existingModelPresent = StringUtils.isNotBlank(existingModel);
Expand All @@ -631,7 +631,7 @@ Map<String, Object> createBillingDocumentModel(Event event, TicketReservation re
//we still save invoice/receipt model to tickets_reservation for backward compatibility
ticketReservationRepository.addReservationInvoiceOrReceiptModel(reservation.getId(), Json.toJson(summary));
}
AffectedRowCountAndKey<Integer> doc = billingDocumentRepository.insert(event.getId(), reservation.getId(), number, reservation.getHasInvoiceNumber() ? INVOICE : RECEIPT, Json.toJson(model), ZonedDateTime.now());
AffectedRowCountAndKey<Long> doc = billingDocumentRepository.insert(event.getId(), reservation.getId(), number, type, Json.toJson(model), ZonedDateTime.now());
auditingRepository.insert(reservation.getId(), userRepository.nullSafeFindIdByUserName(username).orElse(null), event.getId(), Audit.EventType.BILLING_DOCUMENT_GENERATED, new Date(), Audit.EntityType.RESERVATION, reservation.getId(), singletonList(singletonMap("documentId", doc.getKey())));
return model;
}
Expand Down
Loading

0 comments on commit c95f2db

Please sign in to comment.