diff --git a/task04/.gitignore b/task04/.gitignore
new file mode 100644
index 0000000..b56f1dd
--- /dev/null
+++ b/task04/.gitignore
@@ -0,0 +1,33 @@
+README.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/task04/pom.xml b/task04/pom.xml
new file mode 100644
index 0000000..229d8dc
--- /dev/null
+++ b/task04/pom.xml
@@ -0,0 +1,89 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.0.5
+
+
+ de.hststuttgart.vs
+ task04
+ 0.0.1-SNAPSHOT
+ task04
+ task04
+
+ 17
+
+
+
+ org.springframework.boot
+ spring-boot-starter-hateoas
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.webjars
+ webjars-locator
+ 0.45
+
+
+ org.webjars
+ hal-explorer
+ 1.2.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+ test
+
+
+
+
+
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+ 2.2.1
+
+
+ generate-docs
+ prepare-package
+
+ process-asciidoc
+
+
+ html
+ book
+ src/test/resources/templates
+ index.adoc
+ target/generated-docs
+
+
+
+
+
+ org.springframework.restdocs
+ spring-restdocs-asciidoctor
+ ${spring-restdocs.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/Task04Application.java b/task04/src/main/java/de/hststuttgart/vs/task04/Task04Application.java
new file mode 100644
index 0000000..3e8966c
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/Task04Application.java
@@ -0,0 +1,13 @@
+package de.hststuttgart.vs.task04;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Task04Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Task04Application.class, args);
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/CreditNotesAPI.java b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/CreditNotesAPI.java
new file mode 100644
index 0000000..1d98c35
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/CreditNotesAPI.java
@@ -0,0 +1,34 @@
+package de.hststuttgart.vs.task04.api.v1;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import de.hststuttgart.vs.task04.api.v1.mapper.CreditNoteMapper;
+import de.hststuttgart.vs.task04.bm.InvoiceController;
+import de.hststuttgart.vs.task04.bm.exceptions.CreditNoteNotFound;
+
+@RestController
+@RequestMapping("/credit-notes")
+public class CreditNotesAPI {
+
+ private final InvoiceController invoiceController;
+
+ public CreditNotesAPI(final InvoiceController invoiceController) {
+ this.invoiceController = invoiceController;
+ }
+
+ @GetMapping("/{creditNoteId}")
+ public ResponseEntity getCreditNote(@PathVariable final String creditNoteId) {
+ try {
+ final var creditNote = invoiceController.getCreditNote(creditNoteId);
+ final var mappedCreditNote = CreditNoteMapper.map(creditNote);
+ return ResponseEntity.ok(mappedCreditNote);
+ } catch (CreditNoteNotFound e) {
+ return ResponseEntity.notFound().build();
+ }
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/InvoiceAPI.java b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/InvoiceAPI.java
new file mode 100644
index 0000000..866c6b4
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/InvoiceAPI.java
@@ -0,0 +1,104 @@
+package de.hststuttgart.vs.task04.api.v1;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
+
+import java.util.stream.Collectors;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import de.hststuttgart.vs.task04.api.v1.mapper.CreditNoteMapper;
+import de.hststuttgart.vs.task04.api.v1.mapper.InvoiceMapper;
+import de.hststuttgart.vs.task04.api.v1.models.FullCreditNoteRequest;
+import de.hststuttgart.vs.task04.api.v1.models.Invoices;
+import de.hststuttgart.vs.task04.bm.InvoiceController;
+import de.hststuttgart.vs.task04.bm.exceptions.CreditNoteAlreadyExists;
+import de.hststuttgart.vs.task04.bm.exceptions.InvalidOffset;
+import de.hststuttgart.vs.task04.bm.exceptions.InvoiceNotFound;
+import de.hststuttgart.vs.task04.bm.model.PagedInvoices;
+
+@RestController
+@RequestMapping("/invoices")
+public class InvoiceAPI {
+
+ private final InvoiceController invoiceController;
+
+ public InvoiceAPI(final InvoiceController invoiceController) {
+ this.invoiceController = invoiceController;
+ }
+
+ @GetMapping
+ public ResponseEntity getInvoices(
+ @RequestParam(defaultValue = "1") final int offset,
+ @RequestParam(defaultValue = "5") final int limit
+ ) {
+ try {
+ final PagedInvoices pagedInvoices = invoiceController.getInvoices(offset, limit);
+
+ final var mappedInvoices =
+ pagedInvoices.getInvoices().stream().map(InvoiceMapper::map).collect(Collectors.toList());
+
+ final var invoices = new Invoices();
+ invoices.setInvoices(mappedInvoices);
+ // TODO 01: Add self, next and previous link
+ // next should only be displayed if there is a next page
+ // previous should only be displayed if it isn't the first page
+
+ return ResponseEntity.ok(invoices);
+ } catch (final InvalidOffset e) {
+ System.out.println("Offset was invalid");
+ return ResponseEntity.notFound().build();
+ }
+
+
+ }
+
+ @GetMapping("/{invoiceId}")
+ public ResponseEntity getInvoice(@PathVariable("invoiceId") final String invoiceId) {
+ try {
+ final var invoice = invoiceController.getInvoice(invoiceId);
+ final var mappedInvoice = InvoiceMapper.map(invoice);
+ return ResponseEntity.ok(mappedInvoice);
+ } catch (final InvoiceNotFound e) {
+ System.out.println("Invoice " + invoiceId + " not found");
+ return ResponseEntity.notFound().build();
+ }
+ }
+
+ @PostMapping("/{invoiceId}/full-credit-notes")
+ public ResponseEntity createCreditNote(
+ @PathVariable("invoiceId") final String invoiceId,
+ @RequestBody final FullCreditNoteRequest fullCreditNoteRequest) {
+ try {
+ final var creditNote = invoiceController.createCreditNotes(invoiceId, fullCreditNoteRequest.getReason());
+ final var mappedCreditNote = CreditNoteMapper.map(creditNote);
+ return ResponseEntity.ok(mappedCreditNote);
+ } catch (final InvoiceNotFound e) {
+ System.out.println("Invoice " + invoiceId + " not found");
+ return ResponseEntity.notFound().build();
+ } catch (final CreditNoteAlreadyExists e) {
+ System.out.println("Credit Note already exists");
+ return ResponseEntity.status(409).build();
+ }
+ }
+
+ @GetMapping("/{invoiceId}/credit-notes")
+ public ResponseEntity getCreditNotes(@PathVariable("invoiceId") final String invoiceId) {
+ try {
+ final var creditNotes = invoiceController.getCreditNotes(invoiceId);
+ final var mappedCreditNotes = creditNotes.stream().map(CreditNoteMapper::map).toList();
+ return ResponseEntity.ok(mappedCreditNotes);
+ } catch (final InvoiceNotFound e) {
+ System.out.println("Invoice " + invoiceId + " not found");
+ return ResponseEntity.notFound().build();
+ }
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/mapper/CreditNoteMapper.java b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/mapper/CreditNoteMapper.java
new file mode 100644
index 0000000..0c14e41
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/mapper/CreditNoteMapper.java
@@ -0,0 +1,33 @@
+package de.hststuttgart.vs.task04.api.v1.mapper;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
+
+import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
+
+import de.hststuttgart.vs.task04.api.v1.CreditNotesAPI;
+import de.hststuttgart.vs.task04.api.v1.InvoiceAPI;
+import de.hststuttgart.vs.task04.api.v1.models.CreditNoteDO;
+import de.hststuttgart.vs.task04.bm.model.CreditNote;
+
+
+public class CreditNoteMapper {
+
+ public static CreditNoteDO map(final CreditNote creditNote) {
+ final var creditNoteDO = new CreditNoteDO();
+
+ creditNoteDO.setCreditNoteId(creditNote.getCreditNoteId());
+ creditNoteDO.setInvoiceId(creditNote.getInvoiceId());
+ creditNoteDO.setOrderId(creditNote.getOrderId());
+ creditNoteDO.setTotalNetAmount(creditNote.getTotalNetAmount());
+ creditNoteDO.setTotalTaxAmount(creditNote.getTotalTaxAmount());
+ creditNoteDO.setTotalGrossAmount(creditNote.getTotalGrossAmount());
+ creditNoteDO.setCustomer(creditNote.getCustomer());
+ creditNoteDO.setReason(creditNote.getReason());
+
+ // TODO 02: Add a self rel and one that links to the original invoice
+
+
+ return creditNoteDO;
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/mapper/InvoiceMapper.java b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/mapper/InvoiceMapper.java
new file mode 100644
index 0000000..dff2b89
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/mapper/InvoiceMapper.java
@@ -0,0 +1,29 @@
+package de.hststuttgart.vs.task04.api.v1.mapper;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
+
+import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
+
+import de.hststuttgart.vs.task04.api.v1.InvoiceAPI;
+import de.hststuttgart.vs.task04.api.v1.models.InvoiceDO;
+import de.hststuttgart.vs.task04.bm.model.Invoice;
+
+public class InvoiceMapper {
+
+ public static InvoiceDO map(final Invoice invoice) {
+ final var invoiceDO = new InvoiceDO();
+
+ invoiceDO.setInvoiceId(invoice.getInvoiceId());
+ invoiceDO.setOrderId(invoice.getOrderId());
+ invoiceDO.setTotalNetAmount(invoice.getTotalNetAmount());
+ invoiceDO.setTotalTaxAmount(invoice.getTotalTaxAmount());
+ invoiceDO.setTotalGrossAmount(invoice.getTotalGrossAmount());
+ invoiceDO.setCustomer(invoice.getCustomer());
+
+ // TODO 03: Add self rel and one rel to either create a credit note or return a list of credit notes
+
+ return invoiceDO;
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/CreditNoteDO.java b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/CreditNoteDO.java
new file mode 100644
index 0000000..8ef4e95
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/CreditNoteDO.java
@@ -0,0 +1,82 @@
+package de.hststuttgart.vs.task04.api.v1.models;
+
+import java.math.BigDecimal;
+
+import org.springframework.hateoas.RepresentationModel;
+
+public class CreditNoteDO extends RepresentationModel {
+
+ private String creditNoteId;
+ private String invoiceId;
+ private String orderId;
+ private BigDecimal totalNetAmount;
+ private BigDecimal totalTaxAmount;
+ private BigDecimal totalGrossAmount;
+ private Customer customer;
+ private String reason;
+
+ public String getCreditNoteId() {
+ return creditNoteId;
+ }
+
+ public void setCreditNoteId(final String creditNoteId) {
+ this.creditNoteId = creditNoteId;
+ }
+
+ public String getInvoiceId() {
+ return invoiceId;
+ }
+
+ public void setInvoiceId(final String invoiceId) {
+ this.invoiceId = invoiceId;
+ }
+
+ public String getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(final String orderId) {
+ this.orderId = orderId;
+ }
+
+ public BigDecimal getTotalNetAmount() {
+ return totalNetAmount;
+ }
+
+ public void setTotalNetAmount(final BigDecimal totalNetAmount) {
+ this.totalNetAmount = totalNetAmount;
+ }
+
+ public BigDecimal getTotalTaxAmount() {
+ return totalTaxAmount;
+ }
+
+ public void setTotalTaxAmount(final BigDecimal totalTaxAmount) {
+ this.totalTaxAmount = totalTaxAmount;
+ }
+
+ public BigDecimal getTotalGrossAmount() {
+ return totalGrossAmount;
+ }
+
+ public void setTotalGrossAmount(final BigDecimal totalGrossAmount) {
+ this.totalGrossAmount = totalGrossAmount;
+ }
+
+ public Customer getCustomer() {
+ return customer;
+ }
+
+ public void setCustomer(final Customer customer) {
+ this.customer = customer;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(final String reason) {
+ this.reason = reason;
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/Customer.java b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/Customer.java
new file mode 100644
index 0000000..78efcad
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/Customer.java
@@ -0,0 +1,34 @@
+package de.hststuttgart.vs.task04.api.v1.models;
+
+public class Customer {
+
+ private String firstname;
+ private String lastname;
+
+ public String getFirstname() {
+ return firstname;
+ }
+
+ public void setFirstname(final String firstname) {
+ this.firstname = firstname;
+ }
+
+ public String getLastname() {
+ return lastname;
+ }
+
+ public void setLastname(final String lastname) {
+ this.lastname = lastname;
+ }
+
+ public Customer firstname(final String firstname) {
+ this.firstname = firstname;
+ return this;
+ }
+
+ public Customer lastname(final String lastname) {
+ this.lastname = lastname;
+ return this;
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/FullCreditNoteRequest.java b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/FullCreditNoteRequest.java
new file mode 100644
index 0000000..9dc633e
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/FullCreditNoteRequest.java
@@ -0,0 +1,15 @@
+package de.hststuttgart.vs.task04.api.v1.models;
+
+public class FullCreditNoteRequest {
+
+ private String reason;
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(final String reason) {
+ this.reason = reason;
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/InvoiceDO.java b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/InvoiceDO.java
new file mode 100644
index 0000000..1525460
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/InvoiceDO.java
@@ -0,0 +1,64 @@
+package de.hststuttgart.vs.task04.api.v1.models;
+
+import java.math.BigDecimal;
+
+import org.springframework.hateoas.RepresentationModel;
+
+public class InvoiceDO extends RepresentationModel {
+
+ private String invoiceId;
+ private String orderId;
+ private BigDecimal totalNetAmount;
+ private BigDecimal totalTaxAmount;
+ private BigDecimal totalGrossAmount;
+ private Customer customer;
+
+ public String getInvoiceId() {
+ return invoiceId;
+ }
+
+ public void setInvoiceId(final String invoiceId) {
+ this.invoiceId = invoiceId;
+ }
+
+ public String getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(final String orderId) {
+ this.orderId = orderId;
+ }
+
+ public BigDecimal getTotalNetAmount() {
+ return totalNetAmount;
+ }
+
+ public void setTotalNetAmount(final BigDecimal totalNetAmount) {
+ this.totalNetAmount = totalNetAmount;
+ }
+
+ public BigDecimal getTotalTaxAmount() {
+ return totalTaxAmount;
+ }
+
+ public void setTotalTaxAmount(final BigDecimal totalTaxAmount) {
+ this.totalTaxAmount = totalTaxAmount;
+ }
+
+ public BigDecimal getTotalGrossAmount() {
+ return totalGrossAmount;
+ }
+
+ public void setTotalGrossAmount(final BigDecimal totalGrossAmount) {
+ this.totalGrossAmount = totalGrossAmount;
+ }
+
+ public Customer getCustomer() {
+ return customer;
+ }
+
+ public void setCustomer(final Customer customer) {
+ this.customer = customer;
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/Invoices.java b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/Invoices.java
new file mode 100644
index 0000000..a8b5bef
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/Invoices.java
@@ -0,0 +1,19 @@
+package de.hststuttgart.vs.task04.api.v1.models;
+
+import java.util.List;
+
+import org.springframework.hateoas.RepresentationModel;
+
+public class Invoices extends RepresentationModel {
+
+ private List invoices;
+
+ public List getInvoices() {
+ return invoices;
+ }
+
+ public void setInvoices(final List invoices) {
+ this.invoices = invoices;
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/CreditNoteRepository.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/CreditNoteRepository.java
new file mode 100644
index 0000000..ce60786
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/CreditNoteRepository.java
@@ -0,0 +1,19 @@
+package de.hststuttgart.vs.task04.bm;
+
+import java.util.List;
+
+import de.hststuttgart.vs.task04.bm.exceptions.CreditNoteAlreadyExists;
+import de.hststuttgart.vs.task04.bm.exceptions.CreditNoteNotFound;
+import de.hststuttgart.vs.task04.bm.exceptions.InvoiceNotFound;
+import de.hststuttgart.vs.task04.bm.model.CreditNote;
+
+public interface CreditNoteRepository {
+
+ public CreditNote getCreditNote(final String creditNoteId) throws CreditNoteNotFound;
+
+ public List getCreditNotes(final String invoiceId) throws InvoiceNotFound;
+
+ public CreditNote createCreditNotes(final String invoiceId, final String reason) throws InvoiceNotFound,
+ CreditNoteAlreadyExists;
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/InvoiceController.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/InvoiceController.java
new file mode 100644
index 0000000..cc2a5e8
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/InvoiceController.java
@@ -0,0 +1,119 @@
+package de.hststuttgart.vs.task04.bm;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.stereotype.Service;
+
+import de.hststuttgart.vs.task04.api.v1.models.Customer;
+import de.hststuttgart.vs.task04.bm.exceptions.CreditNoteAlreadyExists;
+import de.hststuttgart.vs.task04.bm.exceptions.CreditNoteNotFound;
+import de.hststuttgart.vs.task04.bm.exceptions.InvalidOffset;
+import de.hststuttgart.vs.task04.bm.exceptions.InvoiceNotFound;
+import de.hststuttgart.vs.task04.bm.model.CreditNote;
+import de.hststuttgart.vs.task04.bm.model.Invoice;
+import de.hststuttgart.vs.task04.bm.model.PagedInvoices;
+
+@Service
+public class InvoiceController implements InvoiceRepository, CreditNoteRepository {
+
+ private final List invoices;
+ private final List creditNotes;
+
+ public InvoiceController() {
+ // fake some invoices
+ final var johnDoe = new Customer().firstname("John").lastname("Doe");
+
+ final var invoice = new Invoice()
+ .invoiceId("2023-04-16-0001")
+ .orderId("c082bcbf-550a-4b10-baeb-4047957f70a2")
+ .totalNetAmount(BigDecimal.valueOf(9.34))
+ .totalTaxAmount(BigDecimal.valueOf(0.65))
+ .totalGrossAmount(BigDecimal.valueOf(9.99))
+ .customer(johnDoe)
+ .creditNotePossible(true)
+ .creditNotes(new ArrayList<>());
+
+ invoices = new ArrayList<>();
+ invoices.add(invoice);
+
+ creditNotes = new ArrayList<>();
+ }
+
+ @Override
+ public PagedInvoices getInvoices(final int offset, final int limit) throws InvalidOffset {
+ if (offset < 1 || ((offset - 1) * limit) > invoices.size()) {
+ throw new InvalidOffset();
+ }
+
+ final var invoicePage = invoices
+ .stream()
+ .skip((offset - 1) * limit)
+ .limit(limit)
+ .collect(Collectors.toList());
+
+ final var hasNext = invoices.size() > (offset * limit);
+ final var hasPrevious = offset > 1;
+
+ final var pagedInvoices = new PagedInvoices();
+ pagedInvoices.setPrevious(hasPrevious);
+ pagedInvoices.setNext(hasNext);
+ pagedInvoices.setInvoices(invoicePage);
+
+ return pagedInvoices;
+ }
+
+ @Override
+ public Invoice getInvoice(final String invoiceId) throws InvoiceNotFound {
+ return invoices
+ .stream()
+ .filter(inv -> inv.getInvoiceId().equals(invoiceId))
+ .findFirst()
+ .orElseThrow(InvoiceNotFound::new);
+ }
+
+ @Override
+ public CreditNote createCreditNotes(final String invoiceId, final String reason)
+ throws InvoiceNotFound, CreditNoteAlreadyExists {
+ final var invoice = getInvoice(invoiceId);
+
+ if (!invoice.isCreditNotePossible()) {
+ throw new CreditNoteAlreadyExists();
+ }
+
+ final var creditNote = new CreditNote();
+
+ creditNote.setCreditNoteId("CREDITNOTE-" + invoiceId);
+ creditNote.setInvoiceId(invoiceId);
+ creditNote.setOrderId(invoice.getOrderId());
+ creditNote.setTotalNetAmount(invoice.getTotalNetAmount());
+ creditNote.setTotalTaxAmount(invoice.getTotalTaxAmount());
+ creditNote.setTotalGrossAmount(invoice.getTotalGrossAmount());
+ creditNote.setCustomer(invoice.getCustomer());
+ creditNote.setReason(reason);
+
+ creditNotes.add(creditNote);
+ invoice.setCreditNotePossible(false);
+ invoice.setCreditNotes(List.of(creditNote));
+
+ return creditNote;
+ }
+
+ @Override
+ public List getCreditNotes(final String invoiceId) throws InvoiceNotFound {
+ final var invoice = getInvoice(invoiceId);
+ return invoice.getCreditNotes();
+ }
+
+ @Override
+ public CreditNote getCreditNote(final String creditNoteId) throws CreditNoteNotFound {
+ return creditNotes
+ .stream()
+ .filter(creditNote -> creditNote.getCreditNoteId().equals(creditNoteId))
+ .findFirst()
+ .orElseThrow(CreditNoteNotFound::new);
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/InvoiceRepository.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/InvoiceRepository.java
new file mode 100644
index 0000000..62c5fe7
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/InvoiceRepository.java
@@ -0,0 +1,15 @@
+package de.hststuttgart.vs.task04.bm;
+
+import de.hststuttgart.vs.task04.bm.exceptions.InvalidOffset;
+import de.hststuttgart.vs.task04.bm.exceptions.InvoiceNotFound;
+import de.hststuttgart.vs.task04.bm.model.Invoice;
+import de.hststuttgart.vs.task04.bm.model.PagedInvoices;
+
+public interface InvoiceRepository {
+
+ public PagedInvoices getInvoices(final int offset, final int limit) throws InvalidOffset;
+
+ public Invoice getInvoice(final String invoiceId) throws InvoiceNotFound;
+
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/CreditNoteAlreadyExists.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/CreditNoteAlreadyExists.java
new file mode 100644
index 0000000..9fb55a3
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/CreditNoteAlreadyExists.java
@@ -0,0 +1,5 @@
+package de.hststuttgart.vs.task04.bm.exceptions;
+
+public class CreditNoteAlreadyExists extends Exception{
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/CreditNoteNotFound.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/CreditNoteNotFound.java
new file mode 100644
index 0000000..a895972
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/CreditNoteNotFound.java
@@ -0,0 +1,5 @@
+package de.hststuttgart.vs.task04.bm.exceptions;
+
+public class CreditNoteNotFound extends Exception {
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/InvalidOffset.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/InvalidOffset.java
new file mode 100644
index 0000000..b22f68d
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/InvalidOffset.java
@@ -0,0 +1,5 @@
+package de.hststuttgart.vs.task04.bm.exceptions;
+
+public class InvalidOffset extends Exception{
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/InvoiceNotFound.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/InvoiceNotFound.java
new file mode 100644
index 0000000..327e21e
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/InvoiceNotFound.java
@@ -0,0 +1,5 @@
+package de.hststuttgart.vs.task04.bm.exceptions;
+
+public class InvoiceNotFound extends Exception{
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/model/CreditNote.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/model/CreditNote.java
new file mode 100644
index 0000000..81d8383
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/model/CreditNote.java
@@ -0,0 +1,82 @@
+package de.hststuttgart.vs.task04.bm.model;
+
+import java.math.BigDecimal;
+
+import de.hststuttgart.vs.task04.api.v1.models.Customer;
+
+public class CreditNote {
+
+ private String creditNoteId;
+ private String invoiceId;
+ private String orderId;
+ private BigDecimal totalNetAmount;
+ private BigDecimal totalTaxAmount;
+ private BigDecimal totalGrossAmount;
+ private Customer customer;
+ private String reason;
+
+ public String getCreditNoteId() {
+ return creditNoteId;
+ }
+
+ public void setCreditNoteId(final String creditNoteId) {
+ this.creditNoteId = creditNoteId;
+ }
+
+ public String getInvoiceId() {
+ return invoiceId;
+ }
+
+ public void setInvoiceId(final String invoiceId) {
+ this.invoiceId = invoiceId;
+ }
+
+ public String getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(final String orderId) {
+ this.orderId = orderId;
+ }
+
+ public BigDecimal getTotalNetAmount() {
+ return totalNetAmount;
+ }
+
+ public void setTotalNetAmount(final BigDecimal totalNetAmount) {
+ this.totalNetAmount = totalNetAmount;
+ }
+
+ public BigDecimal getTotalTaxAmount() {
+ return totalTaxAmount;
+ }
+
+ public void setTotalTaxAmount(final BigDecimal totalTaxAmount) {
+ this.totalTaxAmount = totalTaxAmount;
+ }
+
+ public BigDecimal getTotalGrossAmount() {
+ return totalGrossAmount;
+ }
+
+ public void setTotalGrossAmount(final BigDecimal totalGrossAmount) {
+ this.totalGrossAmount = totalGrossAmount;
+ }
+
+ public Customer getCustomer() {
+ return customer;
+ }
+
+ public void setCustomer(final Customer customer) {
+ this.customer = customer;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(final String reason) {
+ this.reason = reason;
+ }
+
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/model/Invoice.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/model/Invoice.java
new file mode 100644
index 0000000..4023c86
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/model/Invoice.java
@@ -0,0 +1,125 @@
+package de.hststuttgart.vs.task04.bm.model;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import de.hststuttgart.vs.task04.api.v1.models.Customer;
+
+public class Invoice {
+
+ private String invoiceId;
+ private String orderId;
+ private BigDecimal totalNetAmount;
+ private BigDecimal totalTaxAmount;
+ private BigDecimal totalGrossAmount;
+ private Customer customer;
+ private boolean isCreditNotePossible;
+ private List creditNotes;
+
+ public List getCreditNotes() {
+ return creditNotes;
+ }
+
+ public void setCreditNotes(final List creditNotes) {
+ this.creditNotes = creditNotes;
+ }
+
+ public boolean isCreditNotePossible() {
+ return isCreditNotePossible;
+ }
+
+ public void setCreditNotePossible(final boolean creditNotePossible) {
+ isCreditNotePossible = creditNotePossible;
+ }
+
+ public String getInvoiceId() {
+ return invoiceId;
+ }
+
+ public void setInvoiceId(final String invoiceId) {
+ this.invoiceId = invoiceId;
+ }
+
+ public String getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(final String orderId) {
+ this.orderId = orderId;
+ }
+
+ public BigDecimal getTotalNetAmount() {
+ return totalNetAmount;
+ }
+
+ public void setTotalNetAmount(final BigDecimal totalNetAmount) {
+ this.totalNetAmount = totalNetAmount;
+ }
+
+ public BigDecimal getTotalTaxAmount() {
+ return totalTaxAmount;
+ }
+
+ public void setTotalTaxAmount(final BigDecimal totalTaxAmount) {
+ this.totalTaxAmount = totalTaxAmount;
+ }
+
+ public BigDecimal getTotalGrossAmount() {
+ return totalGrossAmount;
+ }
+
+ public void setTotalGrossAmount(final BigDecimal totalGrossAmount) {
+ this.totalGrossAmount = totalGrossAmount;
+ }
+
+ public Customer getCustomer() {
+ return customer;
+ }
+
+ public void setCustomer(final Customer customer) {
+ this.customer = customer;
+ }
+
+ public Invoice invoiceId(final String invoiceId) {
+ this.invoiceId = invoiceId;
+ return this;
+ }
+
+ public Invoice orderId(final String orderId) {
+ this.orderId = orderId;
+ return this;
+ }
+
+
+ public Invoice totalNetAmount(final BigDecimal totalNetAmount) {
+ this.totalNetAmount = totalNetAmount;
+ return this;
+ }
+
+
+ public Invoice totalTaxAmount(final BigDecimal totalTaxAmount) {
+ this.totalTaxAmount = totalTaxAmount;
+ return this;
+ }
+
+
+ public Invoice totalGrossAmount(final BigDecimal totalGrossAmount) {
+ this.totalGrossAmount = totalGrossAmount;
+ return this;
+ }
+
+ public Invoice customer(final Customer customer) {
+ this.customer = customer;
+ return this;
+ }
+
+ public Invoice creditNotePossible(final boolean creditNotePossible) {
+ this.isCreditNotePossible = creditNotePossible;
+ return this;
+ }
+
+ public Invoice creditNotes(final List creditNotes) {
+ this.creditNotes = creditNotes;
+ return this;
+ }
+}
diff --git a/task04/src/main/java/de/hststuttgart/vs/task04/bm/model/PagedInvoices.java b/task04/src/main/java/de/hststuttgart/vs/task04/bm/model/PagedInvoices.java
new file mode 100644
index 0000000..329dd55
--- /dev/null
+++ b/task04/src/main/java/de/hststuttgart/vs/task04/bm/model/PagedInvoices.java
@@ -0,0 +1,36 @@
+package de.hststuttgart.vs.task04.bm.model;
+
+import java.util.List;
+
+public class PagedInvoices {
+
+ private boolean previous;
+ private boolean next;
+
+ private List invoices;
+
+ public boolean hasPrevious() {
+ return previous;
+ }
+
+ public void setPrevious(final boolean previous) {
+ this.previous = previous;
+ }
+
+ public boolean hasNext() {
+ return next;
+ }
+
+ public void setNext(final boolean next) {
+ this.next = next;
+ }
+
+ public List getInvoices() {
+ return invoices;
+ }
+
+ public void setInvoices(final List invoices) {
+ this.invoices = invoices;
+ }
+
+}
diff --git a/task04/src/test/java/de/hststuttgart/vs/task04/Task04ApplicationTests.java b/task04/src/test/java/de/hststuttgart/vs/task04/Task04ApplicationTests.java
new file mode 100644
index 0000000..bd67ffa
--- /dev/null
+++ b/task04/src/test/java/de/hststuttgart/vs/task04/Task04ApplicationTests.java
@@ -0,0 +1,13 @@
+package de.hststuttgart.vs.task04;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class Task04ApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/task04/src/test/java/de/hststuttgart/vs/task04/api/v1/InvoiceAPITest.java b/task04/src/test/java/de/hststuttgart/vs/task04/api/v1/InvoiceAPITest.java
new file mode 100644
index 0000000..99419b2
--- /dev/null
+++ b/task04/src/test/java/de/hststuttgart/vs/task04/api/v1/InvoiceAPITest.java
@@ -0,0 +1,152 @@
+package de.hststuttgart.vs.task04.api.v1;
+
+import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks;
+
+import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
+import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
+import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
+import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import de.hststuttgart.vs.task04.api.v1.models.Customer;
+import de.hststuttgart.vs.task04.bm.InvoiceController;
+import de.hststuttgart.vs.task04.bm.model.Invoice;
+import de.hststuttgart.vs.task04.bm.model.PagedInvoices;
+
+@ExtendWith(RestDocumentationExtension.class)
+@WebMvcTest(InvoiceAPI.class)
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class InvoiceAPITest {
+
+ private MockMvc mockMvc;
+
+ @MockBean
+ private InvoiceController invoiceController;
+
+ private RestDocumentationResultHandler documentationHandler;
+
+ @BeforeEach
+ void setUp(final WebApplicationContext webApplicationContext,
+ final RestDocumentationContextProvider restDocumentationContextProvider) {
+ documentationHandler =
+ document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()));
+
+ mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
+ .apply(documentationConfiguration(restDocumentationContextProvider).uris()
+ .withScheme("https")
+ .withPort(443))
+ .alwaysDo(documentationHandler)
+ .build();
+ }
+
+ @Test
+ void should_return_invoices() throws Exception {
+ // We mock the Object so that we can test it easier
+ final var pagedInvoices = new PagedInvoices();
+ pagedInvoices.setInvoices(List.of());
+ pagedInvoices.setNext(false);
+ pagedInvoices.setPrevious(false);
+
+ Mockito.doReturn(pagedInvoices).when(invoiceController).getInvoices(Mockito.anyInt(), Mockito.anyInt());
+
+ this.mockMvc.perform(get("/invoices"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andDo(documentationHandler.document(
+ links(
+ halLinks(),
+ linkWithRel("self").description("Link to this resource")
+ ),
+ responseFields(
+ fieldWithPath("invoices").description("Array that contains all invoices"),
+ subsectionWithPath("_links").description("Link to resources")
+ )
+ ));
+ }
+
+ // TODO 04: test /invoices which should return a list with a next and previous link
+ @Test
+ void should_return_invoices_with_links() throws Exception {
+ // We mock the Object so that we can test it easier
+ final var johnDoe = new Customer().firstname("John").lastname("Doe");
+
+ final var invoice = new Invoice()
+ .invoiceId("2023-04-16-0001")
+ .orderId("c082bcbf-550a-4b10-baeb-4047957f70a2")
+ .totalNetAmount(BigDecimal.valueOf(9.34))
+ .totalTaxAmount(BigDecimal.valueOf(0.65))
+ .totalGrossAmount(BigDecimal.valueOf(9.99))
+ .customer(johnDoe)
+ .creditNotePossible(true)
+ .creditNotes(new ArrayList<>());
+
+ final var pagedInvoices = new PagedInvoices();
+ pagedInvoices.setInvoices(List.of(invoice));
+ pagedInvoices.setNext(true);
+ pagedInvoices.setPrevious(true);
+
+ Mockito.doReturn(pagedInvoices).when(invoiceController).getInvoices(Mockito.anyInt(), Mockito.anyInt());
+
+ this.mockMvc.perform(get("/invoices")
+ .queryParam("offset", "2")
+ .queryParam("limit", "1"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andDo(documentationHandler.document(
+ links(
+ halLinks(),
+ linkWithRel("self").description("Link to this resource"),
+ linkWithRel("next").description("Link to next page"),
+ linkWithRel("previous").description("Link to previous page")
+ ),
+ queryParameters(
+ parameterWithName("offset").description("Indicates the page. Starts with 1").optional(),
+ parameterWithName("limit").description("Limits the amount of invoices per page").optional()
+ ),
+ responseFields(
+ fieldWithPath("invoices").description("Array that contains all invoices"),
+ subsectionWithPath("_links").description("Link to resources")
+ )
+ ));
+ }
+
+ // TODO 05 add a test for /invoices/[invoiceId}
+ // The endpoint should return an invoice that can be refunded (credit note creation is possible)
+
+ // TODO 06 add a test for /invoices/[invoiceId}
+ // The endpoint should return an invoice that can't be refunded (credit note already exists)
+
+ // TODO 07 add a test for /invoices/[invoiceId}
+ // The endpoint should return no invoce (not found)
+
+
+}
diff --git a/task04/src/test/resources/templates/all_invoices.adoc b/task04/src/test/resources/templates/all_invoices.adoc
new file mode 100644
index 0000000..f202834
--- /dev/null
+++ b/task04/src/test/resources/templates/all_invoices.adoc
@@ -0,0 +1,24 @@
+This endpoint returns all invoices. There is no way to change the order of the elements.
+It uses pagination by default. The default values are `1` for `offset` and `5` for `limit`. This gives you the first page which contains 5 invoices.
+
+To navigate through the invoices you can use the provided HATEOAS links.
+
+=== Request structure
+
+include::{snippets}/should_return_invoices/http-request.adoc[]
+
+=== Response fields
+
+include::{snippets}/should_return_invoices/response-fields.adoc[]
+
+=== Response links
+
+include::{snippets}/should_return_invoices/links.adoc[]
+
+=== Example response
+
+include::{snippets}/should_return_invoices/http-response.adoc[]
+
+=== CURL request
+
+include::{snippets}/should_return_invoices/curl-request.adoc[]
diff --git a/task04/src/test/resources/templates/index.adoc b/task04/src/test/resources/templates/index.adoc
new file mode 100644
index 0000000..74c9c68
--- /dev/null
+++ b/task04/src/test/resources/templates/index.adoc
@@ -0,0 +1,9 @@
+= Invoice Manager
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:toc: left
+:toclevels: 4
+:sectlinks:
+
+include::all_invoices.adoc[]