diff --git a/src/main/asciidoc/models/design/inventory.puml b/src/main/asciidoc/models/design/inventory.puml index 19e64bc..bdc5fb0 100644 --- a/src/main/asciidoc/models/design/inventory.puml +++ b/src/main/asciidoc/models/design/inventory.puml @@ -18,8 +18,7 @@ package Salespoint { package catering { package catalog { - interface ConsumableCatalog - interface RentableCatalog + interface CateringCatalog } package inventory { @@ -27,16 +26,21 @@ package catering { + InventoryController(inventory : UniqueInventory) + list(model : Model) : String + edit(model : Model, pid : Product) : String - + edit(model : Model, form : InventoryMutateForm) : String + + edit(model : Model, pid : Product, form : InventoryMutateForm) : String + + editConsumable(form: ConsumableMutateForm, result: Errors, pid : Product, model : Model) : String + + editRentable(form: RentableMutateForm, result: Errors, pid : Product, model : Model) : String + edit(form : InventoryMutateForm, result : Errors, pid : Product, model : Model) : String - + add(model : Model) : String + + add(model : Model, type : String) : String + add(model : Model, form : InventoryMutateForm) : String + + addConsumable(form : ConsumableMutateForm, result : Errors, model : Model) : String + + addRentable(form : RentableMutateForm, result : Errors, model : Model) : String + add(form : InventoryMutateForm, result : Errors, model : Model) : String + delete(pid : Product) : String } - InventoryController --> "1" catering.catalog.ConsumableCatalog : "-consumableCatalog" - InventoryController --> "1" catering.catalog.RentableCatalog : "-rentableCatalog" + InventoryController --> "1" catering.catalog.CateringCatalog : "-cateringCatalog" InventoryController ..> InventoryMutateForm + InventoryController ..> ConsumableMutateForm + InventoryController ..> RentableMutateForm InventoryController .u.> Salespoint.Product InventoryController -u-> "1" Salespoint.UniqueInventory : "-inventory" InventoryController .u.> Salespoint.UniqueInventoryItem @@ -48,13 +52,28 @@ package catering { + InventoryInitializer(inventory : UniqueInventory, catalog : CateringCatalog) + initialize() : void } - InventoryInitializer --> "1" catering.catalog.ConsumableCatalog : "-consumableCatalog" - InventoryInitializer --> "1" catering.catalog.RentableCatalog : "-rentableCatalog" + InventoryInitializer --> "1" catering.catalog.CateringCatalog : "-cateringCatalog" InventoryInitializer .u.|> Salespoint.DataInitializer InventoryInitializer .u.> Salespoint.Quantity InventoryInitializer -u-> "1" Salespoint.UniqueInventory : "-inventory" InventoryInitializer .u.> Salespoint.UniqueInventoryItem InventoryInitializer .u.> Spring.Assert + + class InventoryMutateForm + class ConsumableMutateForm + class RentableMutateForm + ConsumableMutateForm <|-- InventoryMutateForm + RentableMutateForm <|-- InventoryMutateForm + + InventoryMutateForm ..> Salespoint.Quantity + + InventoryMutateForm ..> Salespoint.Product + ConsumableMutateForm ..> Salespoint.Product + RentableMutateForm ..> Salespoint.Product + + InventoryMutateForm ..> Salespoint.UniqueInventoryItem + ConsumableMutateForm ..> Salespoint.UniqueInventoryItem + RentableMutateForm ..> Salespoint.UniqueInventoryItem } } @enduml diff --git a/src/main/asciidoc/models/design/inventory.svg b/src/main/asciidoc/models/design/inventory.svg index 7b8acf7..3a66862 100644 --- a/src/main/asciidoc/models/design/inventory.svg +++ b/src/main/asciidoc/models/design/inventory.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fee4aea82a416567ab42e5e5697152692465fbe834310239c483ab4123bad5ac -size 39559 +oid sha256:4dcaea23cec67916da78e8d262c83e4d1d7c8e4658c39dbd22068468cbc6c8ec +size 49680 diff --git a/src/main/java/catering/catalog/CatalogDataInitializer.java b/src/main/java/catering/catalog/CatalogDataInitializer.java index d473750..ffe4d35 100644 --- a/src/main/java/catering/catalog/CatalogDataInitializer.java +++ b/src/main/java/catering/catalog/CatalogDataInitializer.java @@ -18,12 +18,17 @@ package catering.catalog; import static org.salespointframework.core.Currencies.EURO; +import java.util.Set; +import java.util.Optional; + import org.javamoney.moneta.Money; import org.salespointframework.core.DataInitializer; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import catering.order.OrderType; + @Component @Order(20) class CatalogDataInitializer implements DataInitializer { @@ -41,9 +46,21 @@ class CatalogDataInitializer implements DataInitializer { return; } - cateringCatalog.save(new CatalogDummy("Brötchen Vollkorn", CatalogDummyType.CONSUMABLE, Money.of(1, EURO), Money.of(0.5, EURO), - Money.of(0.75, EURO))); - cateringCatalog.save(new CatalogDummy("Kerze Rot", CatalogDummyType.CONSUMABLE, Money.of(2, EURO), Money.of(1.5, EURO))); - cateringCatalog.save(new CatalogDummy("Brotschneidemaschine Power X 3000", CatalogDummyType.RENTABLE, Money.of(25, EURO), Money.of(10000, EURO))); + cateringCatalog.save(new Consumable( + "Brötchen Vollkorn", + Money.of(1, EURO), + Money.of(0.5, EURO), + Optional.of(Money.of(0.75, EURO)), + Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST))); + cateringCatalog.save(new Rentable( + "Kerze Rot", + Money.of(2, EURO), + Money.of(1.5, EURO), + Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST))); + cateringCatalog.save(new Rentable( + "Brotschneidemaschine Power X 3000", + Money.of(25, EURO), + Money.of(10000, EURO), + Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST))); } } diff --git a/src/main/java/catering/catalog/CatalogDummy.java b/src/main/java/catering/catalog/CatalogDummy.java deleted file mode 100644 index 28424e3..0000000 --- a/src/main/java/catering/catalog/CatalogDummy.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2023 Simon Bruder - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package catering.catalog; - -import javax.money.MonetaryAmount; - -import org.salespointframework.catalog.Product; -import org.springframework.util.Assert; - -import jakarta.persistence.Entity; - -@Entity -public class CatalogDummy extends Product { - MonetaryAmount wholesalePrice; - // Optional promotionPrice; - // enterprise java goes brrr - MonetaryAmount promotionPrice; - CatalogDummyType type; - - // enterprise java goes brrr - @SuppressWarnings({ "unused", "deprecation" }) - private CatalogDummy() { - } - - public CatalogDummy(String name, CatalogDummyType type, MonetaryAmount price, MonetaryAmount wholesalePrice, - MonetaryAmount promotionPrice) { - super(name, price); - Assert.notNull(type, "Type must not be null!"); - Assert.notNull(wholesalePrice, "WholesalePricee must not be null!"); - // PromotionPrice can be null. - this.type = type; - this.wholesalePrice = wholesalePrice; - this.promotionPrice = promotionPrice; - } - - public CatalogDummy(String name, CatalogDummyType type, MonetaryAmount price, MonetaryAmount wholesalePrice) { - this(name, type, price, wholesalePrice, null); - } - - public MonetaryAmount getWholesalePrice() { - return wholesalePrice; - } - - public void setWholesalePrice(MonetaryAmount wholesalePrice) { - Assert.notNull(wholesalePrice, "WholesalePricee must not be null!"); - this.wholesalePrice = wholesalePrice; - } - - public MonetaryAmount getPromotionPrice() { - return promotionPrice; - } - - public void setPromotionPrice(MonetaryAmount promotionPrice) { - this.promotionPrice = promotionPrice; - } - - public CatalogDummyType getType() { - return this.type; - } - - public void setType(CatalogDummyType type) { - Assert.notNull(type, "Type must not be null!"); - this.type = type; - } -} diff --git a/src/main/java/catering/catalog/CatalogDummyType.java b/src/main/java/catering/catalog/CatalogDummyType.java deleted file mode 100644 index 26c815c..0000000 --- a/src/main/java/catering/catalog/CatalogDummyType.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2023 Simon Bruder - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package catering.catalog; - -public enum CatalogDummyType { - CONSUMABLE, - RENTABLE, -} diff --git a/src/main/java/catering/inventory/ConsumableMutateForm.java b/src/main/java/catering/inventory/ConsumableMutateForm.java new file mode 100644 index 0000000..561cde4 --- /dev/null +++ b/src/main/java/catering/inventory/ConsumableMutateForm.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 Simon Bruder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package catering.inventory; + +import static org.salespointframework.core.Currencies.EURO; + +import java.util.Optional; +import java.util.Set; + +import javax.money.MonetaryAmount; +import javax.money.NumberValue; + +import org.javamoney.moneta.Money; +import org.salespointframework.catalog.Product; +import org.salespointframework.inventory.UniqueInventoryItem; + +import catering.catalog.Consumable; +import catering.order.OrderType; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; // NonNegative in enterprise java + +class ConsumableMutateForm extends InventoryMutateForm { + private @NotNull @PositiveOrZero Double wholesalePrice; + private @NotNull Optional<@PositiveOrZero Double> promotionPrice = Optional.empty(); + + public Double getWholesalePrice() { + return wholesalePrice; + } + + public Optional getPromotionPrice() { + return promotionPrice; + } + + public void setWholesalePrice(Double wholesalePrice) { + this.wholesalePrice = wholesalePrice; + } + + public void setPromotionPrice(Optional promotionPrice) { + this.promotionPrice = promotionPrice; + } + + @Override + public Product toProduct() { + return new Consumable( + getName(), + Money.of(getRetailPrice(), EURO), + Money.of(getWholesalePrice(), EURO), + getPromotionPrice().map(price -> Money.of(price, EURO)), + // FIXME: categories for creation of consumables are hardcoded + Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST)); + } + + public static ConsumableMutateForm of(Consumable product, UniqueInventoryItem item) { + ConsumableMutateForm form = new ConsumableMutateForm(); + form.setName(product.getName()); + form.setQuantity(item.getQuantity()); + form.setWholesalePrice(product.getWholesalePrice().getNumber().doubleValueExact()); + form.setRetailPrice(product.getRetailPrice().getNumber().doubleValueExact()); + form.setPromotionPrice( + product.getPromotionPrice().map(MonetaryAmount::getNumber).map(NumberValue::doubleValueExact)); + return form; + } + + @Override + protected void modifyProductPrimitive(Product product) { + if (product instanceof Consumable consumable) { + consumable.setWholesalePrice(Money.of(getWholesalePrice(), EURO)); + consumable.setPromotionPrice(getPromotionPrice().map(price -> Money.of(price, EURO))); + } else { + throw new IllegalArgumentException("ConsumableMutateForm can only modify instances of Consumable"); + } + } +} diff --git a/src/main/java/catering/inventory/InventoryController.java b/src/main/java/catering/inventory/InventoryController.java index e5711db..e6ed1df 100644 --- a/src/main/java/catering/inventory/InventoryController.java +++ b/src/main/java/catering/inventory/InventoryController.java @@ -16,14 +16,6 @@ */ package catering.inventory; -import static org.salespointframework.core.Currencies.EURO; - -import java.util.Optional; - -import javax.money.MonetaryAmount; -import javax.money.NumberValue; - -import org.javamoney.moneta.Money; import org.salespointframework.catalog.Product; import org.salespointframework.inventory.UniqueInventory; import org.salespointframework.inventory.UniqueInventoryItem; @@ -36,11 +28,35 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; -import catering.catalog.CatalogDummy; import catering.catalog.CateringCatalog; import jakarta.validation.Valid; +/* + * TODO TL;DR: Use HandlerMethodArgumentResolver + * + * This class currently has many confusing methods that do a variety of things, + * but mostly work around polymorphic restrictions of Spring. + * Ideally, spring would allow a form to be polymorphic + * based on its attributes, if they are unique, + * or based on another parameter. + * From what I could find, + * this is not possible without implementing a custom HandlerMethodArgumentResolver. + * If there is time, the controller should be changed to use this, + * which should vastly simplify it. + * + * Adding will always require a type parameter, + * as the type is not known at first. + * However, currently there are two (or even three) POST handlers, + * as there are two different form types it must support + * (and an infinite amount of invalid types that it should ignore). + * + * Editing should not require passing any additional type parameter around + * as the type can be inferred from the product. + * Changing the type of a product should not be possible. + */ + @Controller class InventoryController { private final UniqueInventory inventory; @@ -64,17 +80,10 @@ class InventoryController { @PreAuthorize("hasRole('ADMIN')") @GetMapping("/inventory/edit/{pid}") String edit(Model model, @PathVariable Product pid) { - CatalogDummy product = (CatalogDummy) pid; UniqueInventoryItem item = inventory.findByProduct(pid).get(); - return edit(model, - new InventoryMutateForm(product.getType(), - product.getName(), - item.getQuantity(), - product.getWholesalePrice().getNumber().doubleValueExact(), - product.getPrice().getNumber().doubleValueExact(), - Optional.ofNullable(product.getPromotionPrice()) - .map(MonetaryAmount::getNumber) - .map(NumberValue::doubleValueExact))); + final InventoryMutateForm form = InventoryMutateForm.of(pid, item); + + return edit(model, form); } String edit(Model model, InventoryMutateForm form) { @@ -85,23 +94,28 @@ class InventoryController { } @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/inventory/edit/{pid}") - String edit(@Valid @ModelAttribute("form") InventoryMutateForm form, Errors result, @PathVariable Product pid, - Model model) { + @PostMapping(path = "/inventory/edit/{pid}", params = "type=Consumable") + String editConsumable(@Valid @ModelAttribute("form") ConsumableMutateForm form, Errors result, + @PathVariable Product pid, Model model) { + return edit(form, result, pid, model); + } + + @PreAuthorize("hasRole('ADMIN')") + @PostMapping(path = "/inventory/edit/{pid}", params = "type=Rentable") + String editRentable(@Valid @ModelAttribute("form") RentableMutateForm form, Errors result, + @PathVariable Product pid, Model model) { + return edit(form, result, pid, model); + } + + String edit(InventoryMutateForm form, Errors result, Product product, Model model) { if (result.hasErrors()) { return edit(model, form); } - CatalogDummy product = (CatalogDummy) pid; - UniqueInventoryItem item = inventory.findByProduct(pid).get(); - - product.setName(form.getName()); - product.setType(form.getType()); - product.setPrice(Money.of(form.getRetailPrice(), EURO)); - product.setWholesalePrice(Money.of(form.getWholesalePrice(), EURO)); - product.setPromotionPrice(form.getPromotionPrice().map(price -> Money.of(price, EURO)).orElse(null)); + form.modifyProduct(product); product = cateringCatalog.save(product); + UniqueInventoryItem item = inventory.findByProduct(product).get(); // no setQuantity in enterprise java // (though returing a modified object is actually nice) inventory.save(item.increaseQuantity(form.getQuantity().subtract(item.getQuantity()))); @@ -109,9 +123,17 @@ class InventoryController { } @PreAuthorize("hasRole('ADMIN')") - @GetMapping("/inventory/add") - String add(Model model) { - return add(model, InventoryMutateForm.empty()); + @GetMapping(path = "/inventory/add") + String add(Model model, @RequestParam String type) { + switch (type) { + case "Consumable": + return add(model, new ConsumableMutateForm()); + case "Rentable": + return add(model, new RentableMutateForm()); + default: + // TODO better error handling + return "redirect:/inventory"; + } } String add(Model model, InventoryMutateForm form) { @@ -121,17 +143,22 @@ class InventoryController { } @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/inventory/add") - String add(@Valid @ModelAttribute("form") InventoryMutateForm form, Errors result, Model model) { + @PostMapping(path = "/inventory/add", params = "type=Consumable") + String addConsumable(@Valid @ModelAttribute("form") ConsumableMutateForm form, Errors result, Model model) { + return add(form, result, model); + } + + @PreAuthorize("hasRole('ADMIN')") + @PostMapping(path = "/inventory/add", params = "type=Rentable") + String addRentable(@Valid @ModelAttribute("form") ConsumableMutateForm form, Errors result, Model model) { + return add(form, result, model); + } + + String add(@Valid InventoryMutateForm form, Errors result, Model model) { if (result.hasErrors()) { return add(model, form); } - inventory.save(new UniqueInventoryItem( - cateringCatalog - .save(new CatalogDummy(form.getName(), form.getType(), Money.of(form.getRetailPrice(), EURO), - Money.of(form.getWholesalePrice(), EURO), - form.getPromotionPrice().map(price -> Money.of(price, EURO)).orElse(null))), - form.getQuantity())); + inventory.save(new UniqueInventoryItem(cateringCatalog.save(form.toProduct()), form.getQuantity())); return "redirect:/inventory"; } @@ -140,7 +167,7 @@ class InventoryController { String delete(@PathVariable Product pid) { UniqueInventoryItem item = inventory.findByProduct(pid).get(); inventory.delete(item); - cateringCatalog.delete((CatalogDummy) pid); + cateringCatalog.delete(pid); return "redirect:/inventory"; } } diff --git a/src/main/java/catering/inventory/InventoryMutateForm.java b/src/main/java/catering/inventory/InventoryMutateForm.java index 3bff024..8109620 100644 --- a/src/main/java/catering/inventory/InventoryMutateForm.java +++ b/src/main/java/catering/inventory/InventoryMutateForm.java @@ -16,39 +16,33 @@ */ package catering.inventory; -import java.util.Optional; +import static org.salespointframework.core.Currencies.EURO; +import org.javamoney.moneta.Money; +import org.salespointframework.catalog.Product; +import org.salespointframework.inventory.UniqueInventoryItem; import org.salespointframework.quantity.Quantity; -import catering.catalog.CatalogDummyType; +import catering.catalog.Consumable; +import catering.catalog.Rentable; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; // NonNegative in enterprise java -class InventoryMutateForm { - private final @NotNull CatalogDummyType type; - private final @NotEmpty String name; - private final @NotNull Quantity quantity; - private final @NotNull double wholesalePrice, retailPrice; - private final @NotNull Optional promotionPrice; +/** + * Abstract class for handling inventory mutations. + * + * It has children for every product type. + * + * The current implementation requires {@link #forProductType(Class)} and {@link #of(Product, UniqueInventoryItem)} + * to also be updated when a new child class is added. + */ +abstract class InventoryMutateForm { + private @NotEmpty String name; + private @NotNull Quantity quantity; + private @NotNull @PositiveOrZero Double retailPrice; - public InventoryMutateForm(@NotNull CatalogDummyType type, @NotEmpty String name, - @NotNull Quantity quantity, @PositiveOrZero double wholesalePrice, @PositiveOrZero double retailPrice, - @PositiveOrZero Optional promotionPrice) { - this.type = type; - this.name = name; - this.quantity = quantity; - this.wholesalePrice = wholesalePrice; - this.retailPrice = retailPrice; - this.promotionPrice = promotionPrice; - } - - public static InventoryMutateForm empty() { - return new InventoryMutateForm(null, "", Quantity.of(0), 0, 0, Optional.empty()); - } - - public CatalogDummyType getType() { - return type; + public InventoryMutateForm() { } public String getName() { @@ -59,15 +53,80 @@ class InventoryMutateForm { return quantity; } - public double getWholesalePrice() { - return wholesalePrice; - } - - public double getRetailPrice() { + public Double getRetailPrice() { return retailPrice; } - public Optional getPromotionPrice() { - return promotionPrice; + public void setName(String name) { + this.name = name; } + + public void setQuantity(Quantity quantity) { + this.quantity = quantity; + } + + public void setRetailPrice(Double retailPrice) { + this.retailPrice = retailPrice; + } + + /** + * Creates an empty {@link InventoryMutateForm} for {@link Product}s of the given {@link Class}. + * + * @param a child class of {@link Product} + * @param type the concrete {@link Product} the form should mutate + * @return an object of an {@link InventoryMutateForm} subclass + * @throws IllegalArgumentException if the {@literal type} is not supported + */ + public static InventoryMutateForm forProductType(Class type) { + // Java can’t switch over Class in JDK17 (without preview features) + // See https://openjdk.org/jeps/406 for improvement in higher versions. + if (type.equals(Consumable.class)) { + return new ConsumableMutateForm(); + } else if (type.equals(Rentable.class)) { + return new RentableMutateForm(); + } else { + throw new IllegalArgumentException( + "InventoryMutateForm::forProductType not supported for given types"); + } + } + + /** + * Creates a populated {@link InventoryMutateForm} from a given {@link Product} and {@link UniqueInventoryItem}. + * + * @param product an instance of a {@link Product} subclass + * @param item an {@link UniqueInventoryItem} holding a {@link Quantity} + * @return an object of an {@link InventoryMutateForm} subclass + */ + public static InventoryMutateForm of(Product product, UniqueInventoryItem item) { + if (product instanceof Consumable consumable) { + return ConsumableMutateForm.of(consumable, item); + } else if (product instanceof Rentable rentable) { + return RentableMutateForm.of(rentable, item); + } else { + throw new IllegalArgumentException("InventoryMutateForm::ofProductAndItem not supported for given types"); + } + } + + /** + * Creates a new {@link Product} from a populated {@link InventoryMutateForm}. + * + * @return an instance of a {@link Product} subclass + */ + public abstract Product toProduct(); + + /** + * Modifies a given {@link Product} to match the values from the {@link InventoryMutateForm}. + * + * As the {@link Quantity} is stored inside of the {@link UniqueInventoryItem}, + * it has to be updated manually. + * + * @param product the {@link Product} to be updated + */ + public void modifyProduct(Product product) { + product.setName(getName()); + product.setPrice(Money.of(getRetailPrice(), EURO)); + modifyProductPrimitive(product); + } + + protected abstract void modifyProductPrimitive(Product product); } diff --git a/src/main/java/catering/inventory/RentableMutateForm.java b/src/main/java/catering/inventory/RentableMutateForm.java new file mode 100644 index 0000000..8e4ef15 --- /dev/null +++ b/src/main/java/catering/inventory/RentableMutateForm.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 Simon Bruder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package catering.inventory; + +import static org.salespointframework.core.Currencies.EURO; + +import java.util.Set; + +import org.javamoney.moneta.Money; +import org.salespointframework.catalog.Product; +import org.salespointframework.inventory.UniqueInventoryItem; + +import catering.catalog.Rentable; +import catering.order.OrderType; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; + +class RentableMutateForm extends InventoryMutateForm { + private @NotNull @PositiveOrZero Double wholesalePrice; + + public Double getWholesalePrice() { + return wholesalePrice; + } + + public void setWholesalePrice(Double wholesalePrice) { + this.wholesalePrice = wholesalePrice; + } + + public static RentableMutateForm of(Rentable product, UniqueInventoryItem item) { + RentableMutateForm form = new RentableMutateForm(); + form.setName(product.getName()); + form.setQuantity(item.getQuantity()); + form.setWholesalePrice(product.getWholesalePrice().getNumber().doubleValueExact()); + form.setRetailPrice(product.getRetailPrice().getNumber().doubleValueExact()); + return form; + } + + @Override + public Product toProduct() { + return new Rentable( + getName(), + Money.of(getRetailPrice(), EURO), + Money.of(getWholesalePrice(), EURO), + // FIXME: categories for creation of rentables are hardcoded + Set.of(OrderType.EVENT_CATERING, OrderType.MOBILE_BREAKFAST)); + } + + @Override + protected void modifyProductPrimitive(Product product) { + if (product instanceof Rentable rentable) { + rentable.setWholesalePrice(Money.of(getWholesalePrice(), EURO)); + } else { + throw new IllegalArgumentException("RentableMutateForm can only modify instances of Rentable"); + } + } +} diff --git a/src/main/resources/templates/inventory-mutate.html b/src/main/resources/templates/inventory-mutate.html index 3b2f612..91c7035 100644 --- a/src/main/resources/templates/inventory-mutate.html +++ b/src/main/resources/templates/inventory-mutate.html @@ -7,12 +7,9 @@

-
- -
- -
Ungültiger Typ.
-
+
@@ -35,8 +32,7 @@
Ungültiger Verkaufspreis.
-
- +
Ungültiger Aktionspreis.
diff --git a/src/main/resources/templates/inventory.html b/src/main/resources/templates/inventory.html index 826eb26..588e35a 100644 --- a/src/main/resources/templates/inventory.html +++ b/src/main/resources/templates/inventory.html @@ -24,17 +24,18 @@ - - + + - + - + +
diff --git a/src/test/java/catering/inventory/InventoryControllerIntegrationTests.java b/src/test/java/catering/inventory/InventoryControllerIntegrationTests.java index fd21002..355a64f 100644 --- a/src/test/java/catering/inventory/InventoryControllerIntegrationTests.java +++ b/src/test/java/catering/inventory/InventoryControllerIntegrationTests.java @@ -19,8 +19,8 @@ package catering.inventory; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.not; import static org.salespointframework.core.Currencies.EURO; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -29,9 +29,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Optional; + import org.javamoney.moneta.Money; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.salespointframework.catalog.Product; import org.salespointframework.catalog.Product.ProductIdentifier; import org.salespointframework.inventory.UniqueInventory; import org.salespointframework.inventory.UniqueInventoryItem; @@ -44,8 +47,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import catering.catalog.CatalogDummy; -import catering.catalog.CatalogDummyType; +import catering.catalog.Consumable; @AutoConfigureMockMvc @SpringBootTest @@ -57,13 +59,13 @@ class InventoryControllerIntegrationTests { UniqueInventory inventory; UniqueInventoryItem anyInventoryItem; - CatalogDummy anyProduct; + Product anyProduct; ProductIdentifier anyPid; @BeforeEach void populateAnyInventoryItem() { anyInventoryItem = inventory.findAll().stream().findAny().get(); - anyProduct = (CatalogDummy) anyInventoryItem.getProduct(); + anyProduct = anyInventoryItem.getProduct(); anyPid = anyProduct.getId(); } @@ -77,15 +79,15 @@ class InventoryControllerIntegrationTests { @Test @WithMockUser(username = "admin", roles = "ADMIN") - void adminCanAdd() throws Exception { - mvc.perform(get("/inventory/add")) + void adminCanAddConsumable() throws Exception { + mvc.perform(get("/inventory/add?type=Consumable")) .andExpect(status().isOk()) .andExpect(content().string(containsString("Produkt anlegen"))); long itemCountBefore = inventory.findAll().stream().count(); - mvc.perform(post("/inventory/add") - .param("type", "CONSUMABLE") + mvc.perform(post("/inventory/add?type=Consumable") + .queryParam("type", Consumable.class.getSimpleName()) .param("name", "MOCK Schnitzel Wiener Art (vegan)") .param("quantity", "100") .param("wholesalePrice", "3.00") @@ -97,12 +99,11 @@ class InventoryControllerIntegrationTests { assertThat(itemCountAfter).isEqualTo(itemCountBefore + 1); - // TODO: this must be changed once the catalog split is done - assertThat(inventory.findAll().stream()) - .extracting("product.type", "product.name", "quantity", "product.wholesalePrice", "product.price", + assertThat(inventory.findAll().filter(ie -> ie.getProduct() instanceof Consumable).stream()) + .extracting("product.name", "quantity", "product.wholesalePrice", "product.retailPrice", "product.promotionPrice") - .contains(tuple(CatalogDummyType.CONSUMABLE, "MOCK Schnitzel Wiener Art (vegan)", Quantity.of(100), - Money.of(3, EURO), Money.of(7.5, EURO), Money.of(6.66, EURO))); + .contains(tuple("MOCK Schnitzel Wiener Art (vegan)", Quantity.of(100), + Money.of(3, EURO), Money.of(7.5, EURO), Optional.of(Money.of(6.66, EURO)))); } @Test @@ -117,54 +118,52 @@ class InventoryControllerIntegrationTests { assertThat(itemCountAfter).isEqualTo(itemCountBefore - 1); - // TODO: this must be changed once the catalog split is done assertThat(inventory.findAll().stream()) - .extracting("product.type", "product.name", "quantity") - .doesNotContain(tuple(anyProduct.getType(), anyProduct.getName(), anyInventoryItem.getQuantity())); + .extracting("product.name", "quantity") + .doesNotContain(tuple(anyProduct.getName(), anyInventoryItem.getQuantity())); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void adminCanEditConsumable() throws Exception { - // TODO: this must be changed once the catalog split is done - CatalogDummy anyConsumable = inventory.findAll().stream() + Consumable anyConsumable = inventory.findAll().stream() .map(UniqueInventoryItem::getProduct) - .map(p -> (CatalogDummy) p) - .filter(cd -> cd.getType().equals(CatalogDummyType.CONSUMABLE)) + .filter(Consumable.class::isInstance) + .map(Consumable.class::cast) .findAny() .get(); - mvc.perform(get("/inventory/edit/" + anyConsumable.getId())) + mvc.perform( + get("/inventory/edit/" + anyConsumable.getId())) .andExpect(status().isOk()) .andExpect(content().string(containsString("Produkt bearbeiten"))); - boolean hasPromotionPrice = anyConsumable.getPromotionPrice() != null; - mvc.perform(post("/inventory/edit/" + anyConsumable.getId()) + .queryParam("type", Consumable.class.getSimpleName()) .param("type", "CONSUMABLE") .param("name", "MOCK edited") .param("quantity", "4711") .param("wholesalePrice", "0.01") - .param("retailPrice", "0.01") - .param("promotionPrice", hasPromotionPrice ? "" : "5")) + .param("retailPrice", "0.03") + .param("promotionPrice", "0.02")) .andExpect(redirectedUrl("/inventory")); UniqueInventoryItem editedInventoryItem = inventory.findByProductIdentifier(anyConsumable.getId()).stream() .findAny().get(); - CatalogDummy editedProduct = (CatalogDummy) editedInventoryItem.getProduct(); + Consumable editedProduct = (Consumable) editedInventoryItem.getProduct(); assertThat(editedInventoryItem.getQuantity()).isEqualTo(Quantity.of(4711)); assertThat(editedProduct) - .extracting("type", "name", "wholesalePrice", "price", "promotionPrice") - .containsExactly(CatalogDummyType.CONSUMABLE, "MOCK edited", Money.of(0.01, EURO), - Money.of(0.01, EURO), hasPromotionPrice ? null : Money.of(0.01, EURO)); + .extracting("name", "wholesalePrice", "retailPrice", "promotionPrice") + .containsExactly("MOCK edited", Money.of(0.01, EURO), + Money.of(0.03, EURO), Optional.of(Money.of(0.02, EURO))); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void invalidAddReturnsNiceError() throws Exception { mvc.perform(post("/inventory/add") - .param("type", "CONSUMABLE") + .queryParam("type", Consumable.class.getSimpleName()) .param("name", "") .param("quantity", "10") .param("wholesalePrice", "1.00") @@ -176,7 +175,7 @@ class InventoryControllerIntegrationTests { @WithMockUser(username = "admin", roles = "ADMIN") void missingRetailPriceIsNoError() throws Exception { mvc.perform(post("/inventory/add") - .param("type", "CONSUMABLE") + .queryParam("type", Consumable.class.getSimpleName()) .param("name", "MOCK") .param("quantity", "10") .param("wholesalePrice", "1.00") @@ -188,7 +187,7 @@ class InventoryControllerIntegrationTests { @WithMockUser(username = "admin", roles = "ADMIN") void invalidEditReturnsNiceError() throws Exception { mvc.perform(post("/inventory/edit/" + anyPid) - .param("type", "CONSUMABLE") + .queryParam("type", Consumable.class.getSimpleName()) .param("name", "") .param("quantity", "10") .param("wholesalePrice", "1.00") @@ -220,22 +219,25 @@ class InventoryControllerIntegrationTests { @Test void disallowUnauthorizedEditPage() throws Exception { - assertRedirectsToLogin(mvc.perform(get("/inventory/edit/" + anyPid))); + assertRedirectsToLogin( + mvc.perform(get("/inventory/edit/" + anyPid).queryParam("type", Consumable.class.getSimpleName()))); } @Test void disallowUnauthorizedEdit() throws Exception { - assertRedirectsToLogin(mvc.perform(post("/inventory/edit/" + anyPid))); + assertRedirectsToLogin( + mvc.perform(post("/inventory/edit/" + anyPid).queryParam("type", Consumable.class.getSimpleName()))); } @Test void disallowUnauthorizedAddPage() throws Exception { - assertRedirectsToLogin(mvc.perform(get("/inventory/add"))); + assertRedirectsToLogin(mvc.perform(get("/inventory/add").queryParam("type", Consumable.class.getSimpleName()))); } @Test void disallowUnauthorizedAdd() throws Exception { - assertRedirectsToLogin(mvc.perform(post("/inventory/add"))); + assertRedirectsToLogin( + mvc.perform(post("/inventory/add").queryParam("type", Consumable.class.getSimpleName()))); } @Test