From 6de543b0a0a25c76412db193172298a344dfbf28 Mon Sep 17 00:00:00 2001 From: Hericode Date: Wed, 16 Jul 2025 09:52:34 +0200 Subject: [PATCH] Start from somewhere --- .gitignore | 2 + README.md | 79 +++ build.xml | 82 +++ ivy.xml | 13 + pom.xml | 44 ++ .../constants/CustomerContactField.java | 68 +++ .../constants/DefaultImageImplementation.java | 21 + .../common/constants/DefaultImages.java | 33 + .../pasteque/common/constants/ModelName.java | 81 +++ .../pasteque/common/constants/OptionName.java | 69 +++ .../common/constants/ResourceName.java | 148 +++++ .../common/constants/ResourceType.java | 58 ++ .../pasteque/common/constants/ScaleType.java | 64 ++ .../common/constants/package-info.java | 4 + .../AssociationInconsistencyException.java | 121 ++++ .../common/datasource/CategoryDataSource.java | 61 ++ .../common/datasource/CurrencyDataSource.java | 72 +++ .../common/datasource/CustomerDataSource.java | 94 +++ .../common/datasource/ImageDataSource.java | 47 ++ .../datasource/PaymentModeDataSource.java | 58 ++ .../common/datasource/api/APIHost.java | 87 +++ .../common/datasource/api/APIResponse.java | 49 ++ .../datasource/api/APIResponseStatus.java | 63 ++ .../common/datasource/api/APIToken.java | 26 + .../datasource/api/SimpleAPIResponse.java | 63 ++ .../common/datasource/api/package-info.java | 4 + .../SerializedFileDataSource.java | 230 +++++++ .../implementation/package-info.java | 7 + .../common/datasource/package-info.java | 10 + .../datatransfer/dto/APIVersionDTO.java | 97 +++ .../common/datatransfer/dto/CategoryDTO.java | 61 ++ .../common/datatransfer/dto/CommonDTO.java | 218 +++++++ .../datatransfer/dto/CompositionDTO.java | 180 ++++++ .../common/datatransfer/dto/CurrencyDTO.java | 157 +++++ .../common/datatransfer/dto/CustomerDTO.java | 414 +++++++++++++ .../datatransfer/dto/DiscountProfileDTO.java | 71 +++ .../common/datatransfer/dto/FloorDTO.java | 134 ++++ .../common/datatransfer/dto/ImageDTO.java | 123 ++++ .../common/datatransfer/dto/OptionDTO.java | 100 +++ .../common/datatransfer/dto/OrderDTO.java | 367 +++++++++++ .../datatransfer/dto/PaymentModeDTO.java | 244 ++++++++ .../common/datatransfer/dto/ProductDTO.java | 232 +++++++ .../common/datatransfer/dto/ResourceDTO.java | 117 ++++ .../common/datatransfer/dto/RoleDTO.java | 63 ++ .../datatransfer/dto/TariffAreaDTO.java | 134 ++++ .../common/datatransfer/dto/TaxDTO.java | 71 +++ .../common/datatransfer/dto/UserDTO.java | 88 +++ .../common/datatransfer/dto/package-info.java | 12 + .../common/datatransfer/package-info.java | 4 + .../org/pasteque/common/model/Category.java | 80 +++ .../pasteque/common/model/CategoryTree.java | 105 ++++ .../org/pasteque/common/model/Currency.java | 146 +++++ .../org/pasteque/common/model/Customer.java | 152 +++++ .../java/org/pasteque/common/model/Image.java | 58 ++ .../java/org/pasteque/common/model/User.java | 124 ++++ .../model/option/AccountingConfigOption.java | 197 ++++++ .../option/CustomContactFieldsOption.java | 64 ++ .../common/model/option/package-info.java | 4 + .../common/model/order/ComputedPrices.java | 170 ++++++ .../pasteque/common/model/order/Order.java | 245 ++++++++ .../common/model/order/OrderPrice.java | 251 ++++++++ .../common/model/order/PriceLineRound.java | 448 ++++++++++++++ .../common/model/order/PriceNoRound.java | 118 ++++ .../common/model/order/PriceRound.java | 76 +++ .../common/model/order/PriceTotalRound.java | 70 +++ .../common/model/order/ProductLine.java | 381 ++++++++++++ .../common/model/order/package-info.java | 4 + .../pasteque/common/model/package-info.java | 4 + .../org/pasteque/common/package-info.java | 8 + .../org/pasteque/common/util/HashCypher.java | 60 ++ .../org/pasteque/common/util/Hexadecimal.java | 56 ++ .../pasteque/common/util/package-info.java | 4 + .../org/pasteque/common/view/Buttonable.java | 24 + .../pasteque/common/view/CoordStretcher.java | 185 ++++++ .../org/pasteque/common/view/Formatter.java | 141 +++++ .../pasteque/common/view/package-info.java | 10 + .../org/pasteque/coreutil/ImmutableList.java | 111 ++++ .../org/pasteque/coreutil/ParseException.java | 26 + .../coreutil/constants/DiscountTarget.java | 84 +++ .../coreutil/constants/DiscountType.java | 52 ++ .../coreutil/constants/FiscalTicketType.java | 53 ++ .../coreutil/constants/PaymentModeType.java | 116 ++++ .../coreutil/constants/SessionCloseType.java | 60 ++ .../pasteque/coreutil/constants/TaxType.java | 52 ++ .../coreutil/constants/package-info.java | 4 + .../datatransfer/dto/CashRegisterDTO.java | 98 +++ .../datatransfer/dto/CashSessionDTO.java | 138 +++++ .../datatransfer/dto/DTOInterface.java | 26 + .../datatransfer/dto/FiscalTicketDTO.java | 161 +++++ .../datatransfer/dto/MovementDTO.java | 102 ++++ .../coreutil/datatransfer/dto/PaymentDTO.java | 100 +++ .../datatransfer/dto/TaxAmountDTO.java | 114 ++++ .../coreutil/datatransfer/dto/TaxDTO.java | 83 +++ .../coreutil/datatransfer/dto/TicketDTO.java | 547 +++++++++++++++++ .../datatransfer/dto/WeakAssociationDTO.java | 87 +++ .../coreutil/datatransfer/dto/ZTicketDTO.java | 574 ++++++++++++++++++ .../datatransfer/dto/package-info.java | 8 + .../datatransfer/format/BinaryDTOFormat.java | 160 +++++ .../datatransfer/format/DateDTOFormat.java | 124 ++++ .../datatransfer/format/package-info.java | 6 + .../integrity/IntegrityException.java | 19 + .../integrity/IntegrityExceptions.java | 47 ++ .../integrity/IntegrityFieldConstraint.java | 99 +++ .../integrity/IntegrityRecordConstraint.java | 51 ++ .../integrity/InvalidFieldException.java | 161 +++++ .../integrity/InvalidRecordException.java | 57 ++ .../datatransfer/integrity/package-info.java | 4 + .../datatransfer/parser/DTOFactory.java | 128 ++++ .../datatransfer/parser/JSONReader.java | 441 ++++++++++++++ .../coreutil/datatransfer/parser/Reader.java | 340 +++++++++++ .../coreutil/datatransfer/parser/Writer.java | 15 + .../datatransfer/parser/package-info.java | 7 + .../org/pasteque/coreutil/package-info.java | 8 + .../org/pasteque/coreutil/price/Discount.java | 199 ++++++ .../coreutil/price/FinalTaxAmount.java | 82 +++ .../org/pasteque/coreutil/price/Price.java | 149 +++++ .../org/pasteque/coreutil/price/Price2.java | 130 ++++ .../org/pasteque/coreutil/price/Price5.java | 138 +++++ .../org/pasteque/coreutil/price/Quantity.java | 57 ++ .../org/pasteque/coreutil/price/Rate.java | 220 +++++++ .../java/org/pasteque/coreutil/price/Tax.java | 97 +++ .../pasteque/coreutil/price/TaxAmount.java | 82 +++ .../pasteque/coreutil/price/package-info.java | 11 + .../coreutil/transition/LineTransition.java | 66 ++ .../coreutil/transition/OrderTransition.java | 35 ++ .../coreutil/transition/package-info.java | 10 + .../pasteque/major/domain/FiscalTicket.java | 331 ++++++++++ .../major/domain/MajorCashRegister.java | 80 +++ .../major/domain/MajorCashSession.java | 242 ++++++++ .../org/pasteque/major/domain/MajorLine.java | 138 +++++ .../org/pasteque/major/domain/MajorOrder.java | 113 ++++ .../pasteque/major/domain/MajorTicket.java | 160 +++++ .../pasteque/major/domain/MajorZTicket.java | 72 +++ .../org/pasteque/major/domain/Movement.java | 134 ++++ .../org/pasteque/major/domain/Payment.java | 134 ++++ .../pasteque/major/domain/package-info.java | 9 + .../java/org/pasteque/major/package-info.java | 11 + src/main/java/org/pasteque/package-info.java | 30 + src/main/javadoc/javadoc-dark.css | 92 +++ src/main/resources/broken.png | Bin 0 -> 882 bytes 140 files changed, 14915 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.xml create mode 100644 ivy.xml create mode 100644 pom.xml create mode 100644 src/main/java/org/pasteque/common/constants/CustomerContactField.java create mode 100644 src/main/java/org/pasteque/common/constants/DefaultImageImplementation.java create mode 100644 src/main/java/org/pasteque/common/constants/DefaultImages.java create mode 100644 src/main/java/org/pasteque/common/constants/ModelName.java create mode 100644 src/main/java/org/pasteque/common/constants/OptionName.java create mode 100644 src/main/java/org/pasteque/common/constants/ResourceName.java create mode 100644 src/main/java/org/pasteque/common/constants/ResourceType.java create mode 100644 src/main/java/org/pasteque/common/constants/ScaleType.java create mode 100644 src/main/java/org/pasteque/common/constants/package-info.java create mode 100644 src/main/java/org/pasteque/common/datasource/AssociationInconsistencyException.java create mode 100644 src/main/java/org/pasteque/common/datasource/CategoryDataSource.java create mode 100644 src/main/java/org/pasteque/common/datasource/CurrencyDataSource.java create mode 100644 src/main/java/org/pasteque/common/datasource/CustomerDataSource.java create mode 100644 src/main/java/org/pasteque/common/datasource/ImageDataSource.java create mode 100644 src/main/java/org/pasteque/common/datasource/PaymentModeDataSource.java create mode 100644 src/main/java/org/pasteque/common/datasource/api/APIHost.java create mode 100644 src/main/java/org/pasteque/common/datasource/api/APIResponse.java create mode 100644 src/main/java/org/pasteque/common/datasource/api/APIResponseStatus.java create mode 100644 src/main/java/org/pasteque/common/datasource/api/APIToken.java create mode 100644 src/main/java/org/pasteque/common/datasource/api/SimpleAPIResponse.java create mode 100644 src/main/java/org/pasteque/common/datasource/api/package-info.java create mode 100644 src/main/java/org/pasteque/common/datasource/implementation/SerializedFileDataSource.java create mode 100644 src/main/java/org/pasteque/common/datasource/implementation/package-info.java create mode 100644 src/main/java/org/pasteque/common/datasource/package-info.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/APIVersionDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/CategoryDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/CommonDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/CompositionDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/CurrencyDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/CustomerDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/DiscountProfileDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/FloorDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/ImageDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/OptionDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/OrderDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/PaymentModeDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/ProductDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/ResourceDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/RoleDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/TariffAreaDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/TaxDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/UserDTO.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/dto/package-info.java create mode 100644 src/main/java/org/pasteque/common/datatransfer/package-info.java create mode 100644 src/main/java/org/pasteque/common/model/Category.java create mode 100644 src/main/java/org/pasteque/common/model/CategoryTree.java create mode 100644 src/main/java/org/pasteque/common/model/Currency.java create mode 100644 src/main/java/org/pasteque/common/model/Customer.java create mode 100644 src/main/java/org/pasteque/common/model/Image.java create mode 100644 src/main/java/org/pasteque/common/model/User.java create mode 100644 src/main/java/org/pasteque/common/model/option/AccountingConfigOption.java create mode 100644 src/main/java/org/pasteque/common/model/option/CustomContactFieldsOption.java create mode 100644 src/main/java/org/pasteque/common/model/option/package-info.java create mode 100644 src/main/java/org/pasteque/common/model/order/ComputedPrices.java create mode 100644 src/main/java/org/pasteque/common/model/order/Order.java create mode 100644 src/main/java/org/pasteque/common/model/order/OrderPrice.java create mode 100644 src/main/java/org/pasteque/common/model/order/PriceLineRound.java create mode 100644 src/main/java/org/pasteque/common/model/order/PriceNoRound.java create mode 100644 src/main/java/org/pasteque/common/model/order/PriceRound.java create mode 100644 src/main/java/org/pasteque/common/model/order/PriceTotalRound.java create mode 100644 src/main/java/org/pasteque/common/model/order/ProductLine.java create mode 100644 src/main/java/org/pasteque/common/model/order/package-info.java create mode 100644 src/main/java/org/pasteque/common/model/package-info.java create mode 100644 src/main/java/org/pasteque/common/package-info.java create mode 100644 src/main/java/org/pasteque/common/util/HashCypher.java create mode 100644 src/main/java/org/pasteque/common/util/Hexadecimal.java create mode 100644 src/main/java/org/pasteque/common/util/package-info.java create mode 100644 src/main/java/org/pasteque/common/view/Buttonable.java create mode 100644 src/main/java/org/pasteque/common/view/CoordStretcher.java create mode 100644 src/main/java/org/pasteque/common/view/Formatter.java create mode 100644 src/main/java/org/pasteque/common/view/package-info.java create mode 100644 src/main/java/org/pasteque/coreutil/ImmutableList.java create mode 100644 src/main/java/org/pasteque/coreutil/ParseException.java create mode 100644 src/main/java/org/pasteque/coreutil/constants/DiscountTarget.java create mode 100644 src/main/java/org/pasteque/coreutil/constants/DiscountType.java create mode 100644 src/main/java/org/pasteque/coreutil/constants/FiscalTicketType.java create mode 100644 src/main/java/org/pasteque/coreutil/constants/PaymentModeType.java create mode 100644 src/main/java/org/pasteque/coreutil/constants/SessionCloseType.java create mode 100644 src/main/java/org/pasteque/coreutil/constants/TaxType.java create mode 100644 src/main/java/org/pasteque/coreutil/constants/package-info.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/CashRegisterDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/CashSessionDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/DTOInterface.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/FiscalTicketDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/MovementDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/PaymentDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/TaxAmountDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/TaxDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/TicketDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/WeakAssociationDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/ZTicketDTO.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/dto/package-info.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/format/BinaryDTOFormat.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/format/DateDTOFormat.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/format/package-info.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityException.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityExceptions.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityFieldConstraint.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityRecordConstraint.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/integrity/InvalidFieldException.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/integrity/InvalidRecordException.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/integrity/package-info.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/parser/DTOFactory.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/parser/JSONReader.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/parser/Reader.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/parser/Writer.java create mode 100644 src/main/java/org/pasteque/coreutil/datatransfer/parser/package-info.java create mode 100644 src/main/java/org/pasteque/coreutil/package-info.java create mode 100644 src/main/java/org/pasteque/coreutil/price/Discount.java create mode 100644 src/main/java/org/pasteque/coreutil/price/FinalTaxAmount.java create mode 100644 src/main/java/org/pasteque/coreutil/price/Price.java create mode 100644 src/main/java/org/pasteque/coreutil/price/Price2.java create mode 100644 src/main/java/org/pasteque/coreutil/price/Price5.java create mode 100644 src/main/java/org/pasteque/coreutil/price/Quantity.java create mode 100644 src/main/java/org/pasteque/coreutil/price/Rate.java create mode 100644 src/main/java/org/pasteque/coreutil/price/Tax.java create mode 100644 src/main/java/org/pasteque/coreutil/price/TaxAmount.java create mode 100644 src/main/java/org/pasteque/coreutil/price/package-info.java create mode 100644 src/main/java/org/pasteque/coreutil/transition/LineTransition.java create mode 100644 src/main/java/org/pasteque/coreutil/transition/OrderTransition.java create mode 100644 src/main/java/org/pasteque/coreutil/transition/package-info.java create mode 100644 src/main/java/org/pasteque/major/domain/FiscalTicket.java create mode 100644 src/main/java/org/pasteque/major/domain/MajorCashRegister.java create mode 100644 src/main/java/org/pasteque/major/domain/MajorCashSession.java create mode 100644 src/main/java/org/pasteque/major/domain/MajorLine.java create mode 100644 src/main/java/org/pasteque/major/domain/MajorOrder.java create mode 100644 src/main/java/org/pasteque/major/domain/MajorTicket.java create mode 100644 src/main/java/org/pasteque/major/domain/MajorZTicket.java create mode 100644 src/main/java/org/pasteque/major/domain/Movement.java create mode 100644 src/main/java/org/pasteque/major/domain/Payment.java create mode 100644 src/main/java/org/pasteque/major/domain/package-info.java create mode 100644 src/main/java/org/pasteque/major/package-info.java create mode 100644 src/main/java/org/pasteque/package-info.java create mode 100644 src/main/javadoc/javadoc-dark.css create mode 100644 src/main/resources/broken.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a596306 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +ant-lib diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc84602 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +Pasteque core +============= + +Common libraries for Pasteque. + +Setup +----- + +This projects can use either Maven or Ant and Ivy. + +### Setting up Maven + +This build uses `pom.xml`. + +Install maven with (Debian-based) + +``` +apt install maven +``` + +The initialization is done automatically when building the project. + +### Setting up Ant and Ivy + +This build uses `build.xml` and `ivy.xml`. + +Install And and Ivy (Debian-based) + +``` +apt install ant ivy +``` + +Debian doesn't link Ivy to Ant by default. + +``` +ln -s /usr/share/java/ivy.jar /usr/share/ant/lib/ivy.jar +``` + +Install the required dependencies with Ivy. + +``` +ant resolve +``` + +This will download the dependencies from Maven repositories and put them in `ant-lib`. + +Build commands +-------------- + +### Compile + +``` +mvn compile +or +ant compile +``` + +It will create compiled classes in `target/classes` + +### Generate the documentation + +``` +mvn javadoc:javadoc +or +ant javadoc +``` + +This will generate the javadoc and put it it `target/site/apidocs`. + +### Clean the project + +``` +mvn clean +or +ant clean +ant lib-clean +``` + +It will delete the `target` directory. Te reset ant dependencies, use `ant lib-clean` to remove `ant-lib`. diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..495fb57 --- /dev/null +++ b/build.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ivy.xml b/ivy.xml new file mode 100644 index 0000000..2b37a65 --- /dev/null +++ b/ivy.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..addcb90 --- /dev/null +++ b/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + + org.pasteque + org.pasteque.common + 8.0-SNAPSHOT + jar + + Pasteque Common + Common definition, classes and utilities to share data and behaviour between Pasteque applications. + + + + UTF-8 + 17 + 17 + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.8.0 + + + javadoc-dark.css + + + + + + + + + org.json + json + 20240303 + jar + compile + + + diff --git a/src/main/java/org/pasteque/common/constants/CustomerContactField.java b/src/main/java/org/pasteque/common/constants/CustomerContactField.java new file mode 100644 index 0000000..5a56248 --- /dev/null +++ b/src/main/java/org/pasteque/common/constants/CustomerContactField.java @@ -0,0 +1,68 @@ +package org.pasteque.common.constants; + +/** + * Customer's contact field code to be able to customize them with a + * {@link org.pasteque.common.model.option.CustomContactFieldsOption}. + */ +public enum CustomerContactField +{ + /** Code for first name label. Code: {@code Label.Customer.FirstName}. */ + FIRST_NAME("Label.Customer.FirstName"), + /** Code for last name label. Code: {@code Label.Customer.LastName}. */ + LAST_NAME("Label.Customer.LastName"), + /** Code for email label. Code: {@code Label.Customer.Email}. */ + EMAIL("Label.Customer.Email"), + /** Code for phone1 label. Code: {@code Label.Customer.Phone}. */ + PHONE1("Label.Customer.Phone"), + /** Code for phone2 label. Code: {@code Label.Customer.Phone2}. */ + PHONE2("Label.Customer.Phone2"), + /** Code for fax label. Code: {@code Label.Customer.Fax}. */ + FAX("Label.Customer.Fax"), + /** Code for address label. Code: {@code Label.Customer.Addr}. */ + ADDR1("Label.Customer.Addr"), + /** Code for address2 label. Code: {@code Label.Customer.Addr2}. */ + ADDR2("Label.Customer.Addr2"), + /** Code for zip code label. Code: {@code Label.Customer.ZipCode}. */ + ZIP_CODE("Label.Customer.ZipCode"), + /** Code for city label. Code: {@code Label.Customer.City}. */ + CITY("Label.Customer.City"), + /** Code for region label. Code: {@code Label.Customer.Region}. */ + REGION("Label.Customer.Region"), + /** Code for country label. Code: {@code Label.Customer.Country}. */ + COUNTRY("Label.Customer.Country"); + + /** {@see getCode()} */ + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static CustomerContactField fromCode(String code) throws IllegalArgumentException { + for (CustomerContactField v : CustomerContactField.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(code); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + CustomerContactField(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public String getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/common/constants/DefaultImageImplementation.java b/src/main/java/org/pasteque/common/constants/DefaultImageImplementation.java new file mode 100644 index 0000000..0afd2cf --- /dev/null +++ b/src/main/java/org/pasteque/common/constants/DefaultImageImplementation.java @@ -0,0 +1,21 @@ +package org.pasteque.common.constants; + +import java.io.IOException; + +/** + *

Interface to get default images. Client must implement one and + * Instantiate one to use in + * {@link org.pasteque.common.constants.DefaultImages}.

+ */ +public class DefaultImageImplementation +{ + /** + * Get the default image for the given models. + * @param model The type of model. + * @return The image as raw bytes. + * @throws IOException When an error occurs while reading the default image. + */ + public byte[] getDefaultImage(ModelName model) throws IOException { + return this.getClass().getClassLoader().getResourceAsStream("broken.png").readAllBytes(); + } +} diff --git a/src/main/java/org/pasteque/common/constants/DefaultImages.java b/src/main/java/org/pasteque/common/constants/DefaultImages.java new file mode 100644 index 0000000..16caa85 --- /dev/null +++ b/src/main/java/org/pasteque/common/constants/DefaultImages.java @@ -0,0 +1,33 @@ +package org.pasteque.common.constants; + +import java.io.IOException; + +/** + *

Interface to get default images from a singleton.

+ *

Clients must define an implementation by overriding + * {@link org.pasteque.common.constants.DefaultImageImplementation} and + * setting a singleton in {@link singleton}.

+ * {@see org.pasteque.common.constants.DefaultImagesImplementation} + */ +public class DefaultImages +{ + /** + * The singleton to use to load default images. + */ + public static DefaultImageImplementation singleton = null; + + /** + * Get the default image for the given models from the singleton. + * @param model The type of model. + * @return The image as raw bytes. + * @throws IOException When an error occurs while reading an image. + */ + public static byte[] getDefaultImage(ModelName model) throws IOException { + if (singleton == null) { + // Use a dummy implementation with empty images + // not to crash + singleton = new DefaultImageImplementation(); + } + return singleton.getDefaultImage(model); + } +} diff --git a/src/main/java/org/pasteque/common/constants/ModelName.java b/src/main/java/org/pasteque/common/constants/ModelName.java new file mode 100644 index 0000000..b458508 --- /dev/null +++ b/src/main/java/org/pasteque/common/constants/ModelName.java @@ -0,0 +1,81 @@ +package org.pasteque.common.constants; + +/** + *

Model names for loosely coupled records, like images that can be associated + * to multiple types of records.

+ *

They are mostly used for database storage and access.

+ */ +public enum ModelName +{ + /** + * Name for {@link org.pasteque.common.datatransfer.dto.CategoryDTO}. + * Code: {@code category}. + */ + CATEGORY("category"), + /** + * Name for {@link org.pasteque.common.datatransfer.dto.CustomerDTO}. + * Code: {@code customer}. + */ + CUSTOMER("customer"), + /** + * Name for {@link org.pasteque.common.datatransfer.dto.ProductDTO} + * and {@link org.pasteque.common.datatransfer.dto.CompositionDTO}. + * Code: {@code product}. + */ + PRODUCT("product"), + /** + * Name for {@link org.pasteque.common.datatransfer.dto.UserDTO}. + * Code: {@code user}. + */ + USER("user"), + /** + * Name for {@link org.pasteque.common.datatransfer.dto.PaymentModeDTO}. + * Code: {@code paymentmode}. + */ + PAYMENT_MODE("paymentmode"), + /** + * Name for {@link org.pasteque.common.datatransfer.dto.PaymentModeDTO.PaymentModeValueDTO}. + * Code: {@code paymentmodevalue}. + */ + PAYMENT_MODE_VALUE("paymentmodevalue"), + /** + * Name for {@link org.pasteque.common.datatransfer.dto.ImageDTO}. + * Code: {@code image}. + */ + IMAGE("image"); + + /** {@see getCode()} */ + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static ModelName fromCode(String code) throws IllegalArgumentException { + for (ModelName v : ModelName.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(code); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + ModelName(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public String getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/common/constants/OptionName.java b/src/main/java/org/pasteque/common/constants/OptionName.java new file mode 100644 index 0000000..6ed6263 --- /dev/null +++ b/src/main/java/org/pasteque/common/constants/OptionName.java @@ -0,0 +1,69 @@ +package org.pasteque.common.constants; + +/** + * Enumeration of known option names. + */ +public enum OptionName +{ + /** + * Option holding accounting preferences and configuration. + * Code {@code accounting.config}. + */ + ACCOUNTING_CONFIG("accounting.config"), + /** + * Option holding the URL of the preferred back office. + * Code {@code backoffice.url}. + */ + BACKOFFICE_URL("backoffice.url"), + /** + * Option holding custom contact fields for customers. + * Code {@code customer.customFields}. + */ + CUSTOMER_CONTACT_FIELDS("customer.customFields"), + /** + * Option holding accounting preferences and configuration. + * Code {@code jsadmin.[local|/].accountingConfig}. + * @deprecated The option was tied to JSadmin and could be + * configured differently for each URL. Use + * {@link ACCOUNTING_CONFIG} for a single and client-independent + * option instead. + */ + @Deprecated + LEGACY_ACCOUNTING_CONFIG("jsadmin.*.accountingConfig"); + + /** {@see getCode()} */ + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values. This may not be an error + * when using custom options. + */ + public static OptionName fromCode(String code) throws IllegalArgumentException { + for (OptionName v : OptionName.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(code); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + OptionName(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public String getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/common/constants/ResourceName.java b/src/main/java/org/pasteque/common/constants/ResourceName.java new file mode 100644 index 0000000..60b27d0 --- /dev/null +++ b/src/main/java/org/pasteque/common/constants/ResourceName.java @@ -0,0 +1,148 @@ +package org.pasteque.common.constants; + +/** + *

List of known resource names. This list is not exhaustive and should not + * be used to check if a resource name is correct.

+ *

Some resources may not be in use anymore.

+ *

XML documents are sent to the printer on the desktop client. + * These documents control both the printer and the customer's display.

+ */ +public enum ResourceName +{ + /** + * Name for the logo to print on the tickets for the desktop client. + * Code: {@code Printer.Ticket.Logo}. + */ + PRINTER_TICKET_LOGO("Printer.Ticket.Logo"), + /** + * Name for the custom text to print between the logo and the content + * of the ticket for the desktop client. + * Code: {@code Printer.Ticket.Header}. + */ + PRINTER_TICKET_HEADER("Printer.Ticket.Header"), + /** + * Name for the custom text to print at the bottom of the ticket. + * Code: {@code Printer.Ticket.Footer}. + */ + PRINTER_TICKET_FOOTER("Printer.Ticket.Footer"), + /** + * Name for the logo to print on the tickets for the android client + * for the desktop client. + * Code: {@code MobilePrinter.Ticket.Logo}. + */ + MOBILEPRINTER_TICKET_LOGO("MobilePrinter.Ticket.Logo"), + /** + * Name for the custom text to print between the logo and the content + * of the ticket for the android client. + * Code: {@code MobilePrinter.Ticket.Header}. + */ + MOBILEPRINTER_TICKET_HEADER("MobilePrinter.Ticket.Header"), + /** + * Name for the custom text to print at the bottom of the ticket + * for the android client. + * Code: {@code MobilePrinter.Ticket.Footer}. + */ + MOBILEPRINTER_TICKET_FOOTER("MobilePrinter.Ticket.Footer"), + /** + * Name for the xml definition of coins and bills to show when counting + * cash when opening or closing the session. + * Code: {@code payment.cash}. + */ + CASH_VALUES("payment.cash"), + /** + * Name for the xml document to print to solely open the drawer + * from the printer. It is not used to open the drawer when printing + * a ticket. + * Code: {@code Printer.OpenDrawer}. + */ + PRINTER_OPEN_DRAWER("Printer.OpenDrawer"), + /** + * Name for the xml document to print when opening the cash session. + * @deprecated Not in use since Pasteque Desktop 8.10. The document + * is since embedded in code. + * Code: {@code Printer.OpenCash}. + */ + @Deprecated + PRINTER_OPEN_CASH("Printer.OpenCash"), + /** + * Name for the xml document to print when previewing the Z ticket. + * @deprecated Not in use since Pasteque Desktop 8.10. The document + * is since embedded in code. + * Code: {@code Printer.PartialCash}. + */ + @Deprecated + PRINTER_PARTIAL_CASH("Printer.PartialCash"), + /** + * Name for the xml document to print when opening the cash session. + * @deprecated Not in use since Pasteque Desktop 8.10. The document + * is since embedded in code. + * Code: {@code Printer.CloseCash}. + */ + @Deprecated + PRINTER_CLOSE_CASH("Printer.CloseCash"), + /** + * Name for the xml document to print as a welcoming message + * when opening a new order for the desktop client. + * Code: {@code Printer.Start}. + */ + PRINTER_START("Printer.Start"), + /** + * Name for the xml document that defines how order lines are shown + * for the desktop client. + * Code: {@code MajorTicket.Line}. + */ + TICKET_LINE("MajorTicket.Line"), + /** + * Name for the xml document that defines the title of the window + * for the desktop client. + * @deprecated Not in use since Pasteque Desktop 8.9. The title + * is since embedded in code. + * Code: {@code Window.Title}. + */ + @Deprecated + WINDOW_TITLE("Window.Title"), + /** + * Name for the binary image to use as the application logo to + * show along the title in the in-app header. + * @deprecated Removed in Pasteque Desktop 8.9, not in use even + * before. + * Code: {@code Window.Logo}. + */ + @Deprecated + WINDOW_LOGO("Window.Logo"); + + /** {@see getCode()} */ + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static ResourceName fromCode(String code) throws IllegalArgumentException { + for (ResourceName v : ResourceName.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(code); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + ResourceName(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public String getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/common/constants/ResourceType.java b/src/main/java/org/pasteque/common/constants/ResourceType.java new file mode 100644 index 0000000..fa0bde4 --- /dev/null +++ b/src/main/java/org/pasteque/common/constants/ResourceType.java @@ -0,0 +1,58 @@ +package org.pasteque.common.constants; + +/** + * List of known resource types. + */ +public enum ResourceType +{ + /** + * The resource content is plain text. + * Code: {@code 0}. + */ + PLAIN_TEXT(0), + /** + * The resource content is a binary image stored in base64. + * Code: {@code 1}. + */ + IMAGE(1), + /** + * The resource content is raw binary stored in base64. + * Code: {@code 2}. + */ + BINARY(2); + + /** {@see getCode()} */ + private final int code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static ResourceType fromCode(int code) throws IllegalArgumentException { + for (ResourceType v : ResourceType.values()) { + if (v.getCode() == code) { + return v; + } + } + throw new IllegalArgumentException(String.valueOf(code)); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + ResourceType(int code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public int getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/common/constants/ScaleType.java b/src/main/java/org/pasteque/common/constants/ScaleType.java new file mode 100644 index 0000000..d68f782 --- /dev/null +++ b/src/main/java/org/pasteque/common/constants/ScaleType.java @@ -0,0 +1,64 @@ +package org.pasteque.common.constants; + +/** + * Scale types enumeration and units. + */ +public enum ScaleType +{ + /** + * Sold in piece. + * Code: {@code 0}. + */ + ATOMIC(0), + /** + * Sold by kilogrammes. + * Code: {@code 1}. + */ + WEIGHT(1), + /** + * Sold by liters. + * Code: {@code 2}. + */ + VOLUME(2), + /** + * Sold by hours. + * Code: {@code 3}. + */ + TIME(3); + + /** {@see getCode()} */ + private final int code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static ScaleType fromCode(int code) throws IllegalArgumentException { + switch(code) { + case 0: return ATOMIC; + case 1: return WEIGHT; + case 2: return VOLUME; + case 3: return TIME; + } + throw new IllegalArgumentException(Integer.valueOf(code).toString()); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + ScaleType(int code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public int getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/common/constants/package-info.java b/src/main/java/org/pasteque/common/constants/package-info.java new file mode 100644 index 0000000..15732a8 --- /dev/null +++ b/src/main/java/org/pasteque/common/constants/package-info.java @@ -0,0 +1,4 @@ +/** + * Common constants and enumerations. + */ +package org.pasteque.common.constants; diff --git a/src/main/java/org/pasteque/common/datasource/AssociationInconsistencyException.java b/src/main/java/org/pasteque/common/datasource/AssociationInconsistencyException.java new file mode 100644 index 0000000..2c63f03 --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/AssociationInconsistencyException.java @@ -0,0 +1,121 @@ +package org.pasteque.common.datasource; + +/** + * Building a model from DTO that weren't linked together. + */ +public class AssociationInconsistencyException extends Exception +{ + private static final long serialVersionUID = -979786104345156783L; + + /** {@see getFromField()} */ + private final String fromField; + /** {@see getFromValue()} */ + private final String fromValue; + /** {@see getToField()} */ + private final String toField; + /** {@see getToValue()} */ + private final String toValue; + + /** + *

Automatically check inconsistencies and throw the exception.

+ *

It does nothing if everything is fine.

+ *

Use null checks to pass fromValues and toValues:

+ * (from == null) ? null : from.getFromValue() + * @param fromField The name of the field from the main model that should + * link to the target. + * @param fromValue The value of the field to check. Use null if the main + * model is null. + * @param toField The name of the field in the target model that should + * hold the same value. + * @param toValue The value of the field to check against. Use null if the + * target model is null. + * @throws AssociationInconsistencyException When fromValue and toValue + * don't match. + */ + public static void autoThrow(String fromField, Object fromValue, + String toField, Object toValue) + throws AssociationInconsistencyException { + // Null checks + if (fromValue == null && toValue == null) { + return; + } + if (fromValue != null) { + if (toValue == null) { + throw new AssociationInconsistencyException( + fromField, fromValue.toString(), + toField, "null" + ); + } + } else { // fromValue is null + if (toValue != null) { + throw new AssociationInconsistencyException( + fromField, "null", + toField, toValue.toString() + ); + } + } + // Neither are null, equality check + if (!fromValue.equals(toValue)) { + throw new AssociationInconsistencyException( + fromField, fromValue.toString(), + toField, toValue.toString() + ); + } + } + + /** + * Create an exception from all fields. + * @param fromField See {@link getFromField}. + * @param fromValue See {@link getFromValue}. + * @param toField See {@link getFromValue}. + * @param toValue See {@link getToValue}. + */ + public AssociationInconsistencyException( + String fromField, + String fromValue, + String toField, + String toValue) { + super(String.format("Association mismatch: from %s=%s to %s=%s", + fromField, + (fromValue == null) ? "null" : fromValue, + toField, + (toValue == null) ? "null" : toValue)); + this.fromField = fromField; + this.fromValue = fromValue; + this.toField = toField; + this.toValue = toValue; + } + + /** + * Get the name of the field that holds the association. + * @return The name of the field that holds the association. + */ + public String getFromField() { + return fromField; + } + + /** + * Get the value of the associated record. + * @return The value of the associated record. + */ + public String getFromValue() { + return fromValue; + } + + /** + * Get the name of the field of the linked record. + * @return The name of the field of the linked record. + */ + public String getToField() { + return toField; + } + + /** + * Get the value of the field of the linked record that didn't match + * the expected value. + * @return The value of the field of the linked record. + */ + public String getToValue() { + return toValue; + } +} diff --git a/src/main/java/org/pasteque/common/datasource/CategoryDataSource.java b/src/main/java/org/pasteque/common/datasource/CategoryDataSource.java new file mode 100644 index 0000000..d2021f6 --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/CategoryDataSource.java @@ -0,0 +1,61 @@ +package org.pasteque.common.datasource; + +import java.io.IOException; +import java.util.List; +import org.pasteque.common.datatransfer.dto.CategoryDTO; +import org.pasteque.common.model.Category; + +/** + *

Store and load categories.

+ */ +public interface CategoryDataSource +{ + /** + * Store a category. It will be updated if it already exists. + * @param category The category to store. + * @throws IOException When an error occurs while storing the category. + */ + public void storeCategory(CategoryDTO category) throws IOException; + + /** + * Store multiple categories at once. It may be faster than storing them + * one by one. + * @param categories The categories to store + * @param clear Whether to replace the content of the source or update it. + * @throws IOException When an error occurs while storing the categories. + * The data may already be wiped and some categories may have been stored. + */ + public void storeCategories(Iterable categories, boolean clear) throws IOException; + + /** + * Get the list of categories that don't have a parent. + * @return The list of categories at the top of the category tree. + * @throws IOException When an error occurs while reading the source. + */ + public List getTopCategories() throws IOException; + + /** + * Get the list of categories that are children of the given one. + * @param parent The category to get children from. + * @return The list of categories that have the given parent. + * @throws IOException When an error occurs while reading the source. + */ + public List getSubCategories(Category parent) throws IOException; + + /** + * Delete a category from the source. It may leave the source in + * an inconsistent state if a parent category is dropped + * without its children. + * @param reference The reference of the category. + * @return True if the category was found and deleted. False if not found. + * @throws IOException When an error occurs while deleting the currency. + */ + public boolean dropCategory(String reference) throws IOException; + + /** + * Clear the data source of all its content. + * @throws IOException When an error occurs while deleting its content. + * Some data may have been deleted. + */ + public void clearCategories() throws IOException; +} diff --git a/src/main/java/org/pasteque/common/datasource/CurrencyDataSource.java b/src/main/java/org/pasteque/common/datasource/CurrencyDataSource.java new file mode 100644 index 0000000..83495a4 --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/CurrencyDataSource.java @@ -0,0 +1,72 @@ +package org.pasteque.common.datasource; + +import java.io.IOException; +import java.util.List; +import org.pasteque.common.datatransfer.dto.CurrencyDTO; +import org.pasteque.common.model.Currency; +import org.pasteque.coreutil.datatransfer.integrity.InvalidRecordException; + +/** + *

Store and load currencies.

+ *

The behavior of the source is not defined when multiple main currencies + * are stored, but it must always bring the same result from the same source.

+ */ +public interface CurrencyDataSource +{ + /** + * Store a currency. It will be updated if it already exists. + * @param currency The currency to store. + * @throws IOException When an error occurs while storing the currency. + */ + public void storeCurrency(CurrencyDTO currency) throws IOException; + + /** + * Store multiple currencies at once. It may be faster than storing them one + * by one. + * @param currencies The currencies to store + * @param clear Whether to replace the content of the source or update it. + * @throws IOException When an error occurs while storing the currencies. + * The data may already be wiped and some currencies may have been stored. + */ + public void storeCurrencies(Iterable currencies, boolean clear) throws IOException; + + /** + * Find a currency by its reference. + * @param reference The reference of the currency. + * @return The currency, null if not found. + * @throws IOException When an error occurs while reading the source. + */ + public Currency getCurrency(String reference) throws IOException; + + /** + * Get all currencies. + * @return All currencies. + * @throws IOException When an error occurs while reading the source. + */ + public List getCurrencies() throws IOException; + + /** + * Get the main currency. + * @return The currency. It is never null. + * @throws IOException When an error occurs while reading the source. + * @throws InvalidRecordException When no main currency is set. + */ + public Currency getMainCurrency() throws IOException, InvalidRecordException; + + /** + * Delete a currency from the source. It may leave the source in + * an inconsistent state if the main currency is dropped + * without replacement. + * @param reference The reference of the currency. + * @return True if the currency was found and deleted. False if not found. + * @throws IOException When an error occurs while deleting the currency. + */ + public boolean dropCurrency(String reference) throws IOException; + + /** + * Clear the data source of all its content. + * @throws IOException When an error occurs while deleting its content. + * Some data may have been deleted. + */ + public void clearCurrencies() throws IOException; +} diff --git a/src/main/java/org/pasteque/common/datasource/CustomerDataSource.java b/src/main/java/org/pasteque/common/datasource/CustomerDataSource.java new file mode 100644 index 0000000..19d96ed --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/CustomerDataSource.java @@ -0,0 +1,94 @@ +package org.pasteque.common.datasource; + +import java.io.IOException; +import java.util.List; +import org.pasteque.common.datatransfer.dto.CustomerDTO; +import org.pasteque.common.model.Customer; + +/** + *

Store, load and search customers.

+ */ +public interface CustomerDataSource +{ + /** + * Store a customer's account. It will be updated if it already exists. + * @param customer The account to store. + * @throws IOException When an error occurs while storing the account. + */ + public void storeCustomer(CustomerDTO customer) throws IOException; + + /** + * Store multiple accounts at once. It may be faster than storing them one + * by one. + * @param customers The accounts to store + * @param clear Whether to replace the content of the source or update it. + * @throws IOException When an error occurs while storing the accounts. + * The data may already be wiped and some accounts may have been stored. + */ + public void storeCustomers(Iterable customers, boolean clear) throws IOException; + + /** + * Find a customer by their id. + * @param id The id. + * @return The customer, null if not found. + * @throws IOException When an error occurs while reading the source. + */ + public Customer getCustomerById(String id) throws IOException; + + /** + * Find a customer by their card. + * @param card The customer's card. + * @return The customer, null if not found. + * @throws IOException When an error occurs while reading the source. + */ + public Customer getCustomerByCard(String card) throws IOException; + + /** + *

Find customers matching a request.

+ *

The search should be case-insensitive and based upon multiple fields:

+ *
    + *
  • Their display name
  • + *
  • Their card
  • + *
  • Contact information may be used, when using extended search.
  • + *
+ * @param search The search string. + * @param withinContactInfo Whether to use only the basic search keys or + * also search within contact informations. This may be much slower. + * @return A list of customers matching the request. + * @throws IOException When an error occurs while reading the source. + */ + public List findCustomers(String search, boolean withinContactInfo) throws IOException; + + /** + * Store the list of frequent customers. + * @param customers The accounts to store + * @param clear Whether to replace the content of the source or update it. + * @throws IOException When an error occurs while storing the accounts. + * The data may already be wiped and some accounts may have been stored. + */ + public void storeTop10Customers(Iterable customers, boolean clear) throws IOException; + + /** + *

Get the short list of most frequent customers.

+ *

This special list is built from the API and should not be recomputed + * locally.

+ * @return The list of most frequent customers. + * @throws IOException When an error occurs while reading the source. + */ + public List getTop10Customers() throws IOException; + + /** + * Delete a customer's account from the source. + * @param id The id of the account. + * @return True if the account was found and deleted. False if not found. + * @throws IOException When an error occurs while deleting the account. + */ + public boolean dropCustomer(String id) throws IOException; + + /** + * Clear the data source of all its content. + * @throws IOException When an error occurs while deleting its content. + * Some data may have been deleted. + */ + public void clearCustomers() throws IOException; +} diff --git a/src/main/java/org/pasteque/common/datasource/ImageDataSource.java b/src/main/java/org/pasteque/common/datasource/ImageDataSource.java new file mode 100644 index 0000000..a0e93da --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/ImageDataSource.java @@ -0,0 +1,47 @@ +package org.pasteque.common.datasource; + +import java.io.IOException; +import org.pasteque.common.constants.ModelName; +import org.pasteque.common.datatransfer.dto.ImageDTO; + +/** + * Store and load images for other models. + */ +public interface ImageDataSource +{ + /** + * Store an image. It will be updated if it already exists. + * @param image The image to store. + * @throws IOException When an error occurs while storing the image. + */ + public void storeImage(ImageDTO image) throws IOException; + + /** + * Load an image. + * @param modelName The type of model the image is associated to. + * @param reference The reference to load. + * It may not be the actual reference, see + * {@link org.pasteque.common.datatransfer.dto.ImageDTO#getReference()}. + * @return The associated image, null if not found. + * @throws IOException When an error occurs while reading the image. + */ + public ImageDTO getImage(ModelName modelName, String reference) throws IOException; + + /** + * Delete an image from the source. + * @param modelName The type of model the image is associated to. + * @param reference The reference to delete. + * It may not be the actual reference, see + * {@link org.pasteque.common.datatransfer.dto.ImageDTO#getReference()}. + * @return True if the image was found and deleted. False if not found. + * @throws IOException When an error occurs while deleting the image. + */ + public boolean dropImage(ModelName modelName, String reference) throws IOException; + + /** + * Clear the data source of all its content. + * @throws IOException When an error occurs while deleting its content. + * Some data may have been deleted. + */ + public void clearImages() throws IOException; +} diff --git a/src/main/java/org/pasteque/common/datasource/PaymentModeDataSource.java b/src/main/java/org/pasteque/common/datasource/PaymentModeDataSource.java new file mode 100644 index 0000000..42ff1fe --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/PaymentModeDataSource.java @@ -0,0 +1,58 @@ +package org.pasteque.common.datasource; + +import java.io.IOException; +import java.util.List; +import org.pasteque.common.datatransfer.dto.PaymentModeDTO; + +/** + * Store and load payment modes. + */ +public interface PaymentModeDataSource +{ + /** + * Store a payment mode. It will be updated if it already exists. + * @param paymentMode The payment mode to store. + * @throws IOException When an error occurs while storing the payment mode. + */ + public void storePaymentMode(PaymentModeDTO paymentMode) throws IOException; + + /** + * Store multiple payment modes at once. It may be faster than storing them one + * by one. + * @param paymentModes The payment modes to store + * @param clear Whether to replace the content of the source or update it. + * @throws IOException When an error occurs while storing the payment modes. + * The data may already be wiped and some payment modes may have been stored. + */ + public void storePaymentModes(Iterable paymentModes, boolean clear) throws IOException; + + /** + * Find a payment mode by its reference. + * @param reference The reference of the payment mode. + * @return The payment mode, null if not found. + * @throws IOException When an error occurs while reading the source. + */ + public PaymentModeDTO getPaymentMode(String reference) throws IOException; + + /** + * Get all payment modes. + * @return All payment modes. + * @throws IOException When an error occurs while reading the source. + */ + public List getPaymentModes() throws IOException; + + /** + * Delete a payment mode from the source. + * @param reference The reference of the payment mode. + * @return True if the currency was found and deleted. False if not found. + * @throws IOException When an error occurs while deleting the currency. + */ + public boolean dropPaymentMode(String reference) throws IOException; + + /** + * Clear the data source of all its content. + * @throws IOException When an error occurs while deleting its content. + * Some data may have been deleted. + */ + public void clearPaymentModes() throws IOException; +} diff --git a/src/main/java/org/pasteque/common/datasource/api/APIHost.java b/src/main/java/org/pasteque/common/datasource/api/APIHost.java new file mode 100644 index 0000000..52b76c0 --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/api/APIHost.java @@ -0,0 +1,87 @@ +package org.pasteque.common.datasource.api; + +/** + * API host URL. + */ +public class APIHost +{ + /** {@see getHostUrl()} */ + private final String host; + /** {@see getUser()} */ + private final String user; + /** {@see getHostUrl()} */ + private final boolean https; + /** {@see getHardware()} */ + private final String hardware; + + /** + * Create a host without a specific cash register. + * @param host The main URL. + * @param user The API user. + * @param useHttps Whether to use http or https. Ignored if the host + * already contains the protocol. + */ + public APIHost(String host, String user, boolean useHttps) { + if (host.startsWith("http://")) { + host = host.substring(7); + useHttps = false; + } else if (host.startsWith("https://")) { + host = host.substring(8); + useHttps = true; + } + this.host = host; + this.user = user; + this.https = useHttps; + this.hardware = null; + } + + /** + * Create a host for a specific cash register. + * @param host The main URL. + * @param user The API user. + * @param hardware The machine name. + * @param useHttps Whether to use http or https. Ignored if the host + * already contains the protocol. + */ + public APIHost(String host, String user, String hardware, boolean useHttps) { + if (host.startsWith("http://")) { + host = host.substring(7); + useHttps = false; + } else if (host.startsWith("https://")) { + host = host.substring(8); + useHttps = true; + } + this.host = host; + this.user = user; + this.hardware = hardware; + this.https = useHttps; + } + + /** + * Get the URL of the host. + * @return The full URL. + */ + public String getHostUrl() { + if (this.https) { + return String.format("https://%s", this.host); + } else { + return String.format("http://%s", this.host); + } + } + + /** + * Get the login for this host. + * @return The user name. + */ + public String getUser() { + return this.user; + } + + /** + * Get the hardware identifier for this host. + * @return The hardware identifier. Can be null. + */ + public String getHardware() { + return this.hardware; + } +} diff --git a/src/main/java/org/pasteque/common/datasource/api/APIResponse.java b/src/main/java/org/pasteque/common/datasource/api/APIResponse.java new file mode 100644 index 0000000..10d9c0b --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/api/APIResponse.java @@ -0,0 +1,49 @@ +package org.pasteque.common.datasource.api; + +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + * Generic API response. Use subclasses to parse the content. + */ +public class APIResponse +{ + /** {@see getToken()} */ + private final APIToken token; + /** {@see getStatus()} */ + protected final APIResponseStatus status; + /** Internal reader to parse the response. */ + protected final Reader reader; + + /** + * Create a parseable response. + * @param reader The Reader initialized to the response content. + * It must not be shared with any other instance. + * @throws ParseException When an errors occurs while parsing the token + * or status. + */ + public APIResponse(Reader reader) throws ParseException { + this.reader = reader; + this.reader.startObject(); + this.token = new APIToken(reader.readString("token")); + this.status = APIResponseStatus.fromStatus(reader.readString("status")); + this.reader.endObject(); + + } + + /** + * Get the new token associated to this response. + * @return The new token that was sent along the response. + */ + public APIToken getToken() { + return this.token; + } + + /** + * Get the response status. + * @return The response status. + */ + public APIResponseStatus getStatus() { + return this.status; + } +} diff --git a/src/main/java/org/pasteque/common/datasource/api/APIResponseStatus.java b/src/main/java/org/pasteque/common/datasource/api/APIResponseStatus.java new file mode 100644 index 0000000..1dd67f7 --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/api/APIResponseStatus.java @@ -0,0 +1,63 @@ +package org.pasteque.common.datasource.api; + +/** + * Response code from the API. + */ +public enum APIResponseStatus +{ + /** + * The call went well and the response holds the expected content. + * Its code is "ok". + */ + STATUS_OK("ok"), + /** + * The call was rejected due to incorrect input. + * Its code is "rej". + */ + STATUS_REJECTED("rej"), + /** + * An error occured on the side of the API. + * Its code is "err". + */ + STATUS_ERROR("err"); + + /** {@see getStatus()} */ + private String status; + + /** + * Create a ResponseStatus from its code. + * @param status The status to parse. + * @return The according enumeration value. + * @throws IllegalArgumentException If status is null or not an expected status. + */ + public static APIResponseStatus fromStatus(String status) + throws IllegalArgumentException { + if (status == null) { + throw new IllegalArgumentException("null"); + } + if (STATUS_OK.getStatus().equals(status)) { + return STATUS_OK; + } else if (STATUS_REJECTED.getStatus().equals(status)) { + return STATUS_REJECTED; + } else if (STATUS_ERROR.getStatus().equals(status)) { + return STATUS_ERROR; + } + throw new IllegalArgumentException(Integer.valueOf(status).toString()); + } + + /** + * Internal constructor. + * @param status See {@link getStatus()}. + */ + private APIResponseStatus(String status) { + this.status = status; + } + + /** + * Get the associated constant. + * @return The status code. + */ + public String getStatus() { + return this.status; + } +} diff --git a/src/main/java/org/pasteque/common/datasource/api/APIToken.java b/src/main/java/org/pasteque/common/datasource/api/APIToken.java new file mode 100644 index 0000000..3f09f67 --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/api/APIToken.java @@ -0,0 +1,26 @@ +package org.pasteque.common.datasource.api; + +/** + * JWT used to identify authenticated users. + */ +public class APIToken +{ + /** {@see getToken()} */ + private final String token; + + /** + * Create a token from its string. + * @param token The token string. + */ + public APIToken(String token) { + this.token = token; + } + + /** + * Get the raw JWT token. + * @return The raw JWT token. + */ + public String getToken() { + return this.token; + } +} diff --git a/src/main/java/org/pasteque/common/datasource/api/SimpleAPIResponse.java b/src/main/java/org/pasteque/common/datasource/api/SimpleAPIResponse.java new file mode 100644 index 0000000..ce06a38 --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/api/SimpleAPIResponse.java @@ -0,0 +1,63 @@ +package org.pasteque.common.datasource.api; + +import java.util.ArrayList; +import java.util.List; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.dto.DTOInterface; +import org.pasteque.coreutil.datatransfer.parser.DTOFactory; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + * API response that expects a single or an array of DTO. + */ +// TODO WIP +public class SimpleAPIResponse extends APIResponse +{ + private final DTOFactory factory; + + /** + * {@inheritDoc} + * @param reader {@inheritDoc} + * @throws ParseException {@inheritDoc} + */ + public SimpleAPIResponse(Reader reader) throws ParseException { + super(reader); + this.factory = null;//new DTOFactory(this.reader, T.class); + } + + /** + * Get the content as an array of DTO. + * @return The list of DTO read. + * @throws ParseException When the content is not an array + * of the expected DTO class. + */ + public List getArrayContent() throws ParseException { + this.reader.startObject(); // Root + this.reader.startArray("content"); + int size = this.reader.getArraySize(); + List result = new ArrayList(size); + for (int i = 0; i < size; i++) { + this.reader.startObject(i); + result.add(factory.readObject()); + this.reader.endObject(); + } + this.reader.endArray(); // "content" + this.reader.endObject(); // Root + return result; + } + + /** + * Get the content as a single DTO. + * @return The DTO read. + * @throws ParseException When the content is not a single DTO + * of the expected class. + */ + public T getObjectContent() throws ParseException { + this.reader.startObject(); // Root + this.reader.startObject("content"); + T obj = this.factory.readObject(); + this.reader.endObject(); // "content" + this.reader.endObject(); // Root + return obj; + } +} diff --git a/src/main/java/org/pasteque/common/datasource/api/package-info.java b/src/main/java/org/pasteque/common/datasource/api/package-info.java new file mode 100644 index 0000000..521154e --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/api/package-info.java @@ -0,0 +1,4 @@ +/** + * Utilities to read and send data from/to the API. + */ +package org.pasteque.common.datasource.api; diff --git a/src/main/java/org/pasteque/common/datasource/implementation/SerializedFileDataSource.java b/src/main/java/org/pasteque/common/datasource/implementation/SerializedFileDataSource.java new file mode 100644 index 0000000..95e925d --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/implementation/SerializedFileDataSource.java @@ -0,0 +1,230 @@ +package org.pasteque.common.datasource.implementation; + +import java.io.IOException; +import java.util.List; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import org.pasteque.common.constants.ModelName; +import org.pasteque.common.datasource.CurrencyDataSource; +import org.pasteque.common.datasource.ImageDataSource; +import org.pasteque.common.datasource.PaymentModeDataSource; +import org.pasteque.common.datatransfer.dto.CurrencyDTO; +import org.pasteque.common.datatransfer.dto.ImageDTO; +import org.pasteque.common.datatransfer.dto.PaymentModeDTO; +import org.pasteque.common.model.Currency; +import org.pasteque.coreutil.datatransfer.integrity.InvalidRecordException; + +/** + *

Store DTO in serialized form in individual files.

+ *

This source is not efficient and should be used only for a small number + * of records.

+ *

This implementation is system-dependant and files created from a client + * may not be compatible with files created by an other client, even if they + * both share the same source.

+ */ +public class SerializedFileDataSource +implements CurrencyDataSource, ImageDataSource, PaymentModeDataSource +{ + /** + * Replacement to convert names that contains separators (i.e. / or \) + * to not alter the path when converting a reference to a file name. + * Code {@code _SEP_}. + */ + public static final String SEPARATOR_REPLACEMENT = "_SEP_"; + + private File baseDir; + + /** + * Create a source inside the given directory. + * @param baseDir The base directory. It must be writeable. + * @throws IOException When the directory is not writeable. + */ + public SerializedFileDataSource(File baseDir) throws IOException { + this.baseDir = baseDir; + if (!this.baseDir.canWrite()) { + throw new IOException("Base directory must be writeable"); + } + } + + /** + * Get the file for an image. + * @param modelName The model name of the image. + * @param reference The reference of the record. + * @param createDir Whether to create inexistent subdirectories or not. + */ + private File getImageFile(String modelName, String reference, boolean createDir) { + File subDir = new File(this.baseDir, ModelName.IMAGE.getCode()); + if (!subDir.exists() && createDir) { + subDir.mkdirs(); + } + subDir = new File(subDir, modelName); + if (!subDir.exists() && createDir) { + subDir.mkdirs(); + } + return new File(subDir, this.sanitizeReference(reference)); + } + + /** + * Check and transform a file name to not alter the path. + * @param fileName The name of the file to check. + * @return A new file name with characters that alter the path replaced + * by safe ones. + */ + private String sanitizeReference(String fileName) { + String pathSep = System.getProperty("path.separator"); + return fileName.replace(pathSep, SEPARATOR_REPLACEMENT); + } + + /** + * Get the file for a record outside images. + * @param modelName The model name of the record. + * @param reference The reference of the record. + * @param createDir Whether to create inexistent subdirectories or not. + */ + private File getFile(String modelName, String reference, boolean createDir) { + File subDir = new File(this.baseDir, modelName); + if (!subDir.exists() && createDir) { + subDir.mkdirs(); + } + return new File(subDir, this.sanitizeReference(reference)); + } + + /*********************/ + /* Image Data Source */ + /*********************/ + + @Override + public void storeImage(ImageDTO image) throws IOException { + File targetFile = getImageFile(image.getModel(), image.getReference(), true); + FileOutputStream fos = new FileOutputStream(targetFile); + fos.write(image.getImage()); + fos.close(); + } + + @Override + public ImageDTO getImage(ModelName modelName, String reference) throws IOException { + File targetFile = this.getFile(modelName.getCode(), reference, false); + if (!targetFile.exists()) { + return null; + } + if (!targetFile.isFile()) { + throw new IOException(String.format("Target file for image %s %s is not a file", + modelName, reference)); + } + if (!targetFile.canRead()) { + throw new IOException(String.format("Target file for image %s %s is not readable", + modelName, reference)); + } + FileInputStream fis = new FileInputStream(targetFile); + byte[] data = fis.readAllBytes(); + fis.close(); + return new ImageDTO(modelName.getCode(), reference, data); + } + + @Override + public boolean dropImage(ModelName modelName, String reference) throws IOException { + File targetFile = this.getFile(modelName.getCode(), reference, false); + if (targetFile.exists()) { + targetFile.delete(); + return true; + } + return false; + } + + @Override + public void clearImages() throws IOException { + for (ModelName modelName : ModelName.values()) { + File subDir = new File(this.baseDir, modelName.getCode()); + if (subDir.exists()) { + subDir.delete(); + } + } + } + + /***************************/ + /* PaymentMode Data Source */ + /***************************/ + + @Override + public void storePaymentMode(PaymentModeDTO paymentMode) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void storePaymentModes(Iterable paymentModes, boolean clear) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public PaymentModeDTO getPaymentMode(String reference) throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public List getPaymentModes() throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean dropPaymentMode(String reference) throws IOException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void clearPaymentModes() throws IOException { + // TODO Auto-generated method stub + + } + + /************************/ + /* Currency Data Source */ + /************************/ + + @Override + public void storeCurrency(CurrencyDTO currency) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void storeCurrencies(Iterable currencies, boolean clear) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public Currency getCurrency(String reference) throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public List getCurrencies() throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Currency getMainCurrency() throws IOException, InvalidRecordException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean dropCurrency(String reference) throws IOException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void clearCurrencies() throws IOException { + // TODO Auto-generated method stub + + } +} diff --git a/src/main/java/org/pasteque/common/datasource/implementation/package-info.java b/src/main/java/org/pasteque/common/datasource/implementation/package-info.java new file mode 100644 index 0000000..90ce54a --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/implementation/package-info.java @@ -0,0 +1,7 @@ +/** + *

Generic implementations of data sources.

+ *

These implementations does not rely upon any dependency outside the base + * java module. Clients can redefine their own data source optimized for their + * target platform.

+ */ +package org.pasteque.common.datasource.implementation; diff --git a/src/main/java/org/pasteque/common/datasource/package-info.java b/src/main/java/org/pasteque/common/datasource/package-info.java new file mode 100644 index 0000000..90f4e58 --- /dev/null +++ b/src/main/java/org/pasteque/common/datasource/package-info.java @@ -0,0 +1,10 @@ +/** + *

Read and write data from/to a local source.

+ *

Data sources are used for caching, searching and storage, + * but are not linked together. Adding or removing a record to/from + * a source must not alter the status of other sources. + * Persistance and propagation are managed separately.

+ *

They are mid-level between DTO and models. Storing raw DTO and + * reading ready-to-use models.

+ */ +package org.pasteque.common.datasource; diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/APIVersionDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/APIVersionDTO.java new file mode 100644 index 0000000..9b63a36 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/APIVersionDTO.java @@ -0,0 +1,97 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.dto.DTOInterface; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint; +import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Pasteque API version Transfer Object.

+ */ +public class APIVersionDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 4654741357430549138L; + + /** {@see getLevel()} */ + private final int level; + /** {@see getRevision()} */ + private final int revision; + /** {@see getVersion()} */ + private final String version; + + /** + * Create a version from all fields. + * @param level See {@link getLevel()}. + * @param revision See {@link getRevision()}. + * @param version See {@link getVersion()}. + */ + public APIVersionDTO( + int level, + int revision, + String version) { + this.level = level; + this.revision = revision; + this.version = version; + } + + /** + * Read from a raw encoded string. + * @param reader The reader that will parse the data. It must already be reading + * the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}. + * @throws ParseException If the data cannot be parsed or is malformed for + * this DTO. + */ + public APIVersionDTO(Reader reader) throws ParseException { + this.level = reader.readInt("level"); + this.revision = reader.readInt("revision"); + this.version = reader.readString("version"); + } + + /** + * Get the compatibility level. + * Clients and API must share the same level. + * @return The API level. + */ + public int getLevel() { + return this.level; + } + + /** + * Get the revision. All revisions are retro-compatible within + * the same level. + * @return The revision number of the API. + */ + public int getRevision() { + return this.revision; + } + + /** + * Get the String representation of this version. + * @return The API version, commonly level.revision. + */ + public String getVersion() { + return this.version; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + List exceptions = new ArrayList(); + if (this.version == null || "".equals(this.version)) { + exceptions.add(new InvalidFieldException( + IntegrityFieldConstraint.NOT_NULL, + this.getClass().getName(), + "version", + null, + null)); + } + if (!exceptions.isEmpty()) { + throw new IntegrityExceptions(exceptions); + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/CategoryDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/CategoryDTO.java new file mode 100644 index 0000000..8e6f532 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/CategoryDTO.java @@ -0,0 +1,61 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Category of products Data Transfer Object.

+ *

As of Pasteque API version 8, categories cannot be deactivated.

+ */ +public class CategoryDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = 8211717550217795453L; + + /** {@see getParent} */ + private final Integer parent; + + /** + * Create a category from all fields. + * @param id The id. + * @param reference The unique reference. + * @param label The display name. + * @param dispOrder The display order. + * @param hasImage Whether an image is linked to this object. + * @param parent See {@link getParent}. + */ + public CategoryDTO( + Integer id, + String reference, + String label, + int dispOrder, + boolean hasImage, + Integer parent) { + super(id, reference, label, dispOrder, true, hasImage); + this.parent = parent; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public CategoryDTO(Reader reader) throws ParseException { + super(reader, "reference", "label", "dispOrder", null, "hasImage"); + if (!reader.isNull("parent")) { + this.parent = Integer.valueOf(reader.readInt("parent")); + } else { + this.parent = null; + } + } + + /** + * Get the id of the parent category. + * @return The id of the parent category, null when this category is + * a first-level one. + */ + public Integer getParent() { + return this.parent; + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/CommonDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/CommonDTO.java new file mode 100644 index 0000000..55f315d --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/CommonDTO.java @@ -0,0 +1,218 @@ +package org.pasteque.common.datatransfer.dto; + +import java.util.ArrayList; +import java.util.List; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.dto.DTOInterface; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint; +import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + * Helper class for common fields. + */ +/* package */ abstract class CommonDTO implements DTOInterface +{ + /** + * Nullable internal identifier. This field will be deprecated once + * the reference is used to link records. + */ + protected final Integer id; + + /** + * User friendly identifier. In v8 some models do not have reference, + * in that case it can return an empty string (but not null) + * until the API refactoring is done. + */ + protected final String reference; + + /** + * Display name. In v8 some models may use an other name for that field. + */ + protected final String label; + + /** + *

Explicit display order. Records are sorted by dispOrder by default. + * When multiple records share the same dispOrder, they are sorted + * by label. Other sorting methods can be selected.

+ *

In v8, some models do not have a dispOrder, in that case it can + * return 0 until the API refactoring is done.

+ */ + protected final int dispOrder; + + /** + * Whether the record should be visible or not. In v8 some DTO do not have + * this status, in that case it must return true. + */ + protected final boolean active; + + /** + * Whether the record has an image that can be retrieved from elsewhere. + * When the model cannot have an image, it must return false. + */ + protected final boolean hasImage; + + /** + * Create a complete DTO. + * @param id The given id + * @param reference The reference. Set it to "" when the object + * doesn't have a reference yet. + * @param label The display name, label, preferred name... + * @param dispOrder The display order. + * @param active Whether this object is active or not. + * @param hasImage Whether an image is linked to this object. + */ + public CommonDTO( + Integer id, + String reference, + String label, + int dispOrder, + boolean active, + boolean hasImage) { + this.id = id; + this.reference = reference; + this.label = label; + this.dispOrder = dispOrder; + this.active = active; + this.hasImage = hasImage; + } + + /** + *

Read a DTO from a raw encoded string.

+ *

Use arguments to remap keys for generic attributes, set to null + * to not read it from the reader.

+ *

Subclasses must initialize all other fields and call reader.endObject().

+ * @param reader The reader that will parse the data. It must already be reading + * the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}. + * @param referenceKey The key to use to read reference, set null to set it to "". + * @param labelKey The key to use to read label, set null to set it to "". + * @param dispOrderKey The key to use to read dispOrder, set null to set it to 0. + * @param activeKey The key to use to read active, set null to set it to true. + * @param hasImageKey The key to use to read hasImage, set null to set it to false. + * @throws ParseException If the data cannot be parsed or is malformed for + * this DTO. + */ + protected CommonDTO(Reader reader, + String referenceKey, + String labelKey, + String dispOrderKey, + String activeKey, + String hasImageKey) throws ParseException { + this.id = reader.readIntOrNull("id"); + if (referenceKey != null) { + this.reference = reader.readString(referenceKey); + } else { + this.reference = ""; + } + if (labelKey != null) { + this.label = reader.readString(labelKey); + } else { + this.label = ""; + } + if (dispOrderKey != null) { + this.dispOrder = reader.readInt(dispOrderKey); + } else { + this.dispOrder = 0; + } + if (activeKey != null) { + this.active = reader.readBoolean(activeKey); + } else { + this.active = true; + } + if (hasImageKey != null) { + this.hasImage = reader.readBoolean(hasImageKey); + } else { + this.hasImage = false; + } + } + + /** + * Get the identifier. It may be null when not currently stored + * in a database. + * @return The identifier. May be null. + */ + public Integer getId() { + return this.id; + } + + /** + * Get the display name. + * @return The label, name or preferred display name + */ + public String getLabel() { + return this.label; + } + + /** + * Get the Reference. It is a user friendly identifier. + * When the model doesn't have a reference yet, it must be set + * to an empty string until a reference is set for all models. + * @return The reference. + */ + public String getReference() { + return this.reference; + } + + /** + *

Get the explicit display order. Records are sorted by dispOrder by default. + * When multiple records share the same dispOrder, they are sorted + * by label. Other sorting methods can be provided.

+ *

In v8, some models do not have a dispOrder, in that case it can + * return 0 until the API refactoring is done.

+ * @return The display order. 0 when not set. + */ + public int getDispOrder() { + return this.dispOrder; + } + + /** + * Check whether the object is active or was deactivated. + * When the model doesn't have an active state yet, it must + * be set to true until the state is added to all models. + * @return The active state. + */ + public boolean isActive() { + return this.active; + } + + /** + * Check whether an image is linked to the object. + * Images are stored separately. When the model cannot have + * images yet, it must be set to false until images are extended + * to all models. + * @return Whether an image is linked to the object. + */ + public boolean hasImage() { + return this.hasImage; + } + + /** + * Common integrity check. It checks whether a reference is set. + * It is called from {@link #checkIntegrity}. Subclasses should + * call this instead of super.checkIntegrity for common integrity check. + * @return The list of exception thrown during the check. + */ + protected List commonIntegrityCheck() { + List exceptions = new ArrayList(); + if (this.reference == null) { + exceptions.add(new InvalidFieldException( + IntegrityFieldConstraint.NOT_NULL, + this.getClass().getName(), + "reference", + String.valueOf(this.getId()), + null) + ); + } + return exceptions; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + List exceptions = this.commonIntegrityCheck(); + if (!exceptions.isEmpty()) { + throw new IntegrityExceptions(exceptions); + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/CompositionDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/CompositionDTO.java new file mode 100644 index 0000000..8e0fe24 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/CompositionDTO.java @@ -0,0 +1,180 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ImmutableList; + +/** + *

Composition product Data Transfer Object.

+ *

Compositions are products that are composed of multiple other ones, + * picked one in multiple selections.

+ */ +public class CompositionDTO extends ProductDTO implements Serializable +{ + private static final long serialVersionUID = -3223110713928387147L; + + /** Always true */ + protected final boolean composition = true; + /** {@see getCompositionGroups()} */ + private final ImmutableList compositionGroups; + + /** + * Create a products from all fields. + * @param id The product id. + * @param reference The unique reference. + * @param label The display name. + * @param dispOrder The explicit display order within the category. + * @param active If this product should be shown in catalog or not. + * @param hasImage Whether an image is associated to this product. + * @param barcode See {@link getBarcode} + * @param priceBuy See {@link getPriceBuy} + * @param priceSell See {@link getPriceSell} + * @param scaled See {@link isScaled} + * @param scaleType See {@link getScaleType} + * @param scaleValue See {@link getScaleValue} + * @param discountEnabled See {@link isDiscountEnabled} + * @param discountRate See {@link getDiscountRate} + * @param prepay See {@link isPrepay} + * @param category See {@link getCategory} + * @param tax See {@link getTax} + * @param taxedPrice See {@link getTaxedPrice} + * @param compositionGroups See {@link getCompositionGroups} + */ + public CompositionDTO( + Integer id, + String reference, + String label, + int dispOrder, + boolean active, + boolean hasImage, + String barcode, + Double priceBuy, + double priceSell, + boolean scaled, + int scaleType, + double scaleValue, + boolean discountEnabled, + double discountRate, + boolean prepay, + int category, + int tax, + double taxedPrice, + CompositionGroupDTO[] compositionGroups) { + super(id, reference, label, dispOrder, active, hasImage, barcode, + priceBuy, priceSell, scaled, scaleType, scaleValue, + discountEnabled, discountRate, prepay, category, tax, taxedPrice); + this.compositionGroups = new ImmutableList(compositionGroups); + } + + /** + *

Whether this product is a composition or not.

+ * @return True. + */ + @Override + public boolean isComposition() { + return true; + } + + /** + * Get the choices available for this composition. + * @return The array of groups, containing a set of products for each. + */ + public ImmutableList getCompositionGroups() { + if (this.compositionGroups == null) { + return new ImmutableList(); + } + return this.compositionGroups; + } + + /** + *

Data Transfer Object for a group of choices within a composition.

+ *

A composition is completed once a single product has been selected within + * each group.

+ */ + public class CompositionGroupDTO implements Serializable + { + private static final long serialVersionUID = -5237752185922817170L; + + /** {@see getLabel()} */ + private final String label; + /** {@see getDispOrder()} */ + private final int dispOrder; + /** {@see getCompositionProducts()} */ + private final CompositionProductDTO[] compositionProducts; + + /** + * Create a composition group from all fields. + * @param label See {@link getLabel} + * @param dispOrder See {@link getDispOrder} + * @param compositionProducts See {@link getCompositionProducts} + */ + public CompositionGroupDTO(String label, int dispOrder, CompositionProductDTO[] compositionProducts) { + super(); + this.label = label; + this.dispOrder = dispOrder; + this.compositionProducts = compositionProducts; + } + + /** + * Get the name of the group. + * @return The name of the group. + */ + public String getLabel() { + return label; + } + + /** + * Get the explicit display order of the group. + * @return The explicit display order. + */ + public int getDispOrder() { + return dispOrder; + } + + /** + * Get all choices available within this group. + * @return The choices available within this group. + */ + public CompositionProductDTO[] getCompositionProducts() { + return compositionProducts; + } + } + + /** + * Choice of a product in a composition Data Transfer Object. + */ + public class CompositionProductDTO implements Serializable + { + private static final long serialVersionUID = 4024627602322411550L; + + /** {@see getProduct()} */ + private final int product; + /** {@see getDispOrder()} */ + private final int dispOrder; + + /** + * Create a composition product with all fields. + * @param product The id of the product. + * @param dispOrder The explicit display order in the group. + */ + public CompositionProductDTO(int product, int dispOrder) { + this.product = product; + this.dispOrder = dispOrder; + } + + /** + * Get the id of the product. + * @return The id of the product. + */ + public int getProduct() { + return product; + } + + /** + * Get the explicit display order. + * @return The display order. + */ + public int getDispOrder() { + return dispOrder; + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/CurrencyDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/CurrencyDTO.java new file mode 100644 index 0000000..8a6c6b8 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/CurrencyDTO.java @@ -0,0 +1,157 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Currency Data Transfer Object.

+ *

As of Pasteque API version 8, currencies cannot have an associated image + * nor a display order.

+ */ +public class CurrencyDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = -6259735426241774167L; + + /** {@see getSymbol()} */ + private final String symbol; + /** {@see getDecimalSeparator()} */ + private final String decimalSeparator; + /** {@see getThousandsSeparator()} */ + private final String thousandsSeparator; + /** {@see getFormat} */ + private final String format; + /** {@see getRate()} */ + private final double rate; + /** {@see isMain()} */ + private final boolean main; + + /** + * Create a currency from all fields. + * @param id The id. + * @param reference The unique reference. + * @param label The display name. + * @param active Whether this object is active or not. + * @param symbol See {@link getSymbol}. + * @param decimalSeparator See {@link getDecimalSeparator}. + * @param thousandsSeparator See {@link getThousandsSeparator}. + * @param format See {@link getFormat}. + * @param rate See {@link getRate}. + * @param main See {@link isMain}. + */ + public CurrencyDTO( + Integer id, + String reference, + String label, + boolean active, + String symbol, + String decimalSeparator, + String thousandsSeparator, + String format, + double rate, + boolean main) { + super(id, reference, label, 0, active, false); + this.symbol = symbol; + this.decimalSeparator = decimalSeparator; + this.thousandsSeparator = thousandsSeparator; + this.format = format; + this.rate = rate; + this.main = main; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public CurrencyDTO(Reader reader) throws ParseException { + super(reader, "reference", "label", null, "visible", null); + this.symbol = reader.readString("symbol"); + this.decimalSeparator = reader.readString("decimalSeparator"); + this.thousandsSeparator = reader.readString("thousandsSeparator"); + this.format = reader.readString("format"); + this.rate = reader.readDouble("rate"); + this.main = reader.readBoolean("rate"); + } + + /** + * Get the symbol to use for this currency. + * @return The symbol. Empty string when not set. + * @see getFormat() + */ + public String getSymbol() { + if (this.symbol == null) { + return ""; + } + return this.symbol; + } + + /** + * Get the decimal separator. + * @return The string to use as decimal separator. Empty string when not set. + * @see getFormat() + */ + public String getDecimalSeparator() { + if (this.decimalSeparator == null) { + return ""; + } + return this.decimalSeparator; + } + + /** + * Get the thousands separator. + * @return The string to use as thousands separator. Empty string when not set. + * @see getFormat() + */ + public String getThousandsSeparator() { + if (this.thousandsSeparator == null) { + return ""; + } + return this.thousandsSeparator; + } + + /** + *

Get the format string to display values in this currency.

+ *

The format string contains the given symbols:

+ *
    + *
  • '¤' or '$' to print the currency symbol (see {@link getSymbol}
  • + *
  • '.' to print the decimal separator (see {@link getDecimalSeparator}
  • + *
  • ',' to print the thousands separator (see {@link getThousandsSeparator}
  • + *
  • '0' for any significative number
  • + *
  • '#' for any non-significative number
  • + *
+ *

For example for 2 significative numbers and currency after the number: "#,##0.00¤".

+ * @return The format. + * {@see java.text.DecimalFormat} + * {@see java.text.DecimalFormatSymbols} + */ + public String getFormat() { + return this.format; + } + + /** + * Get the conversion rate to the main currency. + * @return The conversion rate. It has no meaning when {@link isMain} is true. + */ + public double getRate() { + return this.rate; + } + + /** + * Whether this currency is the main one. + *

All prices are computed with the main currency, the value of alternative + * currencies are converted to the main one using this rate:

+ *
    + *
  • Value in main currency = value in alt currency / rate
  • + *
  • Value in alt currency = value in main currency * rate
  • + *
  • 1 in main currency = rate in alt currency
  • + *
+ * @return The conversion rate. + * {@see org.pasteque.common.model.Currency#convertToMain} + * {@see org.pasteque.common.model.Currency#convertFromMain} + */ + public boolean isMain() { + return this.main; + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/CustomerDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/CustomerDTO.java new file mode 100644 index 0000000..f4e8848 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/CustomerDTO.java @@ -0,0 +1,414 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import java.util.Date; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Customer account Data Transfer Object.

+ *

As of Pasteque API version 8, customers don't have a reference and it will always be an empty string, + * nor have a display order.

+ *

All contact fields are informative and can be recycled to hold an other information. Options from the API can + * redefine and other meaning by changing the label of the field. All those informations will be redefined + * in future version to be more extensible and ease the synchronization with external CRM softwares.

+ */ +public class CustomerDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = 2904701878615495822L; + + /** {@see getCard()} */ + private final String card; + /** {@see getMaxDebt()} */ + private final double maxDebt; + /** {@see getBalance()} */ + private final double balance; + /** {@see getExpireDate()} */ + private final Date expireDate; + /** {@see getDiscountProfile()} */ + private final Integer discountProfile; + /** {@see getTariffArea()} */ + private final Integer tariffArea; + /** {@see getTax()} */ + private final Integer tax; + + // Contact fields + // These fields may be moved away to ease compatibility with CRM softwares. + /** {@see getFirstName()} */ + private final String firstName; + /** {@see getLastName()} */ + private final String lastName; + /** {@see getEmail()} */ + private final String email; + /** {@see getPhone1()} */ + private final String phone1; + /** {@see getPhone2()} */ + private final String phone2; + /** {@see getFax()} */ + private final String fax; + /** {@see getAddr1()} */ + private final String addr1; + /** {@see getAddr2()} */ + private final String addr2; + /** {@see getZipCode()} */ + private final String zipCode; + /** {@see getCity()} */ + private final String city; + /** {@see getRegion()} */ + private final String region; + /** {@see getCountry()} */ + private final String country; + /** {@see getNote()} */ + private final String note; + + /** + * Create a customer from all fields. + * @param id The id. + * @param label The customer's display name (dispName). + * @param card See {@link getCard}. + * @param maxDebt See {@link getMaxDebt}. + * @param balance See {@link getBalance} + * @param expireDate See {@link getExpireDate}. + * @param discountProfile See {@link getDiscountProfile}. + * @param tariffArea See {@link getTariffArea}. + * @param tax See {@link getTax}. + * @param active See {@link isActive}. + * @param hasImage See {@link hasImage}. + * @param firstName See {@link getFirstName}. + * @param lastName See {@link getLastName}. + * @param email See {@link getEmail}. + * @param phone1 See {@link getPhone1}. + * @param phone2 See {@link getPhone2}. + * @param fax See {@link getFax}. + * @param addr1 See {@link getAddr1}. + * @param addr2 See {@link getAddr2}. + * @param zipCode See {@link getZipCode}. + * @param city See {@link getCity}. + * @param region See {@link getRegion}. + * @param country See {@link getCountry}. + * @param note See {@link getNote}. + */ + public CustomerDTO( + Integer id, + String label, + String card, + double maxDebt, + double balance, + Date expireDate, + Integer discountProfile, + Integer tariffArea, + Integer tax, + boolean active, + boolean hasImage, + String firstName, + String lastName, + String email, + String phone1, + String phone2, + String fax, + String addr1, + String addr2, + String zipCode, + String city, + String region, + String country, + String note) { + super(id, "", label, 0, active, hasImage); + this.card = card; + this.maxDebt = maxDebt; + this.balance = balance; + this.expireDate = expireDate; + this.discountProfile = discountProfile; + this.tariffArea = tariffArea; + this.tax = tax; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.phone1 = phone1; + this.phone2 = phone2; + this.fax = fax; + this.addr1 = addr1; + this.addr2 = addr2; + this.zipCode = zipCode; + this.city = city; + this.region = region; + this.country = country; + this.note = note; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public CustomerDTO(Reader reader) throws ParseException { + super(reader, null, "dispName", null, "visible", "hasImage"); + this.card = reader.readString("card"); + this.maxDebt = reader.readDouble("maxDebt"); + this.balance = reader.readDouble("balance"); + this.expireDate = reader.readDateOrNull("expireDate"); + this.discountProfile = reader.readIntOrNull("discountProfile"); + this.tariffArea = reader.readIntOrNull("tariffArea"); + this.tax = reader.readIntOrNull("tax"); + this.firstName = reader.readStringOrEmpty("firstName"); + this.lastName = reader.readStringOrEmpty("lastName"); + this.email = reader.readStringOrEmpty("email"); + this.phone1 = reader.readStringOrEmpty("phone1"); + this.phone2 = reader.readStringOrEmpty("phone2"); + this.fax = reader.readStringOrEmpty("fax"); + this.addr1 = reader.readStringOrEmpty("addr1"); + this.addr2 = reader.readStringOrEmpty("addr2"); + this.zipCode = reader.readStringOrEmpty("zipCode"); + this.city = reader.readStringOrEmpty("city"); + this.region = reader.readStringOrEmpty("region"); + this.country = reader.readStringOrEmpty("country"); + this.note = reader.readStringOrEmpty("note"); + } + + /** + * Alias of {@link getLabel} to match the technical name. + * @return The preferred display name. + */ + public String getDispName() { + return this.label; + } + + /** + * Optional card number or text. When set, it must Start with a 'c' + * for compatibility with quick scan. + * @return The customer's card number/string. Empty string when not set. + */ + public String getCard() { + if (this.card == null) { + return ""; + } + return this.card; + } + + /** + * The maximum amount of debt this customer can contract. + * The balance may still be above the maximum debt in some cases. + * @return The maximum amount. + */ + public double getMaxDebt() { + return this.maxDebt; + } + + /** + * The current balance of this customer. When it is positive, + * the customer has prepaid something. When negative, the customer has contracted debt. + * @return The current balance. + */ + public double getBalance() { + return this.balance; + } + + /** + * Get the informative expiration date. + * @return The expiration date, can be null. + */ + public Date getExpireDate() { + return this.expireDate; + } + + /** + * Get the id of the discount profile to assign to this customer. + * @return The discount profile id. Can be null. + */ + public Integer getDiscountProfile() { + return this.discountProfile; + } + + /** + * Get the id of the tariff area to assign to this customer. + * @return The tariff area id. Can be null. + */ + public Integer getTariffArea() { + return this.tariffArea; + } + + /** + * Get the id of the alternative tax to assign to orders from this customer. + * @return The alternative tax id. Can be null. + */ + + public Integer getTax() { + return this.tax; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return First name. Will be an empty string when not set. + */ + public String getFirstName() { + if (this.firstName == null) { + return ""; + } + return this.firstName; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Last name. Will be an empty string when not set. + */ + public String getLastName() { + if (this.lastName == null) { + return ""; + } + return this.lastName; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Email address. Will be an empty string when not set. + */ + public String getEmail() { + if (this.email == null) { + return ""; + } + return this.email; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Phone number. Will be an empty string when not set. + */ + public String getPhone1() { + if (this.phone1 == null) { + return ""; + } + return this.phone1; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Alternative phone number. Will be an empty string when not set. + */ + public String getPhone2() { + if (this.phone2 == null) { + return ""; + } + return this.phone2; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Fax number. Will be an empty string when not set. + */ + public String getFax() { + if (this.fax == null) { + return ""; + } + return this.fax; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Address, first line. Will be an empty string when not set. + */ + public String getAddr1() { + if (this.addr1 == null) { + return ""; + } + return this.addr1; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Address, second line. Will be an empty string when not set. + */ + public String getAddr2() { + if (this.addr2 == null) { + return ""; + } + return this.addr2; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Zip code. Will be an empty string when not set. + */ + public String getZipCode() { + if (this.zipCode == null) { + return ""; + } + return this.zipCode; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return City. Will be an empty string when not set. + */ + public String getCity() { + if (this.city == null) { + return ""; + } + return this.city; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Region. Will be an empty string when not set. + */ + public String getRegion() { + if (this.region == null) { + return ""; + } + return this.region; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Country. Will be an empty string when not set. + */ + public String getCountry() { + if (this.country == null) { + return ""; + } + return this.country; + } + + /** + * Contact information that can be recycled to other meanings. + * These fields may be moved away in future versions to ease compatibility + * with CRM softwares. + * @return Free note. Will be an empty string when not set. + */ + public String getNote() { + if (this.note == null) { + return ""; + } + return this.note; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // No reference to check. + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/DiscountProfileDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/DiscountProfileDTO.java new file mode 100644 index 0000000..ba843ec --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/DiscountProfileDTO.java @@ -0,0 +1,71 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Discount profile Data Transfer Object.

+ *

As of Pasteque API version 8, discount profiles cannot have a reference, + * nor display order nor image and cannot be deactivated.

+ */ +public class DiscountProfileDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = -2505636588019239236L; + + /** See {@link getDiscountRate()}. */ + private final double discountRate; + + /** + * Create a discount profile from all fields. + * @param id The id. + * @param label The display name, it is used as reference. + * @param discountRate See {@link getDiscountRate()}. + */ + public DiscountProfileDTO( + Integer id, + String label, + double discountRate) { + super(id, "", label, 0, true, false); + this.discountRate = discountRate; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public DiscountProfileDTO(Reader reader) throws ParseException { + super(reader, null, "label", null, null, null); + this.discountRate = reader.readDouble("rate"); + } + + /** + * Get the label. DiscountProfile has no reference and will use + * its label as one. + * @return The label. + */ + @Override + public String getReference() { + return this.label; + } + + /** + * Get the discount rate to apply. The rate should be between 0.0 and 1.0 + * but it is not checked. + * @return The discount rate to apply for this profile. + */ + public double getDiscountRate() { + return this.discountRate; + } + + @Override + protected List commonIntegrityCheck() { + // DiscountProfile has no reference to check. + return new ArrayList(); + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/FloorDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/FloorDTO.java new file mode 100644 index 0000000..9b85e60 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/FloorDTO.java @@ -0,0 +1,134 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Restaurant floor or room Data Transfer Object.

+ *

As of Pasteque API version 8, floors has no reference, cannot be deactivated, + * nor have an associated image.

+ */ +public class FloorDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = -4474106026709243994L; + + /** {@see getPlaces()} */ + private final ImmutableList places; + + /** + * Create a floor from all fields. + * @param id The id. + * @param label The label. + * @param dispOrder The display order. + * @param places See {@link getPlaces}. + */ + public FloorDTO( + Integer id, + String label, + int dispOrder, + PlaceDTO[] places) { + super(id, "", label, dispOrder, true, false); + this.places = new ImmutableList(places); + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public FloorDTO(Reader reader) throws ParseException { + super(reader, null, "label", "dispOrder", null, null); + reader.startArray("places"); + PlaceDTO[] places = new PlaceDTO[reader.getArraySize()]; + for (int i = 0; i < reader.getArraySize(); i++) { + reader.startObject(i); + places[i] = new PlaceDTO(reader); + reader.endObject(); + } + this.places = new ImmutableList(places); + reader.endArray(); + } + + /** + * Get the list of places within this floor. + * @return The list of places. + */ + public ImmutableList getPlaces() { + if (this.places == null) { + return new ImmutableList(); + } + return this.places; + } + + /** + *

Place Data Transfer Object. Places are always bound to a floor.

+ *

The coordinates are abstract and relative to each other within the floor. + * See {@link org.pasteque.common.view.CoordStretcher} to convert abstract + * coordinates to coordinates within boundaries.

+ */ + public class PlaceDTO implements Serializable + { + private static final long serialVersionUID = 8129968704161611106L; + + /** {@see getLabel()} */ + private final String label; + /** {@see getX()} */ + private final int x; + /** {@see getY()} */ + private final int y; + + /** + * Create a place from all fields. + * @param label The display name. + * @param x See {@link getX}. + * @param y See {@link getY}. + */ + public PlaceDTO(String label, int x, int y) { + this.label = label; + this.x = x; + this.y = y; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public PlaceDTO(Reader reader) throws ParseException { + this.label = reader.readStringOrEmpty("label"); + this.x = reader.readInt("x"); + this.y = reader.readInt("y"); + } + + /** + * Get the name of the place. + * @return The name of the place, empty string when not set. + */ + public String getLabel() { + if (this.label == null) { + return ""; + } + return this.label; + } + + /** + * Get the abstract X coordinate of the place within the floor. + * @return The X coordinate. + */ + public int getX() { + return this.x; + } + + /** + * Get the abstract Y coordinate of the place within the floor. + * @return The Y coordinate. + */ + public int getY() { + return this.y; + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/ImageDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/ImageDTO.java new file mode 100644 index 0000000..9494eb4 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/ImageDTO.java @@ -0,0 +1,123 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import org.pasteque.common.constants.ModelName; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.dto.DTOInterface; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint; +import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Image Data Transfer Object, to be associated to an other DTO + * with the hasImage flag.

+ *

Due to the poor performance of encoding binary data as text, + * this DTO should be mostly used in binary format.

+ */ +public class ImageDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = -7807272808383007554L; + + /** {@see getModel()} */ + private final String model; + /** {@see getReference()} */ + private final String reference; + /** {@see getImage()} */ + private final byte[] imageData; + + /** + * Create an image from all fields. + * @param model See {@link getModel()}. + * @param reference See {@link getReference()}. + * @param imageData See {@link getImage()}. + */ + public ImageDTO( + String model, + String reference, + byte[] imageData) { + this.model = model; + this.reference = reference; + this.imageData = imageData; + } + + /** + * Read from a raw encoded string. + * @param reader The reader that will parse the data. It must already be reading + * the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}. + * @throws ParseException If the data cannot be parsed or is malformed for + * this DTO. + */ + public ImageDTO(Reader reader) throws ParseException { + this.model = reader.readString("model"); + this.reference = reader.readString("id"); + this.imageData = reader.readBinary("image"); + } + + /** + * Get the model name to with the image is attached. + * {@see org.pasteque.common.constants.ModelName} + * @return The model name to with the image is attached. + */ + public String getModel() { + return this.model; + } + + /** + * Get the identifier of the record this image is attached to. + * @return The identifier of the record. Is is always using a String + * representation and may not be the actual reference. + */ + public String getReference() { + return this.reference; + } + + /** + * Get the binary data of the image. + * @return The image. + */ + public byte[] getImage() { + return this.imageData; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + List exceptions = new ArrayList(); + boolean checkModelValue = true; + if (this.model == null || "".equals(this.model)) { + exceptions.add(new InvalidFieldException( + IntegrityFieldConstraint.NOT_NULL, + this.getClass().getName(), + "model", + null, // TODO: id string + null)); + checkModelValue = false; + } + if (this.reference == null || "".equals(this.reference)) { + exceptions.add(new InvalidFieldException( + IntegrityFieldConstraint.NOT_NULL, + this.getClass().getName(), + "id", + null, // TODO: id string + null)); + } + if (checkModelValue) { + try { + ModelName.fromCode(this.model); + } catch (IllegalArgumentException e) { + exceptions.add(new InvalidFieldException( + IntegrityFieldConstraint.ENUM_REQUIRED, + this.getClass().getName(), + "model", + null, // TODO: id string + null)); + } + } + if (!exceptions.isEmpty()) { + throw new IntegrityExceptions(exceptions); + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/OptionDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/OptionDTO.java new file mode 100644 index 0000000..df4196b --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/OptionDTO.java @@ -0,0 +1,100 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.dto.DTOInterface; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint; +import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + * Misc options Data Transfer Object. + */ +public class OptionDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 8951727299457218490L; + + /** {@see getName()} */ + private final String name; + /** {@see getContent()} */ + private final String content; + /** {@see isSystem()} */ + private final boolean system; + + /** + * Create an option from all fields. + * @param name See {@link getName}. + * @param content See {@link getContent}. + * @param system See {@link isSystem}. + */ + public OptionDTO( + String name, + String content, + boolean system) { + this.name = name; + this.content = content; + this.system = system; + } + + /** + * Read from a raw encoded string. + * @param reader The reader that will parse the data. It must already be reading + * the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}. + * @throws ParseException If the data cannot be parsed or is malformed for + * this DTO. + */ + public OptionDTO(Reader reader) throws ParseException { + this.name = reader.readString("name"); + this.content = reader.readStringOrEmpty("content"); + this.system = reader.readBoolean("system"); + } + + /** + * Get the name of the option. The name is used as an identifier. + * @return The name of the option. + */ + public String getName() { + return this.name; + } + + /** + * Get the content (aka value) of the option. Its format is not enforced, but is usually + * JSON-compatible. + * @return The content of the option. Empty string when not set. + */ + public String getContent() { + if (this.content == null) { + return ""; + } + return this.content; + } + + /** + * Check whether this option is a system one or not. System options + * cannot be updated by the user, like a version number. + * @return Whether the option is a system one or not. + */ + public boolean isSystem() { + return this.system; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + List exceptions = new ArrayList(); + if (this.name == null || "".equals(this.name)) { + exceptions.add(new InvalidFieldException( + IntegrityFieldConstraint.NOT_NULL, + this.getClass().getName(), + "name", + null, + null)); + } + if (!exceptions.isEmpty()) { + throw new IntegrityExceptions(exceptions); + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/OrderDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/OrderDTO.java new file mode 100644 index 0000000..6d898d2 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/OrderDTO.java @@ -0,0 +1,367 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.dto.DTOInterface; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + * A mutable and not paid order. Once an order is paid, it is transformed + * into a {@link org.pasteque.coreutil.datatransfer.dto.TicketDTO}. + * TODO: experimental + */ +public class OrderDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = -3898628758749878634L; + + /** See {@link getId()}. */ + private final String id; + /** See {@link getName()}. */ + private final String name; + /** See {@link getCustomName()}. */ + private final String customName; + /** See {@link getCustomer()}. */ + private final Integer customer; + /** See {@link getCustCount()}. */ + private final Integer custCount; + /** See {@link getLines()}. */ + private final ImmutableList lines; + /** See {@link getDiscountProfile()}. */ + private final Integer discountProfile; + /** See {@link getDiscountRate()}. */ + private final double discountRate; + /** See {@link getFinalPrice()}. */ + private final double finalPrice; + /** See {@link getFinalTaxedPrice()}. */ + private final double finalTaxedPrice; + + /** + * Create an order from all fields. + * @param id See {@link getId()}. + * @param name See {@link getName()}. + * @param customName See {@link getCustomName()}. + * @param customer See {@link getCustomer()}. + * @param custCount See {@link getCustCount()}. + * @param lines See {@link getLines()}. + * @param discountProfile See {@link getDiscountProfile()}. + * @param discountRate See {@link getDiscountRate()}. + * @param finalPrice See {@link getFinalPrice()}. + * @param finalTaxedPrice See {@link getFinalTaxedPrice()}. + */ + public OrderDTO( + String id, + String name, + String customName, + Integer customer, + Integer custCount, + OrderLineDTO[] lines, + Integer discountProfile, + double discountRate, + double finalPrice, + double finalTaxedPrice) { + this.id = id; + this.name = name; + this.customName = customName; + this.customer = customer; + this.custCount = custCount; + this.lines = new ImmutableList(lines); + this.discountProfile = discountProfile; + this.discountRate = discountRate; + this.finalPrice = finalPrice; + this.finalTaxedPrice = finalTaxedPrice; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public OrderDTO(Reader reader) throws ParseException { + this.id= reader.readString("id"); + this.name = reader.readString("name"); + this.customName = reader.readStringOrNull("customName"); + this.customer = reader.readIntOrNull("customer"); + this.custCount = reader.readIntOrNull("custCount"); + reader.startArray("lines"); + OrderLineDTO[] lines = new OrderLineDTO[reader.getArraySize()]; + for (int i = 0; i < reader.getArraySize(); i++) { + reader.startObject(i); + lines[i] = new OrderLineDTO(reader); + reader.endObject(); + } + this.lines = new ImmutableList(lines); + reader.endArray(); + this.discountProfile = reader.readIntOrNull("discountProfile"); + this.discountRate = reader.readDouble("discountRate"); + this.finalPrice = reader.readDouble("finalPrice"); + this.finalTaxedPrice = reader.readDouble("finalTaxedPrice"); + } + + /** + * Get the arbitrary id of this order. + * @return The unique identifier of this order. + */ + public String getId() { + return this.id; + } + + /** + * Get the generic name that was set to this order. + * @return The generic name of the order. + */ + public String getName() { + return this.name; + } + + /** + * Get the custom name of this order. + * @return The custom name of this order. Null when not set. + */ + public String getCustomName() { + return this.customName; + } + + /** + * Get the id of the customer's account that was linked to this order. + * @return The id of the customer's account. Null when not set. + */ + public Integer getCustomer() { + return this.customer; + } + + /** + * Get the number of customers set for this order. It is not bound + * to the customer's account. + * @return The number of customers set for this order. Null when no + * number was defined. + */ + public Integer getCustCount() { + return this.custCount; + } + + /** + * Get the content of this order. + * @return The list of lines in this order. + */ + public ImmutableList getLines() { + return this.lines; + } + + /** + * Get the id of the discount profile that was linked to this order. + * @return The id of the discount profile. Null when not set. + */ + public Integer getDiscountProfile() { + return this.discountProfile; + } + + /** + * Get the discount rate applied to the whole order. + * @return The discount rate to apply to the whole order. + */ + public double getDiscountRate() { + return this.discountRate; + } + + /** + * Get the price without taxes after applying the discount. + * @return The price without taxes after applying the discount. + */ + public double getFinalPrice() { + return this.finalPrice; + } + + /** + * Get the price with taxes after applying the discount. + * @return The price with taxes after applying the discount. + */ + public double getFinalTaxedPrice() { + return this.finalTaxedPrice; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } + + /** + * Content of an order. + */ + public class OrderLineDTO implements Serializable + { + private static final long serialVersionUID = 8802466032089572015L; + + /** See {@link getDispOrder()}. */ + private final int dispOrder; + /** See {@link getCustomLabel()}. */ + private final String customLabel; + /** See {@link getProduct()}. */ + private final Integer product; + /** See {@link getCustomUnitPrice()}. */ + private final Double customUnitPrice; + /** See {@link getCustomTaxedUnitPrice()}. */ + private final Double customTaxedUnitPrice; + /** See {@link getQuantity()}. */ + private final double quantity; + /** See {@link getCustomTax()}. */ + private final Integer customTax; + /** See {@link getDiscountRate()}. */ + private final double discountRate; + /** See {@link getFinalPrice()}. */ + private final double finalPrice; + /** See {@link getFinalTaxedPrice()}. */ + private final double finalTaxedPrice; + + /** + * Create a line from all fields. + * @param dispOrder See {@link getDispOrder()}. + * @param customLabel See {@link getCustomLabel()}. + * @param product See {@link getProduct()}. + * @param customUnitPrice See {@link getCustomUnitPrice()}. + * @param customTaxedUnitPrice See {@link getCustomTaxedUnitPrice()}. + * @param quantity See {@link getQuantity()}. + * @param customTax See {@link getCustomTax()}. + * @param discountRate See {@link getDiscountRate()}. + * @param finalPrice See {@link getFinalPrice()}. + * @param finalTaxedPrice See {@link getFinalTaxedPrice()}. + */ + public OrderLineDTO( + int dispOrder, + String customLabel, + Integer product, + Double customUnitPrice, + Double customTaxedUnitPrice, + double quantity, + Integer customTax, + double discountRate, + double finalPrice, + double finalTaxedPrice) { + this.dispOrder = dispOrder; + this.customLabel = customLabel; + this.product = product; + this.customUnitPrice = customUnitPrice; + this.customTaxedUnitPrice = customTaxedUnitPrice; + this.quantity = quantity; + this.customTax = customTax; + this.discountRate = discountRate; + this.finalPrice = finalPrice; + this.finalTaxedPrice = finalTaxedPrice; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public OrderLineDTO(Reader reader) throws ParseException { + this.dispOrder = reader.readInt("dispOrder"); + this.customLabel = reader.readStringOrNull("customLabel"); + this.product = reader.readIntOrNull("product"); + this.customUnitPrice = reader.readDoubleOrNull("customUnitPrice"); + this.customTaxedUnitPrice = reader.readDoubleOrNull("customTaxedUnitPrice"); + this.customTax = reader.readIntOrNull("customTax"); + this.quantity = reader.readDouble("quantity"); + this.discountRate = reader.readDouble("discountRate"); + this.finalPrice = reader.readDouble("finalPrice"); + this.finalTaxedPrice = reader.readDouble("finalTaxedPrice"); + } + + /** + * Get the number of this line, starting from 0. + * @return The index of this line in the order. + */ + public int getDispOrder() { + return this.dispOrder; + } + + /** + * Get the label for a custom product. + * @return The label of the custom product, null when using + * a registered product. + * @see getProduct() + */ + public String getCustomLabel() { + return this.customLabel; + } + + /** + * Get the id of the product. + * @return The id of the product. Null when using a custom product. + */ + public Integer getProduct() { + return this.product; + } + + /** + * Get the custom price of one unit of the product without taxes. + * @return The custom price of one unit of the product without taxes. + * Null when using the price from a registered product. + * @see getProduct() + */ + public Double getCustomUnitPrice() { + return this.customUnitPrice; + } + + /** + * Get the custom price of one unit of the product with taxes. + * @return The custom price of one unit of the product with taxes. + * Null when using the price from a registered product. + */ + public Double getCustomTaxedUnitPrice() { + return this.customTaxedUnitPrice; + } + + /** + * Get the quantity of product in this line. + * @return The quantity of product in this line. + */ + public double getQuantity() { + return this.quantity; + } + + /** + * Get the id of the custom tax to use. + * @return The id of the alternative tax to use. Null when using the + * tax from a registered product. + * @see getProduct() + */ + public Integer getCustomTax() { + return this.customTax; + } + + /** + * Get the discount rate applied to this line. + * @return The discount rate applied only to this line. + * It is independent from the discount of the ticket. + */ + public double getDiscountRate() { + return discountRate; + } + + /** + * Get the price without taxes for this line after applying + * the discount. + * @return The price without taxes for this line after applying + * the discount of the line. + */ + public double getFinalPrice() { + return finalPrice; + } + + /** + * Get the price with taxes for this line after applying + * the discount. + * @return The price with taxes for this line after applying + * the discount of the line. + */ + public double getFinalTaxedPrice() { + return finalTaxedPrice; + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/PaymentModeDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/PaymentModeDTO.java new file mode 100644 index 0000000..2328e40 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/PaymentModeDTO.java @@ -0,0 +1,244 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.Reader; +import org.pasteque.coreutil.ImmutableList; + +/** + *

Payment mode and values Data Transfer Object.

+ *

Payment mode values are pre-defined common values for quick-access, + * like coins, bills or commonly used values.

+ *

Payment mode returns are a set of rules to handle excessive amounts. + * The one to use is the one which is above and closest to minAmount. + * It indicates which payment mode to use to give back the excess. + * When no return is set or the excess is under the minimum amount of all + * returns, the excess is kept and registered as exceptional benefit.

+ */ +public class PaymentModeDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = 5324960228614648256L; + + /** {@see getBackLabel()} */ + private final String backLabel; + /** {@see getType()} */ + private final int type; + /** {@see getThousandsSeparator()} */ + private final ImmutableList values; + /** {@see getFormat} */ + private final ImmutableList returns; + + /** + * Create a payment mode from all fields. + * @param id The id. + * @param reference The unique reference. + * @param label The display name. + * @param dispOrder The display order. + * @param active Whether this object is active or not. + * @param hasImage Whether an image is linked to this object. + * @param backLabel See {@link getBackLabel}. + * @param type See {@link getType}. + * @param values See {@link getValues}. + * @param returns See {@link getReturns}. + */ + public PaymentModeDTO( + Integer id, + String reference, + String label, + int dispOrder, + boolean active, + boolean hasImage, + String backLabel, + int type, + PaymentModeValueDTO[] values, + PaymentModeReturnDTO[] returns) { + super(id, reference, label, dispOrder, active, hasImage); + this.backLabel = backLabel; + this.type = type; + this.values = new ImmutableList(values); + this.returns = new ImmutableList(returns); + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public PaymentModeDTO(Reader reader) throws ParseException { + super(reader, "reference", "label", "dispOrder", "visible", "hasImage"); + this.backLabel = reader.readStringOrEmpty("backLabel"); + this.type = reader.readInt("type"); + reader.startArray("values"); + PaymentModeValueDTO[] values = new PaymentModeValueDTO[reader.getArraySize()]; + for (int i = 0; i < reader.getArraySize(); i++) { + reader.startObject(i); + values[i] = new PaymentModeValueDTO(reader); + reader.endObject(); + } + this.values = new ImmutableList(values); + reader.endArray(); + reader.startArray("returns"); + PaymentModeReturnDTO[] returns = new PaymentModeReturnDTO[reader.getArraySize()]; + for (int i = 0; i < reader.getArraySize(); i++) { + reader.startObject(i); + returns[i] = new PaymentModeReturnDTO(reader); + reader.endObject(); + } + this.returns = new ImmutableList(returns); + reader.endArray(); + } + + /** + * Get the label to use when returning excess amounts. + * @return The label. Empty string when not set. + */ + public String getBackLabel() { + if (this.backLabel == null) { + return ""; + } + return this.backLabel; + } + + /** + * Get the type of this payment mode. The type is a collection of + * flags for some special properties. + * @return The string to use as decimal separator. Empty string when not set. + * @see org.pasteque.coreutil.constants.PaymentModeType + */ + public int getType() { + return this.type; + } + + /** + * Get predefined values for quick-use. + * @return The list of predefined common values. + */ + public ImmutableList getValues() { + if (this.values == null) { + return new ImmutableList(); + } + return this.values; + } + + /** + * Get the rules to return excessive amounts. + * @return The list of rules to give back excessive amounts. + */ + public ImmutableList getReturns() { + if (this.returns == null) { + return new ImmutableList(); + } + return this.returns; + } + + /** + * Payment mode value Data Transfer Object. Values pre-defined + * common values for quick-access, like coins, bills + * or commonly used values. + */ + public class PaymentModeValueDTO implements Serializable + { + private static final long serialVersionUID = 5503522521532600750L; + + /** {@see getValue()} */ + private final double value; + /** {@see hasImage()} */ + private final boolean hasImage; + + /** + * Create a value from all fields + * @param value See {@link getValue()}. + * @param hasImage See {@link hasImage()}. + */ + public PaymentModeValueDTO(double value, boolean hasImage) { + super(); + this.value = value; + this.hasImage = hasImage; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public PaymentModeValueDTO(Reader reader) throws ParseException { + this.value = reader.readDouble("value"); + this.hasImage = reader.readBoolean("hasImage"); + } + + /** + * Get the amount of money associated to this value. + * @return The value in main currency. + */ + public double getValue() { + return this.value; + } + + /** + * Check whether an image is linked to the object. + * Images are stored separately. + * @return Whether an image is linked to the object. + */ + public boolean hasImage() { + return this.hasImage; + } + } + + /** + *

Payment mode returns are a set of rules to handle excessive amounts. + * The one to use is the one which is above and closest to minAmount. + * It indicates which payment mode to use to give back the excess. + * When no return is set or the excess is under the minimum amount of all + * returns, the excess is kept and registered as exceptional benefit.

+ */ + public class PaymentModeReturnDTO implements Serializable + { + private static final long serialVersionUID = -5076562956398887860L; + + /** {@see getMinAmount()} */ + private double minAmount; + /** {@see getReturnMode()} */ + private int returnMode; + + /** + * Create a return from all fields + * @param minAmount See {@link getMinAmount()}. + * @param returnMode See {@link getReturnMode()}. + */ + public PaymentModeReturnDTO(double minAmount, int returnMode) { + super(); + this.minAmount = minAmount; + this.returnMode = returnMode; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public PaymentModeReturnDTO(Reader reader) throws ParseException { + this.minAmount = reader.readDouble("minAmount"); + this.returnMode = reader.readInt("returnMode"); + } + + /** + * Get the minimum excess amount required for this return mode. + * The maximum amount is the minimum amount of an other return. + * @return The minimum excess amount for this return to be useable. + */ + public double getMinAmount() { + return minAmount; + } + + /** + * Get the id of the payment mode to use for this return. + * @return The id of the payment mode to use. + */ + public int getReturnMode() { + return returnMode; + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/ProductDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/ProductDTO.java new file mode 100644 index 0000000..43f0606 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/ProductDTO.java @@ -0,0 +1,232 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + * Product Data Transfer Object. + */ +public class ProductDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = -6083885399091129846L; + + /** {@see getBarcode()} */ + protected final String barcode; + /** {@see getPriceBuy()} */ + protected final Double priceBuy; + /** {@see getPriceSell()} */ + protected final double priceSell; + /** {@see isScaled()} */ + protected final boolean scaled; + /** {@see getScaleType()} */ + protected final int scaleType; + /** {@see getScaleValue()} */ + protected final double scaleValue; + /** {@see isDiscountEnabled()} */ + protected final boolean discountEnabled; + /** {@see getDiscountRate()} */ + protected final double discountRate; + /** {@see isPrepay()} */ + protected final boolean prepay; + /** {@see getCategory()} */ + protected final int category; + /** {@see getTax()} */ + protected final int tax; + /** {@see getTaxedPrice()} */ + protected final double taxedPrice; + + /** + * Create a products from all fields. + * @param id The product id. + * @param reference The unique reference. + * @param label The display name. + * @param dispOrder The explicit display order within the category. + * @param active If this product should be shown in catalog or not. + * @param hasImage Whether an image is associated to this product. + * @param barcode See {@link getBarcode} + * @param priceBuy See {@link getPriceBuy} + * @param priceSell See {@link getPriceSell} + * @param scaled See {@link isScaled} + * @param scaleType See {@link getScaleType} + * @param scaleValue See {@link getScaleValue} + * @param discountEnabled See {@link isDiscountEnabled} + * @param discountRate See {@link getDiscountRate} + * @param prepay See {@link isPrepay} + * @param category See {@link getCategory} + * @param tax See {@link getTax} + * @param taxedPrice See {@link getTaxedPrice} + */ + public ProductDTO( + Integer id, + String reference, + String label, + int dispOrder, + boolean active, + boolean hasImage, + String barcode, + Double priceBuy, + double priceSell, + boolean scaled, + int scaleType, + double scaleValue, + boolean discountEnabled, + double discountRate, + boolean prepay, + int category, + int tax, + double taxedPrice) { + super(id, reference, label, dispOrder, active, hasImage); + this.barcode = barcode; + this.priceBuy = priceBuy; + this.priceSell = priceSell; + this.scaled = scaled; + this.scaleType = scaleType; + this.scaleValue = scaleValue; + this.discountEnabled = discountEnabled; + this.discountRate = discountRate; + this.prepay = prepay; + this.category = category; + this.tax = tax; + this.taxedPrice = taxedPrice; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public ProductDTO(Reader reader) throws ParseException { + super(reader, "reference", "label", "dispOrder", "visible", "hasImage"); + this.barcode = reader.readString("barcode"); + this.priceBuy = reader.readDoubleOrNull("priceBuy"); + this.priceSell = reader.readDouble("priceSell"); + this.scaled = reader.readBoolean("scaled"); + this.scaleType = reader.readInt("scaleType"); + this.scaleValue = reader.readDouble("scaleValue"); + this.discountEnabled = reader.readBoolean("discountEnabled"); + this.discountRate = reader.readDouble("discountRate"); + this.prepay = reader.readBoolean("prepay"); + this.category = reader.readInt("category"); + this.tax = reader.readInt("tax"); + this.taxedPrice = reader.readDouble("taxedPrice"); + } + + /** + * Get the barcode. It doesn't have to match a specific format. + * @return The barcode. Empty string when not set. + */ + public String getBarcode() { + if (this.barcode == null) { + return ""; + } + return this.barcode; + } + + /** + * Get the unit non-taxed buy price, with decimal precision of 5 digits. + * @return The non-taxed buy price. Can be null. + */ + public Double getPriceBuy() { + return this.priceBuy; + } + + /** + * Get the unit non-taxed sell price, with decimal precision of 5 digits. + * @return The non-taxed sell price. + */ + public double getPriceSell() { + return this.priceSell; + } + + /** + * Whether the product should be sold atomically or not. + * @return True when the quantity should be asked + * every time the product is ordered. + */ + public boolean isScaled() { + return this.scaled; + } + + /** + * Get the scale type. + * @return The scale type code. + * {@see fr.pasteque.common.constants.ScaleType} + */ + public int getScaleType() { + return this.scaleType; + } + + /** + * Get the actual quantity stored in one unit of this product. + * It has no meaning when {@link isScaled} is true and is mostly used + * to compute a reference price for 1 complete unit of its content. + * @return The quantity stored in one unit of this product. + * {@see org.pasteque.common.model.Product#getReferencePrice()} + */ + public double getScaleValue() { + return this.scaleValue; + } + + /** + * Whether the discount should automatically apply. + * @return True when a discount should be applied when ordering this product. + * {@see getDiscountRate()} + */ + public boolean isDiscountEnabled() { + return discountEnabled; + } + + /** + * Get the automatic discount to apply when enabled. + * @return The discount rate to apply when ordering this product. + * {@see isDiscountEnabled()} + */ + public double getDiscountRate() { + return discountRate; + } + + /** + *

Whether this product fills the customer's balance.

+ *

Prepay products should have no tax and must not be counted + * in consolidated sales.

+ * @return True when this product fills the customer's balance. + */ + public boolean isPrepay() { + return prepay; + } + + /** + *

Whether this product is a composition or not.

+ * @return False. + * {@see org.pasteque.common.dto.CompositionDTO} + */ + public boolean isComposition() { + return false; + } + + /** + * Get the id of the category this product fits in. + * @return The id of the category. + */ + public int getCategory() { + return this.category; + } + + /** + * Get the id of the tax to apply to this product. + * @return The id of the tax. + */ + public int getTax() { + return this.tax; + } + + /** + * Get the sell price with tax, with 2 decimal precision. + * @return The sell price with tax. + */ + public double getTaxedPrice() { + return this.taxedPrice; + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/ResourceDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/ResourceDTO.java new file mode 100644 index 0000000..546d38a --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/ResourceDTO.java @@ -0,0 +1,117 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import org.pasteque.common.constants.ResourceType; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.dto.DTOInterface; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint; +import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Resource Transfer Object, previous version of + * {@link org.pasteque.common.datatransfer.dto.OptionDTO} and + * {@link org.pasteque.common.datatransfer.dto.ImageDTO}.

+ *

Resources were used for customization and could hold text/xml as well as + * binary images. A few Resources are still in use but they will be replaced + * by either Options or Images.

+ */ +public class ResourceDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = -7746795788087018857L; + + /** {@see getModel()} */ + private final String label; + /** {@see getReference()} */ + private final int type; + /** {@see getImage()} */ + private final String content; + + /** + * Create an image from all fields. + * @param label See {@link getLabel()}. + * @param type See {@link getType()}. + * @param content See {@link getContent()}. + */ + public ResourceDTO( + String label, + int type, + String content) { + this.label = label; + this.type = type; + this.content = content; + } + + /** + * Read from a raw encoded string. + * @param reader The reader that will parse the data. It must already be reading + * the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}. + * @throws ParseException If the data cannot be parsed or is malformed for + * this DTO. + */ + public ResourceDTO(Reader reader) throws ParseException { + this.label = reader.readString("label"); + this.type = reader.readInt("type"); + this.content = reader.readString("content"); + } + + /** + * Get the name of the resource. It also serves as its identifier. + * {@see org.pasteque.common.constants.ResourceName} + * @return The name of the resource. + */ + public String getLabel() { + return this.label; + } + + /** + * Get the type of the resource, either text, image or raw binary. + * {@see org.pasteque.common.constants.ResourceType} + * @return The type of the content of the resource. + */ + public int getType() { + return this.type; + } + + /** + * Get the content of this resource. Binary data are converted to base64. + * @return The content of the resource, either plain text or base64-encoded + * binary. Empty string if not set. + */ + public String getContent() { + if (this.content == null) { + return ""; + } + return this.content; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + List exceptions = new ArrayList(); + if (this.label == null || "".equals(this.label)) { + exceptions.add(new InvalidFieldException( + IntegrityFieldConstraint.NOT_NULL, + this.getClass().getName(), + "label", + null, + null)); + } + try { + ResourceType.fromCode(this.type); + } catch (IllegalArgumentException e) { + exceptions.add(new InvalidFieldException( + IntegrityFieldConstraint.ENUM_REQUIRED, + this.getClass().getName(), + "type", + this.label, + String.valueOf(this.type))); + } + if (!exceptions.isEmpty()) { + throw new IntegrityExceptions(exceptions); + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/RoleDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/RoleDTO.java new file mode 100644 index 0000000..e50308e --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/RoleDTO.java @@ -0,0 +1,63 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.Reader; +import org.pasteque.coreutil.ImmutableList; + +/** + *

User permission group Data Transfer Object.

+ *

As of Pasteque API version 8, customers don't have a reference and it will always be an empty string, + * nor have a display order.

+ *

Each permission is a string. The permission is granted if it is in the list of permissions + * of its associated role.

+ */ +public class RoleDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = 1835635017595972352L; + + /** {@see getPermissions()} */ + private final ImmutableList permissions; + + /** + * Create a role from all fields. + * @param id The id. + * @param label The display name. + * @param permissions See {@link getPermissions}. + */ + public RoleDTO( + Integer id, + String label, + String[] permissions) { + super(id, "", label, 0, true, false); + this.permissions = new ImmutableList(permissions); + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public RoleDTO(Reader reader) throws ParseException { + super(reader, null, "label", null, null, null); + reader.startArray("permissions"); + String[] permissions = new String[reader.getArraySize()]; + for (int i = 0; i < reader.getArraySize(); i++) { + permissions[i] = reader.readString(i); + } + this.permissions = new ImmutableList(permissions); + reader.endArray(); + } + + /** + * Get the list of permissions granted by this role. + * @return All permissions granted, empty array when not set. + */ + public ImmutableList getPermissions() { + if (this.permissions == null) { + return new ImmutableList(); + } + return this.permissions; + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/TariffAreaDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/TariffAreaDTO.java new file mode 100644 index 0000000..3ed46b0 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/TariffAreaDTO.java @@ -0,0 +1,134 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.Reader; +import org.pasteque.coreutil.ImmutableList; + +/** + *

Tariff Area Data Transfer Object.

+ *

As of Pasteque API version 8, tariff areas cannot have an associated image + * nor can be deactivated.

+ */ +public class TariffAreaDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = -2639559615366931293L; + + /** {@see getSymbol()} */ + private final ImmutableList prices; + + /** + * Create a tariff area from all fields. + * @param id The id. + * @param reference The unique reference. + * @param label The display name. + * @param prices See {@link getPrices}. + */ + public TariffAreaDTO( + Integer id, + String reference, + String label, + TariffAreaPriceDTO[] prices) { + super(id, reference, label, 0, true, false); + this.prices = new ImmutableList(prices); + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public TariffAreaDTO(Reader reader) throws ParseException { + super(reader, "reference", "label", "dispOrder", null, null); + reader.startArray("prices"); + TariffAreaPriceDTO[] prices = new TariffAreaPriceDTO[reader.getArraySize()]; + for (int i = 0; i < reader.getArraySize(); i++) { + reader.startObject(i); + prices[i] = new TariffAreaPriceDTO(reader); + reader.endObject(); + } + reader.endArray(); + this.prices = new ImmutableList(prices); + } + + /** + * Get the list of alternative prices associated to this area. + * @return The list of prices. Empty array when not set. + */ + public ImmutableList getPrices() { + if (this.prices == null) { + return new ImmutableList(); + } + return this.prices; + } + + /** + *

Tariff Area Price Data Transfer Object.

+ *

Tariff areas can modify the base price or the tax to apply to + * a set of products.

+ */ + public class TariffAreaPriceDTO implements Serializable + { + private static final long serialVersionUID = 3384262202026051762L; + + /** See {@link getProduct()}. */ + private int product; + /** See {@link getPrice()}. */ + private Double price; + /** See {@link getTax()}. */ + private Integer tax; + + /** + * Create a tariff area price from all fields. + * @param product The id of the product. + * @param price The alternative non-taxed price, null to keep + * the original price. + * @param tax The id of the alternative tax, null to keep + * the original one. + */ + public TariffAreaPriceDTO(int product, Double price, Integer tax) { + this.product = product; + this.price = price; + this.tax = tax; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public TariffAreaPriceDTO(Reader reader) throws ParseException { + this.product = reader.readInt("product"); + this.price = reader.readDoubleOrNull("price"); + this.tax = reader.readIntOrNull("tax"); + } + + /** + * Get the id of the product this price applies to. + * @return The id of the product. + */ + public int getProduct() { + return this.product; + } + + /** + * Get the new non-taxed price for this product. + * @return The alternative non-taxed price for this product in this area. + * Null when the price is not changed. + */ + public Double getPrice() { + return this.price; + } + + /** + * Get the id of the new tax for this product. + * @return The alternative tax id for this product in this area. + * Null when the tax is not changed. + */ + public Integer getTax() { + return this.tax; + } + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/TaxDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/TaxDTO.java new file mode 100644 index 0000000..93e3959 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/TaxDTO.java @@ -0,0 +1,71 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Tax Data Transfer Object.

+ *

As of Pasteque API version 8, taxes cannot have a reference, + * nor display order nor image and cannot be deactivated.

+ */ +public class TaxDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = 8232927221600250003L; + + /** See {@link getTaxRate()}. */ + private final double taxRate; + + /** + * Create a tax from all fields. + * @param id The id. + * @param label The display name, it is used as reference. + * @param taxRate See {@link getTaxRate()}. + */ + public TaxDTO( + Integer id, + String label, + double taxRate) { + super(id, "", label, 0, true, false); + this.taxRate = taxRate; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public TaxDTO(Reader reader) throws ParseException { + super(reader, null, "label", null, null, null); + this.taxRate = Double.valueOf(reader.readDouble("rate")); + } + + /** + * Get the label. Tax has no reference and will use + * its label as one. + * @return The label. + */ + @Override + public String getReference() { + return this.label; + } + + /** + * Get the tax rate to apply. The rate should be between 0.0 and 1.0 + * but it is not checked. + * @return The tax rate to apply. + */ + public double getTaxRate() { + return this.taxRate; + } + + @Override + protected List commonIntegrityCheck() { + // Tax has no reference to check. + return new ArrayList(); + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/UserDTO.java b/src/main/java/org/pasteque/common/datatransfer/dto/UserDTO.java new file mode 100644 index 0000000..2409055 --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/UserDTO.java @@ -0,0 +1,88 @@ +package org.pasteque.common.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Cash register user (operator) Data Transfer Object.

+ *

As of Pasteque API version 8, users don't have a reference and it will always be an empty string, + * nor have a display order.

+ */ +public class UserDTO extends CommonDTO implements Serializable +{ + private static final long serialVersionUID = 5162729826784894378L; + + /** {@see getRole()} */ + private final int role; + /** {@see getCard()} */ + private final String card; + /** {@see getPassword()} */ + private final String password; + + /** + * Create a user from all fields. + * @param id The id. + * @param label The user's display name. + * @param card See {@link getCard()}. + * @param password See {@link getPassword()}. + * @param active See {@link isActive()}. + * @param hasImage See {@link hasImage()}. + * @param role See {@link getRole()}. + */ + public UserDTO( + Integer id, + String label, + String card, + String password, + boolean active, + boolean hasImage, + int role) { + super(id, "", label, 0, active, hasImage); + this.card = card; + this.password = password; + this.role = role; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public UserDTO(Reader reader) throws ParseException { + super(reader, null, "name", null, "active", "hasImage"); + this.card = reader.readStringOrEmpty("card"); + this.password = reader.readStringOrNull("password"); + this.role = reader.readInt("role"); + } + + /** + * Optional card number or text. It doesn't have any constraint. + * @return The user's card number/string. Empty string when not set. + */ + public String getCard() { + if (this.card == null) { + return ""; + } + return this.card; + } + + /** + * Get user's password. See {@link org.pasteque.common.util.HashCypher} + * for the different formats. + * @return The user's password, null or an empty string when not set, + * a plain password or a hashed password with a prefix. + */ + public String getPassword() { + return this.password; + } + + /** + * Get the id of the role assigned to this user (required). + * @return The role id. + */ + public int getRole() { + return this.role; + } +} diff --git a/src/main/java/org/pasteque/common/datatransfer/dto/package-info.java b/src/main/java/org/pasteque/common/datatransfer/dto/package-info.java new file mode 100644 index 0000000..a36776d --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/dto/package-info.java @@ -0,0 +1,12 @@ +/** + *

Data Transfer Objects, read-only data for storage and transmission.

+ * + *

DTO are non-mutable and without behaviour objects. Use DTO to store data + * locally or transmit them.

+ * + *

DTO only have simple data types, like primitives or String to ease + * storage and transmission. The actual models may enforce more restriction + * like the decimal precision. DTO are not meant to be created manually nor + * even manipulated. Use the actual model for these purposes.

+ */ +package org.pasteque.common.datatransfer.dto; diff --git a/src/main/java/org/pasteque/common/datatransfer/package-info.java b/src/main/java/org/pasteque/common/datatransfer/package-info.java new file mode 100644 index 0000000..7e0d2be --- /dev/null +++ b/src/main/java/org/pasteque/common/datatransfer/package-info.java @@ -0,0 +1,4 @@ +/** + * DTO and utilities to read and write data in text format. + */ +package org.pasteque.common.datatransfer; \ No newline at end of file diff --git a/src/main/java/org/pasteque/common/model/Category.java b/src/main/java/org/pasteque/common/model/Category.java new file mode 100644 index 0000000..cb598fe --- /dev/null +++ b/src/main/java/org/pasteque/common/model/Category.java @@ -0,0 +1,80 @@ +package org.pasteque.common.model; + +import java.io.IOException; + +import org.pasteque.common.constants.ModelName; +import org.pasteque.common.datasource.ImageDataSource; +import org.pasteque.common.view.Buttonable; + +/** + *

A single category inside a category tree.

+ *

The parent is not defined, use a + * {@link org.pasteque.common.model.CategoryTree} to navigate between parents + * and children.

+ */ +public class Category implements Buttonable +{ + private String reference; + private String name; + private boolean hasImage; + private Image imageCache; + + /** + * Create a category from all fields. + * @param reference The reference. + * @param name The display name. + * @param hasImage If a custom image is associated. + */ + public Category(String reference, String name, boolean hasImage) { + this.reference = reference; + this.name = name; + this.hasImage = hasImage; + this.imageCache = new Image( + ModelName.CATEGORY, + this.reference, + this.hasImage); + } + + /** + * Get the reference. + * @return The reference. + */ + public String getReference() { + return reference; + } + + /** + * Get the display name. + * @return The display name. + */ + public String getName() { + return name; + } + + @Override // from Buttonable + public String getLabel() { + return this.name; + } + + /** + * Check if this category has a custom image. + * @return True when a custom image is set. + * {@see getImage(ImageDataSource)} + */ + public boolean hasImage() { + return this.hasImage; + } + + /** + *

Get the image to use to show this category.

+ * @param source The source to read the image from. + * @return The image to use. Either the custom one when set or the + * default one otherwise. + * {@see org.pasteque.common.constants.DefaultImages} + * {@see org.pasteque.common.model.Image} + * @throws IOException When an error occurs while reading the source. + */ + public byte[] getImage(ImageDataSource source) throws IOException { + return this.imageCache.getImage(source); + } +} diff --git a/src/main/java/org/pasteque/common/model/CategoryTree.java b/src/main/java/org/pasteque/common/model/CategoryTree.java new file mode 100644 index 0000000..5d53ab6 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/CategoryTree.java @@ -0,0 +1,105 @@ +package org.pasteque.common.model; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import org.pasteque.common.datasource.CategoryDataSource; + +/** + * Incomplete path to browse the category tree. + */ +public class CategoryTree +{ + /** Current path, empty at root. */ + private List path; + /** Cached children categories at the end of the current path. */ + private List children; + /** The source to read children from. */ + private CategoryDataSource source; + + /** + * Create a CategoryTree from the root. + * @param source The source to read top categories from. + * @throws IOException When an error occurs while reading + * the top categories from the source. + */ + public CategoryTree(CategoryDataSource source) throws IOException { + this.path = new LinkedList(); + this.source = source; + this.children = this.source.getTopCategories(); + } + + /** + *

Go deeper in the tree and update the current state.

+ *

Children categories will be loaded from the source.

+ * @param child The child category to browse. It is not checked if this + * category is a child of the current tree. + * @throws IOException When an error occurs while reading the source. + */ + public void browseChild(Category child) throws IOException { + this.children = this.source.getSubCategories(child); + this.path.add(child); + } + + /** + *

Move up in the tree and update the current state.

+ *

Children categories will be loaded from the source. + * Does nothing if already at the root.

+ * @throws IOException When an error occurs while reading the source. + */ + public void browseParent() throws IOException { + if (this.path.size() == 0) { + return; + } + if (path.size() == 1) { + this.children = this.source.getTopCategories(); + } else { + this.children = this.source.getSubCategories(path.get(path.size() - 1)); + } + this.path.remove(this.path.size() - 1); + } + + /** + * Get the current category. + * @return The category for which parent and children refers to. + * Null if no category is currently selected (at root). + * {@see isAtRoot()} + */ + public Category getCurrentCategory() { + if (path.size() == 0) { + return null; + } + return this.path.get(this.path.size() - 1); + } + + /** + * Get children categories. + * @return The list of categories that are children of the current one. + */ + public List getSubcategories() { + return this.children; + } + + /** + *

Check if the current category has a parent in the tree.

+ *

Use this method when there is no dedicated state for when + * no category is selected.

+ * @return True if the current path has more than one element. + * {@see isAtRoot()} + */ + public boolean hasParent() { + return this.path.size() > 1; + } + + /** + *

Check if a category is selected in the tree.

+ *

Use this method when there is a dedicated state to show only + * the categories at the top of the tree.

+ * @return True if no category is selected. Children will all + * have no parent in that case. + * {@see hasParent} + */ + public boolean isAtRoot() { + return this.path.size() == 0; + } +} diff --git a/src/main/java/org/pasteque/common/model/Currency.java b/src/main/java/org/pasteque/common/model/Currency.java new file mode 100644 index 0000000..6b220fc --- /dev/null +++ b/src/main/java/org/pasteque/common/model/Currency.java @@ -0,0 +1,146 @@ +package org.pasteque.common.model; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import org.pasteque.common.datatransfer.dto.CurrencyDTO; +import org.pasteque.coreutil.price.Price2; + +/** + * Currency format and conversion. + */ +public class Currency +{ + /** {@see getName()} */ + private String name; + /** {@see getSymbol()} */ + private String symbol; + /** {@see getDecimalSeparator()} */ + private String decimalSeparator; + /** {@see getThousandsSeparator()} */ + private String thousandsSeparator; + /** {@see getFormat()} */ + private String format; + /** {@see getRate()} */ + private double rate; + /** {@see isMain()} */ + private boolean main; + /** {@see isActive()} */ + private boolean active; + /** + * Cached formatter to print values in the format of this currency. + * Set within the constructor. + */ + private DecimalFormat formatter; + + /** + * Create a currency from transmitted data. + * @param currencyDTO The currency data. + */ + public Currency(CurrencyDTO currencyDTO) { + this.name = currencyDTO.getLabel(); + this.symbol = currencyDTO.getSymbol(); + this.decimalSeparator = currencyDTO.getDecimalSeparator(); + this.thousandsSeparator = currencyDTO.getThousandsSeparator(); + this.format = currencyDTO.getFormat(); + this.rate = currencyDTO.getRate(); + this.main = currencyDTO.isMain(); + this.active = currencyDTO.isActive(); + // Create the formatter + String format = this.format.replace("$", "¤"); + DecimalFormatSymbols symbols = new DecimalFormatSymbols(); + if (this.decimalSeparator != null && !"".equals(this.decimalSeparator)) { + symbols.setDecimalSeparator(this.decimalSeparator.charAt(0)); + symbols.setMonetaryDecimalSeparator(this.decimalSeparator.charAt(0)); + } else { + symbols.setDecimalSeparator('.'); + symbols.setMonetaryDecimalSeparator('.'); + } + if (this.thousandsSeparator != null && !"".equals(this.thousandsSeparator)) { + symbols.setGroupingSeparator(this.thousandsSeparator.charAt(0)); + } else { + symbols.setGroupingSeparator(' '); + } + if (this.symbol != null) { + symbols.setCurrencySymbol(this.symbol); + } + this.formatter = new DecimalFormat(format, symbols); + } + + /** + * Convert a value from this currency to the main currency. + * @param amount The value in this currency. + * @return The value in the main currency. + */ + public Price2 convertToMain(double amount) { + if (this.main) { + return new Price2(amount); + } else { + return new Price2(amount * this.rate); + } + } + + /** + * Convert a value from the main currency to this currency. + * @param amount The value in the main currency. + * @return The value in this currency. + */ + public Price2 convertFromMain(double amount) { + if (this.main) { + return new Price2(amount); + } else { + return new Price2(amount / this.rate); + } + } + + /** + * Get the string representation of a value. + * @param amount The amount to format. + * @param digits The number of decimals to display. + * @return The formatted string, using format, symbol and separators. + */ + public String formatValue(double amount, int digits) { + this.formatter.setMinimumFractionDigits(digits); + this.formatter.setMaximumFractionDigits(digits); + return this.formatter.format(amount); + } + + /** + * Generic getter. + * @return The name. + */ + public String getName() { + return name; + } + + /** + * Generic getter. + * @return The symbol. + */ + public String getSymbol() { + return symbol; + } + + /** + * Generic getter. + * @return The conversion rate. + */ + public double getRate() { + return rate; + } + + /** + * Generic getter. + * @return If this currency is the main one. + */ + public boolean isMain() { + return main; + } + + /** + * Generic getter. + * @return If this currency is active. + */ + public boolean isActive() { + return active; + } +} diff --git a/src/main/java/org/pasteque/common/model/Customer.java b/src/main/java/org/pasteque/common/model/Customer.java new file mode 100644 index 0000000..2e4b281 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/Customer.java @@ -0,0 +1,152 @@ +package org.pasteque.common.model; + +import java.util.Date; + +import org.pasteque.coreutil.price.Price2; + +/** + * Customer account. + */ +public class Customer +{ + + /*public static String getContactFieldName(CustomerContactField field, Option customFields) { + + }*/ + /** + * Nullable internal identifier. This field will be deprecated once + * the reference is used to link records. + */ + private Integer id; + /** + * Whether the record has an image that can be retrieved from elsewhere. + * When the model cannot have an image, it must return false. + */ + private boolean hasImage; + + /** {@see getCard()} */ + private String card; + /** {@see getMaxDebt()} */ + private Price2 maxDebt; + /** {@see getBalance()} */ + private Price2 balance; + /** {@see getExpireDate()} */ + private Date expireDate; + /** {@see getDiscountProfile()} */ + private Integer discountProfile; + /** {@see getTariffArea()} */ + private Integer tariffArea; + /** {@see getTax()} */ + private Integer tax; + + // Contact fields + // These fields may be moved away to ease compatibility with CRM softwares. + /** {@see getFirstName()} */ + private String firstName; + /** {@see getLastName()} */ + private String lastName; + /** {@see getEmail()} */ + private String email; + /** {@see getPhone1()} */ + private String phone1; + /** {@see getPhone2()} */ + private String phone2; + /** {@see getFax()} */ + private String fax; + /** {@see getAddr1()} */ + private String addr1; + /** {@see getAddr2()} */ + private String addr2; + /** {@see getZipCode()} */ + private String zipCode; + /** {@see getCity()} */ + private String city; + /** {@see getRegion()} */ + private String region; + /** {@see getCountry()} */ + private String country; + /** {@see getNote()} */ + private String note; + + /** + * Create an empty customer account. + */ + public Customer() { + this.card = ""; + this.maxDebt = new Price2(0.00); + this.balance = new Price2(0.00); + this.firstName = ""; + this.lastName = ""; + this.email = ""; + this.phone1 = ""; + this.phone2 = ""; + this.fax = ""; + this.addr1 = ""; + this.addr2 = ""; + this.zipCode = ""; + this.city = ""; + this.region = ""; + this.country = ""; + this.note = ""; + } + + /** + * Create an account from from transmitted data. + * @param customerDTO The customer account data. + * @param dpDTO The associated discount profile data. Set null if none. + * @param taDTO The associated tariff area data. + * @param taxDTO The associated alternative tax data. + * @throws AssociationInconsistencyException + * When either the discount profile, tariff area or tax don't match the + * data in the customer account. + */ +/* public Customer(CustomerDTO customerDTO, DiscountProfileDTO dpDTO, + TariffAreaDTO taDTO, TaxDTO taxDTO) throws AssociationInconsistencyException { + this.id = customerDTO.getId(); + this.name = customerDTO.getName(); + this.card = customerDTO.getCard(); + this.maxDebt = new Price2(customerDTO.getMaxDebt()); + this.balance = new Price2(customerDTO.getBalance()); + if (customerDTO.getExpireDate() != null) { + this.expireDate = new Date(customerDTO.getExpireDate()); + } + this.firstName = customerDTO.getFirstName(); + this.lastName = customerDTO.getLastName(); + this.email = customerDTO.getEmail(); + this.phone1 = customerDTO.getPhone1(); + this.phone2 = customerDTO.getPhone2(); + this.fax = customerDTO.getFax(); + this.addr1 = customerDTO.getAddr1(); + this.addr2 = customerDTO.getAddr2(); + this.zipCode = customerDTO.getZipCode(); + this.city = customerDTO.getCity(); + this.region = customerDTO.getRegion(); + this.country = customerDTO.getCountry(); + this.note = customerDTO.getNote(); + // Link checks + Integer discountProfile = customerDTO.getDiscountProfile(); + AssociationInconsistencyException.autoThrow( + "discountProfile", customerDTO.getDiscountProfile(), + "id", (dpDTO == null) ? null : dpDTO.getId() + ); + + this.discountProfile = (dpDTO == null) ? null : new DiscountProfile(dpDTO); + Integer tariffArea = customerDTO.getTariffArea(); + AssociationInconsistencyException.autoThrow( + "tariffArea", tariffArea, + "id", (taDTO == null) ? null : taDTO.getId() + ); + this.tariffArea = (taDTO == null) ? null : new TariffArea(taDTO); + Integer tax = customerDTO.getTay(); + AssociationInconsistencyException.autoThrow( + "tax", tariffArea, + "id", (taxDTO == null) ? null : taxDTO.getId() + ); + this.tax = (taxDTO == null) ? null : new Tax(taxDTO); + }*/ + + /*@Override + public String toString() { + return this.name; + }*/ +} diff --git a/src/main/java/org/pasteque/common/model/Image.java b/src/main/java/org/pasteque/common/model/Image.java new file mode 100644 index 0000000..0f06157 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/Image.java @@ -0,0 +1,58 @@ +package org.pasteque.common.model; + +import java.io.IOException; +import org.pasteque.common.datasource.ImageDataSource; +import org.pasteque.common.datatransfer.dto.ImageDTO; +import org.pasteque.common.constants.DefaultImages; +import org.pasteque.common.constants.ModelName; + +/** + * Helper class to manage image caching and loading. + */ +public class Image +{ + private ModelName model; + private String reference; + private boolean hasImage; + private byte[] imageCache; + + /** + * Create an Image from all fields. + * @param model The type of model for this image. + * @param reference The reference of the record. + * @param hasImage If a custom image is associated to the record. + */ + public Image( + ModelName model, + String reference, + boolean hasImage) { + this.model = model; + this.reference = reference; + this.hasImage = hasImage; + } + + /** + *

Get the image to use.

+ *

When a custom image is set, it is first loaded from the source + * and then cached locally. The default image is never cached.

+ * @param source The source to read the image from, when it is not + * already loaded. + * @return The image to use. Either the custom one when set or the + * default one otherwise. + * @throws IOException When an error occurs while reading the source. + * {@see org.pasteque.common.constants.DefaultImages} + */ + public byte[] getImage(ImageDataSource source) throws IOException { + if (!this.hasImage) { + return DefaultImages.getDefaultImage(this.model); + } + if (this.imageCache != null) { + return this.imageCache; + } + ImageDTO imageDTO = source.getImage(this.model, this.reference); + if (imageDTO != null) { + this.imageCache = imageDTO.getImage(); + } + return this.imageCache; + } +} diff --git a/src/main/java/org/pasteque/common/model/User.java b/src/main/java/org/pasteque/common/model/User.java new file mode 100644 index 0000000..b19a17a --- /dev/null +++ b/src/main/java/org/pasteque/common/model/User.java @@ -0,0 +1,124 @@ +package org.pasteque.common.model; + +import java.util.HashSet; +import java.util.Set; +import org.pasteque.common.datasource.AssociationInconsistencyException; +import org.pasteque.common.datasource.ImageDataSource; +import org.pasteque.common.datatransfer.dto.UserDTO; +import org.pasteque.common.datatransfer.dto.RoleDTO; +import org.pasteque.common.util.HashCypher; + +/** + * Operator of a cash register. + */ +public class User +{ + /** {@see getId()} */ + private Integer id; + /** {@see getName()} */ + private String name; + /** {@see getCard()} */ + private String card; + /** {@see getPassword()} */ + private String password; + /** {@see hasPermission(String)} */ + private Set permissions; + /** {@see isVisible()} */ + private boolean visible; + /** {@see hasImage()} */ + private boolean hasImage; + /** {@see getImage()} */ + private byte[] image; + + /** + * Create an empty user. + */ + public User() { + this.init(); + this.visible = true; + } + + /** + * Create a user from transmitted data. + * @param userDTO The user data. + * @param roleDTO The user's role data. + * @throws AssociationInconsistencyException + * When the role isn't assigned to the user. + */ + public User(UserDTO userDTO, RoleDTO roleDTO) throws AssociationInconsistencyException { + this.init(); + this.id = userDTO.getId(); + this.name = userDTO.getLabel(); + this.card = userDTO.getCard(); + this.password = userDTO.getPassword(); + this.visible = userDTO.isActive(); + this.hasImage = userDTO.hasImage(); + if (!java.util.Objects.equals(userDTO.getRole(), roleDTO.getId())) { + throw new AssociationInconsistencyException( + "role", String.valueOf(userDTO.getRole()), + "id", String.valueOf(roleDTO.getId()) + ); + } + for (String p : roleDTO.getPermissions()) { + this.permissions.add(p); + } + } + + /** + * Common initialization. + */ + private void init() { + this.permissions = new HashSet(); + } + + /** + * Check if this user has the given permission. + * @param permission The permission to check. + * @return True if the user has the given permission within it's role. + */ + public boolean hasPermission(String permission) { + return this.permissions.contains(permission); + } + + /** + *

Get the raw image data associated to this user.

+ *

If {@link hasImage} is false, null is returned without checking + * the source. The image is cached within this user once loaded.

+ * @param source The source to read the image from when not already cached. + * @return The user's image. + */ + public byte[] getImage(ImageDataSource source) { + if (!this.hasImage) { + return null; + } + if (this.image != null) { + return this.image; + } else { + //this.image = source.getImage(ModelName.USER, this.id); + return this.image; + } + } + + /** + * Try to authenticate without a password. + * @return True when the user can authenticate, false otherwise. + */ + public boolean authenticate() { + return this.password == null || this.password.equals("") + || this.password.startsWith("empty:"); + } + + /** + * Try to authenticate with a password. + * @param password The clear password provided to authenticate. + * @return True when the user can authenticate, false otherwise. + */ + public boolean authenticate(String password) { + return HashCypher.authenticate(password, this.password); + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/src/main/java/org/pasteque/common/model/option/AccountingConfigOption.java b/src/main/java/org/pasteque/common/model/option/AccountingConfigOption.java new file mode 100644 index 0000000..ea20132 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/option/AccountingConfigOption.java @@ -0,0 +1,197 @@ +package org.pasteque.common.model.option; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.pasteque.common.constants.OptionName; +import org.pasteque.common.datatransfer.dto.OptionDTO; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.JSONReader; + +/** + *

Option to configure the transmission of sales to the accounting.

+ */ +public class AccountingConfigOption +{ + /** + * Name of the option that holds the accounting configuration. + */ + public static final OptionName NAME = OptionName.ACCOUNTING_CONFIG; + + /** @See {@link getSaleAccount(String). */ + private Map saleAccounts; + /** @See {@link getTaxAccount(String). */ + private Map taxAccounts; + /** @See {@link getPaymentModeAccount(String). */ + private Map paymentModeAccounts; + /** @See {@link getCustomerAccount(String). */ + private Map customerAccounts; + /** + * Misc. accounts. @See {@link getExtraAccount(String). + */ + private Map extraAccounts; + + /** + * Create an accounting configuration from an OptionDTO. + * @param option The option to read. + * @throws IllegalArgumentException If the option is not an accounting + * configuration one. See constants. + * @throws ParseException When an error occurs while reading the content + * of the Option. + */ + public AccountingConfigOption(OptionDTO option) throws ParseException { + this.saleAccounts = new HashMap(); + this.taxAccounts = new HashMap(); + this.paymentModeAccounts = new HashMap(); + this.customerAccounts = new HashMap(); + this.extraAccounts = new HashMap(); + String json = option.getContent(); + JSONReader reader = new JSONReader(json); + reader.startObject(); + this.addKeys(reader, "sales", this.saleAccounts); + this.addKeys(reader, "taxes", this.taxAccounts); + this.addKeys(reader, "paymentModes", this.paymentModeAccounts); + this.addKeys(reader, "customers", this.customerAccounts); + this.addKeys(reader, "extra", this.extraAccounts); + reader.endObject(); + } + + /** + * Fill a map of accounts from the reader. + * @param reader The reader to read accounts from. Its state is not + * modified when returning or throwing an exception. + * @param object The name of the collection in the reader to read values from. + * @param to The map to fill. + * @throws ParseException When a parsing error occurs. + */ + private void addKeys(JSONReader reader, String object, Map to) + throws ParseException { + reader.startObject(object); + try { + for (String ref : reader.listKeys()) { + to.put(ref, reader.readString(ref)); + } + } catch (ParseException e) { + reader.endObject(); + throw e; + } + reader.endObject(); + } + + /** + * Get the account to use for sales of the given category. + * @param categoryRef The reference of the category. + * @return The name of the account, null when not defined. + */ + public String getSaleAccount(String categoryRef) { + return this.saleAccounts.get(categoryRef); + } + + /** + * Get the account to use for collected taxes of the given tax. + * @param taxRef The reference of the tax. + * @return The name of the account, null when not defined. + */ + public String getTaxAccount(String taxRef) { + return this.taxAccounts.get(taxRef); + } + + /** + * Get the account to use for payments of the given mode. + * @param paymentModeRef The reference of the payment mode. + * @return The name of the account, null when not defined. + */ + public String getPaymentModeAccount(String paymentModeRef) { + return this.paymentModeAccounts.get(paymentModeRef); + } + + /** + * Get the account to use for the balance of the given customer. + * @param customerRef The reference of the customer. + * @return The name of the account, null when not defined. + */ + public String getCustomerAccount(String customerRef) { + return this.customerAccounts.get(customerRef); + } + + /** + * Get the account to use for extra credits. + * @return The name of the account, null when not defiled. + */ + public String getExtraCreditAccount() { + return this.extraAccounts.get("extraCredit"); + } + + /** + * Get the account to use for extra debits. + * @return The name of the account, null when not defiled. + */ + public String getExtraDebitAccount() { + return this.extraAccounts.get("extraDebit"); + } + + /** + * Get a generic extra account. + * @param ref The reference for the extra account. + * @return The name of the account, null when not defiled. + */ + public String getExtraAccount(String ref) { + return this.extraAccounts.get(ref); + } + + /** + * Same as below without extra references. Extra credit and extra debits + * are still checked. + * @param categoryRefs The list of reference of category to check. + * @param taxRefs The list of reference of tax to check. + * @param paymentModeRefs The list of reference of payment mode to check. + * @param customerRefs The list of reference of customer to check. + * @return True when an account is set for each reference. False otherwise. + */ + public boolean isExhaustive( + Collection categoryRefs, + Collection taxRefs, + Collection paymentModeRefs, + Collection customerRefs) { + return this.isExhaustive(categoryRefs, taxRefs, paymentModeRefs, customerRefs, null); + } + + /** + * Check if the configuration contains account values for everything. + * @param categoryRefs The list of reference of category to check. + * @param taxRefs The list of reference of tax to check. + * @param paymentModeRefs The list of reference of payment mode to check. + * @param customerRefs The list of reference of customer to check. + * @param extraRefs The list of extra reference to check. Extra + * credit and extra debits are always checked and can be omitted from + * this collection. Can be null. + * @return True when an account is set for each reference. False otherwise. + */ + public boolean isExhaustive( + Collection categoryRefs, + Collection taxRefs, + Collection paymentModeRefs, + Collection customerRefs, + Collection extraRefs) { + if (!this.saleAccounts.keySet().containsAll(categoryRefs)) { + return false; + } + if (!this.taxAccounts.keySet().containsAll(taxRefs)) { + return false; + } + if (!this.paymentModeAccounts.keySet().containsAll(paymentModeRefs)) { + return false; + } + if (!this.customerAccounts.keySet().containsAll(customerRefs)) { + return false; + } + if (!this.extraAccounts.containsKey("extraCredit") + || !this.extraAccounts.containsKey("extraDebit")) { + return false; + } + if (extraRefs != null && !this.extraAccounts.keySet().containsAll(extraRefs)) { + return false; + } + return true; + } +} diff --git a/src/main/java/org/pasteque/common/model/option/CustomContactFieldsOption.java b/src/main/java/org/pasteque/common/model/option/CustomContactFieldsOption.java new file mode 100644 index 0000000..b40cda7 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/option/CustomContactFieldsOption.java @@ -0,0 +1,64 @@ +package org.pasteque.common.model.option; + +import java.util.HashMap; +import java.util.Map; +import org.pasteque.common.constants.CustomerContactField; +import org.pasteque.common.constants.OptionName; +import org.pasteque.common.datatransfer.dto.OptionDTO; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.parser.JSONReader; + +/** + *

Option to rename the contact fields of all customer's account.

+ *

In version 8 contact fields are static and most of them are unused. + * This option allows to recycle those fields for other purposes.

+ */ +public class CustomContactFieldsOption +{ + /** + * Name of the option that holds contact field replacements. + */ + public static final OptionName NAME = OptionName.CUSTOMER_CONTACT_FIELDS; + + /** See {@link getAltLabel(String). */ + private Map replacements; + + /** + * Create a contact field replacement option from an OptionDTO. + * @param option The option to read. + * @throws IllegalArgumentException If the option is not a replacement + * one. See constants. + * @throws ParseException When an error occurs while reading the content + * of the Option. + */ + public CustomContactFieldsOption(OptionDTO option) throws ParseException { + this.replacements = new HashMap(); + String json = option.getContent(); + JSONReader reader = new JSONReader(json); + reader.startObject(); + for (CustomerContactField id : CustomerContactField.values()) { + if (reader.hasKey(id.getCode())) { + this.replacements.put(id, reader.readString(id.getCode())); + } + } + } + + /** + * Check whether this Option will replace the default label + * for the given contact field. + * @param field The field to check for replacement. + * @return True if an alternative label is set for that field. + */ + public boolean hasAltLabel(CustomerContactField field) { + return this.replacements.containsKey(field); + } + + /** + * Get the alternative label for the given field. It is not translatable. + * @param field The field to replace. + * @return The alternative label. + */ + public String getAltLabel(CustomerContactField field) { + return this.replacements.get(field); + } +} diff --git a/src/main/java/org/pasteque/common/model/option/package-info.java b/src/main/java/org/pasteque/common/model/option/package-info.java new file mode 100644 index 0000000..449ff44 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/option/package-info.java @@ -0,0 +1,4 @@ +/** + * Implementations of extra settings for ease of use. + */ +package org.pasteque.common.model.option; diff --git a/src/main/java/org/pasteque/common/model/order/ComputedPrices.java b/src/main/java/org/pasteque/common/model/order/ComputedPrices.java new file mode 100644 index 0000000..93cd188 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/order/ComputedPrices.java @@ -0,0 +1,170 @@ +package org.pasteque.common.model.order; + +import java.util.ArrayList; +import java.util.List; +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.price.Price; +import org.pasteque.coreutil.price.Price5; +import org.pasteque.coreutil.price.TaxAmount; + +/** + *

A group of prices to return prices of a line from a + * {@link PriceRound}.

+ *

This class has a limited scope to be used only from a + * {@link PriceRound} and apply the prices to a {@link ProductLine}.

+ */ +/* package */ final class ComputedPrices +{ + /** See {@link getUnitPrice()}. */ + private Price basePrice; + /** See {@link getUnitTaxedPrice()}. */ + private Price baseTaxedPrice; + /** See {@link getDiscountPrice()}. */ + private Price discountPrice; + /** See {@link getDiscountTaxedPrice()}. */ + private Price discountTaxedPrice; + /** See {@link getTaxes()}. */ + private ImmutableList taxes; + + /** + * Create a price set of 0. + * The prices are set as {@link org.pasteque.coreutil.price.Price5}. + */ + public ComputedPrices() { + this.basePrice = new Price5(0.0); + this.baseTaxedPrice = new Price5(0.0); + this.discountPrice = new Price5(0.0); + this.discountTaxedPrice = new Price5(0.0); + this.taxes = new ImmutableList(); + } + + /** + * Create prices from all fields. + * @param basePrice See {@link getBasePrice()}. + * @param baseTaxedPrice See {@link getBaseTaxedPrice()}. + * @param discountPrice See {@link getDiscountPrice()}. + * @param discountTaxedPrice See {@link getDiscountTaxedPrice()}. + * @param taxes See {@link getTaxes()}. + */ + public ComputedPrices( + Price basePrice, + Price baseTaxedPrice, + Price discountPrice, + Price discountTaxedPrice, + ImmutableList taxes) { + this.basePrice = basePrice; + this.baseTaxedPrice = basePrice; + this.discountPrice = discountPrice; + this.discountTaxedPrice = discountTaxedPrice; + this.taxes = taxes; + } + + /** + * Add the prices from an other set of prices and return the result. + * @param other The other set of prices to add. + * @return A new set of prices with all the prices summed and the + * tax amount summed and/or completed with other taxes. The precision + * of the resulting prices is the lowest of each operand for each price. + */ + public ComputedPrices add(ComputedPrices other) { + List taxSum = new ArrayList(other.getTaxes().size()); + for (TaxAmount amount : this.taxes) { + taxSum.add(amount); + } + for (TaxAmount amount : other.taxes) { + boolean summed = false; + for (int i = 0; i < taxSum.size(); i++) { + TaxAmount iAmount = taxSum.get(i); + try { + TaxAmount sum = iAmount.sum(amount); + taxSum.set(i, sum); + summed = true; + break; + } catch (IllegalArgumentException e) { + continue; // Cannot sum: not the same tax + } + } + if (!summed) { + taxSum.add(amount); + } + } + return new ComputedPrices( + this.basePrice.add(other.getBasePrice()), + this.baseTaxedPrice.add(other.getBaseTaxedPrice()), + this.discountPrice.add(other.getDiscountPrice()), + this.discountTaxedPrice.add(other.getDiscountTaxedPrice()), + new ImmutableList(taxSum)); + } + + /** + * Get the in-quantity price without taxes before applying the discount. + * @return The in-quantity price without taxes before applying the discount. + */ + public Price getBasePrice() { + return this.basePrice; + } + + /** + * Short alias for {@link getBasePrice()}. + * @return The in-quantity price without taxes before applying the discount. + */ + public Price bp() { + return this.basePrice; + } + + /** + * Get the in-quantity price with taxes before applying the discount. + * @return The in-quantity price with taxes before applying the discount. + */ + public Price getBaseTaxedPrice() { + return this.baseTaxedPrice; + } + + /** + * Short alias for {@link getBaseTaxedPrice()}. + * @return The in-quantity price with taxes before applying the discount. + */ + public Price btp() { + return this.baseTaxedPrice; + } + + /** + * Get the in-quantity price without taxes after applying the discount. + * @return The in-quantity price without taxes after applying the discount. + */ + public Price getDiscountPrice() { + return this.discountPrice; + } + + /** + * Short alias for {@link getDiscountPrice()}. + * @return The in-quantity price without taxes after applying the discount. + */ + public Price dp() { + return this.discountPrice; + } + + /** + * Get the in-quantity price with taxes after applying the discount. + * @return The in-quantity price with taxes after applying the discount. + */ + public Price getDiscountTaxedPrice() { + return this.discountTaxedPrice; + } + + /** + * Short alias for {@link getDiscountTaxedPrice()}. + * @return The in-quantity price with taxes after applying the discount. */ + public Price dtp() { + return this.discountTaxedPrice; + } + + /** + * Get the tax amounts after applying the discount. + * There is no method to get the tax amounts before applying the discount + * as they have no meaning and may be inaccurate. + */ + public ImmutableList getTaxes() { + return this.taxes; + } +} diff --git a/src/main/java/org/pasteque/common/model/order/Order.java b/src/main/java/org/pasteque/common/model/order/Order.java new file mode 100644 index 0000000..1402dbc --- /dev/null +++ b/src/main/java/org/pasteque/common/model/order/Order.java @@ -0,0 +1,245 @@ +package org.pasteque.common.model.order; + +import java.util.List; +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.price.Discount; +import org.pasteque.coreutil.price.FinalTaxAmount; +import org.pasteque.coreutil.price.Price2; +import org.pasteque.coreutil.transition.LineTransition; +import org.pasteque.coreutil.transition.OrderTransition; + +/** + *

Basic content of an order. This model defines only the final identification + * of each line and a few meta data affecting the final price.

+ *

A major order can be extended to add more information not related to the + * effective payment. Only a base order can be converted to a + * {@link org.pasteque.major.domain.MajorTicket} by adding payments to it.

+ */ +// TODO WIP, keep OrderManager as a higher-level controller in org.pasteque.common. +public class Order implements OrderTransition +{ + private List lines; + /** See {@see getDiscount()}. */ + private Discount discount; + /** The utility to compute prices and taxes. */ + private OrderPrice orderPrice; + private ImmutableList taxAmounts; + private Price2 finalPrice; + private Price2 finalTaxedPrice; + // TODO add customer reference for invoices? + /* TODO Map ExtraData, JSONlike object|array to pass to DTO */ + + /** + * Create an empty order. + * @param priceRound The method used to compute prices. + */ + public Order(PriceRound priceRound) { + this.orderPrice = new OrderPrice(this, priceRound); + } + + /** + * Recompute prices for the given line. It will reset order prices for + * all lines as the total may be changed. + * @param lineIndex The index of the line to compute. + * @see clearOrderPrices() + * @see computePrices() + */ + public synchronized void recomputeLinePrice(int lineIndex) { + this.orderPrice.computeLocalLinePrice(lineIndex); + this.clearOrderPrices(); + } + + /** + * Recompute all prices. It will compute the price of each line + * including the discount from the order. If a line is not computed + * without the discount, it will be computed first. + * @see recomputeLinePrice(int) + */ + public synchronized void computePrices() { + this.orderPrice.computeGlobalPrices(); + } + + /** + * Clear the total prices of the order and the order prices of each line. + */ + private synchronized void clearOrderPrices() { + for (ProductLine l : this.lines) { + l.setOrderPrices(null); + } + this.finalPrice = null; + this.finalTaxedPrice = null; + this.taxAmounts = null; + } + + /** + * Add a line at the end of the order. It will reset the total prices + * of the order if a discount is set. + * @param line The line to add. + * @see isPriceComputed() + * @see computePrices() + */ + public final synchronized void addLine(ProductLine line) { + this.lines.add(line); + if (this.discount != null) { + this.clearOrderPrices(); + } + } + + /** + * Remove the line at the specified index and shift all subsequent lines + * to fill the gap. It will reset the total prices of the order if a discount + * is set. + * @param lineIndex The index of the line to remove from the order. + * @throws IndexOutOfBoundsException When requesting an index outside + * of the bounds (less than 0 or equal or more than {@link getLineCount()}). + * @see isPriceComputed() + * @see computePrices() + */ + public final synchronized void removeLine(int lineIndex) { + this.lines.remove(lineIndex); + if (this.discount != null) { + this.clearOrderPrices(); + } + } + + /** + * Replace the line at the specified index. It will reset the total prices + * of the order if a discount is set. + * @param lineIndex The index of the line to replace. + * @param updated The new line. + * @throws IndexOutOfBoundsException When requesting an index outside + * of the bounds (less than 0 or equal or more than {@link getLineCount()}). + * @see isPriceComputed() + * @see computePrices() + */ + public final synchronized void replaceLine(int lineIndex, ProductLine updated) { + this.lines.set(lineIndex, updated); + if (this.discount != null) { + this.clearOrderPrices(); + } + } + + /** + * Get the line at the specified index. + * @param index The index of the line. + * @return The line at the specified index. + * @throws IndexOutOfBoundsException When requesting an index outside + * of the bounds (less than 0 or equal or more than {@link getLineCount()}). + */ + public synchronized ProductLine getLine(int index) { + return this.lines.get(index); + } + + /** + * Get the number of lines in this order. + * @return The number of line in this order. + */ + public synchronized int getLineCount() { + return this.lines.size(); + } + + @Override // from OrderTransition + public synchronized ImmutableList getLines() { + LineTransition[] tlines = new LineTransition[this.lines.size()]; + for (int i = 0; i < this.lines.size(); i++) { + tlines[i] = this.lines.get(i); + } + return new ImmutableList(tlines); + } + + /** + * Set the discount applied to the whole order. It will reset the total + * prices of the order. + * @param discount The new discount. It cannot be applied to unit prices. + */ + public final synchronized void setDiscount(Discount discount) { + if ((this.discount == null && discount == null) + || (this.discount != null && this.discount.equals(discount))) { + return; + } + this.discount = discount; + this.clearOrderPrices(); + } + + /** + * The effective discount to apply to the whole price of the order. + * @return The discount. + * @see org.pasteque.coreutil.price.Discount#getEffectiveDiscount(Discount) + */ + public final Discount getDiscount() { + return Discount.getEffectiveDiscount(this.discount); + } + + /** + * Get the discount to apply to the whole price of the order. + * @return The discount, without effective check. + * @see getDiscount() + */ + public final Discount getRawDiscount() { + return this.discount; + } + + /** + * Check whether the prices are set or not. + * @return True when the prices are set. + * @see computePrices() + */ + public boolean isPriceComputed() { + return this.finalPrice != null + && this.taxAmounts != null + && this.finalTaxedPrice != null; + } + + /** + * Get the price without tax of the whole order. + * @return The price without tax of the whole order. + * Null when not computed. + * @see computePrices() + */ + public Price2 getFinalPrice() { + return this.finalPrice; + } + + /** + * Get the amount of taxes of the whole order. + * @return The amount of taxes of the whole order. Null when not computed. + * @see computePrices() + */ + public ImmutableList getTaxAmounts() { + return this.taxAmounts; + } + + /** + * Get the price with taxes of the whole order. + * @return The price with taxes of the whole order. + * Null when not computed. + * @see computePrices() + */ + public Price2 getFinalTaxedPrice() { + return this.finalTaxedPrice; + } + + /** + * Set the prices of the whole order. Limited scope to be called from + * {@link OrderPrice}. + * @param finalPrice See {@link getFinalPrice()}. + * @param taxAmounts See {@link getTaxAmounts()}. + * @param finalTaxedPrice See {@link getFinalTaxedPrice()}. + */ + /* package */ void setOrderPrices( + Price2 finalPrice, + ImmutableList taxAmounts, + Price2 finalTaxedPrice) { + this.finalPrice = finalPrice; + this.taxAmounts = taxAmounts; + this.finalTaxedPrice = finalTaxedPrice; + } + + /** + * Get the method used to compute prices. + * @return The method used to compute prices for this order. + */ + public final PriceRound getPriceRound() { + return this.orderPrice.getPriceRound(); + } +} diff --git a/src/main/java/org/pasteque/common/model/order/OrderPrice.java b/src/main/java/org/pasteque/common/model/order/OrderPrice.java new file mode 100644 index 0000000..ce4d622 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/order/OrderPrice.java @@ -0,0 +1,251 @@ +package org.pasteque.common.model.order; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.constants.DiscountTarget; +import org.pasteque.coreutil.constants.DiscountType; +import org.pasteque.coreutil.price.Discount; +import org.pasteque.coreutil.price.FinalTaxAmount; +import org.pasteque.coreutil.price.Price2; +import org.pasteque.coreutil.price.Price5; +import org.pasteque.coreutil.price.TaxAmount; + +/** + * Price utility. Limited scope to be used by {@link Order} but keep all + * price-related functions into a dedicated class. + */ +// TODO WIP +/* package */ final class OrderPrice +{ + private Order order; + private PriceRound priceRound; + + /** + * Create an empty order. + * @param priceRound The method used to compute prices. + */ + public OrderPrice(Order order, PriceRound priceRound) { + this.order = order; + this.priceRound = priceRound; + } + + /** + * Compute or recompute the price of a line without considering the + * discount from the order. Limited scope to be called from {@link Order}. + */ + /* package */ void computeLocalLinePrice(int lineIndex) { + ProductLine line = this.order.getLine(lineIndex); + this.computeLocalLinePrice(line); + } + + /** + * Compute or recompute the price of a line without considering the + * discount from the order. Use {@link computeGlobalLinePrice(int)} + * to ensure the line is not from an other order. + */ + private void computeLocalLinePrice(ProductLine line) { + ComputedPrices prices = this.priceRound.computeLocalPrices(line); + line.setLocalPrices(prices); + } + + /** + * Sum the local prices from each line. If the prices of a line are not + * computed, they will be computed on the fly. It blocks order changes + * during the computation. + * Limited scope to be called from {@link Order}. + * @return The sum of the local prices of each line. At the end those prices + * are rounded to 2 decimals except tax amounts which are not really relevant. + */ + /* package */ ComputedPrices sumLocalPrices() { + synchronized(this.order) { + if (this.order.getLineCount() == 0) { + return new ComputedPrices( + new Price5(0.0), new Price5(0.0), + new Price5(0.0), new Price5(0.0), + new ImmutableList()); + } + ProductLine line = this.order.getLine(0); + if (!line.isLocalPriceComputed()) { + this.computeLocalLinePrice(line); + } + ComputedPrices result = line.getLocalPrices(); + for (int i = 1; i < this.order.getLineCount(); i++) { + line = this.order.getLine(i); + if (!line.isLocalPriceComputed()) { + this.computeLocalLinePrice(line); + } + result = result.add(this.order.getLine(i).getLocalPrices()); + } + return new ComputedPrices( + new Price2(result.bp()), new Price2(result.btp()), + new Price2(result.dp()), new Price2(result.dtp()), + result.getTaxes()); + } + } + + /** + *

Compute or recompute the price of a line including the discount from + * the order. Use {@link computeGlobalPrices()} as it makes no sense to + * compute only a single line.

+ *

This will spread the discount from the order across all lines. This + * method assumes all local prices are computed.

+ *

Global prices are always set as + * {@link org.pasteque.coreutil.price.Price5}.

+ * @param lineIndex The index of the line from the order. + * @param discountedTotal The base total with the discount from the order. + * @param rate The rate (total / discountedTotal) which correspond to the + * actual discount rate applied to the order. + * @param target The target of the discount applied to the order. + */ + /* package */ void computeOrderLinePrice(int lineIndex, + Price2 discountedTotal, double rate, DiscountTarget target) { + ProductLine line = this.order.getLine(lineIndex); + this.computeOrderLinePrice(line, discountedTotal, rate, target); + } + + /** + * Local function to compute global prices of a line. Use + * {@link computeGlobalLinePrice(int)} to ensure the line is not from + * an other order. + * @param line The line to recompute. It will set its global prices. + * @param discountedTotal The base total with the discount from the order. + * @param rate The rate (total / discountedTotal) which correspond to the + * actual discount rate applied to the order. + * @param target The target of the discount applied to the order. + */ + private void computeOrderLinePrice(ProductLine line, + Price2 discountedTotal, double rate, DiscountTarget target) { + // TODO make sure the sum of global line prices are consistent with the total (see PriceTotalRound) + Price5 discountBasePrice; + Price5 discountedPrice; + ImmutableList taxAmounts; + Price5 discountedTaxedPrice; + Discount discount = new Discount(DiscountType.RATE, target, rate); + switch (target) { + case TOTAL: + discountBasePrice = new Price5(line.getLocalDiscountPrice()); + discountedPrice = discount.applyTo(discountBasePrice); + taxAmounts = PriceNoRound.computeTaxes(line, discountedPrice); + discountedTaxedPrice = PriceNoRound.addTaxes(discountedPrice, taxAmounts); + break; + case TOTAL_TAXED: + discountBasePrice = new Price5(line.getLocalDiscountTaxedPrice()); + discountedTaxedPrice = discount.applyTo(discountBasePrice); + taxAmounts = PriceNoRound.computeReverseTaxes(line, discountedTaxedPrice); + discountedPrice = PriceNoRound.removeTaxes(discountedTaxedPrice, taxAmounts); + break; + case UNIT: + case UNIT_TAXED: + throw new IllegalArgumentException (String.format("Discount of type %s cannot be applied to an order.", + target.getCode())); + default: + assert false : target; + line.setOrderPrices(line.getLocalPrices()); + return; + } + ComputedPrices globalPrices = new ComputedPrices( + line.getLocalDiscountPrice(), line.getLocalDiscountTaxedPrice(), + discountedPrice, discountedTaxedPrice, + taxAmounts); + line.setOrderPrices(globalPrices); + } + + /** + *

Compute or recompute the price of each line including the discount from + * the order. Limited scope to be called from {@link Order}.

+ *

This will spread the discount from the order across all lines. This + * method will compute local prices if not already set. It blocks order changes + * during the computation.

+ *

Global prices are always set as + * {@link org.pasteque.coreutil.price.Price5}.

+ * @throws IllegalStateException When the discount cannot be applied to an order. + * See {@link org.pasteque.coreutil.constants.DiscountTarget}. + */ + /* package */ void computeGlobalPrices() throws IllegalStateException { + synchronized(this.order) { + if (this.order.getLineCount() == 0) { + return; + } + // Make sure each line has local prices set + for (int i = 0; i < this.order.getLineCount(); i++) { + ProductLine line = this.order.getLine(i); + if (!line.isLocalPriceComputed()) { + this.computeLocalLinePrice(line); + } + } + // Easy case: no discount + if (this.order.getDiscount() == null) { + for (int i = 0; i < this.order.getLineCount(); i++) { + ProductLine line = this.order.getLine(i); + line.setOrderPrices(line.getLocalPrices()); + } + return; + } + // Compute discount + Discount discount = this.order.getDiscount(); + ComputedPrices localSum = this.sumLocalPrices(); + Price2 basePrice; + double discountRate; + Price2 discountedPrice; + Price2 discountedTaxedPrice; + switch (discount.getTarget()) { + case UNIT: + // TODO + break; + case UNIT_TAXED: + // TODO + break; + case TOTAL: + basePrice = (Price2) localSum.getDiscountPrice(); + discountedPrice = discount.applyTo(basePrice); + discountRate = discountedPrice.getRatio(basePrice); + // Report the discount to each line + for (int i = 0; i < this.order.getLineCount(); i++) { + this.computeOrderLinePrice(i, discountedPrice, discountRate, discount.getTarget()); + } + break; + case TOTAL_TAXED: + basePrice = (Price2) localSum.getDiscountTaxedPrice(); + discountedTaxedPrice = discount.applyTo(basePrice); + discountRate = discountedTaxedPrice.getRatio(basePrice); + // Report the discount to each line + for (int i = 0; i < this.order.getLineCount(); i++) { + this.computeOrderLinePrice(i, discountedTaxedPrice, discountRate, discount.getTarget()); + } + break; + default: + assert false : discount.getType(); + for (int i = 0; i < this.order.getLineCount(); i++) { + ProductLine line = this.order.getLine(i); + line.setOrderPrices(line.getLocalPrices()); + } + return; + } + // Sum the new discounted prices + ComputedPrices sum = this.order.getLine(0).getOrderPrices(); + for (int i = 1; i < this.order.getLineCount(); i++) { + sum = sum.add(this.order.getLine(i).getOrderPrices()); + } + // Round the final prices to 2 decimals and store them into the order + FinalTaxAmount[] finalTaxes = new FinalTaxAmount[sum.getTaxes().size()]; + for (int i = 0; i < sum.getTaxes().size(); i++) { + TaxAmount amount = sum.getTaxes().get(i); + finalTaxes[i] = new FinalTaxAmount( + amount.getTax(), + new Price2(amount.getBase()), + new Price2(amount.getAmount())); + } + this.order.setOrderPrices( + new Price2(sum.dp()), + new ImmutableList(finalTaxes), + new Price2(sum.dtp())); + } + } + + /** + * Get the method used to compute prices. + * @return The method used to compute prices for this order. + */ + public final PriceRound getPriceRound() { + return this.priceRound; + } +} diff --git a/src/main/java/org/pasteque/common/model/order/PriceLineRound.java b/src/main/java/org/pasteque/common/model/order/PriceLineRound.java new file mode 100644 index 0000000..186b279 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/order/PriceLineRound.java @@ -0,0 +1,448 @@ +package org.pasteque.common.model.order; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.constants.DiscountTarget; +import org.pasteque.coreutil.constants.DiscountType; +import org.pasteque.coreutil.price.Discount; +import org.pasteque.coreutil.price.Price; +import org.pasteque.coreutil.price.Price2; +import org.pasteque.coreutil.price.TaxAmount; +import org.pasteque.coreutil.price.Rate; +import org.pasteque.coreutil.price.Tax; + +/** + *

Round amounts for each line and compute the sums from each rounded line. + * The discount rate applied to the order is added to the discount rate of + * each line to compute the total.

+ *

This method will produce figures suited for accounting but may show + * inconsistencies for common people by moving rounding issues into the total.

+ *

It is best suitable when working mostly with non-taxed prices on + * 2 decimals. When working with taxed prices on 2 decimals, + * {@link org.pasteque.common.model.order.PriceTotalRound} may be more suitable.

+ *

Pass the singleton to an {@link Order} to automagically compute + * the prices from there.

+ *

Computation method

+ *
    + *
  • Each line has a unit price without taxes on 2 or 5 decimals and an indicative + * taxed unit price on 2 decimals. + * Those are multiplied by the quantity and the results are rounded to 2 decimals. + * This is the major difference with {@link org.pasteque.common.model.order.PriceTotalRound} + * which will keep the non-taxed unit and quantity prices on 5 decimals.
  • + *
  • The discount of the line produces new prices, still on 2 decimals.
  • + *
  • The total without the discount from the order is the sum of all rounded + * lines. The tax amounts will not match when recomputed from the sum of the + * bases.
  • + *
  • The discount from the order is applied to produce prices on 5 decimals + * for the line. This discount is also applied to the subtotals of the order, + * to provide results on 2 decimals.
  • + *
+ *

Available figures

+ *
    + *
  • Unit prices with and without taxes with any decimal precision.
  • + *
  • Line value with and without taxes on 2 decimals.
  • + *
  • Tax amounts for each line on 2 decimals
  • + *
  • Untaxed line price + tax amounts = taxed line price, without rounding issue
  • + *
  • Recomputing tax amounts from the total is not accurate.
  • + *
  • Line values with the discount from the order on 2 decimals. Discounts + * applied to the total may not be accurate.
  • + *
+ *

Discount target compatibility

+ *

The discount applied to the line provides rounded results. + * The discount applied to the order can provide mixed results depending + * upon its target:

+ *
    + *
  • {@link org.pasteque.coreutil.constants.DiscountTarget#UNIT} and + * {@link org.pasteque.coreutil.constants.DiscountTarget#UNIT_TAXED} combines + * the discounts for each line and keep rounded results for each line. This is + * the recommended way to keep a consistent rounding method. It provides results + * rounded to 2 decimals for each line with a consistent computing method.
  • + *
  • {@link org.pasteque.coreutil.constants.DiscountTarget#TOTAL} and + * {@link org.pasteque.coreutil.constants.DiscountTarget#TOTAL_TAXED} applies + * a fraction of the discount to each line and round it to 2 decimals. + * The amount of the discount computed from the total as it would be expected + * may not be accurate.
  • + *
+ * @see org.pasteque.common.model.order.PriceTotalRound + */ +// TODO WIP, missing some price computations +public final class PriceLineRound implements PriceRound +{ + /** + * Singleton instance. + */ + public static final PriceLineRound instance = new PriceLineRound(); + + /** + * Private constructor for the singleton. + */ + private PriceLineRound() { } + + /** + * Compute the amounts of taxes from a base price. + * @param line The line to get taxes from. + * @param basePrice The base price from which to compute taxes. + * That is the non-taxed price with or without the discount. + * @return The computed tax amounts. The taxed price can be computed + * from basePrice by passing the result to + * {@link addTaxes(Price2, TaxAmount[])}. + */ + private ImmutableList computeTaxes(ProductLine line, Price2 basePrice) { + ImmutableList taxes = line.getTaxes(); + TaxAmount[] taxAmounts = new TaxAmount[taxes.size()]; + for (int i = 0; i < taxes.size(); i++) { + Price2 amount; + Tax tax = taxes.get(i); + switch (tax.getType()) { + case RATE: + amount = basePrice.multiply(tax.getAmount()); + break; + case FIXED_QUANTITY: + amount = new Price2(line.getQuantity().getQuantity() * tax.getAmount()); + break; + default: + assert false : tax.getType(); + amount = new Price2(0.0); + } + taxAmounts[i] = new TaxAmount(tax, basePrice, amount); + } + return new ImmutableList(taxAmounts); + } + + /** + * Compute the amounts of taxes from a base taxed price. + * @param line The line to get taxes from. + * @param baseTaxedPrice The base price from which to compute taxes. + * That is the taxed price with or without the discount. + * @return The computed tax amounts. The non-taxed price can be computed + * from baseTaxedPrice by passing the result to + * {@link removeTaxes(Price2, TaxAmount[])}. + */ + private ImmutableList computeReverseTaxes(ProductLine line, Price2 baseTaxedPrice) { + ImmutableList taxes = line.getTaxes(); + double totalRate = 0.0; + Price2 totalFixed = new Price2(0.0); + for (int i = 0; i < taxes.size(); i++) { + Tax tax = taxes.get(i); + switch (tax.getType()) { + case RATE: + if (!tax.isIncludedInBase()) { + totalRate += tax.getAmount(); + } + break; + case FIXED_QUANTITY: + if (!tax.isIncludedInBase()) { + totalFixed = totalFixed.add(new Price2(line.getQuantity().getQuantity() * tax.getAmount())); + } + break; + default: + assert false : tax.getType(); + } + } + Price2 basePrice = baseTaxedPrice.subtract(totalFixed); + basePrice = basePrice.divide(1.0 + totalRate); + return this.computeTaxes(line, basePrice); + } + + /** + * Apply a discount to a base price. + * @param line The line from which to get the discount. This method + * accepts a null discount. + * @param basePrice The price from which to compute the discount. + * @return The price with the discount applied. + */ + private Price2 computeDiscount(Price2 basePrice, Discount discount) { + if (discount == null) { + return basePrice; + } + switch (discount.getType()) { + case RATE: + Rate r = new Rate(discount.getValue(), false); + return r.applyTo(basePrice); + case VALUE: + return basePrice.add(-discount.getValue()); + default: + assert false : discount.getType(); + return null; + } + } + + /** + * Add taxes from a base price. + * @param basePrice The price from which to add taxes. Usually the + * non-taxed line price, with or without the discount. + * @param taxAmounts The amounts of taxes, usually computed from + * {@link computeTaxes(ProductLine, Price2)}. + * @return The base price with the tax amounts added when required. + */ + private Price2 addTaxes(Price2 basePrice, ImmutableList taxAmounts) { + Price taxedPrice = basePrice; + for (TaxAmount amount : taxAmounts) { + if (!amount.getTax().isIncludedInBase()) { + taxedPrice = taxedPrice.add(amount.getAmount()); + } + } + return new Price2(taxedPrice); + } + + /** + * Remove taxes from a base price. + * @param basePrice The price from which to remove taxes. Usually the + * taxed line price, with or without the discount. + * @param taxAmounts The amounts of taxes, usually computed from + * {@link computeReverseTaxes(ProductLine, Price2)}. + * @return The base price with the tax amounts subtracted when required. + * When using this method, the tax amounts may not be completely accurate + * when recomputed from the result due to rounding issues. + */ + private Price2 removeTaxes(Price2 basePrice, ImmutableList taxAmounts) { + Price price = basePrice; + for (TaxAmount amount : taxAmounts) { + if (!amount.getTax().isIncludedInBase()) { + price = price.subtract(amount.getAmount()); + } + } + return new Price2(price); + } + + @Override // From PriceRound + public ComputedPrices computeLocalPrices(ProductLine line) { + // Compute base prices without any discount + Price2 basePrice = new Price2(line.getUnitPrice().multiply(line.getQuantity())); + ImmutableList taxAmounts = this.computeTaxes(line, basePrice); + Price2 baseTaxedPrice = this.addTaxes(basePrice, taxAmounts); + // Compute prices with the discount from the line + Discount discount = line.getDiscount(); + if (discount == null) { + return new ComputedPrices(basePrice, baseTaxedPrice, + basePrice, baseTaxedPrice, + taxAmounts); + } else { + Price2 discountBasePrice; + Price2 discountedPrice; + Price2 discountedTaxedPrice; + ImmutableList discountTaxAmounts; + // Compute the discount according to the target + switch (discount.getTarget()) { + case UNIT: + discountBasePrice = new Price2(line.getUnitPrice()); + discountedPrice = this.computeDiscount(discountBasePrice, discount); + discountedPrice = discountedPrice.multiply(line.getQuantity()); + discountTaxAmounts = this.computeTaxes(line, discountedPrice); + discountedTaxedPrice = this.addTaxes(discountedPrice, discountTaxAmounts); + break; + case UNIT_TAXED: + discountBasePrice = new Price2(line.getUnitTaxedPrice()); + discountedTaxedPrice = this.computeDiscount(discountBasePrice, discount); + discountedTaxedPrice = discountedTaxedPrice.multiply(line.getQuantity()); + discountTaxAmounts = this.computeReverseTaxes(line, discountedTaxedPrice); + discountedPrice = this.removeTaxes(discountedTaxedPrice, discountTaxAmounts); + break; + case TOTAL: + discountBasePrice = basePrice; + discountedPrice = this.computeDiscount(discountBasePrice, discount); + discountTaxAmounts = this.computeTaxes(line, discountedPrice); + discountedTaxedPrice = this.addTaxes(discountedPrice, discountTaxAmounts); + break; + case TOTAL_TAXED: + discountBasePrice = baseTaxedPrice; + discountedTaxedPrice = this.computeDiscount(discountBasePrice, discount); + discountTaxAmounts = this.computeReverseTaxes(line, discountedTaxedPrice); + discountedPrice = this.removeTaxes(discountedTaxedPrice, discountTaxAmounts); + break; + default: + assert false : discount.getTarget(); + discountBasePrice = basePrice; + discountedPrice = basePrice; + discountedTaxedPrice = baseTaxedPrice; + discountTaxAmounts = taxAmounts; + } + return new ComputedPrices( + basePrice, baseTaxedPrice, + discountedPrice, discountedTaxedPrice, + discountTaxAmounts); + } + } + + @Override // from PriceRound + public Discount computeFinalDiscount( + ProductLine line, + Discount orderDiscount, + Price subtotal) + throws IllegalStateException { + if (!line.isLocalPriceComputed()) { + throw new IllegalStateException("Local prices must be computed before."); + } + if (Discount.getEffectiveDiscount(line.getDiscount()) == null) { + return orderDiscount; + } + // Compute the ratio the line represents from the total + // to distribute the discount amount from TOTAL and TOTAL_TAXED targets. + double lineRatio; + Price2 lineRatioAmount; + switch (orderDiscount.getTarget()) { + case TOTAL: + lineRatio = line.getLocalDiscountPrice().getRatio(subtotal); + lineRatioAmount = new Price2(subtotal.multiply(lineRatio)); + break; + case TOTAL_TAXED: + lineRatio = line.getLocalDiscountTaxedPrice().getRatio(subtotal); + lineRatioAmount = new Price2(subtotal.multiply(lineRatio)); + break; + case UNIT: + case UNIT_TAXED: // lineRatio not used + lineRatio = 1.0; + lineRatioAmount = new Price2(0.0); + break; + default: + assert false : orderDiscount.getTarget(); + return line.getDiscount(); + } + Discount lineRateDiscount; + Discount appliedDiscount; // Combines the line and order discounts + Price discountBasePrice; + switch (orderDiscount.getType()) { + case VALUE: + // Dispatch the fixed value within the line and recompute the result + switch (orderDiscount.getTarget()) { + case UNIT: + // Apply the fixed value directly to each line + discountBasePrice = orderDiscount.applyTo(new Price2(line.getLocalDiscountPrice())); + appliedDiscount = orderDiscount.toUnitRate(line.getLocalPrice(), discountBasePrice); + break; + case UNIT_TAXED: + // Apply the fixed value directly to each line + discountBasePrice = orderDiscount.applyTo(new Price2(line.getLocalDiscountTaxedPrice())); + appliedDiscount = orderDiscount.toUnitRate(line.getLocalPrice(), discountBasePrice); + break; + case TOTAL: + // Apply a fraction of the fixed value to the line and round the result + discountBasePrice = new Price2(orderDiscount.applyPartlyTo(line.getLocalDiscountPrice(), lineRatio)); + appliedDiscount = orderDiscount.toUnitRate(line.getLocalPrice(), discountBasePrice); + break; + case TOTAL_TAXED: + // Apply a fraction of the fixed value to the line and round the result + discountBasePrice = new Price2(orderDiscount.applyPartlyTo(line.getLocalDiscountTaxedPrice(), lineRatio)); + appliedDiscount = orderDiscount.toUnitRate(line.getLocalDiscountTaxedPrice(), discountBasePrice); + break; + default: + assert false : orderDiscount.getTarget(); + return line.getDiscount(); + } + break; + case RATE: + // Convert the discount from the line to a rate and add the one + // from the order + switch (orderDiscount.getTarget()) { + case UNIT: + // Add the rate directly to each line + discountBasePrice = line.getLocalPrice(); + lineRateDiscount = orderDiscount.toUnitRate(line.getLocalPrice(), line.getLocalDiscountPrice()); + appliedDiscount = new Discount(DiscountType.RATE, DiscountTarget.UNIT, + lineRateDiscount.getValue() + orderDiscount.getValue()); + break; + case UNIT_TAXED: + // Add the rate directly to each line + discountBasePrice = line.getLocalTaxedPrice(); + lineRateDiscount = orderDiscount.toUnitRate(line.getLocalTaxedPrice(), line.getLocalDiscountTaxedPrice()); + appliedDiscount = new Discount(DiscountType.RATE, DiscountTarget.UNIT_TAXED, + lineRateDiscount.getValue() + orderDiscount.getValue()); + break; + case TOTAL: + // Apply the fraction of the total discount to the line and recompute + discountBasePrice = line.getLocalDiscountPrice(); + appliedDiscount = new Discount(DiscountType.RATE, DiscountTarget.TOTAL, + 1.0 - discountBasePrice.subtract(lineRatioAmount).getRatio(discountBasePrice)); + break; + case TOTAL_TAXED: + // Apply the fraction of the total discount to the line and recompute + discountBasePrice = line.getLocalTaxedPrice(); + appliedDiscount = new Discount(DiscountType.RATE, DiscountTarget.TOTAL_TAXED, + 1.0 - discountBasePrice.subtract(lineRatioAmount).getRatio(subtotal)); + break; + default: + assert false : orderDiscount.getTarget(); + return line.getDiscount(); + } + default: + assert false : orderDiscount.getTarget(); + return line.getDiscount(); + } + return appliedDiscount; + } + + @Override // from PriceRound + public ComputedPrices computeGlobalPrices( + ProductLine line, + Discount finalDiscount, + Price subtotal) + throws IllegalStateException { + if (!line.isLocalPriceComputed()) { + throw new IllegalStateException("Local prices must be computed before."); + } + if (Discount.getEffectiveDiscount(finalDiscount) == null) { + return line.getLocalPrices(); + } + // Recompute prices with the new combined discount + Price discountedPrice; + Price discountedTaxedPrice; + ImmutableList discountedTaxAmounts; + switch (finalDiscount.getTarget()) { + case UNIT: // intentional fall-through + case TOTAL: + discountedPrice = finalDiscount.applyTo(new Price2(line.getLocalPrice())); + discountedTaxAmounts = this.computeTaxes(line, (Price2) discountedPrice); + discountedTaxedPrice = this.addTaxes((Price2) discountedPrice, discountedTaxAmounts); + break; + case UNIT_TAXED: // intentional fall-through + case TOTAL_TAXED: + discountedTaxedPrice = finalDiscount.applyTo(new Price2(line.getLocalTaxedPrice())); + discountedTaxAmounts = this.computeReverseTaxes(line, (Price2) discountedTaxedPrice); + discountedPrice = this.removeTaxes((Price2) discountedTaxedPrice, discountedTaxAmounts); + break; + default: + assert false : finalDiscount.getTarget(); + return line.getLocalPrices(); + } + return new ComputedPrices( + line.getLocalPrice(), line.getLocalTaxedPrice(), + discountedPrice, discountedTaxedPrice, + discountedTaxAmounts); + } + + @Override // From PriceRound + public ComputedPrices computeTotal(Order order) throws IllegalStateException { + synchronized(order) { + if (order.getLineCount() == 0) { + return new ComputedPrices( + new Price2(0.0), new Price2(0.0), + new Price2(0.0), new Price2(0.0), + new ImmutableList()); + } + if (!order.getLine(0).isLocalPriceComputed()) { + throw new IllegalStateException("Prices for line 0 are not computed"); + } + Discount discount = Discount.getEffectiveDiscount(order.getDiscount()); + if (discount != null) { + ComputedPrices subtotal = order.getLine(0).getLocalPrices(); + for (int i = 1; i < order.getLineCount(); i++) { + if (!order.getLine(i).isPriceComputed()) { + throw new IllegalStateException("Prices for line " + i + " are not computed"); + } + subtotal = subtotal.add(order.getLine(i).getOrderPrices()); + } + return subtotal; + } + // The final prices of each line are already rounded to 2 decimals. + // Sum everything and that's it. + ComputedPrices total = order.getLine(0).getOrderPrices(); + for (int i = 1; i < order.getLineCount(); i++) { + if (!order.getLine(i).isPriceComputed()) { + throw new IllegalStateException("Prices for line " + i + " are not computed"); + } + total = total.add(order.getLine(i).getLocalPrices()); + } + return total; + } + } +} + diff --git a/src/main/java/org/pasteque/common/model/order/PriceNoRound.java b/src/main/java/org/pasteque/common/model/order/PriceNoRound.java new file mode 100644 index 0000000..a550df8 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/order/PriceNoRound.java @@ -0,0 +1,118 @@ +package org.pasteque.common.model.order; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.price.Price5; +import org.pasteque.coreutil.price.Tax; +import org.pasteque.coreutil.price.TaxAmount; + +/** + * Utility to compute prices without rounding, always using + * {@link org.pasteque.coreutil.price.Price5}. + * This class can be used only to compute amounts that are never presented to + * the customers. + */ +/* package */ final class PriceNoRound +{ + /** Static methods only. */ + private PriceNoRound() { } + + /** + * Compute the amounts of taxes from a base price. + * @param line The line to get taxes from. + * @param basePrice The base price from which to compute taxes. + * @return The computed tax amounts. The taxed price can be computed + * from basePrice by passing the result to + * {@link addTaxes(Price5, TaxAmount[])}. + */ + /* package */ static ImmutableList computeTaxes( + ProductLine line, Price5 basePrice) { + ImmutableList taxes = line.getTaxes(); + TaxAmount[] taxAmounts = new TaxAmount[taxes.size()]; + for (int i = 0; i < taxes.size(); i++) { + Price5 amount; + Tax tax = taxes.get(i); + switch (tax.getType()) { + case RATE: + amount = basePrice.multiply(tax.getAmount()); + break; + case FIXED_QUANTITY: + amount = new Price5(line.getQuantity().getQuantity() * tax.getAmount()); + break; + default: + assert false : tax.getType(); + amount = new Price5(0.0); + } + taxAmounts[i] = new TaxAmount(tax, basePrice, amount); + } + return new ImmutableList(taxAmounts); + } + + /** + * Compute the amounts of taxes from a base taxed price. + * @param line The line to get taxes from. + * @param baseTaxedPrice The base price from which to compute taxes. + * @return The computed tax amounts. The non-taxed price can be computed + * from baseTaxedPrice by passing the result to + * {@link removeTaxes(Price5, TaxAmount[])}. + */ + /* package */ static ImmutableList computeReverseTaxes(ProductLine line, Price5 baseTaxedPrice) { + ImmutableList taxes = line.getTaxes(); + double totalRate = 0.0; + Price5 totalFixed = new Price5(0.0); + for (int i = 0; i < taxes.size(); i++) { + Tax tax = taxes.get(i); + switch (tax.getType()) { + case RATE: + if (!tax.isIncludedInBase()) { + totalRate += tax.getAmount(); + } + break; + case FIXED_QUANTITY: + if (!tax.isIncludedInBase()) { + totalFixed = totalFixed.add(new Price5(line.getQuantity().getQuantity() * tax.getAmount())); + } + break; + default: + assert false : tax.getType(); + } + } + Price5 basePrice = baseTaxedPrice.subtract(totalFixed); + basePrice = basePrice.divide(1.0 + totalRate); + return computeTaxes(line, basePrice); + } + + /** + * Add taxes from a base price. + * @param basePrice The price from which to add taxes. + * @param taxAmounts The amounts of taxes. All amounts are converted to + * {@link org.pasteque.coreutil.price.Price5}. + * @return The base price with the tax amounts added when required. + */ + /* package */ static Price5 addTaxes(Price5 basePrice, ImmutableList taxAmounts) { + Price5 taxedPrice = basePrice; + for (TaxAmount amount : taxAmounts) { + if (!amount.getTax().isIncludedInBase()) { + taxedPrice = taxedPrice.add(new Price5(amount.getAmount())); + } + } + return new Price5(taxedPrice); + } + + /** + * Remove taxes from a base price. + * @param basePrice The price from which to remove taxes. Usually the + * taxed line price, with or without the discount. + * @param taxAmounts The amounts of taxes. All amounts are converted to + * {@link org.pasteque.coreutil.price.Price5}. + * @return The base price with the tax amounts subtracted when required. + */ + /* package */ static Price5 removeTaxes(Price5 basePrice, ImmutableList taxAmounts) { + Price5 price = basePrice; + for (TaxAmount amount : taxAmounts) { + if (!amount.getTax().isIncludedInBase()) { + price = price.subtract(new Price5(amount.getAmount())); + } + } + return price; + } +} diff --git a/src/main/java/org/pasteque/common/model/order/PriceRound.java b/src/main/java/org/pasteque/common/model/order/PriceRound.java new file mode 100644 index 0000000..332fecb --- /dev/null +++ b/src/main/java/org/pasteque/common/model/order/PriceRound.java @@ -0,0 +1,76 @@ +package org.pasteque.common.model.order; + +import org.pasteque.coreutil.price.Discount; +import org.pasteque.coreutil.price.Price; + +/** + *

Compute prices and manage rounding issues.

+ *

Implementations will provide different precision and summing methods + * that moves rounding issues to some prices or others.

+ */ +public interface PriceRound +{ + /** + * Compute prices for the line without considering the discount from + * the order. + * @param line The line to compute the prices from. + * @return The computed prices. + */ + public ComputedPrices computeLocalPrices(ProductLine line); + + /** + * Compute the discount to apply to the price without any discount to + * get the result of applying all discounts. + * @param line The line to compute the final discount for + * @param orderDiscount The discount applied to the whole order. + * @param subtotal The total amount of the order before applying + * the discount. It is either the total with or without taxes according + * to the target of the discount (ignoring whether is is total or by unit). + * @return The combined discount of the one from the line and the one + * from the order. + * @throws IllegalStateException When the local prices of the line + * are not computed. + */ + public Discount computeFinalDiscount( + ProductLine line, + Discount orderDiscount, + Price subtotal) + throws IllegalStateException; + + /** + * Compute prices for the line including the discount from the order. + * @param line The line to compute the prices from. + * @param finalDiscount The final discount to apply. + * @param subtotal The total amount of the order before applying + * the discount. It is either the total with or without taxes according + * to the target of the discount (ignoring whether is is total or by unit). + * @return The computed prices. BasePrice and baseTaxedPrice from this + * result are meaningless and should be shared with + * {@link computeLocalPrices(ProductLine)}. + * @throws IllegalStateException When the local prices of the line + * are not computed. + * @see computeFinalDiscount(ProductLine, Discount, Price) + * @see org.pasteque.common.model.order.PriceNoRound + */ + public ComputedPrices computeGlobalPrices( + ProductLine line, + Discount finalDiscount, + Price subtotal) + throws IllegalStateException; + + /** + * Compute total prices for an order by applying the discount + * to the subtotal. The result may be slightly different from the sum + * of the global prices of each line. + * Implementations must synchronize on the order. + * @param order The order to get the total from. + * @return The final prices and tax amounts. All should be rounded to + * {@link org.pasteque.coreutil.price.Price2} to be payable and writable + * in accounting. The result may be slightly different from the sum + * of the global prices of each line due to the rounding of each resulting + * figure. + * @throws IllegalStateException When the global prices are not set for + * each line. + */ + public ComputedPrices computeTotal(Order order) throws IllegalStateException; +} diff --git a/src/main/java/org/pasteque/common/model/order/PriceTotalRound.java b/src/main/java/org/pasteque/common/model/order/PriceTotalRound.java new file mode 100644 index 0000000..c2568b9 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/order/PriceTotalRound.java @@ -0,0 +1,70 @@ +package org.pasteque.common.model.order; + +import org.pasteque.coreutil.price.Discount; +import org.pasteque.coreutil.price.Price; + +/** + *

Keep precision for each line and round only the total. + * The discount rate of the order is applied to the total of the order.

+ *

This method is best suitable when hiding non-taxed prices or using them + * with 5 decimals. Only the totals are rounded to 2 decimals for accounting. + * When showing the prices for each line, {@link org.pasteque.common.model.order.PriceLineRound} + * may be more suitable.

+ *
    + *
  • Each line has a unit price without taxes on 5 decimals to have an + * almost always corresponding taxed unit price on 2 decimals. + * Those are multiplied by the quantity while keeping the same precision. This is + * the major difference with {@link org.pasteque.common.model.order.PriceLineRound} which + * will round this results to 2 decimals.
  • + *
  • The discount of the line produces new prices, still on 5 and 2 decimals.
  • + *
  • The total without the discount from the order is the sum of all unrounded + * lines. It may be rounded to 2 decimals for display purposes but is still computed + * with 5 decimals.
  • + *
  • The discount from the order is applied to produce prices on 5 decimals + * for the line. This discount is also applied to the subtotals of the order, + * to provide results on 2 decimals.
  • + *
+ *

This method produces figures suited for common people regarding the total + * prices and moves rounding issues into accounting which can handle only two + * digit figures in partial sums.

+ *

Pass the singleton to an {@link Order} to automagically compute + * the prices from there.

+ */ +public final class PriceTotalRound implements PriceRound +{ + /** + * Singleton. + */ + public static final PriceTotalRound instance = new PriceTotalRound(); + + private PriceTotalRound() { } + + @Override // from PriceRound + public ComputedPrices computeLocalPrices(ProductLine line) { + // TODO Auto-generated method stub + return null; + } + + @Override // from PriceRound + public Discount computeFinalDiscount(ProductLine line, Discount orderDiscount, Price subtotal) + throws IllegalStateException { + // TODO Auto-generated method stub + return null; + } + + @Override // from PriceRound + public ComputedPrices computeGlobalPrices( + ProductLine line, + Discount discount, + Price subtotal) { + // TODO Auto-generated method stub + return null; + } + + @Override // from PriceRound + public ComputedPrices computeTotal(Order order) throws IllegalStateException { + // TODO Auto-generated method stub + return null; + } +} + diff --git a/src/main/java/org/pasteque/common/model/order/ProductLine.java b/src/main/java/org/pasteque/common/model/order/ProductLine.java new file mode 100644 index 0000000..6bac17e --- /dev/null +++ b/src/main/java/org/pasteque/common/model/order/ProductLine.java @@ -0,0 +1,381 @@ +package org.pasteque.common.model.order; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.price.Discount; +import org.pasteque.coreutil.price.Price; +import org.pasteque.coreutil.price.Price5; +import org.pasteque.coreutil.price.Quantity; +import org.pasteque.coreutil.price.Tax; +import org.pasteque.coreutil.price.TaxAmount; +import org.pasteque.coreutil.transition.LineTransition; + +/** + *

A line shared with {@link org.pasteque.common.model.order.Order} and + * {@link org.pasteque.major.domain.MajorTicket} to hold basic information + * about what was ordered or sold.

+ *

A line has two set of prices. The local prices are considering only + * the line outside any order. The order prices take in account the discount + * from the order. + */ +// TODO WIP, Immutable notice, add subclasses for compositions and packs, add support for extradata (attributes) +public class ProductLine implements LineTransition +{ + /** See {@link getProductReference()}. */ + private final String productReference; + /** See {@link getProductLabel()}. */ + private final String productLabel; + /** See {@link getCategoryReference()}. */ + private final String categoryReference; + /** See {@link getUnitPrice()}. */ + private final Price unitPrice; + /** See {@link getUnitTaxedPrice()}. */ + private final Price unitTaxedPrice; + /** See {@link getTaxes()}. */ + private final ImmutableList taxes; + /** See {@link getQuantity()}. */ + private final Quantity quantity; + /** See {@link getDiscount()}. */ + private final Discount discount; + /** See {@link getLocalTaxes()}. */ + private ImmutableList localTaxes; + /** See {@link getLocalPrice()}. */ + private Price localPrice; + /** See {@link getLocalTaxedPrice()}. */ + private Price localTaxedPrice; + /** See {@link getLocalDiscountPrice()}. */ + private Price localDiscountPrice; + /** See {@link getLocalDiscountTaxedPrice()}. */ + private Price localDiscountTaxedPrice; + /** See {@link getFinalDiscount} */ + private Discount finalDiscount; + /** See {@link getTotalTaxes()}. */ + private ImmutableList orderTaxes; + /** See {@link getTotalPrice()}. */ + private Price5 orderPrice; + /** See {@link getTotalTaxedPrice()}. */ + private Price5 orderTaxedPrice; + + /** + * Create a line from all fields. The prices are not computed. + * @param productReference See {@link getProductReference()}. + * @param productLabel See {@link getProductLabel()}. + * @param categoryReference See {@link getCategoryReference()}. + * @param unitPrice See {@link getUnitPrice()}. + * @param unitTaxedPrice See {@link getUnitTaxedPrice()}. + * @param quantity See {@link getQuantity()}. + * @param discount See {@link getDiscount()}. + * @param taxes See {@link getTaxes()}. + */ + public ProductLine( + String productReference, + String productLabel, + String categoryReference, + Price unitPrice, + Price unitTaxedPrice, + Quantity quantity, + Discount discount, + ImmutableList taxes) { + this.productReference = productReference; + this.productLabel = productLabel; + this.categoryReference = categoryReference; + this.unitPrice = unitPrice; + this.unitTaxedPrice = unitTaxedPrice; + this.quantity = quantity; + this.discount = discount; + this.taxes = taxes; + } + + /** + * Create an other line with an updated quantity. + * @param newUnitPrice The new unit price. + * @param newQuantity The new quantity. + * @return A new line with copied data with the new unit price and + * quantity. The prices are not computed. + */ + /*public ProductLine updated(Price newUnitPrice, Quantity newQuantity) { + return new ProductLine( + this.productReference), + this.productLabel, + this.categoryReference, + newUnitPrice, + newQuantity, + copyTax, + this.discountRate); + }*/ + + /** + * Set the prices of the line, without the discount from the order. + * It will also clear the global prices. + * Use null to clear the local prices. + * Limited scope to be used by {@link OrderPrice}. + * @param prices The prices to set. + */ + /* package */ void setLocalPrices(ComputedPrices prices) { + this.orderPrice = null; + this.orderTaxedPrice = null; + this.orderTaxes = null; + if (prices == null) { + this.localPrice = null; + this.localTaxedPrice = null; + this.localTaxes = null; + } else { + this.localPrice = prices.getBasePrice(); + this.localTaxedPrice = prices.getBaseTaxedPrice(); + this.localDiscountPrice = prices.getDiscountPrice(); + this.localDiscountTaxedPrice = prices.getDiscountTaxedPrice(); + this.localTaxes = prices.getTaxes(); + } + } + + /** + * Get the set of prices before applying the discount from the order. + * Limited scope to ease price computation. Each price is available + * individually with from its own getter. + * @return The set of prices before applying the discount from the order. + */ + /* package */ ComputedPrices getLocalPrices() { + return new ComputedPrices( + this.localPrice, this.localTaxedPrice, + this.localDiscountPrice, this.localDiscountTaxedPrice, + this.localTaxes); + } + + /** + * Get the unique reference of the product in this line. + * @return The unique reference of the product. + */ + public String getProductReference() { + return this.productReference; + } + + /** + * Get the label of the product in this line. + * @return The label of the product. + */ + public String getProductLabel() { + return this.productLabel; + } + + /** + * Get the reference of the category of the product in this line. + * The category itself is not copied, the category is identified + * for grouping. + * @return The reference of the category. + */ + public String getCategoryReference() { + return this.categoryReference; + } + + /** + * Get the reference price for 1 unit of the product without taxes. + * @return The price for 1 unit. It should be a + * {@link org.pasteque.coreutil.price.Price5} to be able to compute a + * taxed price as {@link org.pasteque.coreutil.price.Price2} and avoid + * rounding issues in most cases. + */ + public Price getUnitPrice() { + return this.unitPrice; + } + + /** + * Get the reference price for 1 unit of the product including taxes. + * @return The price for 1 unit. For B2C, it should be usable as a + * {@link org.pasteque.coreutil.price.Price2} without losing much precision. + * For B2B this is rarely used or only informational. + */ + public Price getUnitTaxedPrice() { + return this.unitTaxedPrice; + } + + /** + * Get the list of taxes to apply to the line. + * @return The list of taxes applicable to the line. + */ + public ImmutableList getTaxes() { + return this.taxes; + } + + /** + * Get the quantity of product in this line. + * @return The quantity of product. + */ + public Quantity getQuantity() { + return this.quantity; + } + + /** + * Get the effective discount applied for this line only. + * @return The effective discount of this line, can be null. + * @see org.pasteque.coreutil.price.Discount#getEffectiveDiscount(Discount) + */ + public Discount getDiscount() { + return Discount.getEffectiveDiscount(discount); + } + + /** + * Get the discount. + * @return The discount, without effective check. + * @see getDiscount() + */ + public Discount getRawDiscount() { + return this.discount; + } + + /** + * Get the price without taxes of this line before applying any discount. + * @return The price without taxes before applying any discount. + */ + @Override // from LineTransition + public Price getPrice() { + return this.localPrice; + } + + /** + * Same as {@link getPrice()} for naming consistency. + * @return The price without taxes before applying any discount. + */ + public Price getLocalPrice() { + return this.localPrice; + } + + /** + * Get the price with taxes of this line before applying any discount. + * @return The price with taxes before applying any discount. + */ + public Price getLocalTaxedPrice() { + return this.localTaxedPrice; + } + + /** + * Get the price without taxes of this line after applying the discount + * of the line. + * @return The price without taxes after applying the discount of the line. + */ + public Price getLocalDiscountPrice() { + return this.localDiscountPrice; + } + + /** + * Get the price with taxes of this line after applying the discount + * of the line. + * @return The price with taxes after applying the discount of the line. + */ + public Price getLocalDiscountTaxedPrice() { + return this.localDiscountTaxedPrice; + } + + /** + * Get the tax amounts for the line including the discount. + * @return The tax amounts computed from the price without taxes + * in quantity and including the discount applied to the line. + * The precision is left to the {@link org.pasteque.common.model.order.PriceRound}. + */ + public ImmutableList getLocalTaxes() { + return this.localTaxes; + } + + /** + * Set the price of the line with the discount from the order. + * Use null to clear the global prices. + * Limited scope to be used by {@link OrderPrice}. + * @param prices The prices to set, null to clear prices. + * Non-discounted prices are ignored. + */ + /* package */ void setOrderPrices(ComputedPrices prices) { + if (prices == null) { + this.orderPrice = null; + this.orderTaxedPrice = null; + this.orderTaxes = null; + } else { + this.orderPrice = new Price5(prices.getDiscountPrice()); + this.orderTaxedPrice = new Price5(prices.getDiscountTaxedPrice()); + this.orderTaxes = prices.getTaxes(); + } + } + + /** + * Set the discount resulting from applying both discount from the line + * and from the order. + * @param finalDiscount The combined discount. + */ + /* package */ void setFinalDiscount(Discount finalDiscount) { + this.finalDiscount = finalDiscount; + } + + @Override // From LineTransition + public Discount getFinalDiscount() { + return this.finalDiscount; + } + + /** + * Get the price without tax including the discount from the order. + * @return The price without tax including the discount from the order. + * Always set to {@link org.pasteque.coreutil.price.Price5} to avoid rounding + * issues when spreading from the total. + */ + @Override // from LineTransition + public Price5 getTotalPrice() { + return this.orderPrice; + } + + /** + * Same as {@link getTotalPrice()} for naming consistency. + * @return The price without tax including the discount from the order. + * Always set to {@link org.pasteque.coreutil.price.Price5} to avoid rounding + * issues when spreading from the total. + */ + public Price5 getOrderPrice() { + return this.orderPrice; + } + + /** + * Get the price with taxes including the discount from the order. + * Limited scope as it has no meaning by itself and is used to compute the + * total in the {@link Order}. + * @return The price without tax including the discount from the order. + * Always set to {@link org.pasteque.coreutil.price.Price5} to avoid rounding + * issues when spreading from the total. + */ + /* package */ Price getOrderTaxedPrice() { + return this.orderTaxedPrice; + } + + @Override // from LineTransition + public ImmutableList getTotalTaxes() { + return this.orderTaxes; + } + + /** + * Get the set of prices after applying the discount from the order. + * Limited scope to ease price computation. Each price is available + * individually with from its own getter. The base prices are copied + * from the local prices after applying the discount from the line. + * @return The set of prices before applying the discount from the order. + */ + /* package */ ComputedPrices getOrderPrices() { + return new ComputedPrices( + this.localDiscountPrice, this.localDiscountTaxedPrice, + this.orderPrice, this.orderTaxedPrice, + this.orderTaxes); + } + + /** + * Check whether the prices of the line are set. + * @return True when the local prices are set. + */ + public boolean isLocalPriceComputed() { + return this.localPrice != null && this.localDiscountPrice != null + && this.localTaxes != null; + } + + /** + * Check whether all prices are computed. Limited scope as the order prices + * has no meaning outside the total from the {@link Order}. + * @return True when local and order prices are set. + */ + /* package */ boolean isPriceComputed() { + return this.isLocalPriceComputed() + && this.orderPrice != null && this.orderTaxedPrice != null + && this.orderTaxes != null; + } +} diff --git a/src/main/java/org/pasteque/common/model/order/package-info.java b/src/main/java/org/pasteque/common/model/order/package-info.java new file mode 100644 index 0000000..52022c6 --- /dev/null +++ b/src/main/java/org/pasteque/common/model/order/package-info.java @@ -0,0 +1,4 @@ +/** + * Create and manipulate orders, with price computation. + */ +package org.pasteque.common.model.order; diff --git a/src/main/java/org/pasteque/common/model/package-info.java b/src/main/java/org/pasteque/common/model/package-info.java new file mode 100644 index 0000000..e8b165d --- /dev/null +++ b/src/main/java/org/pasteque/common/model/package-info.java @@ -0,0 +1,4 @@ +/** + *

Usage classes

+ */ +package org.pasteque.common.model; diff --git a/src/main/java/org/pasteque/common/package-info.java b/src/main/java/org/pasteque/common/package-info.java new file mode 100644 index 0000000..4132a8c --- /dev/null +++ b/src/main/java/org/pasteque/common/package-info.java @@ -0,0 +1,8 @@ +/** + *

Common business logic, outside storing critical data.

+ * + *

This library is designed to be shareable between multiple clients and servers + * to define the common code for the minor versions. + * It is not related to the major version and extends the basic data.

+ */ +package org.pasteque.common; diff --git a/src/main/java/org/pasteque/common/util/HashCypher.java b/src/main/java/org/pasteque/common/util/HashCypher.java new file mode 100644 index 0000000..6a53e43 --- /dev/null +++ b/src/main/java/org/pasteque/common/util/HashCypher.java @@ -0,0 +1,60 @@ +package org.pasteque.common.util; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Utility to manage hashed passwords. + */ +public class HashCypher +{ + /** Static utility-class, cannot be instantiated. */ + private HashCypher() {} + + /** + * Check a password against a hashed password. + * @param password The clear password to check + * @param hashPassword The hashed version of the password, created with + * {@link #hashString(String)}. + * @return True When they match, false otherwise. + */ + public static boolean authenticate(String password, String hashPassword) { + if (hashPassword == null || "".equals(hashPassword) || hashPassword.startsWith("empty:")) { + return password == null || "".equals(password); + } + if (password == null || "".equals(password)) { + return false; + } + if (hashPassword.startsWith("sha1:")) { + String hash = hashString(password).toLowerCase(); + return hashPassword.toLowerCase().equals(hash); + } else if (hashPassword.startsWith("plain:")) { + return hashPassword.equals("plain:" + password); + } else { + return hashPassword.equals(password); + } + } + + /** + * Create a hashed version of a password. + * @param sPassword The password to hash. + * @return A hashed version prefixed by the hashing method. + */ + public static String hashString(String sPassword) { + if (sPassword == null || sPassword.equals("")) { + return "empty:"; + } else { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(sPassword.getBytes("UTF-8")); + byte[] res = md.digest(); + return "sha1:" + Hexadecimal.byte2hex(res); + } catch (NoSuchAlgorithmException e) { + return "plain:" + sPassword; + } catch (UnsupportedEncodingException e) { + return "plain:" + sPassword; + } + } + } +} diff --git a/src/main/java/org/pasteque/common/util/Hexadecimal.java b/src/main/java/org/pasteque/common/util/Hexadecimal.java new file mode 100644 index 0000000..8d0275c --- /dev/null +++ b/src/main/java/org/pasteque/common/util/Hexadecimal.java @@ -0,0 +1,56 @@ +package org.pasteque.common.util; + +/** + *

Binary to hexadecimal format encoder/decoder. + * Not available natively until Java 17.

+ *

This class is there for retrocompatibility with password encoding. + * Use {@link org.pasteque.common.datatransfer.dto.format.BinaryDTOFormat} + * to encode binary data as base64 strings instead.

+ */ +/* package */ class Hexadecimal +{ + private static final char [] hexchars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + /** + * Convert binary data to an hexadecimal string. + * @param data The raw data to encode. + * @return The hexadecimal representation of the binary data, + * without any prefix. + */ + public static String byte2hex(byte[] data) { + StringBuffer sb = new StringBuffer(data.length * 2); + for (int i = 0; i < data.length; i++) { + int high = ((data[i] & 0xF0) >> 4); + int low = (data[i] & 0x0F); + sb.append(hexchars[high]); + sb.append(hexchars[low]); + } + return sb.toString(); + } + + /** + * Convert an hexadecimal string to binary data. + * @param hexa The hexadecimal string, without any prefix. + * @return The binary data. + * @throws IllegalArgumentException When the hexadecimal string + * has an odd number of characters. + */ + public static byte[] hex2byte(String hexa) { + int length = hexa.length(); + if ((length & 0x01) != 0) { + throw new IllegalArgumentException("odd number of characters."); + } + byte[] out = new byte[length >> 1]; + // two characters form the hex value. + for (int i = 0, j = 0; j < length; i++) { + int f = Character.digit(hexa.charAt(j++), 16) << 4; + f = f | Character.digit(hexa.charAt(j++), 16); + out[i] = (byte) (f & 0xFF); + } + + return out; + } + + /** Static utility-class, cannot be instantiated. */ + private Hexadecimal() { } +} diff --git a/src/main/java/org/pasteque/common/util/package-info.java b/src/main/java/org/pasteque/common/util/package-info.java new file mode 100644 index 0000000..42e5848 --- /dev/null +++ b/src/main/java/org/pasteque/common/util/package-info.java @@ -0,0 +1,4 @@ +/** + * Utility classes and general tools. + */ +package org.pasteque.common.util; diff --git a/src/main/java/org/pasteque/common/view/Buttonable.java b/src/main/java/org/pasteque/common/view/Buttonable.java new file mode 100644 index 0000000..d00da9f --- /dev/null +++ b/src/main/java/org/pasteque/common/view/Buttonable.java @@ -0,0 +1,24 @@ +package org.pasteque.common.view; + +import java.io.IOException; +import org.pasteque.common.datasource.ImageDataSource; + +/** + * Interface for records that can be represented as a selection button. + */ +public interface Buttonable +{ + /** + * Get the label to show for this button. + * @return The label of the button. + */ + public String getLabel(); + + /** + * Get the image to use for this button. + * @param source The source to use when the image is not already loaded. + * @return The image of the button. + * @throws IOException When an error occurs while reading the source. + */ + public byte[] getImage(ImageDataSource source) throws IOException; +} diff --git a/src/main/java/org/pasteque/common/view/CoordStretcher.java b/src/main/java/org/pasteque/common/view/CoordStretcher.java new file mode 100644 index 0000000..905c278 --- /dev/null +++ b/src/main/java/org/pasteque/common/view/CoordStretcher.java @@ -0,0 +1,185 @@ +// Openbravo POS is a point of sales application designed for touch screens. +// Copyright (C) 2007-2009 Openbravo, S.L. +// http://www.openbravo.com/product/pos +// +// This file is part of Openbravo POS. +// +// Openbravo POS is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Openbravo POS 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Openbravo POS. If not, see . + +package org.pasteque.common.view; + +/** + * Utility class to recompute coordinates of place buttons inside a floor panel + * according to the size of the container. + */ +public class CoordStretcher +{ + /** The expected button width in pixels. */ + private final int buttonWidth; + /** The expected button height in pixels. */ + private final int buttonHeight; + /** Minimum x position of a table to crop unused space. */ + private int xMin; + /** Maximum x position of a table to crop unused space. */ + private int xMax; + /** Minimum y position of a table to crop unused space. */ + private int yMin; + /** Maximum y position of a table to crop unused space. */ + private int yMax; + /** The expected width of the container in pixels. */ + private int containerWidth; + /** The expected height of the container in pixels. */ + private int containerHeight; + /** The minimum margin for the container, top, right, bottom, left. */ + private int[] margin; + /** The actual margin to add to center the floor in its container. */ + private int marginLeft; + /** The actual margin to add to center the floor in its container. */ + private int marginTop; + private float coordFactor; + /** Whether the size and margins should be recomputed or not. */ + private boolean dirty; + + /** + * Create a stretcher to compute coordinate with the given GUI settings. + * @param buttonWidth The width in pixel of a button. + * @param buttonHeight The height in pixel of a button. + * @param margin The minimum margin between the border of the container + * and a button. + */ + public CoordStretcher(int buttonWidth, int buttonHeight, int margin) { + this(buttonWidth, buttonHeight, margin, margin, margin, margin); + } + + /** + * Create a stretcher to compute coordinate with the given GUI settings. + * @param buttonWidth The width in pixel of a button. + * @param buttonHeight The height in pixel of a button. + * @param marginTop The minimum margin between the border of the container + * and a button. + * @param marginRight The minimum margin between the border of the container + * and a button. + * @param marginBottom The minimum margin between the border of the container + * and a button. + * @param marginLeft The minimum margin between the border of the container + * and a button. + */ + public CoordStretcher(int buttonWidth, int buttonHeight, int marginTop, + int marginRight, int marginBottom, int marginLeft) { + this.buttonWidth = buttonWidth; + this.buttonHeight = buttonHeight; + this.margin = new int[] {marginTop, marginRight, marginBottom, marginLeft}; + this.xMin = Integer.MAX_VALUE; + this.xMax = Integer.MIN_VALUE; + this.yMin = Integer.MAX_VALUE; + this.yMax = Integer.MIN_VALUE; + this.coordFactor = 1.0f; + } + + /** + * Extends the local coordinates to contains the given location. + * The coordinates will be recomputed by removing the unused space + * on the edges. + * @param x The x coordinate of the location. It should match the left side + * of a button. + * @param y The y coordinate of the location. It should match the top side + * of a button. + */ + public void extendBoundsFor(int x, int y) { + if (x < this.xMin) { + this.xMin = x; + this.dirty = true; + } + if (x > this.xMax) { + this.xMax = x; + this.dirty = true; + } + if (y < this.yMin) { + this.yMin = y; + this.dirty = true; + } + if (y > this.yMax) { + this.yMax = y; + this.dirty = true; + } + } + + /** + * Indicate the size of the container. The content will be stretched + * to fit in this size. + * @param width The width in pixel of the container, including margin. + * @param height The height in pixel of the container, including margin. + */ + public void setContainerSize(int width, int height) { + this.containerWidth = width; + this.containerHeight = height; + this.dirty = true; + } + + /** + * Recompute the stretching factor to fill the container while respecting + * the aspect ratio. + */ + private void recomputeRatio() { + int contentWidth = this.containerWidth - this.margin[3] - this.margin[1]; + int contentHeight = this.containerHeight - this.margin[0] - this.margin[2]; + int floorWidth = this.xMax - this.xMin; + int floorHeight = this.yMax - this.yMin; + this.coordFactor = Math.min( + Float.valueOf(contentWidth - this.buttonWidth) / floorWidth, + Float.valueOf(contentHeight - this.buttonHeight) / floorHeight); + this.marginLeft = Double.valueOf(Math.floor((contentWidth - (floorWidth * coordFactor + this.buttonWidth)) / 2)).intValue() + this.margin[3]; + this.marginTop = Double.valueOf(Math.floor((contentHeight - (floorHeight * coordFactor + this.buttonHeight)) / 2)).intValue() + this.margin[0]; + } + + /** + * Compute new coordinates to use most space inside the container. + * @param x The x coordinate. Often the coordinate of a Place. + * @param y The y coordinate. Often the coordiante of a Place. + * @return The new stretched coordinates + */ + public Point stretchCoord(int x, int y) { + if (this.containerWidth == 0 || this.containerHeight == 0) { + return new Point(x, y); + } + if (this.dirty) { + this.recomputeRatio(); + this.dirty = false; + } + int localX = x - this.xMin; + int localY = y - this.yMin; + int posX = Double.valueOf(Math.floor(localX * this.coordFactor)).intValue() + this.marginLeft; + int posY = Double.valueOf(Math.floor(localY * this.coordFactor)).intValue() + this.marginTop; + return new Point(posX, posY); + } + + /** + * A coordinate couple. + */ + public class Point { + /** The X coordinate. */ + public int x; + /** The Y coordinate. */ + public int y; + /** + * Create a new Point + * @param x The x coordinate. + * @param y The y coordinate. + */ + public Point(int x, int y) { + this.x = x; + this.y = y; + } + } +} diff --git a/src/main/java/org/pasteque/common/view/Formatter.java b/src/main/java/org/pasteque/common/view/Formatter.java new file mode 100644 index 0000000..94c437d --- /dev/null +++ b/src/main/java/org/pasteque/common/view/Formatter.java @@ -0,0 +1,141 @@ +package org.pasteque.common.view; + +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Date; +import org.pasteque.common.model.Currency; +import org.pasteque.coreutil.price.Price2; +import org.pasteque.coreutil.price.Price5; + +/** + * Utility class to display various values in a user-friendly format. + */ +public class Formatter +{ + /** See {@link formatPercent(double). */ + private NumberFormat percentFormat; + private DateFormat dateFormat; + private DateFormat timeFormat; + private DateFormat dateTimeFormat; + /** The default currency to use for prices. */ + private Currency defaultCurrency; + + /** + * Create a formatter. + * @param defaultCurrency The currency to use by default to format prices. + * Null for no formatting. + */ + public Formatter(Currency defaultCurrency) { + this.percentFormat = new DecimalFormat("#,##0.##%"); + this.dateFormat = DateFormat.getDateInstance(); + this.timeFormat = DateFormat.getTimeInstance(); + this.dateTimeFormat = DateFormat.getDateTimeInstance(); + this.defaultCurrency = defaultCurrency; + } + + /** + * Display a rate as a percent. + * @param rate The rate to convert. + * @return The formatted percent with either no decimals or 2 decimals. + */ + public String formatPercent(double rate) { + double value = rate * 100.00; + long alldigits = Math.round(Math.abs(rate) * 100000.0); + boolean showDigits = (alldigits % 1000 >= 5); + if (showDigits) { + this.percentFormat.setMinimumFractionDigits(0); + this.percentFormat.setMaximumFractionDigits(0); + } else { + this.percentFormat.setMinimumFractionDigits(2); + this.percentFormat.setMaximumFractionDigits(2); + } + return this.percentFormat.format(value); + } + + private String formatNumber(double value, int decimals) { + NumberFormat format = new DecimalFormat("#,##0.##"); + format.setMaximumFractionDigits(decimals); + format.setMinimumFractionDigits(decimals); + return format.format(value); + } + + /** + * Format a price with 2 decimals according to the default currency. + * @param price The price to format. + * @return The formatted price as text. + */ + public String formatPrice(Price2 price) { + if (this.defaultCurrency == null) { + return this.formatNumber(price.toDouble(), 2); + } + return this.defaultCurrency.formatValue(price.toDouble(), 2); + } + + /** + * Format a price with 2 decimals. + * @param price The price to format. + * @param currency The currency of the price. When null, format + * as a plain number with 2 decimals. + * @return The formatted price as text. + */ + public String formatPrice(Price2 price, Currency currency) { + if (currency == null) { + return this.formatNumber(price.toDouble(), 2); + } + return currency.formatValue(price.toDouble(), 2); + } + + /** + * Format a price with 5 decimals according to the default currency. + * @param price The price to format. + * @return The formatted price as text. + */ + public String formatPrice(Price5 price) { + if (this.defaultCurrency == null) { + return this.formatNumber(price.toDouble(), 5); + } + return this.defaultCurrency.formatValue(price.toDouble(), 5); + } + + /** + * Format a price with 5 decimals. + * @param price The price to format. + * @param currency The currency of the price. When null, format + * as a plain number with 5 decimals. + * @return The formatted price as text. + */ + public String formatPrice(Price5 price, Currency currency) { + if (currency == null) { + return this.formatNumber(price.toDouble(), 5); + } + return currency.formatValue(price.toDouble(), 5); + } + + /** + * Format a date. + * @param date The date to print. + * @return The full date in the system locale. + */ + public String formatDate(Date date) { + return this.dateFormat.format(date); + } + + /** + * Format a time. + * @param date The date to print the time from. + * @return The time in the system locale. + */ + public String formatTime(Date date) { + return this.timeFormat.format(date); + } + + /** + * Format a date with time. + * @param dateTime The date to print. + * @return The date and time in the system locale. + */ + public String formatDateTime(Date dateTime) { + return this.dateTimeFormat.format(dateTime); + } +} diff --git a/src/main/java/org/pasteque/common/view/package-info.java b/src/main/java/org/pasteque/common/view/package-info.java new file mode 100644 index 0000000..0831097 --- /dev/null +++ b/src/main/java/org/pasteque/common/view/package-info.java @@ -0,0 +1,10 @@ +/** + *

Utilities to define views and widgets. This package contains tools + * to present data and ease the creation of widgets or components. + * As views are system-dependent, this package will not contain GUI + * components directly, but utilities to ease the presentation and + * provide a consistent behaviour between implementations.

+ *

Widgets should restrict themselves to the view layer and rely as + * much as possible on this package to get data.

+ */ +package org.pasteque.common.view; diff --git a/src/main/java/org/pasteque/coreutil/ImmutableList.java b/src/main/java/org/pasteque/coreutil/ImmutableList.java new file mode 100644 index 0000000..e42d871 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/ImmutableList.java @@ -0,0 +1,111 @@ +package org.pasteque.coreutil; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.function.Consumer; + +/** + * Immutable proxy-class of a list. + * @param The type of content of the list. + */ +public final class ImmutableList implements Iterable, Serializable +{ + private static final long serialVersionUID = 4982637247874555568L; + + /** See {@link get(int)} or {@link iterator()}. */ + private List content; + + /** + * Create an empty immutable list. + */ + public ImmutableList() { + this.content = new ArrayList(); + } + + /** + * Create an immutable list from an existing collection. + * The references are copied, modifying the initial list + * will not change the content of this list. + * @param list The original list to make an immutable copy from. + * If null, an empty list is created. + */ + public ImmutableList(Collection list) { + if (list == null) { + this.content = new ArrayList(); + } else { + @SuppressWarnings("unchecked") + T[] copy = (T[]) list.toArray(); + this.content = Arrays.asList(copy); + } + } + + /** + * Create an immutable list from an existing array. + * The references are copied, modifying the initial array + * will not change the content of this list. + * @param elements The original array to make an immutable copy from. + * If null, an empty list is created. + */ + public ImmutableList(T[] elements) { + if (elements == null) { + this.content = new ArrayList(); + } else { + T[] copy = Arrays.copyOf(elements, elements.length); + this.content = Arrays.asList(copy); + } + } + + /** + * See {@link java.util.List#get(int)}. + * @param index The index of the element. + * @return The element at the given index. + */ + public T get(int index) { + return this.content.get(index); + } + + /** + * See {@link java.util.List#size()}. + * @return The size of the list. + */ + public int size() { + return this.content.size(); + } + + /** + * See {@link java.lang.Iterable#forEach(java.util.function.Consumer)}. + */ + @Override + public void forEach(Consumer action) { + this.content.forEach(action); + } + + /** + * See {@link java.util.List#iterator()}. + */ + @Override + public Iterator iterator() { + return this.content.iterator(); + } + + /** + * See {@link java.util.List#spliterator()}. + */ + @Override + public Spliterator spliterator() { + return this.content.spliterator(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return this.content.toString(); + } +} diff --git a/src/main/java/org/pasteque/coreutil/ParseException.java b/src/main/java/org/pasteque/coreutil/ParseException.java new file mode 100644 index 0000000..25888fb --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/ParseException.java @@ -0,0 +1,26 @@ +package org.pasteque.coreutil; + +/** + * Data cannot be parsed. + */ +public class ParseException extends Exception +{ + private static final long serialVersionUID = 3552338953002779225L; + + /** + * Create a generic exception with only a message. + * @param message {@inheritDoc}. + */ + public ParseException(String message) { + super(message); + } + + /** + * Create an exception caused by a more specific exception. + * @param message {@inheritDoc}. + * @param cause The more specific exception. + */ + public ParseException(String message, Exception cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/pasteque/coreutil/constants/DiscountTarget.java b/src/main/java/org/pasteque/coreutil/constants/DiscountTarget.java new file mode 100644 index 0000000..9b0b6d1 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/constants/DiscountTarget.java @@ -0,0 +1,84 @@ +package org.pasteque.coreutil.constants; + +/** + *

Rule to check from where the discount applies itself. It defines where the + * rounding will happen first.

+ */ +public enum DiscountTarget +{ + /** + *

Apply the discount to the unit price without taxes.

+ *

When applied to a line, the unit price is reduced, the unit price + * with taxes is recomputed from there.

+ *

When applied to an order, the discount is added to the one + * from the line.

+ */ + UNIT("unit"), + /** + *

Apply the discount to the unit price with taxes. + * The discount will not affect fixed tax amounts and will apply a + * greater discount rate to the base price to compensate them.

+ *

When applied to a line, the unit price with taxes is reduced, which + * will result in a new unit price without taxes on 5 decimals.

+ *

When applied to an order, the discount is added to the one + * from the line.

+ */ + UNIT_TAXED("unitTaxed"), + /** + *

Target for a discount applied to the non-taxed total.

+ *

When applied to a line, the total without tax is reduced, the total + * with taxes is recomputed from there.

+ *

When applied to an order, the total without tax is reduced and the + * amount is also dispatched into each line with a precision of 5 decimal. + * Thus each line will also have a total on 5 decimals. The taxed prices + * are recomputed from there and may introduce some unavoidable rounding + * approximations in the base amounts for each tax.

+ */ + TOTAL("total"), + /** + *

Target for a discount applied to the taxed total. + * The discount will not affect fixed tax amounts and will apply a + * greater discount rate to the base price to compensate them.

+ *

When applied to a line, the total with tax is reduced, which will + * result in new total without taxes on 5 decimals.

+ *

When applied to an order, the total with taxes is reduced. New total + * without taxes are recomputed from there and may introduce some unavoidable + * rounding approximations.

+ */ + TOTAL_TAXED("totalTaxed"); + + /** {@see getCode()} */ + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static final DiscountTarget fromCode(String code) throws IllegalArgumentException { + for (DiscountTarget v : DiscountTarget.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(code); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + private DiscountTarget(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public final String getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/coreutil/constants/DiscountType.java b/src/main/java/org/pasteque/coreutil/constants/DiscountType.java new file mode 100644 index 0000000..b425fba --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/constants/DiscountType.java @@ -0,0 +1,52 @@ +package org.pasteque.coreutil.constants; + +/** + * Types for discounts. The type defines which rule is used to compute the + * discount. + */ +public enum DiscountType +{ + /** + * Type for a discount relative to the price. + */ + RATE("rate"), + /** + * Type for a discount of a fixed amount. + */ + VALUE("value"); + + /** {@see getCode()} */ + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static final DiscountType fromCode(String code) throws IllegalArgumentException { + for (DiscountType v : DiscountType.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(code); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + private DiscountType(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public final String getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/coreutil/constants/FiscalTicketType.java b/src/main/java/org/pasteque/coreutil/constants/FiscalTicketType.java new file mode 100644 index 0000000..85f3369 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/constants/FiscalTicketType.java @@ -0,0 +1,53 @@ +package org.pasteque.coreutil.constants; + +/** + * Types for fiscal tickets. Fiscal tickets can contain multiple kind + * of tickets in immutable raw format. The type is somehow like a + * MIME type. + */ +public enum FiscalTicketType +{ + /** Type for regular tickets, for a single transaction. Code {@code tkt}. */ + TICKET("tkt"), + /** + * Type for aggregated tickets, one for each cash session with all + * the consolidated figures. Code {@code z}. + */ + Z_TICKET("z"); + + /** {@see getCode()} */ + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static final FiscalTicketType fromCode(String code) throws IllegalArgumentException { + for (FiscalTicketType v : FiscalTicketType.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(code); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + private FiscalTicketType(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public final String getCode() { + return this.code; + } + +} diff --git a/src/main/java/org/pasteque/coreutil/constants/PaymentModeType.java b/src/main/java/org/pasteque/coreutil/constants/PaymentModeType.java new file mode 100644 index 0000000..8b3330f --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/constants/PaymentModeType.java @@ -0,0 +1,116 @@ +package org.pasteque.coreutil.constants; + +/** + *

Payment mode type enumeration.

+ *

Payment mode are using flags to determine if special behaviors or checks + * should be triggered when the payment mode is used. But theses flags are not + * Independent and only a limited set of values make sense.

+ *

This enum lists the common values for payment mode types.

+ */ +public enum PaymentModeType +{ + /** + * The payment mode does not have special effects. + * Code: {@code 0}. + */ + DEFAULT(0), + /** + * The payment mode requires a customer account to be used. + * Code: {@code 1}. + */ + CUSTOMER(1), + /** + * The payment mode requires a customer account, uses their balance + * and allows to contract debt (negative balance). + * Code: {@code 3}. + */ + DEBT(3), + /** + * The payment mode requires a customer account, uses their balance + * but do not allow them to contract debt (keep a positive or zero balance). + * Code: {@code 5}. + */ + PREPAID(5); + + /** + * Bit that indicates that the payment mode requires a customer account + * to be used. + * Code: {@code 0x01}. + * @see requiresCustomer() + */ + public static final int FLAG_CUSTOMER = 0x01; + /** + * Bit that indicates that the payment mode will use the customer's balance, + * and allows the customer to contract debt. + * Code: {@code 0x02}. + * @see allowsDebt() + */ + public static final int FLAG_DEBT = 0x02; + /** + * Bit that indicates that the payment mode will use the customer's balance, + * but not allow the customer to contract debt. + * Code: {@code 0x04}. + * {@see allowsPrepaid()} + */ + public static final int FLAG_PREPAID = 0x04; + + /** {@see getCode()} */ + private final int code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static PaymentModeType fromCode(int code) throws IllegalArgumentException { + for (PaymentModeType v : PaymentModeType.values()) { + if (v.getCode() == code) { + return v; + } + } + throw new IllegalArgumentException(Integer.valueOf(code).toString()); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + PaymentModeType(int code) { + this.code = code; + } + + /** + * Whether payment modes of this type requires a customer account. + * @return True when the type has the {@link FLAG_CUSTOMER} flag set. + */ + public boolean requiresCustomer() { + return (this.code & FLAG_CUSTOMER) > 0; + } + + /** + * Whether payment modes of this type allows to contract debt. + * @return True when the type has the {@link FLAG_DEBT} flag set. + */ + public boolean allowsDebt() { + return (this.code & FLAG_DEBT) > 0; + } + + /** + * Whether payment modes of this type uses the customer's positive balance. + * @return True when the type has the {@link FLAG_PREPAID} + * or {@link FLAG_DEBT} flag set. + */ + public boolean allowPrepaid() { + return (this.code & (FLAG_PREPAID | FLAG_DEBT)) > 0; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public int getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/coreutil/constants/SessionCloseType.java b/src/main/java/org/pasteque/coreutil/constants/SessionCloseType.java new file mode 100644 index 0000000..4af7772 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/constants/SessionCloseType.java @@ -0,0 +1,60 @@ +package org.pasteque.coreutil.constants; + +/** + * Type of period to close when closing a session. + * TODO: experimental + */ +public enum SessionCloseType +{ + /** + * Close only the current session which is commonly a day of work. + * Code {@code 0}. + */ + SIMPLE(0), + /** + * Close the current session and the current period, + * which is commonly a month. Code {@code 1}. + */ + PERIOD(1), + /** + * Close the current session and the fiscal year, which is commonly a year. + * Code {@code 2}. + */ + FISCAL_YEAR(2); + + /** {@see getCode()} */ + private final int code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static SessionCloseType fromCode(int code) throws IllegalArgumentException { + for (SessionCloseType v : SessionCloseType.values()) { + if (v.getCode() == code) { + return v; + } + } + throw new IllegalArgumentException(String.valueOf(code)); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + SessionCloseType(int code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public int getCode() { + return this.code; + } + +} diff --git a/src/main/java/org/pasteque/coreutil/constants/TaxType.java b/src/main/java/org/pasteque/coreutil/constants/TaxType.java new file mode 100644 index 0000000..a1c81a5 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/constants/TaxType.java @@ -0,0 +1,52 @@ +package org.pasteque.coreutil.constants; + +/** + * Types for taxes. The type defines which rule is used to compute tax amounts + * from a base amount. + */ +public enum TaxType +{ + /** + * Type for rate applied to a base amount. + */ + RATE("rate"), + /** + * Type for a fixed amount applied to each unit. + */ + FIXED_QUANTITY("fixedQuantity"); + + /** {@see getCode()} */ + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static final TaxType fromCode(String code) throws IllegalArgumentException { + for (TaxType v : TaxType.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(code); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + private TaxType(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public final String getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/coreutil/constants/package-info.java b/src/main/java/org/pasteque/coreutil/constants/package-info.java new file mode 100644 index 0000000..d656535 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/constants/package-info.java @@ -0,0 +1,4 @@ +/** + * Core feature constants and enumerations. + */ +package org.pasteque.coreutil.constants; diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/CashRegisterDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/CashRegisterDTO.java new file mode 100644 index 0000000..6d30259 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/CashRegisterDTO.java @@ -0,0 +1,98 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityException; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Physical cash register hardware Data Transfer Object.

+ *

All tickets must be assigned to a cash register and a cash session.

+ */ +public final class CashRegisterDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 1079581813013284003L; + + /** {@see getReference()} */ + private String reference; + + /** {@see getLabel()} */ + private String label; + + /** {@see getNextTicketNumber()} */ + private int nextTicketNumber; + + /** + * Create from all fields. + * @param reference The unique reference. + * @param label The display name. + * @param nextTicketNumber See {@link getNextTicketNumber}. + */ + public CashRegisterDTO( + String reference, + String label, + int nextTicketNumber) { + this.reference = reference; + this.label = label; + this.nextTicketNumber = nextTicketNumber; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public CashRegisterDTO(Reader reader) throws ParseException { + this.reference = reader.readString("reference"); + this.label = reader.readString("label"); + this.nextTicketNumber = reader.readInt("nextTicketId"); + } + + /** + * Get the reference. It is a user friendly identifier. + * @return The reference. + */ + public String getReference() { + return this.reference; + } + + /** + * Get the display name. + * @return The label of this cash register. + */ + public String getLabel() { + return this.label; + } + + /** + * Get the number to assign to the next ticket from the situation known by the sender. + * It may not be solely accurate when other tickets are stored locally. + * @return The number to assign to the next ticket. + */ + public int getNextTicketNumber() { + return nextTicketNumber; + } + + /** + * Check if the reference and labels are set and nextTicketNumber is + * positive or zero. + * @throws IntegrityExceptions When the constraints are not satisfied. + */ + public void checkIntegrity() throws IntegrityExceptions { + List list = new LinkedList(); + IntegrityExceptions.addCheck(list, InvalidFieldException.nonNull( + this.reference, "CashRegister", "reference", this.reference)); + IntegrityExceptions.addCheck(list, InvalidFieldException.nonNull( + this.label, "CashRegister", "label", this.reference)); + IntegrityExceptions.addCheck(list, InvalidFieldException.positive( + this.nextTicketNumber, "CashRegister", "nextTicketNumber", this.reference)); + if (!list.isEmpty()) { + throw new IntegrityExceptions(list); + } + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/CashSessionDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/CashSessionDTO.java new file mode 100644 index 0000000..e8c6324 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/CashSessionDTO.java @@ -0,0 +1,138 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import java.util.Date; +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.DTOFactory; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Non finished cash session Data Transfer Object.

+ *

There can be only one active session for each + * {@link org.pasteque.coreutil.datatransfer.dto.CashRegisterDTO}. Once a session is + * closed, a {@link org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO} is created + * and the next session is created in non-opened state.

+ */ +public final class CashSessionDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 2312930086844551950L; + + /** See {@link getCashRegister()}. */ + private WeakAssociationDTO cashRegister; + /** See {@link getSequence()}. */ + private int sequence; + /** See {@link isContinuous()}. */ + private boolean continuous; + /** See {@link getOpenDate()}. */ + private Date openDate; + /** See {@link getMovements()}. */ + private ImmutableList movements; + /** See {@link getTickets()}. */ + private ImmutableList tickets; + + /** + * Create a cash session from all fields. + * @param cashRegister See {@link getCashRegister()}. + * @param sequence See {@link getSequence()}. + * @param continuous See {@link isContinuous()}. + * @param openDate See {@link getOpenDate()}. + * @param movements See {@link getMovements()}. + * @param tickets See {@link getTickets()}. + */ + public CashSessionDTO( + WeakAssociationDTO cashRegister, + int sequence, + boolean continuous, + Date openDate, + ImmutableList movements, + ImmutableList tickets) { + this.cashRegister = cashRegister; + this.sequence = sequence; + this.continuous = continuous; + this.openDate = openDate; + this.movements = movements; + this.tickets = tickets; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public CashSessionDTO(Reader reader) throws ParseException { + reader.startObject("cashRegister"); + this.cashRegister = new WeakAssociationDTO(reader); + reader.endObject(); + this.sequence = reader.readInt("sequence"); + this.continuous = reader.readBoolean("continuous"); + this.openDate = reader.readDateOrNull("openDate"); + DTOFactory mvtFacto = new DTOFactory(reader, MovementDTO.class); + this.movements = mvtFacto.immutableReadObjects("movements"); + DTOFactory tktFacto = new DTOFactory(reader, FiscalTicketDTO.class); + this.tickets = tktFacto.immutableReadObjects("tickets"); + } + + /** + * Get the cash register associated to this session. + * @return The cash register. It may not link to an existing cash register. + */ + public WeakAssociationDTO getCashRegister() { + return this.cashRegister; + } + + /** + * Get the sequence number of this session. The sequence starts from 1 + * and is increased every time a new session is opened. There can be only + * one active (i.e. not closed) session for a cash register at a time. + * @return The sequence number of this session. + */ + public int getSequence() { + return this.sequence; + } + + /** + * Check if the session was continuous starting from the previous one. + * The session is continuous when it started on the same machine than + * the previous one and with the previous one in cache. + * @return True when the session is marked as continuous. + * @see org.pasteque.coreutil.datatransfer.dto.ZTicketDTO#isContinuous() + */ + public boolean isContinuous() { + return this.continuous; + } + + /** + * Get the date when the session was opened. + * @return The open date. It may be null if the session was not opened yet. + * @see org.pasteque.coreutil.datatransfer.dto.ZTicketDTO#getOpenDate() + */ + public Date getOpenDate() { + return this.openDate; + } + + /** + * Get the movements not justified by a ticket, including the starting + * amounts. + * @return The movements. Empty when not counted. + * @see org.pasteque.coreutil.datatransfer.dto.ZTicketDTO#getMovements() + */ + public ImmutableList getMovements() { + return this.movements; + } + + /** + * Get the list of tickets currently registered within this session. + * @return The tickets registered within this session. + */ + public ImmutableList getTickets() { + return this.tickets; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/DTOInterface.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/DTOInterface.java new file mode 100644 index 0000000..60c9dc5 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/DTOInterface.java @@ -0,0 +1,26 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +/*import org.pasteque.common.dto.parser.Writer;*/ + +/** + *

Common interface for DTO.

+ *

All DTO must have a constructor from a + * {@link org.pasteque.coreutil.datatransfer.parser.Reader} + * and some data.

+ */ +public interface DTOInterface +{ + /* public DTOImplementation(Reader reader) throws ParseException */ + + /** + * Check whether the DTO is well formed, have all required data + * and fulfill other integrity constraints. + * @throws IntegrityExceptions When at least one + * {@link org.pasteque.coreutil.datatransfer.integrity.IntegrityException} + * is thrown. + */ + public void checkIntegrity() throws IntegrityExceptions; + + /*public String encode(Writer writer);*/ +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/FiscalTicketDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/FiscalTicketDTO.java new file mode 100644 index 0000000..b78b07f --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/FiscalTicketDTO.java @@ -0,0 +1,161 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import java.util.Date; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Immutable and signed ticket records. Fiscal tickets are used to comply + * with the immutability and integrity check of the underlying ticket.

+ *

Fiscal tickets contains a ticket in raw format to stay immutable even + * after updates. The ticket is stored in the {@link getContent() content} and + * signed with the previous one.

+ *

The sequence, number and date of the fiscal ticket can differ from + * the one of the ticket in content. Fiscal tickets are stored in independent + * sequences, usually matching a cash register, with incremental number. + * The date of the fiscal ticket is the date of registration of the fiscal + * ticket, which may differ from the date of the ticket in content.

+ *

Number 0 is reserved for the special and only mutable fiscal ticket for + * the "End Of Sequence" (EOS). This fiscal ticket is always signed with the + * last fiscal ticket of the sequence and allows to check if a sequence is + * complete.

+ *

The signature is chained to the previous fiscal ticket in the same + * sequence. The signature is computed based upon the sequence, number and + * content and the signature of the previous fiscal ticket in the same sequence.

+ *

For tickets that can be read easily, + * see {@link org.pasteque.coreutil.datatransfer.dto.TicketDTO} and + * {@link org.pasteque.coreutil.datatransfer.dto.ZTicketDTO}. + */ +public final class FiscalTicketDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 1622469748625149642L; + + /** See {@link getType()}. */ + private String type; + /** See {@link getSequence()}. */ + private String sequence; + /** See {@link getNumber()}. */ + private int number; + /** See {@link getCreateDate()}. */ + private Date createDate; + /** See {@link getContent()}. */ + private String content; + /** See {@link getSignature()}. */ + private String signature; + /** See {@link getWriteDate()}. */ + private Date writeDate; + + /** + * Create from all fields. + * @param type The type of ticket. + * @param sequence The sequence of the fiscal ticket. + * @param number The number of the fiscal ticket. + * @param createDate The creation date of the fiscal ticket. + * @param content The content of the underlying ticket. + * @param signature The chained signature of the record. + * @param writeDate The date when the fiscal ticket was received. + */ + public FiscalTicketDTO( + String type, + String sequence, + int number, + Date createDate, + String content, + String signature, + Date writeDate) { + this.type = type; + this.sequence = sequence; + this.number = number; + this.createDate = createDate; + this.content = content; + this.signature = signature; + this.writeDate = writeDate; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public FiscalTicketDTO(Reader reader) throws ParseException { + this.type = reader.readString("type"); + this.sequence = reader.readString("sequence"); + this.number = reader.readInt("number"); + this.createDate = reader.readDate("date"); + this.content = reader.readString("content"); + this.signature = reader.readString("signature"); + this.writeDate = reader.readDate("writeDate"); + } + + /** + * Get the type of this ticket. + * @see org.pasteque.coreutil.constants.FiscalTicketType + * @return The type of ticket in content. + */ + public String getType() { + return this.type; + } + + /** + * Get the identifier of the sequence. A sequence is generally identifying + * a cash register. + * @return The sequence of this ticket. + */ + public String getSequence() { + return this.sequence; + } + + /** + * Get the number of this fiscal ticket. The number is incremental + * within each session and starts from 1. + * Number 0 is reserved for the special "End Of Sequence" (EOF) ticket. + * @return The number of this ticket. + */ + public int getNumber() { + return this.number; + } + + /** + * Get the registration date of this fiscal ticket. + * It may be different from the date of the ticket in content. + * @return The registration date. + */ + public Date getCreateDate() { + return this.createDate; + } + + /** + * Get the raw representation of the registered ticket. + * @return The content of the ticket. + */ + public String getContent() { + return this.content; + } + + /** + * Get the signature of this fiscal ticket, chained with the previous one. + * @return The signature. + * @see org.pasteque.major.domain.FiscalTicket#checkSignature(org.pasteque.major.model.FiscalTicket) + */ + public String getSignature() { + return this.signature; + } + + /** + * Get the registration date of this fiscal ticket. + * It may be different from the date of the ticket in content + * and is only informational. + * @return The registration date. + */ + public Date getWriteDate() { + return this.writeDate; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/MovementDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/MovementDTO.java new file mode 100644 index 0000000..8b4695f --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/MovementDTO.java @@ -0,0 +1,102 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import java.util.Date; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Change in currency amounts not introduced by the payment of a ticket. + * This can be the initial amount in the cash drawer, or movements during + * the active session from external reasons, like paying an invoice + * or adding or removing cash from the drawer.

+ *

Movements are only used to detect errors when closing the session. + * They are not exhaustive for accounting.

+ */ +public final class MovementDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 2409481140667639418L; + + /** See {@link getPaymentMode()}. */ + private WeakAssociationDTO paymentMode; + /** See {@link getCurrency()}. */ + private WeakAssociationDTO currency; + /** See {@link getCurrencyAmount()}. */ + private double currencyAmount; + /** See {@link getDate()}. */ + private Date date; + + /** + * Create a movement from all fields. + * @param paymentMode See {@link getPaymentMode()}. + * @param currency See {@link getCurrency()}. + * @param currencyAmount See {@link getCurrencyAmount()}. + * @param date See {@link getDate()}. + */ + public MovementDTO( + WeakAssociationDTO paymentMode, + WeakAssociationDTO currency, + double currencyAmount, + Date date) { + this.paymentMode = paymentMode; + this.currency = currency; + this.currencyAmount = currencyAmount; + this.date = date; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public MovementDTO(Reader reader) throws ParseException { + reader.startObject("paymentMode"); + this.paymentMode = new WeakAssociationDTO(reader); + reader.endObject(); + reader.startObject("currency"); + this.currency = new WeakAssociationDTO(reader); + reader.endObject(); + this.currencyAmount = reader.readDouble("currencyAmount"); + this.date = reader.readDate("date"); + } + + /** + * Get the payment mode used for this movement. + * @return The payment mode. It may not link to an existing payment mode. + */ + public WeakAssociationDTO getPaymentMode() { + return this.paymentMode; + } + + /** + * Get the currency used for this movement. + * @return The currency. It may not link to an existing currency. + */ + public WeakAssociationDTO getCurrency() { + return this.currency; + } + + /** + * Get the amount of the movement, in the currency of the movement. + * @return The amount of the movement, in the currency of the movement. + */ + public double getCurrencyAmount() { + return this.currencyAmount; + } + + /** + * Get the date when the movement was done. + * @return The date when the movement was done. + */ + public Date getDate() { + return this.date; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/PaymentDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/PaymentDTO.java new file mode 100644 index 0000000..e712830 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/PaymentDTO.java @@ -0,0 +1,100 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Payment amount Data Transfer Object. A payment may refer to a single one + * or aggregated data for the same mode and currency.

+ *

Used in {@link org.pasteque.coreutil.datatransfer.dto.TicketDTO} for + * individual payment in {@link org.pasteque.coreutil.datatransfer.dto.ZTicketDTO} + * for aggregated amount by payment mode and currency.

+ */ +public final class PaymentDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 3195260108460565293L; + + /** See {@link getPaymentMode()}. */ + private WeakAssociationDTO paymentMode; + /** See {@link getCurrency()}. */ + private WeakAssociationDTO currency; + /** See {@link getAmount()}. */ + private double amount; + /** See {@link getCurrencyAmount()}. */ + private double currencyAmount; + + /** + * Create a payment from all fields. + * @param paymentMode See {@link getPaymentMode()}. + * @param amount See {@link getAmount()}. + * @param currency See {@link getCurrency()}. + * @param currencyAmount See {@link getCurrencyAmount()}. + */ + public PaymentDTO( + WeakAssociationDTO paymentMode, + double amount, + WeakAssociationDTO currency, + double currencyAmount) { + this.paymentMode = paymentMode; + this.amount = amount; + this.currency = currency; + this.currencyAmount = currencyAmount; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public PaymentDTO(Reader reader) throws ParseException { + reader.startObject("paymentMode"); + this.paymentMode = new WeakAssociationDTO(reader); + reader.endObject(); + this.amount = reader.readDouble("amount"); + reader.startObject("currency"); + this.currency = new WeakAssociationDTO(reader); + reader.endObject(); + this.currencyAmount = reader.readDouble("currencyAmount"); + } + + /** + * Get the payment mode used at registration time. + * @return The payment mode. It may not link to a registered payment mode. + */ + public WeakAssociationDTO getPaymentMode() { + return this.paymentMode; + } + + /** + * Get the amount in main currency for this payment. + * @return The amount of this payment in the main currency. + */ + public double getAmount() { + return this.amount; + } + + /** + * Get the currency used at registration time. + * @return The currency. It may not link to a registered currency. + */ + public WeakAssociationDTO getCurrency() { + return this.currency; + } + + /** + * Get the amount in the currency used for this payment. + * @return The amount in the currency of this payment. + */ + public double getCurrencyAmount() { + return this.currencyAmount; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/TaxAmountDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/TaxAmountDTO.java new file mode 100644 index 0000000..77aaacd --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/TaxAmountDTO.java @@ -0,0 +1,114 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Tax amount Data Transfer Object. A tax amount may refer to a single one + * or aggregated data for a single line, a ticket or a Z ticket.

+ *

Due to aggregation and rounding, the amount may not be strictly equals + * from the base multiplied by the tax rate.

+ *

A tax amount is always expressed in the main currency

+ */ +public final class TaxAmountDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 7429827880917696693L; + + /** See {@link getTax()}. */ + private WeakAssociationDTO tax; + /** See {@link getTaxRate()}. */ + private double taxRate; + /** See {@link getBase()}. */ + private double base; + /** See {@link getAmount()}. */ + private double amount; + /** See {@link getIncludedInBase()}. */ + private boolean includedInBase; + + /** + * Create a tax from all fields. + * @param tax See {@link getTax()}. + * @param taxRate See {@link getTaxRate()}. + * @param base See {@link getBase()}. + * @param amount See {@link getAmount()}. + * @param includedInBase See {@link getIncludedInBase()}. + */ + public TaxAmountDTO( + WeakAssociationDTO tax, + double taxRate, + double base, + double amount, + boolean includedInBase) { + this.tax = tax; + this.taxRate = taxRate; + this.base = base; + this.amount = amount; + this.includedInBase = includedInBase; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public TaxAmountDTO(Reader reader) throws ParseException { + reader.startObject("tax"); + this.tax = new WeakAssociationDTO(reader); + reader.endObject(); + this.taxRate = reader.readDouble("taxRate"); + this.base = reader.readDouble("base"); + this.amount = reader.readDouble("amount"); + this.includedInBase = reader.readBoolean("includedInBase"); + } + + /** + * Get the tax at registration time. + * @return The tax. It may not link to a registered tax. + */ + public WeakAssociationDTO getTax() { + return this.tax; + } + + /** + * Get the rate to apply. + * @return The tax rate. + */ + public double getTaxRate() { + return this.taxRate; + } + + /** + * Get the base amount associated to this tax, used to compute the amount. + * @return The base amount in the main currency. + */ + public double getBase() { + return this.base; + } + + /** + * Get the amount of taxes collected for this tax. + * @return The amount of tax in the main currency. + */ + public double getAmount() { + return this.amount; + } + + /** + * Get whether this tax amount is included in the base or + * is added to it. + * @return True when the tax amount is included in the base amount. + * False when it must be added to it. + */ + public final boolean getIncludedInBase() { + return this.includedInBase; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/TaxDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/TaxDTO.java new file mode 100644 index 0000000..5bc26e8 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/TaxDTO.java @@ -0,0 +1,83 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + * Tax Data Transfer Object. + */ +public final class TaxDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 525507510660586665L; + /** See {@link getType()}. */ + private String type; + /** See {@link getAmount()}. */ + private double amount; + /** See {@link getIncludedInBase()}. */ + private boolean includedInBase; + + /** + * Create a tax from all fields. + * @param type See {@link getType()}. + * @param amount See {@link getAmount()}. + * @param includedInBase See {@link getIncludedInBase()}. + */ + public TaxDTO( + String type, + double amount, + boolean includedInBase) { + this.type = type; + this.amount = amount; + this.includedInBase = includedInBase; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public TaxDTO(Reader reader) throws ParseException { + this.type = reader.readString("type"); + this.amount = reader.readDouble("amount"); + this.includedInBase = reader.readBoolean("includedInBase"); + } + + /** + * Get the type of the tax. + * @return The type of the tax, which determines how amounts are computed. + * @see org.pasteque.coreutil.constants.TaxType + * @see org.pasteque.coreutil.price.Tax#getType() + */ + public String getType() { + return this.type; + } + + /** + * Get the amount used to compute tax amounts. + * @return The amount used to compute tax amounts. + * @see org.pasteque.coreutil.price.Tax#getAmount() + */ + public double getAmount() { + return this.amount; + } + + /** + * Get whether this tax amount is included in the base or + * is added to it. + * @return True when the tax amount is included in the base amount. + * False when it must be added to it. + * @see org.pasteque.coreutil.price.Tax#isIncludedInBase() + */ + public final boolean getIncludedInBase() { + return this.includedInBase; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/TicketDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/TicketDTO.java new file mode 100644 index 0000000..bf839c4 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/TicketDTO.java @@ -0,0 +1,547 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import java.util.Date; +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.DTOFactory; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Receipt of a finalized and paid order. Tickets are immutable unless changes + * are applied to the data structure. Because of that they cannot be signed.

+ *

All data are copied into this DTO to make tickets completely independent.

+ *

For completely immutable tickets that can be stored and archived see + * {@link org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO}. + */ +public final class TicketDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = 8831777799341373103L; + + /** See {@link getCashRegister()}. */ + private WeakAssociationDTO cashRegister; + /** See {@link getSequence()}. */ + private int sequence; + /** See {@link getNumber()}. */ + private int number; + /** See {@link getDate()}. */ + private Date date; + /** See {@link getUser()}. */ + private WeakAssociationDTO user; + /** See {@link getCustCount()}. */ + private Integer custCount; + /** See {@link getCustomer()}. */ + private WeakAssociationDTO customer; + /** See {@link getCustBalance()}. */ + private double custBalance; + /** See {@link getTariffArea()}. */ + private WeakAssociationDTO tariffArea; + /** See {@link getLines()}. */ + private ImmutableList lines; + /** See {@link getPayments()}. */ + private ImmutableList payments; + /** See {@link getTaxes()}. */ + private ImmutableList taxes; + /** See {@link getPrice()}. */ + private double price; + /** See {@link getTaxedPrice()}. */ + private double taxedPrice; + /** See {@link getDiscountProfile()}. */ + private WeakAssociationDTO discountProfile; + /** See {@link getDiscountRate()}. */ + private double discountRate; + /** See {@link getFinalPrice()}. */ + private double finalPrice; + /** See {@link getFinalTaxedPrice()}. */ + private double finalTaxedPrice; + + /** + * Create an ticket from all fields. + * @param cashRegister See {@link getCashRegister()}. + * @param sequence See {@link getSequence()}. + * @param number See {@link getNumber()}. + * @param date See {@link getDate()}. + * @param user See {@link getUser()}. + * @param custCount See {@link getCustCount()}. + * @param customer See {@link getCustomer()}. + * @param custBalance See {@link getCustBalance()}. + * @param tariffArea See {@link getTariffArea()}. + * @param lines See {@link getLines()}. + * @param payments See {@link getPayments()}. + * @param taxes See {@link getTaxes()}. + * @param price See {@link getPrice()}. + * @param taxedPrice See {@link getTaxedPrice()}. + * @param discountProfile See {@link getDiscountProfile()}. + * @param discountRate See {@link getDiscountRate()}. + * @param finalPrice See {@link getFinalPrice()}. + * @param finalTaxedPrice See {@link getFinalTaxedPrice()}. + */ + public TicketDTO( + WeakAssociationDTO cashRegister, + int sequence, + int number, + Date date, + WeakAssociationDTO user, + Integer custCount, + WeakAssociationDTO customer, + double custBalance, + WeakAssociationDTO tariffArea, + TicketLineDTO[] lines, + PaymentDTO[] payments, + TaxAmountDTO[] taxes, + double price, + double taxedPrice, + WeakAssociationDTO discountProfile, + double discountRate, + double finalPrice, + double finalTaxedPrice) { + this.cashRegister = cashRegister; + this.sequence = sequence; + this.number = number; + this.date = date; + this.user = user; + this.custCount = custCount; + this.customer = customer; + this.custBalance = custBalance; + this.tariffArea = tariffArea; + this.lines = new ImmutableList(lines); + this.payments = new ImmutableList(payments); + this.taxes = new ImmutableList(taxes); + this.price = price; + this.taxedPrice = taxedPrice; + this.discountProfile = discountProfile; + this.discountRate = discountRate; + this.finalPrice = finalPrice; + this.finalTaxedPrice = finalTaxedPrice; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public TicketDTO(Reader reader) throws ParseException { + reader.startObject("cashRegister"); + this.cashRegister = new WeakAssociationDTO(reader); + reader.endObject(); + this.sequence = reader.readInt("sequence"); + this.number = reader.readInt("number"); + this.date = reader.readDate("date"); + this.user = WeakAssociationDTO.fromReader(reader, "user"); + this.custCount = reader.readIntOrNull("custCount"); + this.customer = WeakAssociationDTO.fromReader(reader, "customer"); + this.custBalance = reader.readDouble("custBalance"); + this.tariffArea = WeakAssociationDTO.fromReader(reader, "tariffArea"); + this.price = reader.readDouble("price"); + this.taxedPrice = reader.readDouble("taxedPrice"); + this.discountProfile = WeakAssociationDTO.fromReader(reader, "discountProfile"); + this.discountRate = reader.readDouble("discountRate"); + this.finalPrice = reader.readDouble("finalPrice"); + this.finalTaxedPrice = reader.readDouble("finalTaxedPrice"); + DTOFactory lineFacto = new DTOFactory(reader, TicketLineDTO.class); + this.lines = lineFacto.immutableReadObjects("lines"); + DTOFactory taxFacto = new DTOFactory(reader, TaxAmountDTO.class); + this.taxes = taxFacto.immutableReadObjects("taxes"); + DTOFactory pmtFacto = new DTOFactory(reader, PaymentDTO.class); + this.payments = pmtFacto.immutableReadObjects("payments"); + } + + /** + * Get the unique reference for this ticket. + * @return The reference, formatted as {@code --}. + */ + public String getReference() { + return String.format("%d-%d-%d", + this.cashRegister, + this.sequence, + this.number); + } + + /** + * Get the id of the cash register. + * @return The id of the cash register. + */ + public WeakAssociationDTO getCashRegister() { + return this.cashRegister; + } + + /** + * Get the sequence of the cash register. + * @return The sequence of the cash register in which this ticket + * was created. + */ + public int getSequence() { + return this.sequence; + } + + /** + * Get the number of the ticket. + * @return The unique number of the ticket for the cash register. + */ + public int getNumber() { + return this.number; + } + + /** + * Get the date of creation (payment) of the ticket. + * @return The date when this ticket was paid and created. + */ + public Date getDate() { + return this.date; + } + + /** + * Get the user that created this ticket. + * @return The user. + */ + public WeakAssociationDTO getUser() { + return this.user; + } + + /** + * Get the number of customers set for this ticket. It is not bound + * to the customer's account. + * @return The number of customers set for this ticket. Null when no + * number was defined. + */ + public Integer getCustCount() { + return this.custCount; + } + + /** + * Get the customer's account linked to this ticket. + * @return The customer's account. Null when not assigned + * to a customer's account. + */ + public WeakAssociationDTO getCustomer() { + return this.customer; + } + + /** + * Get the variation of the customer's balance introduced by this ticket. + * @return The variation of the customer's balance. + */ + public double getCustBalance() { + return this.custBalance; + } + + /** + * Get the tariff area used for this ticket. + * @return The tariff area. Null when not using a tariff area. + */ + public WeakAssociationDTO getTariffArea() { + return this.tariffArea; + } + + /** + * Get the content of the order. + * @return The list of article lines. + */ + public ImmutableList getLines() { + return this.lines; + } + + /** + * Get the list of payments used to pay this ticket. + * @return The list of payments used to pay this ticket. + */ + public ImmutableList getPayments() { + return this.payments; + } + + /** + * Get the list of consolidated taxes after discounts. + * @return The list of tax amounts by tax. + */ + public ImmutableList getTaxes() { + return this.taxes; + } + + /** + * Get the price of the whole ticket without tax and before + * ticket discount. + * @return The price without tax nor ticket discount. + */ + public double getPrice() { + return this.price; + } + + /** + * Get the price of the whole ticket before applying the ticket discount. + * @return The price with taxes, without ticket discount. + */ + public double getTaxedPrice() { + return this.taxedPrice; + } + + /** + * Get the discount profile assigned to this ticket. + * @return The discount profile, null when not discount profile + * was assigned. + */ + public WeakAssociationDTO getDiscountProfile() { + return this.discountProfile; + } + + /** + * Get the actual discount rate applied to the whole ticket. + * @return The discount rate applied to the whole ticket. It may be set + * even without using a discount profile. + */ + public double getDiscountRate() { + return this.discountRate; + } + + /** + * Get the price without taxes including the ticket discount. + * @return The price without taxes after the ticket discount. + */ + public double getFinalPrice() { + return this.finalPrice; + } + + /** + * Get the taxed price including the ticket discount. This is the + * amount that had to be paid. + * @return The price with taxes after the ticket discount. + */ + public double getFinalTaxedPrice() { + return this.finalTaxedPrice; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } + + /** + * Content of the ticket. The line doesn't make use of a + * {@link org.pasteque.coreutil.datatransfer.dto.WeakAssociationDTO} for + * the product because the reference can be null. It duplicates all the + * required informations directly because most of them can be modified + * on the fly when making an order. + */ + public class TicketLineDTO implements DTOInterface, Serializable + { + private static final long serialVersionUID = -9065013247136552814L; + + /** See {@link getDispOrder()}. */ + private final int dispOrder; + /** See {@link getProductReference()}. */ + private final String productReference; + /** See {@link getProductLabel()}. */ + private final String productLabel; + /** See {@link getUnitPrice()}. */ + private final double unitPrice; + /** See {@link getTaxedUnitPrice()}. */ + private final double taxedUnitPrice; + /** See {@link getQuantity()}. */ + private final double quantity; + /** See {@link getPrice()}. */ + private final double price; + /** See {@link getTaxedPrice()}. */ + private final double taxedPrice; + /** See {@link getTax()}. */ + private final WeakAssociationDTO tax; + /** See {@link getTaxRate()}. */ + private final double taxRate; + /** See {@link getDiscountRate()}. */ + private final double discountRate; + /** See {@link getFinalPrice()}. */ + private final double finalPrice; + /** See {@link getFinalTaxedPrice()}. */ + private final double finalTaxedPrice; + + /** + * Create a line from all fields. + * @param dispOrder See {@link getDispOrder()}. + * @param productReference See {@link getProductReference()}. + * @param productLabel See {@link getProductLabel()}. + * @param unitPrice See {@link getUnitPrice()}. + * @param taxedUnitPrice See {@link getTaxedUnitPrice()}. + * @param quantity See {@link getQuantity()}. + * @param price See {@link getPrice()}. + * @param taxedPrice See {@link getTaxedPrice()}. + * @param tax See {@link getTax()}. + * @param taxRate See {@link getTaxRate()}. + * @param discountRate See {@link getDiscountRate()}. + * @param finalPrice See {@link getFinalPrice()}. + * @param finalTaxedPrice See {@link getFinalTaxedPrice()}. + */ + public TicketLineDTO( + int dispOrder, + String productReference, + String productLabel, + double unitPrice, + double taxedUnitPrice, + double quantity, + double price, + double taxedPrice, + WeakAssociationDTO tax, + double taxRate, + double discountRate, + double finalPrice, + double finalTaxedPrice) { + this.dispOrder = dispOrder; + this.productReference = productReference; + this.productLabel = productLabel; + this.unitPrice = unitPrice; + this.taxedUnitPrice = taxedUnitPrice; + this.quantity = quantity; + this.price = price; + this.taxedPrice = taxedPrice; + this.tax = tax; + this.taxRate = taxRate; + this.discountRate = discountRate; + this.finalPrice = finalPrice; + this.finalTaxedPrice = finalTaxedPrice; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public TicketLineDTO(Reader reader) throws ParseException { + this.dispOrder = reader.readInt("dispOrder"); + this.productReference = reader.readStringOrNull("productReference"); + this.productLabel = reader.readString("productLabel"); + this.unitPrice = reader.readDouble("unitPrice"); + this.taxedUnitPrice = reader.readDouble("taxedUnitPrice"); + this.quantity = reader.readDouble("quantity"); + this.price = reader.readDouble("price"); + this.taxedPrice = reader.readDouble("taxedPrice"); + reader.startObject("tax"); + this.tax = new WeakAssociationDTO(reader); + reader.endObject(); + this.taxRate = reader.readDouble("taxRate"); + this.discountRate = reader.readDouble("discountRate"); + this.finalPrice = reader.readDouble("finalPrice"); + this.finalTaxedPrice = reader.readDouble("finalTaxedPrice"); + } + + /** + * Get the number of this line, starting from 0. + * @return The index of this line in the ticket. + */ + public int getDispOrder() { + return this.dispOrder; + } + + /** + * Get the reference of the product. + * @return The reference of the product. Null when using a custom product. + */ + public String getProductReference() { + return this.productReference; + } + + /** + * Get the label of the product. It is always set even if no + * product was associated. + * @return The label of the product. + */ + public String getProductLabel() { + return this.productLabel; + } + + /** + * Get the price of one unit of the product without taxes. + * @return The price of one unit of the product without taxes. + */ + public double getUnitPrice() { + return this.unitPrice; + } + + /** + * Get the price of one unit of the product with taxes. + * @return The price of one unit of the product with taxes. + */ + public double getTaxedUnitPrice() { + return this.taxedUnitPrice; + } + + /** + * Get the quantity of product in this line. + * @return The quantity of product in this line. + */ + public double getQuantity() { + return this.quantity; + } + + /** + * Get the price of the line without taxes before discount. + * @return The price of the line without taxes before applying + * the discount of the line. It is not affected by the discount + * of the ticket. + */ + public double getPrice() { + return this.price; + } + + /** + * Get the price of the line with taxes before discount. + * @return The price of the line with taxes before applying + * the discount of the line. It is not affected by the discount + * of the ticket. + */ + public double getTaxedPrice() { + return this.taxedPrice; + } + + /** + * Get the id of the tax of the product. + * @return The id of the tax of the product. + */ + public WeakAssociationDTO getTax() { + return this.tax; + } + + /** + * Get the tax rate of this line. + * @return The tax rate applied for the product. + */ + public double getTaxRate() { + return this.taxRate; + } + + /** + * Get the discount rate applied to this line. + * @return The discount rate applied only to this line. + * It is independent from the discount of the ticket. + */ + public double getDiscountRate() { + return this.discountRate; + } + + /** + * Get the price without taxes for this line after applying + * the discount. + * @return The price without taxes for this line after applying + * the discount of the line. + */ + public double getFinalPrice() { + return this.finalPrice; + } + + /** + * Get the price with taxes for this line after applying + * the discount. + * @return The price with taxes for this line after applying + * the discount of the line. + */ + public double getFinalTaxedPrice() { + return this.finalTaxedPrice; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/WeakAssociationDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/WeakAssociationDTO.java new file mode 100644 index 0000000..f809df8 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/WeakAssociationDTO.java @@ -0,0 +1,87 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + * Minimal identifier to reference an other DTO. A WeakLinkDTO allows to track + * the referenced DTO but only includes the minimal requirements to be useable + * even if the referenced DTO is deleted or modified. + */ +public class WeakAssociationDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = -4033459839844194734L; + + /** See {@link getReference()}. */ + private final String reference; + /** See {@link getLabel()}. */ + private final String label; + + /** + * Create a weak association from all fields. + * @param reference The reference of the associated element at the time + * of creation. + * @param label The label of the associated element at the time + * of creation. + */ + public WeakAssociationDTO(String reference, String label) { + this.reference = reference; + this.label = label; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public WeakAssociationDTO(Reader reader) throws ParseException { + this.reference = reader.readString("reference"); + this.label = reader.readString("label"); + } + + /** + * Instantiate from raw data from a reader. It may be null. + * @param reader The reader that must be currently pointing to the parent + * of this object. + * @param objectName The key to read from the reader. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + * @return A new WeakAssociationDTO or null if the value of the key + * is null. + */ + public static WeakAssociationDTO fromReader(Reader reader, String objectName) throws ParseException { + if (reader.isNull(objectName)) { + return null; + } + reader.startObject(objectName); + WeakAssociationDTO ret = new WeakAssociationDTO(reader); + reader.endObject(); + return ret; + } + + /** + * Get the reference of the element as it was at the time of creation. + * It is not guaranteed that the associated element still even exists. + * @return The reference of the associated element at the time of creation. + */ + public String getReference() { + return this.reference; + } + + /** + * Get the label of the element as it was at the time of creation. + * It is not guaranteed that the associated element still even exists + * or still has the same label. + * @return The label of the associated element at the time of creation. + */ + public String getLabel() { + return this.label; + } + + public void checkIntegrity() throws IntegrityExceptions { + // TODO: check integrity + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/ZTicketDTO.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/ZTicketDTO.java new file mode 100644 index 0000000..b4e9e13 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/ZTicketDTO.java @@ -0,0 +1,574 @@ +package org.pasteque.coreutil.datatransfer.dto; + +import java.io.Serializable; +import java.util.Date; +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions; +import org.pasteque.coreutil.datatransfer.parser.DTOFactory; +import org.pasteque.coreutil.datatransfer.parser.Reader; + +/** + *

Receipt of a finalized cash session. Once a cash session is closed, + * a Z MajorTicket is generated to summarize sales, taxes, payments and changes to + * customer's balance. This summary can be registered in accounting.

+ *

For consistency with mutable data the ticket references to, only + * {@link org.pasteque.coreutil.datatransfer.dto.WeakAssociationDTO} are used to + * keep ZTickets completely independent.

+ *

Z Tickets are immutable unless changes are applied to the data structure. + * Because of that they cannot be signed. For completely immutable tickets that + * can be stored and archived see + * {@link org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO}. + */ +public final class ZTicketDTO implements DTOInterface, Serializable +{ + private static final long serialVersionUID = -2784800199238764159L; + + /** See {@link getCashRegister()}. */ + private WeakAssociationDTO cashRegister; + /** See {@link getSequence()}. */ + private int sequence; + /** See {@link isContinuous()}. */ + private boolean continuous; + /** See {@link getOpenDate()}. */ + private Date openDate; + /** See {@link getCloseDate()}. */ + private Date closeDate; + /** See {@link getMovements()}. */ + private ImmutableList movements; + /** See {@link getCloseAmounts()}. */ + private ImmutableList closeAmounts; + /** See {@link getExpectedAmounts()}. */ + private ImmutableList expectedAmounts; + /** See {@link getUser()}. */ + private WeakAssociationDTO user; + /** See {@link getTicketCount()}. */ + private int ticketCount; + /** See {@link getCustCount()}. */ + private Integer custCount; + /** See {@link getCs()}. */ + private double cs; + /** See {@link getCsPeriod()}. */ + private double csPeriod; + /** See {@link getCsFYear()}. */ + private double csFYear; + /** See {@link getCsPerpetual()}. */ + private double csPerpetual; + /** See {@link getTaxes()}. */ + private ImmutableList taxes; + /** See {@link getCatSales()}. */ + private ImmutableList catSales; + /** See {@link getCatTaxes()}. */ + private ImmutableList catTaxes; + /** See {@link getPayments()}. */ + private ImmutableList payments; + /** See {@link getCustBalances()}. */ + private ImmutableList custBalances; + + /** + * Create an ticket from all fields. + * @param cashRegister See {@link getCashRegister()}. + * @param sequence See {@link getSequence()}. + * @param continuous See {@link isContinuous()}. + * @param openDate See {@link getOpenDate()}. + * @param closeDate See {@link getCloseDate()}. + * @param movements See {@link getMovements()}. + * @param closeAmounts See {@link getCloseAmounts()}. + * @param expectedAmounts See {@link getExpectedAmounts()}. + * @param user See {@link getUser()}. + * @param ticketCount See {@link getTicketCount()}. + * @param custCount See {@link getCustCount()}. + * @param cs See {@link getCs()}. + * @param csPeriod See {@link getCsPeriod()}. + * @param csFYear See {@link getCsFYear()}. + * @param csPerpetual See {@link getCsPerpetual()}. + * @param taxes See {@link getTaxes()}. + * @param catSales See {@link getCatSales()}. + * @param catTaxes See {@link getCatTaxes()}. + * @param payments See {@link getPayments()}. + * @param custBalances See {@link getCustBalances()}. + */ + public ZTicketDTO( + WeakAssociationDTO cashRegister, + int sequence, + boolean continuous, + Date openDate, + Date closeDate, + MovementDTO[] movements, + PaymentDTO[] closeAmounts, + PaymentDTO[] expectedAmounts, + WeakAssociationDTO user, + int ticketCount, + Integer custCount, + Double cs, + double csPeriod, + double csFYear, + double csPerpetual, + TaxAmountDTO[] taxes, + ZTicketCatSalesDTO[] catSales, + ZTicketCatTaxesDTO[] catTaxes, + PaymentDTO[] payments, + ZTicketCustBalanceDTO[] custBalances) { + this.cashRegister = cashRegister; + this.sequence = sequence; + this.continuous = continuous; + this.openDate = openDate; + this.closeDate = closeDate; + this.movements = new ImmutableList(movements); + this.closeAmounts = new ImmutableList(closeAmounts); + this.expectedAmounts = new ImmutableList(expectedAmounts); + this.user = user; + this.ticketCount = ticketCount; + this.custCount = custCount; + this.cs = cs; + this.csPeriod = csPeriod; + this.csFYear = csFYear; + this.csPerpetual = csPerpetual; + this.taxes = new ImmutableList(taxes); + this.catSales = new ImmutableList(catSales); + this.catTaxes = new ImmutableList(catTaxes); + this.payments = new ImmutableList(payments); + this.custBalances = new ImmutableList(custBalances); + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public ZTicketDTO(Reader reader) throws ParseException { + reader.startObject("cashRegister"); + this.cashRegister = new WeakAssociationDTO(reader); + reader.endObject(); + this.sequence = reader.readInt("sequence"); + this.continuous = reader.readBoolean("continuous"); + this.openDate = reader.readDateOrNull("openDate"); + this.closeDate = reader.readDateOrNull("closeDate"); + DTOFactory mvtFacto = new DTOFactory(reader, MovementDTO.class); + this.movements = mvtFacto.immutableReadObjects("movements"); + DTOFactory pmtFacto = new DTOFactory(reader, PaymentDTO.class); + this.closeAmounts = pmtFacto.immutableReadObjects("closeAmounts"); + this.expectedAmounts = pmtFacto.immutableReadObjects("expectedAmounts"); + reader.startObject("user"); + this.user = new WeakAssociationDTO(reader); + reader.endObject(); + this.ticketCount = reader.readIntOrNull("ticketCount"); + this.custCount = reader.readIntOrNull("custCount"); + this.cs = reader.readDoubleOrNull("cs"); + this.csPeriod = reader.readDouble("csPeriod"); + this.csFYear = reader.readDouble("csFYear"); + this.csPerpetual = reader.readDouble("csPerpetual"); + DTOFactory taxFacto = new DTOFactory(reader, TaxAmountDTO.class); + this.taxes = taxFacto.immutableReadObjects("taxes"); + DTOFactory catSFacto = new DTOFactory(reader, ZTicketCatSalesDTO.class); + this.catSales = catSFacto.immutableReadObjects("catSales"); + DTOFactory catTFacto = new DTOFactory(reader, ZTicketCatTaxesDTO.class); + this.catTaxes = catTFacto.immutableReadObjects("catTaxes"); + this.payments = pmtFacto.immutableReadObjects("payments"); + DTOFactory custBFacto = new DTOFactory(reader, ZTicketCustBalanceDTO.class); + this.custBalances = custBFacto.immutableReadObjects("custBalances"); + reader.startArray("custBalances"); + } + + /** + * Get the unique reference for this ticket. + * @return The reference, formatted as {@code -}. + */ + public String getReference() { + return String.format("%d-%d", + this.cashRegister.getReference(), + this.sequence); + } + + /** + * Get the cash register. + * @return The reference and label of the cash register. + */ + public WeakAssociationDTO getCashRegister() { + return this.cashRegister; + } + + /** + * Get the sequence of the cash register. + * @return The sequence of the cash register in which this ticket + * was created. + */ + public int getSequence() { + return this.sequence; + } + + /** + * Check if the session was continuous starting from the previous one. + * The session is continuous when it started on the same machine than + * the previous one and with the previous one in cache. + * @return True when the session is marked as continuous. + * @see org.pasteque.coreutil.datatransfer.dto.CashSessionDTO#isContinuous() + */ + public boolean isContinuous() { + return continuous; + } + + /** + * Get the date when the session was opened. + * @return The open date. + * @see org.pasteque.coreutil.datatransfer.dto.CashSessionDTO#getOpenDate() + */ + public Date getOpenDate() { + return this.openDate; + } + + /** + * Get the date when the session was closed. + * @return The close date. + */ + public Date getCloseDate() { + return this.closeDate; + } + + /** + * Get the movements not justified by a ticket, including the starting + * amounts. + * @return The movements. Empty when not counted. + * @see org.pasteque.coreutil.datatransfer.dto.CashSessionDTO#getMovements() + */ + public ImmutableList getMovements() { + return this.movements; + } + + /** + * Get the ending amounts. + * @return The ending amounts. Empty when not counted. + */ + public ImmutableList getCloseAmounts() { + return this.closeAmounts; + } + + /** + * Get the expected ending amounts. + * @return The expected ending cash amounts, from movements + payments. + * Empty when not counted. + */ + public ImmutableList getExpectedAmounts() { + return this.expectedAmounts; + } + + /** + * Get the id of the user that closed this session. + * @return The reference of label of the user. + */ + public WeakAssociationDTO getUser() { + return this.user; + } + + /** + * Get the number of tickets registered withing this session. + * @return The number of tickets registered withing this session. + */ + public int getTicketCount() { + return ticketCount; + } + + /** + * Get the number of customers set for this ticket. It is not bound + * to the customer's account. + * @return The number of customers set for this ticket. Null when no + * number was defined. + */ + public Integer getCustCount() { + return this.custCount; + } + + /** + * Get the consolidated sales amount for this session. + * @return The consolidated sales amount for this session. + */ + public double getCs() { + return cs; + } + + /** + * Get the cumulative consolidated sales for the period. + * @return The cumulative consolidated sales for the period. + */ + public double getCsPeriod() { + return csPeriod; + } + + /** + * Get the cumulative consolidated sales for the fiscal year. + * @return The cumulative consolidated sales for the fiscal year. + */ + public double getCsFYear() { + return csFYear; + } + + /** + * Get the perpetual cumulative consolidated sales for this cash register. + * @return The perpetual cumulative consolidated sales for this cash + * register. + */ + public double getCsPerpetual() { + return csPerpetual; + } + + /** + * Get the list of consolidated taxes after discounts. + * @return The list of tax amounts by tax. + */ + public ImmutableList getTaxes() { + return this.taxes; + } + + /** + * Get the list of consolidated sales by category. + * @return The list of consolidated sales by category. + */ + public ImmutableList getCatSales() { + return this.catSales; + } + + /** + * Get the informative list of consolidated taxes by category. + * @return The list of consolidated taxes by category. + */ + public ImmutableList getCatTaxes() { + return this.catTaxes; + } + + /** + * Get the list of consolidated amounts per payments. + * @return The list of consolidated amounts of payments. + */ + public ImmutableList getPayments() { + return this.payments; + } + + /** + * Get the list of consolidated variation of customer's balance + * per customer. + * @return The list of consolidated variation of customer's balance + * per customer. + */ + public ImmutableList getCustBalances() { + return this.custBalances; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } + + /** + * Consolidated sales for a category. + */ + public class ZTicketCatSalesDTO implements DTOInterface, Serializable + { + private static final long serialVersionUID = -6392552469204214096L; + + /** See {@link getCategory()}. */ + private final WeakAssociationDTO category; + /** See {@link getAmount()}. */ + private final double amount; + + /** + * Create a category sales from all fields. + * @param category See {@link getCategory()}. + * @param amount See {@link getAmount()}. + */ + public ZTicketCatSalesDTO( + WeakAssociationDTO category, + double amount) { + this.category = category; + this.amount = amount; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public ZTicketCatSalesDTO(Reader reader) throws ParseException { + reader.startObject("category"); + this.category = new WeakAssociationDTO(reader); + reader.endObject(); + this.amount = reader.readDouble("amount"); + } + + /** + * Get the category. + * @return The reference and label of the category. + */ + public WeakAssociationDTO getCategory() { + return this.category; + } + + /** + * Get the amount of sales for this category. + * @return The amount of sales in the main currency. + */ + public double getAmount() { + return this.amount; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } + } + + /** + * Informative taxes collected by category. + * The sum of amount may differ because of rounding issues if rounding + * happens. Use ZTicketTaxesDTO for the actual amount of taxes. + */ + public class ZTicketCatTaxesDTO implements DTOInterface, Serializable + { + private static final long serialVersionUID = -8934368775977813408L; + + /** See {@link getCategory()}. */ + private final WeakAssociationDTO category; + /** See {@link getTax()}. */ + private final WeakAssociationDTO tax; + /** See {@link getBase()}. */ + private final double base; + /** See {@link getAmount()}. */ + private final double amount; + + /** + * Create a category taxes from all fields. + * @param category See {@link getCategory()}. + * @param tax See {@link getTax()}. + * @param base See {@link getBase()}. + * @param amount See {@link getAmount()}. + */ + public ZTicketCatTaxesDTO( + WeakAssociationDTO category, + WeakAssociationDTO tax, + double base, + double amount) { + this.category = category; + this.tax = tax; + this.base = base; + this.amount = amount; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public ZTicketCatTaxesDTO(Reader reader) throws ParseException { + reader.startObject("category"); + this.category = new WeakAssociationDTO(reader); + reader.endObject(); + reader.startObject("tax"); + this.tax = new WeakAssociationDTO(reader); + reader.endObject(); + this.base = reader.readDouble("base"); + this.amount = reader.readDouble("amount"); + } + + /** + * Get the category. + * @return The reference and label of the category. + */ + public WeakAssociationDTO getCategory() { + return this.category; + } + + /** + * Get the tax. + * @return The reference and label of the tax. + */ + public WeakAssociationDTO getTax() { + return this.tax; + } + + /** + * Get the base amount associated to this tax. + * @return The base amount in the main currency. + */ + public double getBase() { + return this.base; + } + + /** + * Get the amount of tax for this category. + * @return The amount of sales in the main currency. + */ + public double getAmount() { + return this.amount; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } + } + + /** + * Consolidated variation of a customer's balance. + */ + public class ZTicketCustBalanceDTO implements DTOInterface, Serializable + { + private static final long serialVersionUID = 7698608167553284402L; + + /** See {@link getCustomer()}. */ + private final WeakAssociationDTO customer; + /** See {@link getBalance()}. */ + private final double balance; + + /** + * Create a customer's balance variation from all fields. + * @param customer See {@link getCustomer()}. + * @param balance See {@link getBalance()}. + */ + public ZTicketCustBalanceDTO( + WeakAssociationDTO customer, + double balance) { + this.customer = customer; + this.balance = balance; + } + + /** + * Instantiate from raw data from a reader. + * @param reader The reader that must be currently pointing to this object. + * @throws ParseException When an error occurs while parsing the object. + * See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details. + */ + public ZTicketCustBalanceDTO(Reader reader) throws ParseException { + reader.startObject("customer"); + this.customer = new WeakAssociationDTO(reader); + reader.endObject(); + this.balance = reader.readDouble("balance"); + } + + /** + * Get the variation of the balance for this customer. + * @return The variation of the balance in the main currency. + */ + public double getBalance() { + return this.balance; + } + + /** + * Get the customer. + * @return The reference and label of the customer. + */ + public WeakAssociationDTO getCustomer() { + return this.customer; + } + + @Override + public void checkIntegrity() throws IntegrityExceptions { + // TODO Auto-generated method stub + + } + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/dto/package-info.java b/src/main/java/org/pasteque/coreutil/datatransfer/dto/package-info.java new file mode 100644 index 0000000..588e65f --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/dto/package-info.java @@ -0,0 +1,8 @@ +/** + *

Data Transfer Object, immutable records with only primitive typed data + * and getters.

+ *

All the DTO are final to ensure the immutability. Extensions should use + * encapsulation to add more data or ease-of-use, to ensure the major package + * using them cannot be alterated.

+ */ +package org.pasteque.coreutil.datatransfer.dto; diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/format/BinaryDTOFormat.java b/src/main/java/org/pasteque/coreutil/datatransfer/format/BinaryDTOFormat.java new file mode 100644 index 0000000..8651a1b --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/format/BinaryDTOFormat.java @@ -0,0 +1,160 @@ +package org.pasteque.coreutil.datatransfer.format; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + *

Convert binary data from/to String encoding.

+ *

It will link to various base64 encoder/decoder based upon their + * availability on the system:

+ *
    + *
  • java.util.Base64: since Java SE 1.8, Android 8.0 (level 26)
  • + *
  • android.util.Base64: since Android 2.2 (level 8)
  • + *
+ *

The detection of a suitable class is done on startup (within static { }). + * If no suitable encoder/decoder is found, it will throw an Error.

+ */ +public class BinaryDTOFormat +{ + /** Reflective object for encoding. */ + private static final Object encodeObject; + /** Reflective method for encoding. */ + private static final Method encodeFunction; + /** Reflective object for decoding. */ + private static final Object decodeObject; + /** Reflective method for decoding. */ + private static final Method decodeFunction; + /** Information about how to call reflective methods. */ + private static final EncodeDecode encodeDecode; + + /** + * Look for an available encoder/decoder on the system. + */ + static { + /* non final assignments */ + EncodeDecode assignEncodeDecode; + Object assignEncodeObject; + Method assignEncodeFunction; + Object assignDecodeObject; + Method assignDecodeFunction; + try { + /* Try java.util.base64 */ + Class jub64e = Class.forName("java.util.Base64.Encoder"); + Class jub64d = Class.forName("java.util.Base64.Decoder"); + try { + assignEncodeDecode = EncodeDecode.JAVA_UTIL_BASE64; + assignEncodeObject = jub64e.getMethod("getEncoder").invoke(null); + assignEncodeFunction = jub64e.getMethod("encode", Class.forName("byte[]")); + assignDecodeObject = jub64d.getMethod("getDecoder").invoke(null); + assignDecodeFunction = jub64d.getMethod("decode", Class.forName("String")); + } catch (NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException e) { + throw new Error("Unable to link java.util.Base64.[Encode|Decode] methods", e); + } + } catch (ClassNotFoundException java_util_base64_not_found) { + /* Try android.util.base64 */ + try { + Class aub64 = Class.forName("android.util.Base64"); + try { + assignEncodeDecode = EncodeDecode.ANDROID_UTIL_BASE64; + assignEncodeObject = null; + assignEncodeFunction = aub64.getMethod("encodeToString", Class.forName("byte[]"), Class.forName("int")); + assignDecodeObject = null; + assignDecodeFunction = aub64.getMethod("decode", Class.forName("String"), Class.forName("int")); + } catch (NoSuchMethodException | SecurityException e) { + throw new Error("Unable to link android.util.Base64 methods", e); + } + } catch (ClassNotFoundException android_util_base64_not_found) { + /* No suitable base64 class found */ + throw new LinkageError("Unable to find any base64 encoder/decoder"); + } + } + /* final assignments */ + encodeDecode = assignEncodeDecode; + encodeObject = assignEncodeObject; + encodeFunction = assignEncodeFunction; + decodeObject = assignDecodeObject; + decodeFunction = assignDecodeFunction; + } + + /** + * Decode binary data from a base64-encoded string. + * May throw IllegalArgumentException if the string is not base64-encoded. + * @param strData The base64 encoded string. + * @return The binary data. + */ + public static byte[] fromString(String strData) { + return encodeDecode.decode(strData); + } + + /** + * Encode binary data to a base64-encoded string. + * @param binData The binary data to encode. + * @return The base64-encoded string. + */ + public static String toString(byte[] binData) { + return encodeDecode.encode(binData); + } + + /** Static utility-class, cannot be instantiated. */ + private BinaryDTOFormat() { } + + /** Enumeration of encode/decode signatures. */ + private enum EncodeDecode + { + /** Signature from java.util.Base64. */ + JAVA_UTIL_BASE64 + { + @Override + public byte[] decode(String strData) { + try { + return (byte[]) decodeFunction.invoke(decodeObject, strData); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new Error("Unable to call dynamically linked base64 decode", e); + } + } + + @Override + public String encode(byte[] binData) { + try { + return (String) encodeFunction.invoke(encodeObject, binData); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new Error("Unable to call dynamically linked base64 encode", e); + } + } + }, + /** Signature from android.util.Base64. */ + ANDROID_UTIL_BASE64 + { + @Override + public byte[] decode(String strData) { + try { + return (byte[]) decodeFunction.invoke(null, strData, 0 /*DEFAULT*/); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new Error("Unable to call dynamically linked base64 decode", e); + } + } + + @Override + public String encode(byte[] binData) { + try { + return (String) encodeFunction.invoke(null, binData, 0 /* DEFAULT */); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new Error("Unable to call dynamically linked base64 encode", e); + } + } + }; + + /** + * The decoding function. + * @param strData The base64 string. + * @return The binary data. + */ + public abstract byte[] decode(String strData); + /** + * The encoding function. + * @param binData The binary data. + * @return The base64 string. + */ + public abstract String encode(byte[] binData); + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/format/DateDTOFormat.java b/src/main/java/org/pasteque/coreutil/datatransfer/format/DateDTOFormat.java new file mode 100644 index 0000000..1ee2a27 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/format/DateDTOFormat.java @@ -0,0 +1,124 @@ +package org.pasteque.coreutil.datatransfer.format; + +import java.util.Calendar; +import java.util.Date; +import org.pasteque.coreutil.ParseException; + +/** + *

Convert date data from/to string/number encoding.

+ *

Dates are stored in local time, without timezone. + * Accepted date formats are:

+ *
    + *
  • "YYYY-MM-DD" or "YYYY-MM-DD hh:mm:ss"
  • + *
  • Timestamp in seconds (not milliseconds)
  • + *
  • Null
  • + *
+ *

The detection of a suitable class is done on startup (within static { }). + * If no suitable encoder/decoder is found, it will throw an Error.

+ */ +// TODO: handle Date timezone +public class DateDTOFormat +{ + /** Regex for the YYYY-MM-DD format. */ + private static final String DATE_REGEX = "\\A\\d{4}-\\d{2}-\\d{2}\\z"; + /** Regex for the YYYY-MM-DD hh:mm:ss format. */ + private static final String DATETIME_REGEX = "\\A\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\z"; + + /** + * Convert a YYYY-MM-DD string to a Date. + * @param strData The string to parse. It is trimmed before being parsed. + * @return The date. Null when strData is null or empty. + * @throws ParseException When strData does conform to a date format. + */ + public static Date parseDate(String strData) throws ParseException { + if (strData == null || strData.trim().isEmpty()) { + return null; + } + boolean hasTime = false; + String trimmed = strData.trim(); + if (!trimmed.matches(DATE_REGEX)) { + throw new ParseException("Unacceptable date format, must conform to YYYY-MM-DD[ hh:mm:ss]."); + } + if (trimmed.matches(DATETIME_REGEX)) { + hasTime = true; + } + Calendar c = Calendar.getInstance(); + int year = Integer.parseInt(trimmed.substring(0, 4)); + int month = Integer.parseInt(trimmed.substring(5, 7)); + int day = Integer.parseInt(trimmed.substring(8, 10)); + int hour = 0; + int minute = 0; + int second = 0; + if (hasTime) { + hour = Integer.parseInt(trimmed.substring(11, 13)); + minute = Integer.parseInt(trimmed.substring(14, 16)); + second = Integer.parseInt(trimmed.substring(17, 19)); + } + c.set(year, month - 1, day, hour, minute, second); + return c.getTime(); + } + + /** + * Convert a timestamp in seconds to a Date. + * @param secTimestamp The timestamp in seconds, local time. + * @return The date. + */ + public static Date parseDate(long secTimestamp) { + return new Date(secTimestamp * 1000); + } + + /** + * Convert the date to the YYYY-MM-DD format. + * @param date The date to convert. + * @return The date in YYYY-MM-DD format. Null if date is null. + */ + public static String toDateString(Date date) { + if (date == null) { + return null; + } + Calendar c = Calendar.getInstance(); + c.setTime(date); + return String.format("%04d-%02d-%02d", + c.get(Calendar.YEAR), + c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH) + ); + } + + /** + * Convert the date to the YYYY-MM-DD hh:mm:ss format. + * @param date The date to convert. + * @return The date in YYYY-MM-DD hh:mm:ss format. Null if date is null. + */ + public static String toDateTimeString(Date date) { + if (date == null) { + return null; + } + Calendar c = Calendar.getInstance(); + c.setTime(date); + return String.format("%04d-%02d-%02d %02d:%02d:%02d", + c.get(Calendar.YEAR), + c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH), + c.get(Calendar.HOUR_OF_DAY), + c.get(Calendar.MINUTE), + c.get(Calendar.SECOND) + ); + } + + /** + * Convert the date to a timestamp in seconds. + * @param date The date to convert. + * @return The timestamp in seconds. + * @throws NullPointerException When date is null. + */ + public static long toSecTimestamp(Date date) { + if (date == null) { + throw new NullPointerException("Date cannot be null"); + } + return date.getTime() / 1000; + } + + /** Static utility-class, cannot be instantiated. */ + private DateDTOFormat() { } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/format/package-info.java b/src/main/java/org/pasteque/coreutil/datatransfer/format/package-info.java new file mode 100644 index 0000000..90da3f5 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/format/package-info.java @@ -0,0 +1,6 @@ +/** + *

Common conversion of non-string nor number data from/to string.

+ *

These conversion tools are only about data representation for DTO. + * For user friendly formats see {@link org.pasteque.common.view.Formatter}.

+ */ +package org.pasteque.coreutil.datatransfer.format; diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityException.java b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityException.java new file mode 100644 index 0000000..5994e33 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityException.java @@ -0,0 +1,19 @@ +package org.pasteque.coreutil.datatransfer.integrity; + +/** + * Dummy class to group exceptions for {@link IntegrityException}. + */ +public abstract class IntegrityException extends Exception +{ + private static final long serialVersionUID = 6208213847306142896L; + + /** {@inheritDoc} */ + public IntegrityException() { + super(); + } + + /** {@inheritDoc} */ + public IntegrityException(String message) { + super(message); + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityExceptions.java b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityExceptions.java new file mode 100644 index 0000000..7b80723 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityExceptions.java @@ -0,0 +1,47 @@ +package org.pasteque.coreutil.datatransfer.integrity; + +import java.util.List; + +/** + * List of {@link org.pasteque.coreutil.datatransfer.integrity.IntegrityException} + * to get all integrity errors at once instead of throwing only the first exception. + */ +public class IntegrityExceptions extends Exception +{ + private static final long serialVersionUID = -2603644891253059810L; + + /** {@see getCauses()} */ + private IntegrityException[] causes; + + /** + * Utility function to add an exception to a list when not null. + * Use this function with static checks from subclasses of + * {@link IntegrityException}. + * @param causes The list to which the exception will be added. + * @param exception The exception to add if not null. + */ + public static void addCheck( + List causes, + IntegrityException exception) { + if (exception != null) { + causes.add(exception); + } + } + + /** + * Create a container of exceptions. + * @param causes The list of exception thrown. + */ + public IntegrityExceptions(List causes) { + super(); + this.causes = (IntegrityException[]) causes.toArray(); + } + + /** + * Get all exceptions that were thrown during an integrity check. + * @return An array of causes. Some may overlap but it should be avoided. + */ + public IntegrityException[] getCauses() { + return this.causes; + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityFieldConstraint.java b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityFieldConstraint.java new file mode 100644 index 0000000..99d0c5d --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityFieldConstraint.java @@ -0,0 +1,99 @@ +package org.pasteque.coreutil.datatransfer.integrity; + +/** + * List of integrity constraints on a specific field. + */ +public enum IntegrityFieldConstraint +{ + /** + * The field is read only and was updated. + */ + READ_ONLY("ReadOnly"), + /** + * The value must be unique and is already used by an other record of + * the same model. + */ + UNIQUE("UniqueValue"), + /** + * The field links to an other record that was not found. + */ + ASSOCIATION_NOT_FOUND("AssociationNotFound"), + /** + * The default flag was removed but no other record has it. + */ + DEFAULT_REQUIRED("DefaultRequired"), + /** + * The value cannot be null. This constraint precedes other type-checks. + */ + NOT_NULL("NotNull"), + /** + * The value must be an array. + */ + ARRAY_REQUIRED("ArrayRequired"), + /** + * The value must be within enumerated values. + */ + ENUM_REQUIRED("EnumRequired"), + /** + * The value must be a decimal number. + */ + DECIMAL_REQUIRED("Float"), + /** + * The value must be a boolean. + */ + BOOLEAN_REQUIRED("Boolean"), + /** + * The value must be an integer. + */ + INTEGER_REQUIRED("Integer"), + /** + * The numeric value must be positive or zero. + */ + NUMBER_POSITIVE("NumberPositive"), + /** + * The value must be a date, or the date cannot be read. + */ + INVALID_DATE("InvalidDate"), + /** + * The value must be a date range or the range cannot be read. + */ + INVALID_DATERANGE("InvalidDateRange"), + /** + * Closing a cash session requires that it was previously opened. + */ + OPEN_CASH_REQUIRED("OpenedCashRequired"); + + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static IntegrityFieldConstraint fromCode(String code) throws IllegalArgumentException { + for (IntegrityFieldConstraint v : IntegrityFieldConstraint.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(Integer.valueOf(code).toString()); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + IntegrityFieldConstraint(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public String getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityRecordConstraint.java b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityRecordConstraint.java new file mode 100644 index 0000000..84ab29c --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/IntegrityRecordConstraint.java @@ -0,0 +1,51 @@ +package org.pasteque.coreutil.datatransfer.integrity; + +/** + * List of integrity constraints for a whole record. + */ +public enum IntegrityRecordConstraint +{ + /** + * The record is read-only and one of its field was updated. + */ + READ_ONLY("ReadOnly"), + /** + * The record is generated and cannot be written directly. + */ + GENERATED("Generated"); + + /** {@see getCode()} */ + private final String code; + + /** + * Create from it's code. + * @param code The code value. + * @return The according enumeration value. + * @throws IllegalArgumentException When code is not found + * within the enumerated values + */ + public static IntegrityRecordConstraint fromCode(String code) throws IllegalArgumentException { + for (IntegrityRecordConstraint v : IntegrityRecordConstraint.values()) { + if (v.getCode().equals(code)) { + return v; + } + } + throw new IllegalArgumentException(Integer.valueOf(code).toString()); + } + + /** + * Internal constructor. + * @param code See {@link getCode()}. + */ + IntegrityRecordConstraint(String code) { + this.code = code; + } + + /** + * Get the associated constant. + * @return The code for DTO. + */ + public String getCode() { + return this.code; + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/integrity/InvalidFieldException.java b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/InvalidFieldException.java new file mode 100644 index 0000000..22eb125 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/InvalidFieldException.java @@ -0,0 +1,161 @@ +package org.pasteque.coreutil.datatransfer.integrity; + +/** + * The integrity check of a record failed because one of it's field has an invalid value. + */ +public class InvalidFieldException extends IntegrityException +{ + private static final long serialVersionUID = 2512125224337518801L; + + /** {@see getConstraint()} */ + private final IntegrityFieldConstraint constraint; + /** {@see getClassName()} */ + private final String className; + /** {@see getFieldName()} */ + private final String fieldName; + /** {@see getId()} */ + private final String id; + /** {@see getValue()} */ + private final String value; + + /** + * Check if a string is not null nor empty. + * @param value The value to check. + * @param className See {@link getClassName()}. + * @param fieldName See {@link getFieldName()}. + * @param id See {@link getId()}. + * @return An InvalidFieldException with an + * {@link IntegrityFieldConstraint#NOT_NULL} constraint when the trimmed + * value is null or an empty string. + */ + public static InvalidFieldException nonNull( + String value, + String className, + String fieldName, + String id) { + if (value == null || "".equals(value.trim())) { + return new InvalidFieldException( + IntegrityFieldConstraint.NOT_NULL, + className, + fieldName, + id, + value); + } + return null; + } + + /** + * Check if a number is positive or zero. + * @param value The value to check. + * @param className See {@link getClassName()}. + * @param fieldName See {@link getFieldName()}. + * @param id See {@link getId()}. + * @return An InvalidFieldException with an + * {@link IntegrityFieldConstraint#NUMBER_POSITIVE} constraint when the + * value is negative. + */ + public static InvalidFieldException positive( + int value, + String className, + String fieldName, + String id) { + if (value < 0) { + return new InvalidFieldException( + IntegrityFieldConstraint.NUMBER_POSITIVE, + className, + fieldName, + id, + String.valueOf(value)); + } + return null; + } + + /** + * Check if a number is positive or zero. + * @param value The value to check. + * @param className See {@link getClassName()}. + * @param fieldName See {@link getFieldName()}. + * @param id See {@link getId()}. + * @return An InvalidFieldException with an + * {@link IntegrityFieldConstraint#NUMBER_POSITIVE} constraint when the + * value is less than -0.000005 to handle floating point approximations. + */ + public static InvalidFieldException positive( + float value, + String className, + String fieldName, + String id) { + if (value < -0.000005) { + return new InvalidFieldException( + IntegrityFieldConstraint.NUMBER_POSITIVE, + className, + fieldName, + id, + String.valueOf(value)); + } + return null; + } + + /** + * Construct an exception from all fields. + * @param constraint See {@link getConstraint()}. + * @param className See {@link getClassName()}. + * @param fieldName See {@link getFieldName()}. + * @param id See {@link getId()}. + * @param value See {@link getValue()}. + */ + public InvalidFieldException( + IntegrityFieldConstraint constraint, + String className, + String fieldName, + String id, + String value) { + super(String.format("InvalidFieldException: %s", constraint.getCode())); + this.constraint = constraint; + this.className = className; + this.fieldName = fieldName; + this.id = id; + this.value = value; + } + + /** + * Get the constraint that was violated. + * @return The constraint. + */ + public IntegrityFieldConstraint getConstraint() { + return constraint; + } + + /** + * Get the name of the class that was checked. + * @return The name of the class the check was run against. + */ + public String getClassName() { + return className; + } + + /** + * Get the name of the field that was checked. + * @return The name of the field the check was run against. + */ + + public String getFieldName() { + return fieldName; + } + + /** + * Get the id of the record that was checked. + * @return The id of the record the check was run against. + */ + public String getId() { + return id; + } + + /** + * Get the faulty value of the field. + * @return The value of the field that was checked. + */ + public String getValue() { + return value; + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/integrity/InvalidRecordException.java b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/InvalidRecordException.java new file mode 100644 index 0000000..337b48a --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/InvalidRecordException.java @@ -0,0 +1,57 @@ +package org.pasteque.coreutil.datatransfer.integrity; + +/** + * The integrity check of a record failed because it didn't satisfy + * model-wide constraints. + */ +public class InvalidRecordException extends IntegrityException +{ + private static final long serialVersionUID = -6234201939816973179L; + + /** {@see getConstraint()} */ + private final IntegrityRecordConstraint constraint; + /** {@see getClassName()} */ + private final String className; + /** {@see getId()} */ + private final String id; + + /** + * Construct an exception from all fields. + * @param constraint See {@link getConstraint()}. + * @param className See {@link getClassName()}. + * @param id See {@link getId()}. + */ + public InvalidRecordException( + IntegrityRecordConstraint constraint, + String className, + String id) { + super(String.format("InvalidRecordException %s", constraint.getCode())); + this.constraint = constraint; + this.className = className; + this.id = id; + } + + /** + * Get the constraint that was violated. + * @return The constraint. + */ + public IntegrityRecordConstraint getConstraint() { + return constraint; + } + + /** + * Get the name of the class that was checked. + * @return The name of the class the check was run against. + */ + public String getClassName() { + return className; + } + + /** + * Get the id of the record that was checked. + * @return The id of the record the check was run against. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/integrity/package-info.java b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/package-info.java new file mode 100644 index 0000000..5304db2 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/integrity/package-info.java @@ -0,0 +1,4 @@ +/** + * Integrity of data. + */ +package org.pasteque.coreutil.datatransfer.integrity; diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/parser/DTOFactory.java b/src/main/java/org/pasteque/coreutil/datatransfer/parser/DTOFactory.java new file mode 100644 index 0000000..a2f04e2 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/parser/DTOFactory.java @@ -0,0 +1,128 @@ +package org.pasteque.coreutil.datatransfer.parser; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.dto.DTOInterface; + +/** + * Factory to read DTOs from a {@link Reader}. + */ +public class DTOFactory +{ + /** + * A reader that should be pointing to a single object upon each calls. + * Passed on construction. + */ + private final Reader reader; + /** + * The reflective constructor of the target DTO. + * Automatically instantiated on construction. + */ + private final Constructor constructor; + + /** + * Create a factory to read DTO from a reader. + * @param reader The reader to read from. + * @param clazz The class of the objects to read. + * @throws AssertionError When clazz doesn't have a clazz(Reader) constructor. + */ + public DTOFactory(Reader reader, Class clazz) { + this.reader = reader; + try { + this.constructor = clazz.getConstructor(Reader.class); + } catch (NoSuchMethodException e) { + throw new AssertionError(String.format("No suitable DTO constructor for %s: DTO(Reader)", clazz.toString())); + } + } + + /** + * Create a DTO from the current state of the reader. + * The reader must point to an object. + * @return The DTO created. + * @throws ParseException When the instantiation failed. + */ + public T readObject() throws ParseException { + try { + return this.constructor.newInstance(this.reader); + } catch (InvocationTargetException | IllegalAccessException | InstantiationException e) { + if (e.getCause() == null) { + throw new ParseException("Could not call constructor", e); + } + if (e.getCause() instanceof ParseException) { + throw (ParseException) e.getCause(); + } + throw new ParseException("Could not call constructor", e); + } + } + + /** + * Create an array of DTO from the current state of the reader. + * The reader must point to an array. + * @return The array of DTOs read from the reader. + * @throws ParseException When an error occurs while reading the objects. + * The state of the reader is not modified. + */ + public T[] readObjects() throws ParseException { + @SuppressWarnings("unchecked") + T[] array = (T[]) new ArrayList(reader.getArraySize()).toArray(); + for (int i = 0; i < reader.getArraySize(); i++) { + reader.startObject(i); + try { + array[i] = this.readObject(); + } catch (ParseException objE) { + reader.endObject(); + throw objE; + } + reader.endObject(); + } + return array; + } + + /** + * Create an immutable list of DTO from the current state of the reader. + * The reader must point to an array. + * @return The array of DTOs read from the reader. + * @throws ParseException When an error occurs while reading the objects. + * The state of the reader is not modified. + */ + public ImmutableList immutableReadObjects() throws ParseException { + return new ImmutableList(this.readObjects()); + } + + /** + * Create an array of DTO from the current state of the reader. + * @param arrayKey The key of the array to read from the current state + * of the reader. The state is not modified even if an exception is thrown. + * @return The array of DTOs read from the reader. + * @throws ParseException When an error occurs while reading the objects. + * The state of the reader is not modified. + */ + public T[] readObjects(String arrayKey) throws ParseException { + this.reader.startArray(arrayKey); + T[] array; + try { + array = this.readObjects(); + this.reader.endArray(); + return array; + } catch (ParseException arrE) { + this.reader.endArray(); + throw arrE; + } + } + + /** + * Create an immutable list of DTO from the current state of the reader. + * @param arrayKey The key of the array to read from the current state + * of the reader. The state is not modified even if an exception is thrown. + * @return The array of DTOs read from the reader. + * @throws ParseException When an error occurs while reading the objects. + * The state of the reader is not modified. + */ + public ImmutableList immutableReadObjects(String arrayKey) throws ParseException { + return new ImmutableList(this.readObjects(arrayKey)); + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/parser/JSONReader.java b/src/main/java/org/pasteque/coreutil/datatransfer/parser/JSONReader.java new file mode 100644 index 0000000..4c532d5 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/parser/JSONReader.java @@ -0,0 +1,441 @@ +package org.pasteque.coreutil.datatransfer.parser; + +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.pasteque.coreutil.ParseException; +import org.pasteque.coreutil.datatransfer.format.DateDTOFormat; +import org.pasteque.coreutil.datatransfer.format.BinaryDTOFormat; + +/** + * Read values from a JSON representation of data. + */ +public class JSONReader implements Reader +{ + /** Constant for the state of not having parsed anything yet. */ + private static final String ROOT_NAME = "__ROOT__"; + /** The underlying json string. */ + private final String json; + /** Current DOM-like path. */ + private List path; + /** + * The current object at the end of the current path. + * It is null when at root or reading an array. + */ + private JSONObject currentObject; + /** + * The current array at the end of the current path. + * It is null when at root or reading an object. + */ + private JSONArray currentArray; + /** The root object. Null when reading an array. */ + private JSONObject mainObject; + /** The root array. Null when reading an object. */ + private JSONArray mainArray; + + /** + * Create a reader to parse the given string. + * @param json The string to parse. + */ + public JSONReader(String json) { + this.json = json; + this.path = new LinkedList(); + } + + private void runPath() { + this.currentObject = null; + this.currentArray = null; + if (this.path.isEmpty()) { + return; + } + Path root = this.path.get(0); + if (root.type == PathType.OBJECT) { + this.currentObject = this.mainObject; + } else { + this.currentArray = this.mainArray; + } + for (int i = 1; i < this.path.size(); i++) { + Path p = this.path.get(i); + if (p.type == PathType.OBJECT) { + if (this.currentObject != null) { + this.currentObject = this.currentObject.getJSONObject(p.name); + } else { + this.currentObject = this.currentArray.getJSONObject(Integer.parseInt(p.name)); + this.currentArray = null; + } + } else { + if (this.currentObject != null) { + this.currentArray = this.currentObject.getJSONArray(p.name); + this.currentObject = null; + } else { + this.currentArray = this.currentArray.getJSONArray(Integer.parseInt(p.name)); + } + } + } + } + + private void checkCurrentObject() throws ParseException { + if (this.currentObject == null) { + throw new ParseException("Object required"); + } + } + + private void checkCurrentArray() throws ParseException { + if (this.currentArray == null) { + throw new ParseException("Array required"); + } + } + + private void checkCurrentArrayIndex(int index) throws ParseException { + this.checkCurrentArray(); + if (index < 0 || this.currentArray.length() >= index) { + throw new ParseException("Requesting an index out of bounds", new ArrayIndexOutOfBoundsException(index)); + } + } + + @Override + public boolean hasKey(String key) throws ParseException { + this.checkCurrentObject(); + return this.currentObject.has(key); + } + + @Override + public List listKeys() throws ParseException { + this.checkCurrentObject(); + Set keySet = this.currentObject.keySet(); + List keys = new ArrayList(keySet.size()); + for (String key : keySet) { + keys.add(key); + } + keys.sort(String.CASE_INSENSITIVE_ORDER); + return keys; + } + + @Override + public int getArraySize() throws ParseException { + this.checkCurrentArray(); + return this.currentArray.length(); + } + + @Override + public boolean isNull(String key) throws ParseException { + this.checkCurrentObject(); + return this.currentObject.isNull(key); + } + + @Override + public boolean isNull(int index) throws ParseException { + this.checkCurrentArrayIndex(index); + return this.currentArray.isNull(index); + } + + @Override + public boolean readBoolean(String key) throws ParseException { + this.checkCurrentObject(); + try { + return this.currentObject.getBoolean(key); + } catch (JSONException e) { + throw new ParseException("Boolean value expected", e); + } + } + + @Override + public Boolean readBooleanOrNull(String key) throws ParseException { + return (this.isNull(key)) ? null : Boolean.valueOf(this.readBoolean(key)); + } + + @Override + public boolean readBoolean(int index) throws ParseException { + this.checkCurrentArrayIndex(index); + try { + return this.currentArray.getBoolean(index); + } catch (JSONException e) { + throw new ParseException("Boolean value expected", e); + } + } + + @Override + public Boolean readBooleanOrNull(int index) throws ParseException { + return (this.isNull(index)) ? null : Boolean.valueOf(this.readBoolean(index)); + } + + @Override + public String readString(String key) throws ParseException { + this.checkCurrentObject(); + try { + return this.currentObject.getString(key); + } catch (JSONException e) { + throw new ParseException("String value expected", e); + } + } + + @Override + public String readStringOrNull(String key) throws ParseException { + return (this.isNull(key)) ? null : this.readString(key); + } + + @Override + public String readStringOrEmpty(String key) throws ParseException { + return (this.isNull(key)) ? "" : this.readString(key); + } + + @Override + public String readString(int index) throws ParseException { + this.checkCurrentArrayIndex(index); + try { + return this.currentArray.getString(index); + } catch (JSONException e) { + throw new ParseException("String value expected", e); + } + } + + @Override + public String readStringOrNull(int index) throws ParseException { + return (this.isNull(index)) ? null : this.readString(index); + } + + @Override + public String readStringOrEmpty(int index) throws ParseException { + return (this.isNull(index)) ? "" : this.readString(index); + } + + @Override + public int readInt(String key) throws ParseException { + this.checkCurrentObject(); + try { + return Integer.valueOf(this.currentObject.getInt(key)); + } catch (JSONException e) { + throw new ParseException("Integer value expected", e); + } + } + + @Override + public Integer readIntOrNull(String key) throws ParseException { + return (this.isNull(key)) ? null : Integer.valueOf(this.readInt(key)); + } + + @Override + public int readInt(int index) throws ParseException { + this.checkCurrentArrayIndex(index); + try { + return Integer.valueOf(this.currentArray.getInt(index)); + } catch (JSONException e) { + throw new ParseException("Integer value expected", e); + } + } + + @Override + public Integer readIntOrNull(int index) throws ParseException { + return (this.isNull(index)) ? null : Integer.valueOf(this.readInt(index)); + } + + + @Override + public double readDouble(String key) throws ParseException { + this.checkCurrentObject(); + try { + return Double.valueOf(this.currentObject.getDouble(key)); + } catch (JSONException e) { + throw new ParseException("Double value expected", e); + } + } + + @Override + public Double readDoubleOrNull(String key) throws ParseException { + return (this.isNull(key)) ? null : Double.valueOf(this.readDouble(key)); + } + + @Override + public double readDouble(int index) throws ParseException { + this.checkCurrentArray(); + try { + return Double.valueOf(this.currentArray.getDouble(index)); + } catch (JSONException e) { + throw new ParseException("Double value expected", e); + } + } + + @Override + public Double readDoubleOrNull(int index) throws ParseException { + return (this.isNull(index)) ? null : Double.valueOf(this.readDouble(index)); + } + + @Override + public Date readDate(String key) throws ParseException { + this.checkCurrentObject(); + try { + Object value = this.currentObject.get(key); + if (value instanceof String) { + return DateDTOFormat.parseDate((String) value); + } else if (value instanceof Long) { + return DateDTOFormat.parseDate(((Long) value).longValue()); + } + throw new ParseException("Date value expected"); + } catch (JSONException | IllegalArgumentException e) { + throw new ParseException("Date value expected", e); + } + } + + @Override + public Date readDateOrNull(String key) throws ParseException { + return (this.isNull(key)) ? null : this.readDate(key); + } + + @Override + public Date readDate(int index) throws ParseException { + this.checkCurrentArrayIndex(index); + try { + Object value = this.currentArray.get(index); + if (value instanceof String) { + return DateDTOFormat.parseDate((String) value); + } else if (value instanceof Long) { + return DateDTOFormat.parseDate(((Long) value).longValue()); + } + throw new ParseException("Date value expected"); + } catch (JSONException | IllegalArgumentException e) { + throw new ParseException("Date value expected", e); + } + } + + @Override + public Date readDateOrNull(int index) throws ParseException { + return (this.isNull(index)) ? null : this.readDate(index); + } + + @Override + public byte[] readBinary(String key) throws ParseException { + this.checkCurrentObject(); + try { + String b64data = this.currentObject.getString(key); + return BinaryDTOFormat.fromString(b64data); + } catch (JSONException | IllegalArgumentException e) { + throw new ParseException("Base64 string value expected", e); + } + } + + @Override + public byte[] readBinaryOrNull(String key) throws ParseException { + return (this.isNull(key)) ? null : this.readBinary(key); + } + + @Override + public byte[] readBinary(int index) throws ParseException { + this.checkCurrentArrayIndex(index); + try { + String b64data = this.currentArray.getString(index); + return BinaryDTOFormat.fromString(b64data); + } catch (JSONException | IllegalArgumentException e) { + throw new ParseException("Base64 string value expected", e); + } + } + + @Override + public byte[] readBinaryOrNull(int index) throws ParseException { + return (this.isNull(index)) ? null : this.readBinary(index); + } + + @Override + public void startObject() throws ParseException { + if (this.currentObject == null && this.currentArray == null + && this.mainObject == null && this.mainArray == null) { + this.mainObject = new JSONObject(this.json); + this.path.add(new Path(PathType.OBJECT, ROOT_NAME)); + } else { + throw new ParseException("Illegal start of root object"); + } + } + + @Override + public void startObject(String key) throws ParseException { + this.checkCurrentObject(); + try { + this.currentObject = this.currentObject.getJSONObject(key); + } catch (JSONException e) { + throw new ParseException("JSONObject value expected", e); + } + this.path.add(new Path(PathType.OBJECT, key)); + } + + @Override + public void startObject(int index) throws ParseException { + this.checkCurrentArrayIndex(index); + try { + this.currentObject = this.currentArray.getJSONObject(index); + } catch (JSONException e) { + throw new ParseException("JSONObject value expected", e); + } + this.currentArray = null; + this.path.add(new Path(PathType.OBJECT, index)); + } + + @Override + public void endObject() throws ParseException { + this.checkCurrentObject(); + this.path.remove(this.path.size() - 1); + this.runPath(); + } + + @Override + public void startArray() throws ParseException { + if (this.currentObject == null && this.currentArray == null + && this.mainObject == null && this.mainArray == null) { + this.mainArray = new JSONArray(this.json); + this.path.add(new Path(PathType.ARRAY, ROOT_NAME)); + } else { + throw new ParseException("Illegal start of root array"); + } + } + + @Override + public void startArray(String key) throws ParseException { + this.checkCurrentObject(); + try { + this.currentArray = this.currentObject.getJSONArray(key); + } catch (JSONException e) { + throw new ParseException("JSONArray value expected", e); + } + this.currentObject = null; + this.path.add(new Path(PathType.ARRAY, key)); + } + + @Override + public void startArray(int index) throws ParseException { + this.checkCurrentArrayIndex(index); + try { + this.currentArray = this.currentArray.getJSONArray(index); + } catch (JSONException e) { + throw new ParseException("JSONArray value expected", e); + } + this.path.add(new Path(PathType.ARRAY, index)); + } + + @Override + public void endArray() throws ParseException { + this.checkCurrentArray(); + this.path.remove(this.path.size() - 1); + this.runPath(); + } + + private class Path { + public final PathType type; + public final String name; + public Path(PathType type, String name) { + this.type = type; + this.name = name; + } + public Path(PathType type, int index) { + this.type = type; + this.name = String.valueOf(index); + } + } + + private enum PathType { + OBJECT, + ARRAY + } +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/parser/Reader.java b/src/main/java/org/pasteque/coreutil/datatransfer/parser/Reader.java new file mode 100644 index 0000000..de516b9 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/parser/Reader.java @@ -0,0 +1,340 @@ +package org.pasteque.coreutil.datatransfer.parser; + +import java.util.Date; +import java.util.List; +import org.pasteque.coreutil.ParseException; + +/** + *

Data reader.

+ *

A reader must always be initialized from its content, typically within + * the constructor.

+ */ +public interface Reader +{ + /** + * Check whether the key exists in the current element. + * @param key The key to search. + * @return True if the current element has the given key, even if its value + * is null. + * @throws ParseException When requesting a key from outside an object. + */ + public boolean hasKey(String key) throws ParseException; + + /** + * List the keys in the current element. + * @return The list of keys, ordered alphabetically. + * @throws ParseException When requesting a key from outside an object. + */ + public List listKeys() throws ParseException; + + /** + * Get the size of the current array. + * @return The size of the current array. + * @throws ParseException When requesting the size outside an array. + */ + public int getArraySize() throws ParseException; + + /** + * Check whether the value of the given key is null. + * @param key The key to read in the current element. + * @return True if the value is explicitly null, false otherwise. + * @throws ParseException When requesting a key from outside an object + * or a non-existing key. + */ + public boolean isNull(String key) throws ParseException; + + /** + * Check whether the value at the given index is null. + * @param index The index to read in the current element. + * @return True if the value is explicitly null, false otherwise. + * @throws ParseException When requesting an index from outside an array + * or outside bounds. + */ + public boolean isNull(int index) throws ParseException; + + /** + * Read a boolean from the given key of the current element. + * @param key The key to read in the current element. + * @return The boolean value. + * @throws ParseException When requesting a key from outside an object, + * requesting a non-existing key or non-boolean value. + */ + public boolean readBoolean(String key) throws ParseException; + + /** + * Read a boolean from the given key of the current element. + * @param key The key to read in the current element. + * @return The boolean value or null if the value is explicitely null. + * @throws ParseException When requesting a key from outside an object, + * requesting a non-existing key or non-boolean value. + */ + public Boolean readBooleanOrNull(String key) throws ParseException; + + /** + * Read a boolean at the given index of the current element. + * @param index The index to read in the current element. + * @return The boolean value. + * @throws ParseException When requesting an index from outside an array, + * requesting outside bounds or non-boolean value. + */ + public boolean readBoolean(int index) throws ParseException; + + /** + * Read a boolean at the given index of the current element. + * @param index The index to read in the current element. + * @return The boolean value or null if the value is explicitely null. + * @throws ParseException When requesting an index from outside an array, + * requesting outside bounds or non-boolean value. + */ + public Boolean readBooleanOrNull(int index) throws ParseException; + + /** + * Read a String from the given key of the current element. + * @param key The key to read in the current element. + * @return The String value, cannot be null. + * @throws ParseException When requesting a key from outside an object, + * a non-string value or null. + */ + public String readString(String key) throws ParseException; + + /** + * Read a String from the given key of the current element. + * @param key The key to read in the current element. + * @return The String value or null if the value is explicitely null. + * @throws ParseException When requesting a key from outside an object + * or a non-string value. + */ + public String readStringOrNull(String key) throws ParseException; + + /** + * Read a String from the given key of the current element. + * @param key The key to read in the current element. + * @return The String value or empty string if the value is explicitely null. + * @throws ParseException When requesting a key from outside an object + * or a non-string value. + */ + public String readStringOrEmpty(String key) throws ParseException; + + /** + * Read a String at the given index of the current element. + * @param index The index to read in the current element. + * @return The String value, cannot be null. + * @throws ParseException When requesting an index from outside an array, + * requesting outside bounds, a non-string value or null. + */ + public String readString(int index) throws ParseException; + + /** + * Read a String at the given index of the current element. + * @param index The index to read in the current element. + * @return The String value or null if the value is explicitely null. + * @throws ParseException When requesting an index from outside an array, + * requesting outside bounds or a non-string value. + */ + public String readStringOrNull(int index) throws ParseException; + + /** + * Read a String at the given index of the current element. + * @param index The index to read in the current element. + * @return The String value or empty string if the value is explicitely null. + * @throws ParseException When requesting an index from outside an array, + * requesting outside bounds or a non-string value. + */ + public String readStringOrEmpty(int index) throws ParseException; + + /** + * Read an Integer from the given key of the current element. + * @param key The key to read in the current element. + * @return The int value. + * @throws ParseException When requesting a key from outside an object, + * or a non-int value. + */ + public int readInt(String key) throws ParseException; + + /** + * Read an Integer from the given key of the current element. + * @param key The key to read in the current element. + * @return The int value or null if the value is explicitely null. + * @throws ParseException When requesting a key from outside an object, + * or a non-int value. + */ + public Integer readIntOrNull(String key) throws ParseException; + + /** + * Read an Integer at the given index of the current element. + * @param index The index to read in the current element. + * @return The int value. + * @throws ParseException When requesting an index from outside an array, + * requesting outside bounds or non-int value. + */ + public int readInt(int index) throws ParseException; + + /** + * Read an Integer at the given index of the current element. + * @param index The index to read in the current element. + * @return The int value or null if the value is explicitely null. + * @throws ParseException When requesting an index from outside an array, + * requesting outside bounds or non-int value. + */ + public Integer readIntOrNull(int index) throws ParseException; + + /** + * Read a decimal number from the given key of the current element. + * @param key The key to read in the current element. + * @return The number. + * @throws ParseException When requesting a key from outside an object, + * a non-numeric value or null. + */ + public double readDouble(String key) throws ParseException; + + /** + * Read a decimal number from the given key of the current element. + * @param key The key to read in the current element. + * @return The number or null if the value is explicitely null. + * @throws ParseException When requesting a key from outside an object + * or a non-numeric value. + */ + public Double readDoubleOrNull(String key) throws ParseException; + + /** + * Read a decimal number at the given index of the current element. + * @param index The index to read in the current element. + * @return The number. + * @throws ParseException When requesting an index from outside an array, + * requesting outside bounds, non-numeric value or null. + */ + public double readDouble(int index) throws ParseException; + + /** + * Read a decimal number at the given index of the current element. + * @param index The index to read in the current element. + * @return The number or null if the value is explicitely null. + * @throws ParseException When requesting an index from outside an array, + * requesting outside bounds or non-numeric value. + */ + public Double readDoubleOrNull(int index) throws ParseException; + + /** + * Read a date from the given key of the current element. + * @param key The key to read in the current element. + * @return The date value, cannot be null. + * @throws ParseException When requesting a key from outside an object, + * a non-date value or null. + */ + public Date readDate(String key) throws ParseException; + + /** + * Read a date from the given key of the current element. + * @param key The key to read in the current element. + * @return The date value or null if the value is explicitely null. + * @throws ParseException When requesting a key from outside an object + * or a non-date value. + */ + public Date readDateOrNull(String key) throws ParseException; + + /** + * Read a date at the given index of the current element. + * @param index The index to read in the current element. + * @return The date value, cannot be null. + * @throws ParseException When requesting a index from outside an array, + * a non-date value or null. + */ + public Date readDate(int index) throws ParseException; + + /** + * Read a date at the given index of the current element. + * @param index The index to read in the current element. + * @return The date value or null if the value is explicitely null. + * @throws ParseException When requesting a index from outside an array + * or a non-date value. + */ + public Date readDateOrNull(int index) throws ParseException; + + /** + * Read binary data as base64 from the given key of the current element. + * @param key The key to read in the current element. + * @return The raw binary value, cannot be null. + * @throws ParseException When requesting a key from outside an object, + * a non-base64 value or null. + */ + public byte[] readBinary(String key) throws ParseException; + + /** + * Read binary data as base64 from the given key of the current element. + * @param key The key to read in the current element. + * @return The raw binary value or null if the value is explicitely null. + * @throws ParseException When requesting a key from outside an object + * or a non-base64 value. + */ + public byte[] readBinaryOrNull(String key) throws ParseException; + + /** + * Read binary data as base64 at the given index of the current element. + * @param index The index to read in the current element. + * @return The raw binary value, cannot be null. + * @throws ParseException When requesting a index from outside an array, + * a non-base64 value or null. + */ + public byte[] readBinary(int index) throws ParseException; + + /** + * Read binary data as base64 at the given index of the current element. + * @param index The index to read in the current element. + * @return The raw binary value or null if the value is explicitely null. + * @throws ParseException When requesting a index from outside an array + * or a non-base64 value. + */ + public byte[] readBinaryOrNull(int index) throws ParseException; + + /** + * Start (create) a dictionary as the root element + * @throws ParseException When the root element is already created. + */ + public void startObject() throws ParseException; + + /** + * Start (create) a dictionary as a child element. + * @param key The key to read in the current dictionary. + * @throws ParseException When requesting a key from outside an object. + */ + public void startObject(String key) throws ParseException; + + /** + * Start (create) a dictionary as a the n-th element of the current array. + * @param index The index to read in the current array. + * @throws ParseException When requesting an index from outside an array. + */ + public void startObject(int index) throws ParseException; + + /** + * Finalize the current object and return up into the tree. + * @throws ParseException When the current element is not an object. + */ + public void endObject() throws ParseException; + + /** + * Start (create) an array as the root element. + * @throws ParseException When the root element is already created. + */ + public void startArray() throws ParseException; + + /** + * Start (create) an array as a child element. + * @param key The key in the parent dictionary. + * @throws ParseException When requesting a key from outside an object. + */ + public void startArray(String key) throws ParseException; + + /** + * Start (create) an array as a the n-th element of the current array. + * @param index The index in the current array. + * @throws ParseException When requesting an index from outside an array. + */ + public void startArray(int index) throws ParseException; + + /** + * Finalize the current array and return up into the tree. + * @throws ParseException When the current element is not an array. + */ + public void endArray() throws ParseException; +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/parser/Writer.java b/src/main/java/org/pasteque/coreutil/datatransfer/parser/Writer.java new file mode 100644 index 0000000..473d02f --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/parser/Writer.java @@ -0,0 +1,15 @@ +package org.pasteque.coreutil.datatransfer.parser; + +/** + *

Data writer.

+ *

A reader must always be initialized from its DTO, typically within + * the constructor.

+ */ +public interface Writer +{ + /** + * Get the encoded data. + * @return The encoded data. + */ + public String getOutput(); +} diff --git a/src/main/java/org/pasteque/coreutil/datatransfer/parser/package-info.java b/src/main/java/org/pasteque/coreutil/datatransfer/parser/package-info.java new file mode 100644 index 0000000..1005f66 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/datatransfer/parser/package-info.java @@ -0,0 +1,7 @@ +/** + *

Read and write DTO from/to string representations.

+ *

DTO are serializable, no parser is required to read/write + * in binary format. Binary data stored as text must use the + * base64 format.

+ */ +package org.pasteque.coreutil.datatransfer.parser; diff --git a/src/main/java/org/pasteque/coreutil/package-info.java b/src/main/java/org/pasteque/coreutil/package-info.java new file mode 100644 index 0000000..cc030f9 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/package-info.java @@ -0,0 +1,8 @@ +/** + *

Core features that are required for major features but can also be used + * outside it. The major library will include it as a strict requirement. Other + * projects that don't need major features can add it as a dependency.

+ *

This package declares the basic data structures and allow reading. + * The major package will contain the behaviour and writing.

+ */ +package org.pasteque.coreutil; diff --git a/src/main/java/org/pasteque/coreutil/price/Discount.java b/src/main/java/org/pasteque/coreutil/price/Discount.java new file mode 100644 index 0000000..d200fb5 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/Discount.java @@ -0,0 +1,199 @@ +package org.pasteque.coreutil.price; + +import org.pasteque.coreutil.constants.DiscountTarget; +import org.pasteque.coreutil.constants.DiscountType; + +/** + * Definition of a discount. The same class can be used for line discounts + * or order discount, but all targets cannot be applied to orders. + * @see org.pasteque.coreutil.constants.DiscountType + * @see org.pasteque.coreutil.constants.DiscountTarget + */ +public class Discount +{ + /** See {@link getType()}. */ + private final DiscountType type; + /** See {@link getTarget()}. */ + private final DiscountTarget target; + /** See {@link getValue()}. */ + private final double value; + + /** + * Get the discount if it has an effect. + * @param discount The discount to get, can be null. + * @return Null if the discount is null or has a zero value, the discount + * otherwise. + */ + public static Discount getEffectiveDiscount(Discount discount) { + if (discount == null) { + return null; + } + if (discount.value == 0.0) { + return null; + } + return discount; + } + + /** + * Create a discount from all field. + * @param type See {@link getType()}. + * @param target See {@link getTarget()}. + * @param value See {@link getValue()}. + */ + public Discount( + DiscountType type, + DiscountTarget target, + double value) { + this.type = type; + this.target = target; + this.value = value; + } + + /** + * Convert the discount to a + * {@link org.pasteque.coreutil.constants.DiscountType#RATE} discount. + * @param fromPrice The price to which the discount should apply. + * @return A new discount with its type transformed to + * {@link org.pasteque.coreutil.constants.DiscountType#RATE} and value + * adjusted from the original price. + */ + public Discount toRate(Price fromPrice) { + switch (this.type) { + case RATE: + return this; + case VALUE: + Price reduced = fromPrice.add(-this.value); + double rate = 1.0 - reduced.getRatio(fromPrice); + return new Discount(DiscountType.RATE, this.target, rate); + default: + assert false : this.type; + return null; + } + } + + /** + * Convert the discount to a + * {@link org.pasteque.coreutil.constants.DiscountType#RATE} discount + * with {@link org.pasteque.coreutil.constants.DiscountTarget#UNIT} or + * {@link org.pasteque.coreutil.constants.DiscountTarget#UNIT_TAXED} target. + * Use this conversion to be able to combine discounts with different types + * and targets. + * @param fromPrice The target price before applying the discount. + * @param toPrice The price after applying the discount. + * @return A new discount that get the same result. + */ + public Discount toUnitRate(Price fromPrice, Price toPrice) { + double ratio = toPrice.getRatio(fromPrice); + switch (this.target) { + case UNIT: + case TOTAL: + return new Discount(DiscountType.RATE, DiscountTarget.UNIT, ratio); + case UNIT_TAXED: + case TOTAL_TAXED: + return new Discount(DiscountType.RATE, DiscountTarget.UNIT_TAXED, ratio); + default: + assert false : this.target; + return this; + } + } + + /** + * Compute a discounted price from a base price. + * @param price The price before applying the discount. + * @return The price after applying the discount. + */ + public Price2 applyTo(Price2 price) { + switch (this.type) { + case RATE: + Rate r = new Rate(this.value, false); + return r.applyTo(price); + case VALUE: + return price.add(-this.value); + default: + assert false : this.type; + return null; + } + } + + /** + * Compute a discounted price from a base price. + * @param price The price before applying the discount. + * @return The price after applying the discount. + */ + public Price5 applyTo(Price5 price) { + switch (this.type){ + case RATE: + Rate r = new Rate(this.value, false); + return r.applyTo(price); + case VALUE: + return price.add(-this.value); + default: + assert false : this.type; + return null; + } + } + + /** + *

Apply only a fraction of the discount to a price. + * The resulting price always uses 5 decimals to avoid the introduction + * of rounding approximations.

+ * @param price The original price. It will be converted to a + * {@link org.pasteque.coreutil.price.Price5} to avoid rounding + * approximations. + * @param rate The fraction of the discount to apply. + * @return The price after applying a fraction of the discount. + */ + public Price5 applyPartlyTo(Price price, double rate) { + Discount partial = new Discount(this.type, this.target, this.value * rate); + return partial.applyTo(new Price5(price)); + } + + /** + * Get the type of the discount, defining how to compute an amount. + * @return The type of the discount. + */ + public DiscountType getType() { + return this.type; + } + + /** + * Get the target of the discount, defining from which price the + * value should be applied to dispatch rounding approximations. + * @return The target of the discount. + */ + public DiscountTarget getTarget() { + return this.target; + } + + /** + * Get the value of the discount to apply. Its meaning depends upon + * its type. + * @return The value of the discount. + */ + public double getValue() { + return this.value; + } + + /** + * Two discounts are considered equals when they share the same + * type, target and value. + * @param o The other object to compare. + * @return True when o is a discount and shares the same type, + * target and value. + */ + @Override + public boolean equals(Object o) { + return (o instanceof Discount) + && ((Discount) o).type == this.type + && ((Discount) o).target == this.target + && ((Discount) o).value == this.value; + } + + @Override + public int hashCode() { + return String.format("%s%s%.10f", + this.type.getCode(), + this.target.getCode(), + this.value).hashCode(); + } +} diff --git a/src/main/java/org/pasteque/coreutil/price/FinalTaxAmount.java b/src/main/java/org/pasteque/coreutil/price/FinalTaxAmount.java new file mode 100644 index 0000000..9845f66 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/FinalTaxAmount.java @@ -0,0 +1,82 @@ +package org.pasteque.coreutil.price; + +/** + *

Tax amounts, as {@link org.pasteque.coreutil.price.Price2} for the total.

+ * @see org.pasteque.coreutil.price.TaxAmount + */ +public final class FinalTaxAmount +{ + /** See {@link getTax()}. */ + private Tax tax; + /** See {@link getBase()}. */ + private Price2 base; + /** See {@link getAmount()}. */ + private Price2 amount; + + /** + * Create a tax amount from all fields. + * @param tax See {@link getTax()}. + * @param base See {@link getBase()}. + * @param amount See {@link getAmount()}. + */ + public FinalTaxAmount( + Tax tax, + Price2 base, + Price2 amount) { + this.tax = tax; + this.base = base; + this.amount = amount; + } + + /** + * Create a new tax amount by adding an other amount of the same tax. + * @param other The other amount to add. + * @return A new amount containing the sum of the amounts. + * @throws IllegalArgumentException When other is not an amount + * of the same type. + */ + public FinalTaxAmount sum(FinalTaxAmount other) throws IllegalArgumentException { + if (!this.isSameTax(other)) { + throw new IllegalArgumentException("Cannot add amounts from different taxes"); + } + return new FinalTaxAmount( + this.tax, + base.add(other.getBase()), + amount.add(other.getAmount())); + } + + /** + * Get the tax associated to this amount. + * @return The tax. + */ + public Tax getTax() { + return this.tax; + } + + /** + * Get the base amount from which the tax is computed. It has + * no effect for fixed taxes but is used for rates. + * @return The base amount to compute the tax. + */ + public Price2 getBase() { + return this.base; + } + + /** + * The amount of tax. + * @return The amount of tax. + */ + public Price2 getAmount() { + return this.amount; + } + + /** + * Check whether an other tax amount is an amount of the same tax. + * @param other The other amount to check. + * @return True when both amounts are from the same tax. That is, both + * taxes shares the same reference. + */ + public boolean isSameTax(FinalTaxAmount other) { + return this.getTax().getReference().equals(other.getTax().getReference()); + } +} diff --git a/src/main/java/org/pasteque/coreutil/price/Price.java b/src/main/java/org/pasteque/coreutil/price/Price.java new file mode 100644 index 0000000..3e18c16 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/Price.java @@ -0,0 +1,149 @@ +package org.pasteque.coreutil.price; + +import java.math.BigDecimal; + +/** + *

Price with arbitrary precision. Subclasses must define a constant + * precision across all of their instances, usually by providing a + * singleton of a {@link java.math.MathContext}.

+ *

Subclasses must also implement all abstract operations to return + * a instance of the same subclass. Operations with an abstract Price + * must return an instance of the same subclass when the precision is + * lower, or call the operation from the other operand.

+ */ +public abstract class Price implements Comparable +{ + /** + * The underlying number. + */ + /* package */ BigDecimal number; + + /** + * Empty constructor for subclasses to handle arbitrary precision + * and conversion. + */ + /* package */ Price() { } + + /* package */ Price(BigDecimal number) { + this.number = number; + } + + /** + * Convert to a double for final presentation. + * @return The closest double for this value. + */ + public final double toDouble() { + return this.number.doubleValue(); + } + + /** + * Get the number of decimal this price is capable of computing. + * @return The number of decimal. + */ + public int getPrecision() { + return this.number.precision(); + } + + /** + * Add to this number, keep the same precision. + * @param number The number to add. + * @return The result of the operation, without changing the precision. + */ + public abstract Price add(int number); + + /** + * Add to this number, keep the same precision. + * @param number The number to add. + * @return The result of the operation, without changing the precision. + */ + public abstract Price add(double number); + + /** + * Add to this number, keep the lowest precision. + * @param number The number to add. + * @return The result of the operation, with the lowest precision + * of the two operands. + */ + public abstract Price add(Price number); + + /** + * Subtract to this number, keep the same precision. + * @param number The number to subtract. + * @return The result of the operation, without changing the precision. + */ + public abstract Price subtract(int number); + + /** + * Subtract to this number, keep the same precision. + * @param number The number to subtract. + * @return The result of the operation, without changing the precision. + */ + public abstract Price subtract(double number); + + /** + * Subtract to this number, keep the lowest precision. + * @param number The number to subtract. + * @return The result of the operation, with the lowest precision + * of the two operands. + */ + public abstract Price subtract(Price number); + + /** + * Multiply this number, keep the same precision. + * @param factor The factor to multiply this number. + * @return The result of the operation, without changing the precision. + */ + public abstract Price multiply(int factor); + + /** + * Multiply this number, keep the same precision. + * @param factor The factor to multiply this number. + * @return The result of the operation, without changing the precision. + */ + public abstract Price multiply(double factor); + + /** + * Multiply this number, keep the same precision. + * @param quantity The factor to multiply this number. + * @return The result of the operation, without changing the precision. + */ + public abstract Price multiply(Quantity quantity); + + /** + * Divide this number, keep the same precision. + * @param factor The factor to divide this number. + * @return The result of the operation, without changing the precision. + */ + public abstract Price divide(int factor); + + /** + * Divide this number, keep the same precision. + * @param factor The factor to divide this number. + * @return The result of the operation, without changing the precision. + */ + public abstract Price divide(double factor); + + /** + * Get the ratio this price represent from a total. + * @param total The total price to compare this price to. + * @return The ratio of this price from the total. + */ + public double getRatio(Price total) { + return this.number.divide(total.number).doubleValue(); + } + + /** + * Compare two Price. + * @param to The Price to compare to. + * @return -1, 0, or 1 as this BigDecimal is numerically less than, + * equal to, or greater than to. + */ + public int compareTo(Price to) { + return this.number.compareTo(to.number); + } + + @Override + public int compareTo(BigDecimal to) { + return this.number.compareTo(to); + } +} diff --git a/src/main/java/org/pasteque/coreutil/price/Price2.java b/src/main/java/org/pasteque/coreutil/price/Price2.java new file mode 100644 index 0000000..41a0b01 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/Price2.java @@ -0,0 +1,130 @@ +package org.pasteque.coreutil.price; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +/** + *

A 2 decimal precision number. This precision is used for taxed prices, + * which match the precision used for payments.

+ *

As {@link java.math.BigDecimal}, Price2 are immutable. All operations will + * return a new Price2 with the result of the operation.

+ */ +public final class Price2 extends Price +{ + /** Precision shared with all instances. */ + private static final int PRECISION = 2; + + /** + * BigDecimal context, 2 decimal precision and regular rounding. + */ + private static final MathContext CTX = new MathContext(PRECISION, RoundingMode.HALF_UP); + + /** + * Internal constructor for the result of operations. + * @param result The result of an operation. + */ + /* package */ Price2(BigDecimal result) { + super(result); + } + + /** + * Create a price with 2 decimal precision from an other price. + * @param other The price to convert. If it is more precise, the resulting + * price is rounded. If less, the precision is augmented for future + * operations. + */ + public Price2(Price other) { + super(); + this.number = new BigDecimal(other.number.toString(), CTX); + } + + /** + * Convert a number to a 2 decimal precision number. + * @param price The raw number. It will be rounded to 2 decimals. + */ + public Price2(double price) { + super(new BigDecimal(price, CTX)); + } + + @Override + public Price2 add(int number) { + return new Price2(this.number.add(new BigDecimal(number), CTX)); + } + + @Override + public Price2 add(double number) { + return new Price2(this.number.add(new BigDecimal(number), CTX)); + } + + /** + * Add to this number, with the same precision. + * @param number The number to add. + * @return The result of the operation, with the same precision. + */ + public Price2 add(Price2 number) { + return new Price2(this.number.add(number.number, CTX)); + } + + @Override + public Price add(Price number) { + if (this.getPrecision() <= number.getPrecision()) { + return new Price2(this.number.add(number.number, CTX)); + } else { + return number.add(this); + } + } + + @Override + public Price2 subtract(int number) { + return new Price2(this.number.subtract(new BigDecimal(number), CTX)); + } + + @Override + public Price2 subtract(double number) { + return new Price2(this.number.subtract(new BigDecimal(number), CTX)); + } + + /** + * Subtract to this number, with the same precision. + * @param number The number to subtract. + * @return The result of the operation, with the same precision. + */ + public Price2 subtract(Price2 number) { + return new Price2(this.number.subtract(number.number, CTX)); + } + + @Override + public Price subtract(Price number) { + if (this.getPrecision() <= number.getPrecision()) { + return new Price2(this.number.subtract(number.number, CTX)); + } else { + return number.subtract(this); + } + } + + @Override + public Price2 multiply(int factor) { + return new Price2(this.number.multiply(new BigDecimal(factor), CTX)); + } + + @Override + public Price2 multiply(double factor) { + return new Price2(this.number.multiply(new BigDecimal(factor), CTX)); + } + + @Override + public Price2 multiply(Quantity quantity) { + return this.multiply(quantity.getQuantity()); + } + + @Override + public Price2 divide(int factor) { + return new Price2(this.number.divide(new BigDecimal(factor), CTX)); + } + + @Override + public Price2 divide(double factor) { + return new Price2(this.number.divide(new BigDecimal(factor), CTX)); + } +} diff --git a/src/main/java/org/pasteque/coreutil/price/Price5.java b/src/main/java/org/pasteque/coreutil/price/Price5.java new file mode 100644 index 0000000..457ae2d --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/Price5.java @@ -0,0 +1,138 @@ +package org.pasteque.coreutil.price; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +/** + *

A 5 decimal precision number. This precision is mostly used for non-taxed prices, + * which are not payable directly and are used for precise operations.

+ *

As {@link java.math.BigDecimal}, Price5 are immutable. All operations will + * return a new Price5 with the result of the operation.

+ */ +public final class Price5 extends Price +{ + /** Precision shared with all instances. */ + private static final int PRECISION = 5; + + /** + * BigDecimal context, 5 decimal precision and regular rounding. + */ + private static final MathContext CTX = new MathContext(PRECISION, RoundingMode.HALF_UP); + + /** + * Inner constructor for the result of operations. + * @param result The result of an operation. + */ + /* package */ Price5(BigDecimal result) { + super(result); + } + + /** + * Create a price with 5 decimal precision from an other price. + * @param other The price to convert. If it is more precise, the resulting + * price is rounded. If less, the precision is augmented for future + * operations. + */ + public Price5(Price other) { + super(); + this.number = new BigDecimal(other.number.toString(), CTX); + } + + /** + * Convert a number to a 2 decimal precision number. + * @param price The raw number. It will be rounded to 2 decimals. + */ + public Price5(double price) { + super(new BigDecimal(price, CTX)); + } + + @Override + public Price5 add(int number) { + return new Price5(this.number.add(new BigDecimal(number), CTX)); + } + + @Override + public Price5 add(double number) { + return new Price5(this.number.add(new BigDecimal(number), CTX)); + } + + /** + * Add to this number, keep the same precision. + * @param number The number to add. + * @return The result of the operation, without changing the precision. + */ + public Price5 add(Price5 number) { + return new Price5(this.number.add(number.number, CTX)); + } + + @Override + public Price add(Price number) { + if (this.getPrecision() <= number.getPrecision()) { + return new Price5(this.number.add(number.number, CTX)); + } else { + return number.add(this); + } + } + + @Override + public Price5 subtract(int number) { + return new Price5(this.number.subtract(new BigDecimal(number), CTX)); + } + + @Override + public Price5 subtract(double number) { + return new Price5(this.number.subtract(new BigDecimal(number), CTX)); + } + + /** + * Subtract to this number, keep the same precision. + * @param number The number to subtract. + * @return The result of the operation, without changing the precision. + */ + public Price5 subtract(Price5 number) { + return new Price5(this.number.subtract(number.number, CTX)); + } + + @Override + public Price subtract(Price number) { + if (this.getPrecision() <= number.getPrecision()) { + return new Price5(this.number.subtract(number.number, CTX)); + } else { + return number.subtract(this); + } + } + + @Override + public Price5 multiply(int factor) { + return new Price5(this.number.multiply(new BigDecimal(factor), CTX)); + } + + @Override + public Price5 multiply(double factor) { + return new Price5(this.number.multiply(new BigDecimal(factor), CTX)); + } + + @Override + public Price5 multiply(Quantity quantity) { + return this.multiply(quantity.getQuantity()); + } + + @Override + public Price5 divide(int factor) { + return new Price5(this.number.divide(new BigDecimal(factor), CTX)); + } + + @Override + public Price5 divide(double factor) { + return new Price5(this.number.divide(new BigDecimal(factor), CTX)); + } + + /** + * Round this number to a 2 digit precision number for payment. + * @return The same number, with less precision. + */ + public Price2 toPrice2() { + return new Price2(this); + } +} diff --git a/src/main/java/org/pasteque/coreutil/price/Quantity.java b/src/main/java/org/pasteque/coreutil/price/Quantity.java new file mode 100644 index 0000000..cf54689 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/Quantity.java @@ -0,0 +1,57 @@ +package org.pasteque.coreutil.price; + +/** + * Representation of a quantity. + */ +public final class Quantity +{ + /** See {@link getQuantity()}. */ + private double quantity; + /** See {@link getMetricSymbol(). */ + private String metricSymbol; + + /** + * Create a quantity from all fields. + * @param quantity See {@link getQuantity()}. + * @param metricSymbol See {@link getMetricSymbol()}, converted to an empty + * string if null. + */ + public Quantity(double quantity, String metricSymbol) { + this.quantity = quantity; + if (metricSymbol == null) { + this.metricSymbol = ""; + } else { + this.metricSymbol = metricSymbol; + } + } + + /** + * Get the quantity. + * @return The quantity. + */ + public double getQuantity() { + return this.quantity; + } + + /** + * Get the metric symbol. I.e. empty for a self-describing quantity, + * "kg" for kilogramm, "L" for liter. + * @return The metric symbol used for this quantity. + * An empty symbol is used conventionally for atomic quantities. + */ + public String getMetricSymbol() { + return metricSymbol; + } + + @Override + public boolean equals(Object o) { + return (o instanceof Quantity) + && ((Quantity) o).quantity == this.quantity + && ((Quantity) o).metricSymbol.equals(this.metricSymbol); + } + + @Override + public int hashCode() { + return String.format("%.10f%s", this.quantity, this.metricSymbol).hashCode(); + } +} diff --git a/src/main/java/org/pasteque/coreutil/price/Rate.java b/src/main/java/org/pasteque/coreutil/price/Rate.java new file mode 100644 index 0000000..2093ab9 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/Rate.java @@ -0,0 +1,220 @@ +package org.pasteque.coreutil.price; + +/** + *

Representation of a rate.

+ *

A Rate can additive or subtractive. Additive rates are used to augment + * prices from a base price. In example to compute taxed prices + * from base prices. Subtractive rates are used to diminish prices from a + * base price. In example to compute discounts.

+ *

Rates can be applied or reverted. Reverting a rate will give the initial + * value before the rate was applied.

+ *

For examples:

+ *
    + *
  • Applying an additive rate of 0.2 to 10.00 will give 12.00.
  • + *
  • Applying a subtractive rate of 0.2 to 10.00 will give 8.00.
  • + *
  • Reverting an additive rate of 0.2 from 12.00 will give 10.00.
  • + *
  • Reverting a subtractive rate of 0.2 from 8.00 will give 10.00.
  • + *
+ */ +public final class Rate +{ + private double rate; + private boolean additive; + + /** + * Create a rate from a number. + * @param rate The rate to apply. The absolute value is used. + * When the rate is > 1, it is read as a percent instead of a rate. + * @param additive If the rate should increase the price. + * Use True to add from a base price (i.e. applying taxes), + * use false to subtract from a base price (i.e. discount rate). + */ + public Rate(double rate, boolean additive) { + double localRate = Math.abs(rate); + if (localRate > 1.0) { + localRate = localRate / 100.00; + } + this.rate = localRate; + this.additive = additive; + } + + /** + * Create a rate from a number. + * @param rate The rate to apply. When using a positive value, + * this rate is additive, otherwise it is subtractive. + * When the |rate| is > 1, it is read as a percent instead of a rate. + */ + public Rate(double rate) { + this.additive = rate > 0.00; + double localRate = Math.abs(rate); + if (localRate > 1.0) { + localRate = localRate / 100.00; + } + this.rate = localRate; + } + + /** + *

Create a rate by combining two rates. The result is the sum + * of the rates but is capped between 0 and 1. It may change its + * additiveness though.

+ *

For example, combining two non-additive rates of 0.2 and 0.3 will + * result in a non-additive rate of 0.5. Combining and additive rate of 0.2 + * and a non-additive rate of 0.5 will result in a non-additive rate of 0.3.

+ * @param rate1 The first rate to combine. + * @param rate2 The other rate to combine. + */ + public Rate(Rate rate1, Rate rate2) { + this.rate = rate1.rate; + this.additive = rate1.additive; + if (rate1.additive == rate2.additive) { + this.rate += rate2.rate; + } else { + this.rate -= rate2.rate; + if (this.rate < 0) { + this.rate = Math.abs(rate); + this.additive = !this.additive; + } + } + if (this.rate > 1.0) { + this.rate = 1.0; + } + } + + /** + * Apply this rate to a Price2. + * @param base The base price. + * @return The new price augmented by the rate when additive, + * or diminished by the rate when subtractive. + */ + public Price2 applyTo(Price2 base) { + if (this.additive) { + return base.multiply(1.00 + this.rate); + } else { + return base.multiply(1.00 - this.rate); + } + } + + /** + * Apply this rate to a Price5. + * @param base The base price. + * @return The new price augmented by the rate when additive, + * or diminished by the rate when subtractive. + */ + public Price5 applyTo(Price5 base) { + if (this.additive) { + return base.multiply(1.00 + this.rate); + } else { + return base.multiply(1.00 - this.rate); + } + } + + /** + * Get the variation before and after applying the rate. + * @param base The base price. + * @return The variation between the base price and the price after + * the rate is applied. This variation is always positive when the + * base price is positive, even when the rate is subtractive. + */ + public Price2 getVariation(Price2 base) { + if (this.additive) { + return this.applyTo(base).subtract(base); + } else { + return base.subtract(this.applyTo(base)); + } + } + + /** + * Get the variation before and after applying the rate. + * @param base The base price. + * @return The variation between the base price and the price after + * the rate is applied. This variation is always positive when the + * base price is positive, even when the rate is subtractive. + */ + public Price5 getVariation(Price5 base) { + if (this.additive) { + return this.applyTo(base).subtract(base); + } else { + return base.subtract(this.applyTo(base)); + } + } + + /** + * Get the base price from the result. + * @param computed The price after the rate was applied. + * @return The base price before the rate was applied. + */ + public Price2 revertFrom(Price2 computed) { + if (this.additive) { + return computed.divide(1.00 + this.rate); + } else { + return computed.divide(1.00 - this.rate); + } + } + + /** + * Get the base price from the result. + * @param computed The price after the rate was applied. + * @return The base price before the rate was applied. + */ + public Price5 revertFrom(Price5 computed) { + if (this.additive) { + return computed.divide(1.00 + this.rate); + } else { + return computed.divide(1.00 - this.rate); + } + } + + /** + * Get the variation before and after reverting the rate. + * @param computed The price after the rate was applied. + * @return The variation between the price after the rate was applied + * and the base price. This variation is always positive when the + * price is positive, even when the rate is subtractive. + */ + public Price2 getRevertVariation(Price2 computed) { + if (this.additive) { + return computed.subtract(this.revertFrom(computed)); + } else { + return this.revertFrom(computed).subtract(computed); + } + } + + /** + * Get the variation before and after reverting the rate. + * @param computed The price after the rate was applied. + * @return The variation between the price after the rate was applied + * and the base price. This variation is always positive when the + * price is positive, even when the rate is subtractive. + */ + public Price5 getRevertVariation(Price5 computed) { + if (this.additive) { + return computed.subtract(this.revertFrom(computed)); + } else { + return this.revertFrom(computed).subtract(computed); + } + } + + /** + * Get the rate to apply. + * @return The rate, between 0 and 1. + */ + public double getRate() { + return this.rate; + } + + /** + * Check whether this rate will augment prices or reduce them. + * @return True when applying the rate will augment prices. + */ + public boolean isAdditive() { + return this.additive; + } + + /** + * Check whether this rate will augment prices or reduce them. + * @return True when applying the rate will decrease prices. + */ + public boolean isSubtractive() { + return !this.additive; + } +} diff --git a/src/main/java/org/pasteque/coreutil/price/Tax.java b/src/main/java/org/pasteque/coreutil/price/Tax.java new file mode 100644 index 0000000..38b95a0 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/Tax.java @@ -0,0 +1,97 @@ +package org.pasteque.coreutil.price; + +import org.pasteque.coreutil.constants.TaxType; + +/** + * Description of a tax to be able to compute the amount from a base price. + */ +public class Tax +{ + /** See {@link getType()}. */ + private TaxType type; + /** See {@link getReference()}. */ + private String reference; + /** See {@link getLabel()}. */ + private String label; + /** See {@link getAmount()}. */ + private double amount; + /** See {@link isIncludedInBase()}. */ + private boolean includedInBase; + + /** + * Create a tax from all fields. + * @param type See {@link getType()}. + * @param reference See {@link getReference()}. + * @param label See {@link getLabel()}. + * @param amount See {@link getAmount()}. + * @param includedInBase See {@link isIncludedInBase()}. + */ + public Tax( + TaxType type, + String reference, + String label, + double amount, + boolean includedInBase) { + this.type = type; + this.reference = reference; + this.label = label; + this.amount = amount; + this.includedInBase = includedInBase; + } + + /** + * Create from a DTO. Shared references are immutable. + * @param dto The DTO to convert. + * TODO: TaxDTO is TaxAmountDTO + */ + /*public Tax(TaxDTO dto) { + this.type = TaxType.fromCode(dto.getType()); + this.reference = dto.getReference(); + this.label = dto.getLabel(); + this.amount = dto.getAmount(); + this.includedInBase = dto.getIncludedInBase(); + }*/ + + /** + * Get the type of the tax, to know how to compute amounts. + * @return The type of the tax. + */ + public TaxType getType() { + return this.type; + } + + /** + * Get the unique reference of this tax. + * @return The unique reference. + */ + public String getReference() { + return this.reference; + } + + /** + * Get the label of this tax. + * @return The label of this tax. + */ + public String getLabel() { + return this.label; + } + + /** + * Get the amount of tax for computation, its meaning depends upon + * its type. + * @return The rate or fixed amount of tax, according to its type. + */ + public double getAmount() { + return this.amount; + } + + /** + * Get whether the amount of tax is added to the base price or included + * in it to compute the taxed price. + * @return True when the tax amount must be included in the base price, + * false when the tax amount must be added to the base price. + */ + public boolean isIncludedInBase() { + return this.includedInBase; + } +} diff --git a/src/main/java/org/pasteque/coreutil/price/TaxAmount.java b/src/main/java/org/pasteque/coreutil/price/TaxAmount.java new file mode 100644 index 0000000..b164a42 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/TaxAmount.java @@ -0,0 +1,82 @@ +package org.pasteque.coreutil.price; + +/** + *

Tax amounts, in various precision for different purposes.

+ *

Use {@link org.pasteque.common.model.order.PriceRound} to compute amounts.

+ */ +public final class TaxAmount +{ + /** See {@link getTax()}. */ + private Tax tax; + /** See {@link getBase()}. */ + private Price base; + /** See {@link getAmount()}. */ + private Price amount; + + /** + * Create a tax amount from all fields. + * @param tax See {@link getTax()}. + * @param base See {@link getBase()}. + * @param amount See {@link getAmount()}. + */ + public TaxAmount( + Tax tax, + Price base, + Price amount) { + this.tax = tax; + this.base = base; + this.amount = amount; + } + + /** + * Create a new tax amount by adding an other amount of the same tax. + * @param other The other amount to add. + * @return A new amount containing the sum of the amounts. + * @throws IllegalArgumentException When other is not an amount + * of the same type. + */ + public TaxAmount sum(TaxAmount other) throws IllegalArgumentException { + if (!this.isSameTax(other)) { + throw new IllegalArgumentException("Cannot add amounts from different taxes"); + } + return new TaxAmount( + this.tax, + base.add(other.getBase()), + amount.add(other.getAmount())); + } + + /** + * Get the tax associated to this amount. + * @return The tax. + */ + public Tax getTax() { + return this.tax; + } + + /** + * Get the base amount from which the tax is computed. It has + * no effect for fixed taxes but is used for rates. + * @return The base amount to compute the tax. + */ + public Price getBase() { + return this.base; + } + + /** + * The amount of tax. + * @return The amount of tax. + */ + public Price getAmount() { + return this.amount; + } + + /** + * Check whether an other tax amount is an amount of the same tax. + * @param other The other amount to check. + * @return True when both amounts are from the same tax. That is, both + * taxes shares the same reference. + */ + public boolean isSameTax(TaxAmount other) { + return this.getTax().getReference().equals(other.getTax().getReference()); + } +} diff --git a/src/main/java/org/pasteque/coreutil/price/package-info.java b/src/main/java/org/pasteque/coreutil/price/package-info.java new file mode 100644 index 0000000..b1ba43a --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/price/package-info.java @@ -0,0 +1,11 @@ +/** + *

Utilities to compute prices and make math operations on them.

+ *

Rounding issues are unavoidable when dispatching amounts into prices + * with only 2 decimals. This happens when a fixed amount is dispatched into + * multiple amounts, without increasing the precision. That is, when + * dispatching a discount to multiple lines, either for categories or taxes.

+ *

When such rounding issues happen, the sum of untaxed prices and tax amounts + * differs from the grand total. The French accounting put these unavoidable + * differences in accounts 658 and 758.

+ */ +package org.pasteque.coreutil.price; diff --git a/src/main/java/org/pasteque/coreutil/transition/LineTransition.java b/src/main/java/org/pasteque/coreutil/transition/LineTransition.java new file mode 100644 index 0000000..649a36a --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/transition/LineTransition.java @@ -0,0 +1,66 @@ +package org.pasteque.coreutil.transition; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.price.Discount; +import org.pasteque.coreutil.price.Price; +import org.pasteque.coreutil.price.Quantity; +import org.pasteque.coreutil.price.TaxAmount; + +/** + * Transitional interface to mark order lines suitable to source a MajorOrder. + */ +public interface LineTransition { + /** + * Get the unique reference of the product in this line. + * @return The unique reference of the product. + */ + public String getProductReference(); + + /** + * Get the label of the product in this line. + * @return The label of the product. + */ + public String getProductLabel(); + + /** + * Get the reference price for 1 unit of the product without taxes. + * @return The price for 1 unit. It should be a + * {@link org.pasteque.coreutil.price.Price5} to be able to compute a + * taxed price as {@link org.pasteque.coreutil.price.Price2} and avoid + * rounding issues in most cases. + */ + public Price getUnitPrice(); + + /** + * Get the quantity of product in this line. + * @return The quantity of product. + */ + public Quantity getQuantity(); + + /** + * Get the list of taxes to apply to the line. + * @return The list of taxes applicable to the line. + * The amount may be of any precision. + */ + public ImmutableList getTotalTaxes(); + + /** + * Get the price without taxes of this line before the discount. + * @return The price without taxes before applying any discount. + */ + public Price getPrice(); + + /** + * Get the price without tax including the discount. + * @return The price without tax including the discount. + */ + public Price getTotalPrice(); + + /** + * Get the full discount applied to the base price to compute + * the total. It may be computed from multiple discounts applied + * to the the line. + * @return The full discount applied to the base price. + */ + public Discount getFinalDiscount(); +} diff --git a/src/main/java/org/pasteque/coreutil/transition/OrderTransition.java b/src/main/java/org/pasteque/coreutil/transition/OrderTransition.java new file mode 100644 index 0000000..135f964 --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/transition/OrderTransition.java @@ -0,0 +1,35 @@ +package org.pasteque.coreutil.transition; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.price.FinalTaxAmount; +import org.pasteque.coreutil.price.Price2; + +/** + * Transitional interface to mark order suitable to source a MajorOrder. + */ +public interface OrderTransition +{ + /** + * Get the price without tax of the whole order. + * @return The price without tax of the whole order. + */ + public Price2 getFinalPrice(); + + /** + * Get the amount of taxes of the whole order. + * @return The amount of taxes of the whole order. + */ + public ImmutableList getTaxAmounts(); + + /** + * Get the price with taxes of the whole order. + * @return The price with taxes of the whole order. + */ + public Price2 getFinalTaxedPrice(); + + /** + * Get the content of the order. + * @return The content of the order. + */ + public ImmutableList getLines(); +} diff --git a/src/main/java/org/pasteque/coreutil/transition/package-info.java b/src/main/java/org/pasteque/coreutil/transition/package-info.java new file mode 100644 index 0000000..1f96d0d --- /dev/null +++ b/src/main/java/org/pasteque/coreutil/transition/package-info.java @@ -0,0 +1,10 @@ +/** + *

Transitionnal interfaces to convert data from the common package to the + * major package.

+ *

The aim of this package is to loosen the dependency between common and + * major. It should contain only interfaces for classes from the common package + * to implement without introducing a dependency to the major package.

+ *

The immutability of the converted data is ensured on the major side, + * implementations can provide references to the data.

+ */ +package org.pasteque.coreutil.transition; diff --git a/src/main/java/org/pasteque/major/domain/FiscalTicket.java b/src/main/java/org/pasteque/major/domain/FiscalTicket.java new file mode 100644 index 0000000..ea27afb --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/FiscalTicket.java @@ -0,0 +1,331 @@ +package org.pasteque.major.domain; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import org.pasteque.coreutil.datatransfer.format.DateDTOFormat; +import org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO; +import org.pasteque.coreutil.constants.FiscalTicketType; + +/** + *

Immutable and signed ticket records. Fiscal tickets are used to comply + * with the immutability and integrity check of the underlying ticket.

+ *

Fiscal tickets contains a ticket in raw format to stay immutable even + * after updates. The ticket is stored in the {@link getContent() content} and + * signed with the previous one.

+ * @see org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO + */ +public class FiscalTicket +{ + /** + * The name of the default hash algorithm to use. + * @see java.security.MessageDigest(String) + */ + private static final String DEFAULT_HASH_ALGORITHM = "SHA3-512"; + /** + * The prefix to add to the signature to indicate the signature + * was generated with SHA3-512. + */ + private static final String SHA3_512_PREFIX = "sha3-512:"; + /** + * The prefix to add to the signature to indicate the signature + * was generated with BCrypt. + */ + private static final String BCRYPT_PREFIX = "bcrypt:"; + + /** See {@link getType()}. */ + private final FiscalTicketType type; + /** See {@link getSequence()}. */ + private final String sequence; + /** See {@link getNumber()}. */ + private final int number; + /** See {@link getCreateDate()}. */ + private final Date createDate; + /** See {@link getContent()}. */ + private final String content; + /** See {@link getSignature()}. */ + private final String signature; + /** See {@link getWriteDate()}. */ + private final Date writeDate; + + /** + * Create from all fields. + * @param type The type of ticket. + * @param sequence The sequence of the fiscal ticket. + * @param number The number of the fiscal ticket. + * @param createDate The creation date of the fiscal ticket. + * @param content The content of the underlying ticket. + * @param signature The chained signature of the record. + * @param writeDate The date when the fiscal ticket was received. + */ + public FiscalTicket( + FiscalTicketType type, + String sequence, + int number, + Date createDate, + String content, + String signature, + Date writeDate) { + this.type = type; + this.sequence = sequence; + this.number = number; + this.createDate = createDate; + this.content = content; + this.signature = signature; + this.writeDate = writeDate; + } + + /** + * Create and chain a new fiscal ticket. + * @param createDate See {@link getCreateDate()}. + * @param content The content of the underlying ticket. + * @param previous The previous fiscal ticket. The type, sequence, number + * and signature will be deducted from it without sharing references. + * @param writeDate See {@link getWriteDate()}. + * @throws NoSuchAlgorithmException When the default algorithm is not + * implemented. + */ + public FiscalTicket( + Date createDate, + String content, + FiscalTicket previous, + Date writeDate) + throws NoSuchAlgorithmException { + this.type = previous.getType(); + this.sequence = new String(previous.getSequence()); + this.number = previous.getNumber() + 1; + this.createDate = createDate; + this.content = content; + this.signature = this.computeSignature(previous); + this.writeDate = writeDate; + } + + /** + * Create from a DTO. All data are copied to break references. + * @param dto The DTO to convert. + * @throws IllegalArgumentException When the type is unknown. + * @see org.pasteque.coreutil.constants.FiscalTicketType + */ + public FiscalTicket(FiscalTicketDTO dto) throws IllegalArgumentException { + this.type = FiscalTicketType.fromCode(dto.getType()); + this.sequence = new String(dto.getSequence()); + this.number = dto.getNumber(); + this.createDate = new Date(dto.getCreateDate().getTime()); + this.content = new String(dto.getContent()); + this.signature = new String(dto.getSignature()); + this.writeDate = new Date(dto.getWriteDate().getTime()); + } + + /** + * Get the text used to generate the hash. + * @return A string to use to generate the signature, including + * the sequence, the number, the date and the content of the ticket. + */ + protected final String getHashBase() { + return String.format("%s-%i-%i-%s", + this.sequence, + this.number, + DateDTOFormat.toSecTimestamp(this.createDate), + this.content); + } + + /** + * Get the text used to generate the hash. + * @param previous The previous fiscal ticket to chain the signature. + * Can be null to get the hash of the first ticket of a chain. + * @return A string to use to generate the signature, including + * the sequence, the number, the date and the content of the ticket, + * and the signature of the previous one if provided. + */ + protected final String getHashBase(FiscalTicket previous) { + if (previous == null) { + return this.getHashBase(); + } + return String.format("%s-%s", + previous.getSignature(), + this.getHashBase()); + } + + /** + * Set the signature from the content of this ticket and the + * previous one, using the default algorithm. + * @param previous The previous ticket to chain the signatures. + * Can be null for the first one of the chain. + * @throws NoSuchAlgorithmException When the default algorithm is not + * implemented. + * @return The signature to apply to this fiscal ticket. + * @throws AssertionError When the signature is already set. + */ + protected final String computeSignature(FiscalTicket previous) + throws NoSuchAlgorithmException, AssertionError { + if (this.signature != null) { + throw new AssertionError("Trying to sign an already signed FiscalTicket"); + } + return this.computeSignature(previous, DEFAULT_HASH_ALGORITHM); + } + + /** + * Set the signature from the content of this ticket and the + * previous one, using the given algorithm. + * @param previous The previous ticket to chain the signatures. + * Can be null for the first one of the chain. + * @param algorithm Algorithm name to use. Supported ones: "sha3-512" and + * "bcrypt" (case insensitive). + * @throws NoSuchAlgorithmException When the default algorithm is not + * implemented. + * @throws AssertionError When the signature is already set or when + * the algorithm is not allowed. + */ + private String computeSignature(FiscalTicket previous, String algorithm) + throws NoSuchAlgorithmException, AssertionError { + String hashBase = this.getHashBase(previous); + if ("sha3-512".equals(algorithm.toLowerCase())) { + return getSignatureSHA3512(hashBase); + } else if ("bcrypt".equals(algorithm.toLowerCase())) { + return getSignatureBCrypt(hashBase); + } else { + throw new AssertionError(String.format("Cannot sign fiscal ticket with %s", + algorithm)); + } + } + + /** + * Create a hash with SHA3-512. + * @param hashBase The string to hash. + * @throws NoSuchAlgorithmException When SHA3-512 is not implemented. + * @return The SHA3-512 digest, prefixed by "sha3-512:". + */ + private static String getSignatureSHA3512(String hashBase) + throws NoSuchAlgorithmException { + MessageDigest encrypt = MessageDigest.getInstance("SHA3-512"); + try { + byte[] hash = encrypt.digest(hashBase.getBytes("UTF-8")); + return String.format("%s:%s", + SHA3_512_PREFIX, + new String(hash, "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new AssertionError("UTF-8 is not available"); + } + } + + @Deprecated + /** + * Compute the signature using BCrypt. + * @deprecated This algorithm is not suited to sign fiscal tickets. + * It is still there only for retro-compatibility and tests. + * @param hashBase The string to hash. + * @return The BCrypt digest, prefixed by "bcrypt:". + */ + private static String getSignatureBCrypt(String hashBase) { + // TODO include BCrypt + // also add @throws NoSuchAlgorithmException When BCrypt is not implemented. + throw new UnsupportedOperationException(); + } + + /** + * Check if the signature of this ticket is correct. + * @param previous The previous fiscal ticket this one is linked to. + * Can be null when checking the first ticket of a chain. + * @throws NoSuchAlgorithmException When the algorithm used to sign + * the fiscal ticket is not implemented. + * @return True when the signature is correct, false if it doesn't match + * with the expected one or if this fiscal ticket is not signed. + */ + public final boolean checkSignature(FiscalTicket previous) + throws NoSuchAlgorithmException { + if (this.signature == null) { + return false; + } + String hashBase = this.getHashBase(previous); + if (this.signature.startsWith(SHA3_512_PREFIX)) { + return this.signature.equals(getSignatureSHA3512(hashBase)); + } else if (this.signature.startsWith(BCRYPT_PREFIX)) { + return this.signature.equals(getSignatureBCrypt(hashBase)); + } else { + // First versions had no prefix and used BCrypt + String prefixedSign = String.format("%s:%s", + BCRYPT_PREFIX, + this.signature); + return prefixedSign.equals(getSignatureBCrypt(hashBase)); + } + } + + /** + * Get the type of this ticket. + * @return The type of ticket in content. + */ + public final FiscalTicketType getType() { + return this.type; + } + + /** + * Get the identifier of the sequence. A sequence is generally identifying + * a cash register. + * @return The sequence of this ticket. + */ + public final String getSequence() { + return this.sequence; + } + + /** + * Get the number of this fiscal ticket. The number is incremental + * within each session and starts from 1. + * Number 0 is reserved for the special "End Of Sequence" (EOF) ticket. + * @return The number of this ticket. + */ + public final int getNumber() { + return this.number; + } + + /** + * Get the creation date of this fiscal ticket. + * It is the same as the date of the ticket in content. + * @return The creation date. + */ + public final Date getCreateDate() { + return this.createDate; + } + + /** + * Get the raw representation of the registered ticket. + * @return The content of the ticket. + */ + public final String getContent() { + return this.content; + } + + /** + * Get the signature of this fiscal ticket, chained with the previous one. + * @return The signature. + * @see org.pasteque.major.domain.FiscalTicket#checkSignature(org.pasteque.major.model.FiscalTicket) + */ + public final String getSignature() { + return this.signature; + } + + /** + * Get the registration date of this fiscal ticket. + * It may be different from the date of the ticket in content + * and is only informational. + * @return The registration date. + */ + public final Date getWriteDate() { + return this.writeDate; + } + + /** + * Get a DTO from this object. + * @return The content of this object as a DTO. + */ + public final FiscalTicketDTO toDTO() { + return new FiscalTicketDTO( + this.type.getCode(), + this.sequence, + this.number, + this.createDate, + this.content, + this.signature, + this.writeDate); + } +} diff --git a/src/main/java/org/pasteque/major/domain/MajorCashRegister.java b/src/main/java/org/pasteque/major/domain/MajorCashRegister.java new file mode 100644 index 0000000..0920e82 --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/MajorCashRegister.java @@ -0,0 +1,80 @@ +package org.pasteque.major.domain; + +import org.pasteque.coreutil.datatransfer.dto.CashRegisterDTO; + +/** + *

Model for a cash register. Tickets are associated to a cash register + * session, which themselves reference a cash register.

+ */ +public class MajorCashRegister +{ + /** {@see getReference()} */ + private String reference; + /** {@see getLabel()} */ + private String label; + /** {@see getNextTicketNumber()} */ + private int nextTicketNumber; + + /** + * Create from all fields. + * @param reference See {@link getReference()}. + * @param label See {@link getLabel()}. + * @param nextTicketNumber The number to assign to the next ticket + * from this cash register. + */ + public MajorCashRegister( + String reference, + String label, + int nextTicketNumber) { + this.reference = reference; + this.label = label; + this.nextTicketNumber = nextTicketNumber; + } + + /** + * Create from a DTO. All data are copied to break references. + * @param dto The DTO to convert. + */ + public MajorCashRegister(CashRegisterDTO dto) { + this.reference = new String(dto.getReference()); + this.label = new String(dto.getLabel()); + this.nextTicketNumber = dto.getNextTicketNumber(); + } + + /** + * Get the reference. It is a user friendly identifier. + * @return The reference. + */ + public final String getReference() { + return this.reference; + } + + /** + * Get the display name. + * @return The label of this cash register. + */ + public final String getLabel() { + return this.label; + } + + /** + * Get the number to assign to the next ticket from the situation + * known by the sender and increment it. + * @return The number to assign to the next ticket, + * that is incremented after that.. + */ + /* package */ final int assignNextTicketNumber() { + int next = this.nextTicketNumber; + this.nextTicketNumber++; + return next; + } + + /** + * Cancel the assignment of the ticket number. Use it solely to revert + * from ticket creation when an error occurs. It will allow to get the + * same ticket number to recreate it. + */ + /* package */ final void revertNextTicketNumber() { + this.nextTicketNumber--; + } +} diff --git a/src/main/java/org/pasteque/major/domain/MajorCashSession.java b/src/main/java/org/pasteque/major/domain/MajorCashSession.java new file mode 100644 index 0000000..0ada81c --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/MajorCashSession.java @@ -0,0 +1,242 @@ +package org.pasteque.major.domain; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import org.pasteque.coreutil.datatransfer.dto.CashSessionDTO; +import org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO; +import org.pasteque.coreutil.datatransfer.dto.MovementDTO; + +/** + *

Ongoing cash session, managing movements and payments.

+ *

This is the main class to manage transactions. A session must + * start by opening it with {@link open(Date, List)}. Then movements + * and tickets can be added with {@link addMovement(Movement)} and + * {@link addTicket(MajorOrder, List)}. Finally the session can be closed + * to generate a {@link MajorZTicket} and set the state of the next session. + */ +public final class MajorCashSession +{ + /** See {@link getCashRegister()}. */ + private MajorCashRegister cashRegister; + /** See {@link getSequence()}. */ + private int sequence; + /** See {@link isContinuous()}. */ + private boolean continuous; + /** See {@link getOpenDate()}. */ + private Date openDate; + /** See {@link getMovements()}. */ + private List movements; + /** See {@link getTickets()}. */ + private List tickets; + /** See {@link isClosed()}. */ + private boolean closed; + + /** + * Create a cash session from all fields. + * @param cashRegister See {@link getCashRegister()}. + * @param sequence See {@link getSequence()}. + * @param continuous See {@link isContinuous()}. + * @param openDate See {@link getOpenDate()}. + * @param movements The list of movements registered during this session. + * @param tickets The list of tickets already registered during this session. + */ + public MajorCashSession( + MajorCashRegister cashRegister, + int sequence, + boolean continuous, + Date openDate, + List movements, + List tickets) { + this.cashRegister = cashRegister; + this.sequence = sequence; + this.continuous = continuous; + this.openDate = openDate; + this.movements = movements; + this.tickets = tickets; + this.closed = false; + } + + /** + * Create from a DTO. All data are copied to break references. + * @param dto The DTO to convert. + * @param cashRegister The cash register to assign. + * @throws IllegalArgumentException When the cash register doesn't match + * the one described in the dto. + * @see org.pasteque.coreutil.constants.FiscalTicketType + */ + public MajorCashSession(CashSessionDTO dto, MajorCashRegister cashRegister) + throws IllegalArgumentException { + if (!dto.getCashRegister().getReference().equals(cashRegister.getReference())) { + throw new IllegalArgumentException("Cash register doesn't match the one from the DTO"); + } + this.cashRegister = cashRegister; + this.sequence = dto.getSequence(); + this.continuous = dto.isContinuous(); + this.openDate = new Date(dto.getOpenDate().getTime()); + this.movements = new LinkedList(); + for (MovementDTO mvtDTO : dto.getMovements()) { + this.movements.add(new Movement(mvtDTO)); + } + this.tickets = new LinkedList(); + for (FiscalTicketDTO tktDTO : dto.getTickets()) { + this.tickets.add(new MajorTicket(tktDTO)); + } + this.closed = false; + } + + /** + * Open the session to allow registering movements and tickets. + * @param openDate The date when the session is opened. + * @param startingAmounts The amounts in the drawer when the session is opened. + * @return True when the session was opened. False if it was already opened + * and did nothing. + */ + public boolean open(Date openDate, List startingAmounts) { + if (this.isOpened()) { + return false; + } + this.openDate = openDate; + this.movements.addAll(startingAmounts); + return true; + } + + /** + * Add a movement to this session. + * @param movement The movement to add. + * @return True when the session is opened and the movement was added. + * False otherwise. + * @see isOpened() + */ + public boolean addMovement(Movement movement) { + if (!this.isOpened()) { + return false; + } + this.movements.add(movement); + return true; + } + + /** + * Get the count of movements registered within this session. + * @return The count of movements. + * @see java.util.List#size() + */ + public int getMovementsSize() { + return this.movements.size(); + } + + /** + * Get a movement. + * @param index The index in the internal movement list. + * @return The n-th registered movement. + * @see java.util.List#get(int) + */ + public Movement getMovement(int index) { + return this.movements.get(index); + } + + /** + *

Create a ticket in this sequence if not closed and return it.

+ *

The session must be opened. The closed state is registered + * only as a temporary state, as a closed session should be discarded + * right after is it closed and transformed into a + * {@link org.pasteque.major.domain.MajorZTicket}

. + * @param order The order that was paid. + * @param payments The payments used to pay the order. + * @return The ticket created from the order and the payments. It is + * already added to the tickets of this session. Null when trying to + * register a ticket to a not opened or closed session. + * @throws IllegalStateException When the session is not opened. + * @see isOpened() + */ + public MajorTicket addTicket(MajorOrder order, List payments) { + if (!this.isOpened()) { + throw new IllegalStateException("Session must be opened to register a ticket"); + } + MajorTicket tkt = new MajorTicket(this, order, payments); + this.tickets.add(tkt); + return tkt; + } + + /** + * Get the count of tickets registered within this session. + * @return The count of tickets. + * @see java.util.List#size() + */ + public int getTicketsSize() { + return this.tickets.size(); + } + + /** + * Get a ticket. + * @param index The index in the internal ticket list. + * @return The n-th registered ticket. + * @see java.util.List#get(int) + */ + public MajorTicket getTicket(int index) { + return this.tickets.get(index); + } + + /** + * Check whether this session is opened and can accept operations. + * @return True when the session was opened and not closed yet. + */ + public boolean isOpened() { + return this.openDate != null && !this.closed; + } + + /** + * Check whether this session was closed and cannot accept new tickets. + * @return True when the session is closed (i.e. {@link close()} was called. + */ + public boolean isClosed() { + return this.closed; + } + + /** + * Close this session and prevent registering new tickets on it. + * When a session is closed, registering a ticket will fail silently. + * @return The resulting MajorZTicket. + */ + public MajorZTicket close() { + return null; + } + + /** + * Get the cash register assigned to this session. + * @return The cash register assigned to this session. + */ + public MajorCashRegister getCashRegister() { + return cashRegister; + } + + /** + * Get the sequence of this session for this cash register. + * @return The sequence number. It will be increased when the + * next session is created. + */ + public int getSequence() { + return sequence; + } + + /** + * Check if the session was continuous starting from the previous one. + * The session is continuous when it started on the same machine than + * the previous one and with the previous one in cache. + * @return True when the session is marked as continuous. + * @see org.pasteque.coreutil.datatransfer.dto.ZTicketDTO#isContinuous() + */ + public boolean isContinuous() { + return continuous; + } + + /** + * Get the date when the session was opened. + * @return The open date. It may be null if the session was not opened yet. + * @see org.pasteque.coreutil.datatransfer.dto.ZTicketDTO#getOpenDate() + */ + public Date getOpenDate() { + return openDate; + } +} diff --git a/src/main/java/org/pasteque/major/domain/MajorLine.java b/src/main/java/org/pasteque/major/domain/MajorLine.java new file mode 100644 index 0000000..683fd70 --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/MajorLine.java @@ -0,0 +1,138 @@ +package org.pasteque.major.domain; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.price.Discount; +import org.pasteque.coreutil.price.Price; +import org.pasteque.coreutil.price.Quantity; +import org.pasteque.coreutil.price.TaxAmount; +import org.pasteque.coreutil.transition.LineTransition; + +/** + * Final and immutable version of a line of an order or ticket. + *

This class implements {@link org.pasteque.coreutil.transition.LineTransition} + * for ease of use, but instances are not expected to be generated from + * themselves.

+ * @see org.pasteque.major.domain.MajorOrder + * @see org.pasteque.major.domain.MajorTicket + */ +// TODO add extradata to pass to MajorTicket +public final class MajorLine implements LineTransition +{ + /** See {@link getProductReference()}. */ + private final String productReference; + /** See {@link getProductLabel()}. */ + private final String productLabel; + /** See {@link getUnitPrice()}. */ + private final Price unitPrice; + /** See {@link getQuantity()}. */ + private final Quantity quantity; + /** See {@link getPrice()}. */ + private final Price price; + /** See {@link getTotalTaxes()}. */ + private final ImmutableList totalTaxes; + /** See {@link getTotalPrice()}. */ + private final Price totalPrice; + /** See {@link getFinalDiscount()}. */ + private final Discount finalDiscount; + + /** + *

Create a major line from outside this package. + * The content is copied to break references and ensure immutability.

+ *

This method has a limited scope to be created only from a + * {@link org.pasteque.major.domain.MajorOrder}.

+ * @param from The implementation of the content to convert. + */ + /* package */ static ImmutableList fromTransition(ImmutableList from) { + MajorLine[] ret = new MajorLine[from.size()]; + for (int i = 0; i < from.size(); i++) { + ret[i] = new MajorLine(from.get(i)); + } + return new ImmutableList(ret); + } + + /** + *

Create from all data.

+ *

This method has a limited scope to be created from tests.

+ * @param productReference See {@link getProductReference()}. + * @param productLabel See {@link getProductLabel()}. + * @param unitPrice See {@link getUnitPrice()}. + * @param quantity See {@link getQuantity()}. + * @param price See {@link getPrice()}. + * @param totalTaxes See {@link getTotalTaxes()}. + * @param totalPrice See {@link getTotalPrice()}. + * @param finalDiscount See {@link getFinalDiscount()}. + */ + /* package */ MajorLine( + String productReference, + String productLabel, + Price unitPrice, + Quantity quantity, + Price price, + ImmutableList totalTaxes, + Price totalPrice, + Discount finalDiscount) { + this.productReference = productReference; + this.productLabel = productLabel; + this.unitPrice = unitPrice; + this.quantity = quantity; + this.price = price; + this.totalTaxes = totalTaxes; + this.totalPrice = totalPrice; + this.finalDiscount = finalDiscount; + } + + /** + * Create a line from a compatible source. + * @param source The line to convert as a MajorLine. + */ + public MajorLine(LineTransition source) { + this.productReference = source.getProductReference(); + this.productLabel = source.getProductLabel(); + this.unitPrice = source.getUnitPrice(); + this.quantity = source.getQuantity(); + this.price = source.getPrice(); + this.totalTaxes = source.getTotalTaxes(); + this.totalPrice = source.getTotalPrice(); + this.finalDiscount = source.getFinalDiscount(); + } + + @Override // from LineTransition + public String getProductReference() { + return this.productReference; + } + + @Override // from LineTransition + public String getProductLabel() { + return this.productLabel; + } + + @Override // from LineTransition + public Price getUnitPrice() { + return this.unitPrice; + } + + @Override // from LineTransition + public Quantity getQuantity() { + return this.quantity; + } + + @Override // from LineTransition + public ImmutableList getTotalTaxes() { + return this.totalTaxes; + } + + @Override // from LineTransition + public Price getPrice() { + return this.price; + } + + @Override // from LineTransition + public Price getTotalPrice() { + return this.totalPrice; + } + + @Override // from LineTransition + public Discount getFinalDiscount() { + return this.finalDiscount; + } +} diff --git a/src/main/java/org/pasteque/major/domain/MajorOrder.java b/src/main/java/org/pasteque/major/domain/MajorOrder.java new file mode 100644 index 0000000..56cdfa3 --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/MajorOrder.java @@ -0,0 +1,113 @@ +package org.pasteque.major.domain; + +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.price.FinalTaxAmount; +import org.pasteque.coreutil.price.Price2; +import org.pasteque.coreutil.transition.LineTransition; +import org.pasteque.coreutil.transition.OrderTransition; + +/** + *

Final and immutable version of an order to be transformed to a + * {@link MajorTicket} when assigning payments to it.

+ *

This class implements {@link org.pasteque.coreutil.transition.OrderTransition} + * for ease of use, but instances are not expected to be generated from + * themselves.

+ */ +// TODO add extradata to pass to MajorTicket +public final class MajorOrder implements OrderTransition +{ + private final Price2 finalPrice; + private final ImmutableList taxAmounts; + private final Price2 finalTaxedPrice; + private final ImmutableList lines; + + /** + * Create a major order from all data. + * @param finalPrice See {@link getFinalPrice()}. + * @param taxAmounts See {@link getTaxAmounts()}. + * @param finalTaxedPrice See {@link getFinalTaxedPrice()}. + * @param lines See {@link getLines()}. + */ + public MajorOrder( + Price2 finalPrice, + ImmutableList taxAmounts, + Price2 finalTaxedPrice, + ImmutableList lines) { + this.finalPrice = finalPrice; + this.taxAmounts = taxAmounts; + this.finalTaxedPrice = finalTaxedPrice; + this.lines = lines; + } + + /** + * Create a major order from outside this package. + * The content is copied to break references and ensure immutability. + * @param order The implementation of the order to convert. + */ + public MajorOrder(OrderTransition order) { + this.finalPrice = order.getFinalPrice(); + this.taxAmounts = order.getTaxAmounts(); + this.finalTaxedPrice = order.getFinalTaxedPrice(); + MajorLine[] convertLines = new MajorLine[order.getLines().size()]; + ImmutableList orderLines = order.getLines(); + for (int i = 0; i < orderLines.size(); i++) { + LineTransition trans = orderLines.get(i); + convertLines[i] = new MajorLine( + trans.getProductReference(), + trans.getProductLabel(), + trans.getUnitPrice(), + trans.getQuantity(), + trans.getPrice(), + trans.getTotalTaxes(), + trans.getTotalPrice(), + trans.getFinalDiscount()); + + } + this.lines = MajorLine.fromTransition(order.getLines()); + } + + /** + * Get the price without tax of the whole order. + * @return The price without tax of the whole order. + */ + public Price2 getFinalPrice() { + return this.finalPrice; + } + + /** + * Get the amount of taxes of the whole order. + * @return The amount of taxes of the whole order. + */ + public ImmutableList getTaxAmounts() { + return this.taxAmounts; + } + + /** + * Get the price with taxes of the whole order. + * @return The price with taxes of the whole order. + */ + public Price2 getFinalTaxedPrice() { + return this.finalTaxedPrice; + } + + /** + * Get the content of the order. + * @return The lines in the order. + * @see getMajorLines() + */ + public ImmutableList getLines() { + LineTransition[] transLines = new LineTransition[this.lines.size()]; + for (int i = 0; i < this.lines.size(); i++) { + transLines[i] = this.lines.get(i); + } + return new ImmutableList(transLines); + } + + /** + * Get the content of the order. + * @return The lines in the order. + */ + public ImmutableList getMajorLines() { + return this.lines; + } +} diff --git a/src/main/java/org/pasteque/major/domain/MajorTicket.java b/src/main/java/org/pasteque/major/domain/MajorTicket.java new file mode 100644 index 0000000..5b4d6c3 --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/MajorTicket.java @@ -0,0 +1,160 @@ +package org.pasteque.major.domain; + +import java.util.Date; +import java.util.List; +import org.pasteque.coreutil.constants.FiscalTicketType; +import org.pasteque.coreutil.price.Price2; +import org.pasteque.coreutil.price.TaxAmount; +import org.pasteque.coreutil.ImmutableList; +import org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO; + +/** + *

Model for a finalized ticket, resulting from the payment of an order. + * This model contains only the basic informations required for the major + * version: identification, prices, tax amounts and payments. Any other + * information are provided with extra data.

+ *

All of these informations are also stored in the underlying + * {@link org.pasteque.major.domain.FiscalTicket} in a raw format.

+ *

Tickets are created from a {@link MajorCashSession}.

+ *

Decorators can be used to handle extra data more conveniently.

+ */ +public final class MajorTicket +{ + /** See {@link getFiscalTicket(). */ + private final FiscalTicket fiscal; + /** See {@link getCashRegister(). */ + private MajorCashRegister cashRegister; + /** See {@link getSequence(). */ + private int sequence; + /** See {@link getDate(). */ + private Date date; + /** See {@link getLines(). */ + private ImmutableList lines; + /** See {@link getPayments(). */ + private ImmutableList payments; + /** See {@link getTaxAmounts(). */ + private ImmutableList taxAmounts; + /** See {@link getFinalTaxedPrice(). */ + private Price2 finalTaxedPrice; + /** See {@link getFinalPrice(). */ + private Price2 finalPrice; + // TODO: add extra data + + /** + * Create by linking an order and payments. It will create the associated + * {@link org.pasteque.major.domain.FiscalTicket}. + * This function has a limited scope to be called only from a + * {@link org.pasteque.major.domain.MajorCashSession}. + */ + /* package */ MajorTicket(MajorCashSession session, MajorOrder content, List payments) { + // TODO: implement + throw new UnsupportedOperationException("Not implemented yet."); + } + + /** + * Create from immutable content. + * @param fromTicket The ticket to convert. + * @throws IllegalArgumentException When the type of the ticket is not + * {@link org.pasteque.coreutil.constants.FiscalTicketType#TICKET}. + */ + public MajorTicket(FiscalTicket fromTicket) throws IllegalArgumentException { + if (!FiscalTicketType.TICKET.equals(fromTicket.getType())) { + throw new IllegalArgumentException("MajorTicket can only be created from a FiscalTicket of type TICKET"); + } + // TODO: implement + throw new UnsupportedOperationException("Not implemented yet."); + } + + /** + * Create from a DTO. All data are copied to break references. + * @param dto The DTO to convert. + * @throws IllegalArgumentException When the type is not + * {@link org.pasteque.coreutil.constants.FiscalTicketType#TICKET}. + * @see org.pasteque.coreutil.constants.FiscalTicketType + */ + public MajorTicket(FiscalTicketDTO dto) throws IllegalArgumentException { + if (!FiscalTicketType.TICKET.getCode().equals(dto.getType())) { + throw new IllegalArgumentException("MajorTicket can only be created from a FiscalTicket of type TICKET"); + } + // TODO: implement + throw new UnsupportedOperationException("Not implemented yet."); + } + + /* + * MajorTicket cannot be created from a TicketDTO. TicketDTO are + * higher-level data designed for reading, but not immutable-writing. + */ + + /** + * Get the underlying fiscal ticket. + * @return The underlying fiscal ticket. + */ + public FiscalTicket getFiscalTicket() { + return this.fiscal; + } + + /** + * Get the cash register this ticket was created from. + * @return The cash register. + */ + public MajorCashRegister getCashRegister() { + return this.cashRegister; + } + + /** + * Get the sequence of the cash register session in which this ticket + * was created. + * @return The sequence of the cash register. + */ + public int getSequence() { + return this.sequence; + } + + /** + * Get the date and time when this ticket was paid. + * @return The payment date, when the ticket was created.. + */ + public Date getDate() { + return date; + } + + /** + * Get the content of the ticket that was paid. + * @return The content of the ticket. + */ + public ImmutableList getLines() { + return lines; + } + + /** + * Get the payments used to pay this ticket. + * @return The list of payments used to pay the ticket. + */ + public ImmutableList getPayments() { + return payments; + } + + /** + * Get the sum of tax amounts grouped by tax. + * @return The list of tax amounts. + */ + public ImmutableList getTaxAmounts() { + return taxAmounts; + } + + /** + * Get the total price of the ticket, including taxes and discounts. + * @return The total price. + */ + public Price2 getFinalTaxedPrice() { + return finalTaxedPrice; + } + + /** + * Get the total price of the ticket without taxes, including discounts + * @return The total price without taxes. + */ + public Price2 getFinalPrice() { + return finalPrice; + } +} \ No newline at end of file diff --git a/src/main/java/org/pasteque/major/domain/MajorZTicket.java b/src/main/java/org/pasteque/major/domain/MajorZTicket.java new file mode 100644 index 0000000..898b014 --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/MajorZTicket.java @@ -0,0 +1,72 @@ +package org.pasteque.major.domain; + +import java.util.Date; +import java.util.List; +import org.pasteque.coreutil.constants.FiscalTicketType; +import org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO; + +/** + *

Model for a closed cash session, with consolidated figures.

+ *

ZTickets are created from a {@link MajorCashSession} on close.

+ */ +public final class MajorZTicket +{ + private final FiscalTicket fiscal; + + /** + * Create by closing a cash session. It will create the associated + * {@link org.pasteque.major.domain.FiscalTicket}. + * This function has a limited scope to be called only from a + * {@link org.pasteque.major.domain.MajorCashSession}. + */ + /* package */ MajorZTicket( + MajorCashSession session, + Date closeDate, + int closeType, + List endAmounts) { + // TODO: not implemented + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Create from immutable content. + * @param fromTicket The ticket to convert. + * @throws IllegalArgumentException When the type of the ticket is not + * {@link org.pasteque.coreutil.constants.FiscalTicketType#Z_TICKET}. + */ + public MajorZTicket(FiscalTicket fromTicket) throws IllegalArgumentException { + if (!FiscalTicketType.Z_TICKET.getCode().equals(fromTicket.getType())) { + throw new IllegalArgumentException("MajorZTicket can only be created from a FiscalTicket of type Z_TICKET"); + } + // TODO: not implemented + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Create from a DTO. All data are copied to break references. + * @param dto The DTO to convert. + * @throws IllegalArgumentException When the type is not + * {@link org.pasteque.coreutil.constants.FiscalTicketType#Z_TICKET}. + * @see org.pasteque.coreutil.constants.FiscalTicketType + */ + public MajorZTicket(FiscalTicketDTO dto) throws IllegalArgumentException { + if (!FiscalTicketType.Z_TICKET.getCode().equals(dto.getType())) { + throw new IllegalArgumentException("MajorZTicket can only be created from a FiscalTicket of type Z_TICKET"); + } + // TODO: implement + throw new UnsupportedOperationException("Not implemented yet."); + } + + /* + * MajorZTicket cannot be created from a ZTicketDTO. ZTicketDTO are + * higher-level data designed for reading, but not immutable-writing. + */ + + /** + * Get the underlying fiscal ticket. + * @return The fiscal ticket. + */ + public FiscalTicket getFiscal() { + return this.fiscal; + } +} diff --git a/src/main/java/org/pasteque/major/domain/Movement.java b/src/main/java/org/pasteque/major/domain/Movement.java new file mode 100644 index 0000000..cb3699a --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/Movement.java @@ -0,0 +1,134 @@ +package org.pasteque.major.domain; + +import java.util.Date; +import org.pasteque.coreutil.datatransfer.dto.MovementDTO; +import org.pasteque.coreutil.datatransfer.dto.WeakAssociationDTO; +import org.pasteque.coreutil.price.Price2; + +/** + *

Basic immutable representation of a movement amount.

+ *

Movement is centered around the amount. It only have weak references + * to payment mode and currency as they only need to be identified regarding + * the major version.

+ */ +public class Movement +{ + /** See {@link getPaymentModeReference()}. */ + private final String paymentModeReference; + /** See {@link getPaymentModeLabel()}. */ + private final String paymentModeLabel; + /** See {@link getCurrencyReference()}. */ + private final String currencyReference; + /** See {@link getCurrencyLabel()}. */ + private final String currencyLabel; + /** See {@link getCurrencyAmount()}. */ + private final Price2 currencyAmount; + /** See {@link getDate()}. */ + private Date date; + + /** + * Create a payment from all fields. + * @param paymentModeReference See {@link getPaymentModeReference()}. + * @param paymentModeLabel See {@link getPaymentModeLabel()}. + * @param currencyReference See {@link getCurrencyReference()}. + * @param currencyLabel See {@link getCurrencyLabel()}. + * @param currencyAmount See {@link getCurrencyAmount()}. + * @param date See {@link getDate()}. + */ + public Movement( + String paymentModeReference, + String paymentModeLabel, + String currencyReference, + String currencyLabel, + Price2 currencyAmount, + Date date) { + this.paymentModeReference = paymentModeReference; + this.paymentModeLabel = paymentModeLabel; + this.currencyReference = currencyReference; + this.currencyLabel = currencyLabel; + this.currencyAmount = currencyAmount; + this.date = date; + } + + /** + * Create from a DTO. Shared references are immutable. + * @param dto The DTO to convert. + */ + public Movement(MovementDTO dto) { + this.paymentModeReference = dto.getPaymentMode().getReference(); + this.paymentModeLabel = dto.getPaymentMode().getLabel(); + this.currencyReference = dto.getCurrency().getReference(); + this.currencyLabel = dto.getCurrency().getLabel(); + this.currencyAmount = new Price2(dto.getCurrencyAmount()); + this.date = new Date(dto.getDate().getTime()); + } + + /** + * Get the reference of the payment mode as it was at the time of creation. + * It is not guaranteed that the payment mode still even exists. + * @return The reference of the payment mode at the time of creation. + */ + public final String getPaymentModeReference() { + return paymentModeReference; + } + + /** + * Get the label of the payment mode as it was at the time of creation. + * @return The label of the payment mode at the time of creation. + */ + public final String getPaymentModeLabel() { + return paymentModeLabel; + } + + /** + * Get the reference of the currency as it was at the time of creation. + * It is not guaranteed that the currency still even exists. + * @return The reference of the currency at the time of creation. + */ + public final String getCurrencyReference() { + return currencyReference; + } + + /** + * Get the label of the currency as it was at the time of creation. + * @return The label of the currency at the time of creation. + */ + public final String getCurrencyLabel() { + return currencyLabel; + } + + /** + * Get the amount paid in the currency that was used + * at the time of creation. + * @return The amount paid in the currency used at the time of creation. + * When the main currency was used, the currency amount and the amound + * are the same. + */ + public final Price2 getCurrencyAmount() { + return currencyAmount; + } + + /** + * Get the date when the movement was done. + * @return The date when the movement was done. + */ + public final Date getDate() { + return this.date; + } + + /** + * Get a DTO from this object. + * @return The content of this object as a DTO. + */ + public final MovementDTO toDTO() { + return new MovementDTO( + new WeakAssociationDTO( + this.paymentModeReference, + this.paymentModeLabel), + new WeakAssociationDTO( + this.currencyReference, + this.currencyLabel), + this.currencyAmount.toDouble(), + this.date); + } +} diff --git a/src/main/java/org/pasteque/major/domain/Payment.java b/src/main/java/org/pasteque/major/domain/Payment.java new file mode 100644 index 0000000..69f66f9 --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/Payment.java @@ -0,0 +1,134 @@ +package org.pasteque.major.domain; + +import org.pasteque.coreutil.datatransfer.dto.WeakAssociationDTO; +import org.pasteque.coreutil.price.Price2; +import org.pasteque.coreutil.datatransfer.dto.PaymentDTO; + +/** + *

Basic immutable representation of a payment amount.

+ *

Payment is centered around the amounts. It only have weak references + * to payment mode and currency as they only need to be identified regarding + * the major version.

+ */ +public final class Payment +{ + /** See {@link getPaymentModeReference()}. */ + private final String paymentModeReference; + /** See {@link getPaymentModeLabel()}. */ + private final String paymentModeLabel; + /** See {@link getAmount()}. */ + private final Price2 amount; + /** See {@link getCurrencyReference()}. */ + private final String currencyReference; + /** See {@link getCurrencyLabel()}. */ + private final String currencyLabel; + /** See {@link getCurrencyAmount()}. */ + private final Price2 currencyAmount; + + /** + * Create a payment from all fields. + * @param paymentModeReference See {@link getPaymentModeReference()}. + * @param paymentModeLabel See {@link getPaymentModeLabel()}. + * @param amount See {@link getAmount()}. + * @param currencyReference See {@link getCurrencyReference()}. + * @param currencyLabel See {@link getCurrencyLabel()}. + * @param currencyAmount See {@link getCurrencyAmount()}. + */ + public Payment( + String paymentModeReference, + String paymentModeLabel, + String currencyReference, + String currencyLabel, + Price2 amount, + Price2 currencyAmount) { + this.paymentModeReference = paymentModeReference; + this.paymentModeLabel = paymentModeLabel; + this.currencyReference = currencyReference; + this.currencyLabel = currencyLabel; + this.amount = amount; + this.currencyAmount = currencyAmount; + } + + /** + * Create from a DTO. Shared references are immutable. + * @param dto The DTO to convert. + */ + public Payment(PaymentDTO dto) { + this.paymentModeReference = dto.getPaymentMode().getReference(); + this.paymentModeLabel = dto.getPaymentMode().getLabel(); + this.amount = new Price2(dto.getAmount()); + this.currencyReference = dto.getCurrency().getReference(); + this.currencyLabel = dto.getCurrency().getLabel(); + this.currencyAmount = new Price2(dto.getCurrencyAmount()); + } + + /** + * Get the reference of the payment mode as it was at the time of creation. + * It is not guaranteed that the payment mode still even exists. + * @return The reference of the payment mode at the time of creation. + */ + public final String getPaymentModeReference() { + return paymentModeReference; + } + + /** + * Get the label of the payment mode as it was at the time of creation. + * @return The label of the payment mode at the time of creation. + */ + public final String getPaymentModeLabel() { + return paymentModeLabel; + } + + /** + * Get the amount paid in the main currency at the time of creation. + * The conversion rate may have changed since. + * @return The amount paid in the main currency at the time of creation + */ + public final Price2 getAmount() { + return amount; + } + + /** + * Get the reference of the currency as it was at the time of creation. + * It is not guaranteed that the currency still even exists. + * @return The reference of the currency at the time of creation. + */ + public final String getCurrencyReference() { + return currencyReference; + } + + /** + * Get the label of the currency as it was at the time of creation. + * @return The label of the currency at the time of creation. + */ + public final String getCurrencyLabel() { + return currencyLabel; + } + + /** + * Get the amount paid in the currency that was used + * at the time of creation. + * @return The amount paid in the currency used at the time of creation. + * When the main currency was used, the currency amount and the amound + * are the same. + */ + public final Price2 getCurrencyAmount() { + return currencyAmount; + } + + /** + * Get a DTO from this object. + * @return The content of this object as a DTO. + */ + public final PaymentDTO toDTO() { + return new PaymentDTO( + new WeakAssociationDTO( + this.paymentModeReference, + this.paymentModeLabel), + this.amount.toDouble(), + new WeakAssociationDTO( + this.currencyReference, + this.currencyLabel), + this.currencyAmount.toDouble()); + } +} diff --git a/src/main/java/org/pasteque/major/domain/package-info.java b/src/main/java/org/pasteque/major/domain/package-info.java new file mode 100644 index 0000000..052eb36 --- /dev/null +++ b/src/main/java/org/pasteque/major/domain/package-info.java @@ -0,0 +1,9 @@ +/** + *

Manipulating data. This package contains model definition and methods + * to create or modify them.

+ *

Theses classes can have behavioural code and may be extended + * but not modified. All properties and methods are then final but the class + * may be extended to include more data.

+ */ +// TODO: add a way to propagate extra data to the DTO +package org.pasteque.major.domain; diff --git a/src/main/java/org/pasteque/major/package-info.java b/src/main/java/org/pasteque/major/package-info.java new file mode 100644 index 0000000..1c26c99 --- /dev/null +++ b/src/main/java/org/pasteque/major/package-info.java @@ -0,0 +1,11 @@ +/** + *

Package that contains critical features, which will constitute a major version + * regarding the French law.

+ *

All features in this package must be final and can rely only on the base + * java packages and on {@link org.pasteque.coreutil}. Except constants that + * can be extended but not modified. This ensures the behaviour cannot be altered + * in any way when using the major version.

+ *

When creating a release of this package, all dependencies outside the base java + * packages must be included in the release to ensure everything stays immutable.

+ */ +package org.pasteque.major; diff --git a/src/main/java/org/pasteque/package-info.java b/src/main/java/org/pasteque/package-info.java new file mode 100644 index 0000000..496dd34 --- /dev/null +++ b/src/main/java/org/pasteque/package-info.java @@ -0,0 +1,30 @@ +/** + *

Common libraries for Pastèque.

+ *

This package contains multiple libraries that can be combined to create + * a full software. Most of this segmentation is only for legal purposes, + * with a major version and a minor version. The major version contains features + * relative to resgistering new tickets by proceeding to payments. The minor + * version contains the GUI, flows and is used along the major version to + * create a full client. Any software that proceeds to payments must use the + * major version for this purposes, any other softwares can and must omit it.

+ *

The legal certification implies to identify and make sure the binaries are + * unmodifiable and cannot be bypassed. The certification covers the major + minor + * versions side by side. Any change to the major version must be approved by a + * new certification process before use. Changes to the minor version are + * allowed and are checked during periodic controls.

+ *
+ *
Major version
+ *
Behaviour to register new tickets and proceed to payments. The major + * version is build with {@link org.pasteque.major} and its dependency + * {@link org.pasteque.coreutil}. Both are provided by a single library to + * be able to identify any modification.
+ *
Minor version
+ *
Used along the major version, it defines a complete client. The minor + * version can rely upon {@link org.pasteque.common} to ease the implementation.
+ *
Other version
+ *
Any software not designed to register sales can include + * {@link org.pasteque.common} and its dependency {@link org.pasteque.coreutil} + * without any restriction, even with modifications.
+ *
+ */ +package org.pasteque; diff --git a/src/main/javadoc/javadoc-dark.css b/src/main/javadoc/javadoc-dark.css new file mode 100644 index 0000000..a2c4f55 --- /dev/null +++ b/src/main/javadoc/javadoc-dark.css @@ -0,0 +1,92 @@ +/* + * Dark mode taken from + * https://docs.oracle.com/en/java/javase/21/javadoc/programmers-guide-javadoc-css-themes.html + * modified to use media-query and with some adjustments. + */ +@media (prefers-color-scheme: dark) { +body { + background-color: #404040; + color: #e0e0e3; +} +.summary section[class$="-summary"], +.details section[class$="-details"], +.class-uses.detail, +.serialized-class-details, +.inherited-list, +section[class$="-details"] .detail { + background-color: #484848; + border-color: #383838; +} +.block { + color: #e6e7ef; +} +a:link, a:visited { + color: #a0c0f8; +} +main a[href*="://"]::after { + background-image: url('data:image/svg+xml; utf8, \ + \ + \ + '); +} +.top-nav { + background-color: #505076; + color: #ffffff; +} +.sub-nav, +.table-header, +body.class-declaration-page .summary h3, +body.class-declaration-page .details h3, +body.class-declaration-page .summary .inherited-list h2 { + background-color: #303030; +} +body.class-declaration-page .summary h3, +body.class-declaration-page .details h3, +body.class-declaration-page .summary .inherited-list h2 { + border-color: #282828; +} +.title { + color: #ffffff; +} +.table.plain { + border-color: #000000; +} +.summary-table { + border-color: #3b3b3b; +} +div.table-tabs > button.table-tab { + background-color: #506078; +} +div.table-tabs > button.active-table-tab { + background-color: #f8991c; +} +.even-row-color { + background-color: #484848; +} +.odd-row-color { + background-color: #383838; +} +dl.notes dt { + color: #e7e7eb; +} +:root { + --detail-background-color: #404040; + --selected-background-color: #f8981d; + --selected-text-color: #253441; + --selected-link-color: #1f389c; + --link-color-active: #ffb863; + --snippet-background-color: #383838; + --snippet-text-color: var(--block-text-color); + --snippet-highlight-color: #f7c590; + --border-color: #383838; + --search-input-background-color: #000000; + --search-input-text-color: #ffffff; + --search-input-placeholder-color: #909090; + --search-tag-highlight-color: #ffff00; + --copy-icon-brightness: 250%; + --copy-button-background-color-active: rgba(168, 168, 176, 0.3); + --invalid-tag-background-color: #ffe6e6; + --invalid-tag-text-color: #000000; +} +} diff --git a/src/main/resources/broken.png b/src/main/resources/broken.png new file mode 100644 index 0000000000000000000000000000000000000000..6a88bdb08e17ca8f37fd3c348136afa1d03b9928 GIT binary patch literal 882 zcmV-&1C9KNP)tx@vGjoshvxp@CvNge-PsOP1c#lkPno zo}_1~i9v=QxIDP``@VecIp-cxRsN4~0oK>o1^C8w-G|1QbEh#%BohC$+wCQwG&3{P zc02puPE1UEpU>wrxm*qr8Ol^usZ=UoY;JBA_xAS2fhAE@N~O|vp-`AgzkEr!wubor zaK`C0so7at`8*pN8}E!UXIubd%-LKnM`djd=gpg;OaTOFt*$a&D4?pJ0%K0^P-JM* zzKH)-K->?CR8fFb3N;4W?f!Gm18b31t4G{}>(s|#Agr~~5`mw7!1?1Ddg&t87_2eq z`SUo>e#iOs7i=mO>#gnc=`*&*#6rhG#>OCCcnG9~R2x=jnh9DqZcz_1YI0eRhoLH^xqi(-`P4o6VG@Fo0 zMwDSl^Uht&pIfok7<7Qz{f}{%7(f*nc=95#uDc;oaZVs6E}j~LT)2R@x)NPK3?uSn z3U6f@=kk|5owYVf;j35FZp@+w2iR&A+;$VFrI#&(1ws?Q# zDw0g1LC|{?&Oj>qi>DgVs-mjk2hrZ(%=UexW2znFK~2B0=S8Mn>SJp#x}+ikBIw9S z7ft|m0bJMp>+tY!YJ6dV%A-fvqoZCsscO`A$7ZyuMs3!e3!|faa{oTHS`FY}adENX z0_^Ya7t7`H)9LByk;!Ywil(eXCjFZw;-$Ka&xYXATM07*qo IM6N<$g2^(U`~Uy| literal 0 HcmV?d00001