diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 000000000..8fb587348 --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,15 @@ +name: CI for Java Invoice +on: [push] +jobs: + test: + name: Unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'adopt' + - name: Test + run: mvn test \ No newline at end of file diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 000000000..d1f49a71a --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,40 @@ +#-------------------------------------------------------------------------------# +# Discover all capabilities of Qodana in our documentation # +# https://www.jetbrains.com/help/qodana/about-qodana.html # +#-------------------------------------------------------------------------------# + +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: + - master + - tdd + +jobs: + qodana: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2026.1 + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} + with: + # When pr-mode is set to true, Qodana analyzes only the files that have been changed + pr-mode: false + use-caches: true + post-pr-comment: true + use-annotations: true + # Upload Qodana results (SARIF, other artifacts, logs) as an artifact to the job + upload-result: false + # quick-fixes available in Ultimate and Ultimate Plus plans + push-fixes: 'none' \ No newline at end of file diff --git a/config/checkstyle.xml b/config/checkstyle.xml new file mode 100644 index 000000000..24335bb6c --- /dev/null +++ b/config/checkstyle.xml @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/effective-pom.xml b/effective-pom.xml new file mode 100644 index 000000000..21a89f947 --- /dev/null +++ b/effective-pom.xml @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + 4.0.0 + pl.edu.agh.mwo.hellomaven + hello-maven + 1.0.0-SNAPSHOT + + 17 + 17 + 3.2.0 + UTF-8 + + + + org.junit.jupiter + junit-jupiter-api + 6.0.3 + test + + + + + + false + + central + Central Repository + https://repo.maven.apache.org/maven2 + + + + + + false + + central + Central Repository + https://repo.maven.apache.org/maven2 + + + + /Users/olenka/Desktop/java-invoice-tdd/src/main/java + /Users/olenka/Desktop/java-invoice-tdd/src/main/scripts + /Users/olenka/Desktop/java-invoice-tdd/src/test/java + /Users/olenka/Desktop/java-invoice-tdd/target/classes + /Users/olenka/Desktop/java-invoice-tdd/target/test-classes + + + /Users/olenka/Desktop/java-invoice-tdd/src/main/resources + + + + + /Users/olenka/Desktop/java-invoice-tdd/src/test/resources + + + /Users/olenka/Desktop/java-invoice-tdd/target + hello-maven-1.0.0-SNAPSHOT + + + + maven-antrun-plugin + 3.1.0 + + + maven-assembly-plugin + 3.7.1 + + + maven-dependency-plugin + 3.7.0 + + + maven-release-plugin + 3.0.1 + + + + + + maven-jar-plugin + 3.2.0 + + + default-jar + package + + jar + + + + + true + pl.edu.agh.mwo.hellomaven.App + + + + + + + + + true + pl.edu.agh.mwo.hellomaven.App + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + pl.edu.agh.mwo.hellomaven.App + + + + maven-clean-plugin + 3.2.0 + + + default-clean + clean + + clean + + + + + + maven-resources-plugin + 3.4.0 + + + default-testResources + process-test-resources + + testResources + + + + default-resources + process-resources + + resources + + + + + + maven-compiler-plugin + 3.15.0 + + + default-compile + compile + + compile + + + + default-testCompile + test-compile + + testCompile + + + + + + maven-surefire-plugin + 3.5.4 + + + default-test + test + + test + + + + + + maven-install-plugin + 3.1.4 + + + default-install + install + + install + + + + + + maven-deploy-plugin + 3.1.4 + + + default-deploy + deploy + + deploy + + + + + + maven-site-plugin + 3.12.1 + + + default-site + site + + site + + + /Users/olenka/Desktop/java-invoice-tdd/target/site + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + default-deploy + site-deploy + + deploy + + + /Users/olenka/Desktop/java-invoice-tdd/target/site + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + + /Users/olenka/Desktop/java-invoice-tdd/target/site + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + + + /Users/olenka/Desktop/java-invoice-tdd/target/site + + diff --git a/pom.xml b/pom.xml index 2d40c3e8a..411c4d9c9 100644 --- a/pom.xml +++ b/pom.xml @@ -19,11 +19,33 @@ org.apache.maven.plugins - maven-compiler-plugin + maven-checkstyle-plugin + 3.3.1 - 17 - 17 - + + ${project.basedir}/config/checkstyle.xml + + true + true + false + + + + validate + validate + + check + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + 17 + 17 + @@ -32,7 +54,7 @@ junit junit - 4.12 + 4.13.2 test @@ -40,5 +62,11 @@ hamcrest-all 1.3 + + org.hamcrest + hamcrest + 2.2 + test + - + \ No newline at end of file diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 000000000..fcaa9673e --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,49 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# + +################################################################################# +# WARNING: Do not store sensitive information in this file, # +# as its contents will be included in the Qodana report. # +################################################################################# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +projectJDK: "26" #(Applied in CI/CD pipeline) + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +# Quality gate. Will fail the CI/CD pipeline if any condition is not met +# severityThresholds - configures maximum thresholds for different problem severities +# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code +# Code Coverage is available in Ultimate and Ultimate Plus plans +#failureConditions: +# severityThresholds: +# any: 15 +# critical: 5 +# testCoverageThresholds: +# fresh: 70 +# total: 50 + +#Qodana supports other languages, for example, Python, JavaScript, TypeScript, Go, C#, PHP +#For all supported languages see https://www.jetbrains.com/help/qodana/linters.html +linter: jetbrains/qodana-jvm-community:2026.1 diff --git a/src/main/java/pl/edu/agh/mwo/invoice/Invoice.java b/src/main/java/pl/edu/agh/mwo/invoice/Invoice.java index 3fd0def7a..9df7f3e97 100644 --- a/src/main/java/pl/edu/agh/mwo/invoice/Invoice.java +++ b/src/main/java/pl/edu/agh/mwo/invoice/Invoice.java @@ -1,62 +1,87 @@ package pl.edu.agh.mwo.invoice; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import pl.edu.agh.mwo.invoice.product.Product; public class Invoice { - private Map products = new HashMap<>(); + private static int nextNumber = 1; + private final int number; + private final Map products = new LinkedHashMap(); - public void addProduct(Product product) { - addProduct(product, 1); + public Invoice() { + number = nextNumber++; } - public void addProduct(Product product, Integer quantity) { - - if (product == null) { - throw new IllegalArgumentException("Product cannot be null"); - } - - if (quantity == null || quantity <= 0) { - throw new IllegalArgumentException("Quantity must be positive"); - } - products.put(product, quantity); + public int getNumber() { + return number; } - public BigDecimal getSubtotal() { + public String getPrintout() { + StringBuilder printout = new StringBuilder(); + String separator = System.lineSeparator(); - BigDecimal subtotal = BigDecimal.ZERO; + printout.append("Numer faktury: ") + .append(number) + .append(separator); for (Map.Entry entry : products.entrySet()) { Product product = entry.getKey(); Integer quantity = entry.getValue(); - subtotal = subtotal.add( - product.getPrice().multiply(BigDecimal.valueOf(quantity))); + printout.append(product.getName()) + .append(", ") + .append(quantity) + .append(" szt., ") + .append(product.getPrice()) + .append(separator); } - return subtotal; + + printout.append("Liczba pozycji: ") + .append(products.size()); + + return printout.toString(); } - public BigDecimal getTax() { - BigDecimal tax = BigDecimal.ZERO; - for (Map.Entry entry : products.entrySet()) { - Product product = entry.getKey(); - Integer quantity = entry.getValue(); + public void addProduct(Product product) { + addProduct(product, 1); + } + + public void addProduct(Product product, Integer quantity) { + if (product == null || quantity <= 0) { + throw new IllegalArgumentException(); + } + + Integer currentQuantity = products.get(product); - BigDecimal productTax = - product.getPrice() - .multiply(product.getTaxPercent()) - .multiply(BigDecimal.valueOf(quantity)); - tax = tax.add(productTax); + if (currentQuantity == null) { + products.put(product, quantity); + } else { + products.put(product, currentQuantity + quantity); } - return tax; } - public BigDecimal getTotal() { - return getSubtotal().add(getTax()); + public BigDecimal getNetTotal() { + BigDecimal totalNet = BigDecimal.ZERO; + for (Product product : products.keySet()) { + BigDecimal quantity = new BigDecimal(products.get(product)); + totalNet = totalNet.add(product.getPrice().multiply(quantity)); + } + return totalNet; + } + + public BigDecimal getTaxTotal() { + return getGrossTotal().subtract(getNetTotal()); + } + + public BigDecimal getGrossTotal() { + BigDecimal totalGross = BigDecimal.ZERO; + for (Product product : products.keySet()) { + BigDecimal quantity = new BigDecimal(products.get(product)); + totalGross = totalGross.add(product.getPriceWithTax().multiply(quantity)); + } + return totalGross; } } diff --git a/src/main/java/pl/edu/agh/mwo/invoice/package-info.java b/src/main/java/pl/edu/agh/mwo/invoice/package-info.java new file mode 100644 index 000000000..05fbd5def --- /dev/null +++ b/src/main/java/pl/edu/agh/mwo/invoice/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides classes for creating and managing invoices. + */ +package pl.edu.agh.mwo.invoice; \ No newline at end of file diff --git a/src/main/java/pl/edu/agh/mwo/invoice/product/BottleOfWine.java b/src/main/java/pl/edu/agh/mwo/invoice/product/BottleOfWine.java new file mode 100644 index 000000000..933fd3e05 --- /dev/null +++ b/src/main/java/pl/edu/agh/mwo/invoice/product/BottleOfWine.java @@ -0,0 +1,18 @@ +package pl.edu.agh.mwo.invoice.product; + +import java.math.BigDecimal; + +public class BottleOfWine extends OtherProduct { + + private static final BigDecimal EXCISE_DUTY = + new BigDecimal("5.56"); + + public BottleOfWine(String name, BigDecimal price) { + super(name, price); + } + + @Override + public BigDecimal getPriceWithTax() { + return super.getPriceWithTax().add(EXCISE_DUTY); + } +} \ No newline at end of file diff --git a/src/main/java/pl/edu/agh/mwo/invoice/product/FuelCanister.java b/src/main/java/pl/edu/agh/mwo/invoice/product/FuelCanister.java new file mode 100644 index 000000000..7b24b312b --- /dev/null +++ b/src/main/java/pl/edu/agh/mwo/invoice/product/FuelCanister.java @@ -0,0 +1,18 @@ +package pl.edu.agh.mwo.invoice.product; + +import java.math.BigDecimal; + +public class FuelCanister extends TaxFreeProduct { + + private static final BigDecimal EXCISE_DUTY = + new BigDecimal("5.56"); + + public FuelCanister(String name, BigDecimal price) { + super(name, price); + } + + @Override + public BigDecimal getPriceWithTax() { + return super.getPriceWithTax().add(EXCISE_DUTY); + } +} \ No newline at end of file diff --git a/src/main/java/pl/edu/agh/mwo/invoice/product/Product.java b/src/main/java/pl/edu/agh/mwo/invoice/product/Product.java index ee782a311..6ceba79d1 100644 --- a/src/main/java/pl/edu/agh/mwo/invoice/product/Product.java +++ b/src/main/java/pl/edu/agh/mwo/invoice/product/Product.java @@ -1,6 +1,7 @@ package pl.edu.agh.mwo.invoice.product; import java.math.BigDecimal; +import java.util.Objects; public abstract class Product { private final String name; @@ -10,15 +11,10 @@ public abstract class Product { private final BigDecimal taxPercent; protected Product(String name, BigDecimal price, BigDecimal tax) { - - if (name == null || name.isBlank()) { - throw new IllegalArgumentException("Product name cannot be null or blank"); - } - - if (price == null || price.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException("Product price cannot be null or negative"); + if (name == null || name.equals("") || price == null || tax == null || tax.compareTo(new BigDecimal(0)) < 0 + || price.compareTo(new BigDecimal(0)) < 0) { + throw new IllegalArgumentException(); } - this.name = name; this.price = price; this.taxPercent = tax; @@ -37,6 +33,30 @@ public BigDecimal getTaxPercent() { } public BigDecimal getPriceWithTax() { - return price.add(price.multiply(taxPercent)); + return price.multiply(taxPercent).add(price); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + Product product = (Product) object; + + return Objects.equals(getName(), product.getName()) + && getPrice().compareTo(product.getPrice()) == 0; + } + + @Override + public int hashCode() { + return Objects.hash( + getClass(), + getName(), + getPrice().stripTrailingZeros()); } } diff --git a/src/main/java/pl/edu/agh/mwo/invoice/product/package-info.java b/src/main/java/pl/edu/agh/mwo/invoice/product/package-info.java new file mode 100644 index 000000000..b08eb4784 --- /dev/null +++ b/src/main/java/pl/edu/agh/mwo/invoice/product/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides classes representing different types of products. + */ +package pl.edu.agh.mwo.invoice.product; \ No newline at end of file diff --git a/src/test/java/pl/edu/agh/mwo/invoice/InvoiceTest.java b/src/test/java/pl/edu/agh/mwo/invoice/InvoiceTest.java index 7f4b6f795..f15ad2c48 100644 --- a/src/test/java/pl/edu/agh/mwo/invoice/InvoiceTest.java +++ b/src/test/java/pl/edu/agh/mwo/invoice/InvoiceTest.java @@ -12,10 +12,13 @@ import pl.edu.agh.mwo.invoice.product.OtherProduct; import pl.edu.agh.mwo.invoice.product.Product; import pl.edu.agh.mwo.invoice.product.TaxFreeProduct; +import pl.edu.agh.mwo.invoice.product.BottleOfWine; +import pl.edu.agh.mwo.invoice.product.FuelCanister; public class InvoiceTest { private Invoice invoice; + @Before public void createEmptyInvoiceForTheTest() { invoice = new Invoice(); @@ -23,17 +26,17 @@ public void createEmptyInvoiceForTheTest() { @Test public void testEmptyInvoiceHasEmptySubtotal() { - Assert.assertThat(BigDecimal.ZERO, Matchers.comparesEqualTo(invoice.getSubtotal())); + Assert.assertThat(BigDecimal.ZERO, Matchers.comparesEqualTo(invoice.getNetTotal())); } @Test public void testEmptyInvoiceHasEmptyTaxAmount() { - Assert.assertThat(BigDecimal.ZERO, Matchers.comparesEqualTo(invoice.getTax())); + Assert.assertThat(BigDecimal.ZERO, Matchers.comparesEqualTo(invoice.getTaxTotal())); } @Test public void testEmptyInvoiceHasEmptyTotal() { - Assert.assertThat(BigDecimal.ZERO, Matchers.comparesEqualTo(invoice.getTotal())); + Assert.assertThat(BigDecimal.ZERO, Matchers.comparesEqualTo(invoice.getGrossTotal())); } @Test @@ -42,21 +45,21 @@ public void testInvoiceSubtotalWithTwoDifferentProducts() { Product apples = new TaxFreeProduct("Owoce", new BigDecimal("10")); invoice.addProduct(onions); invoice.addProduct(apples); - Assert.assertThat(new BigDecimal("20"), Matchers.comparesEqualTo(invoice.getSubtotal())); + Assert.assertThat(new BigDecimal("20"), Matchers.comparesEqualTo(invoice.getNetTotal())); } @Test public void testInvoiceSubtotalWithManySameProducts() { Product onions = new TaxFreeProduct("Warzywa", BigDecimal.valueOf(10)); invoice.addProduct(onions, 100); - Assert.assertThat(new BigDecimal("1000"), Matchers.comparesEqualTo(invoice.getSubtotal())); + Assert.assertThat(new BigDecimal("1000"), Matchers.comparesEqualTo(invoice.getNetTotal())); } @Test public void testInvoiceHasTheSameSubtotalAndTotalIfTaxIsZero() { Product taxFreeProduct = new TaxFreeProduct("Warzywa", new BigDecimal("199.99")); invoice.addProduct(taxFreeProduct); - Assert.assertThat(invoice.getTotal(), Matchers.comparesEqualTo(invoice.getSubtotal())); + Assert.assertThat(invoice.getNetTotal(), Matchers.comparesEqualTo(invoice.getGrossTotal())); } @Test @@ -64,7 +67,7 @@ public void testInvoiceHasProperSubtotalForManyProducts() { invoice.addProduct(new TaxFreeProduct("Owoce", new BigDecimal("200"))); invoice.addProduct(new DairyProduct("Maslanka", new BigDecimal("100"))); invoice.addProduct(new OtherProduct("Wino", new BigDecimal("10"))); - Assert.assertThat(new BigDecimal("310"), Matchers.comparesEqualTo(invoice.getSubtotal())); + Assert.assertThat(new BigDecimal("310"), Matchers.comparesEqualTo(invoice.getNetTotal())); } @Test @@ -75,7 +78,7 @@ public void testInvoiceHasProperTaxValueForManyProduct() { invoice.addProduct(new DairyProduct("Kefir", new BigDecimal("100"))); // tax: 2.30 invoice.addProduct(new OtherProduct("Piwko", new BigDecimal("10"))); - Assert.assertThat(new BigDecimal("10.30"), Matchers.comparesEqualTo(invoice.getTax())); + Assert.assertThat(new BigDecimal("10.30"), Matchers.comparesEqualTo(invoice.getTaxTotal())); } @Test @@ -86,7 +89,7 @@ public void testInvoiceHasProperTotalValueForManyProduct() { invoice.addProduct(new DairyProduct("Maslo", new BigDecimal("100"))); // price with tax: 12.30 invoice.addProduct(new OtherProduct("Chipsy", new BigDecimal("10"))); - Assert.assertThat(new BigDecimal("320.30"), Matchers.comparesEqualTo(invoice.getTotal())); + Assert.assertThat(new BigDecimal("320.30"), Matchers.comparesEqualTo(invoice.getGrossTotal())); } @Test @@ -97,7 +100,7 @@ public void testInvoiceHasPropoerSubtotalWithQuantityMoreThanOne() { invoice.addProduct(new DairyProduct("Kozi Serek", new BigDecimal("10")), 3); // 1000x pinezka - price: 10 invoice.addProduct(new OtherProduct("Pinezka", new BigDecimal("0.01")), 1000); - Assert.assertThat(new BigDecimal("50"), Matchers.comparesEqualTo(invoice.getSubtotal())); + Assert.assertThat(new BigDecimal("50"), Matchers.comparesEqualTo(invoice.getNetTotal())); } @Test @@ -108,7 +111,7 @@ public void testInvoiceHasPropoerTotalWithQuantityMoreThanOne() { invoice.addProduct(new DairyProduct("Chedar", new BigDecimal("10")), 3); // 1000x pinezka - price with tax: 12.30 invoice.addProduct(new OtherProduct("Pinezka", new BigDecimal("0.01")), 1000); - Assert.assertThat(new BigDecimal("54.70"), Matchers.comparesEqualTo(invoice.getTotal())); + Assert.assertThat(new BigDecimal("54.70"), Matchers.comparesEqualTo(invoice.getGrossTotal())); } @Test(expected = IllegalArgumentException.class) @@ -125,4 +128,79 @@ public void testInvoiceWithNegativeQuantity() { public void testAddingNullProduct() { invoice.addProduct(null); } -} + + @Test + public void testInvoiceNumbersAreAssignedAutomatically() { + Invoice firstInvoice = new Invoice(); + Invoice secondInvoice = new Invoice(); + + Assert.assertEquals( + firstInvoice.getNumber() + 1, + secondInvoice.getNumber() + ); + } + + @Test + public void testInvoicePrintoutContainsProductsAndInvoiceNumber() { + invoice.addProduct( + new TaxFreeProduct("Kubek", new BigDecimal("5")), 2); + invoice.addProduct( + new DairyProduct("Ser", new BigDecimal("10")), 3); + + String separator = System.lineSeparator(); + + String expected = "Numer faktury: " + + invoice.getNumber() + + separator + + "Kubek, 2 szt., 5" + + separator + + "Ser, 3 szt., 10" + + separator + + "Liczba pozycji: 2"; + + Assert.assertEquals(expected, invoice.getPrintout()); + } + + @Test + public void testAddingEquivalentProductsIncreasesQuantity() { + Product firstMug = new TaxFreeProduct( + "Kubek", new BigDecimal("5")); + + Product secondMug = new TaxFreeProduct( + "Kubek", new BigDecimal("5.00")); + + invoice.addProduct(firstMug, 2); + invoice.addProduct(secondMug, 3); + + String separator = System.lineSeparator(); + + String expected = "Numer faktury: " + + invoice.getNumber() + + separator + + "Kubek, 5 szt., 5" + + separator + + "Liczba pozycji: 1"; + + Assert.assertEquals(expected, invoice.getPrintout()); + } + + @Test + public void testBottleOfWineIncludesVatAndExciseDuty() { + invoice.addProduct( + new BottleOfWine("Wino", new BigDecimal("10"))); + + Assert.assertThat( + new BigDecimal("7.86"), + Matchers.comparesEqualTo(invoice.getTaxTotal())); + } + + @Test + public void testFuelCanisterIncludesOnlyExciseDuty() { + invoice.addProduct( + new FuelCanister("Paliwo", new BigDecimal("10"))); + + Assert.assertThat( + new BigDecimal("5.56"), + Matchers.comparesEqualTo(invoice.getTaxTotal())); + } + }