From d586d1557ce04062c2c4fdb052c2a9fe4cc36a4c Mon Sep 17 00:00:00 2001 From: magoeke Date: Mon, 17 Apr 2023 01:30:56 +0200 Subject: [PATCH] Added task04 --- task04/.gitignore | 33 ++++ task04/pom.xml | 89 ++++++++++ .../vs/task04/Task04Application.java | 13 ++ .../vs/task04/api/v1/CreditNotesAPI.java | 34 ++++ .../vs/task04/api/v1/InvoiceAPI.java | 104 ++++++++++++ .../api/v1/mapper/CreditNoteMapper.java | 33 ++++ .../task04/api/v1/mapper/InvoiceMapper.java | 29 ++++ .../vs/task04/api/v1/models/CreditNoteDO.java | 82 ++++++++++ .../vs/task04/api/v1/models/Customer.java | 34 ++++ .../api/v1/models/FullCreditNoteRequest.java | 15 ++ .../vs/task04/api/v1/models/InvoiceDO.java | 64 ++++++++ .../vs/task04/api/v1/models/Invoices.java | 19 +++ .../vs/task04/bm/CreditNoteRepository.java | 19 +++ .../vs/task04/bm/InvoiceController.java | 119 ++++++++++++++ .../vs/task04/bm/InvoiceRepository.java | 15 ++ .../exceptions/CreditNoteAlreadyExists.java | 5 + .../bm/exceptions/CreditNoteNotFound.java | 5 + .../task04/bm/exceptions/InvalidOffset.java | 5 + .../task04/bm/exceptions/InvoiceNotFound.java | 5 + .../vs/task04/bm/model/CreditNote.java | 82 ++++++++++ .../vs/task04/bm/model/Invoice.java | 125 ++++++++++++++ .../vs/task04/bm/model/PagedInvoices.java | 36 +++++ .../vs/task04/Task04ApplicationTests.java | 13 ++ .../vs/task04/api/v1/InvoiceAPITest.java | 152 ++++++++++++++++++ .../resources/templates/all_invoices.adoc | 24 +++ .../src/test/resources/templates/index.adoc | 9 ++ 26 files changed, 1163 insertions(+) create mode 100644 task04/.gitignore create mode 100644 task04/pom.xml create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/Task04Application.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/api/v1/CreditNotesAPI.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/api/v1/InvoiceAPI.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/api/v1/mapper/CreditNoteMapper.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/api/v1/mapper/InvoiceMapper.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/CreditNoteDO.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/Customer.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/FullCreditNoteRequest.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/InvoiceDO.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/api/v1/models/Invoices.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/CreditNoteRepository.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/InvoiceController.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/InvoiceRepository.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/CreditNoteAlreadyExists.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/CreditNoteNotFound.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/InvalidOffset.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/exceptions/InvoiceNotFound.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/model/CreditNote.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/model/Invoice.java create mode 100644 task04/src/main/java/de/hststuttgart/vs/task04/bm/model/PagedInvoices.java create mode 100644 task04/src/test/java/de/hststuttgart/vs/task04/Task04ApplicationTests.java create mode 100644 task04/src/test/java/de/hststuttgart/vs/task04/api/v1/InvoiceAPITest.java create mode 100644 task04/src/test/resources/templates/all_invoices.adoc create mode 100644 task04/src/test/resources/templates/index.adoc 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[]