invoicer initial version
Change-Id: Ib20a96c224f5c055874f72f8f9a04a4dc8bbbc24
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/BoostrapInitialData.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/BoostrapInitialData.java
new file mode 100644
index 0000000..b00920c
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/BoostrapInitialData.java
@@ -0,0 +1,49 @@
+package pl.hackerspace;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+import pl.hackerspace.domain.Client;
+import pl.hackerspace.domain.Invoice;
+import pl.hackerspace.repository.ClientRepository;
+import pl.hackerspace.repository.InvoiceRepository;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Component
+@Slf4j
+@RequiredArgsConstructor
+public class BoostrapInitialData implements CommandLineRunner {
+
+ private final ClientRepository clientRepository;
+ private final InvoiceRepository invoiceRepository;
+
+ @Override
+ public void run(String... args) {
+ log.info("Saving new clients");
+ Client client = Client.builder()
+ .price(BigDecimal.valueOf(200))
+ .nip("PL5252497215")
+ .name("Arseniy Sorokin")
+ .addressLine1("ul. Bródnowska 3/23")
+ .addressLine2("03-439 Warszawa, Polska")
+ .serviceName("Dostęp do Internetu - Umowa HSWAW/666 - Opłata abonamentowa %invoice_month_string%")
+ .email("arssorokin@gmail.com")
+ .build();
+ clientRepository.save(client);
+ clientRepository.save(Client.builder()
+ .price(BigDecimal.valueOf(100))
+ .nip("PL111")
+ .name("Pope Francis")
+ .addressLine1("St.Peter's square")
+ .addressLine2("Rome")
+ .serviceName("Dostęp do Internetu - Umowa HSWAW/2137 - Opłata abonamentowa %invoice_month_string%")
+ .email("pope@vatican.va")
+ .build());
+ log.info("Saving last invoice");
+ invoiceRepository.save(Invoice.builder().id(21196).invoiceTitle("FV21196").creationDate(LocalDateTime.now())
+ .client(client).pdfContent(new byte[]{}).build());
+ }
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/SpringBootReactApplication.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/SpringBootReactApplication.java
new file mode 100644
index 0000000..f25cd9c
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/SpringBootReactApplication.java
@@ -0,0 +1,15 @@
+package pl.hackerspace;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@SpringBootApplication
+@EnableJpaRepositories
+public class SpringBootReactApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootReactApplication.class, args);
+ }
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/controller/ClientsController.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/controller/ClientsController.java
new file mode 100644
index 0000000..8bf0650
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/controller/ClientsController.java
@@ -0,0 +1,54 @@
+package pl.hackerspace.controller;
+
+import pl.hackerspace.domain.Client;
+import pl.hackerspace.repository.ClientRepository;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/clients")
+public class ClientsController {
+
+ private final ClientRepository clientRepository;
+
+ public ClientsController(ClientRepository clientRepository) {
+ this.clientRepository = clientRepository;
+ }
+
+ @GetMapping
+ public List<Client> getClients() {
+ return clientRepository.findAll();
+ }
+
+ @GetMapping("/{nip}")
+ public Client getClient(@PathVariable String nip) {
+ return clientRepository.findById(nip).orElseThrow(RuntimeException::new);
+ }
+
+ @PostMapping
+ public ResponseEntity<Client> createClient(@RequestBody Client client) throws URISyntaxException {
+ Client savedClient = clientRepository.save(client);
+ return ResponseEntity.created(new URI("/clients/" + savedClient.getNip())).body(savedClient);
+ }
+
+ @PutMapping("/{nip}")
+ public ResponseEntity<Client> updateClient(@PathVariable String nip, @RequestBody Client client) {
+ Client currentClient = clientRepository.findById(nip).orElseThrow(RuntimeException::new);
+ currentClient.setName(client.getName());
+ currentClient.setEmail(client.getEmail());
+ currentClient = clientRepository.save(client);
+
+ return ResponseEntity.ok(currentClient);
+ }
+
+ @DeleteMapping("/{nip}")
+ public ResponseEntity<Void> deleteClient(@PathVariable String nip) {
+ clientRepository.deleteById(nip);
+ return ResponseEntity.ok().build();
+ }
+
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/controller/InvoicesController.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/controller/InvoicesController.java
new file mode 100644
index 0000000..45844cb
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/controller/InvoicesController.java
@@ -0,0 +1,42 @@
+package pl.hackerspace.controller;
+
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.core.io.Resource;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import pl.hackerspace.domain.Client;
+import pl.hackerspace.domain.Invoice;
+import pl.hackerspace.dto.InvoiceGenerationDto;
+import pl.hackerspace.service.InvoiceService;
+
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/invoices")
+@RequiredArgsConstructor
+@CrossOrigin(value = {"*"}, exposedHeaders = {"Content-Disposition"})
+public class InvoicesController {
+
+ private final InvoiceService invoiceService;
+
+ @PostMapping
+ @ResponseBody
+ public ResponseEntity<Resource> generateSingleInvoice(@RequestBody InvoiceGenerationDto request)
+ throws IOException {
+ return invoiceService.generateNewInvoice(request);
+ }
+
+ @GetMapping("/all")
+ @ResponseBody
+ public void generateAllSubscriberInvoices(HttpServletResponse response, @RequestParam String monthOfInvoice)
+ throws IOException {
+ invoiceService.generateInvoicesForAllSubscribers(response, monthOfInvoice);
+ }
+
+ @GetMapping
+ public List<Invoice> getInvoices() {
+ return invoiceService.findAll();
+ }
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/domain/Client.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/domain/Client.java
new file mode 100644
index 0000000..2dab10f
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/domain/Client.java
@@ -0,0 +1,69 @@
+package pl.hackerspace.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.Set;
+
+@Entity
+@Table(name = "client")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Client {
+
+ @Id
+ private String nip;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false)
+ private String email;
+
+ private String addressLine1;
+ private String addressLine2;
+
+ @Column(nullable = false)
+ private String serviceName;
+
+ @Column(nullable = false)
+ private BigDecimal price;
+
+ @Builder.Default
+ @Column(nullable = false)
+ private BigDecimal amount = BigDecimal.valueOf(1);
+
+ @Builder.Default
+ @Column(nullable = false)
+ private BigDecimal vat = BigDecimal.valueOf(23);
+
+ @Builder.Default
+ @Column(nullable = false)
+ private int paymentOffsetDays = 14;
+
+ @Builder.Default
+ private boolean subscriber = true;
+
+ private boolean prepaid;
+
+ @OneToMany(mappedBy="client")
+ Set<Invoice> invoices;
+
+ public byte[] getInvoiceForSubscriptionMonth(String monthOfInvoice) {
+ return invoices.stream()
+ .filter(i -> monthOfInvoice.equals(i.getMonthOfSubscription()))
+ .map(Invoice::getPdfContent)
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/domain/Invoice.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/domain/Invoice.java
new file mode 100644
index 0000000..e32f5fe
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/domain/Invoice.java
@@ -0,0 +1,41 @@
+package pl.hackerspace.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "invoice")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Invoice {
+
+ @Id
+ private long id;
+
+ @Column(nullable = false)
+ private String invoiceTitle;
+
+ private String monthOfSubscription;
+
+ @Column(nullable = false)
+ private LocalDateTime creationDate;
+
+ @Column(columnDefinition = "VARBINARY(50000)", nullable = false)
+ private byte[] pdfContent;
+
+ @ManyToOne
+ @JoinColumn(nullable = false)
+ private Client client;
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/dto/CustomInvoiceDataDto.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/dto/CustomInvoiceDataDto.java
new file mode 100644
index 0000000..06494e2
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/dto/CustomInvoiceDataDto.java
@@ -0,0 +1,23 @@
+package pl.hackerspace.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class CustomInvoiceDataDto {
+
+ private String customServiceName;
+
+ private BigDecimal customPrice;
+
+ private BigDecimal customAmount = BigDecimal.valueOf(1);
+
+ private BigDecimal customVat = BigDecimal.valueOf(23);
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/dto/InvoiceGenerationDto.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/dto/InvoiceGenerationDto.java
new file mode 100644
index 0000000..ce86085
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/dto/InvoiceGenerationDto.java
@@ -0,0 +1,22 @@
+package pl.hackerspace.dto;
+
+import jakarta.persistence.Column;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class InvoiceGenerationDto {
+
+ private String nip;
+
+ private String monthOfInvoice;
+
+ private CustomInvoiceDataDto customInvoiceData;
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/repository/ClientRepository.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/repository/ClientRepository.java
new file mode 100644
index 0000000..d3eb10f
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/repository/ClientRepository.java
@@ -0,0 +1,12 @@
+package pl.hackerspace.repository;
+
+import org.springframework.stereotype.Repository;
+import pl.hackerspace.domain.Client;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+@Repository
+public interface ClientRepository extends JpaRepository<Client, String> {
+ List<Client> findBySubscriberTrue();
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/repository/InvoiceRepository.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/repository/InvoiceRepository.java
new file mode 100644
index 0000000..f112c3b
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/repository/InvoiceRepository.java
@@ -0,0 +1,14 @@
+package pl.hackerspace.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+import pl.hackerspace.domain.Client;
+import pl.hackerspace.domain.Invoice;
+
+@Repository
+public interface InvoiceRepository extends JpaRepository<Invoice, String> {
+
+ @Query(value = "SELECT max(id) FROM Invoice")
+ long getMaxId();
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/service/InvoiceService.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/service/InvoiceService.java
new file mode 100644
index 0000000..2cdb88e
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/service/InvoiceService.java
@@ -0,0 +1,143 @@
+package pl.hackerspace.service;
+
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.ContentDisposition;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StreamUtils;
+import pl.hackerspace.domain.Client;
+import pl.hackerspace.domain.Invoice;
+import pl.hackerspace.dto.CustomInvoiceDataDto;
+import pl.hackerspace.dto.InvoiceGenerationDto;
+import pl.hackerspace.repository.ClientRepository;
+import pl.hackerspace.repository.InvoiceRepository;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static pl.hackerspace.service.TemplateService.withTemplate;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class InvoiceService {
+
+ private final ClientRepository clientRepository;
+
+ private final InvoiceRepository invoiceRepository;
+
+ @Transactional
+ public ResponseEntity<Resource> generateNewInvoice(InvoiceGenerationDto generationRequest) throws IOException {
+ Client client = clientRepository.findById(generationRequest.getNip()).orElseThrow(() -> new RuntimeException("Not found"));
+ return withTemplate(template -> {
+ byte[] invoice;
+ if (!client.isSubscriber() || generationRequest.getCustomInvoiceData() != null) {
+ invoice = createPdfInvoice(client, template, null, generationRequest.getCustomInvoiceData());
+ } else {
+ invoice = getOrCreateSubscriptionPdfInvoice(template, client, generationRequest.getMonthOfInvoice());
+ }
+ return ResponseEntity.ok()
+ .headers(getHttpHeaders(client.getName() + " " + generationRequest.getMonthOfInvoice()))
+ .contentLength(-1)
+ .contentType(MediaType.APPLICATION_PDF)
+ .body(new ByteArrayResource(invoice));
+ });
+ }
+
+
+ @Transactional
+ public void generateInvoicesForAllSubscribers(HttpServletResponse response, String monthOfInvoice) throws IOException {
+ setHeaders(response, "application/zip", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE) + ".zip");
+ List<Client> subscribers = clientRepository.findBySubscriberTrue();
+ withTemplate(template -> {
+ try (ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream())) {
+ for (Client client : subscribers) {
+ try (InputStream inputStream = new ByteArrayInputStream(
+ getOrCreateSubscriptionPdfInvoice(template, client, monthOfInvoice))) {
+ zipOutputStream.putNextEntry(new ZipEntry(getPdfFilename(client.getName() + " "
+ + monthOfInvoice)));
+ StreamUtils.copy(inputStream, zipOutputStream);
+ zipOutputStream.flush();
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ });
+ }
+
+ private byte[] getOrCreateSubscriptionPdfInvoice(String template, Client client, final String monthOfInvoice) {
+ byte[] invoice = client.getInvoiceForSubscriptionMonth(monthOfInvoice);
+ if (invoice == null) {
+ invoice = createPdfInvoice(client, template, monthOfInvoice, null);
+ }
+ return invoice;
+ }
+
+ private static void setHeaders(HttpServletResponse response, String contentType, String filename) {
+ response.setContentType(contentType);
+ response.setHeader(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
+ .filename(filename, StandardCharsets.UTF_8)
+ .build()
+ .toString());
+ }
+
+ private byte[] createPdfInvoice(Client client, String template, String monthOfSubscription,
+ CustomInvoiceDataDto customInvoiceData) {
+ LocalDateTime creationDate = LocalDateTime.now();
+ Invoice newInvoice = createNewInvoice(client, creationDate, monthOfSubscription);
+ byte[] bytes = TemplateService.convertHtmlToPdf(template, client, creationDate,
+ newInvoice.getInvoiceTitle(), monthOfSubscription, customInvoiceData);
+ newInvoice.setPdfContent(bytes);
+ save(newInvoice);
+ return bytes;
+ }
+
+ private static HttpHeaders getHttpHeaders(final String filename) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION);
+ headers.add(HttpHeaders.CONTENT_TYPE, "application/octet-stream");
+ headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=" + getPdfFilename(filename));
+ return headers;
+ }
+
+ private static String getPdfFilename(String filename) {
+ return filename + ".pdf";
+ }
+
+ public Invoice createNewInvoice(Client client, final LocalDateTime creationDate, String monthOfSubscription) {
+ long nextInvoiceId = invoiceRepository.getMaxId() + 1;
+ return Invoice.builder().id(nextInvoiceId)
+ .invoiceTitle("FV" + nextInvoiceId)
+ .creationDate(creationDate)
+ .monthOfSubscription(monthOfSubscription)
+ .client(client)
+ .build();
+ }
+
+ public void save(Invoice newInvoice) {
+ invoiceRepository.save(newInvoice);
+ }
+
+ public List<Invoice> findAll() {
+ return invoiceRepository.findAll();
+ }
+}
diff --git a/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/service/TemplateService.java b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/service/TemplateService.java
new file mode 100644
index 0000000..5f8ee31
--- /dev/null
+++ b/personal/arsenicum/invoicer/src/main/java/pl/hackerspace/service/TemplateService.java
@@ -0,0 +1,130 @@
+package pl.hackerspace.service;
+
+import com.lowagie.text.pdf.BaseFont;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.springframework.stereotype.Service;
+import org.xhtmlrenderer.layout.SharedContext;
+import org.xhtmlrenderer.pdf.ITextRenderer;
+import pl.hackerspace.domain.Client;
+import pl.hackerspace.dto.CustomInvoiceDataDto;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Slf4j
+@Service
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class TemplateService {
+
+ public static final DecimalFormat MONEY_FORMAT = twoDecimalsFormatter();
+ public static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM");
+ public static final String HTML_TEMPLATE_FILE = "invoiceTemplates/invoice_one_service.html";
+
+ public static <A> A withTemplate(Function<String, A> execute) throws IOException {
+ try (InputStream resourceAsStream = TemplateService.class.getClassLoader().getResourceAsStream(HTML_TEMPLATE_FILE)) {
+ String template = new String(resourceAsStream.readAllBytes());
+ return execute.apply(template);
+ }
+ }
+
+ static String populateTemplate(String unprocessedTemplate, Client client, final String subscriptionMonth,
+ final LocalDateTime invoiceCreationDate, final String invoiceTitle,
+ CustomInvoiceDataDto customInvoiceData) {
+ boolean isCustom = customInvoiceData != null;
+ BigDecimal price = isCustom ? customInvoiceData.getCustomPrice() : client.getPrice();
+ BigDecimal amount = isCustom ? customInvoiceData.getCustomAmount() : client.getAmount();
+ BigDecimal vat = isCustom ? customInvoiceData.getCustomVat() : client.getVat();
+ String serviceName = isCustom ? customInvoiceData.getCustomServiceName() : client.getServiceName();
+ BigDecimal totalNet = amount.multiply(price);
+ String processedHtml = unprocessedTemplate
+ .replace("%client_name%", client.getName())
+ .replace("%invoice_title%", invoiceTitle)
+ .replace("%client_price%", formatAsMoney(price))
+ .replace("%client_addressLine1%", client.getAddressLine1())
+ .replace("%client_addressLine2%", client.getAddressLine2())
+ .replace("%client_service_name%", serviceName)
+ .replace("%client_nip%", Optional.ofNullable(client.getNip()).map(nip -> "NIP: " + nip).orElse(""))
+ .replace("%client_total_net%", formatAsMoney(totalNet))
+ .replace("%client_total_gross%", formatAsMoney(totalNet.add(percentageValue(totalNet, vat))))
+ .replace("%client_amount%", formatAsMoney(amount))
+ .replace("%client_vat%", formatAsMoney(vat))
+ .replace("%client_total_tax%", formatAsMoney(percentageValue(totalNet, vat)))
+ .replace("%invoice_month_string%", subscriptionMonth)
+ .replace("%client_payment_date%", formatAsDate(invoiceCreationDate.plusDays(client.getPaymentOffsetDays())))
+ .replace("%invoice_date%", formatAsDate(invoiceCreationDate));
+ validateAllPlaceholdersSubstituted(processedHtml);
+ return processedHtml;
+ }
+
+ private static String formatAsMoney(BigDecimal totalNet) {
+ return MONEY_FORMAT.format(totalNet);
+ }
+
+ private static String formatAsDate(LocalDateTime invoiceCreationDate) {
+ return invoiceCreationDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+
+ private static void validateAllPlaceholdersSubstituted(String processed) {
+ Set<String> allMatches = new HashSet<>();
+ Matcher m = Pattern.compile("%(\\w*?)%").matcher(processed);
+ while (m.find()) {
+ allMatches.add(m.group());
+ }
+ if (!allMatches.isEmpty()) {
+ throw new IllegalStateException("There are unsubstituted placeholders in the template: " + allMatches);
+ }
+ }
+
+ private static DecimalFormat twoDecimalsFormatter() {
+ DecimalFormat df = new DecimalFormat();
+ df.setMaximumFractionDigits(2);
+ df.setMinimumFractionDigits(2);
+ df.setGroupingUsed(false);
+ return df;
+ }
+
+ public static BigDecimal percentageValue(BigDecimal base, BigDecimal pct) {
+ return base.multiply(pct).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
+ }
+
+ static byte[] convertHtmlToPdf(String unprocessedTemplate, Client client, final LocalDateTime creationDate,
+ final String invoiceTitle, final String monthOfInvoice, CustomInvoiceDataDto customInvoiceData) {
+ String processedHtml = populateTemplate(unprocessedTemplate, client, monthOfInvoice, creationDate, invoiceTitle,
+ customInvoiceData);
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ ITextRenderer renderer = new ITextRenderer();
+ SharedContext sharedContext = renderer.getSharedContext();
+ sharedContext.setPrint(true);
+ sharedContext.setInteractive(false);
+ renderer.getFontResolver().addFont("fonts/calibri.ttf", BaseFont.IDENTITY_H, true);
+ renderer.setDocumentFromString(htmlToXhtml(processedHtml));
+ renderer.layout();
+ renderer.createPDF(outputStream);
+ return outputStream.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException("Exception during pdf conversion", e);
+ }
+ }
+
+ private static String htmlToXhtml(String html) {
+ Document document = Jsoup.parse(html);
+ document.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
+ return document.html();
+ }
+}