From 3754bd211972611c2b459b6a6ffaaf1787699c91 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 1 Aug 2022 09:19:12 +0200 Subject: [PATCH 001/329] ci: checkout the repo with atedeg-bot permission --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c46b3c63..165a33f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,9 +64,17 @@ jobs: runs-on: ubuntu-22.04 if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/alpha' || github.ref == 'refs/heads/beta') steps: + - name: Setup atedeg-bot + id: atedeg-bot + uses: tibdex/github-app-token@v1.6.0 + with: + app_id: ${{ secrets.ATEDEG_BOT_APP_ID }} + private_key: ${{ secrets.ATEDEG_BOT_PRIVATE_KEY }} + - name: Checkout current branch uses: actions/checkout@v3 with: + token: ${{ steps.atedeg-bot.outputs.token }} fetch-depth: 0 - name: Release @@ -77,4 +85,4 @@ jobs: pgp-passphrase: ${{ secrets.PGP_PASSPHRASE }} sonatype-username: ${{ secrets.SONATYPE_USERNAME }} sonatype-password: ${{ secrets.SONATYPE_PASSWORD }} - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.atedeg-bot.outputs.token }} From 1aa0168e6842504012db0e297d50f09de9ace846 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 20 Jul 2022 14:56:06 +0200 Subject: [PATCH 002/329] build: add refined library --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 059a9f02..c6034d0a 100644 --- a/build.sbt +++ b/build.sbt @@ -87,8 +87,8 @@ lazy val root = project ) .aggregate(utils) -lazy val utils = project - .in(file("utils")) +lazy val `milk-planning` = project + .in(file("milk-planning")) .settings(commonSettings) lazy val `products-shared-kernel` = project From 5cb1080adb277765ac2cb6194e98e4c3e9a6aad7 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 20 Jul 2022 15:32:43 +0200 Subject: [PATCH 003/329] chore: add utils sub-project --- build.sbt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index c6034d0a..41ba6eea 100644 --- a/build.sbt +++ b/build.sbt @@ -85,11 +85,16 @@ lazy val root = project formats = Seq(JacocoReportFormats.XML), ), ) - .aggregate(utils) + .aggregate(utils, `milk-planning`) + +lazy val utils = project + .in(file("utils")) + .settings(commonSettings) lazy val `milk-planning` = project .in(file("milk-planning")) .settings(commonSettings) + .dependsOn(utils) lazy val `products-shared-kernel` = project .in(file("products-shared-kernel")) From 3ac0d51c81c32636905e29efad99dfd1be60b818 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 20 Jul 2022 15:33:11 +0200 Subject: [PATCH 004/329] docs: add milk-planning documentation --- docs/_docs/milk-planning.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/_docs/milk-planning.md diff --git a/docs/_docs/milk-planning.md b/docs/_docs/milk-planning.md new file mode 100644 index 00000000..d84b074e --- /dev/null +++ b/docs/_docs/milk-planning.md @@ -0,0 +1,23 @@ +--- +title: Milk Planning +--- + +# Milk Planning + +Every Saturday Raffaella has to estimate the quantity of milk necessary to produce all products +for the following week. +She makes this estimate by taking into account the following factors: + +- the milk processed in the same period of the previous year +- all incoming orders that have to be processed in the following week +- the current stock + +> 💡 A domain event will be sent to the restocking B.C. so that it can make a milk order + +## Ubiquitous Language + +{% include milk-planning-ul.md %} + +## Domain Events + +{% include milk-planning-de.md %} From fa83eeeec89b037a7b3c20ef0e71828bd2039140 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 20 Jul 2022 15:34:04 +0200 Subject: [PATCH 005/329] feat: define first batch of ubiquitous language --- .../atedeg/mdm/milkplanning/types/Types.scala | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala new file mode 100644 index 00000000..53f585ed --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -0,0 +1,65 @@ +package dev.atedeg.mdm.milkplanning.types + +import dev.atedeg.mdm.utils.{ NumberInClosedRange, PositiveDecimal, PositiveNumber } +import eu.timepit.refined.api.Refined +import eu.timepit.refined.numeric.Interval + +/** + * A type of cheese. + */ +enum CheeseType: + case Squacquerone + case Casatella + case Ricotta + case Stracchino + case Caciotta + +/** + * A [[CheeseType type of cheese]] with its respective [[Size size]]. + */ +enum Product(val cheeseType: CheeseType): + case Squacquerone(size: SquacqueroneSizeInGrams) extends Product(CheeseType.Squacquerone) + case Casatella(size: CasatellaSizeInGrams) extends Product(CheeseType.Casatella) + case Ricotta(size: RicottaSizeInGrams) extends Product(CheeseType.Ricotta) + case Stracchino(size: StracchinoSizeInGrams) extends Product(CheeseType.Stracchino) + case Caciotta(size: CaciottaSizeInGrams) extends Product(CheeseType.Caciotta) + +type SquacqueroneSizeInGrams = 100 | 250 | 350 | 800 | 1000 | 1500 +type CasatellaSizeInGrams = 300 | 350 | 800 | 1000 +type RicottaSizeInGrams = 350 | 1800 +type StracchinoSizeInGrams = 250 | 1000 +type CaciottaSizeInGrams = 1200 + +/** + * Milk processed in order to produce cheese. + */ +final case class ProcessedMilk(quantity: WeightInQuintals) + +/** + * A weight expressed in quintals. + * @note it must be a [[PositiveDecimal positive decimal number]]. + * @example `WeightInQuintals(1.1)` is a valid weight of 110 kg. + * @example `WeightInQuintals(-20.5)` is not a valid weight. + */ +final case class WeightInQuintals(n: PositiveDecimal) + +/** + * A [[Week week]] of a given [[Year year]]. + */ +final case class Period(week: Week, year: Year) + +/** + * The number of a week in a year + * @note it must be a [[NumberInClosedRange number]] between 1 and 52 inclusive. + * @example `Week(1)` is a valid week. + * @example `Week(54)` is not a valid week. + */ +final case class Week(n: NumberInClosedRange[1, 52]) + +/** + * A year. + * @note it must be a [[PositiveNumber positive number]]. + * @example `Year(2022)` is a valid year. + * @example `Year(-1000)` is not a valid year. + */ +final case class Year(n: PositiveNumber) From 07f3d52d105fb28b7af2fccc06ec1df348ff9d5b Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 20 Jul 2022 16:00:23 +0200 Subject: [PATCH 006/329] docs: update domain description --- docs/_docs/milk-planning.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_docs/milk-planning.md b/docs/_docs/milk-planning.md index d84b074e..2c433736 100644 --- a/docs/_docs/milk-planning.md +++ b/docs/_docs/milk-planning.md @@ -4,12 +4,12 @@ title: Milk Planning # Milk Planning -Every Saturday Raffaella has to estimate the quantity of milk necessary to produce all products +Every Saturday Raffaella has to estimate the quintals of milk necessary to produce all products for the following week. She makes this estimate by taking into account the following factors: -- the milk processed in the same period of the previous year -- all incoming orders that have to be processed in the following week +- the quintals of milk processed in the same period of the previous year +- the quintals of milk needed by the incoming orders that have to be processed in the following week (this is made reading from a recipe book the the quintals of milk needed to produce a quintal of a given product) - the current stock > 💡 A domain event will be sent to the restocking B.C. so that it can make a milk order From 6fc86c29304a7160ab34e7b874b205bd3e5797b4 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 20 Jul 2022 16:01:10 +0200 Subject: [PATCH 007/329] refactor: align code to ubiquitous language --- .../dev/atedeg/mdm/milkplanning/types/Types.scala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index 53f585ed..dab9b298 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -1,9 +1,10 @@ package dev.atedeg.mdm.milkplanning.types -import dev.atedeg.mdm.utils.{ NumberInClosedRange, PositiveDecimal, PositiveNumber } import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Interval +import dev.atedeg.mdm.utils.{ NumberInClosedRange, PositiveDecimal, PositiveNumber } + /** * A type of cheese. */ @@ -33,15 +34,15 @@ type CaciottaSizeInGrams = 1200 /** * Milk processed in order to produce cheese. */ -final case class ProcessedMilk(quantity: WeightInQuintals) +final case class ProcessedMilk(quantity: QuintalsOfMilk) /** - * A weight expressed in quintals. + * A quantity of milk expressed in quintals. * @note it must be a [[PositiveDecimal positive decimal number]]. - * @example `WeightInQuintals(1.1)` is a valid weight of 110 kg. - * @example `WeightInQuintals(-20.5)` is not a valid weight. + * @example `QuintalsOfMilk(1.1)` is a valid weight of 110 kg. + * @example `QuintalsOfMilk(-20.5)` is not a valid weight. */ -final case class WeightInQuintals(n: PositiveDecimal) +final case class QuintalsOfMilk(n: PositiveDecimal) /** * A [[Week week]] of a given [[Year year]]. From 3b21d486ff07170c7a7179d444c69041affafd06 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 20 Jul 2022 16:17:41 +0200 Subject: [PATCH 008/329] docs: update domain description --- docs/_docs/milk-planning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/milk-planning.md b/docs/_docs/milk-planning.md index 2c433736..3ca9ce54 100644 --- a/docs/_docs/milk-planning.md +++ b/docs/_docs/milk-planning.md @@ -9,7 +9,7 @@ for the following week. She makes this estimate by taking into account the following factors: - the quintals of milk processed in the same period of the previous year -- the quintals of milk needed by the incoming orders that have to be processed in the following week (this is made reading from a recipe book the the quintals of milk needed to produce a quintal of a given product) +- the quintals of milk needed by the products that have to be produced in the following week (this is made reading from a recipe book the the quintals of milk needed to produce a quintal of a given product) - the current stock > 💡 A domain event will be sent to the restocking B.C. so that it can make a milk order From 956703d9a4144d5e95dec5264ef4e179f0ee4626 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 20 Jul 2022 16:18:26 +0200 Subject: [PATCH 009/329] feat: complete domain modelling --- .../atedeg/mdm/milkplanning/types/Types.scala | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index dab9b298..2075085a 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -2,8 +2,7 @@ package dev.atedeg.mdm.milkplanning.types import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Interval - -import dev.atedeg.mdm.utils.{ NumberInClosedRange, PositiveDecimal, PositiveNumber } +import dev.atedeg.mdm.utils.{ NonNegativeNumber, NumberInClosedRange, PositiveDecimal, PositiveNumber } /** * A type of cheese. @@ -64,3 +63,22 @@ final case class Week(n: NumberInClosedRange[1, 52]) * @example `Year(-1000)` is not a valid year. */ final case class Year(n: PositiveNumber) + +/** + * It defines the how many [[QuintalsOfMilk quintals of milk]] are needed to produce a quintal of a given + * [[CheeseType cheese type]]. + */ +type RecipeBook = CheeseType => QuintalsOfMilk + +/** + * It defines, for each [[Product product]], the [[StockedQuantity quantity in stock]]. + */ +type Stock = Product => StockedQuantity + +/** + * A quantity of a stocked [[Product product]], it may also be zero. + * @note it must be a [[NonNegativeNumber non-negative number]]. + * @example `StockedQuantity(0)` is valid. + * @example `StockedQuantity(-1)` is invalid. + */ +final case class StockedQuantity(n: NonNegativeNumber) From 52fd8873d79927b5dada3c964e57f48295ee2dee Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 20 Jul 2022 16:40:51 +0200 Subject: [PATCH 010/329] chore: add ubidoc configuration file --- .ubidoc.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .ubidoc.yml diff --git a/.ubidoc.yml b/.ubidoc.yml new file mode 100644 index 00000000..361a9e89 --- /dev/null +++ b/.ubidoc.yml @@ -0,0 +1,30 @@ +tables: + - name: "milk-planning-ul" + termName: "Term" + definitionName: "Definition" + rows: + - enum: "CheeseType" + - enum: "Product" + - class: "ProcessedMilk" + - class: "QuintalsOfMilk" + - class: "Period" + - type: "RecipeBook" + - type: "Stock" + - class: "StockedQuantity" +ignored: + - class: "Week" + - class: "Year" + - case: "Squacquerone" + - case: "Casatella" + - case: "Ricotta" + - case: "Stracchino" + - case: "Caciotta" + - type: "SquacqueroneSizeInGrams" + - type: "CasatellaSizeInGrams" + - type: "RicottaSizeInGrams" + - type: "StracchinoSizeInGrams" + - type: "CaciottaSizeInGrams" + - type: "PositiveNumber" + - type: "PositiveDecimal" + - type: "NumberInClosedRange" + - type: "NonNegativeNumber" \ No newline at end of file From f9fe29a48c0791e0d2a59483396f5195398ec7d5 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 22 Jul 2022 09:10:00 +0200 Subject: [PATCH 011/329] docs: improve domain description --- docs/_docs/milk-planning.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/_docs/milk-planning.md b/docs/_docs/milk-planning.md index 3ca9ce54..70bf1ca2 100644 --- a/docs/_docs/milk-planning.md +++ b/docs/_docs/milk-planning.md @@ -9,8 +9,10 @@ for the following week. She makes this estimate by taking into account the following factors: - the quintals of milk processed in the same period of the previous year -- the quintals of milk needed by the products that have to be produced in the following week (this is made reading from a recipe book the the quintals of milk needed to produce a quintal of a given product) +- the quintals of milk needed by the products that have to be produced in the following week + (this is made reading from a recipe book the the quintals of milk needed to produce a quintal of a given product) - the current stock +- the quintals of milk already in stock > 💡 A domain event will be sent to the restocking B.C. so that it can make a milk order From 242a875a1684d22ace87ba51d04b4a5b1589c790 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 22 Jul 2022 15:24:14 +0200 Subject: [PATCH 012/329] feat: definition of domain events for milk-planning --- .../scala/dev/atedeg/mdm/milkplanning/types/Events.scala | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala new file mode 100644 index 00000000..ad3ab2e7 --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.milkplanning.types + +enum IncomingEvent: + case ProductRemovedFromStock(product: Product) + case ProductAddedToStock(product: Product) + case RestockedMilk(quintalsOfMilk: QuintalsOfMilk) + +enum OutgoingEvent: + case OrderMilk(n: QuintalsOfMilk) From fcdcbbf16818d8103e595fd9f44476f77e2fa0be Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 22 Jul 2022 15:28:02 +0200 Subject: [PATCH 013/329] feat: first definition of domain actions --- .../dev/atedeg/mdm/milkplanning/types/Actions.scala | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala new file mode 100644 index 00000000..2fc603b7 --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.milkplanning.types + +// TODO: raises an event +def estimateQuintalsOfMilk( + milkOfThePreviousYear: QuintalsOfMilk, + milkNeededByProducts: QuintalsOfMilk, + currentStock: Stock, + stockedMilk: QuintalsOfMilk, +): Unit = ??? From bd925d551c80f1f0ff36a3e9bd1d2c98fe863c39 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 22 Jul 2022 15:33:31 +0200 Subject: [PATCH 014/329] style: sort import --- .../src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index 2075085a..3c3b6532 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -2,6 +2,7 @@ package dev.atedeg.mdm.milkplanning.types import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Interval + import dev.atedeg.mdm.utils.{ NonNegativeNumber, NumberInClosedRange, PositiveDecimal, PositiveNumber } /** From 034e985e9fdf1a64342db5d28787a088d880a224 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 26 Jul 2022 16:57:58 +0200 Subject: [PATCH 015/329] chore: define some utility functions --- .../atedeg/mdm/milkplanning/utils/TypesOps.scala | 13 +++++++++++++ .../dev/atedeg/mdm/milkplanning/dsl/Dsl.scala | 14 ++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala create mode 100644 milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/dsl/Dsl.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala new file mode 100644 index 00000000..f4490a2d --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala @@ -0,0 +1,13 @@ +package dev.atedeg.mdm.milkplanning.utils + +import scala.annotation.targetName + +import dev.atedeg.mdm.milkplanning.types.QuintalsOfMilk + +extension (qom1: QuintalsOfMilk) + + @targetName("plus") + def +(qom2: QuintalsOfMilk): QuintalsOfMilk = ??? + + @targetName("minus") + def -(qom2: QuintalsOfMilk): Option[QuintalsOfMilk] = ??? diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/dsl/Dsl.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/dsl/Dsl.scala new file mode 100644 index 00000000..4b5d3dff --- /dev/null +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/dsl/Dsl.scala @@ -0,0 +1,14 @@ +package dev.atedeg.mdm.milkplanning.dsl + +import eu.timepit.refined.numeric.NonNegative +import eu.timepit.refined.refineV + +import dev.atedeg.mdm.milkplanning.types.{ QuintalsOfMilk, StockedQuantity } + +extension [A](x: Option[A]) + + @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) + def coerce: A = x.get + +extension (n: Double) def quintalsOfMilk: QuintalsOfMilk = QuintalsOfMilk(refineV[NonNegative](n).toOption.coerce) +extension (n: Int) def stockedQuantity: StockedQuantity = StockedQuantity(refineV[NonNegative](n).toOption.coerce) From 697e87df07da321d200e29d499396b362743f9cf Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 26 Jul 2022 16:59:02 +0200 Subject: [PATCH 016/329] feat: first basic implementation for estimate quintals of milk --- .../mdm/milkplanning/types/Actions.scala | 14 ++++++-- .../atedeg/mdm/milkplanning/types/Types.scala | 10 ++++-- .../mdm/milkplanning/types/ActionsTest.scala | 36 +++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala index 2fc603b7..34366698 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala @@ -1,9 +1,17 @@ package dev.atedeg.mdm.milkplanning.types -// TODO: raises an event -def estimateQuintalsOfMilk( +import cats.Monad + +import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.* +import dev.atedeg.mdm.milkplanning.utils.* +import dev.atedeg.mdm.utils.{ emit, thenReturn, Emits } + +def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( milkOfThePreviousYear: QuintalsOfMilk, milkNeededByProducts: QuintalsOfMilk, currentStock: Stock, stockedMilk: QuintalsOfMilk, -): Unit = ??? +): M[QuintalsOfMilk] = { + val res = milkOfThePreviousYear + milkNeededByProducts + emit[M, OrderMilk](OrderMilk(res)) thenReturn res +} diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index 3c3b6532..92b05ce1 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -3,7 +3,13 @@ package dev.atedeg.mdm.milkplanning.types import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Interval -import dev.atedeg.mdm.utils.{ NonNegativeNumber, NumberInClosedRange, PositiveDecimal, PositiveNumber } +import dev.atedeg.mdm.utils.{ + NonNegativeDecimal, + NonNegativeNumber, + NumberInClosedRange, + PositiveDecimal, + PositiveNumber, +} /** * A type of cheese. @@ -42,7 +48,7 @@ final case class ProcessedMilk(quantity: QuintalsOfMilk) * @example `QuintalsOfMilk(1.1)` is a valid weight of 110 kg. * @example `QuintalsOfMilk(-20.5)` is not a valid weight. */ -final case class QuintalsOfMilk(n: PositiveDecimal) +final case class QuintalsOfMilk(n: NonNegativeDecimal) /** * A [[Week week]] of a given [[Year year]]. diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala new file mode 100644 index 00000000..9a022d80 --- /dev/null +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -0,0 +1,36 @@ +package dev.atedeg.mdm.milkplanning.types + +import cats.data.Writer +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +import dev.atedeg.mdm.milkplanning.dsl.* +import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.OrderMilk + +class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers { + + Feature("Estimate the quintals of milk needed for the following week") { + Scenario("Simple estimation") { + Given("A request for ordering milk") + val qomPreviousYear = 4.0.quintalsOfMilk + val qomNeededByProd = 3.5.quintalsOfMilk + val currentStock: Stock = _ => 0.stockedQuantity + val stockedMilk = 0.0.quintalsOfMilk + When("the estimation is completed") + val estimationMonad: Writer[List[OrderMilk], QuintalsOfMilk] = estimateQuintalsOfMilk( + qomPreviousYear, + qomNeededByProd, + currentStock, + stockedMilk, + ) + Then("an event should be raised in order to place the order") + val (events, qom) = estimationMonad.run + events should have length 1 + events.headOption match { + case Some(e) => e.n should equal(qom.n) + case _ => fail("The events list must have at least one element") + } + } + } +} From 9838b73bbcf5727e442412df97bdc4be2c9ff569 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 26 Jul 2022 22:07:27 +0200 Subject: [PATCH 017/329] style: reformat files --- .../mdm/milkplanning/types/Actions.scala | 4 ++-- .../mdm/milkplanning/utils/TypesOps.scala | 21 +++++++++++++++++-- .../mdm/milkplanning/types/ActionsTest.scala | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala index 34366698..d8ae59e9 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala @@ -12,6 +12,6 @@ def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( currentStock: Stock, stockedMilk: QuintalsOfMilk, ): M[QuintalsOfMilk] = { - val res = milkOfThePreviousYear + milkNeededByProducts - emit[M, OrderMilk](OrderMilk(res)) thenReturn res + val estimation = milkOfThePreviousYear + milkNeededByProducts - stockedMilk + emit[M, OrderMilk](OrderMilk(estimation)) thenReturn estimation } diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala index f4490a2d..16877cf9 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala @@ -2,12 +2,29 @@ package dev.atedeg.mdm.milkplanning.utils import scala.annotation.targetName +import eu.timepit.refined.api.Refined +import eu.timepit.refined.numeric.{ NonNegative, Positive } +import eu.timepit.refined.refineV + import dev.atedeg.mdm.milkplanning.types.QuintalsOfMilk +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.{ NonNegativeDecimal, PositiveDecimal } +import dev.atedeg.mdm.utils.given extension (qom1: QuintalsOfMilk) @targetName("plus") - def +(qom2: QuintalsOfMilk): QuintalsOfMilk = ??? + def +(qom2: QuintalsOfMilk): QuintalsOfMilk = { + val result: Option[NonNegativeDecimal] = qom1.n.value + qom2.n.value + result match + case Some(value) => QuintalsOfMilk(value) + case _ => ??? // TODO: how can we handle this case? + } @targetName("minus") - def -(qom2: QuintalsOfMilk): Option[QuintalsOfMilk] = ??? + def -(qom2: QuintalsOfMilk): QuintalsOfMilk = { + val result: Option[NonNegativeDecimal] = qom1.n.value - qom2.n.value + result match + case Some(value) => QuintalsOfMilk(value) + case _ => QuintalsOfMilk(0.0.nonNegativeDecimal) + } diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index 9a022d80..dc34bb66 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -28,7 +28,7 @@ class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers { val (events, qom) = estimationMonad.run events should have length 1 events.headOption match { - case Some(e) => e.n should equal(qom.n) + case Some(e) => e.n should equal(qom) case _ => fail("The events list must have at least one element") } } From fdf4954d69f7cb678704d7de57aade2f74fb24de Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 27 Jul 2022 15:24:31 +0200 Subject: [PATCH 018/329] chore: add utilities --- .../mdm/milkplanning/utils/TypesOps.scala | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala index 16877cf9..b5737607 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala @@ -2,29 +2,28 @@ package dev.atedeg.mdm.milkplanning.utils import scala.annotation.targetName -import eu.timepit.refined.api.Refined -import eu.timepit.refined.numeric.{ NonNegative, Positive } +import cats.kernel.Order +import eu.timepit.refined.predicates.all.NonNegative import eu.timepit.refined.refineV import dev.atedeg.mdm.milkplanning.types.QuintalsOfMilk import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.{ NonNegativeDecimal, PositiveDecimal } import dev.atedeg.mdm.utils.given -extension (qom1: QuintalsOfMilk) - - @targetName("plus") - def +(qom2: QuintalsOfMilk): QuintalsOfMilk = { - val result: Option[NonNegativeDecimal] = qom1.n.value + qom2.n.value - result match - case Some(value) => QuintalsOfMilk(value) - case _ => ??? // TODO: how can we handle this case? - } - - @targetName("minus") - def -(qom2: QuintalsOfMilk): QuintalsOfMilk = { - val result: Option[NonNegativeDecimal] = qom1.n.value - qom2.n.value - result match - case Some(value) => QuintalsOfMilk(value) - case _ => QuintalsOfMilk(0.0.nonNegativeDecimal) - } +object QuintalsOfMilkOps: + + given Order[QuintalsOfMilk] with + override def compare(x: QuintalsOfMilk, y: QuintalsOfMilk): Int = Order[NonNegativeDecimal].compare(x.n, y.n) + + extension (qom1: QuintalsOfMilk) + + @targetName("plus") + def +(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.n plus qom2.n) + + @targetName("minus") + def -(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.n minus qom2.n) + + extension (d: Double) + + @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) // FIXME: try with a macro + def quintalsOfMilk: QuintalsOfMilk = QuintalsOfMilk(refineV[NonNegative](d).toOption.get) From 9c1ee7e7ca14be804d793eeef4c02362c0dac53b Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 27 Jul 2022 15:26:05 +0200 Subject: [PATCH 019/329] chore: second draft implementation --- .../mdm/milkplanning/types/Actions.scala | 30 +++++++++---- .../atedeg/mdm/milkplanning/types/Types.scala | 5 +++ .../mdm/milkplanning/types/ActionsTest.scala | 42 +++++++++---------- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala index d8ae59e9..c53adf50 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala @@ -1,17 +1,29 @@ package dev.atedeg.mdm.milkplanning.types -import cats.Monad +import cats.{ Monad, Order } +import cats.syntax.all.* +import eu.timepit.refined.auto.autoUnwrap import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.* -import dev.atedeg.mdm.milkplanning.utils.* -import dev.atedeg.mdm.utils.{ emit, thenReturn, Emits } +import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.* +import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.given +import dev.atedeg.mdm.utils.{ emit, max, thenReturn, when, Emits, given } def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( - milkOfThePreviousYear: QuintalsOfMilk, - milkNeededByProducts: QuintalsOfMilk, + milkOfPreviousYear: QuintalsOfMilk, + requestedProductsForWeek: List[RequestedProduct], currentStock: Stock, + recipeBook: RecipeBook, stockedMilk: QuintalsOfMilk, -): M[QuintalsOfMilk] = { - val estimation = milkOfThePreviousYear + milkNeededByProducts - stockedMilk - emit[M, OrderMilk](OrderMilk(estimation)) thenReturn estimation -} +): M[QuintalsOfMilk] = + val milkNeeded = milkNeededForProducts(requestedProductsForWeek, currentStock, recipeBook) + val estimatedMilk = magicAiEstimator(milkOfPreviousYear, milkNeeded, stockedMilk) + when(estimatedMilk > 0.quintalsOfMilk)(emit(OrderMilk(estimatedMilk): OrderMilk)).thenReturn(estimatedMilk) + +private def milkNeededForProducts(value: List[RequestedProduct], stock: Stock, book: RecipeBook): QuintalsOfMilk = ??? + +private def magicAiEstimator( + milkOfPreviousYear: QuintalsOfMilk, + milkNeeded: QuintalsOfMilk, + stockedMilk: QuintalsOfMilk, +): QuintalsOfMilk = max(milkOfPreviousYear, milkNeeded) - stockedMilk diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index 92b05ce1..67858c67 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -1,5 +1,7 @@ package dev.atedeg.mdm.milkplanning.types +import java.time.LocalDateTime + import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Interval @@ -89,3 +91,6 @@ type Stock = Product => StockedQuantity * @example `StockedQuantity(-1)` is invalid. */ final case class StockedQuantity(n: NonNegativeNumber) + +final case class Quantity(q: PositiveNumber) +final case class RequestedProduct(product: Product, quantity: Quantity, requiredBy: LocalDateTime) diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index dc34bb66..f3050c6f 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -11,26 +11,26 @@ import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.OrderMilk class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers { Feature("Estimate the quintals of milk needed for the following week") { - Scenario("Simple estimation") { - Given("A request for ordering milk") - val qomPreviousYear = 4.0.quintalsOfMilk - val qomNeededByProd = 3.5.quintalsOfMilk - val currentStock: Stock = _ => 0.stockedQuantity - val stockedMilk = 0.0.quintalsOfMilk - When("the estimation is completed") - val estimationMonad: Writer[List[OrderMilk], QuintalsOfMilk] = estimateQuintalsOfMilk( - qomPreviousYear, - qomNeededByProd, - currentStock, - stockedMilk, - ) - Then("an event should be raised in order to place the order") - val (events, qom) = estimationMonad.run - events should have length 1 - events.headOption match { - case Some(e) => e.n should equal(qom) - case _ => fail("The events list must have at least one element") - } - } +// Scenario("Simple estimation") { +// Given("A request for ordering milk") +// val qomPreviousYear = 4.0.quintalsOfMilk +// val qomNeededByProd = 3.5.quintalsOfMilk +// val currentStock: Stock = _ => 0.stockedQuantity +// val stockedMilk = 0.0.quintalsOfMilk +// When("the estimation is completed") +// val estimationMonad: Writer[List[OrderMilk], QuintalsOfMilk] = estimateQuintalsOfMilk( +// qomPreviousYear, +// qomNeededByProd, +// currentStock, +// stockedMilk, +// ) +// Then("an event should be raised in order to place the order") +// val (events, qom) = estimationMonad.run +// events should have length 1 +// events.headOption match { +// case Some(e) => e.n should equal(qom) +// case _ => fail("The events list must have at least one element") +// } +// } } } From 5f5a48145323d7ee1ea8f968593d486be59de2fd Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 27 Jul 2022 17:03:57 +0200 Subject: [PATCH 020/329] chore: remove unused conversion --- .../atedeg/mdm/milkplanning/utils/TypesOps.scala | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala index b5737607..33b2eae3 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala @@ -13,17 +13,14 @@ import dev.atedeg.mdm.utils.given object QuintalsOfMilkOps: given Order[QuintalsOfMilk] with - override def compare(x: QuintalsOfMilk, y: QuintalsOfMilk): Int = Order[NonNegativeDecimal].compare(x.n, y.n) + + override def compare(x: QuintalsOfMilk, y: QuintalsOfMilk): Int = + Order[NonNegativeDecimal].compare(x.quintals, y.quintals) extension (qom1: QuintalsOfMilk) @targetName("plus") - def +(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.n plus qom2.n) + def +(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.quintals plus qom2.quintals) @targetName("minus") - def -(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.n minus qom2.n) - - extension (d: Double) - - @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) // FIXME: try with a macro - def quintalsOfMilk: QuintalsOfMilk = QuintalsOfMilk(refineV[NonNegative](d).toOption.get) + def -(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.quintals minus qom2.quintals) From 8ca1ee6a81485e5bde5096acad5b6ac8c4c47b76 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 27 Jul 2022 17:04:57 +0200 Subject: [PATCH 021/329] refactor: rename some fields and add missing scaladoc --- .../atedeg/mdm/milkplanning/types/Types.scala | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index 67858c67..241d944c 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -50,7 +50,7 @@ final case class ProcessedMilk(quantity: QuintalsOfMilk) * @example `QuintalsOfMilk(1.1)` is a valid weight of 110 kg. * @example `QuintalsOfMilk(-20.5)` is not a valid weight. */ -final case class QuintalsOfMilk(n: NonNegativeDecimal) +final case class QuintalsOfMilk(quintals: NonNegativeDecimal) /** * A [[Week week]] of a given [[Year year]]. @@ -90,7 +90,17 @@ type Stock = Product => StockedQuantity * @example `StockedQuantity(0)` is valid. * @example `StockedQuantity(-1)` is invalid. */ -final case class StockedQuantity(n: NonNegativeNumber) +final case class StockedQuantity(quantity: NonNegativeNumber) -final case class Quantity(q: PositiveNumber) +/** + * A quantity of something. + * @example `Quantity(-2)` is not a valid quantity. + * @example `Quantity(20)` is a valida quantity. + */ +final case class Quantity(n: PositiveNumber) + +/** + * A [[Product product]] requested in a given [[Quantity quantity]] that has to be produced by the given + * [[LocalDateTime date]]. + */ final case class RequestedProduct(product: Product, quantity: Quantity, requiredBy: LocalDateTime) From 492a1c00a522976ab0bab60efe206a9f5ce37934 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 27 Jul 2022 17:05:37 +0200 Subject: [PATCH 022/329] refactor: improve method readability --- .../main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala index c53adf50..606cb962 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala @@ -18,7 +18,7 @@ def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( ): M[QuintalsOfMilk] = val milkNeeded = milkNeededForProducts(requestedProductsForWeek, currentStock, recipeBook) val estimatedMilk = magicAiEstimator(milkOfPreviousYear, milkNeeded, stockedMilk) - when(estimatedMilk > 0.quintalsOfMilk)(emit(OrderMilk(estimatedMilk): OrderMilk)).thenReturn(estimatedMilk) + when(estimatedMilk.quintals > 0)(emit(OrderMilk(estimatedMilk): OrderMilk)).thenReturn(estimatedMilk) private def milkNeededForProducts(value: List[RequestedProduct], stock: Stock, book: RecipeBook): QuintalsOfMilk = ??? From 96c1ed3726caf279b272308017be655c3804a51b Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 27 Jul 2022 17:42:08 +0200 Subject: [PATCH 023/329] chore: add minus method to Quantity class --- .../main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index 241d944c..b7ed6172 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -5,6 +5,7 @@ import java.time.LocalDateTime import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Interval +import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.{ NonNegativeDecimal, NonNegativeNumber, @@ -97,7 +98,9 @@ final case class StockedQuantity(quantity: NonNegativeNumber) * @example `Quantity(-2)` is not a valid quantity. * @example `Quantity(20)` is a valida quantity. */ -final case class Quantity(n: PositiveNumber) +final case class Quantity(n: PositiveNumber) { + def -(p: StockedQuantity): NonNegativeNumber = this.n.toNonNegativeNumber - p.quantity +} /** * A [[Product product]] requested in a given [[Quantity quantity]] that has to be produced by the given From d914fe7bafa9211be938c4bd61d064bf8f9774e0 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 27 Jul 2022 17:43:21 +0200 Subject: [PATCH 024/329] feat: implement logic for calculate milk needed for products --- .../atedeg/mdm/milkplanning/types/Actions.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala index 606cb962..a64e2321 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala @@ -1,6 +1,7 @@ package dev.atedeg.mdm.milkplanning.types import cats.{ Monad, Order } +import cats.data.NonEmptyList import cats.syntax.all.* import eu.timepit.refined.auto.autoUnwrap @@ -11,7 +12,7 @@ import dev.atedeg.mdm.utils.{ emit, max, thenReturn, when, Emits, given } def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( milkOfPreviousYear: QuintalsOfMilk, - requestedProductsForWeek: List[RequestedProduct], + requestedProductsForWeek: NonEmptyList[RequestedProduct], currentStock: Stock, recipeBook: RecipeBook, stockedMilk: QuintalsOfMilk, @@ -20,7 +21,16 @@ def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( val estimatedMilk = magicAiEstimator(milkOfPreviousYear, milkNeeded, stockedMilk) when(estimatedMilk.quintals > 0)(emit(OrderMilk(estimatedMilk): OrderMilk)).thenReturn(estimatedMilk) -private def milkNeededForProducts(value: List[RequestedProduct], stock: Stock, book: RecipeBook): QuintalsOfMilk = ??? +private def milkNeededForProducts( + requestedProducts: NonEmptyList[RequestedProduct], + stock: Stock, + book: RecipeBook, +): QuintalsOfMilk = + requestedProducts + .map(p => (p.product, p.quantity)) + .map { case (prod, quantity) => (prod, quantity - stock(prod)) } + .map { case (prod, quantity) => book(prod.cheeseType) ** quantity } + .reduce(_ + _) private def magicAiEstimator( milkOfPreviousYear: QuintalsOfMilk, From 8e890e475abd75353a836f8a948144e676ceff0d Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 27 Jul 2022 17:43:59 +0200 Subject: [PATCH 025/329] chore: fix target name on extension methods --- .../dev/atedeg/mdm/milkplanning/utils/TypesOps.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala index 33b2eae3..79a7bc61 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala @@ -6,7 +6,7 @@ import cats.kernel.Order import eu.timepit.refined.predicates.all.NonNegative import eu.timepit.refined.refineV -import dev.atedeg.mdm.milkplanning.types.QuintalsOfMilk +import dev.atedeg.mdm.milkplanning.types.{ Quantity, QuintalsOfMilk, StockedQuantity } import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given @@ -19,8 +19,11 @@ object QuintalsOfMilkOps: extension (qom1: QuintalsOfMilk) - @targetName("plus") + @targetName("plusQuintals") def +(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.quintals plus qom2.quintals) - @targetName("minus") + @targetName("minusQuintals") def -(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.quintals minus qom2.quintals) + + @targetName("timesQuintals") + def **(nn: NonNegativeNumber): QuintalsOfMilk = QuintalsOfMilk(qom1.quintals * nn.toNonNegativeDecimal) From 0832a82a3d3be27801b1cab35653061651d5c409 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Thu, 28 Jul 2022 14:25:56 +0200 Subject: [PATCH 026/329] test: partial implementation of the test --- .../mdm/milkplanning/types/ActionsTest.scala | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index f3050c6f..9af3ee31 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -1,36 +1,55 @@ package dev.atedeg.mdm.milkplanning.types -import cats.data.Writer +import java.time.LocalDateTime + +import cats.data.{ NonEmptyList, Writer } import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers import dev.atedeg.mdm.milkplanning.dsl.* import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.OrderMilk +import dev.atedeg.mdm.milkplanning.types.Product.* +import dev.atedeg.mdm.utils.* + +trait Fixture { + + val recipeBook: RecipeBook = Map( + CheeseType.Squacquerone -> QuintalsOfMilk(1.3), + CheeseType.Casatella -> QuintalsOfMilk(1.3), + CheeseType.Ricotta -> QuintalsOfMilk(1.1), + CheeseType.Stracchino -> QuintalsOfMilk(1.4), + CheeseType.Caciotta -> QuintalsOfMilk(1.4), + ) +} -class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers { +class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Fixture { Feature("Estimate the quintals of milk needed for the following week") { -// Scenario("Simple estimation") { -// Given("A request for ordering milk") -// val qomPreviousYear = 4.0.quintalsOfMilk -// val qomNeededByProd = 3.5.quintalsOfMilk -// val currentStock: Stock = _ => 0.stockedQuantity -// val stockedMilk = 0.0.quintalsOfMilk -// When("the estimation is completed") -// val estimationMonad: Writer[List[OrderMilk], QuintalsOfMilk] = estimateQuintalsOfMilk( -// qomPreviousYear, -// qomNeededByProd, -// currentStock, -// stockedMilk, -// ) -// Then("an event should be raised in order to place the order") -// val (events, qom) = estimationMonad.run -// events should have length 1 -// events.headOption match { -// case Some(e) => e.n should equal(qom) -// case _ => fail("The events list must have at least one element") -// } -// } + Scenario("Raffaella wants to estimate the quintals of milk") { + Given("the quintals of milk of the previous year for the same period") + And("a list of products to be produced") + And("an empty stock") + And("no milk in stock") + val qomPreviousYear = QuintalsOfMilk(4.0) + val requestedProducts = NonEmptyList.of( + RequestedProduct(Squacquerone(100), Quantity(50), LocalDateTime.now()), + ) + val currentStock: Stock = _ => StockedQuantity(0) + val stockedMilk = QuintalsOfMilk(0.0) + When("the estimation is ready to be computed") + val estimatorMonad: Writer[List[OrderMilk], QuintalsOfMilk] = estimateQuintalsOfMilk( + qomPreviousYear, + requestedProducts, + currentStock, + recipeBook, + stockedMilk, + ) + val (events, estimation) = estimatorMonad.run + println(estimation) + + Then("the result will be in the interval [..., ...]") + + } } } From 800a9f487b3b60a4bada5d4d5332a9fab1eee7ec Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Thu, 28 Jul 2022 19:20:35 +0200 Subject: [PATCH 027/329] chore: adapt code for products-shared-kernel --- build.sbt | 2 +- .../mdm/milkplanning/types/Actions.scala | 13 +++--- .../mdm/milkplanning/types/Events.scala | 2 + .../atedeg/mdm/milkplanning/types/Types.scala | 41 ++----------------- .../mdm/milkplanning/utils/TypesOps.scala | 18 ++------ .../dev/atedeg/mdm/milkplanning/dsl/Dsl.scala | 14 ------- .../mdm/milkplanning/types/ActionsTest.scala | 21 +++++----- 7 files changed, 30 insertions(+), 81 deletions(-) delete mode 100644 milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/dsl/Dsl.scala diff --git a/build.sbt b/build.sbt index 41ba6eea..94443517 100644 --- a/build.sbt +++ b/build.sbt @@ -94,7 +94,7 @@ lazy val utils = project lazy val `milk-planning` = project .in(file("milk-planning")) .settings(commonSettings) - .dependsOn(utils) + .dependsOn(utils, `products-shared-kernel`) lazy val `products-shared-kernel` = project .in(file("products-shared-kernel")) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala index a64e2321..89c8f3f1 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala @@ -1,14 +1,16 @@ package dev.atedeg.mdm.milkplanning.types -import cats.{ Monad, Order } +import cats.Monad import cats.data.NonEmptyList -import cats.syntax.all.* import eu.timepit.refined.auto.autoUnwrap import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.* import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.* import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.given -import dev.atedeg.mdm.utils.{ emit, max, thenReturn, when, Emits, given } +import dev.atedeg.mdm.milkplanning.utils.given +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given +import dev.atedeg.mdm.utils.monads.{ emit, thenReturn, when, Emits } def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( milkOfPreviousYear: QuintalsOfMilk, @@ -28,8 +30,9 @@ private def milkNeededForProducts( ): QuintalsOfMilk = requestedProducts .map(p => (p.product, p.quantity)) - .map { case (prod, quantity) => (prod, quantity - stock(prod)) } - .map { case (prod, quantity) => book(prod.cheeseType) ** quantity } + .map { case (prod, quantity) => (prod, quantity.n - stock(prod).quantity) } + .map { case (prod, quantity) => book(prod.cheeseType).quintals * quantity } + .map(_.toQuintalsOfMilk) .reduce(_ + _) private def magicAiEstimator( diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala index ad3ab2e7..7f5efad7 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala @@ -1,5 +1,7 @@ package dev.atedeg.mdm.milkplanning.types +import dev.atedeg.mdm.products.Product + enum IncomingEvent: case ProductRemovedFromStock(product: Product) case ProductAddedToStock(product: Product) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index b7ed6172..9f84f870 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -5,40 +5,9 @@ import java.time.LocalDateTime import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Interval +import dev.atedeg.mdm.products.{ CheeseType, Product } import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.{ - NonNegativeDecimal, - NonNegativeNumber, - NumberInClosedRange, - PositiveDecimal, - PositiveNumber, -} - -/** - * A type of cheese. - */ -enum CheeseType: - case Squacquerone - case Casatella - case Ricotta - case Stracchino - case Caciotta - -/** - * A [[CheeseType type of cheese]] with its respective [[Size size]]. - */ -enum Product(val cheeseType: CheeseType): - case Squacquerone(size: SquacqueroneSizeInGrams) extends Product(CheeseType.Squacquerone) - case Casatella(size: CasatellaSizeInGrams) extends Product(CheeseType.Casatella) - case Ricotta(size: RicottaSizeInGrams) extends Product(CheeseType.Ricotta) - case Stracchino(size: StracchinoSizeInGrams) extends Product(CheeseType.Stracchino) - case Caciotta(size: CaciottaSizeInGrams) extends Product(CheeseType.Caciotta) - -type SquacqueroneSizeInGrams = 100 | 250 | 350 | 800 | 1000 | 1500 -type CasatellaSizeInGrams = 300 | 350 | 800 | 1000 -type RicottaSizeInGrams = 350 | 1800 -type StracchinoSizeInGrams = 250 | 1000 -type CaciottaSizeInGrams = 1200 +import dev.atedeg.mdm.utils.given /** * Milk processed in order to produce cheese. @@ -51,7 +20,7 @@ final case class ProcessedMilk(quantity: QuintalsOfMilk) * @example `QuintalsOfMilk(1.1)` is a valid weight of 110 kg. * @example `QuintalsOfMilk(-20.5)` is not a valid weight. */ -final case class QuintalsOfMilk(quintals: NonNegativeDecimal) +final case class QuintalsOfMilk(quintals: NonNegativeNumber) derives Plus, Times, Minus /** * A [[Week week]] of a given [[Year year]]. @@ -98,9 +67,7 @@ final case class StockedQuantity(quantity: NonNegativeNumber) * @example `Quantity(-2)` is not a valid quantity. * @example `Quantity(20)` is a valida quantity. */ -final case class Quantity(n: PositiveNumber) { - def -(p: StockedQuantity): NonNegativeNumber = this.n.toNonNegativeNumber - p.quantity -} +final case class Quantity(n: NonNegativeNumber) derives Plus, Times, Minus /** * A [[Product product]] requested in a given [[Quantity quantity]] that has to be produced by the given diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala index 79a7bc61..97652d50 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala @@ -3,27 +3,17 @@ package dev.atedeg.mdm.milkplanning.utils import scala.annotation.targetName import cats.kernel.Order +import eu.timepit.refined.auto.autoUnwrap import eu.timepit.refined.predicates.all.NonNegative import eu.timepit.refined.refineV import dev.atedeg.mdm.milkplanning.types.{ Quantity, QuintalsOfMilk, StockedQuantity } import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.given +import dev.atedeg.mdm.utils.NonNegativeNumber object QuintalsOfMilkOps: given Order[QuintalsOfMilk] with + override def compare(x: QuintalsOfMilk, y: QuintalsOfMilk): Int = Order[Int].compare(x.quintals, y.quintals) - override def compare(x: QuintalsOfMilk, y: QuintalsOfMilk): Int = - Order[NonNegativeDecimal].compare(x.quintals, y.quintals) - - extension (qom1: QuintalsOfMilk) - - @targetName("plusQuintals") - def +(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.quintals plus qom2.quintals) - - @targetName("minusQuintals") - def -(qom2: QuintalsOfMilk): QuintalsOfMilk = QuintalsOfMilk(qom1.quintals minus qom2.quintals) - - @targetName("timesQuintals") - def **(nn: NonNegativeNumber): QuintalsOfMilk = QuintalsOfMilk(qom1.quintals * nn.toNonNegativeDecimal) + extension (n: NonNegativeNumber) def toQuintalsOfMilk: QuintalsOfMilk = QuintalsOfMilk(n) diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/dsl/Dsl.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/dsl/Dsl.scala deleted file mode 100644 index 4b5d3dff..00000000 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/dsl/Dsl.scala +++ /dev/null @@ -1,14 +0,0 @@ -package dev.atedeg.mdm.milkplanning.dsl - -import eu.timepit.refined.numeric.NonNegative -import eu.timepit.refined.refineV - -import dev.atedeg.mdm.milkplanning.types.{ QuintalsOfMilk, StockedQuantity } - -extension [A](x: Option[A]) - - @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) - def coerce: A = x.get - -extension (n: Double) def quintalsOfMilk: QuintalsOfMilk = QuintalsOfMilk(refineV[NonNegative](n).toOption.coerce) -extension (n: Int) def stockedQuantity: StockedQuantity = StockedQuantity(refineV[NonNegative](n).toOption.coerce) diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index 9af3ee31..4a708c17 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -7,19 +7,20 @@ import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers -import dev.atedeg.mdm.milkplanning.dsl.* import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.OrderMilk -import dev.atedeg.mdm.milkplanning.types.Product.* +import dev.atedeg.mdm.products.CheeseType +import dev.atedeg.mdm.products.Grams +import dev.atedeg.mdm.products.Product.* import dev.atedeg.mdm.utils.* trait Fixture { val recipeBook: RecipeBook = Map( - CheeseType.Squacquerone -> QuintalsOfMilk(1.3), - CheeseType.Casatella -> QuintalsOfMilk(1.3), - CheeseType.Ricotta -> QuintalsOfMilk(1.1), - CheeseType.Stracchino -> QuintalsOfMilk(1.4), - CheeseType.Caciotta -> QuintalsOfMilk(1.4), + CheeseType.Squacquerone -> QuintalsOfMilk(1), + CheeseType.Casatella -> QuintalsOfMilk(1), + CheeseType.Ricotta -> QuintalsOfMilk(1), + CheeseType.Stracchino -> QuintalsOfMilk(1), + CheeseType.Caciotta -> QuintalsOfMilk(1), ) } @@ -31,12 +32,12 @@ class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with F And("a list of products to be produced") And("an empty stock") And("no milk in stock") - val qomPreviousYear = QuintalsOfMilk(4.0) + val qomPreviousYear = QuintalsOfMilk(4) val requestedProducts = NonEmptyList.of( - RequestedProduct(Squacquerone(100), Quantity(50), LocalDateTime.now()), + RequestedProduct(Squacquerone(Grams(100)), Quantity(50), LocalDateTime.now()), ) val currentStock: Stock = _ => StockedQuantity(0) - val stockedMilk = QuintalsOfMilk(0.0) + val stockedMilk = QuintalsOfMilk(0) When("the estimation is ready to be computed") val estimatorMonad: Writer[List[OrderMilk], QuintalsOfMilk] = estimateQuintalsOfMilk( qomPreviousYear, From 239ca7e18ce0735479da58548aed0c3f15b209f5 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 11:40:38 +0200 Subject: [PATCH 028/329] chore(utils): define Ceil typeclass --- .../dev/atedeg/mdm/utils/NumericOps.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala index 5a85ceff..31f33d85 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala @@ -2,6 +2,8 @@ package dev.atedeg.mdm.utils import scala.annotation.targetName +import eu.timepit.refined.api.Refined +import eu.timepit.refined.numeric.{ NonNegative, Positive } import shapeless3.deriving.K0 trait Plus[N]: @@ -20,6 +22,10 @@ trait Div[N]: def div(x: N, y: N): N extension (x: N) @targetName("divOperator") def /(y: N) = div(x, y) +trait Ceil[N]: + def ceil(n: N): N + extension (n: N) def toCeil: N = ceil(n) + object Plus: given [N: Numeric]: Plus[N] with @@ -59,3 +65,15 @@ object Div: inline given divGen[N](using inst: K0.ProductInstances[Div, N]): Div[N] with def div(n1: N, n2: N): N = inst.map2(n1, n2)([n] => (d: Div[n], n1: n, n2: n) => d.div(n1, n2)) + +given [N, P <: Positive | NonNegative: ValidFor[N]](using C: Ceil[N]): Ceil[N Refined P] with + override def ceil(n: N Refined P): N Refined P = coerce(C.ceil(n.value)) + +given Ceil[Double] with + override def ceil(n: Double): Double = math.ceil(n) + +object Ceil: + inline def derived[A](using gen: K0.ProductGeneric[A]): Ceil[A] = ceilGen + + inline given ceilGen[N](using inst: K0.ProductInstances[Ceil, N]): Ceil[N] with + override def ceil(n: N): N = inst.map(n)([n] => (c: Ceil[n], n: n) => c.ceil(n)) From a187399b697fed9d0b6c0049c3883549be4e41d5 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 11:42:12 +0200 Subject: [PATCH 029/329] chore(utils): define some extensions method on refined types --- utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala index 6604ca35..d4aa5ad2 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala @@ -15,6 +15,14 @@ type NumberInClosedRange[L, U] = Int Refined Interval.Closed[L, U] type NonNegativeNumber = Int Refined NonNegative type NonNegativeDecimal = Double Refined NonNegative +extension [N: Numeric](n: N Refined Positive) def toNonNegative: N Refined NonNegative = coerce(n.value) + +extension [N, P <: Positive | NonNegative: ValidFor[N]: ValidFor[Double]](n: N Refined P)(using N: Numeric[N]) + def toDecimal: Double Refined P = coerce(N.toDouble(n.value)) + +extension [P <: Positive | NonNegative: ValidFor[Double]](d: Double Refined P) + def toNumber: NonNegativeNumber = coerce(d.value.toInt) + // `T Refined P` has an order relation if `T` has an order relation given refinedOrd[N: Order, P]: Order[N Refined P] with override def compare(x: N Refined P, y: N Refined P): Int = Order[N].compare(x.value, y.value) From 1272bfa2f5635987d010eef68838faae9b6326c6 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 11:44:45 +0200 Subject: [PATCH 030/329] refactor: introduce Yield concept to adapt the code to the UL --- .../dev/atedeg/mdm/milkplanning/types/Types.scala | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index 9f84f870..6fad6710 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -44,10 +44,17 @@ final case class Week(n: NumberInClosedRange[1, 52]) final case class Year(n: PositiveNumber) /** - * It defines the how many [[QuintalsOfMilk quintals of milk]] are needed to produce a quintal of a given - * [[CheeseType cheese type]]. + * Represent how many [[QuintalsOfMilk quintals of milk]] are needed to produce a given quantity of [[Product product]]. + * @example In order to produce 180kg of a product are necessary 10 quintals of milk, in this case the yield is `5.55`. + * @example `Yield(0)` is not a valid yield. + * @example `Yield(5.55)` is a valid yield. */ -type RecipeBook = CheeseType => QuintalsOfMilk +final case class Yield(n: PositiveDecimal) + +/** + * It defines, for each [[Product product]], the [[Yield yield]] of the milk. + */ +type RecipeBook = CheeseType => Yield /** * It defines, for each [[Product product]], the [[StockedQuantity quantity in stock]]. @@ -67,7 +74,7 @@ final case class StockedQuantity(quantity: NonNegativeNumber) * @example `Quantity(-2)` is not a valid quantity. * @example `Quantity(20)` is a valida quantity. */ -final case class Quantity(n: NonNegativeNumber) derives Plus, Times, Minus +final case class Quantity(n: PositiveNumber) derives Plus, Times /** * A [[Product product]] requested in a given [[Quantity quantity]] that has to be produced by the given From 99255966c677eecc2e855864a8aba2a6081186d2 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 16:26:04 +0200 Subject: [PATCH 031/329] chore(utils): rename extension method --- .../main/scala/dev/atedeg/mdm/utils/NumericOps.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala index 31f33d85..f4f69f25 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala @@ -23,8 +23,8 @@ trait Div[N]: extension (x: N) @targetName("divOperator") def /(y: N) = div(x, y) trait Ceil[N]: - def ceil(n: N): N - extension (n: N) def toCeil: N = ceil(n) + def toCeil(n: N): N + extension (n: N) def ceil: N = toCeil(n) object Plus: @@ -67,13 +67,13 @@ object Div: def div(n1: N, n2: N): N = inst.map2(n1, n2)([n] => (d: Div[n], n1: n, n2: n) => d.div(n1, n2)) given [N, P <: Positive | NonNegative: ValidFor[N]](using C: Ceil[N]): Ceil[N Refined P] with - override def ceil(n: N Refined P): N Refined P = coerce(C.ceil(n.value)) + override def toCeil(n: N Refined P): N Refined P = coerce(C.toCeil(n.value)) given Ceil[Double] with - override def ceil(n: Double): Double = math.ceil(n) + override def toCeil(n: Double): Double = math.ceil(n) object Ceil: inline def derived[A](using gen: K0.ProductGeneric[A]): Ceil[A] = ceilGen inline given ceilGen[N](using inst: K0.ProductInstances[Ceil, N]): Ceil[N] with - override def ceil(n: N): N = inst.map(n)([n] => (c: Ceil[n], n: n) => c.ceil(n)) + override def toCeil(n: N): N = inst.map(n)([n] => (c: Ceil[n], n: n) => c.toCeil(n)) From 3bc061284132a6a10cf7e50eb05e569ade90b544 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 16:27:37 +0200 Subject: [PATCH 032/329] chore(utils): improve extension method name --- .../main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala index 97652d50..cf24911b 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala @@ -16,4 +16,4 @@ object QuintalsOfMilkOps: given Order[QuintalsOfMilk] with override def compare(x: QuintalsOfMilk, y: QuintalsOfMilk): Int = Order[Int].compare(x.quintals, y.quintals) - extension (n: NonNegativeNumber) def toQuintalsOfMilk: QuintalsOfMilk = QuintalsOfMilk(n) + extension (n: NonNegativeDecimal) def quintalsOfMilk: QuintalsOfMilk = QuintalsOfMilk(n.toNumber) From 30a6df22f870f68c24a56d03844121e337ace6e9 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 16:30:28 +0200 Subject: [PATCH 033/329] chore(utils): add SafeAction stack --- .../main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala index 28ac4077..e77d3c62 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala @@ -1,6 +1,6 @@ package dev.atedeg.mdm.utils.monads -import cats.data.{ EitherT, Reader, WriterT } +import cats.data.{ EitherT, Reader, Writer, WriterT } private type Reading[State] = [A] =>> Reader[State, A] private type EmittingT[M[_], Event] = [A] =>> WriterT[M, List[Event], A] @@ -16,6 +16,13 @@ type ActionWithState[Error, Event, Result, State] = EitherT[EmittingT[Reading[St */ type Action[Error, Event, Result] = ActionWithState[Error, Event, Result, Unit] +/** + * The same as an [[Action action]] but does not fail. + */ +type SafeAction[Event, Result] = Writer[List[Event], Result] + +extension [Event, Result](action: SafeAction[Event, Result]) def execute: (List[Event], Result) = action.run + extension [Error, Event, Result, State](action: ActionWithState[Error, Event, Result, State]) /** * `a.execute(s)` runs the [[ActionWithState action]] `a` with a state `s` returning all the From 412f67541ad8de4a3dc5aee1aa415608d7cd47c6 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 16:31:11 +0200 Subject: [PATCH 034/329] chore(products): define Product unapply --- .../main/scala/dev/atedeg/mdm/products/Products.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala index 150af0a1..b90beaaa 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala @@ -1,11 +1,12 @@ package dev.atedeg.mdm.products -import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.given -import eu.timepit.refined.api.{Refined, Validate} +import eu.timepit.refined.api.{ Refined, Validate } import eu.timepit.refined.predicates.all.Positive import eu.timepit.refined.refineV +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given + /** * A weight in grams. */ @@ -50,3 +51,6 @@ val allStracchinoWeights = all[StracchinoWeightsInGrams] type CaciottaWeightsInGrams = (500, 1000) type CaciottaWeightInGrams = OneOf[CaciottaWeightsInGrams] val allCaciottaWeights = all[CaciottaWeightsInGrams] + +object Product: + def unapply(prod: Product): (CheeseType, Grams) = (prod.cheeseType, prod.weight) From 479d68dc038dd949dbc8db39acbeab142c35a2e0 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 16:32:30 +0200 Subject: [PATCH 035/329] style: reformat file --- .../main/scala/dev/atedeg/mdm/products/Utils.scala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala index c6cc7f25..304e0426 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala @@ -1,26 +1,27 @@ package dev.atedeg.mdm.products +import scala.compiletime.* + import cats.data.NonEmptyList -import dev.atedeg.mdm.utils.{PositiveNumber, coerce} import eu.timepit.refined.predicates.all.Positive -import scala.compiletime.* +import dev.atedeg.mdm.utils.{ coerce, PositiveNumber } type OneOf[T <: Tuple] = T match case (t *: EmptyTuple) => t case (t *: ts) => t | OneOf[ts] inline def all[T <: Tuple]: NonEmptyList[OneOf[T]] = inline erasedValue[T] match - case _:(n *: EmptyTuple) => + case _: (n *: EmptyTuple) => val v = checkInt[n](constValue[n]) NonEmptyList.one(toGrams(v)).asInstanceOf[NonEmptyList[OneOf[T]]] - case _:(n *: gs) => + case _: (n *: gs) => val v = checkInt[n](constValue[n]) NonEmptyList(toGrams(v), all[gs].toList).asInstanceOf[NonEmptyList[OneOf[T]]] case _ => compiletime.error("Cannot work on a tuple with elements that are not Grams") -private inline def checkInt[T](inline n: T): Int = inline erasedValue[T] match +inline private def checkInt[T](inline n: T): Int = inline erasedValue[T] match case _: Int => n.asInstanceOf[Int] case _ => compiletime.error(codeOf(n) + " is not an int") -private[products] def toGrams(n: Int): Grams = Grams(coerce[Int, Positive](n)) \ No newline at end of file +private[products] def toGrams(n: Int): Grams = Grams(coerce[Int, Positive](n)) From 823cb59cca0e0c5062bf3910f1eb98ca0f165046 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 16:33:51 +0200 Subject: [PATCH 036/329] chore: remove unused calsses --- .../atedeg/mdm/milkplanning/types/Types.scala | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala index 6fad6710..668fa187 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala @@ -22,27 +22,6 @@ final case class ProcessedMilk(quantity: QuintalsOfMilk) */ final case class QuintalsOfMilk(quintals: NonNegativeNumber) derives Plus, Times, Minus -/** - * A [[Week week]] of a given [[Year year]]. - */ -final case class Period(week: Week, year: Year) - -/** - * The number of a week in a year - * @note it must be a [[NumberInClosedRange number]] between 1 and 52 inclusive. - * @example `Week(1)` is a valid week. - * @example `Week(54)` is not a valid week. - */ -final case class Week(n: NumberInClosedRange[1, 52]) - -/** - * A year. - * @note it must be a [[PositiveNumber positive number]]. - * @example `Year(2022)` is a valid year. - * @example `Year(-1000)` is not a valid year. - */ -final case class Year(n: PositiveNumber) - /** * Represent how many [[QuintalsOfMilk quintals of milk]] are needed to produce a given quantity of [[Product product]]. * @example In order to produce 180kg of a product are necessary 10 quintals of milk, in this case the yield is `5.55`. From 34aa275fb14bba2a3264679d4f7b72f67e592fbb Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 16:34:20 +0200 Subject: [PATCH 037/329] feat: complete quintals of milk estimation --- .../mdm/milkplanning/types/Actions.scala | 27 +++++++--- .../mdm/milkplanning/types/ActionsTest.scala | 52 ++++++++++++------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala index 89c8f3f1..48621551 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala @@ -2,16 +2,20 @@ package dev.atedeg.mdm.milkplanning.types import cats.Monad import cats.data.NonEmptyList -import eu.timepit.refined.auto.autoUnwrap +import cats.syntax.all.* import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.* import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.* import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.given import dev.atedeg.mdm.milkplanning.utils.given +import dev.atedeg.mdm.products.Product import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.{ emit, thenReturn, when, Emits } +/** + * . + */ def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( milkOfPreviousYear: QuintalsOfMilk, requestedProductsForWeek: NonEmptyList[RequestedProduct], @@ -26,14 +30,23 @@ def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( private def milkNeededForProducts( requestedProducts: NonEmptyList[RequestedProduct], stock: Stock, - book: RecipeBook, + recipeBook: RecipeBook, ): QuintalsOfMilk = requestedProducts - .map(p => (p.product, p.quantity)) - .map { case (prod, quantity) => (prod, quantity.n - stock(prod).quantity) } - .map { case (prod, quantity) => book(prod.cheeseType).quintals * quantity } - .map(_.toQuintalsOfMilk) - .reduce(_ + _) + .map(milkNeededForProduct(_, stock, recipeBook)) + .foldLeft(0.quintalsOfMilk)(_ + _) + +private def milkNeededForProduct( + product: RequestedProduct, + stock: Stock, + recipeBook: RecipeBook, +): QuintalsOfMilk = + val RequestedProduct(p @ Product(cheeseType, weight), quantity, _) = product + val unitsToProduce = quantity.n.toNonNegative - stock(p).quantity + val gramsToProduce = unitsToProduce * weight.n.toNonNegative + val quintalsToProduce = gramsToProduce.toDecimal / 100_000 + val neededQuintals = recipeBook(cheeseType).n.toNonNegative * quintalsToProduce + neededQuintals.ceil.quintalsOfMilk private def magicAiEstimator( milkOfPreviousYear: QuintalsOfMilk, diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index 4a708c17..d1c9fbb6 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -2,55 +2,69 @@ package dev.atedeg.mdm.milkplanning.types import java.time.LocalDateTime -import cats.data.{ NonEmptyList, Writer } +import cats.data.{ NonEmptyList, NonEmptyMap, Writer } +import cats.implicits.catsKernelOrderingForOrder +import cats.syntax.all.* import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.OrderMilk -import dev.atedeg.mdm.products.CheeseType -import dev.atedeg.mdm.products.Grams +import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.* +import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.given +import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } import dev.atedeg.mdm.products.Product.* import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given +import dev.atedeg.mdm.utils.monads.* trait Fixture { val recipeBook: RecipeBook = Map( - CheeseType.Squacquerone -> QuintalsOfMilk(1), - CheeseType.Casatella -> QuintalsOfMilk(1), - CheeseType.Ricotta -> QuintalsOfMilk(1), - CheeseType.Stracchino -> QuintalsOfMilk(1), - CheeseType.Caciotta -> QuintalsOfMilk(1), + CheeseType.Squacquerone -> Yield(5.55), + CheeseType.Casatella -> Yield(5.55), + CheeseType.Ricotta -> Yield(4.54), + CheeseType.Stracchino -> Yield(6.55), + CheeseType.Caciotta -> Yield(8.33), ) } +@SuppressWarnings(Array("org.wartremover.warts.Any")) class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Fixture { Feature("Estimate the quintals of milk needed for the following week") { Scenario("Raffaella wants to estimate the quintals of milk") { Given("the quintals of milk of the previous year for the same period") + val qomPreviousYear = 12.quintalsOfMilk And("a list of products to be produced") - And("an empty stock") - And("no milk in stock") - val qomPreviousYear = QuintalsOfMilk(4) val requestedProducts = NonEmptyList.of( - RequestedProduct(Squacquerone(Grams(100)), Quantity(50), LocalDateTime.now()), + RequestedProduct(Squacquerone(100), Quantity(500), LocalDateTime.now()), + RequestedProduct(Squacquerone(250), Quantity(300), LocalDateTime.now()), + RequestedProduct(Ricotta(350), Quantity(50), LocalDateTime.now()), + RequestedProduct(Caciotta(500), Quantity(100), LocalDateTime.now()), ) + And("an empty stock") val currentStock: Stock = _ => StockedQuantity(0) + And("there is no milk in stock") val stockedMilk = QuintalsOfMilk(0) - When("the estimation is ready to be computed") - val estimatorMonad: Writer[List[OrderMilk], QuintalsOfMilk] = estimateQuintalsOfMilk( + When("estimating the necessary quintals of milk") + val estimateAction: SafeAction[OrderMilk, QuintalsOfMilk] = estimateQuintalsOfMilk( qomPreviousYear, requestedProducts, currentStock, recipeBook, stockedMilk, ) - val (events, estimation) = estimatorMonad.run - println(estimation) - - Then("the result will be in the interval [..., ...]") - + val (events, estimation) = estimateAction.execute + Then("the result should be at least greater than the quintals of milk needed to produce all products") + val quintalsForRequestedProducts = + requestedProducts.map { case RequestedProduct(Product(_, weight), quantity, _) => weight.n * quantity.n } + .map(_.toDecimal.toNonNegative / 100_000) + .map(_.ceil.quintalsOfMilk) + .foldLeft(0.quintalsOfMilk)(_ + _) + estimation should be > quintalsForRequestedProducts + events should not be empty + events.map(_.n) should contain(estimation) } } } From a01947ce143edad845b78dc878130dd15f91ab95 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 16:42:00 +0200 Subject: [PATCH 038/329] chore: suppress a ridiculous warning by scalafix --- .../main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala index 48621551..ca16a7a1 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala @@ -36,6 +36,7 @@ private def milkNeededForProducts( .map(milkNeededForProduct(_, stock, recipeBook)) .foldLeft(0.quintalsOfMilk)(_ + _) +@SuppressWarnings(Array("scalafix:DisableSyntax.noValPatterns")) private def milkNeededForProduct( product: RequestedProduct, stock: Stock, From a01e9478fd5b0a60b03e1f58967185faf82e2860 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 16:49:22 +0200 Subject: [PATCH 039/329] build: remove strict equality --- build.sbt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 94443517..5168dde9 100644 --- a/build.sbt +++ b/build.sbt @@ -36,9 +36,7 @@ ThisBuild / developers := List( ), ) -ThisBuild / scalacOptions += "-language:strictEquality" - -ThisBuild / wartremoverErrors ++= Warts.allBut(Wart.Overloading) +ThisBuild / wartremoverErrors ++= Warts.allBut(Wart.Overloading, Wart.Equals) ThisBuild / scalafixDependencies ++= Seq( "com.github.xuwei-k" %% "scalafix-rules" % "0.2.1", From ebc1f8dad55367adce441ef97cc07d327d456d26 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 17:56:25 +0200 Subject: [PATCH 040/329] chore: update classes to be in the UL --- .ubidoc.yml | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index 361a9e89..dbf947aa 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -3,28 +3,13 @@ tables: termName: "Term" definitionName: "Definition" rows: - - enum: "CheeseType" - - enum: "Product" - class: "ProcessedMilk" - class: "QuintalsOfMilk" - - class: "Period" + - class: "Yield" - type: "RecipeBook" - type: "Stock" - class: "StockedQuantity" -ignored: - - class: "Week" - - class: "Year" - - case: "Squacquerone" - - case: "Casatella" - - case: "Ricotta" - - case: "Stracchino" - - case: "Caciotta" - - type: "SquacqueroneSizeInGrams" - - type: "CasatellaSizeInGrams" - - type: "RicottaSizeInGrams" - - type: "StracchinoSizeInGrams" - - type: "CaciottaSizeInGrams" - - type: "PositiveNumber" - - type: "PositiveDecimal" - - type: "NumberInClosedRange" - - type: "NonNegativeNumber" \ No newline at end of file + - class: "Quantity" + - class: "RequestedProduct" + +ignored: [] \ No newline at end of file From a2e01748f7a94c7d2117a44e10c7ca048f3690bb Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 18:13:13 +0200 Subject: [PATCH 041/329] feat: define incoming and outgoing events --- .../mdm/milkplanning/types/Events.scala | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala index 7f5efad7..078556bb 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala @@ -1,11 +1,24 @@ package dev.atedeg.mdm.milkplanning.types +import cats.data.NonEmptyList + import dev.atedeg.mdm.products.Product +/** + * Events managed by the bounded context. + */ enum IncomingEvent: - case ProductRemovedFromStock(product: Product) - case ProductAddedToStock(product: Product) - case RestockedMilk(quintalsOfMilk: QuintalsOfMilk) + /** + * Event representing an order placed used to estimate the [[QuintalsOfMilk quintals of milk]] to be ordered. + */ + case ReceivedOrder(products: NonEmptyList[RequestedProduct]) +/** + * Events sent by the bounded context. + */ enum OutgoingEvent: + /** + * Event to order the [[QuintalsOfMilk quintals of milk]] needed for the next week. + * This event is emitted every week on saturday. + */ case OrderMilk(n: QuintalsOfMilk) From 502701e78f5628449261692653570d08d61e3cf4 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 29 Jul 2022 18:13:31 +0200 Subject: [PATCH 042/329] chore: define files for incoming and outgoing events --- .ubidoc.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.ubidoc.yml b/.ubidoc.yml index dbf947aa..f8cc66d6 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -12,4 +12,16 @@ tables: - class: "Quantity" - class: "RequestedProduct" + - name: "milk-planning-incoming" + termName: "Event" + definitionName: "Description" + rows: + - case: "ReceivedOrder" + + - name: "milk-planning-outgoing" + termName: "Event" + definitionName: "Description" + rows: + - case: "OrderMilk" + ignored: [] \ No newline at end of file From 91cf5881ad73b7333746b54996cc64d81709b644 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 30 Jul 2022 10:47:04 +0200 Subject: [PATCH 043/329] chore: move out at top level all the classes in the package types as suggested by @vitlinda --- .../scala/dev/atedeg/mdm/milkplanning/{types => }/Actions.scala | 0 .../scala/dev/atedeg/mdm/milkplanning/{types => }/Events.scala | 0 .../scala/dev/atedeg/mdm/milkplanning/{types => }/Types.scala | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/{types => }/Actions.scala (100%) rename milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/{types => }/Events.scala (100%) rename milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/{types => }/Types.scala (100%) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala similarity index 100% rename from milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Actions.scala rename to milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala similarity index 100% rename from milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Events.scala rename to milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala similarity index 100% rename from milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/types/Types.scala rename to milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala From 6190b460ee97be6f2deab2da60eb3e273bb5a237 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 30 Jul 2022 10:58:49 +0200 Subject: [PATCH 044/329] docs: add incoming and outgoing events tables --- docs/_docs/milk-planning.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/_docs/milk-planning.md b/docs/_docs/milk-planning.md index 70bf1ca2..0819c10a 100644 --- a/docs/_docs/milk-planning.md +++ b/docs/_docs/milk-planning.md @@ -22,4 +22,10 @@ She makes this estimate by taking into account the following factors: ## Domain Events -{% include milk-planning-de.md %} +### Incoming Events + +{% include milk-planning-incoming.md %} + +### Outgoing Events + +{% include milk-planning-outgoing.md %} From 53fe784fedb3c3afa88f488d0f098a8d99f4381a Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 30 Jul 2022 11:25:52 +0200 Subject: [PATCH 045/329] chore(utils): define implicit conversion between refined types --- utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala index d4aa5ad2..05ac7fd4 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala @@ -23,6 +23,12 @@ extension [N, P <: Positive | NonNegative: ValidFor[N]: ValidFor[Double]](n: N R extension [P <: Positive | NonNegative: ValidFor[Double]](d: Double Refined P) def toNumber: NonNegativeNumber = coerce(d.value.toInt) +given Conversion[PositiveNumber, NonNegativeNumber] with + override def apply(x: PositiveNumber): NonNegativeNumber = coerce(x.value) + +given Conversion[PositiveDecimal, NonNegativeDecimal] with + override def apply(x: PositiveDecimal): NonNegativeDecimal = coerce(x.value) + // `T Refined P` has an order relation if `T` has an order relation given refinedOrd[N: Order, P]: Order[N Refined P] with override def compare(x: N Refined P, y: N Refined P): Int = Order[N].compare(x.value, y.value) From 19ef20021d04676f655523fe6f7f83488409d8d7 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 30 Jul 2022 11:26:43 +0200 Subject: [PATCH 046/329] chore: fix package path --- .../src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala | 2 +- .../src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala | 2 +- .../scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala | 2 +- .../scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala index 078556bb..61af4b67 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala @@ -1,4 +1,4 @@ -package dev.atedeg.mdm.milkplanning.types +package dev.atedeg.mdm.milkplanning import cats.data.NonEmptyList diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala index 668fa187..7bd20f2a 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala @@ -1,4 +1,4 @@ -package dev.atedeg.mdm.milkplanning.types +package dev.atedeg.mdm.milkplanning import java.time.LocalDateTime diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala index cf24911b..f061d6d5 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/utils/TypesOps.scala @@ -7,7 +7,7 @@ import eu.timepit.refined.auto.autoUnwrap import eu.timepit.refined.predicates.all.NonNegative import eu.timepit.refined.refineV -import dev.atedeg.mdm.milkplanning.types.{ Quantity, QuintalsOfMilk, StockedQuantity } +import dev.atedeg.mdm.milkplanning.{ Quantity, QuintalsOfMilk, StockedQuantity } import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.NonNegativeNumber diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index d1c9fbb6..fbfc3bcb 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -9,7 +9,8 @@ import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers -import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.OrderMilk +import dev.atedeg.mdm.milkplanning.* +import dev.atedeg.mdm.milkplanning.OutgoingEvent.OrderMilk import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.* import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.given import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } From dd470313e321f6bdd0a42253e30b88c65146fa4f Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 30 Jul 2022 11:27:38 +0200 Subject: [PATCH 047/329] refactor: use implicit conversion between refined types and improve method docs --- .../dev/atedeg/mdm/milkplanning/Actions.scala | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala index ca16a7a1..a5454efd 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala @@ -1,10 +1,10 @@ -package dev.atedeg.mdm.milkplanning.types +package dev.atedeg.mdm.milkplanning import cats.Monad import cats.data.NonEmptyList import cats.syntax.all.* -import dev.atedeg.mdm.milkplanning.types.OutgoingEvent.* +import dev.atedeg.mdm.milkplanning.OutgoingEvent.* import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.* import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.given import dev.atedeg.mdm.milkplanning.utils.given @@ -14,7 +14,10 @@ import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.{ emit, thenReturn, when, Emits } /** - * . + * Estimate the amount of milk needed for the following week's production. + * The estimate takes into account the milk processed in the same week last year, + * the products ordered for the following week, the current [[Stock stock]] and the [[QuintalsOfMilk quintals of milk]] + * currently in stock and return the [[QuintalsOfMilk quintals of milk]] needed for the following week. */ def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( milkOfPreviousYear: QuintalsOfMilk, @@ -43,10 +46,10 @@ private def milkNeededForProduct( recipeBook: RecipeBook, ): QuintalsOfMilk = val RequestedProduct(p @ Product(cheeseType, weight), quantity, _) = product - val unitsToProduce = quantity.n.toNonNegative - stock(p).quantity - val gramsToProduce = unitsToProduce * weight.n.toNonNegative + val unitsToProduce = (quantity.n: NonNegativeNumber) - stock(p).quantity + val gramsToProduce = unitsToProduce * weight.n val quintalsToProduce = gramsToProduce.toDecimal / 100_000 - val neededQuintals = recipeBook(cheeseType).n.toNonNegative * quintalsToProduce + val neededQuintals = quintalsToProduce * recipeBook(cheeseType).n neededQuintals.ceil.quintalsOfMilk private def magicAiEstimator( From 5a173cf47e838b54a01da4edaff1d902b49411b1 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 30 Jul 2022 15:19:36 +0200 Subject: [PATCH 048/329] refactor: apply all suggestions by @giacomocavalieri --- .ubidoc.yml | 6 +++--- .../main/scala/dev/atedeg/mdm/milkplanning/Actions.scala | 8 ++++---- .../main/scala/dev/atedeg/mdm/milkplanning/Types.scala | 5 +++-- .../dev/atedeg/mdm/milkplanning/types/ActionsTest.scala | 4 ++-- .../src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala | 3 --- utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala | 3 +++ 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index f8cc66d6..ade82522 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -16,12 +16,12 @@ tables: termName: "Event" definitionName: "Description" rows: - - case: "ReceivedOrder" + - case: "IncomingEvent.ReceivedOrder" - name: "milk-planning-outgoing" termName: "Event" definitionName: "Description" rows: - - case: "OrderMilk" + - case: "OutgoingEvent.OrderMilk" -ignored: [] \ No newline at end of file +ignored: [] diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala index a5454efd..adc5a276 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala @@ -8,7 +8,7 @@ import dev.atedeg.mdm.milkplanning.OutgoingEvent.* import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.* import dev.atedeg.mdm.milkplanning.utils.QuintalsOfMilkOps.given import dev.atedeg.mdm.milkplanning.utils.given -import dev.atedeg.mdm.products.Product +import dev.atedeg.mdm.products.{ Grams, Product } import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.{ emit, thenReturn, when, Emits } @@ -45,9 +45,9 @@ private def milkNeededForProduct( stock: Stock, recipeBook: RecipeBook, ): QuintalsOfMilk = - val RequestedProduct(p @ Product(cheeseType, weight), quantity, _) = product - val unitsToProduce = (quantity.n: NonNegativeNumber) - stock(p).quantity - val gramsToProduce = unitsToProduce * weight.n + val RequestedProduct(p @ Product(cheeseType, Grams(weight)), Quantity(quantity), _) = product + val unitsToProduce = quantity.toNonNegative - stock(p).quantity + val gramsToProduce = unitsToProduce * weight val quintalsToProduce = gramsToProduce.toDecimal / 100_000 val neededQuintals = quintalsToProduce * recipeBook(cheeseType).n neededQuintals.ceil.quintalsOfMilk diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala index 7bd20f2a..61c44835 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala @@ -23,8 +23,9 @@ final case class ProcessedMilk(quantity: QuintalsOfMilk) final case class QuintalsOfMilk(quintals: NonNegativeNumber) derives Plus, Times, Minus /** - * Represent how many [[QuintalsOfMilk quintals of milk]] are needed to produce a given quantity of [[Product product]]. - * @example In order to produce 180kg of a product are necessary 10 quintals of milk, in this case the yield is `5.55`. + * A decimal that represents the yield of milk when producing a given [[CheeseType cheese type]]: + * i.e. to produce `n` quintals of a given [[CheeseType cheese type]], `yield of cheese type * n` + * [[QuintalsOfMilk quintals of milk]] must be used. * @example `Yield(0)` is not a valid yield. * @example `Yield(5.55)` is a valid yield. */ diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index fbfc3bcb..0abdb53e 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -19,7 +19,7 @@ import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.* -trait Fixture { +trait Mocks { val recipeBook: RecipeBook = Map( CheeseType.Squacquerone -> Yield(5.55), @@ -31,7 +31,7 @@ trait Fixture { } @SuppressWarnings(Array("org.wartremover.warts.Any")) -class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Fixture { +class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Feature("Estimate the quintals of milk needed for the following week") { Scenario("Raffaella wants to estimate the quintals of milk") { diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala index f4f69f25..7e0b2006 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala @@ -66,9 +66,6 @@ object Div: inline given divGen[N](using inst: K0.ProductInstances[Div, N]): Div[N] with def div(n1: N, n2: N): N = inst.map2(n1, n2)([n] => (d: Div[n], n1: n, n2: n) => d.div(n1, n2)) -given [N, P <: Positive | NonNegative: ValidFor[N]](using C: Ceil[N]): Ceil[N Refined P] with - override def toCeil(n: N Refined P): N Refined P = coerce(C.toCeil(n.value)) - given Ceil[Double] with override def toCeil(n: Double): Double = math.ceil(n) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala index 05ac7fd4..f2b8760a 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala @@ -29,6 +29,9 @@ given Conversion[PositiveNumber, NonNegativeNumber] with given Conversion[PositiveDecimal, NonNegativeDecimal] with override def apply(x: PositiveDecimal): NonNegativeDecimal = coerce(x.value) +given [N, P <: Positive | NonNegative: ValidFor[N]](using C: Ceil[N]): Ceil[N Refined P] with + override def toCeil(n: N Refined P): N Refined P = coerce(C.toCeil(n.value)) + // `T Refined P` has an order relation if `T` has an order relation given refinedOrd[N: Order, P]: Order[N Refined P] with override def compare(x: N Refined P, y: N Refined P): Int = Order[N].compare(x.value, y.value) From 5bb59438a0881ae3e1275929458b8948141cb44f Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 1 Aug 2022 11:22:31 +0200 Subject: [PATCH 049/329] build: set version scheme and skip publish for utils and root project --- build.sbt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.sbt b/build.sbt index 5168dde9..c8d48cf8 100644 --- a/build.sbt +++ b/build.sbt @@ -5,6 +5,7 @@ ThisBuild / scalaVersion := scala3Version ThisBuild / organization := "dev.atedeg" ThisBuild / homepage := Some(url("https://github.com/atedeg/mdm")) ThisBuild / licenses := List("Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0")) +ThisBuild / versionScheme := Some("early-semver") ThisBuild / ubidoc / targetDirectory := baseDirectory.value / "_includes" ThisBuild / ubidoc / lookupDirectory := target.value / "site" @@ -82,12 +83,16 @@ lazy val root = project title = "mdm coverage report", formats = Seq(JacocoReportFormats.XML), ), + publish / skip := true, ) .aggregate(utils, `milk-planning`) lazy val utils = project .in(file("utils")) .settings(commonSettings) + .settings( + publish / skip := true, + ) lazy val `milk-planning` = project .in(file("milk-planning")) From 181921f95a21ab325a784f0afb2096395fea915e Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 1 Aug 2022 11:55:09 +0200 Subject: [PATCH 050/329] build: set groupid to dev.atedeg.mdm --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index c8d48cf8..9c002358 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ val scala3Version = "3.1.3" val scalaTestVersion = "3.2.13" ThisBuild / scalaVersion := scala3Version -ThisBuild / organization := "dev.atedeg" +ThisBuild / organization := "dev.atedeg.mdm" ThisBuild / homepage := Some(url("https://github.com/atedeg/mdm")) ThisBuild / licenses := List("Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0")) ThisBuild / versionScheme := Some("early-semver") @@ -66,6 +66,7 @@ lazy val root = project name := "mdm", sonatypeCredentialHost := "s01.oss.sonatype.org", sonatypeRepository := "https://s01.oss.sonatype.org/service/local", + sonatypeProfileName := "dev.atedeg", ScalaUnidoc / unidoc / target := file("target/site"), Compile / doc / scalacOptions := Seq( "-project", From aab9ad1bb44042a669049b924fbf1a16bfc3f3c5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 1 Aug 2022 19:18:38 +0000 Subject: [PATCH 051/329] chore(release): 1.0.0-beta.1 [skip ci] # 1.0.0-beta.1 (2022-08-01) ### Features * complete domain modelling ([956703d](https://github.com/atedeg/mdm/commit/956703d9a4144d5e95dec5264ef4e179f0ee4626)) * complete quintals of milk estimation ([34aa275](https://github.com/atedeg/mdm/commit/34aa275fb14bba2a3264679d4f7b72f67e592fbb)) * define first batch of ubiquitous language ([fa83eee](https://github.com/atedeg/mdm/commit/fa83eeeec89b037a7b3c20ef0e71828bd2039140)) * define incoming and outgoing events ([a2e0174](https://github.com/atedeg/mdm/commit/a2e01748f7a94c7d2117a44e10c7ca048f3690bb)) * definition of domain events for milk-planning ([242a875](https://github.com/atedeg/mdm/commit/242a875a1684d22ace87ba51d04b4a5b1589c790)) * first basic implementation for estimate quintals of milk ([697e87d](https://github.com/atedeg/mdm/commit/697e87df07da321d200e29d499396b362743f9cf)) * first definition of domain actions ([fcdcbbf](https://github.com/atedeg/mdm/commit/fcdcbbf16818d8103e595fd9f44476f77e2fa0be)) * implement logic for calculate milk needed for products ([d914fe7](https://github.com/atedeg/mdm/commit/d914fe7bafa9211be938c4bd61d064bf8f9774e0)) --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..de65c725 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# 1.0.0-beta.1 (2022-08-01) + + +### Features + +* complete domain modelling ([956703d](https://github.com/atedeg/mdm/commit/956703d9a4144d5e95dec5264ef4e179f0ee4626)) +* complete quintals of milk estimation ([34aa275](https://github.com/atedeg/mdm/commit/34aa275fb14bba2a3264679d4f7b72f67e592fbb)) +* define first batch of ubiquitous language ([fa83eee](https://github.com/atedeg/mdm/commit/fa83eeeec89b037a7b3c20ef0e71828bd2039140)) +* define incoming and outgoing events ([a2e0174](https://github.com/atedeg/mdm/commit/a2e01748f7a94c7d2117a44e10c7ca048f3690bb)) +* definition of domain events for milk-planning ([242a875](https://github.com/atedeg/mdm/commit/242a875a1684d22ace87ba51d04b4a5b1589c790)) +* first basic implementation for estimate quintals of milk ([697e87d](https://github.com/atedeg/mdm/commit/697e87df07da321d200e29d499396b362743f9cf)) +* first definition of domain actions ([fcdcbbf](https://github.com/atedeg/mdm/commit/fcdcbbf16818d8103e595fd9f44476f77e2fa0be)) +* implement logic for calculate milk needed for products ([d914fe7](https://github.com/atedeg/mdm/commit/d914fe7bafa9211be938c4bd61d064bf8f9774e0)) From cfb36b84db795a42dd80c914008915931897b658 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 1 Aug 2022 15:23:23 +0200 Subject: [PATCH 052/329] chore: exclude metals file and vscode folder --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 6f07df45..fbb03eeb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,16 @@ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 .idea/ +.bloop/ .bsp/ +# metals +project/metals.sbt +.metals + +# vs code +.vscode + # CMake cmake-build-*/ @@ -33,6 +41,8 @@ lib_managed/ src_managed/ project/boot/ project/plugins/project/ +project/project/ +project/target/ .history .cache .lib/ From 7ad084c401ce69c5c72c0c2ba6f37e9b8b51b140 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Wed, 20 Jul 2022 17:49:30 +0200 Subject: [PATCH 053/329] chore: add semanticdb to gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fbb03eeb..88cde666 100644 --- a/.gitignore +++ b/.gitignore @@ -55,5 +55,5 @@ project/target/ hs_err_pid* node_modules/ - _includes/ +*.semanticdb From 607f2d6eeef2ca287572f2d7c479d8cc666d05df Mon Sep 17 00:00:00 2001 From: ndido98 Date: Wed, 20 Jul 2022 19:01:55 +0200 Subject: [PATCH 054/329] build: add stocking project --- build.sbt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 9c002358..e26ce3af 100644 --- a/build.sbt +++ b/build.sbt @@ -86,7 +86,11 @@ lazy val root = project ), publish / skip := true, ) - .aggregate(utils, `milk-planning`) + .aggregate( + stocking, + utils, + `milk-planning` + ) lazy val utils = project .in(file("utils")) @@ -104,3 +108,7 @@ lazy val `products-shared-kernel` = project .in(file("products-shared-kernel")) .settings(commonSettings) .dependsOn(utils) + +lazy val stocking = project + .in(file("stocking")) + .settings(commonSettings) From 438725f640cee66c04f37b1e947ab1a52df5e118 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Wed, 20 Jul 2022 19:02:44 +0200 Subject: [PATCH 055/329] docs: add bounded context documentation --- docs/_docs/stocking.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/_docs/stocking.md diff --git a/docs/_docs/stocking.md b/docs/_docs/stocking.md new file mode 100644 index 00000000..47aa8d47 --- /dev/null +++ b/docs/_docs/stocking.md @@ -0,0 +1,13 @@ +--- +title: Stocking +--- + +# Stocking + +After a batch of cheeses has aged for the required amount of time, +one of them is selected to perform quality assurance. +Its result could be either positive or negative. +The former results in the cheeses being wrapped, labelled and put in the refrigeration room; +the latter results in the entire batch being discarded. + +To label a cheese the worker has to weigh it, and an automated system will print an appropriate label. From 028ee56eff798fa7a6f63eabdb19e77c7ace4b79 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Wed, 20 Jul 2022 19:02:58 +0200 Subject: [PATCH 056/329] feat: add stocking bounded context --- .../dev/atedeg/mdm/stocking/Actions.scala | 21 +++++++ .../dev/atedeg/mdm/stocking/Errors.scala | 7 +++ .../dev/atedeg/mdm/stocking/Events.scala | 19 +++++++ .../scala/dev/atedeg/mdm/stocking/Types.scala | 57 +++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala new file mode 100644 index 00000000..cf279747 --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -0,0 +1,21 @@ +package dev.atedeg.mdm.stocking + +/** + * Approves a batch after quality assurance. + */ +def approveBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Passed = ??? + +/** + * Rejects a batch after quality assurance. + */ +def rejectBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Failed = ??? + +/** + * Labels a product given the [[Batch batch]] it comes from and its [[WeightInGrams actual weight]] + * as given by the scale. + * + * @note it can raise a [[WeightNotInRange weight-not-in-range]] error. + * @note it emits a [[StockedProduct "stocked product"]] event. + */ +// TODO: can fail (weight not in range), can emit StockedProduct +def labelProduct(batch: Batch, actualWeight: WeightInGrams): LabelledProduct = ??? diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala new file mode 100644 index 00000000..3217c4c3 --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala @@ -0,0 +1,7 @@ +package dev.atedeg.mdm.stocking + +/** + * An error raised by the [[labelProduct() labelling action]] if the actual weight is too far + * from the expected weights. + */ +final case class WeightNotInRange(expectedWeight: WeightInGrams, actualWeight: WeightInGrams) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala new file mode 100644 index 00000000..ecd13085 --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala @@ -0,0 +1,19 @@ +package dev.atedeg.mdm.stocking + +/** + * The events that may be produced by the bounded context. + */ +enum OutgoingEvent: + /** + * Fired when a label is printed for a [[Product product]], which is then stocked. + */ + case ProductStocked(labelledProduct: LabelledProduct) + +/** + * The events that have to be handled by the bounded context. + */ +enum IncomingEvent: + /** + * Received when a [[Batch batch]] is ready for quality assurance. + */ + case BatchReadyForQualityAssurance(batch: BatchID) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala new file mode 100644 index 00000000..b1820524 --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -0,0 +1,57 @@ +package dev.atedeg.mdm.stocking + +import java.util.UUID +import java.time.LocalDateTime + +import cats.data.NonEmptyList + +// FIXME: shared kernel +type Product = Int +type CheeseType = Int +type Quantity = Int +type PositiveDecimal = Double + +/** + * A batch of products of a certain [[CheeseType type]], uniquely identified by an [[BatchID ID]], + * which hasn't been quality assured. + */ +enum Batch: + /** + * An aging batch that will become ready for quality assurance at the given date and time. + */ + case Aging(id: BatchID, cheeseType: CheeseType, readyFrom: LocalDateTime) + + /** + * A batch that is ready for quality assurance. + */ + case ReadyForQualityAssurance(batchID: BatchID, cheeseType: CheeseType) + +/** + * A batch of products of a certain [[CheeseType type]] uniquely identified by an [[BatchID ID]], + * which has undergone quality assurance. + */ +enum QualityAssuredBatch: + /** + * A batch which passed quality assurance. + */ + case Passed(id: BatchID, cheeseType: CheeseType) + + /** + * A batch which failed quality assurance. + */ + case Failed(id: BatchID, cheeseType: CheeseType) + +/** + * Uniquely identifies a [[Batch batch]]. + */ +final case class BatchID(id: UUID) + +/** + * A [[CheeseType cheese type]] with its respective [[Quantity quantity]] and the [[BatchID ID of the batch]] it belongs to. + */ +final case class LabelledProduct(cheeseType: CheeseType, quantity: Quantity, batchID: BatchID) + +/** + * A weight in grams reported by a scale. + */ +final case class WeightInGrams(grams: PositiveDecimal) From 3821db050306b1a48aa36de98b21afa11fa893d7 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Mon, 25 Jul 2022 18:47:54 +0200 Subject: [PATCH 057/329] build: add dependency on utils subproject --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index e26ce3af..5a596921 100644 --- a/build.sbt +++ b/build.sbt @@ -112,3 +112,4 @@ lazy val `products-shared-kernel` = project lazy val stocking = project .in(file("stocking")) .settings(commonSettings) + .dependsOn(utils) From 68692067f705eb16db9a3dd496f8c87fdfffe752 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Mon, 25 Jul 2022 18:48:38 +0200 Subject: [PATCH 058/329] chore: change batch ID argname for consistency --- stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index b1820524..97d63947 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -1,7 +1,7 @@ package dev.atedeg.mdm.stocking -import java.util.UUID import java.time.LocalDateTime +import java.util.UUID import cats.data.NonEmptyList @@ -24,7 +24,7 @@ enum Batch: /** * A batch that is ready for quality assurance. */ - case ReadyForQualityAssurance(batchID: BatchID, cheeseType: CheeseType) + case ReadyForQualityAssurance(id: BatchID, cheeseType: CheeseType) /** * A batch of products of a certain [[CheeseType type]] uniquely identified by an [[BatchID ID]], From 7cbe9c0fa2a6dc7b2ecdfbba9a32d16bdcb17695 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Mon, 25 Jul 2022 18:49:20 +0200 Subject: [PATCH 059/329] feat: implement stocking actions --- .../dev/atedeg/mdm/stocking/Actions.scala | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index cf279747..0bf1f243 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -1,21 +1,51 @@ package dev.atedeg.mdm.stocking +import cats.Monad +import cats.data.NonEmptySet + +import dev.atedeg.mdm.stocking.OutgoingEvent.ProductStocked +import dev.atedeg.mdm.utils.* + /** * Approves a batch after quality assurance. */ -def approveBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Passed = ??? +def approveBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Passed = + QualityAssuredBatch.Passed(batch.id, batch.cheeseType) /** * Rejects a batch after quality assurance. */ -def rejectBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Failed = ??? +def rejectBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Failed = + QualityAssuredBatch.Failed(batch.id, batch.cheeseType) /** - * Labels a product given the [[Batch batch]] it comes from and its [[WeightInGrams actual weight]] + * Labels a product given the [[QualityAssuredBatch.Passed batch]] it comes from and its [[WeightInGrams actual weight]] * as given by the scale. * * @note it can raise a [[WeightNotInRange weight-not-in-range]] error. - * @note it emits a [[StockedProduct "stocked product"]] event. + * @note it emits a [[ProductStocked "product stocked"]] event. */ -// TODO: can fail (weight not in range), can emit StockedProduct -def labelProduct(batch: Batch, actualWeight: WeightInGrams): LabelledProduct = ??? +def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: Emits[ProductStocked]]( + batch: QualityAssuredBatch.Passed, + actualWeight: WeightInGrams, +): M[LabelledProduct] = + val weights = NonEmptySet.of(WeightInGrams(1), WeightInGrams(2), WeightInGrams(3)) + val nearestWeight = getNearestWeight(weights)(actualWeight) + val labelledProduct = LabelledProduct(batch.cheeseType, 1, batch.id) + isWeightInRange(weights, 0.05)(actualWeight) + .otherwiseRaise(WeightNotInRange(nearestWeight, actualWeight)) + .andThen(emit(ProductStocked(labelledProduct))) + .thenReturn(labelledProduct) + +private def getNearestWeight(expectedWeights: NonEmptySet[WeightInGrams])(actualWeight: WeightInGrams): WeightInGrams = + expectedWeights.reduceLeft((acc, elem) => + if math.abs(actualWeight.grams - acc.grams) < math.abs(actualWeight.grams - elem.grams) then acc else elem, + ) + +private def isWeightInRange( + expectedWeights: NonEmptySet[WeightInGrams], + tolerancePercentage: Double, +)(actualWeight: WeightInGrams): Boolean = + val nearestWeight = getNearestWeight(expectedWeights)(actualWeight) + actualWeight.grams >= nearestWeight.grams * (1 - tolerancePercentage) + && actualWeight.grams <= nearestWeight.grams * (1 + tolerancePercentage) From 4dece0a979f2fddab1bddfa696dabc3b726d6c7e Mon Sep 17 00:00:00 2001 From: ndido98 Date: Tue, 26 Jul 2022 12:29:31 +0200 Subject: [PATCH 060/329] refactor: rewrite labelProduct --- .../dev/atedeg/mdm/stocking/Actions.scala | 27 +++++++------------ .../scala/dev/atedeg/mdm/utils/Range.scala | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/Range.scala diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 0bf1f243..da8e99ab 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -1,7 +1,7 @@ package dev.atedeg.mdm.stocking import cats.Monad -import cats.data.NonEmptySet +import cats.data.{ NonEmptyList, NonEmptySet } import dev.atedeg.mdm.stocking.OutgoingEvent.ProductStocked import dev.atedeg.mdm.utils.* @@ -25,27 +25,20 @@ def rejectBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Fail * @note it can raise a [[WeightNotInRange weight-not-in-range]] error. * @note it emits a [[ProductStocked "product stocked"]] event. */ -def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: Emits[ProductStocked]]( +def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: CanEmit[ProductStocked]]( batch: QualityAssuredBatch.Passed, actualWeight: WeightInGrams, ): M[LabelledProduct] = - val weights = NonEmptySet.of(WeightInGrams(1), WeightInGrams(2), WeightInGrams(3)) - val nearestWeight = getNearestWeight(weights)(actualWeight) + val weights = NonEmptyList.of(WeightInGrams(1), WeightInGrams(2), WeightInGrams(3)) + val candidate = nearestWeight(weights)(actualWeight) val labelledProduct = LabelledProduct(batch.cheeseType, 1, batch.id) - isWeightInRange(weights, 0.05)(actualWeight) - .otherwiseRaise(WeightNotInRange(nearestWeight, actualWeight)) - .andThen(emit(ProductStocked(labelledProduct))) + actualWeight.grams + .isInRange(candidate.grams +- 5.percent) + .otherwiseRaise(WeightNotInRange(candidate, actualWeight)) + .andThen(emit(ProductStocked(labelledProduct): ProductStocked)) .thenReturn(labelledProduct) -private def getNearestWeight(expectedWeights: NonEmptySet[WeightInGrams])(actualWeight: WeightInGrams): WeightInGrams = +private def nearestWeight(expectedWeights: NonEmptyList[WeightInGrams])(actualWeight: WeightInGrams): WeightInGrams = expectedWeights.reduceLeft((acc, elem) => - if math.abs(actualWeight.grams - acc.grams) < math.abs(actualWeight.grams - elem.grams) then acc else elem, + if math.abs(actualWeight.grams - elem.grams) < math.abs(actualWeight.grams - acc.grams) then elem else acc, ) - -private def isWeightInRange( - expectedWeights: NonEmptySet[WeightInGrams], - tolerancePercentage: Double, -)(actualWeight: WeightInGrams): Boolean = - val nearestWeight = getNearestWeight(expectedWeights)(actualWeight) - actualWeight.grams >= nearestWeight.grams * (1 - tolerancePercentage) - && actualWeight.grams <= nearestWeight.grams * (1 + tolerancePercentage) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Range.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/Range.scala new file mode 100644 index 00000000..0692182b --- /dev/null +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/Range.scala @@ -0,0 +1,27 @@ +package dev.atedeg.mdm.utils + +import scala.annotation.targetName +import scala.math.Numeric.Implicits.* +import scala.math.Ordering.Implicits.* + +final case class Range[T: Ordering] private[utils] (min: T, max: T) + +final case class RangePercentage private[utils] (percentage: Double) + +extension [T: Numeric](x: T) + + def percent: RangePercentage = RangePercentage(x.toDouble / 100) + + @targetName("plusMinus") + @SuppressWarnings(Array("org.wartremover.warts.Overloading")) + def +-(y: T): Range[T] = Range(x - y, x + y) + + @targetName("plusMinusPercent") + @SuppressWarnings(Array("org.wartremover.warts.Overloading")) + def +-(y: RangePercentage): Range[Double] = + Range(x.toDouble * (1.0 - y.percentage), x.toDouble * (1.0 + y.percentage)) + +extension [T: Ordering](x: T) + + def isInRange(range: Range[T]): Boolean = + x >= range.min && x <= range.max From 3b45d88111bb78574f643e5d37d23f6335edcc54 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Tue, 26 Jul 2022 12:29:57 +0200 Subject: [PATCH 061/329] test: add tests --- .../scala/dev/atedeg/mdm/stocking/Tests.scala | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala new file mode 100644 index 00000000..d4919c3a --- /dev/null +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -0,0 +1,58 @@ +package dev.atedeg.mdm.stocking + +import java.util.UUID + +import cats.data.{ Writer, WriterT } +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +type Error[E] = [A] =>> Either[E, A] +type EmitterT[E] = [M[_]] =>> [A] =>> WriterT[M, List[E], A] + +trait Mocks { + val batchID: BatchID = BatchID(UUID.randomUUID()) + val cheeseType: CheeseType = 0 + val readyForQA: Batch.ReadyForQualityAssurance = Batch.ReadyForQualityAssurance(batchID, cheeseType) +} + +class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { + + Feature("Quality assurance") { + Scenario("An operator marks a batch as passing quality assurance") { + Given("a ready-for-QA batch") + When("the operator passes the batch") + val passed = approveBatch(readyForQA) + Then("the batch should be marked as QA-passed") + passed shouldBe a[QualityAssuredBatch.Passed] + passed.id shouldEqual batchID + passed.cheeseType shouldEqual cheeseType + } + Scenario("An operator marks a batch as failing quality assurance") { + Given("a ready-for-QA batch") + When("the operator fails the batch") + val failed = rejectBatch(readyForQA) + Then("the batch should be marked as QA-failed") + failed shouldBe a[QualityAssuredBatch.Failed] + failed.id shouldEqual batchID + failed.cheeseType shouldEqual cheeseType + } + Scenario("An operator tries to print a label for a cheese within weight range from a batch") { + Given("a batch") + val passed = approveBatch(readyForQA) + val cheeseTypeWeight = WeightInGrams(100) + When("the operator prints a label for a product within weight range") + val labelledMonad: EmitterT[OutgoingEvent.ProductStocked][Error[WeightNotInRange]][LabelledProduct] = + labelProduct(passed, cheeseTypeWeight) + Then("the label should be printed with the correct information") + val result: Either[WeightNotInRange, (List[OutgoingEvent.ProductStocked], LabelledProduct)] = labelledMonad.run + And("an event should be emitted") + } + Scenario("An operator tries to print a label for a cheese outside weight range from a batch") { + Given("a batch") + When("the operator prints a label for a cheese outside weight range") + Then("the label should not be printed") + And("an error should be raised") + } + } +} From 365966f67beeda1778e5dd1cc314c6a40e54c775 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Tue, 26 Jul 2022 14:59:42 +0200 Subject: [PATCH 062/329] test: complete tests --- .../dev/atedeg/mdm/stocking/Actions.scala | 1 + .../scala/dev/atedeg/mdm/stocking/Tests.scala | 23 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index da8e99ab..879afbed 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -5,6 +5,7 @@ import cats.data.{ NonEmptyList, NonEmptySet } import dev.atedeg.mdm.stocking.OutgoingEvent.ProductStocked import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.monads.* /** * Approves a batch after quality assurance. diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index d4919c3a..7f83d71d 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -7,8 +7,8 @@ import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers -type Error[E] = [A] =>> Either[E, A] -type EmitterT[E] = [M[_]] =>> [A] =>> WriterT[M, List[E], A] +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.monads.* trait Mocks { val batchID: BatchID = BatchID(UUID.randomUUID()) @@ -40,19 +40,32 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Scenario("An operator tries to print a label for a cheese within weight range from a batch") { Given("a batch") val passed = approveBatch(readyForQA) - val cheeseTypeWeight = WeightInGrams(100) + val cheeseTypeWeight = WeightInGrams(1) When("the operator prints a label for a product within weight range") - val labelledMonad: EmitterT[OutgoingEvent.ProductStocked][Error[WeightNotInRange]][LabelledProduct] = + val labelledMonad: Action[WeightNotInRange, OutgoingEvent.ProductStocked, LabelledProduct] = labelProduct(passed, cheeseTypeWeight) Then("the label should be printed with the correct information") - val result: Either[WeightNotInRange, (List[OutgoingEvent.ProductStocked], LabelledProduct)] = labelledMonad.run + val (events, result) = labelledMonad.execute + val expectedLabelledProduct = LabelledProduct(passed.cheeseType, 1, passed.id) + result.isRight shouldBe true + result.toOption shouldEqual Some(expectedLabelledProduct) And("an event should be emitted") + events should contain(OutgoingEvent.ProductStocked(expectedLabelledProduct)) } Scenario("An operator tries to print a label for a cheese outside weight range from a batch") { Given("a batch") + val passed = approveBatch(readyForQA) + val cheeseTypeWeight = WeightInGrams(100) When("the operator prints a label for a cheese outside weight range") + val labelledMonad: Action[WeightNotInRange, OutgoingEvent.ProductStocked, LabelledProduct] = + labelProduct(passed, cheeseTypeWeight) Then("the label should not be printed") + val (events, result) = labelledMonad.execute + result.isLeft shouldBe true And("an error should be raised") + result.left.toOption shouldEqual Some(WeightNotInRange(WeightInGrams(3), WeightInGrams(100))) + And("no events should be emitted") + events shouldBe empty } } } From 4baf6fa64b0a62db4cd13a1f0d67c4a587e7594c Mon Sep 17 00:00:00 2001 From: ndido98 Date: Tue, 26 Jul 2022 16:51:41 +0200 Subject: [PATCH 063/329] chore: address review comments --- .../dev/atedeg/mdm/stocking/Actions.scala | 16 ++++++------ .../scala/dev/atedeg/mdm/stocking/Types.scala | 2 ++ .../scala/dev/atedeg/mdm/stocking/Tests.scala | 25 +++++++++---------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 879afbed..88102eca 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -2,8 +2,10 @@ package dev.atedeg.mdm.stocking import cats.Monad import cats.data.{ NonEmptyList, NonEmptySet } +import cats.implicits.toReducibleOps -import dev.atedeg.mdm.stocking.OutgoingEvent.ProductStocked +import dev.atedeg.mdm.stocking.OutgoingEvent.* +import dev.atedeg.mdm.stocking.grams import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.monads.* @@ -22,15 +24,12 @@ def rejectBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Fail /** * Labels a product given the [[QualityAssuredBatch.Passed batch]] it comes from and its [[WeightInGrams actual weight]] * as given by the scale. - * - * @note it can raise a [[WeightNotInRange weight-not-in-range]] error. - * @note it emits a [[ProductStocked "product stocked"]] event. */ def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: CanEmit[ProductStocked]]( batch: QualityAssuredBatch.Passed, actualWeight: WeightInGrams, ): M[LabelledProduct] = - val weights = NonEmptyList.of(WeightInGrams(1), WeightInGrams(2), WeightInGrams(3)) + val weights = NonEmptyList.of(1.grams, 2.grams, 3.grams) // FIXME: get available weights val candidate = nearestWeight(weights)(actualWeight) val labelledProduct = LabelledProduct(batch.cheeseType, 1, batch.id) actualWeight.grams @@ -39,7 +38,6 @@ def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: CanEmit[ProductStocked .andThen(emit(ProductStocked(labelledProduct): ProductStocked)) .thenReturn(labelledProduct) -private def nearestWeight(expectedWeights: NonEmptyList[WeightInGrams])(actualWeight: WeightInGrams): WeightInGrams = - expectedWeights.reduceLeft((acc, elem) => - if math.abs(actualWeight.grams - elem.grams) < math.abs(actualWeight.grams - acc.grams) then elem else acc, - ) +private def nearestWeight(weights: NonEmptyList[WeightInGrams])(actualWeight: WeightInGrams): WeightInGrams = + def distanceFromActualWeight(weight: WeightInGrams) = math.abs(weight.grams - actualWeight.grams) + weights.minimumBy(distanceFromActualWeight) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index 97d63947..afccf79c 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -55,3 +55,5 @@ final case class LabelledProduct(cheeseType: CheeseType, quantity: Quantity, bat * A weight in grams reported by a scale. */ final case class WeightInGrams(grams: PositiveDecimal) + +extension (n: PositiveDecimal) def grams: WeightInGrams = WeightInGrams(n) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index 7f83d71d..cfcf439a 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -3,10 +3,13 @@ package dev.atedeg.mdm.stocking import java.util.UUID import cats.data.{ Writer, WriterT } +import org.scalatest.EitherValues.* import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers +import dev.atedeg.mdm.stocking.OutgoingEvent.* +import dev.atedeg.mdm.stocking.grams import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.monads.* @@ -40,30 +43,26 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Scenario("An operator tries to print a label for a cheese within weight range from a batch") { Given("a batch") val passed = approveBatch(readyForQA) - val cheeseTypeWeight = WeightInGrams(1) + val weight = 1.grams When("the operator prints a label for a product within weight range") - val labelledMonad: Action[WeightNotInRange, OutgoingEvent.ProductStocked, LabelledProduct] = - labelProduct(passed, cheeseTypeWeight) + val labelAction: Action[WeightNotInRange, ProductStocked, LabelledProduct] = labelProduct(passed, weight) Then("the label should be printed with the correct information") - val (events, result) = labelledMonad.execute + val (events, result) = labelAction.execute val expectedLabelledProduct = LabelledProduct(passed.cheeseType, 1, passed.id) - result.isRight shouldBe true - result.toOption shouldEqual Some(expectedLabelledProduct) + result.value shouldEqual expectedLabelledProduct And("an event should be emitted") - events should contain(OutgoingEvent.ProductStocked(expectedLabelledProduct)) + events should contain(ProductStocked(expectedLabelledProduct)) } Scenario("An operator tries to print a label for a cheese outside weight range from a batch") { Given("a batch") val passed = approveBatch(readyForQA) - val cheeseTypeWeight = WeightInGrams(100) + val weight = 100.grams When("the operator prints a label for a cheese outside weight range") - val labelledMonad: Action[WeightNotInRange, OutgoingEvent.ProductStocked, LabelledProduct] = - labelProduct(passed, cheeseTypeWeight) + val labelAction: Action[WeightNotInRange, ProductStocked, LabelledProduct] = labelProduct(passed, weight) Then("the label should not be printed") - val (events, result) = labelledMonad.execute - result.isLeft shouldBe true + val (events, result) = labelAction.execute And("an error should be raised") - result.left.toOption shouldEqual Some(WeightNotInRange(WeightInGrams(3), WeightInGrams(100))) + result.left.value shouldEqual WeightNotInRange(3.grams, 100.grams) And("no events should be emitted") events shouldBe empty } From f621078df2178a7c5c995dacacd949e34f722784 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Tue, 26 Jul 2022 16:52:21 +0200 Subject: [PATCH 064/329] chore: remove no longer useful warning --- utils/src/main/scala/dev/atedeg/mdm/utils/Range.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Range.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/Range.scala index 0692182b..dd204c66 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/Range.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/Range.scala @@ -13,11 +13,9 @@ extension [T: Numeric](x: T) def percent: RangePercentage = RangePercentage(x.toDouble / 100) @targetName("plusMinus") - @SuppressWarnings(Array("org.wartremover.warts.Overloading")) def +-(y: T): Range[T] = Range(x - y, x + y) @targetName("plusMinusPercent") - @SuppressWarnings(Array("org.wartremover.warts.Overloading")) def +-(y: RangePercentage): Range[Double] = Range(x.toDouble * (1.0 - y.percentage), x.toDouble * (1.0 + y.percentage)) From 55fc9185e46dd479906e9541a8c8b4b19f9a9993 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 13:08:54 +0200 Subject: [PATCH 065/329] build: remove strict equality --- build.sbt | 4 +- .../dev/atedeg/mdm/stocking/Actions.scala | 47 ++++++++++++++----- .../dev/atedeg/mdm/stocking/Errors.scala | 18 +++++-- .../dev/atedeg/mdm/stocking/Events.scala | 3 ++ .../scala/dev/atedeg/mdm/stocking/Types.scala | 31 +++++++++--- 5 files changed, 80 insertions(+), 23 deletions(-) diff --git a/build.sbt b/build.sbt index 5a596921..27f4399a 100644 --- a/build.sbt +++ b/build.sbt @@ -89,7 +89,8 @@ lazy val root = project .aggregate( stocking, utils, - `milk-planning` + `milk-planning`, + `products-shared-kernel`, ) lazy val utils = project @@ -113,3 +114,4 @@ lazy val stocking = project .in(file("stocking")) .settings(commonSettings) .dependsOn(utils) + .dependsOn(`products-shared-kernel`) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 88102eca..113e8a7b 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -3,12 +3,36 @@ package dev.atedeg.mdm.stocking import cats.Monad import cats.data.{ NonEmptyList, NonEmptySet } import cats.implicits.toReducibleOps +import eu.timepit.refined.auto.autoUnwrap +import dev.atedeg.mdm.products.* +import dev.atedeg.mdm.stocking.Errors.* import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.stocking.grams import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.* +/** + * Gets how many products are missing from the stock, given the desired stock. + */ +def getMissingCountFromProductStock( + availableStock: AvailableStock, + desiredStock: DesiredStock, +)(product: Product): AvailableQuantity = AvailableQuantity( + availableStock(product).n - desiredStock(product).n.toNonNegative, +) + +/** + * Removes the given quantity of a certain product from the stock, giving the new current stock. + */ +def removeFromStock[M[_]: Monad: CanRaise[NotEnoughStock]]( + stock: AvailableStock, +)(product: Product, quantity: AvailableQuantity): M[AvailableStock] = + (stock(product).n >= quantity.n) + .otherwiseRaise(NotEnoughStock(product, quantity, stock(product)): NotEnoughStock) + .thenReturn(stock + (product -> AvailableQuantity(stock(product).n - quantity.n))) + /** * Approves a batch after quality assurance. */ @@ -27,17 +51,16 @@ def rejectBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Fail */ def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: CanEmit[ProductStocked]]( batch: QualityAssuredBatch.Passed, - actualWeight: WeightInGrams, + actualWeight: Grams[PositiveNumber], ): M[LabelledProduct] = - val weights = NonEmptyList.of(1.grams, 2.grams, 3.grams) // FIXME: get available weights - val candidate = nearestWeight(weights)(actualWeight) - val labelledProduct = LabelledProduct(batch.cheeseType, 1, batch.id) - actualWeight.grams - .isInRange(candidate.grams +- 5.percent) - .otherwiseRaise(WeightNotInRange(candidate, actualWeight)) - .andThen(emit(ProductStocked(labelledProduct): ProductStocked)) - .thenReturn(labelledProduct) - -private def nearestWeight(weights: NonEmptyList[WeightInGrams])(actualWeight: WeightInGrams): WeightInGrams = - def distanceFromActualWeight(weight: WeightInGrams) = math.abs(weight.grams - actualWeight.grams) + batch.cheeseType match + case CheeseType.Squacquerone => + val candidate = nearestWeight(allSquacqueroneWeights)(actualWeight) + ??? + case _ => ??? + +private def nearestWeight(weights: NonEmptyList[Grams[Int]])( + actualWeight: Grams[Int], +): Grams[Int] = + def distanceFromActualWeight(weight: Grams[Int]) = math.abs(weight.n - actualWeight.n) weights.minimumBy(distanceFromActualWeight) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala index 3217c4c3..25d67a12 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala @@ -1,7 +1,19 @@ package dev.atedeg.mdm.stocking +import dev.atedeg.mdm.products.Product + /** - * An error raised by the [[labelProduct() labelling action]] if the actual weight is too far - * from the expected weights. + * The errors that have to be handled by the bounded context. */ -final case class WeightNotInRange(expectedWeight: WeightInGrams, actualWeight: WeightInGrams) +enum Errors: + /** + * An error raised by the [[labelProduct() labelling action]] if the actual weight is too far + * from the expected weights. + */ + case WeightNotInRange(expectedWeight: WeightInGrams, actualWeight: WeightInGrams) + + /** + * An error raised by the [[removeFromStock() removal from stock action]] if the quantity to be removed from stock + * exceeds the available one + */ + case NotEnoughStock(product: Product, triedQuantity: AvailableQuantity, actualQuantity: AvailableQuantity) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala index ecd13085..88f5e118 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala @@ -1,5 +1,7 @@ package dev.atedeg.mdm.stocking +import dev.atedeg.mdm.products.Product + /** * The events that may be produced by the bounded context. */ @@ -17,3 +19,4 @@ enum IncomingEvent: * Received when a [[Batch batch]] is ready for quality assurance. */ case BatchReadyForQualityAssurance(batch: BatchID) + case ProductRemovedFromStock(quantity: AvailableQuantity, product: Product) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index afccf79c..2068e8a8 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -5,11 +5,28 @@ import java.util.UUID import cats.data.NonEmptyList -// FIXME: shared kernel -type Product = Int -type CheeseType = Int -type Quantity = Int -type PositiveDecimal = Double +import dev.atedeg.mdm.products.* +import dev.atedeg.mdm.utils.* + +/** + * The available quantity of a certain product. + */ +final case class AvailableQuantity(n: NonNegativeNumber) + +/** + * The desired quantity of a certain product. + */ +final case class DesiredQuantity(n: PositiveNumber) + +/** + * The available quantity of a certain product. + */ +type AvailableStock = Map[Product, AvailableQuantity] + +/** + * The desired quantity of a certain product. + */ +type DesiredStock = Map[Product, DesiredQuantity] /** * A batch of products of a certain [[CheeseType type]], uniquely identified by an [[BatchID ID]], @@ -47,9 +64,9 @@ enum QualityAssuredBatch: final case class BatchID(id: UUID) /** - * A [[CheeseType cheese type]] with its respective [[Quantity quantity]] and the [[BatchID ID of the batch]] it belongs to. + * A [[Product product]] with its respective [[Quantity quantity]] and the [[BatchID ID of the batch]] it belongs to. */ -final case class LabelledProduct(cheeseType: CheeseType, quantity: Quantity, batchID: BatchID) +final case class LabelledProduct(cheeseType: Product, quantity: AvailableQuantity, batchID: BatchID) /** * A weight in grams reported by a scale. From 460d6ebbda063f771e5dbf715e72e7081487dc2a Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 13:12:09 +0200 Subject: [PATCH 066/329] chore(products): add .grams extension method --- .../src/main/scala/dev/atedeg/mdm/products/Products.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala index b90beaaa..4fc3af34 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala @@ -1,8 +1,6 @@ package dev.atedeg.mdm.products -import eu.timepit.refined.api.{ Refined, Validate } -import eu.timepit.refined.predicates.all.Positive -import eu.timepit.refined.refineV +import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given @@ -12,6 +10,8 @@ import dev.atedeg.mdm.utils.given */ final case class Grams(n: PositiveNumber) +extension (n: PositiveNumber) def grams: Grams = Grams(n) + /** * A type of cheese. */ From 038da8b78adf337c0bc34ad06bb224abceaed0a7 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 13:12:33 +0200 Subject: [PATCH 067/329] fix(products): change macro to generate list of weights per cheese type --- .../src/main/scala/dev/atedeg/mdm/products/Utils.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala index 304e0426..8a392909 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala @@ -3,23 +3,26 @@ package dev.atedeg.mdm.products import scala.compiletime.* import cats.data.NonEmptyList +import dev.atedeg.mdm.utils.{PositiveNumber, coerce} import eu.timepit.refined.predicates.all.Positive -import dev.atedeg.mdm.utils.{ coerce, PositiveNumber } +import scala.compiletime.* type OneOf[T <: Tuple] = T match case (t *: EmptyTuple) => t case (t *: ts) => t | OneOf[ts] +@SuppressWarnings(Array("scalafix:DisableSyntax.asInstanceOf")) inline def all[T <: Tuple]: NonEmptyList[OneOf[T]] = inline erasedValue[T] match case _: (n *: EmptyTuple) => val v = checkInt[n](constValue[n]) - NonEmptyList.one(toGrams(v)).asInstanceOf[NonEmptyList[OneOf[T]]] + NonEmptyList.one(v).asInstanceOf[NonEmptyList[OneOf[T]]] case _: (n *: gs) => val v = checkInt[n](constValue[n]) - NonEmptyList(toGrams(v), all[gs].toList).asInstanceOf[NonEmptyList[OneOf[T]]] + NonEmptyList(v, all[gs].toList).asInstanceOf[NonEmptyList[OneOf[T]]] case _ => compiletime.error("Cannot work on a tuple with elements that are not Grams") +@SuppressWarnings(Array("scalafix:DisableSyntax.asInstanceOf")) inline private def checkInt[T](inline n: T): Int = inline erasedValue[T] match case _: Int => n.asInstanceOf[Int] case _ => compiletime.error(codeOf(n) + " is not an int") From 099bc05fd440dc3c582e1ed849076b57c6ca70a9 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 13:13:19 +0200 Subject: [PATCH 068/329] feat(stocking): integrate stocking bounded context with shared kernel --- .../dev/atedeg/mdm/stocking/Actions.scala | 49 +++++++++----- .../dev/atedeg/mdm/stocking/Errors.scala | 6 +- .../scala/dev/atedeg/mdm/stocking/Types.scala | 12 ++-- .../scala/dev/atedeg/mdm/stocking/Tests.scala | 66 ++++++++++++++++--- 4 files changed, 97 insertions(+), 36 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 113e8a7b..1f4cae04 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -2,13 +2,11 @@ package dev.atedeg.mdm.stocking import cats.Monad import cats.data.{ NonEmptyList, NonEmptySet } -import cats.implicits.toReducibleOps -import eu.timepit.refined.auto.autoUnwrap +import cats.syntax.all.* import dev.atedeg.mdm.products.* import dev.atedeg.mdm.stocking.Errors.* import dev.atedeg.mdm.stocking.OutgoingEvent.* -import dev.atedeg.mdm.stocking.grams import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.* @@ -19,19 +17,19 @@ import dev.atedeg.mdm.utils.monads.* def getMissingCountFromProductStock( availableStock: AvailableStock, desiredStock: DesiredStock, -)(product: Product): AvailableQuantity = AvailableQuantity( - availableStock(product).n - desiredStock(product).n.toNonNegative, -) +)(product: Product): MissingQuantity = + if availableStock(product).n >= desiredStock(product).n.toNonNegative then MissingQuantity(0) + else MissingQuantity(desiredStock(product).n.toNonNegative - availableStock(product).n) /** * Removes the given quantity of a certain product from the stock, giving the new current stock. */ def removeFromStock[M[_]: Monad: CanRaise[NotEnoughStock]]( stock: AvailableStock, -)(product: Product, quantity: AvailableQuantity): M[AvailableStock] = - (stock(product).n >= quantity.n) +)(product: Product, quantity: DesiredQuantity): M[AvailableStock] = + (stock(product).n > quantity.n.toNonNegative) .otherwiseRaise(NotEnoughStock(product, quantity, stock(product)): NotEnoughStock) - .thenReturn(stock + (product -> AvailableQuantity(stock(product).n - quantity.n))) + .thenReturn(stock + (product -> AvailableQuantity(stock(product).n - quantity.n.toNonNegative))) /** * Approves a batch after quality assurance. @@ -51,16 +49,31 @@ def rejectBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Fail */ def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: CanEmit[ProductStocked]]( batch: QualityAssuredBatch.Passed, - actualWeight: Grams[PositiveNumber], + actualWeight: Grams, ): M[LabelledProduct] = - batch.cheeseType match + val (candidate: Int, product: Product) = batch.cheeseType match case CheeseType.Squacquerone => - val candidate = nearestWeight(allSquacqueroneWeights)(actualWeight) - ??? - case _ => ??? + val w: SquacqueroneWeightInGrams = nearestWeight(allSquacqueroneWeights)(actualWeight) + (w, Product.Squacquerone(w)) + case CheeseType.Casatella => + val w: CasatellaWeightInGrams = nearestWeight(allCasatellaWeights)(actualWeight) + (w, Product.Casatella(w)) + case CheeseType.Ricotta => + val w: RicottaWeightInGrams = nearestWeight(allRicottaWeights)(actualWeight) + (w, Product.Ricotta(w)) + case CheeseType.Stracchino => + val w: StracchinoWeightInGrams = nearestWeight(allStracchinoWeights)(actualWeight) + (w, Product.Stracchino(w)) + case CheeseType.Caciotta => + val w: CasatellaWeightInGrams = nearestWeight(allCasatellaWeights)(actualWeight) + (w, Product.Casatella(w)) + val labelledProduct = LabelledProduct(product, AvailableQuantity(1), batch.id) + actualWeight.n.value.toDouble + .isInRange(candidate.toDouble +- 5.percent) + .otherwiseRaise(WeightNotInRange(Grams(coerce(candidate)), actualWeight): WeightNotInRange) + .andThen(emit(ProductStocked(labelledProduct): ProductStocked)) + .thenReturn(labelledProduct) -private def nearestWeight(weights: NonEmptyList[Grams[Int]])( - actualWeight: Grams[Int], -): Grams[Int] = - def distanceFromActualWeight(weight: Grams[Int]) = math.abs(weight.n - actualWeight.n) +private def nearestWeight[T <: Int](weights: NonEmptyList[T])(actualWeight: Grams): T = + def distanceFromActualWeight(weight: T) = weight distanceFrom actualWeight.n.value weights.minimumBy(distanceFromActualWeight) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala index 25d67a12..06a3b3c1 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala @@ -1,6 +1,6 @@ package dev.atedeg.mdm.stocking -import dev.atedeg.mdm.products.Product +import dev.atedeg.mdm.products.* /** * The errors that have to be handled by the bounded context. @@ -10,10 +10,10 @@ enum Errors: * An error raised by the [[labelProduct() labelling action]] if the actual weight is too far * from the expected weights. */ - case WeightNotInRange(expectedWeight: WeightInGrams, actualWeight: WeightInGrams) + case WeightNotInRange(expectedWeight: Grams, actualWeight: Grams) /** * An error raised by the [[removeFromStock() removal from stock action]] if the quantity to be removed from stock * exceeds the available one */ - case NotEnoughStock(product: Product, triedQuantity: AvailableQuantity, actualQuantity: AvailableQuantity) + case NotEnoughStock(product: Product, triedQuantity: DesiredQuantity, actualQuantity: AvailableQuantity) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index 2068e8a8..e2658746 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -18,6 +18,11 @@ final case class AvailableQuantity(n: NonNegativeNumber) */ final case class DesiredQuantity(n: PositiveNumber) +/** + * The required quantity of a certain product to reach the desired stock level. + */ +final case class MissingQuantity(n: NonNegativeNumber) + /** * The available quantity of a certain product. */ @@ -67,10 +72,3 @@ final case class BatchID(id: UUID) * A [[Product product]] with its respective [[Quantity quantity]] and the [[BatchID ID of the batch]] it belongs to. */ final case class LabelledProduct(cheeseType: Product, quantity: AvailableQuantity, batchID: BatchID) - -/** - * A weight in grams reported by a scale. - */ -final case class WeightInGrams(grams: PositiveDecimal) - -extension (n: PositiveDecimal) def grams: WeightInGrams = WeightInGrams(n) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index cfcf439a..9fc82992 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -8,19 +8,66 @@ import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers +import dev.atedeg.mdm.products.* +import dev.atedeg.mdm.stocking.Errors.* import dev.atedeg.mdm.stocking.OutgoingEvent.* -import dev.atedeg.mdm.stocking.grams import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.monads.* trait Mocks { val batchID: BatchID = BatchID(UUID.randomUUID()) - val cheeseType: CheeseType = 0 + val cheeseType: CheeseType = CheeseType.Squacquerone + val product: Product = Product.Squacquerone(100) val readyForQA: Batch.ReadyForQualityAssurance = Batch.ReadyForQualityAssurance(batchID, cheeseType) } class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { + Feature("Missing stock") { + Scenario("There are missing products from the desired stock") { + Given("An available stock") + val available = Map(product -> AvailableQuantity(10)) + And("a desired stock") + val desired = Map(product -> DesiredQuantity(20)) + When("someone asks how many products are missing to reach the desired stock") + val missing = getMissingCountFromProductStock(available, desired)(product) + Then("the missing quantity should be greater than zero") + missing shouldBe MissingQuantity(10) + } + Scenario("There are more products than needed in the desired stock") { + Given("An available stock") + val available = Map(product -> AvailableQuantity(20)) + And("a desired stock") + val desired = Map(product -> DesiredQuantity(10)) + When("someone asks how many products are missing to reach the desired stock") + val missing = getMissingCountFromProductStock(available, desired)(product) + Then("the missing quantity should be zero") + missing shouldBe MissingQuantity(0) + } + Scenario("Removal from stock with enough available products") { + Given("An available stock") + val available = Map(product -> AvailableQuantity(10)) + And("a quantity to remove from stock") + val toRemove = DesiredQuantity(5) + When("someone removes the product from the stock") + val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(product, toRemove) + Then("the stock should be updated") + val (_, result) = action.execute + result.value shouldEqual Map(product -> AvailableQuantity(5)) + } + Scenario("Removal from stock with not enough available products") { + Given("An available stock") + val available = Map(product -> AvailableQuantity(10)) + And("a quantity to remove from stock") + val toRemove = DesiredQuantity(50) + When("someone removes the product from the stock") + val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(product, toRemove) + Then("an error should be raised") + val (_, result) = action.execute + result.left.value shouldEqual NotEnoughStock(product, toRemove, AvailableQuantity(10)) + } + } + Feature("Quality assurance") { Scenario("An operator marks a batch as passing quality assurance") { Given("a ready-for-QA batch") @@ -40,15 +87,18 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { failed.id shouldEqual batchID failed.cheeseType shouldEqual cheeseType } + } + + Feature("Label printing") { Scenario("An operator tries to print a label for a cheese within weight range from a batch") { Given("a batch") val passed = approveBatch(readyForQA) - val weight = 1.grams + val correctWeight = 102.grams When("the operator prints a label for a product within weight range") - val labelAction: Action[WeightNotInRange, ProductStocked, LabelledProduct] = labelProduct(passed, weight) + val labelAction: Action[WeightNotInRange, ProductStocked, LabelledProduct] = labelProduct(passed, correctWeight) Then("the label should be printed with the correct information") val (events, result) = labelAction.execute - val expectedLabelledProduct = LabelledProduct(passed.cheeseType, 1, passed.id) + val expectedLabelledProduct = LabelledProduct(product, AvailableQuantity(1), passed.id) result.value shouldEqual expectedLabelledProduct And("an event should be emitted") events should contain(ProductStocked(expectedLabelledProduct)) @@ -56,13 +106,13 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Scenario("An operator tries to print a label for a cheese outside weight range from a batch") { Given("a batch") val passed = approveBatch(readyForQA) - val weight = 100.grams + val wrongWeight = 50.grams When("the operator prints a label for a cheese outside weight range") - val labelAction: Action[WeightNotInRange, ProductStocked, LabelledProduct] = labelProduct(passed, weight) + val labelAction: Action[WeightNotInRange, ProductStocked, LabelledProduct] = labelProduct(passed, wrongWeight) Then("the label should not be printed") val (events, result) = labelAction.execute And("an error should be raised") - result.left.value shouldEqual WeightNotInRange(3.grams, 100.grams) + result.left.value shouldEqual WeightNotInRange(product.weight, wrongWeight) And("no events should be emitted") events shouldBe empty } From 64cde377efc26a844ce93899390df5939f0ec9f6 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 13:13:54 +0200 Subject: [PATCH 069/329] fix(utils): move givens from outside compatnion objects and add Distance type class --- .../dev/atedeg/mdm/utils/NumericOps.scala | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala index 7e0b2006..daf85feb 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala @@ -26,46 +26,63 @@ trait Ceil[N]: def toCeil(n: N): N extension (n: N) def ceil: N = toCeil(n) +trait Distance[N]: + def distance(x: N, y: N): N + extension (x: N) def distanceFrom(y: N) = distance(x, y) + +given [N: Numeric]: Plus[N] with + override def plus(x: N, y: N): N = Numeric[N].plus(x, y) + object Plus: - given [N: Numeric]: Plus[N] with - override def plus(x: N, y: N): N = Numeric[N].plus(x, y) inline def derived[A](using gen: K0.ProductGeneric[A]): Plus[A] = plusGen inline given plusGen[N](using inst: K0.ProductInstances[Plus, N]): Plus[N] with def plus(n1: N, n2: N): N = inst.map2(n1, n2)([n] => (p: Plus[n], n1: n, n2: n) => p.plus(n1, n2)) +given [N: Numeric]: Times[N] with + override def times(x: N, y: N): N = Numeric[N].times(x, y) + object Times: - given [N: Numeric]: Times[N] with - override def times(x: N, y: N): N = Numeric[N].times(x, y) inline def derived[A](using gen: K0.ProductGeneric[A]): Times[A] = timesGen inline given timesGen[N](using inst: K0.ProductInstances[Times, N]): Times[N] with def times(n1: N, n2: N): N = inst.map2(n1, n2)([n] => (t: Times[n], n1: n, n2: n) => t.times(n1, n2)) +given [N: Numeric]: Minus[N] with + override def minus(x: N, y: N): N = Numeric[N].minus(x, y) + object Minus: - given [N: Numeric]: Minus[N] with - override def minus(x: N, y: N): N = Numeric[N].minus(x, y) inline def derived[A](using gen: K0.ProductGeneric[A]): Minus[A] = minusGen inline given minusGen[N](using inst: K0.ProductInstances[Minus, N]): Minus[N] with def minus(n1: N, n2: N): N = inst.map2(n1, n2)([n] => (m: Minus[n], n1: n, n2: n) => m.minus(n1, n2)) -object Div: +given Div[Int] with + override def div(x: Int, y: Int): Int = x / y - given Div[Int] with - override def div(x: Int, y: Int): Int = x / y +given [N: Fractional]: Div[N] with + override def div(x: N, y: N): N = Fractional[N].div(x, y) - given [N: Fractional]: Div[N] with - override def div(x: N, y: N): N = Fractional[N].div(x, y) +object Div: inline def derived[A](using gen: K0.ProductGeneric[A]): Div[A] = divGen inline given divGen[N](using inst: K0.ProductInstances[Div, N]): Div[N] with def div(n1: N, n2: N): N = inst.map2(n1, n2)([n] => (d: Div[n], n1: n, n2: n) => d.div(n1, n2)) +given [N](using N: Numeric[N]): Distance[N] with + override def distance(x: N, y: N): N = N.abs(N.minus(x, y)) + +object Distance: + + inline def derived[A](using gen: K0.ProductGeneric[A]): Distance[A] = distanceGen + + inline given distanceGen[N](using inst: K0.ProductInstances[Distance, N]): Distance[N] with + def distance(n1: N, n2: N): N = inst.map2(n1, n2)([n] => (m: Distance[n], n1: n, n2: n) => m.distance(n1, n2)) + given Ceil[Double] with override def toCeil(n: Double): Double = math.ceil(n) From 0f2e14c94e479848e2f83052d48145db449a353c Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 15:24:44 +0200 Subject: [PATCH 070/329] fix: add type annotations to silence wartremover --- .../main/scala/dev/atedeg/mdm/stocking/Actions.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 1f4cae04..96edd92e 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -54,19 +54,19 @@ def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: CanEmit[ProductStocked val (candidate: Int, product: Product) = batch.cheeseType match case CheeseType.Squacquerone => val w: SquacqueroneWeightInGrams = nearestWeight(allSquacqueroneWeights)(actualWeight) - (w, Product.Squacquerone(w)) + (w: Int, Product.Squacquerone(w): Product) case CheeseType.Casatella => val w: CasatellaWeightInGrams = nearestWeight(allCasatellaWeights)(actualWeight) - (w, Product.Casatella(w)) + (w: Int, Product.Casatella(w): Product) case CheeseType.Ricotta => val w: RicottaWeightInGrams = nearestWeight(allRicottaWeights)(actualWeight) - (w, Product.Ricotta(w)) + (w: Int, Product.Ricotta(w): Product) case CheeseType.Stracchino => val w: StracchinoWeightInGrams = nearestWeight(allStracchinoWeights)(actualWeight) - (w, Product.Stracchino(w)) + (w: Int, Product.Stracchino(w): Product) case CheeseType.Caciotta => val w: CasatellaWeightInGrams = nearestWeight(allCasatellaWeights)(actualWeight) - (w, Product.Casatella(w)) + (w: Int, Product.Casatella(w): Product) val labelledProduct = LabelledProduct(product, AvailableQuantity(1), batch.id) actualWeight.n.value.toDouble .isInRange(candidate.toDouble +- 5.percent) From 9c0f30fed2e8192ce96b1aa86f082c5cdf0c6dfa Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 15:26:36 +0200 Subject: [PATCH 071/329] feat: add new batch event --- .../scala/dev/atedeg/mdm/stocking/Events.scala | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala index 88f5e118..f483b65b 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala @@ -1,6 +1,8 @@ package dev.atedeg.mdm.stocking -import dev.atedeg.mdm.products.Product +import java.time.LocalDateTime + +import dev.atedeg.mdm.products.{ CheeseType, Product } /** * The events that may be produced by the bounded context. @@ -19,4 +21,13 @@ enum IncomingEvent: * Received when a [[Batch batch]] is ready for quality assurance. */ case BatchReadyForQualityAssurance(batch: BatchID) - case ProductRemovedFromStock(quantity: AvailableQuantity, product: Product) + + /** + * Received when a [[Product product]] is removed from the stock. + */ + case ProductRemovedFromStock(quantity: DesiredQuantity, product: Product) + + /** + * Received when a [[Batch.Aging batch]] is created. + */ + case NewBatch(batchID: BatchID, cheeseType: CheeseType, readyFrom: LocalDateTime) From 4c1085719546cfcec574f1e5ab60e3adb7088661 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 17:57:18 +0200 Subject: [PATCH 072/329] chore: add tables for stocking docs --- .ubidoc.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.ubidoc.yml b/.ubidoc.yml index ade82522..822ab645 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -1,4 +1,23 @@ tables: + - name: "stocking-ul" + rows: + - type: "AvailableStock" + - type: "DesiredStock" + - class: "AvailableQuantity" + - class: "DesiredQuantity" + - class: "MissingQuantity" + - enum: "Batch" + - enum: "QualityAssuredBatch" + - class: "BatchID" + - class: "LabelledProduct" + - name: "stocking-incoming" + rows: + - case: "ProductStocked" + - name: "stocking-outgoing" + rows: + - case: "BatchReadyForQualityAssurance" + - case: "ProductRemovedFromStock" + - case: "NewBatch" - name: "milk-planning-ul" termName: "Term" definitionName: "Definition" From 124ba9107f735107e876ef7a678a87e72e68c868 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 17:57:50 +0200 Subject: [PATCH 073/329] docs: add generated tables placeholder --- docs/_docs/stocking.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/_docs/stocking.md b/docs/_docs/stocking.md index 47aa8d47..c4dd7074 100644 --- a/docs/_docs/stocking.md +++ b/docs/_docs/stocking.md @@ -11,3 +11,17 @@ The former results in the cheeses being wrapped, labelled and put in the refrige the latter results in the entire batch being discarded. To label a cheese the worker has to weigh it, and an automated system will print an appropriate label. + +## Ubiquitous Language + +{% include stocking-ul.md %} + +## Domain Events + +### Incoming Events + +{% include stocking-incoming.md %} + +### Outgoing Events + +{% include stocking-outgoing.md %} \ No newline at end of file From 7e645b8ea1f4b8298bf04eb0add38d81647aef69 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Fri, 29 Jul 2022 19:10:34 +0200 Subject: [PATCH 074/329] test: improve coverage --- .../scala/dev/atedeg/mdm/stocking/Tests.scala | 56 +++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index 9fc82992..4a15a711 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -17,7 +17,11 @@ import dev.atedeg.mdm.utils.monads.* trait Mocks { val batchID: BatchID = BatchID(UUID.randomUUID()) val cheeseType: CheeseType = CheeseType.Squacquerone - val product: Product = Product.Squacquerone(100) + val squacquerone: Product = Product.Squacquerone(100) + val casatella: Product = Product.Casatella(300) + val ricotta: Product = Product.Ricotta(350) + val stracchino: Product = Product.Stracchino(250) + val caciotta: Product = Product.Caciotta(500) val readyForQA: Batch.ReadyForQualityAssurance = Batch.ReadyForQualityAssurance(batchID, cheeseType) } @@ -26,45 +30,65 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Feature("Missing stock") { Scenario("There are missing products from the desired stock") { Given("An available stock") - val available = Map(product -> AvailableQuantity(10)) + val available = Map( + squacquerone -> AvailableQuantity(10), + casatella -> AvailableQuantity(20), + ricotta -> AvailableQuantity(30), + stracchino -> AvailableQuantity(40), + caciotta -> AvailableQuantity(50), + ) And("a desired stock") - val desired = Map(product -> DesiredQuantity(20)) + val desired = Map( + squacquerone -> DesiredQuantity(20), + casatella -> DesiredQuantity(30), + ricotta -> DesiredQuantity(40), + stracchino -> DesiredQuantity(50), + caciotta -> DesiredQuantity(60), + ) When("someone asks how many products are missing to reach the desired stock") - val missing = getMissingCountFromProductStock(available, desired)(product) + val missingSquacquerone = getMissingCountFromProductStock(available, desired)(squacquerone) + val missingCasatella = getMissingCountFromProductStock(available, desired)(casatella) + val missingRicotta = getMissingCountFromProductStock(available, desired)(ricotta) + val missingStracchino = getMissingCountFromProductStock(available, desired)(stracchino) + val missingCaciotta = getMissingCountFromProductStock(available, desired)(caciotta) Then("the missing quantity should be greater than zero") - missing shouldBe MissingQuantity(10) + missingSquacquerone shouldBe MissingQuantity(10) + missingCasatella shouldBe MissingQuantity(10) + missingRicotta shouldBe MissingQuantity(10) + missingStracchino shouldBe MissingQuantity(10) + missingCaciotta shouldBe MissingQuantity(10) } Scenario("There are more products than needed in the desired stock") { Given("An available stock") - val available = Map(product -> AvailableQuantity(20)) + val available = Map(squacquerone -> AvailableQuantity(20)) And("a desired stock") - val desired = Map(product -> DesiredQuantity(10)) + val desired = Map(squacquerone -> DesiredQuantity(10)) When("someone asks how many products are missing to reach the desired stock") - val missing = getMissingCountFromProductStock(available, desired)(product) + val missing = getMissingCountFromProductStock(available, desired)(squacquerone) Then("the missing quantity should be zero") missing shouldBe MissingQuantity(0) } Scenario("Removal from stock with enough available products") { Given("An available stock") - val available = Map(product -> AvailableQuantity(10)) + val available = Map(squacquerone -> AvailableQuantity(10)) And("a quantity to remove from stock") val toRemove = DesiredQuantity(5) When("someone removes the product from the stock") - val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(product, toRemove) + val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(squacquerone, toRemove) Then("the stock should be updated") val (_, result) = action.execute - result.value shouldEqual Map(product -> AvailableQuantity(5)) + result.value shouldEqual Map(squacquerone -> AvailableQuantity(5)) } Scenario("Removal from stock with not enough available products") { Given("An available stock") - val available = Map(product -> AvailableQuantity(10)) + val available = Map(squacquerone -> AvailableQuantity(10)) And("a quantity to remove from stock") val toRemove = DesiredQuantity(50) When("someone removes the product from the stock") - val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(product, toRemove) + val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(squacquerone, toRemove) Then("an error should be raised") val (_, result) = action.execute - result.left.value shouldEqual NotEnoughStock(product, toRemove, AvailableQuantity(10)) + result.left.value shouldEqual NotEnoughStock(squacquerone, toRemove, AvailableQuantity(10)) } } @@ -98,7 +122,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { val labelAction: Action[WeightNotInRange, ProductStocked, LabelledProduct] = labelProduct(passed, correctWeight) Then("the label should be printed with the correct information") val (events, result) = labelAction.execute - val expectedLabelledProduct = LabelledProduct(product, AvailableQuantity(1), passed.id) + val expectedLabelledProduct = LabelledProduct(squacquerone, AvailableQuantity(1), passed.id) result.value shouldEqual expectedLabelledProduct And("an event should be emitted") events should contain(ProductStocked(expectedLabelledProduct)) @@ -112,7 +136,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Then("the label should not be printed") val (events, result) = labelAction.execute And("an error should be raised") - result.left.value shouldEqual WeightNotInRange(product.weight, wrongWeight) + result.left.value shouldEqual WeightNotInRange(squacquerone.weight, wrongWeight) And("no events should be emitted") events shouldBe empty } From 049dec1937a080c995863ca4d18e07092d1acc80 Mon Sep 17 00:00:00 2001 From: ndido98 Date: Sat, 30 Jul 2022 16:47:52 +0200 Subject: [PATCH 075/329] refactor: rewrite labelProduct for better clarity --- .../scala/dev/atedeg/mdm/products/Utils.scala | 28 ++++++++++++++ .../dev/atedeg/mdm/stocking/Actions.scala | 37 ++++++------------- .../scala/dev/atedeg/mdm/utils/Refined.scala | 4 ++ 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala index 8a392909..88718748 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala @@ -3,6 +3,7 @@ package dev.atedeg.mdm.products import scala.compiletime.* import cats.data.NonEmptyList +import cats.kernel.Order import dev.atedeg.mdm.utils.{PositiveNumber, coerce} import eu.timepit.refined.predicates.all.Positive @@ -28,3 +29,30 @@ inline private def checkInt[T](inline n: T): Int = inline erasedValue[T] match case _ => compiletime.error(codeOf(n) + " is not an int") private[products] def toGrams(n: Int): Grams = Grams(coerce[Int, Positive](n)) + +given Order[Grams] with + def compare(x: Grams, y: Grams): Int = Order[Int].compare(x.n.value, y.n.value) + +extension (cheeseType: CheeseType) + + /** + * Creates a [[Product product]] with the given [[CheeseType cheese type]] if it can find a + * weight amongst the allowed ones that matches the given predicate. + */ + def withWeight(predicate: Int => Boolean): Option[Product] = + // Somehow Scala does not understand that a function (A => B) could also be treated as a function + // [a <: A] => a => B and won't compile unless I explicitly convert the function myself... + def p = [t <: Int] => (n: t) => predicate(n) + cheeseType match + case CheeseType.Squacquerone => allSquacqueroneWeights.find(p(_)).map(Product.Squacquerone(_)) + case CheeseType.Casatella => allCasatellaWeights.find(p(_)).map(Product.Casatella(_)) + case CheeseType.Ricotta => allRicottaWeights.find(p(_)).map(Product.Ricotta(_)) + case CheeseType.Stracchino => allStracchinoWeights.find(p(_)).map(Product.Stracchino(_)) + case CheeseType.Caciotta => allCaciottaWeights.find(p(_)).map(Product.Caciotta(_)) + + def allowedWeights: NonEmptyList[Grams] = cheeseType match + case CheeseType.Squacquerone => allSquacqueroneWeights.map(toGrams) + case CheeseType.Casatella => allCasatellaWeights.map(toGrams) + case CheeseType.Ricotta => allRicottaWeights.map(toGrams) + case CheeseType.Stracchino => allStracchinoWeights.map(toGrams) + case CheeseType.Caciotta => allCaciottaWeights.map(toGrams) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 96edd92e..87f66452 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -5,6 +5,7 @@ import cats.data.{ NonEmptyList, NonEmptySet } import cats.syntax.all.* import dev.atedeg.mdm.products.* +import dev.atedeg.mdm.products.given import dev.atedeg.mdm.stocking.Errors.* import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.utils.* @@ -51,29 +52,15 @@ def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: CanEmit[ProductStocked batch: QualityAssuredBatch.Passed, actualWeight: Grams, ): M[LabelledProduct] = - val (candidate: Int, product: Product) = batch.cheeseType match - case CheeseType.Squacquerone => - val w: SquacqueroneWeightInGrams = nearestWeight(allSquacqueroneWeights)(actualWeight) - (w: Int, Product.Squacquerone(w): Product) - case CheeseType.Casatella => - val w: CasatellaWeightInGrams = nearestWeight(allCasatellaWeights)(actualWeight) - (w: Int, Product.Casatella(w): Product) - case CheeseType.Ricotta => - val w: RicottaWeightInGrams = nearestWeight(allRicottaWeights)(actualWeight) - (w: Int, Product.Ricotta(w): Product) - case CheeseType.Stracchino => - val w: StracchinoWeightInGrams = nearestWeight(allStracchinoWeights)(actualWeight) - (w: Int, Product.Stracchino(w): Product) - case CheeseType.Caciotta => - val w: CasatellaWeightInGrams = nearestWeight(allCasatellaWeights)(actualWeight) - (w: Int, Product.Casatella(w): Product) - val labelledProduct = LabelledProduct(product, AvailableQuantity(1), batch.id) - actualWeight.n.value.toDouble - .isInRange(candidate.toDouble +- 5.percent) - .otherwiseRaise(WeightNotInRange(Grams(coerce(candidate)), actualWeight): WeightNotInRange) - .andThen(emit(ProductStocked(labelledProduct): ProductStocked)) - .thenReturn(labelledProduct) + val closestAllowedWeight = batch.cheeseType.allowedWeights.closestTo(actualWeight) + for { + product <- batch.cheeseType + .withWeight(closeTo(actualWeight)) + .ifMissingRaise(WeightNotInRange(closestAllowedWeight, actualWeight): WeightNotInRange) + labelledProduct = LabelledProduct(product, AvailableQuantity(1), batch.id) + _ <- emit(ProductStocked(labelledProduct): ProductStocked) + } yield labelledProduct -private def nearestWeight[T <: Int](weights: NonEmptyList[T])(actualWeight: Grams): T = - def distanceFromActualWeight(weight: T) = weight distanceFrom actualWeight.n.value - weights.minimumBy(distanceFromActualWeight) +private def closeTo(weight: Grams)(n: Int): Boolean = weight.n.value.toDouble isInRange (n.toDouble +- 5.percent) + +extension (gs: NonEmptyList[Grams]) private def closestTo(g: Grams): Grams = gs.minimumBy(_.n.value - g.n.value) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala index f2b8760a..b5b32a3e 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala @@ -69,3 +69,7 @@ given refinedMinus[N: Numeric, P <: NonNegative: ValidFor[N]](using Op: Minus[N] override def minus(x: N Refined P, y: N Refined P): N Refined P = coerce(if y.value > x.value then Numeric[N].zero else Op.minus(x.value, y.value)) + +given refinedDistance[N, P <: NonNegative: ValidFor[N]](using D: Distance[N]): Distance[N Refined P] with + + override def distance(x: N Refined P, y: N Refined P): N Refined P = coerce(D.distance(x.value, y.value)) From 356d48584aeaa51342f01f045abad80c6e297d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Di=20Domenico?= Date: Sat, 30 Jul 2022 18:18:50 +0200 Subject: [PATCH 076/329] chore: address review comments Co-authored-by: Giacomo Cavalieri Co-authored-by: Linda Vitali <62563336+vitlinda@users.noreply.github.com> --- docs/_docs/stocking.md | 2 +- .../src/main/scala/dev/atedeg/mdm/products/Utils.scala | 3 +++ .../src/main/scala/dev/atedeg/mdm/stocking/Actions.scala | 6 +++--- .../src/main/scala/dev/atedeg/mdm/stocking/Errors.scala | 4 ++-- .../src/main/scala/dev/atedeg/mdm/stocking/Types.scala | 6 +++--- .../src/test/scala/dev/atedeg/mdm/stocking/Tests.scala | 8 +++++--- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/_docs/stocking.md b/docs/_docs/stocking.md index c4dd7074..41ce6d31 100644 --- a/docs/_docs/stocking.md +++ b/docs/_docs/stocking.md @@ -4,7 +4,7 @@ title: Stocking # Stocking -After a batch of cheeses has aged for the required amount of time, +After a batch of cheeses has ripened for the required amount of time, one of them is selected to perform quality assurance. Its result could be either positive or negative. The former results in the cheeses being wrapped, labelled and put in the refrigeration room; diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala index 88718748..d227a7a0 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala @@ -50,6 +50,9 @@ extension (cheeseType: CheeseType) case CheeseType.Stracchino => allStracchinoWeights.find(p(_)).map(Product.Stracchino(_)) case CheeseType.Caciotta => allCaciottaWeights.find(p(_)).map(Product.Caciotta(_)) +/** + * Returns a [[NonEmptyList list]] of all the allowed weights in [[Grams grams]] for a given [[CheeseType cheese type]]. + */ def allowedWeights: NonEmptyList[Grams] = cheeseType match case CheeseType.Squacquerone => allSquacqueroneWeights.map(toGrams) case CheeseType.Casatella => allCasatellaWeights.map(toGrams) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 87f66452..392f528f 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -13,7 +13,7 @@ import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.* /** - * Gets how many products are missing from the stock, given the desired stock. + * Gets [[MissingQuantity how many products]] are missing from the [[AvailableStock stock]], given the [[DesiredStock desired stock]]. */ def getMissingCountFromProductStock( availableStock: AvailableStock, @@ -23,7 +23,7 @@ def getMissingCountFromProductStock( else MissingQuantity(desiredStock(product).n.toNonNegative - availableStock(product).n) /** - * Removes the given quantity of a certain product from the stock, giving the new current stock. + * Removes the given quantity of a certain [[Product product]] from the [[AvailableStock stock]], giving the new current [[AvailableStock stock]]. */ def removeFromStock[M[_]: Monad: CanRaise[NotEnoughStock]]( stock: AvailableStock, @@ -45,7 +45,7 @@ def rejectBatch(batch: Batch.ReadyForQualityAssurance): QualityAssuredBatch.Fail QualityAssuredBatch.Failed(batch.id, batch.cheeseType) /** - * Labels a product given the [[QualityAssuredBatch.Passed batch]] it comes from and its [[WeightInGrams actual weight]] + * Labels a [[Product product]] given the [[QualityAssuredBatch.Passed batch]] it comes from and its [[Grams actual weight]] * as given by the scale. */ def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: CanEmit[ProductStocked]]( diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala index 06a3b3c1..c1bbadda 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala @@ -8,12 +8,12 @@ import dev.atedeg.mdm.products.* enum Errors: /** * An error raised by the [[labelProduct() labelling action]] if the actual weight is too far - * from the expected weights. + * from the expected weight. */ case WeightNotInRange(expectedWeight: Grams, actualWeight: Grams) /** * An error raised by the [[removeFromStock() removal from stock action]] if the quantity to be removed from stock - * exceeds the available one + * exceeds the available one. */ case NotEnoughStock(product: Product, triedQuantity: DesiredQuantity, actualQuantity: AvailableQuantity) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index e2658746..f8144990 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -39,7 +39,7 @@ type DesiredStock = Map[Product, DesiredQuantity] */ enum Batch: /** - * An aging batch that will become ready for quality assurance at the given date and time. + * An aging batch that will become [[Batch.ReadyForQualityAssurance ready for quality assurance]] at the given date and time. */ case Aging(id: BatchID, cheeseType: CheeseType, readyFrom: LocalDateTime) @@ -54,12 +54,12 @@ enum Batch: */ enum QualityAssuredBatch: /** - * A batch which passed quality assurance. + * A batch which [[approveBatch() passed quality assurance]]. */ case Passed(id: BatchID, cheeseType: CheeseType) /** - * A batch which failed quality assurance. + * A batch which [[rejectBatch() failed quality assurance]]. */ case Failed(id: BatchID, cheeseType: CheeseType) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index 4a15a711..3659c7bc 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -82,7 +82,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Scenario("Removal from stock with not enough available products") { Given("An available stock") val available = Map(squacquerone -> AvailableQuantity(10)) - And("a quantity to remove from stock") + And("a quantity to remove from stock that is greater than the available one") val toRemove = DesiredQuantity(50) When("someone removes the product from the stock") val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(squacquerone, toRemove) @@ -117,8 +117,9 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Scenario("An operator tries to print a label for a cheese within weight range from a batch") { Given("a batch") val passed = approveBatch(readyForQA) + And("A weight that is within the allowed range") val correctWeight = 102.grams - When("the operator prints a label for a product within weight range") + When("the operator tries to print a label") val labelAction: Action[WeightNotInRange, ProductStocked, LabelledProduct] = labelProduct(passed, correctWeight) Then("the label should be printed with the correct information") val (events, result) = labelAction.execute @@ -130,8 +131,9 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Scenario("An operator tries to print a label for a cheese outside weight range from a batch") { Given("a batch") val passed = approveBatch(readyForQA) + And("a weight that is outside the allowed range") val wrongWeight = 50.grams - When("the operator prints a label for a cheese outside weight range") + When("the operator tries to print a label") val labelAction: Action[WeightNotInRange, ProductStocked, LabelledProduct] = labelProduct(passed, wrongWeight) Then("the label should not be printed") val (events, result) = labelAction.execute From fc991adfd6fca351d8d16de404747db4cda2bc63 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 11:44:16 +0200 Subject: [PATCH 077/329] style: fix lowercase generic type Co-authored-by: Nicolas Farabegoli --- .../src/main/scala/dev/atedeg/mdm/products/Utils.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala index d227a7a0..d35ffdc7 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala @@ -42,7 +42,7 @@ extension (cheeseType: CheeseType) def withWeight(predicate: Int => Boolean): Option[Product] = // Somehow Scala does not understand that a function (A => B) could also be treated as a function // [a <: A] => a => B and won't compile unless I explicitly convert the function myself... - def p = [t <: Int] => (n: t) => predicate(n) + def p = [T <: Int] => (n: T) => predicate(n) cheeseType match case CheeseType.Squacquerone => allSquacqueroneWeights.find(p(_)).map(Product.Squacquerone(_)) case CheeseType.Casatella => allCasatellaWeights.find(p(_)).map(Product.Casatella(_)) From 1de75242ab665288768dfa9b61fabf3357e48897 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 11:31:56 +0200 Subject: [PATCH 078/329] refactor: move Grams extension method to utils file --- .../src/main/scala/dev/atedeg/mdm/products/Products.scala | 2 -- .../src/main/scala/dev/atedeg/mdm/products/Utils.scala | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala index 4fc3af34..11e88be1 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala @@ -10,8 +10,6 @@ import dev.atedeg.mdm.utils.given */ final case class Grams(n: PositiveNumber) -extension (n: PositiveNumber) def grams: Grams = Grams(n) - /** * A type of cheese. */ diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala index d35ffdc7..f647a08e 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala @@ -30,6 +30,8 @@ inline private def checkInt[T](inline n: T): Int = inline erasedValue[T] match private[products] def toGrams(n: Int): Grams = Grams(coerce[Int, Positive](n)) +extension (n: PositiveNumber) def grams: Grams = Grams(n) + given Order[Grams] with def compare(x: Grams, y: Grams): Int = Order[Int].compare(x.n.value, y.n.value) From fe5fd1cb4eac6630aa71fbeb4277ea2b10bcc2af Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 11:36:14 +0200 Subject: [PATCH 079/329] docs: change dock for stock(s) --- .../src/main/scala/dev/atedeg/mdm/stocking/Types.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index f8144990..baf4b691 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -24,12 +24,15 @@ final case class DesiredQuantity(n: PositiveNumber) final case class MissingQuantity(n: NonNegativeNumber) /** - * The available quantity of a certain product. + * The currently available [[Product products]] in stock; each one is available + * in a certain [[AvailableQuantity quantity]] (that could also be zero if the + * product is out-of-stock). */ type AvailableStock = Map[Product, AvailableQuantity] /** - * The desired quantity of a certain product. + * The [[DesiredQuantity desired quantity]] of each [[Product product]] that should + * always be in stock in order to have a safe margin to keep order fulfillment going. */ type DesiredStock = Map[Product, DesiredQuantity] From 86810b58aa1ef9ef9bf96c9435da6eba9bdb0ba1 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 11:43:00 +0200 Subject: [PATCH 080/329] test: reword a test description --- stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index 3659c7bc..e663a887 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -51,7 +51,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { val missingRicotta = getMissingCountFromProductStock(available, desired)(ricotta) val missingStracchino = getMissingCountFromProductStock(available, desired)(stracchino) val missingCaciotta = getMissingCountFromProductStock(available, desired)(caciotta) - Then("the missing quantity should be greater than zero") + Then("the missing quantity should be the difference between the available and desired quantities") missingSquacquerone shouldBe MissingQuantity(10) missingCasatella shouldBe MissingQuantity(10) missingRicotta shouldBe MissingQuantity(10) From dd9ce999def71b7440c3521c703d1aca5c90b6c0 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 11:55:40 +0200 Subject: [PATCH 081/329] refactor: move shared kernel utility functions to utils package --- .../dev/atedeg/mdm/products/Inline.scala | 28 +++++++++ .../dev/atedeg/mdm/products/Products.scala | 11 ++-- .../scala/dev/atedeg/mdm/products/Utils.scala | 63 ------------------- .../mdm/products/utils/CheeseTypeOps.scala | 33 ++++++++++ .../atedeg/mdm/products/utils/GramsOps.scala | 14 +++++ .../dev/atedeg/mdm/stocking/Actions.scala | 3 +- .../scala/dev/atedeg/mdm/stocking/Tests.scala | 1 + 7 files changed, 84 insertions(+), 69 deletions(-) create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Inline.scala create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/CheeseTypeOps.scala create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/GramsOps.scala diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Inline.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Inline.scala new file mode 100644 index 00000000..f542b5b2 --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Inline.scala @@ -0,0 +1,28 @@ +package dev.atedeg.mdm.products + +import scala.compiletime.* + +import cats.data.NonEmptyList +import cats.kernel.Order +import eu.timepit.refined.predicates.all.Positive + +import dev.atedeg.mdm.utils.{ coerce, PositiveNumber } + +type OneOf[T <: Tuple] = T match + case (t *: EmptyTuple) => t + case (t *: ts) => t | OneOf[ts] + +@SuppressWarnings(Array("scalafix:DisableSyntax.asInstanceOf")) +inline def all[T <: Tuple]: NonEmptyList[OneOf[T]] = inline erasedValue[T] match + case _: (n *: EmptyTuple) => + val v = checkInt[n](constValue[n]) + NonEmptyList.one(v).asInstanceOf[NonEmptyList[OneOf[T]]] + case _: (n *: gs) => + val v = checkInt[n](constValue[n]) + NonEmptyList(v, all[gs].toList).asInstanceOf[NonEmptyList[OneOf[T]]] + case _ => compiletime.error("Cannot work on a tuple with elements that are not Grams") + +@SuppressWarnings(Array("scalafix:DisableSyntax.asInstanceOf")) +inline private def checkInt[T](inline n: T): Int = inline erasedValue[T] match + case _: Int => n.asInstanceOf[Int] + case _ => compiletime.error(codeOf(n) + " is not an int") diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala index 11e88be1..0161fcea 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala @@ -1,5 +1,6 @@ package dev.atedeg.mdm.products +import dev.atedeg.mdm.products.utils.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.* @@ -24,11 +25,11 @@ enum CheeseType: * A [[CheeseType type of cheese]] with its respective [[Grams weight]]. */ enum Product(val cheeseType: CheeseType, val weight: Grams): - case Squacquerone(w: SquacqueroneWeightInGrams) extends Product(CheeseType.Squacquerone, toGrams(w)) - case Casatella(w: CasatellaWeightInGrams) extends Product(CheeseType.Casatella, toGrams(w)) - case Ricotta(w: RicottaWeightInGrams) extends Product(CheeseType.Ricotta, toGrams(w)) - case Stracchino(w: StracchinoWeightInGrams) extends Product(CheeseType.Stracchino, toGrams(w)) - case Caciotta(w: CaciottaWeightInGrams) extends Product(CheeseType.Caciotta, toGrams(w)) + case Squacquerone(w: SquacqueroneWeightInGrams) extends Product(CheeseType.Squacquerone, coerceToGrams(w)) + case Casatella(w: CasatellaWeightInGrams) extends Product(CheeseType.Casatella, coerceToGrams(w)) + case Ricotta(w: RicottaWeightInGrams) extends Product(CheeseType.Ricotta, coerceToGrams(w)) + case Stracchino(w: StracchinoWeightInGrams) extends Product(CheeseType.Stracchino, coerceToGrams(w)) + case Caciotta(w: CaciottaWeightInGrams) extends Product(CheeseType.Caciotta, coerceToGrams(w)) type SquacqueroneWeightsInGrams = (100, 250, 350, 800, 1000, 1500) type SquacqueroneWeightInGrams = OneOf[SquacqueroneWeightsInGrams] diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala index f647a08e..e69de29b 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala @@ -1,63 +0,0 @@ -package dev.atedeg.mdm.products - -import scala.compiletime.* - -import cats.data.NonEmptyList -import cats.kernel.Order -import dev.atedeg.mdm.utils.{PositiveNumber, coerce} -import eu.timepit.refined.predicates.all.Positive - -import scala.compiletime.* - -type OneOf[T <: Tuple] = T match - case (t *: EmptyTuple) => t - case (t *: ts) => t | OneOf[ts] - -@SuppressWarnings(Array("scalafix:DisableSyntax.asInstanceOf")) -inline def all[T <: Tuple]: NonEmptyList[OneOf[T]] = inline erasedValue[T] match - case _: (n *: EmptyTuple) => - val v = checkInt[n](constValue[n]) - NonEmptyList.one(v).asInstanceOf[NonEmptyList[OneOf[T]]] - case _: (n *: gs) => - val v = checkInt[n](constValue[n]) - NonEmptyList(v, all[gs].toList).asInstanceOf[NonEmptyList[OneOf[T]]] - case _ => compiletime.error("Cannot work on a tuple with elements that are not Grams") - -@SuppressWarnings(Array("scalafix:DisableSyntax.asInstanceOf")) -inline private def checkInt[T](inline n: T): Int = inline erasedValue[T] match - case _: Int => n.asInstanceOf[Int] - case _ => compiletime.error(codeOf(n) + " is not an int") - -private[products] def toGrams(n: Int): Grams = Grams(coerce[Int, Positive](n)) - -extension (n: PositiveNumber) def grams: Grams = Grams(n) - -given Order[Grams] with - def compare(x: Grams, y: Grams): Int = Order[Int].compare(x.n.value, y.n.value) - -extension (cheeseType: CheeseType) - - /** - * Creates a [[Product product]] with the given [[CheeseType cheese type]] if it can find a - * weight amongst the allowed ones that matches the given predicate. - */ - def withWeight(predicate: Int => Boolean): Option[Product] = - // Somehow Scala does not understand that a function (A => B) could also be treated as a function - // [a <: A] => a => B and won't compile unless I explicitly convert the function myself... - def p = [T <: Int] => (n: T) => predicate(n) - cheeseType match - case CheeseType.Squacquerone => allSquacqueroneWeights.find(p(_)).map(Product.Squacquerone(_)) - case CheeseType.Casatella => allCasatellaWeights.find(p(_)).map(Product.Casatella(_)) - case CheeseType.Ricotta => allRicottaWeights.find(p(_)).map(Product.Ricotta(_)) - case CheeseType.Stracchino => allStracchinoWeights.find(p(_)).map(Product.Stracchino(_)) - case CheeseType.Caciotta => allCaciottaWeights.find(p(_)).map(Product.Caciotta(_)) - -/** - * Returns a [[NonEmptyList list]] of all the allowed weights in [[Grams grams]] for a given [[CheeseType cheese type]]. - */ - def allowedWeights: NonEmptyList[Grams] = cheeseType match - case CheeseType.Squacquerone => allSquacqueroneWeights.map(toGrams) - case CheeseType.Casatella => allCasatellaWeights.map(toGrams) - case CheeseType.Ricotta => allRicottaWeights.map(toGrams) - case CheeseType.Stracchino => allStracchinoWeights.map(toGrams) - case CheeseType.Caciotta => allCaciottaWeights.map(toGrams) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/CheeseTypeOps.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/CheeseTypeOps.scala new file mode 100644 index 00000000..19b84f83 --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/CheeseTypeOps.scala @@ -0,0 +1,33 @@ +package dev.atedeg.mdm.products.utils + +import cats.data.NonEmptyList + +import dev.atedeg.mdm.products.* + +extension (cheeseType: CheeseType) + + /** + * Creates a [[Product product]] with the given [[CheeseType cheese type]] if it can find a + * weight amongst the allowed ones that matches the given predicate. + */ + def withWeight(predicate: Int => Boolean): Option[Product] = + // Somehow Scala does not understand that a function (A => B) could also be treated as a function + // [a <: A] => a => B and won't compile unless I explicitly convert the function myself... + def p = [T <: Int] => (n: T) => predicate(n) + cheeseType match + case CheeseType.Squacquerone => allSquacqueroneWeights.find(p(_)).map(Product.Squacquerone(_)) + case CheeseType.Casatella => allCasatellaWeights.find(p(_)).map(Product.Casatella(_)) + case CheeseType.Ricotta => allRicottaWeights.find(p(_)).map(Product.Ricotta(_)) + case CheeseType.Stracchino => allStracchinoWeights.find(p(_)).map(Product.Stracchino(_)) + case CheeseType.Caciotta => allCaciottaWeights.find(p(_)).map(Product.Caciotta(_)) + + /** + * Returns a [[NonEmptyList list]] of all the allowed weights in [[Grams grams]] for a given [[CheeseType cheese type]]. + */ + def allowedWeights: NonEmptyList[Grams] = (cheeseType match + case CheeseType.Squacquerone => allSquacqueroneWeights + case CheeseType.Casatella => allCasatellaWeights + case CheeseType.Ricotta => allRicottaWeights + case CheeseType.Stracchino => allStracchinoWeights + case CheeseType.Caciotta => allCaciottaWeights + ).map(coerceToGrams) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/GramsOps.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/GramsOps.scala new file mode 100644 index 00000000..d69eb399 --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/GramsOps.scala @@ -0,0 +1,14 @@ +package dev.atedeg.mdm.products.utils + +import cats.kernel.Order +import eu.timepit.refined.predicates.all.Positive + +import dev.atedeg.mdm.products.Grams +import dev.atedeg.mdm.utils.{ coerce, PositiveNumber } + +private[products] def coerceToGrams(n: Int): Grams = Grams(coerce[Int, Positive](n)) + +extension (n: PositiveNumber) def grams: Grams = Grams(n) + +given Order[Grams] with + def compare(x: Grams, y: Grams): Int = Order[Int].compare(x.n.value, y.n.value) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 392f528f..e830c1a8 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -5,7 +5,8 @@ import cats.data.{ NonEmptyList, NonEmptySet } import cats.syntax.all.* import dev.atedeg.mdm.products.* -import dev.atedeg.mdm.products.given +import dev.atedeg.mdm.products.utils.* +import dev.atedeg.mdm.products.utils.given import dev.atedeg.mdm.stocking.Errors.* import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.utils.* diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index e663a887..1135b3b1 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -9,6 +9,7 @@ import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers import dev.atedeg.mdm.products.* +import dev.atedeg.mdm.products.utils.* import dev.atedeg.mdm.stocking.Errors.* import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.utils.* From 304396e08dec33b49d929d444512887bb3ed790a Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 14:10:45 +0200 Subject: [PATCH 082/329] style: scalafmtAll --- .../src/main/scala/dev/atedeg/mdm/products/Products.scala | 2 -- .../src/main/scala/dev/atedeg/mdm/products/Utils.scala | 0 2 files changed, 2 deletions(-) delete mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala index 0161fcea..a9d79638 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala @@ -1,8 +1,6 @@ package dev.atedeg.mdm.products import dev.atedeg.mdm.products.utils.* -import dev.atedeg.mdm.utils.* - import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Utils.scala deleted file mode 100644 index e69de29b..00000000 From cf20f7d60400adf695c69de19aeaf9134764cde0 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 14:15:23 +0200 Subject: [PATCH 083/329] docs: update ubidoc.yml --- .ubidoc.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index 822ab645..495da459 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -9,15 +9,16 @@ tables: - enum: "Batch" - enum: "QualityAssuredBatch" - class: "BatchID" + name: "Batch ID" - class: "LabelledProduct" - name: "stocking-incoming" rows: - - case: "ProductStocked" + - case: "IncomingEvent.ProductStocked" - name: "stocking-outgoing" rows: - - case: "BatchReadyForQualityAssurance" - - case: "ProductRemovedFromStock" - - case: "NewBatch" + - case: "OutgoingEvent.BatchReadyForQualityAssurance" + - case: "OutgoingEvent.ProductRemovedFromStock" + - case: "OutgoingEvent.NewBatch" - name: "milk-planning-ul" termName: "Term" definitionName: "Definition" From e2f6cf019dc28bb5f42a34523e42b2f3cc7f1920 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 14:20:25 +0200 Subject: [PATCH 084/329] refactor: change Errors to Error --- stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala | 2 +- stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala | 2 +- stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index e830c1a8..afdf6690 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -7,7 +7,7 @@ import cats.syntax.all.* import dev.atedeg.mdm.products.* import dev.atedeg.mdm.products.utils.* import dev.atedeg.mdm.products.utils.given -import dev.atedeg.mdm.stocking.Errors.* +import dev.atedeg.mdm.stocking.Error.* import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala index c1bbadda..d247abe7 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala @@ -5,7 +5,7 @@ import dev.atedeg.mdm.products.* /** * The errors that have to be handled by the bounded context. */ -enum Errors: +enum Error: /** * An error raised by the [[labelProduct() labelling action]] if the actual weight is too far * from the expected weight. diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index 1135b3b1..d983269b 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -10,7 +10,7 @@ import org.scalatest.matchers.should.Matchers import dev.atedeg.mdm.products.* import dev.atedeg.mdm.products.utils.* -import dev.atedeg.mdm.stocking.Errors.* +import dev.atedeg.mdm.stocking.Error.* import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.monads.* From ef371631854d8139bd445a19ccd4de3bca9fcf7c Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 14:22:37 +0200 Subject: [PATCH 085/329] refactor: use conversion toNonNegative --- .../src/main/scala/dev/atedeg/mdm/stocking/Actions.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index afdf6690..aafd72d6 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -20,7 +20,7 @@ def getMissingCountFromProductStock( availableStock: AvailableStock, desiredStock: DesiredStock, )(product: Product): MissingQuantity = - if availableStock(product).n >= desiredStock(product).n.toNonNegative then MissingQuantity(0) + if availableStock(product).n >= desiredStock(product).n then MissingQuantity(0) else MissingQuantity(desiredStock(product).n.toNonNegative - availableStock(product).n) /** @@ -29,9 +29,9 @@ def getMissingCountFromProductStock( def removeFromStock[M[_]: Monad: CanRaise[NotEnoughStock]]( stock: AvailableStock, )(product: Product, quantity: DesiredQuantity): M[AvailableStock] = - (stock(product).n > quantity.n.toNonNegative) + (stock(product).n > quantity.n) .otherwiseRaise(NotEnoughStock(product, quantity, stock(product)): NotEnoughStock) - .thenReturn(stock + (product -> AvailableQuantity(stock(product).n - quantity.n.toNonNegative))) + .thenReturn(stock + (product -> AvailableQuantity(stock(product).n - quantity.n))) /** * Approves a batch after quality assurance. From 171b4033f75fbeec9e1cabb1fc055da2697b8f05 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 14:27:44 +0200 Subject: [PATCH 086/329] refactor: add Quantity domain concept --- .../src/main/scala/dev/atedeg/mdm/stocking/Actions.scala | 2 +- .../src/main/scala/dev/atedeg/mdm/stocking/Errors.scala | 7 ++++--- .../src/main/scala/dev/atedeg/mdm/stocking/Types.scala | 5 +++++ .../src/test/scala/dev/atedeg/mdm/stocking/Tests.scala | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index aafd72d6..2cfb129e 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -28,7 +28,7 @@ def getMissingCountFromProductStock( */ def removeFromStock[M[_]: Monad: CanRaise[NotEnoughStock]]( stock: AvailableStock, -)(product: Product, quantity: DesiredQuantity): M[AvailableStock] = +)(product: Product, quantity: Quantity): M[AvailableStock] = (stock(product).n > quantity.n) .otherwiseRaise(NotEnoughStock(product, quantity, stock(product)): NotEnoughStock) .thenReturn(stock + (product -> AvailableQuantity(stock(product).n - quantity.n))) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala index d247abe7..6fea4f7c 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Errors.scala @@ -13,7 +13,8 @@ enum Error: case WeightNotInRange(expectedWeight: Grams, actualWeight: Grams) /** - * An error raised by the [[removeFromStock() removal from stock action]] if the quantity to be removed from stock - * exceeds the available one. + * An error raised by the [[removeFromStock() removal from stock action]] if the + * [[Quantity quantity to be removed]] from stock + * exceeds the [[AvailableQuantity available one]]. */ - case NotEnoughStock(product: Product, triedQuantity: DesiredQuantity, actualQuantity: AvailableQuantity) + case NotEnoughStock(product: Product, triedQuantity: Quantity, actualQuantity: AvailableQuantity) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index baf4b691..068f389f 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -18,6 +18,11 @@ final case class AvailableQuantity(n: NonNegativeNumber) */ final case class DesiredQuantity(n: PositiveNumber) +/** + * A quantity of something. + */ +final case class Quantity(n: PositiveNumber) + /** * The required quantity of a certain product to reach the desired stock level. */ diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index d983269b..d8477e8f 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -73,7 +73,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Given("An available stock") val available = Map(squacquerone -> AvailableQuantity(10)) And("a quantity to remove from stock") - val toRemove = DesiredQuantity(5) + val toRemove = Quantity(5) When("someone removes the product from the stock") val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(squacquerone, toRemove) Then("the stock should be updated") @@ -84,7 +84,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Given("An available stock") val available = Map(squacquerone -> AvailableQuantity(10)) And("a quantity to remove from stock that is greater than the available one") - val toRemove = DesiredQuantity(50) + val toRemove = Quantity(50) When("someone removes the product from the stock") val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(squacquerone, toRemove) Then("an error should be raised") From 8b7f506ca89dc56ecebb3057f62f28c92a2b44a0 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 1 Aug 2022 21:07:31 +0000 Subject: [PATCH 087/329] chore(release): 1.0.0-beta.2 [skip ci] # [1.0.0-beta.2](https://github.com/atedeg/mdm/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-08-01) ### Bug Fixes * add type annotations to silence wartremover ([0f2e14c](https://github.com/atedeg/mdm/commit/0f2e14c94e479848e2f83052d48145db449a353c)) * **products:** change macro to generate list of weights per cheese type ([038da8b](https://github.com/atedeg/mdm/commit/038da8b78adf337c0bc34ad06bb224abceaed0a7)) * **utils:** move givens from outside compatnion objects and add Distance type class ([64cde37](https://github.com/atedeg/mdm/commit/64cde377efc26a844ce93899390df5939f0ec9f6)) ### Features * add new batch event ([9c0f30f](https://github.com/atedeg/mdm/commit/9c0f30fed2e8192ce96b1aa86f082c5cdf0c6dfa)) * add stocking bounded context ([028ee56](https://github.com/atedeg/mdm/commit/028ee56eff798fa7a6f63eabdb19e77c7ace4b79)) * implement stocking actions ([7cbe9c0](https://github.com/atedeg/mdm/commit/7cbe9c0fa2a6dc7b2ecdfbba9a32d16bdcb17695)) * **stocking:** integrate stocking bounded context with shared kernel ([099bc05](https://github.com/atedeg/mdm/commit/099bc05fd440dc3c582e1ed849076b57c6ca70a9)) --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de65c725..ceb98b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# [1.0.0-beta.2](https://github.com/atedeg/mdm/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-08-01) + + +### Bug Fixes + +* add type annotations to silence wartremover ([0f2e14c](https://github.com/atedeg/mdm/commit/0f2e14c94e479848e2f83052d48145db449a353c)) +* **products:** change macro to generate list of weights per cheese type ([038da8b](https://github.com/atedeg/mdm/commit/038da8b78adf337c0bc34ad06bb224abceaed0a7)) +* **utils:** move givens from outside compatnion objects and add Distance type class ([64cde37](https://github.com/atedeg/mdm/commit/64cde377efc26a844ce93899390df5939f0ec9f6)) + + +### Features + +* add new batch event ([9c0f30f](https://github.com/atedeg/mdm/commit/9c0f30fed2e8192ce96b1aa86f082c5cdf0c6dfa)) +* add stocking bounded context ([028ee56](https://github.com/atedeg/mdm/commit/028ee56eff798fa7a6f63eabdb19e77c7ace4b79)) +* implement stocking actions ([7cbe9c0](https://github.com/atedeg/mdm/commit/7cbe9c0fa2a6dc7b2ecdfbba9a32d16bdcb17695)) +* **stocking:** integrate stocking bounded context with shared kernel ([099bc05](https://github.com/atedeg/mdm/commit/099bc05fd440dc3c582e1ed849076b57c6ca70a9)) + # 1.0.0-beta.1 (2022-08-01) From 16197141ed7e906ba6dcf628eb59544bb05e4dc4 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 15:46:26 +0200 Subject: [PATCH 088/329] build: add command alias for ubidoc --- build.sbt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sbt b/build.sbt index 27f4399a..771703d4 100644 --- a/build.sbt +++ b/build.sbt @@ -59,6 +59,8 @@ val commonSettings = Seq( ), ) +addCommandAlias("ubidocGenerate", "clean; unidoc; ubidoc; clean; unidoc") + lazy val root = project .in(file(".")) .enablePlugins(ScalaUnidocPlugin) From f624d25e1728ac7ee701496e97ba78f1629cf7d3 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 15:54:55 +0200 Subject: [PATCH 089/329] ci: remove site separate workflow --- .github/workflows/site.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/site.yml diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml deleted file mode 100644 index 5f4ad7a8..00000000 --- a/.github/workflows/site.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Build and publish documentation site - -on: - push: - branches: - - main - pull_request: - -jobs: - deploy: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v3 - with: - submodules: true - fetch-depth: 0 - - - name: Build Site - run: sbt 'unidoc; ubidoc; clean; unidoc' - - - name: Deploy site - if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} - uses: JamesIves/github-pages-deploy-action@v4.4.0 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: gh-pages - FOLDER: target/site - From a16ef127cc5268f5240fbf9e6b91d55ca4b53aee Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 15:55:53 +0200 Subject: [PATCH 090/329] ci: add deploy site step in workflow using semver version --- .github/workflows/ci.yml | 8 ++++++++ .releaserc.yml | 1 + 2 files changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 165a33f0..1fcf1fdd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,3 +86,11 @@ jobs: sonatype-username: ${{ secrets.SONATYPE_USERNAME }} sonatype-password: ${{ secrets.SONATYPE_PASSWORD }} github-token: ${{ steps.atedeg-bot.outputs.token }} + + - name: Deploy site + if: github.ref == 'refs/heads/main' + uses: JamesIves/github-pages-deploy-action@v4.4.0 + with: + GITHUB_TOKEN: ${{ steps.atedeg-bot.outputs.token }} + BRANCH: gh-pages + FOLDER: target/site diff --git a/.releaserc.yml b/.releaserc.yml index 587a8348..246eaae7 100644 --- a/.releaserc.yml +++ b/.releaserc.yml @@ -15,6 +15,7 @@ plugins: - publishCmd: | export CI_COMMIT_TAG="true" sbt ci-release + sbt 'set ThisBuild / version := "${nextRelease.version}"' ubidocGenerate - - '@semantic-release/git' - assets: - CHANGELOG.md From b92c8352b8b491cce958fbd1647b2bb86b0a8d33 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 25 Jul 2022 09:49:09 +0200 Subject: [PATCH 091/329] build: add production subproject --- build.sbt | 6 ++++++ .../scala/dev/atedeg/mdm/production/Types.scala | 2 ++ .../mdm/production/Tests.scala.semanticdb | Bin 0 -> 3835 bytes .../utils/LiteralConversions.scala.semanticdb | Bin 0 -> 17551 bytes .../mdm/utils/NumericOps.scala.semanticdb | Bin 0 -> 45780 bytes .../atedeg/mdm/utils/Refined.scala.semanticdb | Bin 0 -> 60433 bytes .../dev/atedeg/mdm/utils/Utils.scala.semanticdb | Bin 0 -> 2231 bytes .../mdm/utils/monads/Monads.scala.semanticdb | Bin 0 -> 36802 bytes .../mdm/utils/monads/Stacks.scala.semanticdb | Bin 0 -> 13433 bytes 9 files changed, 8 insertions(+) create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/Types.scala create mode 100644 production/src/test/scala/dev/atedeg/mdm/production/Tests.scala.semanticdb create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala.semanticdb create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala.semanticdb create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala.semanticdb create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/Utils.scala.semanticdb create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala.semanticdb create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala.semanticdb diff --git a/build.sbt b/build.sbt index 771703d4..d6b4a42c 100644 --- a/build.sbt +++ b/build.sbt @@ -91,6 +91,7 @@ lazy val root = project .aggregate( stocking, utils, + production, `milk-planning`, `products-shared-kernel`, ) @@ -107,6 +108,11 @@ lazy val `milk-planning` = project .settings(commonSettings) .dependsOn(utils, `products-shared-kernel`) +lazy val production = project + .in(file("production")) + .dependsOn(utils) + .settings(commonSettings) + lazy val `products-shared-kernel` = project .in(file("products-shared-kernel")) .settings(commonSettings) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala new file mode 100644 index 00000000..7d895bde --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -0,0 +1,2 @@ +package dev.atedeg.mdm.production + diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala.semanticdb b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala.semanticdb new file mode 100644 index 0000000000000000000000000000000000000000..b4003af2dcdfd8456a72d5a9f651d77b4bb9c607 GIT binary patch literal 3835 zcmb_eT~8ZF6wO+;>kh^&Lkx=>I}3&coZv||fa9i$N}Lcy%9oU&N`1<*-ogg6i@a;3 zzU8@({SEyG{d29XTzXBaw0GASvYi9$nCpFmlNM3DQt_Z zRzU82J@HlGoP^B}KY6|vJ^rw<**2=?r=MFz34GGDKeQIzpx0_^bd{RZbpF(GKVED7 zrID$bMx|8{wM7Y^3}}@u(D{#WfDLT_h`l-d=sHm|UES5KOna1VPV_|>cm=*l9a-@2 z+$3LQrZL+pi2sBy|7ql3jc(C9bTb$p$y1N``TyqH(P-#~XRgEc{NMNG@*S2;%O6Mif|rkd6UpQ zw*c4;fz1HBPAf@U5N(NQ717m+wzkLW1*jJ;sTcGaU|&+-fOQuq1C5_^8`VmB(J0~8Odm!$;V#jy8w97E{#!#Kem#i30piZY>%p7idm;KG zpVlSHY3}woC&M2GT(@yeS)H{`&8!x>Rw>h38{AWP3_8~K?=Z#sj zOdn>FdFQykJ@f*pV?n2aqp090){;8Oxg=JSb03^JaOQAlo0B+vs9jKWQ1odOJ_9A? z9;(cPG7qCxCso<=`xguf22IW2E>_`U^$J{EMCHW{F7o{?fwPplTkg97hB3{=r0>h1 zEKj2(eXoI1OWhZDu?`pOSKwj;l^YpcvVV=L@@O@XYE}N9jD6cxe%YYZmMLMUz{27YS8bV9v3awoVDKAtb z_g4GW?Dn?r^|m|v51#aPt=>-eVRv_L`-x?Bw!6LFUgyE#bM!7}f<+&ZE-70?K{ji* z$@=H-5l>%-g~4d(xa@5Zdi*8a##_b3Cn<#Fp!h)%ijj;$a(uj0gkmJ4kQ^!OA(5Zj6!mBsAk7VMj<&*YDz~jl2J&G8THg+B%_cVKBm%9jARs&BWYa`ijj;$ na>6i0C`K|0$+5Gn2*pT7Avuh86`>f(C?rSJV?`)N(hL0$7Oxw2 literal 0 HcmV?d00001 diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala.semanticdb b/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala.semanticdb new file mode 100644 index 0000000000000000000000000000000000000000..10bd437b59e1c92220d78b119b712a31f3bc621b GIT binary patch literal 17551 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 literal 0 HcmV?d00001 diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala.semanticdb b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala.semanticdb new file mode 100644 index 0000000000000000000000000000000000000000..aa099bb4eb616f00d65d940583ea9be5dc7010a3 GIT binary patch literal 60433 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쭆j~!wXpIa@Y7?L|hauH-WGTeBDeO*bE`$`a-xTap z;gLio)qebPAOxwx?sdS2b!ho8p%WeEDvYiT$tZPfx&!*#foPK}e<;?-xf{*u7lNSzw%I&tno3-K8 z7xBZvV2|<=DU41Rab1+%+pu9OEHlANiqyIdUa}2)CZrOYvT@2sic~(0UHddryYk;% zS63>40O2Lms8*+ABRa-GpqoabmqwyTLRTS>FLtNRQ2-g!0wU8Lg-IQN5R2V;OjKz( zjVLvZU2Yw6fQc%0r?JN@Fd-8_r%qJPI}sFX zI9bJ&1cnnU=cQap0|>ElUdojw03lY+i@4&29-W9CI`u2(orv{2sg?6muIvX8V&&Y= z6%nL#AxP=c3Q~lobs2OV z){$R+6C;i0`!x9X29b}NZvd|j>dz;lyfpG};hqUmp4cx)qqrbwCDlTtWD2$&(xxII z1Q@aHkVedzrfxg9*dVl6WmK9qxTdF4#NyC}di=iv@Rs{xb3AUuV{yQ^*7tZ~Y(k9K zh@aM?bXZ^Xle`r%6^#tB)3J`7+je$u@9gaE+uGIF)6tXe*m0n@e_MY~XK&xu?OnYi z!yrYS_ddvv8H2`(wBdWQ&)Q4-;4MsPzC^OMedLr;S%X^*_>h5 z3mN9GdEMGFG6FQ!>BPev_t83tyHEP9UfK8WWD&W$J$4`1zH|B$#Z^N%g7+mcF&Xa_4XKvUCj>n-KP!48s&M^Ig`xv^!ODJ zUwl93iMbhE6BQr%T?UBiIKV;v1nb3T$Od;Ebzr*Bp@?N}cgcw>xF$2>DaHDs%*CP2NrSC{F%%c0(VvYOWx?b~pjug9s4TkJ55T#?g> z&AknL!noI)PCYqe++nm@Eyk9zdWu?-e=APdx}=pb{Mm*Z=c~)y4$mH($&8A_LbyI~ zM8ut~I1MePhSB$?R!=C9gjZA?>hgI-_X%05pi42xQ=09{D<+}|pH~$Bk-ee|yTB`M zZyC88B)H}6v%RMsX1Gs^zKF|1d>DH~>_v89OQQn`5ZsG5lX?pi57f#Zxah?zro;m19+w7v2x6SSo za+sy2@s`=nc4q=nGf4T2zp5gn=*EA(2}FtJWWfaLNmvTH+ecm#-X$GP^KUWZo33{e a|C8;IM83dK>}**KDBT)9%EJXd>i+=A7#LUp literal 0 HcmV?d00001 diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Utils.scala.semanticdb b/utils/src/main/scala/dev/atedeg/mdm/utils/Utils.scala.semanticdb new file mode 100644 index 0000000000000000000000000000000000000000..5629bbb7c8c7046d3b9abf19b36479a5d80dc69f GIT binary patch literal 2231 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 vK@bE7kJ0oi+6&o2ap7#CFf%hfGrcx@c7A Date: Mon, 25 Jul 2022 15:32:53 +0200 Subject: [PATCH 092/329] feat: add ubiquitous language,closes #75 --- .../dev/atedeg/mdm/production/Types.scala | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index 7d895bde..e55a9490 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -1,2 +1,59 @@ package dev.atedeg.mdm.production +import java.util.UUID + +type Product = Int // TODO: shared kernel +type CheeseType = Int // TODO: shared kernel +type Quantity = Int // TODO: maybe is in utils +type WeightInQuintals = Int // TODO: maybe is in utils +type ProductionID = UUID // TODO: make me a case class + +enum Production: + /** + * A [[Production production]] that needs to be started, it specifies the [[Product product]] to produce + * and the [[Quantity quantity]] in which it needs to be produced. + */ + case ToStart(ID: ProductionID, productToProduce: Product, quantityToProduce: Quantity) + + /** + * A [[Production production]] that has already started, it specifies the [[Product product]] that is being produced + * and the [[Quantity quantity]] in which it is being produced. + */ + case InProgress(ID: ProductionID, productInProduction: Product, quantityInProduction: Quantity) + + /** + * A [[Production production]] that ended, it has a [[LotNumber lot number]] and specified the [[Product product]] + * that was produced and in which [[Quantity quantity]] it was produced. + */ + case Ended(ID: ProductionID, lotNumber: LotNumber, producedProduct: Product, producedQuantity: Quantity) + +/** + * A lot number. TODO: ask domain experts how it can be obtained. + */ +final case class LotNumber() + +/** + * A list of [[RecipeLine ingredients and the respective quantity]] needed to produce a quintal of a product. + */ +final case class Recipe(lines: List[RecipeLine]) + +/** + * A line of a [[Recipe recipe]] containing an [[Ingredient ingredient]] and the [[WeightInQuintals weight in quintals]] + * of it needed by the recipe. + */ +final case class RecipeLine(ingredient: Ingredient, quintalsNeeded: WeightInQuintals) + +/** + * An ingredient that may be needed by a [[Recipe recipe]]. + */ +enum Ingredient: + case Milk + case Cream + case Rennet + case Salt + case Probiotics + +/** + * Associates to each [[CheeseType cheese type]] the [[Recipe recipe]] to produce a quintal of it. + */ +type RecipeBook = CheeseType => Recipe From 5d50f1221cb6fe5348a711cba2d5d3b0edb81c66 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 25 Jul 2022 15:33:54 +0200 Subject: [PATCH 093/329] feat: add domain actions and events, closes #77 --- .../dev/atedeg/mdm/production/Actions.scala | 22 +++++++++++++++++++ .../dev/atedeg/mdm/production/Errors.scala | 6 +++++ .../dev/atedeg/mdm/production/Events.scala | 17 ++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/Actions.scala create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/Errors.scala create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/Events.scala diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala new file mode 100644 index 00000000..00430d7b --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -0,0 +1,22 @@ +package dev.atedeg.mdm.production + +import cats.Monad +import dev.atedeg.mdm.utils.{CanRaise, Emits} +import OutgoingEvent.* + +// TODO: Does this action emits a "startProduction" event that is used by the machines? Or does it +// communicate in a different way with the smart machines in order to start the production? +/** + * Starts a [[Production.ToStart production]] by calculating the quintals of [[Ingredient ingredients]] needed to + * produce the specified [[Product product]]; the [[Ingredient ingredients]] needed are specified + * by the [[Recipe recipe]] read from a [[RecipeBook recipe book]]. + */ +def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[IngredientUsed]] + (recipeBook: RecipeBook) + (production: Production.ToStart): Production.InProgress = ??? + +/** + * Ends a [[Production.InProgress production]] by assigning it a [[LotNumber lot number]]. + */ +def endProduction[M[_]: Monad: Emits[ProductionEnded]] + (production: Production.InProgress): Production.Ended = ??? \ No newline at end of file diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Errors.scala b/production/src/main/scala/dev/atedeg/mdm/production/Errors.scala new file mode 100644 index 00000000..a35e2191 --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/Errors.scala @@ -0,0 +1,6 @@ +package dev.atedeg.mdm.production + +/** + * Error raised in case there is no [[Recipe recipe]] for a given [[CheeseType cheese type]]. + */ +final case class MissingRecipe(forType: CheeseType) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala new file mode 100644 index 00000000..4ab3c90c --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala @@ -0,0 +1,17 @@ +package dev.atedeg.mdm.production + +/** + * The events that may be produced by the bounded context. + */ +enum OutgoingEvent: + /** + * Fired when an [[Ingredient ingredient]] is used to [[startProduction() start a production]]. + * It specifies the [[Ingredient ingredient]] used and the [[WeightInQuintals quintals]] consumed. + */ + case IngredientUsed(ingredient: Ingredient, weight: WeightInQuintals) + + /** + * Fired when a [[Production.InProgress production]] is terminated, given a + * [[LotNumber lot number]] and sent to the refrigeration room. + */ + case ProductionEnded(productionID: ProductionID, lotNumber: LotNumber) From 3c903ea4ff80536c2411083f28517c47e9eb71f3 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 25 Jul 2022 15:34:27 +0200 Subject: [PATCH 094/329] docs: add production bounded context documentation first draft --- docs/_docs/production.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/_docs/production.md diff --git a/docs/_docs/production.md b/docs/_docs/production.md new file mode 100644 index 00000000..5ad9be29 --- /dev/null +++ b/docs/_docs/production.md @@ -0,0 +1,19 @@ +--- +title: Production +--- + +# Production +Every day the dairyman receives instructions from Raffaella on the cheese he needs to +produce for the day. A production specifies the quantity of a product that needs to +be produced. Each type of cheese has a recipe which has different lines that specify +the quintals of each ingredient needed to produce a quintal of the given type of +cheese. +> _e.g._ The recipe for a quintal of ricotta requires 1.5 quintals of milk +> a tenth of quintal of rennet and a tenth of quintal of salt + +Once the appropriate recipe is chosen the production can start by retrieving the +needed ingredients. + +Once an in-progress production ends (the smart machines will send an appropriate message +to signal the end of the process), the produced cheese is assigned a lot number +and stored in a refrigeration room. \ No newline at end of file From be883490c5b887c4027ac06c408bb5b2cc98b297 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 25 Jul 2022 23:38:04 +0200 Subject: [PATCH 095/329] refactor: refactor types definition --- .../main/scala/dev/atedeg/mdm/production/Types.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index e55a9490..6a811016 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -2,7 +2,8 @@ package dev.atedeg.mdm.production import java.util.UUID -type Product = Int // TODO: shared kernel +final case class Product(cheeseType: CheeseType, weight: WeightInGrams) // TODO: shared kernel +type WeightInGrams = Int type CheeseType = Int // TODO: shared kernel type Quantity = Int // TODO: maybe is in utils type WeightInQuintals = Int // TODO: maybe is in utils @@ -13,19 +14,19 @@ enum Production: * A [[Production production]] that needs to be started, it specifies the [[Product product]] to produce * and the [[Quantity quantity]] in which it needs to be produced. */ - case ToStart(ID: ProductionID, productToProduce: Product, quantityToProduce: Quantity) + case ToStart(ID: ProductionID, productToProduce: Product, unitsToProduce: Quantity) /** * A [[Production production]] that has already started, it specifies the [[Product product]] that is being produced * and the [[Quantity quantity]] in which it is being produced. */ - case InProgress(ID: ProductionID, productInProduction: Product, quantityInProduction: Quantity) + case InProgress(ID: ProductionID, productInProduction: Product, unitsInProduction: Quantity) /** * A [[Production production]] that ended, it has a [[LotNumber lot number]] and specified the [[Product product]] * that was produced and in which [[Quantity quantity]] it was produced. */ - case Ended(ID: ProductionID, lotNumber: LotNumber, producedProduct: Product, producedQuantity: Quantity) + case Ended(ID: ProductionID, lotNumber: LotNumber, producedProduct: Product, producedUnits: Quantity) /** * A lot number. TODO: ask domain experts how it can be obtained. @@ -56,4 +57,4 @@ enum Ingredient: /** * Associates to each [[CheeseType cheese type]] the [[Recipe recipe]] to produce a quintal of it. */ -type RecipeBook = CheeseType => Recipe +type RecipeBook = CheeseType => Option[Recipe] From 474ce44f9d7c849307775cd233fe22bc55fc5e81 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 25 Jul 2022 23:39:27 +0200 Subject: [PATCH 096/329] feat: add first draft implementation of one of the actions --- .../scala/dev/atedeg/mdm/production/Actions.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index 00430d7b..092f4741 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -1,7 +1,8 @@ package dev.atedeg.mdm.production import cats.Monad -import dev.atedeg.mdm.utils.{CanRaise, Emits} +import cats.syntax.all.* +import dev.atedeg.mdm.utils.* import OutgoingEvent.* // TODO: Does this action emits a "startProduction" event that is used by the machines? Or does it @@ -13,7 +14,16 @@ import OutgoingEvent.* */ def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[IngredientUsed]] (recipeBook: RecipeBook) - (production: Production.ToStart): Production.InProgress = ??? + (production: Production.ToStart) + : M[Production.InProgress] = + val typeToProduce = production.productToProduce.cheeseType + val gramsOfSingleUnit = production.productToProduce.weight + for { + recipe <- recipeBook(typeToProduce) ifMissingRaise MissingRecipe(typeToProduce) + quintalsToProduce = production.unitsToProduce * gramsOfSingleUnit//.toQuintals + neededIngredients = recipe.lines.map(l => (l.ingredient, l.quintalsNeeded * quintalsToProduce)) + _ <- neededIngredients.forEachDo((i, w) => emit(IngredientUsed(i, w): IngredientUsed)) + } yield Production.InProgress(production.ID, production.productToProduce, production.unitsToProduce) /** * Ends a [[Production.InProgress production]] by assigning it a [[LotNumber lot number]]. From b2d001e23ba9a0a141386762c1b00c5b3a514fa6 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Tue, 26 Jul 2022 12:29:26 +0200 Subject: [PATCH 097/329] chore: add utility definitions --- .../dev/atedeg/mdm/production/Utils.scala | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/Utils.scala diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala b/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala new file mode 100644 index 00000000..8fd50e13 --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala @@ -0,0 +1,26 @@ +package dev.atedeg.mdm.production + +import scala.annotation.targetName + +type PositiveDouble = Double // TODO: get from utils +final case class WeightInGrams(grams: PositiveDouble) +final case class WeightInQuintals(quintals: PositiveDouble) + +extension (weight: WeightInGrams) + def map(f: PositiveDouble => PositiveDouble): WeightInGrams = WeightInGrams(f(weight.grams)) + def toQuintals: WeightInQuintals = WeightInQuintals(weight.grams / 100_000) + +extension (weight: WeightInQuintals) + def map(f: PositiveDouble => PositiveDouble): WeightInQuintals = WeightInQuintals(f(weight.quintals)) + def toGrams: WeightInGrams = WeightInGrams(weight.quintals * 100_000) + @targetName("multiply") def *(other: WeightInQuintals) = weight.map(_ * other.quintals) + +extension (q: Quantity) + @targetName("multiplyGrams") def *(weight: WeightInGrams): WeightInGrams = weight.map(_ * q) + @targetName("multiplyQuintals") def *(weight: WeightInQuintals): WeightInQuintals = weight.map(_ * q) + +extension (q: QuintalsOfIngredient) + def map(f: PositiveDouble => PositiveDouble): QuintalsOfIngredient = + QuintalsOfIngredient(q.quintals.map(f), q.ingredient) + + def *(w: WeightInQuintals): QuintalsOfIngredient = QuintalsOfIngredient(w * q.quintals, q.ingredient) \ No newline at end of file From 9fcd177ccf54e17a3e97980e726b063c101e02a6 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Tue, 26 Jul 2022 12:29:58 +0200 Subject: [PATCH 098/329] refactor: rework the ubiquitous language and events --- .../dev/atedeg/mdm/production/Actions.scala | 24 ++++++++++++++----- .../dev/atedeg/mdm/production/Events.scala | 7 +++--- .../dev/atedeg/mdm/production/Types.scala | 14 ++++------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index 092f4741..bbef17ec 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -5,14 +5,18 @@ import cats.syntax.all.* import dev.atedeg.mdm.utils.* import OutgoingEvent.* -// TODO: Does this action emits a "startProduction" event that is used by the machines? Or does it +// TODO: Does this action emit a "startProduction" event that is used by the machines? Or does it // communicate in a different way with the smart machines in order to start the production? +// TODO: Also maybe a better event would be a StartProduction event with the quintals of ingredients +// needed and the production ID (instead of many small events that are harder to keep together)!!! +// This event would represent the signal for the machines to assemble the materials and start the +// production process. /** * Starts a [[Production.ToStart production]] by calculating the quintals of [[Ingredient ingredients]] needed to * produce the specified [[Product product]]; the [[Ingredient ingredients]] needed are specified * by the [[Recipe recipe]] read from a [[RecipeBook recipe book]]. */ -def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[IngredientUsed]] +def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[StartProduction]] (recipeBook: RecipeBook) (production: Production.ToStart) : M[Production.InProgress] = @@ -20,13 +24,21 @@ def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[IngredientUsed]] val gramsOfSingleUnit = production.productToProduce.weight for { recipe <- recipeBook(typeToProduce) ifMissingRaise MissingRecipe(typeToProduce) - quintalsToProduce = production.unitsToProduce * gramsOfSingleUnit//.toQuintals - neededIngredients = recipe.lines.map(l => (l.ingredient, l.quintalsNeeded * quintalsToProduce)) - _ <- neededIngredients.forEachDo((i, w) => emit(IngredientUsed(i, w): IngredientUsed)) + quintalsToProduce = (production.unitsToProduce * gramsOfSingleUnit).toQuintals + neededIngredients = recipe.lines.map(_ * quintalsToProduce) + _ <- emit(StartProduction(neededIngredients): StartProduction) } yield Production.InProgress(production.ID, production.productToProduce, production.unitsToProduce) /** * Ends a [[Production.InProgress production]] by assigning it a [[LotNumber lot number]]. */ def endProduction[M[_]: Monad: Emits[ProductionEnded]] - (production: Production.InProgress): Production.Ended = ??? \ No newline at end of file + (production: Production.InProgress): M[Production.Ended] = for { + lotNumber: LotNumber <- getLotNumber + id = production.ID + producedProduct = production.productInProduction + unitsProduced = production.unitsInProduction + _ <- emit(ProductionEnded(id, lotNumber): ProductionEnded) + } yield Production.Ended(id, lotNumber, producedProduct, unitsProduced) + +def getLotNumber[M[_]: Monad]: M[LotNumber] = ??? \ No newline at end of file diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala index 4ab3c90c..3ea04a0c 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala @@ -5,10 +5,11 @@ package dev.atedeg.mdm.production */ enum OutgoingEvent: /** - * Fired when an [[Ingredient ingredient]] is used to [[startProduction() start a production]]. - * It specifies the [[Ingredient ingredient]] used and the [[WeightInQuintals quintals]] consumed. + * Fired when a [[Production.ToStart production]] needs to be started, specifies the + * [[QuintalsOfIngredient needed ingredients and the quantity]] necessary to sustain the + * production. */ - case IngredientUsed(ingredient: Ingredient, weight: WeightInQuintals) + case StartProduction(neededIngredient: List[QuintalsOfIngredient]) /** * Fired when a [[Production.InProgress production]] is terminated, given a diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index 6a811016..f39f51ce 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -1,13 +1,10 @@ package dev.atedeg.mdm.production -import java.util.UUID final case class Product(cheeseType: CheeseType, weight: WeightInGrams) // TODO: shared kernel -type WeightInGrams = Int type CheeseType = Int // TODO: shared kernel type Quantity = Int // TODO: maybe is in utils -type WeightInQuintals = Int // TODO: maybe is in utils -type ProductionID = UUID // TODO: make me a case class +type ProductionID = java.util.UUID // TODO: make me a case class enum Production: /** @@ -34,15 +31,14 @@ enum Production: final case class LotNumber() /** - * A list of [[RecipeLine ingredients and the respective quantity]] needed to produce a quintal of a product. + * A list of [[QuintalsOfIngredient ingredients and the respective quintals]] needed to produce a quintal of a product. */ -final case class Recipe(lines: List[RecipeLine]) +final case class Recipe(lines: List[QuintalsOfIngredient]) /** - * A line of a [[Recipe recipe]] containing an [[Ingredient ingredient]] and the [[WeightInQuintals weight in quintals]] - * of it needed by the recipe. + * An [[Ingredient ingredient]] and a [[WeightInQuintals weight in quintals]]. */ -final case class RecipeLine(ingredient: Ingredient, quintalsNeeded: WeightInQuintals) +final case class QuintalsOfIngredient(quintals: WeightInQuintals, ingredient: Ingredient) /** * An ingredient that may be needed by a [[Recipe recipe]]. From 9a4e26371a70f8f8b5f963b43a4ed32d5628c394 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Tue, 26 Jul 2022 14:18:26 +0200 Subject: [PATCH 099/329] style: scalafmt --- .../dev/atedeg/mdm/production/Actions.scala | 49 ++++++++----------- .../dev/atedeg/mdm/production/Types.scala | 1 - .../dev/atedeg/mdm/production/Utils.scala | 3 +- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index bbef17ec..b64f7fb7 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -1,44 +1,37 @@ package dev.atedeg.mdm.production +import OutgoingEvent.* import cats.Monad import cats.syntax.all.* + import dev.atedeg.mdm.utils.* -import OutgoingEvent.* -// TODO: Does this action emit a "startProduction" event that is used by the machines? Or does it -// communicate in a different way with the smart machines in order to start the production? -// TODO: Also maybe a better event would be a StartProduction event with the quintals of ingredients -// needed and the production ID (instead of many small events that are harder to keep together)!!! -// This event would represent the signal for the machines to assemble the materials and start the -// production process. /** * Starts a [[Production.ToStart production]] by calculating the quintals of [[Ingredient ingredients]] needed to * produce the specified [[Product product]]; the [[Ingredient ingredients]] needed are specified * by the [[Recipe recipe]] read from a [[RecipeBook recipe book]]. */ -def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[StartProduction]] - (recipeBook: RecipeBook) - (production: Production.ToStart) - : M[Production.InProgress] = - val typeToProduce = production.productToProduce.cheeseType - val gramsOfSingleUnit = production.productToProduce.weight - for { - recipe <- recipeBook(typeToProduce) ifMissingRaise MissingRecipe(typeToProduce) - quintalsToProduce = (production.unitsToProduce * gramsOfSingleUnit).toQuintals - neededIngredients = recipe.lines.map(_ * quintalsToProduce) - _ <- emit(StartProduction(neededIngredients): StartProduction) - } yield Production.InProgress(production.ID, production.productToProduce, production.unitsToProduce) +def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[StartProduction]](recipeBook: RecipeBook)( + production: Production.ToStart, +): M[Production.InProgress] = + val typeToProduce = production.productToProduce.cheeseType + val gramsOfSingleUnit = production.productToProduce.weight + for { + recipe <- recipeBook(typeToProduce) ifMissingRaise MissingRecipe(typeToProduce) + quintalsToProduce = (production.unitsToProduce * gramsOfSingleUnit).toQuintals + neededIngredients = recipe.lines.map(_ * quintalsToProduce) + _ <- emit(StartProduction(neededIngredients): StartProduction) + } yield Production.InProgress(production.ID, production.productToProduce, production.unitsToProduce) /** * Ends a [[Production.InProgress production]] by assigning it a [[LotNumber lot number]]. */ -def endProduction[M[_]: Monad: Emits[ProductionEnded]] - (production: Production.InProgress): M[Production.Ended] = for { - lotNumber: LotNumber <- getLotNumber - id = production.ID - producedProduct = production.productInProduction - unitsProduced = production.unitsInProduction - _ <- emit(ProductionEnded(id, lotNumber): ProductionEnded) - } yield Production.Ended(id, lotNumber, producedProduct, unitsProduced) +def endProduction[M[_]: Monad: Emits[ProductionEnded]](production: Production.InProgress): M[Production.Ended] = for { + lotNumber <- getLotNumber + id = production.ID + producedProduct = production.productInProduction + unitsProduced = production.unitsInProduction + _ <- emit(ProductionEnded(id, lotNumber): ProductionEnded) +} yield Production.Ended(id, lotNumber, producedProduct, unitsProduced) -def getLotNumber[M[_]: Monad]: M[LotNumber] = ??? \ No newline at end of file +def getLotNumber[M[_]: Monad]: M[LotNumber] = ??? diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index f39f51ce..022df6b1 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -1,6 +1,5 @@ package dev.atedeg.mdm.production - final case class Product(cheeseType: CheeseType, weight: WeightInGrams) // TODO: shared kernel type CheeseType = Int // TODO: shared kernel type Quantity = Int // TODO: maybe is in utils diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala b/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala index 8fd50e13..b591e8ec 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala @@ -20,7 +20,8 @@ extension (q: Quantity) @targetName("multiplyQuintals") def *(weight: WeightInQuintals): WeightInQuintals = weight.map(_ * q) extension (q: QuintalsOfIngredient) + def map(f: PositiveDouble => PositiveDouble): QuintalsOfIngredient = QuintalsOfIngredient(q.quintals.map(f), q.ingredient) - def *(w: WeightInQuintals): QuintalsOfIngredient = QuintalsOfIngredient(w * q.quintals, q.ingredient) \ No newline at end of file + def *(w: WeightInQuintals): QuintalsOfIngredient = QuintalsOfIngredient(w * q.quintals, q.ingredient) From db5773486f66f1a02a7232091fe48348a357c0d5 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Tue, 26 Jul 2022 14:31:17 +0200 Subject: [PATCH 100/329] chore(utils): add extension methods --- .../src/main/scala/dev/atedeg/mdm/production/Utils.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala b/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala index b591e8ec..2d1e18c7 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala @@ -14,14 +14,15 @@ extension (weight: WeightInQuintals) def map(f: PositiveDouble => PositiveDouble): WeightInQuintals = WeightInQuintals(f(weight.quintals)) def toGrams: WeightInGrams = WeightInGrams(weight.quintals * 100_000) @targetName("multiply") def *(other: WeightInQuintals) = weight.map(_ * other.quintals) + def of(ingredient: Ingredient): QuintalsOfIngredient = QuintalsOfIngredient(weight, ingredient) extension (q: Quantity) @targetName("multiplyGrams") def *(weight: WeightInGrams): WeightInGrams = weight.map(_ * q) @targetName("multiplyQuintals") def *(weight: WeightInQuintals): WeightInQuintals = weight.map(_ * q) extension (q: QuintalsOfIngredient) + def map(f: PositiveDouble => PositiveDouble): QuintalsOfIngredient = q.quintals.map(f) of q.ingredient + def *(w: WeightInQuintals): QuintalsOfIngredient = (w * q.quintals) of q.ingredient - def map(f: PositiveDouble => PositiveDouble): QuintalsOfIngredient = - QuintalsOfIngredient(q.quintals.map(f), q.ingredient) - - def *(w: WeightInQuintals): QuintalsOfIngredient = QuintalsOfIngredient(w * q.quintals, q.ingredient) +extension (d: PositiveDouble) + def quintals: WeightInQuintals = WeightInQuintals(d) \ No newline at end of file From 83151460bf83c5ed11c2ef90b5b20e1efec6e981 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Tue, 26 Jul 2022 14:51:22 +0200 Subject: [PATCH 101/329] test: add tests' skeleton --- .../dev/atedeg/mdm/production/Tests.scala | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 production/src/test/scala/dev/atedeg/mdm/production/Tests.scala diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala new file mode 100644 index 00000000..aa8497cb --- /dev/null +++ b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala @@ -0,0 +1,37 @@ +package dev.atedeg.mdm.production + +import java.util.UUID +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +trait Mocks { + +} + +class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { + Feature("Production management") { + Scenario("A production is started") { + Given("a production that has to be started") + When("it is started") + Then("an event is emitted to notify that the production should start") + And("the correct amount of products is computed") + And("the production is started") + } + + Scenario("A production is started with no recipe") { + Given("a production that has to be started") + And("has no recipe") + When("it is started") + Then("an error is raised") + And("no production is started") + } + + Scenario("A production is ended") { + Given("a production that is in progress") + When("it is ended") + Then("it should assign it a correct lot number") + And("emit an event to notify that the production ended") + } + } +} \ No newline at end of file From 3abd9aeb11c349a997ac0384493c6cf886813268 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Fri, 29 Jul 2022 13:29:28 +0200 Subject: [PATCH 102/329] chore: add semanticdb to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 88cde666..e9735077 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.semanticdb + ### Intellij ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 From 438d413111617de32b45261815e5f48371583ec0 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Fri, 29 Jul 2022 13:36:24 +0200 Subject: [PATCH 103/329] style: scalafix and scalafmt --- .../main/scala/dev/atedeg/mdm/production/Actions.scala | 1 + .../src/main/scala/dev/atedeg/mdm/production/Utils.scala | 3 +-- .../src/test/scala/dev/atedeg/mdm/production/Tests.scala | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index b64f7fb7..b3114ac6 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -5,6 +5,7 @@ import cats.Monad import cats.syntax.all.* import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.monads.* /** * Starts a [[Production.ToStart production]] by calculating the quintals of [[Ingredient ingredients]] needed to diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala b/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala index 2d1e18c7..e48e0411 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala @@ -24,5 +24,4 @@ extension (q: QuintalsOfIngredient) def map(f: PositiveDouble => PositiveDouble): QuintalsOfIngredient = q.quintals.map(f) of q.ingredient def *(w: WeightInQuintals): QuintalsOfIngredient = (w * q.quintals) of q.ingredient -extension (d: PositiveDouble) - def quintals: WeightInQuintals = WeightInQuintals(d) \ No newline at end of file +extension (d: PositiveDouble) def quintals: WeightInQuintals = WeightInQuintals(d) diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala index aa8497cb..2d6bd7f3 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala @@ -1,15 +1,15 @@ package dev.atedeg.mdm.production import java.util.UUID + import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers -trait Mocks { - -} +trait Mocks {} class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { + Feature("Production management") { Scenario("A production is started") { Given("a production that has to be started") @@ -34,4 +34,4 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { And("emit an event to notify that the production ended") } } -} \ No newline at end of file +} From 49854796d9965cbb83bd537d6b2af535eca70483 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 15:56:58 +0200 Subject: [PATCH 104/329] chore: remove all *.semanticdb --- .../mdm/production/Tests.scala.semanticdb | Bin 3835 -> 0 bytes .../utils/LiteralConversions.scala.semanticdb | Bin 17551 -> 0 bytes .../mdm/utils/NumericOps.scala.semanticdb | Bin 45780 -> 0 bytes .../atedeg/mdm/utils/Refined.scala.semanticdb | Bin 60433 -> 0 bytes .../dev/atedeg/mdm/utils/Utils.scala.semanticdb | Bin 2231 -> 0 bytes .../mdm/utils/monads/Monads.scala.semanticdb | Bin 36802 -> 0 bytes .../mdm/utils/monads/Stacks.scala.semanticdb | Bin 13433 -> 0 bytes 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 production/src/test/scala/dev/atedeg/mdm/production/Tests.scala.semanticdb delete mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala.semanticdb delete mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/NumericOps.scala.semanticdb delete mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala.semanticdb delete mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/Utils.scala.semanticdb delete mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala.semanticdb delete mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala.semanticdb diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala.semanticdb b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala.semanticdb deleted file mode 100644 index b4003af2dcdfd8456a72d5a9f651d77b4bb9c607..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3835 zcmb_eT~8ZF6wO+;>kh^&Lkx=>I}3&coZv||fa9i$N}Lcy%9oU&N`1<*-ogg6i@a;3 zzU8@({SEyG{d29XTzXBaw0GASvYi9$nCpFmlNM3DQt_Z zRzU82J@HlGoP^B}KY6|vJ^rw<**2=?r=MFz34GGDKeQIzpx0_^bd{RZbpF(GKVED7 zrID$bMx|8{wM7Y^3}}@u(D{#WfDLT_h`l-d=sHm|UES5KOna1VPV_|>cm=*l9a-@2 z+$3LQrZL+pi2sBy|7ql3jc(C9bTb$p$y1N``TyqH(P-#~XRgEc{NMNG@*S2;%O6Mif|rkd6UpQ zw*c4;fz1HBPAf@U5N(NQ717m+wzkLW1*jJ;sTcGaU|&+-fOQuq1C5_^8`VmB(J0~8Odm!$;V#jy8w97E{#!#Kem#i30piZY>%p7idm;KG zpVlSHY3}woC&M2GT(@yeS)H{`&8!x>Rw>h38{AWP3_8~K?=Z#sj zOdn>FdFQykJ@f*pV?n2aqp090){;8Oxg=JSb03^JaOQAlo0B+vs9jKWQ1odOJ_9A? z9;(cPG7qCxCso<=`xguf22IW2E>_`U^$J{EMCHW{F7o{?fwPplTkg97hB3{=r0>h1 zEKj2(eXoI1OWhZDu?`pOSKwj;l^YpcvVV=L@@O@XYE}N9jD6cxe%YYZmMLMUz{27YS8bV9v3awoVDKAtb z_g4GW?Dn?r^|m|v51#aPt=>-eVRv_L`-x?Bw!6LFUgyE#bM!7}f<+&ZE-70?K{ji* z$@=H-5l>%-g~4d(xa@5Zdi*8a##_b3Cn<#Fp!h)%ijj;$a(uj0gkmJ4kQ^!OA(5Zj6!mBsAk7VMj<&*YDz~jl2J&G8THg+B%_cVKBm%9jARs&BWYa`ijj;$ na>6i0C`K|0$+5Gn2*pT7Avuh86`>f(C?rSJV?`)N(hL0$7Oxw2 diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala.semanticdb b/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala.semanticdb deleted file mode 100644 index 10bd437b59e1c92220d78b119b712a31f3bc621b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17551 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 diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala.semanticdb b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala.semanticdb deleted file mode 100644 index aa099bb4eb616f00d65d940583ea9be5dc7010a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60433 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쭆j~!wXpIa@Y7?L|hauH-WGTeBDeO*bE`$`a-xTap z;gLio)qebPAOxwx?sdS2b!ho8p%WeEDvYiT$tZPfx&!*#foPK}e<;?-xf{*u7lNSzw%I&tno3-K8 z7xBZvV2|<=DU41Rab1+%+pu9OEHlANiqyIdUa}2)CZrOYvT@2sic~(0UHddryYk;% zS63>40O2Lms8*+ABRa-GpqoabmqwyTLRTS>FLtNRQ2-g!0wU8Lg-IQN5R2V;OjKz( zjVLvZU2Yw6fQc%0r?JN@Fd-8_r%qJPI}sFX zI9bJ&1cnnU=cQap0|>ElUdojw03lY+i@4&29-W9CI`u2(orv{2sg?6muIvX8V&&Y= z6%nL#AxP=c3Q~lobs2OV z){$R+6C;i0`!x9X29b}NZvd|j>dz;lyfpG};hqUmp4cx)qqrbwCDlTtWD2$&(xxII z1Q@aHkVedzrfxg9*dVl6WmK9qxTdF4#NyC}di=iv@Rs{xb3AUuV{yQ^*7tZ~Y(k9K zh@aM?bXZ^Xle`r%6^#tB)3J`7+je$u@9gaE+uGIF)6tXe*m0n@e_MY~XK&xu?OnYi z!yrYS_ddvv8H2`(wBdWQ&)Q4-;4MsPzC^OMedLr;S%X^*_>h5 z3mN9GdEMGFG6FQ!>BPev_t83tyHEP9UfK8WWD&W$J$4`1zH|B$#Z^N%g7+mcF&Xa_4XKvUCj>n-KP!48s&M^Ig`xv^!ODJ zUwl93iMbhE6BQr%T?UBiIKV;v1nb3T$Od;Ebzr*Bp@?N}cgcw>xF$2>DaHDs%*CP2NrSC{F%%c0(VvYOWx?b~pjug9s4TkJ55T#?g> z&AknL!noI)PCYqe++nm@Eyk9zdWu?-e=APdx}=pb{Mm*Z=c~)y4$mH($&8A_LbyI~ zM8ut~I1MePhSB$?R!=C9gjZA?>hgI-_X%05pi42xQ=09{D<+}|pH~$Bk-ee|yTB`M zZyC88B)H}6v%RMsX1Gs^zKF|1d>DH~>_v89OQQn`5ZsG5lX?pi57f#Zxah?zro;m19+w7v2x6SSo za+sy2@s`=nc4q=nGf4T2zp5gn=*EA(2}FtJWWfaLNmvTH+ecm#-X$GP^KUWZo33{e a|C8;IM83dK>}**KDBT)9%EJXd>i+=A7#LUp diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Utils.scala.semanticdb b/utils/src/main/scala/dev/atedeg/mdm/utils/Utils.scala.semanticdb deleted file mode 100644 index 5629bbb7c8c7046d3b9abf19b36479a5d80dc69f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2231 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 vK@bE7kJ0oi+6&o2ap7#CFf%hfGrcx@c7A Date: Mon, 1 Aug 2022 16:33:42 +0200 Subject: [PATCH 105/329] refactor: use common definitions in utils --- build.sbt | 2 +- .../dev/atedeg/mdm/production/Actions.scala | 17 +++--- .../dev/atedeg/mdm/production/Errors.scala | 2 + .../dev/atedeg/mdm/production/Events.scala | 6 ++- .../dev/atedeg/mdm/production/Types.scala | 52 +++++++++++++------ .../dev/atedeg/mdm/production/Utils.scala | 27 ---------- .../utils/QuintalsOfIngredientOps.scala | 14 +++++ 7 files changed, 66 insertions(+), 54 deletions(-) delete mode 100644 production/src/main/scala/dev/atedeg/mdm/production/Utils.scala create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala diff --git a/build.sbt b/build.sbt index d6b4a42c..dcc3bbb4 100644 --- a/build.sbt +++ b/build.sbt @@ -110,7 +110,7 @@ lazy val `milk-planning` = project lazy val production = project .in(file("production")) - .dependsOn(utils) + .dependsOn(utils, `products-shared-kernel`) .settings(commonSettings) lazy val `products-shared-kernel` = project diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index b3114ac6..dd26093d 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -1,10 +1,14 @@ package dev.atedeg.mdm.production +import java.util.UUID + import OutgoingEvent.* import cats.Monad import cats.syntax.all.* +import dev.atedeg.mdm.production.utils.* import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.* /** @@ -19,20 +23,19 @@ def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[StartProduction] val gramsOfSingleUnit = production.productToProduce.weight for { recipe <- recipeBook(typeToProduce) ifMissingRaise MissingRecipe(typeToProduce) - quintalsToProduce = (production.unitsToProduce * gramsOfSingleUnit).toQuintals + quintalsToProduce = (production.unitsToProduce.n * gramsOfSingleUnit.n).toDecimal / 100_000 neededIngredients = recipe.lines.map(_ * quintalsToProduce) _ <- emit(StartProduction(neededIngredients): StartProduction) } yield Production.InProgress(production.ID, production.productToProduce, production.unitsToProduce) /** - * Ends a [[Production.InProgress production]] by assigning it a [[LotNumber lot number]]. + * Ends a [[Production.InProgress production]] by assigning it a [[BatchID batch ID]]. */ def endProduction[M[_]: Monad: Emits[ProductionEnded]](production: Production.InProgress): M[Production.Ended] = for { - lotNumber <- getLotNumber - id = production.ID + batchID <- generateBatchID producedProduct = production.productInProduction unitsProduced = production.unitsInProduction - _ <- emit(ProductionEnded(id, lotNumber): ProductionEnded) -} yield Production.Ended(id, lotNumber, producedProduct, unitsProduced) + _ <- emit(ProductionEnded(production.ID, batchID): ProductionEnded) +} yield Production.Ended(production.ID, batchID, producedProduct, unitsProduced) -def getLotNumber[M[_]: Monad]: M[LotNumber] = ??? +def generateBatchID[M[_]: Monad]: M[BatchID] = BatchID(UUID.randomUUID).pure diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Errors.scala b/production/src/main/scala/dev/atedeg/mdm/production/Errors.scala index a35e2191..a82c684f 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Errors.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Errors.scala @@ -1,5 +1,7 @@ package dev.atedeg.mdm.production +import dev.atedeg.mdm.products.CheeseType + /** * Error raised in case there is no [[Recipe recipe]] for a given [[CheeseType cheese type]]. */ diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala index 3ea04a0c..624528cf 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala @@ -1,5 +1,7 @@ package dev.atedeg.mdm.production +import cats.data.NonEmptyList + /** * The events that may be produced by the bounded context. */ @@ -9,10 +11,10 @@ enum OutgoingEvent: * [[QuintalsOfIngredient needed ingredients and the quantity]] necessary to sustain the * production. */ - case StartProduction(neededIngredient: List[QuintalsOfIngredient]) + case StartProduction(neededIngredient: NonEmptyList[QuintalsOfIngredient]) /** * Fired when a [[Production.InProgress production]] is terminated, given a * [[LotNumber lot number]] and sent to the refrigeration room. */ - case ProductionEnded(productionID: ProductionID, lotNumber: LotNumber) + case ProductionEnded(productionID: ProductionID, batchID: BatchID) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index 022df6b1..78eadda1 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -1,38 +1,56 @@ package dev.atedeg.mdm.production -final case class Product(cheeseType: CheeseType, weight: WeightInGrams) // TODO: shared kernel -type CheeseType = Int // TODO: shared kernel -type Quantity = Int // TODO: maybe is in utils -type ProductionID = java.util.UUID // TODO: make me a case class +import java.util.UUID + +import cats.data.NonEmptyList + +import dev.atedeg.mdm.products.* +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given + +/** + * Counts the number of units of something. + */ +final case class NumberOfUnits(n: PositiveNumber) enum Production: /** * A [[Production production]] that needs to be started, it specifies the [[Product product]] to produce * and the [[Quantity quantity]] in which it needs to be produced. */ - case ToStart(ID: ProductionID, productToProduce: Product, unitsToProduce: Quantity) + case ToStart(ID: ProductionID, productToProduce: Product, unitsToProduce: NumberOfUnits) /** * A [[Production production]] that has already started, it specifies the [[Product product]] that is being produced * and the [[Quantity quantity]] in which it is being produced. */ - case InProgress(ID: ProductionID, productInProduction: Product, unitsInProduction: Quantity) + case InProgress(ID: ProductionID, productInProduction: Product, unitsInProduction: NumberOfUnits) /** - * A [[Production production]] that ended, it has a [[LotNumber lot number]] and specified the [[Product product]] + * A [[Production production]] that ended, it has a [[BatchID lot number]] and specified the [[Product product]] * that was produced and in which [[Quantity quantity]] it was produced. */ - case Ended(ID: ProductionID, lotNumber: LotNumber, producedProduct: Product, producedUnits: Quantity) + case Ended(ID: ProductionID, batchID: BatchID, producedProduct: Product, producedUnits: NumberOfUnits) /** - * A lot number. TODO: ask domain experts how it can be obtained. + * An ID used to uniquely identify a [[Production production]]. */ -final case class LotNumber() +final case class ProductionID(ID: UUID) + +/** + * An ID used to uniquely identify a batch of cheese. + */ +final case class BatchID(ID: UUID) + +/** + * Associates to each [[CheeseType cheese type]] the [[Recipe recipe]] to produce a quintal of it. + */ +type RecipeBook = CheeseType => Option[Recipe] /** * A list of [[QuintalsOfIngredient ingredients and the respective quintals]] needed to produce a quintal of a product. */ -final case class Recipe(lines: List[QuintalsOfIngredient]) +final case class Recipe(lines: NonEmptyList[QuintalsOfIngredient]) /** * An [[Ingredient ingredient]] and a [[WeightInQuintals weight in quintals]]. @@ -40,7 +58,12 @@ final case class Recipe(lines: List[QuintalsOfIngredient]) final case class QuintalsOfIngredient(quintals: WeightInQuintals, ingredient: Ingredient) /** - * An ingredient that may be needed by a [[Recipe recipe]]. + * A weight expressed in quintals. + */ +final case class WeightInQuintals(n: PositiveDecimal) derives Times + +/** + * An ingredient that may be needed by a [[Recipe recipe]] to produce a [[CheeseType type of cheese]]. */ enum Ingredient: case Milk @@ -48,8 +71,3 @@ enum Ingredient: case Rennet case Salt case Probiotics - -/** - * Associates to each [[CheeseType cheese type]] the [[Recipe recipe]] to produce a quintal of it. - */ -type RecipeBook = CheeseType => Option[Recipe] diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala b/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala deleted file mode 100644 index e48e0411..00000000 --- a/production/src/main/scala/dev/atedeg/mdm/production/Utils.scala +++ /dev/null @@ -1,27 +0,0 @@ -package dev.atedeg.mdm.production - -import scala.annotation.targetName - -type PositiveDouble = Double // TODO: get from utils -final case class WeightInGrams(grams: PositiveDouble) -final case class WeightInQuintals(quintals: PositiveDouble) - -extension (weight: WeightInGrams) - def map(f: PositiveDouble => PositiveDouble): WeightInGrams = WeightInGrams(f(weight.grams)) - def toQuintals: WeightInQuintals = WeightInQuintals(weight.grams / 100_000) - -extension (weight: WeightInQuintals) - def map(f: PositiveDouble => PositiveDouble): WeightInQuintals = WeightInQuintals(f(weight.quintals)) - def toGrams: WeightInGrams = WeightInGrams(weight.quintals * 100_000) - @targetName("multiply") def *(other: WeightInQuintals) = weight.map(_ * other.quintals) - def of(ingredient: Ingredient): QuintalsOfIngredient = QuintalsOfIngredient(weight, ingredient) - -extension (q: Quantity) - @targetName("multiplyGrams") def *(weight: WeightInGrams): WeightInGrams = weight.map(_ * q) - @targetName("multiplyQuintals") def *(weight: WeightInQuintals): WeightInQuintals = weight.map(_ * q) - -extension (q: QuintalsOfIngredient) - def map(f: PositiveDouble => PositiveDouble): QuintalsOfIngredient = q.quintals.map(f) of q.ingredient - def *(w: WeightInQuintals): QuintalsOfIngredient = (w * q.quintals) of q.ingredient - -extension (d: PositiveDouble) def quintals: WeightInQuintals = WeightInQuintals(d) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala new file mode 100644 index 00000000..b25f888d --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala @@ -0,0 +1,14 @@ +package dev.atedeg.mdm.production.utils + +import dev.atedeg.mdm.production.* +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given + +import scala.annotation.targetName + +extension (q: QuintalsOfIngredient) + @targetName("quintalsOfIngredientTimesDecimal") + def *(n: PositiveDecimal) = QuintalsOfIngredient(q.quintals * n.quintals, q.ingredient) + +extension (n: PositiveDecimal) + def quintals : WeightInQuintals = WeightInQuintals(n) \ No newline at end of file From 7718e6eb011f8b1f6c2239dd5eb449a7ad326491 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 17:10:38 +0200 Subject: [PATCH 106/329] refactor: add incoming events and related actions --- .../dev/atedeg/mdm/production/Actions.scala | 26 ++++++++++++------- .../dev/atedeg/mdm/production/Events.scala | 7 +++++ .../dev/atedeg/mdm/production/Types.scala | 12 +++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index dd26093d..169f02e8 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -1,16 +1,24 @@ package dev.atedeg.mdm.production import java.util.UUID - import OutgoingEvent.* import cats.Monad +import cats.data.NonEmptyList import cats.syntax.all.* - import dev.atedeg.mdm.production.utils.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.* +/** + * Prepares the new [[Production.ToStart productions to start]] from a given + * [[ProductionPlan production plan]]. + */ +def setupProductions(productionPlan: ProductionPlan): NonEmptyList[Production.ToStart] = + productionPlan.plan.map(item => Production.ToStart(generateProductionId, item.productToProduce, item.units)) + +private def generateProductionId: ProductionID = ProductionID(UUID.randomUUID) + /** * Starts a [[Production.ToStart production]] by calculating the quintals of [[Ingredient ingredients]] needed to * produce the specified [[Product product]]; the [[Ingredient ingredients]] needed are specified @@ -31,11 +39,11 @@ def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[StartProduction] /** * Ends a [[Production.InProgress production]] by assigning it a [[BatchID batch ID]]. */ -def endProduction[M[_]: Monad: Emits[ProductionEnded]](production: Production.InProgress): M[Production.Ended] = for { - batchID <- generateBatchID - producedProduct = production.productInProduction - unitsProduced = production.unitsInProduction - _ <- emit(ProductionEnded(production.ID, batchID): ProductionEnded) -} yield Production.Ended(production.ID, batchID, producedProduct, unitsProduced) +def endProduction[M[_]: Monad: Emits[ProductionEnded]](production: Production.InProgress): M[Production.Ended] = + val batchID = generateBatchID + val producedProduct = production.productInProduction + val unitsProduced = production.unitsInProduction + emit(ProductionEnded(production.ID, batchID): ProductionEnded) + .thenReturn(Production.Ended(production.ID, batchID, producedProduct, unitsProduced)) -def generateBatchID[M[_]: Monad]: M[BatchID] = BatchID(UUID.randomUUID).pure +private def generateBatchID: BatchID = BatchID(UUID.randomUUID) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala index 624528cf..5033fe8d 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala @@ -18,3 +18,10 @@ enum OutgoingEvent: * [[LotNumber lot number]] and sent to the refrigeration room. */ case ProductionEnded(productionID: ProductionID, batchID: BatchID) + +enum IncomingEvent: + /** + * Specifies the [[ProductionPlan production plan]] for the day with + * the [[Product products]] that need to be produced. + */ + case ProductionPlanReady(productionPlan: ProductionPlan) \ No newline at end of file diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index 78eadda1..e10dac7a 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -8,6 +8,18 @@ import dev.atedeg.mdm.products.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given +/** + * A production plan that specifies how many [[NumberOfUnits units]] of + * [[Product products]] need to be produced. + */ +final case class ProductionPlan(plan: NonEmptyList[ProductionPlanItem]) + +/** + * A single line of a [[ProductionPlan production plan]] that specifies a + * [[Product product]] and how many [[NumberOfUnits units]] of it to produce. + */ +final case class ProductionPlanItem(productToProduce: Product, units: NumberOfUnits) + /** * Counts the number of units of something. */ From b8f3846c52d79f4f76677733b21d5c8ba77e681d Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 18:02:29 +0200 Subject: [PATCH 107/329] test: implement tests --- .../dev/atedeg/mdm/production/Events.scala | 2 +- .../dev/atedeg/mdm/production/Types.scala | 10 +-- .../utils/QuintalsOfIngredientOps.scala | 3 +- .../dev/atedeg/mdm/production/Tests.scala | 74 ++++++++++++++++++- 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala index 5033fe8d..8319e02e 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala @@ -15,7 +15,7 @@ enum OutgoingEvent: /** * Fired when a [[Production.InProgress production]] is terminated, given a - * [[LotNumber lot number]] and sent to the refrigeration room. + * [[BatchID batch ID]] and sent to the refrigeration room. */ case ProductionEnded(productionID: ProductionID, batchID: BatchID) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index e10dac7a..ab2e31be 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -78,8 +78,8 @@ final case class WeightInQuintals(n: PositiveDecimal) derives Times * An ingredient that may be needed by a [[Recipe recipe]] to produce a [[CheeseType type of cheese]]. */ enum Ingredient: - case Milk - case Cream - case Rennet - case Salt - case Probiotics + case Milk extends Ingredient + case Cream extends Ingredient + case Rennet extends Ingredient + case Salt extends Ingredient + case Probiotics extends Ingredient diff --git a/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala index b25f888d..f6518b04 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala @@ -11,4 +11,5 @@ extension (q: QuintalsOfIngredient) def *(n: PositiveDecimal) = QuintalsOfIngredient(q.quintals * n.quintals, q.ingredient) extension (n: PositiveDecimal) - def quintals : WeightInQuintals = WeightInQuintals(n) \ No newline at end of file + def quintals : WeightInQuintals = WeightInQuintals(n) + def of(i: Ingredient): QuintalsOfIngredient = QuintalsOfIngredient(n.quintals, i) diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala index 2d6bd7f3..bf727dfc 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala @@ -1,37 +1,103 @@ package dev.atedeg.mdm.production -import java.util.UUID +import cats.data.NonEmptyList +import dev.atedeg.mdm.products.{CheeseType, Product} +import java.util.UUID import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers +import org.scalatest.EitherValues.* +import dev.atedeg.mdm.production.* +import dev.atedeg.mdm.production.utils.* +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given +import dev.atedeg.mdm.utils.doubleToPositiveDecimal +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.monads.given +import Ingredient.* +import OutgoingEvent.* -trait Mocks {} +extension (n: PositiveNumber) + def ofProd(p: Product): ProductionPlanItem = ProductionPlanItem(p, NumberOfUnits(n)) + +trait Mocks { + val productionPlan: ProductionPlan = ProductionPlan(NonEmptyList.of( + 10_000 ofProd Product.Caciotta(500), + 10_000 ofProd Product.Caciotta(1000), + 10_000 ofProd Product.Ricotta(350), + )) + val List( + caciotta500Production, + caciotta1000Production, + ricotta350Production, + ) = setupProductions(productionPlan).toList + val recipeBook: RecipeBook = Map( + CheeseType.Caciotta -> Recipe(NonEmptyList.of( + 10 of Milk, + 10 of Cream, + 10 of Rennet, + 10 of Salt, + 10 of Probiotics, + )) + ).get +} class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Feature("Production management") { Scenario("A production is started") { Given("a production that has to be started") + val production = caciotta500Production When("it is started") + val startAction: Action[MissingRecipe, StartProduction, Production.InProgress] = + startProduction(recipeBook)(production) + val (events, result) = startAction.execute Then("an event is emitted to notify that the production should start") And("the correct amount of products is computed") + val expectedIngredients = NonEmptyList.of[QuintalsOfIngredient]( + 500 of Milk, + 500 of Cream, + 500 of Rennet, + 500 of Salt, + 500 of Probiotics, + ) + events should contain(StartProduction(expectedIngredients)) And("the production is started") + result.value shouldBe Production.InProgress(production.ID, production.productToProduce, production.unitsToProduce) } Scenario("A production is started with no recipe") { Given("a production that has to be started") + val production = caciotta1000Production And("has no recipe") + val recipeBook = Map[CheeseType, Recipe]() When("it is started") + val startAction: Action[MissingRecipe, StartProduction, Production.InProgress] = + startProduction(recipeBook.get)(production) + val (events, result) = startAction.execute Then("an error is raised") + result.left.value shouldBe MissingRecipe(CheeseType.Caciotta) And("no production is started") + events shouldBe empty } Scenario("A production is ended") { Given("a production that is in progress") + val production: Production.InProgress = Production.InProgress( + ricotta350Production.ID, + ricotta350Production.productToProduce, + ricotta350Production.unitsToProduce, + ) When("it is ended") - Then("it should assign it a correct lot number") - And("emit an event to notify that the production ended") + val endAction: SafeAction[ProductionEnded, Production.Ended] = endProduction(production) + val (events, result) = endAction.execute + Then("it should emit an event to notify that the production ended") + result shouldBe a[Production.Ended] + result.ID shouldBe production.ID + result.producedUnits shouldBe production.unitsInProduction + result.producedProduct shouldBe production.productInProduction + events should contain(ProductionEnded(result.ID, result.batchID)) } } } From 27c7e9ea00868760cd74f7b513a06aa790a62673 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 18:22:58 +0200 Subject: [PATCH 108/329] test: improve coverage --- .../dev/atedeg/mdm/production/Actions.scala | 2 + .../dev/atedeg/mdm/production/Events.scala | 2 +- .../utils/QuintalsOfIngredientOps.scala | 7 +- .../dev/atedeg/mdm/production/Tests.scala | 89 +++++++++---------- 4 files changed, 49 insertions(+), 51 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index 169f02e8..1893fdde 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -1,10 +1,12 @@ package dev.atedeg.mdm.production import java.util.UUID + import OutgoingEvent.* import cats.Monad import cats.data.NonEmptyList import cats.syntax.all.* + import dev.atedeg.mdm.production.utils.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala index 8319e02e..671a8151 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala @@ -24,4 +24,4 @@ enum IncomingEvent: * Specifies the [[ProductionPlan production plan]] for the day with * the [[Product products]] that need to be produced. */ - case ProductionPlanReady(productionPlan: ProductionPlan) \ No newline at end of file + case ProductionPlanReady(productionPlan: ProductionPlan) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala index f6518b04..b187b79f 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala @@ -1,15 +1,16 @@ package dev.atedeg.mdm.production.utils +import scala.annotation.targetName + import dev.atedeg.mdm.production.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given -import scala.annotation.targetName - extension (q: QuintalsOfIngredient) + @targetName("quintalsOfIngredientTimesDecimal") def *(n: PositiveDecimal) = QuintalsOfIngredient(q.quintals * n.quintals, q.ingredient) extension (n: PositiveDecimal) - def quintals : WeightInQuintals = WeightInQuintals(n) + def quintals: WeightInQuintals = WeightInQuintals(n) def of(i: Ingredient): QuintalsOfIngredient = QuintalsOfIngredient(n.quintals, i) diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala index bf727dfc..14d39976 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala @@ -1,67 +1,63 @@ package dev.atedeg.mdm.production -import cats.data.NonEmptyList -import dev.atedeg.mdm.products.{CheeseType, Product} - import java.util.UUID + +import Ingredient.* +import OutgoingEvent.* +import cats.data.NonEmptyList +import org.scalatest.EitherValues.* import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers -import org.scalatest.EitherValues.* + import dev.atedeg.mdm.production.* import dev.atedeg.mdm.production.utils.* +import dev.atedeg.mdm.products.{ CheeseType, Product } import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.doubleToPositiveDecimal +import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.* import dev.atedeg.mdm.utils.monads.given -import Ingredient.* -import OutgoingEvent.* -extension (n: PositiveNumber) - def ofProd(p: Product): ProductionPlanItem = ProductionPlanItem(p, NumberOfUnits(n)) +extension (n: PositiveNumber) def ofProd(p: Product): ProductionPlanItem = ProductionPlanItem(p, NumberOfUnits(n)) trait Mocks { - val productionPlan: ProductionPlan = ProductionPlan(NonEmptyList.of( - 10_000 ofProd Product.Caciotta(500), - 10_000 ofProd Product.Caciotta(1000), - 10_000 ofProd Product.Ricotta(350), - )) - val List( - caciotta500Production, - caciotta1000Production, - ricotta350Production, - ) = setupProductions(productionPlan).toList - val recipeBook: RecipeBook = Map( - CheeseType.Caciotta -> Recipe(NonEmptyList.of( - 10 of Milk, - 10 of Cream, - 10 of Rennet, - 10 of Salt, - 10 of Probiotics, - )) - ).get + private val productionID = ProductionID(UUID.randomUUID) + val production: Production.ToStart = Production.ToStart(productionID, Product.Caciotta(500), NumberOfUnits(10_000)) + val allIngredients: NonEmptyList[Ingredient] = NonEmptyList.of(Milk, Cream, Rennet, Salt, Probiotics) } +@SuppressWarnings(Array("scalafix:DisableSyntax.noValPatterns")) class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Feature("Production management") { + Scenario("A production plan is handled") { + Given("A production plan") + val productionPlan = ProductionPlan( + NonEmptyList.of( + 10_000 ofProd Product.Caciotta(500), + 20_000 ofProd Product.Caciotta(1000), + ), + ) + When("it is used to setup the productions") + val productions = setupProductions(productionPlan) + Then("the final productions should match the plan's ones") + val List(p1, p2) = productions.toList + (p1.unitsToProduce, p1.productToProduce) shouldBe (NumberOfUnits(10_000), Product.Caciotta(500)) + (p2.unitsToProduce, p2.productToProduce) shouldBe (NumberOfUnits(20_000), Product.Caciotta(1000)) + } + Scenario("A production is started") { Given("a production that has to be started") - val production = caciotta500Production + And("a recipe book") + val recipeBook = Map(CheeseType.Caciotta -> Recipe(allIngredients.map(10 of _))).get When("it is started") val startAction: Action[MissingRecipe, StartProduction, Production.InProgress] = startProduction(recipeBook)(production) val (events, result) = startAction.execute Then("an event is emitted to notify that the production should start") And("the correct amount of products is computed") - val expectedIngredients = NonEmptyList.of[QuintalsOfIngredient]( - 500 of Milk, - 500 of Cream, - 500 of Rennet, - 500 of Salt, - 500 of Probiotics, - ) + val expectedIngredients = allIngredients.map(500 of _) events should contain(StartProduction(expectedIngredients)) And("the production is started") result.value shouldBe Production.InProgress(production.ID, production.productToProduce, production.unitsToProduce) @@ -69,12 +65,11 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Scenario("A production is started with no recipe") { Given("a production that has to be started") - val production = caciotta1000Production And("has no recipe") - val recipeBook = Map[CheeseType, Recipe]() + val emptyRecipeBook = Map[CheeseType, Recipe]().get When("it is started") val startAction: Action[MissingRecipe, StartProduction, Production.InProgress] = - startProduction(recipeBook.get)(production) + startProduction(emptyRecipeBook)(production) val (events, result) = startAction.execute Then("an error is raised") result.left.value shouldBe MissingRecipe(CheeseType.Caciotta) @@ -84,19 +79,19 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Scenario("A production is ended") { Given("a production that is in progress") - val production: Production.InProgress = Production.InProgress( - ricotta350Production.ID, - ricotta350Production.productToProduce, - ricotta350Production.unitsToProduce, + val productionInProgress: Production.InProgress = Production.InProgress( + production.ID, + production.productToProduce, + production.unitsToProduce, ) When("it is ended") - val endAction: SafeAction[ProductionEnded, Production.Ended] = endProduction(production) + val endAction: SafeAction[ProductionEnded, Production.Ended] = endProduction(productionInProgress) val (events, result) = endAction.execute Then("it should emit an event to notify that the production ended") result shouldBe a[Production.Ended] - result.ID shouldBe production.ID - result.producedUnits shouldBe production.unitsInProduction - result.producedProduct shouldBe production.productInProduction + result.ID shouldBe productionInProgress.ID + result.producedUnits shouldBe productionInProgress.unitsInProduction + result.producedProduct shouldBe productionInProgress.productInProduction events should contain(ProductionEnded(result.ID, result.batchID)) } } From 590ebc00189fa7b9c85bee36ab1930c5320ad93d Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 1 Aug 2022 18:50:20 +0200 Subject: [PATCH 109/329] docs: update documentation --- .ubidoc.yml | 27 +++++++++++++++++++++++++++ docs/_docs/production.md | 28 ++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index 495da459..605c5dc3 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -44,4 +44,31 @@ tables: rows: - case: "OutgoingEvent.OrderMilk" + - name: "production-ul" + rows: + - class: "ProductionPlan" + - class: "ProductionPlanItem" + - case: "Production.ToStart" + name: "Production To Start" + - case: "Production.InProgress" + name: "Production In Progress" + - case: "Production.Ended" + name: "Ended Production" + - type: "RecipeBook" + - class: "Recipe" + - class: "QuintalsOfIngredient" + - enum: "Ingredient" + - name: "production-outgoing" + termName: "Event" + definitionName: "Description" + rows: + - case: "OutgoingEvent.StartProduction" + - case: "OutgoingEvent.ProductionEnded" + - name: "production-incoming" + termName: "Event" + definitionName: "Description" + rows: + - case: "IncomingEvent.ProductionPlanReady" + + ignored: [] diff --git a/docs/_docs/production.md b/docs/_docs/production.md index 5ad9be29..4fb7ff42 100644 --- a/docs/_docs/production.md +++ b/docs/_docs/production.md @@ -3,10 +3,12 @@ title: Production --- # Production -Every day the dairyman receives instructions from Raffaella on the cheese he needs to -produce for the day. A production specifies the quantity of a product that needs to -be produced. Each type of cheese has a recipe which has different lines that specify -the quintals of each ingredient needed to produce a quintal of the given type of +Every day the dairyman receives a production plan from Raffaella, containing instructions +on the cheese he needs to produce for the day. + +To determine the ingredients to use for a production the dairyman uses a +recipe book where, for each type of cheese, there is a recipe. +A recipe specifies the quintals of each ingredient needed to produce a quintal of the given type of cheese. > _e.g._ The recipe for a quintal of ricotta requires 1.5 quintals of milk > a tenth of quintal of rennet and a tenth of quintal of salt @@ -15,5 +17,19 @@ Once the appropriate recipe is chosen the production can start by retrieving the needed ingredients. Once an in-progress production ends (the smart machines will send an appropriate message -to signal the end of the process), the produced cheese is assigned a lot number -and stored in a refrigeration room. \ No newline at end of file +to signal the end of the process), the produced cheese is assigned a batch ID +and stored in a refrigeration room. + +## Ubiquitous Language + +{% include production-ul.md %} + +## Domain Events + +### Incoming Events + +{% include production-incoming.md %} + +### Outgoing Events + +{% include production-outgoing.md %} From 3e7b928238d3971ca14ff011fbb8b09efac2e711 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Tue, 2 Aug 2022 09:12:11 +0200 Subject: [PATCH 110/329] chore: refactor inline conversions code --- .../atedeg/mdm/products/utils/GramsOps.scala | 2 +- .../atedeg/mdm/utils/LiteralConversions.scala | 25 ++++++------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/GramsOps.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/GramsOps.scala index d69eb399..cec61b5f 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/GramsOps.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/GramsOps.scala @@ -6,7 +6,7 @@ import eu.timepit.refined.predicates.all.Positive import dev.atedeg.mdm.products.Grams import dev.atedeg.mdm.utils.{ coerce, PositiveNumber } -private[products] def coerceToGrams(n: Int): Grams = Grams(coerce[Int, Positive](n)) +private[products] def coerceToGrams(n: Int): Grams = Grams(coerce(n)) extension (n: PositiveNumber) def grams: Grams = Grams(n) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala index 82c8295c..f0cdd828 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala @@ -1,41 +1,30 @@ package dev.atedeg.mdm.utils -import scala.compiletime.{ codeOf, constValue } +import scala.compiletime.constValue import scala.language.implicitConversions -import scala.quoted.{ Expr, Quotes } import eu.timepit.refined.numeric.Interval +import eu.timepit.refined.numeric.Interval.Closed import eu.timepit.refined.predicates.all.{ NonNegative, Positive } -import eu.timepit.refined.refineV @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion", "org.wartremover.warts.OptionPartial")) inline implicit def intToNumberInRange[L <: Int & Singleton, U <: Int & Singleton]( inline i: Int, ): NumberInClosedRange[L, U] = - inline if constValue[L] <= i && i <= constValue[U] - then refineV[Interval.Closed[L, U]](i).toOption.get - else compiletime.error("Not in the desired range") + inline if constValue[L] <= i && i <= constValue[U] then coerce(i) else compiletime.error("Not in the desired range") @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion", "org.wartremover.warts.OptionPartial")) inline implicit def intToPositiveNumber(inline i: Int): PositiveNumber = - inline if i > 0 - then refineV[Positive](i).toOption.get - else compiletime.error("Not a positive number") + inline if i > 0 then coerce(i) else compiletime.error("Not a positive number") @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion", "org.wartremover.warts.OptionPartial")) inline implicit def intToNonNegativeNumber(inline i: Int): NonNegativeNumber = - inline if i >= 0 - then refineV[NonNegative](i).toOption.get - else compiletime.error("Not a non-negative number") + inline if i >= 0 then coerce(i) else compiletime.error("Not a non-negative number") @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion", "org.wartremover.warts.OptionPartial")) inline implicit def doubleToPositiveDecimal(inline d: Double): PositiveDecimal = - inline if d > 0 - then refineV[Positive](d).toOption.get - else compiletime.error("Not a positive decimal") + inline if d > 0 then coerce(d) else compiletime.error("Not a positive decimal") @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion", "org.wartremover.warts.OptionPartial")) inline implicit def doubleToNonNegativeDecimal(inline d: Double): NonNegativeDecimal = - inline if d >= 0 - then refineV[NonNegative](d).toOption.get - else compiletime.error("Not a non-negative decimal") + inline if d >= 0 then coerce(d) else compiletime.error("Not a non-negative decimal") From 5e0734ebcea6178d3080c91227f4ff1a6706372d Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Tue, 2 Aug 2022 16:11:02 +0200 Subject: [PATCH 111/329] build: skip publish of shared kernel Co-authored-by: Nicolas Farabegoli --- build.sbt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.sbt b/build.sbt index dcc3bbb4..ac3b1a56 100644 --- a/build.sbt +++ b/build.sbt @@ -116,6 +116,9 @@ lazy val production = project lazy val `products-shared-kernel` = project .in(file("products-shared-kernel")) .settings(commonSettings) + .settings( + publish / skip := true, + ) .dependsOn(utils) lazy val stocking = project From e5f8f8393d6abce062bf35297aef0f6c21864815 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 2 Aug 2022 14:22:51 +0000 Subject: [PATCH 112/329] chore(release): 1.0.0-beta.3 [skip ci] # [1.0.0-beta.3](https://github.com/atedeg/mdm/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-02) ### Features * add domain actions and events, closes [#77](https://github.com/atedeg/mdm/issues/77) ([5d50f12](https://github.com/atedeg/mdm/commit/5d50f1221cb6fe5348a711cba2d5d3b0edb81c66)) * add first draft implementation of one of the actions ([474ce44](https://github.com/atedeg/mdm/commit/474ce44f9d7c849307775cd233fe22bc55fc5e81)) * add ubiquitous language,closes [#75](https://github.com/atedeg/mdm/issues/75) ([6cfe906](https://github.com/atedeg/mdm/commit/6cfe906cd70361719c9c24dabc676157b2b4e976)) --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb98b60..57dad8f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [1.0.0-beta.3](https://github.com/atedeg/mdm/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-02) + + +### Features + +* add domain actions and events, closes [#77](https://github.com/atedeg/mdm/issues/77) ([5d50f12](https://github.com/atedeg/mdm/commit/5d50f1221cb6fe5348a711cba2d5d3b0edb81c66)) +* add first draft implementation of one of the actions ([474ce44](https://github.com/atedeg/mdm/commit/474ce44f9d7c849307775cd233fe22bc55fc5e81)) +* add ubiquitous language,closes [#75](https://github.com/atedeg/mdm/issues/75) ([6cfe906](https://github.com/atedeg/mdm/commit/6cfe906cd70361719c9c24dabc676157b2b4e976)) + # [1.0.0-beta.2](https://github.com/atedeg/mdm/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-08-01) From 04c182b7bc2461d1a5908f82ddf3fcc4150c0a74 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 16:32:52 +0200 Subject: [PATCH 113/329] fix: relationships between restocking and production --- docs/_assets/images/contextMap.svg | 254 ++++++++++++++--------------- docs/_docs/context-map.md | 5 +- docs/mdm.cml | 2 +- 3 files changed, 122 insertions(+), 139 deletions(-) diff --git a/docs/_assets/images/contextMap.svg b/docs/_assets/images/contextMap.svg index 5a30ee1b..4e580608 100644 --- a/docs/_assets/images/contextMap.svg +++ b/docs/_assets/images/contextMap.svg @@ -1,169 +1,151 @@ - - + + ContextMapGraph - + ClientOrders - -ClientOrders + +ClientOrders - + -ProductionPlanning - -ProductionPlanning +MilkPlanning + +MilkPlanning - + -ClientOrders->ProductionPlanning - -                                         - - -D - - -ACL - -U +ClientOrders->MilkPlanning + +                                         + + +D + + +ACL + +U - - -MilkPlanning - -MilkPlanning + + +ProductionPlanning + +ProductionPlanning - + -ClientOrders->MilkPlanning - -                                         - - -D - - -ACL - -U +ClientOrders->ProductionPlanning + +                                         + + +D + + +ACL + +U - + Production - -Production + +Production ProductionPlanning->Production - -                                         - - -D - - -CF - -U + +                                         + + +D + + +CF + +U + + + +Restocking + +Restocking + + + +Production->Restocking + +                                         + +D + +U - + Stocking - -Stocking + +Stocking - + Production->Stocking - -                                         - - -D - - -CF - -U + +                                         + + +D + + +CF + +U - + -Stocking->ClientOrders - -                                         - -D - - -U - - -OHS, PL - - - -Stocking->ProductionPlanning - -                                         - - -D - - -ACL - -U +Restocking->MilkPlanning + +                                         + + +D + + +ACL + +U - + -Stocking->MilkPlanning - -                                         - - -D - - -ACL - -U - - - -Restocking - -Restocking - - - -Restocking->Production - -                                         - - -D - - -ACL - -U +Stocking->ClientOrders + +                                         + +D + + +U + + +OHS, PL - + -Restocking->MilkPlanning - -                                         - - -D - - -ACL - -U +Stocking->ProductionPlanning + +                                         + + +D + + +ACL + +U diff --git a/docs/_docs/context-map.md b/docs/_docs/context-map.md index 82de5b93..f52f9323 100644 --- a/docs/_docs/context-map.md +++ b/docs/_docs/context-map.md @@ -28,9 +28,10 @@ title: Context Map - `Stocking [D, CF] <- [U] Production` `Production` informs `Stocking` that a batch is ripening. Since `Production` and `Stocking` are tightly coupled, the latter is Conformist. -- `Production [D, ACL] <- [U] Restocking` +- `Restocking [D] <- [U, CF] Production` `Production` informs `Restocking` when some raw materials are consumed. - `Production` is a downstream bounded context and needs an Anti-Corruption Layer since `Restocking` is going to be a generic bounded context. + `Production` is an upstream bounded context that *conforms* to the `Restocking` downstream bounded context. + This is necessary since `Restocking` is going to be a generic bounded context whose API we will not be able to freely change. There is a *Shared Kernel* among the bounded contexts which contains the definitions for **product** and **cheese type**. This choice was taken as the two aforementioned concepts are crucial for the cheese factory and a change in any of the definitions must be reflected in all diff --git a/docs/mdm.cml b/docs/mdm.cml index 3fb0eea7..6c66e29e 100644 --- a/docs/mdm.cml +++ b/docs/mdm.cml @@ -13,7 +13,7 @@ ContextMap MDM { ClientOrders [D] <- [U, OHS, PL] Stocking ProductionPlanning [D, ACL] <- [U] Stocking Stocking [D, CF] <- [U] Production - Production [D, ACL] <- [U] Restocking + Restocking [D] <- [U, CF] Production } BoundedContext MilkPlanning From 6b8b0f9b152f5c526163116ddb0cfc551e091e2c Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Tue, 2 Aug 2022 16:32:11 +0200 Subject: [PATCH 114/329] fix: fix ubidoc table --- .ubidoc.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index 605c5dc3..d989c378 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -11,14 +11,14 @@ tables: - class: "BatchID" name: "Batch ID" - class: "LabelledProduct" - - name: "stocking-incoming" - rows: - - case: "IncomingEvent.ProductStocked" - name: "stocking-outgoing" rows: - - case: "OutgoingEvent.BatchReadyForQualityAssurance" - - case: "OutgoingEvent.ProductRemovedFromStock" - - case: "OutgoingEvent.NewBatch" + - case: "OutgoingEvent.ProductStocked" + - name: "stocking-incoming" + rows: + - case: "IncomingEvent.BatchReadyForQualityAssurance" + - case: "IncomingEvent.ProductRemovedFromStock" + - case: "IncomingEvent.NewBatch" - name: "milk-planning-ul" termName: "Term" definitionName: "Definition" From ffb5328ed3efc945ad20dbc182231f1aa32becb0 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Tue, 2 Aug 2022 16:33:40 +0200 Subject: [PATCH 115/329] ci: add ubidoc generation --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fcf1fdd..d8f1dd96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,9 @@ jobs: files: ./target/scala-${{steps.get-scala-version.outputs.scala-version}}/jacoco/report/aggregate/jacoco.xml verbose: true + - name: Build docsite + run: sbt ubidocGenerate + publish: name: Publish needs: [build] From f565d70e293d1faf62bb63c6ccdc24b8947686fb Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 2 Aug 2022 14:48:27 +0000 Subject: [PATCH 116/329] chore(release): 1.0.0-beta.4 [skip ci] # [1.0.0-beta.4](https://github.com/atedeg/mdm/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-02) ### Bug Fixes * fix ubidoc table ([6b8b0f9](https://github.com/atedeg/mdm/commit/6b8b0f9b152f5c526163116ddb0cfc551e091e2c)) * relationships between restocking and production ([04c182b](https://github.com/atedeg/mdm/commit/04c182b7bc2461d1a5908f82ddf3fcc4150c0a74)) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57dad8f6..1762982e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [1.0.0-beta.4](https://github.com/atedeg/mdm/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-02) + + +### Bug Fixes + +* fix ubidoc table ([6b8b0f9](https://github.com/atedeg/mdm/commit/6b8b0f9b152f5c526163116ddb0cfc551e091e2c)) +* relationships between restocking and production ([04c182b](https://github.com/atedeg/mdm/commit/04c182b7bc2461d1a5908f82ddf3fcc4150c0a74)) + # [1.0.0-beta.3](https://github.com/atedeg/mdm/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-02) From a4c3f1569144e0e4144f2cb54368856ae04c937d Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 16:54:05 +0200 Subject: [PATCH 117/329] docs: fix context map --- docs/_assets/images/contextMap.svg | 236 +++++++++++++++-------------- docs/_docs/context-map.md | 4 +- docs/mdm.cml | 2 +- 3 files changed, 123 insertions(+), 119 deletions(-) diff --git a/docs/_assets/images/contextMap.svg b/docs/_assets/images/contextMap.svg index 4e580608..ce184892 100644 --- a/docs/_assets/images/contextMap.svg +++ b/docs/_assets/images/contextMap.svg @@ -1,151 +1,155 @@ - - + + ContextMapGraph - + ClientOrders - -ClientOrders + +ClientOrders - + -MilkPlanning - -MilkPlanning +ProductionPlanning + +ProductionPlanning - + -ClientOrders->MilkPlanning - -                                         - - -D - - -ACL - -U +ClientOrders->ProductionPlanning + +                                         + + +D + + +ACL + +U - - -ProductionPlanning - -ProductionPlanning + + +MilkPlanning + +MilkPlanning - + -ClientOrders->ProductionPlanning - -                                         - - -D - - -ACL - -U +ClientOrders->MilkPlanning + +                                         + + +D + + +ACL + +U - + Production - -Production + +Production ProductionPlanning->Production - -                                         - - -D - - -CF - -U - - - -Restocking - -Restocking - - - -Production->Restocking - -                                         - -D - -U + +                                         + + +D + + +CF + +U - + Stocking - -Stocking + +Stocking - + Production->Stocking - -                                         - - -D - - -CF - -U + +                                         + + +D + + +CF + +U - - -Restocking->MilkPlanning - -                                         - - -D - - -ACL - -U + + +Restocking + +Restocking + + + +Production->Restocking + +                                         + +D + + +U + + +OHS, PL Stocking->ClientOrders - -                                         - -D - - -U - - -OHS, PL + +                                         + +D + + +U + + +OHS, PL - + Stocking->ProductionPlanning - -                                         - - -D - - -ACL - -U + +                                         + + +D + + +ACL + +U + + + +Restocking->MilkPlanning + +                                         + + +D + + +ACL + +U diff --git a/docs/_docs/context-map.md b/docs/_docs/context-map.md index f52f9323..e2a35c4f 100644 --- a/docs/_docs/context-map.md +++ b/docs/_docs/context-map.md @@ -30,8 +30,8 @@ title: Context Map Since `Production` and `Stocking` are tightly coupled, the latter is Conformist. - `Restocking [D] <- [U, CF] Production` `Production` informs `Restocking` when some raw materials are consumed. - `Production` is an upstream bounded context that *conforms* to the `Restocking` downstream bounded context. - This is necessary since `Restocking` is going to be a generic bounded context whose API we will not be able to freely change. + `Production` is an upstream Open-Host Service and must expose a published language as the `Restocking` downstream bounded context + is going to be generic and we will not be able to freely change its API. There is a *Shared Kernel* among the bounded contexts which contains the definitions for **product** and **cheese type**. This choice was taken as the two aforementioned concepts are crucial for the cheese factory and a change in any of the definitions must be reflected in all diff --git a/docs/mdm.cml b/docs/mdm.cml index 6c66e29e..81d72599 100644 --- a/docs/mdm.cml +++ b/docs/mdm.cml @@ -13,7 +13,7 @@ ContextMap MDM { ClientOrders [D] <- [U, OHS, PL] Stocking ProductionPlanning [D, ACL] <- [U] Stocking Stocking [D, CF] <- [U] Production - Restocking [D] <- [U, CF] Production + Restocking [D] <- [U, OHS, PL] Production } BoundedContext MilkPlanning From 5027fbe30199faa5e0fd9785edc0a10b8ca213a5 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:40:43 +0200 Subject: [PATCH 118/329] docs: context map for stocking and client orders --- docs/_assets/images/contextMap.svg | 218 ++++++++++++++--------------- docs/_docs/context-map.md | 4 +- docs/mdm.cml | 2 +- 3 files changed, 112 insertions(+), 112 deletions(-) diff --git a/docs/_assets/images/contextMap.svg b/docs/_assets/images/contextMap.svg index ce184892..cc2c90c9 100644 --- a/docs/_assets/images/contextMap.svg +++ b/docs/_assets/images/contextMap.svg @@ -1,155 +1,155 @@ - - + + ContextMapGraph - + ClientOrders - -ClientOrders + +ClientOrders ProductionPlanning - -ProductionPlanning + +ProductionPlanning ClientOrders->ProductionPlanning - -                                         - - -D - - -ACL - -U + +                                         + + +D + + +ACL + +U + + + +Stocking + +Stocking + + + +ClientOrders->Stocking + +                                         + + +D + + +ACL + +U MilkPlanning - -MilkPlanning + +MilkPlanning - + ClientOrders->MilkPlanning - -                                         - - -D - - -ACL - -U + +                                         + + +D + + +ACL + +U Production - -Production + +Production - + ProductionPlanning->Production - -                                         - - -D - - -CF - -U - - - -Stocking - -Stocking + +                                         + + +D + + +CF + +U - + Production->Stocking - -                                         - - -D - - -CF - -U + +                                         + + +D + + +CF + +U Restocking - -Restocking + +Restocking - + Production->Restocking - -                                         - -D - - -U - - -OHS, PL - - - -Stocking->ClientOrders - -                                         - -D - - -U - - -OHS, PL + +                                         + +D + + +U + + +OHS, PL - + Stocking->ProductionPlanning - -                                         - - -D - - -ACL - -U + +                                         + + +D + + +ACL + +U Restocking->MilkPlanning - -                                         - - -D - - -ACL - -U + +                                         + + +D + + +ACL + +U diff --git a/docs/_docs/context-map.md b/docs/_docs/context-map.md index e2a35c4f..b7f3d1d4 100644 --- a/docs/_docs/context-map.md +++ b/docs/_docs/context-map.md @@ -18,9 +18,9 @@ title: Context Map `ProductionPlanning` provides `Production` with the production plan for the day. As `ProductionPlanning` is the service provider concerning `Production`, they are respectively upstream and downstream. Since `ProductionPlanning` and `Production` are tightly coupled, the latter is *Conformist*. -- `ClientOrders [D] <- [U, OHS, PL] Stocking` +- `Stocking [D, ACL] <- [U] ClientOrders` `Stocking` receives a message from `ClientOrders` notifying the removal from stock of certain products. - `Stocking ` is a *Open-Host Service* and must expose a *published language*, as `ClientOrders` is going to be a generic bounded context and it will be + `Stocking ` has an *Anti-Corruption Layer*, as `ClientOrders` is going to be a generic bounded context and it will be impossible to control the format of the messages. - `ProductionPlanning [D, ACL] <- [U] Stocking` `ProductionPlannig` asks `Stocking` for the amount of products missing from the stock. diff --git a/docs/mdm.cml b/docs/mdm.cml index 81d72599..1ffac3d6 100644 --- a/docs/mdm.cml +++ b/docs/mdm.cml @@ -10,7 +10,7 @@ ContextMap MDM { ProductionPlanning [D, ACL] <- [U] ClientOrders MilkPlanning [D, ACL] <- [U] Restocking Production [D, CF] <- [U] ProductionPlanning - ClientOrders [D] <- [U, OHS, PL] Stocking + Stocking [D, ACL] <- [U] ClientOrders ProductionPlanning [D, ACL] <- [U] Stocking Stocking [D, CF] <- [U] Production Restocking [D] <- [U, OHS, PL] Production From 8bffbc6638db269169031bc283a68e2014707997 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 16:56:48 +0200 Subject: [PATCH 119/329] build: add restocking project --- build.sbt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.sbt b/build.sbt index ac3b1a56..3593b4a8 100644 --- a/build.sbt +++ b/build.sbt @@ -94,6 +94,7 @@ lazy val root = project production, `milk-planning`, `products-shared-kernel`, + restocking, ) lazy val utils = project @@ -126,3 +127,9 @@ lazy val stocking = project .settings(commonSettings) .dependsOn(utils) .dependsOn(`products-shared-kernel`) + +lazy val restocking = project + .in(file("restocking")) + .settings(commonSettings) + .dependsOn(utils) + .dependsOn(`products-shared-kernel`) From d0223d052660fd7eae8112cf60e36295c5e80eba Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:01:32 +0200 Subject: [PATCH 120/329] refactor: move ingredients to shared kernel --- .../main/scala/dev/atedeg/mdm/production/Types.scala | 9 --------- .../production/utils/QuintalsOfIngredientOps.scala | 1 + .../test/scala/dev/atedeg/mdm/production/Tests.scala | 3 ++- .../scala/dev/atedeg/mdm/products/Ingredients.scala | 11 +++++++++++ 4 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index ab2e31be..6bef54ad 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -74,12 +74,3 @@ final case class QuintalsOfIngredient(quintals: WeightInQuintals, ingredient: In */ final case class WeightInQuintals(n: PositiveDecimal) derives Times -/** - * An ingredient that may be needed by a [[Recipe recipe]] to produce a [[CheeseType type of cheese]]. - */ -enum Ingredient: - case Milk extends Ingredient - case Cream extends Ingredient - case Rennet extends Ingredient - case Salt extends Ingredient - case Probiotics extends Ingredient diff --git a/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala index b187b79f..752c3e5a 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala @@ -3,6 +3,7 @@ package dev.atedeg.mdm.production.utils import scala.annotation.targetName import dev.atedeg.mdm.production.* +import dev.atedeg.mdm.products.Ingredient import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala index 14d39976..6506eedd 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala @@ -2,7 +2,6 @@ package dev.atedeg.mdm.production import java.util.UUID -import Ingredient.* import OutgoingEvent.* import cats.data.NonEmptyList import org.scalatest.EitherValues.* @@ -13,6 +12,8 @@ import org.scalatest.matchers.should.Matchers import dev.atedeg.mdm.production.* import dev.atedeg.mdm.production.utils.* import dev.atedeg.mdm.products.{ CheeseType, Product } +import dev.atedeg.mdm.products.Ingredient +import dev.atedeg.mdm.products.Ingredient.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.doubleToPositiveDecimal import dev.atedeg.mdm.utils.given diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala new file mode 100644 index 00000000..b96f6a61 --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala @@ -0,0 +1,11 @@ +package dev.atedeg.mdm.products + +/** + * An ingredient that may be needed by a [[Recipe recipe]] to produce a [[CheeseType type of cheese]]. + */ +enum Ingredient: + case Milk extends Ingredient + case Cream extends Ingredient + case Rennet extends Ingredient + case Salt extends Ingredient + case Probiotics extends Ingredient \ No newline at end of file From fe8f9bd623f360dd9b47ab074afe92e1df5f56fb Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:01:56 +0200 Subject: [PATCH 121/329] docs: update milk-planning entity doc --- .../src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala index 61c44835..d8bfff92 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala @@ -16,9 +16,9 @@ final case class ProcessedMilk(quantity: QuintalsOfMilk) /** * A quantity of milk expressed in quintals. - * @note it must be a [[PositiveDecimal positive decimal number]]. - * @example `QuintalsOfMilk(1.1)` is a valid weight of 110 kg. - * @example `QuintalsOfMilk(-20.5)` is not a valid weight. + * @note it must be a [[NonNegativeNumber non negative number]]. + * @example `QuintalsOfMilk(1)` is a valid weight of 110 kg. + * @example `QuintalsOfMilk(-2)` is not a valid weight. */ final case class QuintalsOfMilk(quintals: NonNegativeNumber) derives Plus, Times, Minus From 790c06b3b581749fe8110d33b1fc1809fea61e46 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:02:18 +0200 Subject: [PATCH 122/329] docs: add restocking description --- docs/_docs/restocking.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/_docs/restocking.md diff --git a/docs/_docs/restocking.md b/docs/_docs/restocking.md new file mode 100644 index 00000000..70015fae --- /dev/null +++ b/docs/_docs/restocking.md @@ -0,0 +1,22 @@ +--- +title: Restocking +--- + +# Restocking + +The Restocking bounded context is responsible to fulfill the requests to order +milk coming from the Milk Planning. +Moreover, it keeps tracks of the quintals of stocked milk that could be used by +other bounded contexts. +Lastly, this bounded context also keeps tracks of other ingredients (e.g. rennet, +salt, probiotics, etc.) and their consumption when a production is started. + +# Ubiquitous Language + +{% include restocking-ul.md %} + +# Domain Events + +## Incoming Events + +{% include restocking-incoming.md %} From 36af124611279107621dca88931483f72eb8ec81 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:02:47 +0200 Subject: [PATCH 123/329] feat: add restocking bc --- .../dev/atedeg/mdm/restocking/Actions.scala | 16 ++++++++ .../dev/atedeg/mdm/restocking/Events.scala | 8 ++++ .../dev/atedeg/mdm/restocking/Types.scala | 38 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/Actions.scala create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Actions.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Actions.scala new file mode 100644 index 00000000..3899baff --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Actions.scala @@ -0,0 +1,16 @@ +package dev.atedeg.mdm.restocking + +import cats.data.NonEmptyList +import cats.syntax.all.* + +import dev.atedeg.mdm.utils.given + +/** + * Given a list of ingredients needed to [[IncomingEvent.ProductionStarted start a production]], + * it removes that [[WeightInQuintals quantity]] from the [[Stock stock]]. + */ +def consumeIngredients(stock: Stock)(ingredients: NonEmptyList[QuintalsOfIngredient]): Stock = + ingredients.foldLeft(stock) { case (newStock, QuintalsOfIngredient(q, i)) => + val toSubtract = StockedQuantity(q.n) + newStock.updatedWith(i)(oldQuantity => oldQuantity.map(_ - toSubtract)) + } diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala new file mode 100644 index 00000000..227136e1 --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala @@ -0,0 +1,8 @@ +package dev.atedeg.mdm.restocking + +import cats.data.NonEmptyList + +enum IncomingEvent: + case OrderMilk(quintals: QuintalsOfMilk) + case ProductionStarted(ingredients: NonEmptyList[QuintalsOfIngredient]) + diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala new file mode 100644 index 00000000..a539cdfa --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala @@ -0,0 +1,38 @@ +package dev.atedeg.mdm.restocking + +import dev.atedeg.mdm.products.Ingredient +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given + +/** + * A quantity of milk expressed in quintals. + * @note it must be a [[PositiveNumber positive number]]. + * @example `QuintalsOfMilk(1)` is a valid weight of 110 kg. + * @example `QuintalsOfMilk(0)` is not a valid weight. + */ +final case class QuintalsOfMilk(quintals: PositiveNumber) + +/** + * An [[Ingredient ingredient]] and a [[WeightInQuintals weight in quintals]]. + */ +final case class QuintalsOfIngredient(quintals: WeightInQuintals, ingredient: Ingredient) + +/** + * A weight expressed in quintals. + */ +final case class WeightInQuintals(n: PositiveDecimal) + +/** + * Quintals of stocked milk. + */ +final case class StockedMilk(quintals: NonNegativeDecimal) + +/** + * The quantity of ingredients in stock. + */ +type Stock = Map[Ingredient, StockedQuantity] + +/** + * A stocked quantity. + */ +final case class StockedQuantity(quintals: NonNegativeDecimal) derives Minus \ No newline at end of file From b710f7dc9b202a1ae5bc980d3c352f20f4d827cb Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:03:11 +0200 Subject: [PATCH 124/329] docs: add ubidoc config file for restocking --- .ubidoc.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index d989c378..34abb053 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -57,7 +57,6 @@ tables: - type: "RecipeBook" - class: "Recipe" - class: "QuintalsOfIngredient" - - enum: "Ingredient" - name: "production-outgoing" termName: "Event" definitionName: "Description" @@ -70,5 +69,18 @@ tables: rows: - case: "IncomingEvent.ProductionPlanReady" + - name: "restocking-ul" + rows: + - type: "Stock" + - class: "StockedMilk" + - class: "StockedQuantity" + - class: "QuintalsOfIngredient" + - class: "QuintalsOfMilk" + - name: "restocking-incoming" + termName: "Event" + definitionName: "Description" + rows: + - case: "IncomingEvent.OrderMilk" + - case: "IncomingEvent.ProductionStarted" ignored: [] From 30a3ad002a31805c3d953570524bcb9c911da7d1 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:08:18 +0200 Subject: [PATCH 125/329] docs: add scaladoc to events --- .../scala/dev/atedeg/mdm/restocking/Events.scala | 12 +++++++++++- .../main/scala/dev/atedeg/mdm/restocking/Types.scala | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala index 227136e1..23363d1e 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala @@ -2,7 +2,17 @@ package dev.atedeg.mdm.restocking import cats.data.NonEmptyList +/** + * The events handled by this bounded context. + */ enum IncomingEvent: + /** + * Received when an order for milk has to be placed. + */ case OrderMilk(quintals: QuintalsOfMilk) - case ProductionStarted(ingredients: NonEmptyList[QuintalsOfIngredient]) + /** + * Received when a production is started. + * It consumes the given [[QuintalsOfIngredient ingredients]]. + */ + case ProductionStarted(ingredients: NonEmptyList[QuintalsOfIngredient]) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala index a539cdfa..c04d0bef 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala @@ -35,4 +35,4 @@ type Stock = Map[Ingredient, StockedQuantity] /** * A stocked quantity. */ -final case class StockedQuantity(quintals: NonNegativeDecimal) derives Minus \ No newline at end of file +final case class StockedQuantity(quintals: NonNegativeDecimal) derives Minus From 443ba473202ca59dccffb5dd4ab3fb4aa843f74f Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:17:59 +0200 Subject: [PATCH 126/329] style: scalafmt --- .../dev/atedeg/mdm/production/Types.scala | 1 - .../dev/atedeg/mdm/products/Ingredients.scala | 22 +++--- .../dev/atedeg/mdm/restocking/Actions.scala | 32 ++++---- .../dev/atedeg/mdm/restocking/Events.scala | 36 ++++----- .../dev/atedeg/mdm/restocking/Types.scala | 76 +++++++++---------- 5 files changed, 83 insertions(+), 84 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index 6bef54ad..ff1b8214 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -73,4 +73,3 @@ final case class QuintalsOfIngredient(quintals: WeightInQuintals, ingredient: In * A weight expressed in quintals. */ final case class WeightInQuintals(n: PositiveDecimal) derives Times - diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala index b96f6a61..0fba881d 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala @@ -1,11 +1,11 @@ -package dev.atedeg.mdm.products - -/** - * An ingredient that may be needed by a [[Recipe recipe]] to produce a [[CheeseType type of cheese]]. - */ -enum Ingredient: - case Milk extends Ingredient - case Cream extends Ingredient - case Rennet extends Ingredient - case Salt extends Ingredient - case Probiotics extends Ingredient \ No newline at end of file +package dev.atedeg.mdm.products + +/** + * An ingredient that may be needed by a [[Recipe recipe]] to produce a [[CheeseType type of cheese]]. + */ +enum Ingredient: + case Milk extends Ingredient + case Cream extends Ingredient + case Rennet extends Ingredient + case Salt extends Ingredient + case Probiotics extends Ingredient diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Actions.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Actions.scala index 3899baff..0e78598b 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Actions.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Actions.scala @@ -1,16 +1,16 @@ -package dev.atedeg.mdm.restocking - -import cats.data.NonEmptyList -import cats.syntax.all.* - -import dev.atedeg.mdm.utils.given - -/** - * Given a list of ingredients needed to [[IncomingEvent.ProductionStarted start a production]], - * it removes that [[WeightInQuintals quantity]] from the [[Stock stock]]. - */ -def consumeIngredients(stock: Stock)(ingredients: NonEmptyList[QuintalsOfIngredient]): Stock = - ingredients.foldLeft(stock) { case (newStock, QuintalsOfIngredient(q, i)) => - val toSubtract = StockedQuantity(q.n) - newStock.updatedWith(i)(oldQuantity => oldQuantity.map(_ - toSubtract)) - } +package dev.atedeg.mdm.restocking + +import cats.data.NonEmptyList +import cats.syntax.all.* + +import dev.atedeg.mdm.utils.given + +/** + * Given a list of ingredients needed to [[IncomingEvent.ProductionStarted start a production]], + * it removes that [[WeightInQuintals quantity]] from the [[Stock stock]]. + */ +def consumeIngredients(stock: Stock)(ingredients: NonEmptyList[QuintalsOfIngredient]): Stock = + ingredients.foldLeft(stock) { case (newStock, QuintalsOfIngredient(q, i)) => + val toSubtract = StockedQuantity(q.n) + newStock.updatedWith(i)(oldQuantity => oldQuantity.map(_ - toSubtract)) + } diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala index 23363d1e..4eb5cb87 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Events.scala @@ -1,18 +1,18 @@ -package dev.atedeg.mdm.restocking - -import cats.data.NonEmptyList - -/** - * The events handled by this bounded context. - */ -enum IncomingEvent: - /** - * Received when an order for milk has to be placed. - */ - case OrderMilk(quintals: QuintalsOfMilk) - - /** - * Received when a production is started. - * It consumes the given [[QuintalsOfIngredient ingredients]]. - */ - case ProductionStarted(ingredients: NonEmptyList[QuintalsOfIngredient]) +package dev.atedeg.mdm.restocking + +import cats.data.NonEmptyList + +/** + * The events handled by this bounded context. + */ +enum IncomingEvent: + /** + * Received when an order for milk has to be placed. + */ + case OrderMilk(quintals: QuintalsOfMilk) + + /** + * Received when a production is started. + * It consumes the given [[QuintalsOfIngredient ingredients]]. + */ + case ProductionStarted(ingredients: NonEmptyList[QuintalsOfIngredient]) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala index c04d0bef..deade225 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala @@ -1,38 +1,38 @@ -package dev.atedeg.mdm.restocking - -import dev.atedeg.mdm.products.Ingredient -import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.given - -/** - * A quantity of milk expressed in quintals. - * @note it must be a [[PositiveNumber positive number]]. - * @example `QuintalsOfMilk(1)` is a valid weight of 110 kg. - * @example `QuintalsOfMilk(0)` is not a valid weight. - */ -final case class QuintalsOfMilk(quintals: PositiveNumber) - -/** - * An [[Ingredient ingredient]] and a [[WeightInQuintals weight in quintals]]. - */ -final case class QuintalsOfIngredient(quintals: WeightInQuintals, ingredient: Ingredient) - -/** - * A weight expressed in quintals. - */ -final case class WeightInQuintals(n: PositiveDecimal) - -/** - * Quintals of stocked milk. - */ -final case class StockedMilk(quintals: NonNegativeDecimal) - -/** - * The quantity of ingredients in stock. - */ -type Stock = Map[Ingredient, StockedQuantity] - -/** - * A stocked quantity. - */ -final case class StockedQuantity(quintals: NonNegativeDecimal) derives Minus +package dev.atedeg.mdm.restocking + +import dev.atedeg.mdm.products.Ingredient +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given + +/** + * A quantity of milk expressed in quintals. + * @note it must be a [[PositiveNumber positive number]]. + * @example `QuintalsOfMilk(1)` is a valid weight of 110 kg. + * @example `QuintalsOfMilk(0)` is not a valid weight. + */ +final case class QuintalsOfMilk(quintals: PositiveNumber) + +/** + * An [[Ingredient ingredient]] and a [[WeightInQuintals weight in quintals]]. + */ +final case class QuintalsOfIngredient(quintals: WeightInQuintals, ingredient: Ingredient) + +/** + * A weight expressed in quintals. + */ +final case class WeightInQuintals(n: PositiveDecimal) + +/** + * Quintals of stocked milk. + */ +final case class StockedMilk(quintals: NonNegativeDecimal) + +/** + * The quantity of ingredients in stock. + */ +type Stock = Map[Ingredient, StockedQuantity] + +/** + * A stocked quantity. + */ +final case class StockedQuantity(quintals: NonNegativeDecimal) derives Minus From c2438447d52c5f8bf6aa4480b84dd90584316706 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 13:29:31 +0200 Subject: [PATCH 127/329] test: add restocking tests --- .../dev/atedeg/mdm/restocking/Tests.scala | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala diff --git a/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala b/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala new file mode 100644 index 00000000..16660dec --- /dev/null +++ b/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala @@ -0,0 +1,48 @@ +package dev.atedeg.mdm.restocking + +import cats.data.NonEmptyList +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +import dev.atedeg.mdm.products.{ Ingredient, Product } +import dev.atedeg.mdm.products.Ingredient.{ Cream, Milk, Probiotics, Rennet, Salt } +import dev.atedeg.mdm.utils.given + +trait Mocks { + private val milk = QuintalsOfIngredient(WeightInQuintals(5.5), Milk) + private val cream = QuintalsOfIngredient(WeightInQuintals(3.0), Cream) + private val rennet = QuintalsOfIngredient(WeightInQuintals(2.0), Rennet) + private val salt = QuintalsOfIngredient(WeightInQuintals(3.0), Salt) + private val probiotics = QuintalsOfIngredient(WeightInQuintals(0.1), Probiotics) + + val ingredients: NonEmptyList[QuintalsOfIngredient] = NonEmptyList.of(milk, cream, rennet, salt, probiotics) + +} + +class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { + + Feature("Consume ingredients") { + Scenario("Some ingredients are consumed as a result of the producion process") { + Given("An available stock") + val stock: Stock = Map( + Milk -> StockedQuantity(10.0), + Cream -> StockedQuantity(20.0), + Rennet -> StockedQuantity(30.0), + Salt -> StockedQuantity(40.0), + Probiotics -> StockedQuantity(50.0), + ) + When("Some ingredients are consumed") + val newStock = consumeIngredients(stock)(ingredients) + Then("the stock should be consequently updated") + val updatedStock = Map( + Milk -> StockedQuantity(4.5), + Cream -> StockedQuantity(17.0), + Rennet -> StockedQuantity(28.0), + Salt -> StockedQuantity(37.0), + Probiotics -> StockedQuantity(49.9), + ) + newStock shouldBe updatedStock + } + } +} From fc45496ef539c206e58bd020552e0e5b94cefdbb Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 13:39:49 +0200 Subject: [PATCH 128/329] style: scalafmt --- .../dev/atedeg/mdm/restocking/Tests.scala | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala b/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala index 16660dec..b55713b8 100644 --- a/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala +++ b/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala @@ -1,48 +1,48 @@ -package dev.atedeg.mdm.restocking - -import cats.data.NonEmptyList -import org.scalatest.GivenWhenThen -import org.scalatest.featurespec.AnyFeatureSpec -import org.scalatest.matchers.should.Matchers - -import dev.atedeg.mdm.products.{ Ingredient, Product } -import dev.atedeg.mdm.products.Ingredient.{ Cream, Milk, Probiotics, Rennet, Salt } -import dev.atedeg.mdm.utils.given - -trait Mocks { - private val milk = QuintalsOfIngredient(WeightInQuintals(5.5), Milk) - private val cream = QuintalsOfIngredient(WeightInQuintals(3.0), Cream) - private val rennet = QuintalsOfIngredient(WeightInQuintals(2.0), Rennet) - private val salt = QuintalsOfIngredient(WeightInQuintals(3.0), Salt) - private val probiotics = QuintalsOfIngredient(WeightInQuintals(0.1), Probiotics) - - val ingredients: NonEmptyList[QuintalsOfIngredient] = NonEmptyList.of(milk, cream, rennet, salt, probiotics) - -} - -class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { - - Feature("Consume ingredients") { - Scenario("Some ingredients are consumed as a result of the producion process") { - Given("An available stock") - val stock: Stock = Map( - Milk -> StockedQuantity(10.0), - Cream -> StockedQuantity(20.0), - Rennet -> StockedQuantity(30.0), - Salt -> StockedQuantity(40.0), - Probiotics -> StockedQuantity(50.0), - ) - When("Some ingredients are consumed") - val newStock = consumeIngredients(stock)(ingredients) - Then("the stock should be consequently updated") - val updatedStock = Map( - Milk -> StockedQuantity(4.5), - Cream -> StockedQuantity(17.0), - Rennet -> StockedQuantity(28.0), - Salt -> StockedQuantity(37.0), - Probiotics -> StockedQuantity(49.9), - ) - newStock shouldBe updatedStock - } - } -} +package dev.atedeg.mdm.restocking + +import cats.data.NonEmptyList +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +import dev.atedeg.mdm.products.{ Ingredient, Product } +import dev.atedeg.mdm.products.Ingredient.{ Cream, Milk, Probiotics, Rennet, Salt } +import dev.atedeg.mdm.utils.given + +trait Mocks { + private val milk = QuintalsOfIngredient(WeightInQuintals(5.5), Milk) + private val cream = QuintalsOfIngredient(WeightInQuintals(3.0), Cream) + private val rennet = QuintalsOfIngredient(WeightInQuintals(2.0), Rennet) + private val salt = QuintalsOfIngredient(WeightInQuintals(3.0), Salt) + private val probiotics = QuintalsOfIngredient(WeightInQuintals(0.1), Probiotics) + + val ingredients: NonEmptyList[QuintalsOfIngredient] = NonEmptyList.of(milk, cream, rennet, salt, probiotics) + +} + +class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { + + Feature("Consume ingredients") { + Scenario("Some ingredients are consumed as a result of the producion process") { + Given("An available stock") + val stock: Stock = Map( + Milk -> StockedQuantity(10.0), + Cream -> StockedQuantity(20.0), + Rennet -> StockedQuantity(30.0), + Salt -> StockedQuantity(40.0), + Probiotics -> StockedQuantity(50.0), + ) + When("Some ingredients are consumed") + val newStock = consumeIngredients(stock)(ingredients) + Then("the stock should be consequently updated") + val updatedStock = Map( + Milk -> StockedQuantity(4.5), + Cream -> StockedQuantity(17.0), + Rennet -> StockedQuantity(28.0), + Salt -> StockedQuantity(37.0), + Probiotics -> StockedQuantity(49.9), + ) + newStock shouldBe updatedStock + } + } +} From c78555feb81b452309a948a5374a5425aa4d4ffa Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 18:43:28 +0200 Subject: [PATCH 129/329] refactor(build): depends on list --- build.sbt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 3593b4a8..4e8d7a31 100644 --- a/build.sbt +++ b/build.sbt @@ -131,5 +131,4 @@ lazy val stocking = project lazy val restocking = project .in(file("restocking")) .settings(commonSettings) - .dependsOn(utils) - .dependsOn(`products-shared-kernel`) + .dependsOn(utils, `products-shared-kernel`) From 55fcb1c3bd3a936cf46518ebb3086e40793f90c8 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 18:51:06 +0200 Subject: [PATCH 130/329] refactor: use scala 3 syntax in tests --- .../src/test/scala/dev/atedeg/mdm/restocking/Tests.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala b/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala index b55713b8..c54e7e17 100644 --- a/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala +++ b/restocking/src/test/scala/dev/atedeg/mdm/restocking/Tests.scala @@ -9,7 +9,7 @@ import dev.atedeg.mdm.products.{ Ingredient, Product } import dev.atedeg.mdm.products.Ingredient.{ Cream, Milk, Probiotics, Rennet, Salt } import dev.atedeg.mdm.utils.given -trait Mocks { +trait Mocks: private val milk = QuintalsOfIngredient(WeightInQuintals(5.5), Milk) private val cream = QuintalsOfIngredient(WeightInQuintals(3.0), Cream) private val rennet = QuintalsOfIngredient(WeightInQuintals(2.0), Rennet) @@ -18,9 +18,7 @@ trait Mocks { val ingredients: NonEmptyList[QuintalsOfIngredient] = NonEmptyList.of(milk, cream, rennet, salt, probiotics) -} - -class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { +class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Feature("Consume ingredients") { Scenario("Some ingredients are consumed as a result of the producion process") { @@ -45,4 +43,3 @@ class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with M newStock shouldBe updatedStock } } -} From 3246081668d6bac3ef768c4e745eb014bc244a01 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 18:52:14 +0200 Subject: [PATCH 131/329] docs: remove useless examples --- .../src/main/scala/dev/atedeg/mdm/restocking/Types.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala index deade225..03763207 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Types.scala @@ -6,9 +6,6 @@ import dev.atedeg.mdm.utils.given /** * A quantity of milk expressed in quintals. - * @note it must be a [[PositiveNumber positive number]]. - * @example `QuintalsOfMilk(1)` is a valid weight of 110 kg. - * @example `QuintalsOfMilk(0)` is not a valid weight. */ final case class QuintalsOfMilk(quintals: PositiveNumber) From dbc7db98f44382c4b3db3c8a62b55e865cc734fb Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 18:55:10 +0200 Subject: [PATCH 132/329] refactor: removed useless extends to ingredients case class --- .../scala/dev/atedeg/mdm/products/Ingredients.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala index 0fba881d..6d83594a 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Ingredients.scala @@ -4,8 +4,8 @@ package dev.atedeg.mdm.products * An ingredient that may be needed by a [[Recipe recipe]] to produce a [[CheeseType type of cheese]]. */ enum Ingredient: - case Milk extends Ingredient - case Cream extends Ingredient - case Rennet extends Ingredient - case Salt extends Ingredient - case Probiotics extends Ingredient + case Milk + case Cream + case Rennet + case Salt + case Probiotics From 126c77dc4b915263d27e8d06dc92f8fa55eadd69 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 18:56:19 +0200 Subject: [PATCH 133/329] docs: remove useless examples in milk planning bc --- .../src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala index d8bfff92..f4fa9952 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala @@ -16,9 +16,6 @@ final case class ProcessedMilk(quantity: QuintalsOfMilk) /** * A quantity of milk expressed in quintals. - * @note it must be a [[NonNegativeNumber non negative number]]. - * @example `QuintalsOfMilk(1)` is a valid weight of 110 kg. - * @example `QuintalsOfMilk(-2)` is not a valid weight. */ final case class QuintalsOfMilk(quintals: NonNegativeNumber) derives Plus, Times, Minus From 517ed999ad4f4759329ff4310705783b735afa1d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 3 Aug 2022 18:22:09 +0000 Subject: [PATCH 134/329] chore(release): 1.0.0-beta.5 [skip ci] # [1.0.0-beta.5](https://github.com/atedeg/mdm/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-03) ### Features * add restocking bc ([36af124](https://github.com/atedeg/mdm/commit/36af124611279107621dca88931483f72eb8ec81)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1762982e..b7f86f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.0.0-beta.5](https://github.com/atedeg/mdm/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-03) + + +### Features + +* add restocking bc ([36af124](https://github.com/atedeg/mdm/commit/36af124611279107621dca88931483f72eb8ec81)) + # [1.0.0-beta.4](https://github.com/atedeg/mdm/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-02) From c43d1df8d42cadf74b551fc17f0e18658735f08b Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 27 Jul 2022 11:49:41 +0200 Subject: [PATCH 135/329] docs: add bc description --- build.sbt | 6 ++++++ docs/_docs/production-planning.md | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/_docs/production-planning.md diff --git a/build.sbt b/build.sbt index 4e8d7a31..06e0aa4a 100644 --- a/build.sbt +++ b/build.sbt @@ -93,6 +93,7 @@ lazy val root = project utils, production, `milk-planning`, + `production-planning`, `products-shared-kernel`, restocking, ) @@ -132,3 +133,8 @@ lazy val restocking = project .in(file("restocking")) .settings(commonSettings) .dependsOn(utils, `products-shared-kernel`) + +lazy val `production-planning` = project + .in(file("production-planning")) + .settings(commonSettings) + .dependsOn(utils) diff --git a/docs/_docs/production-planning.md b/docs/_docs/production-planning.md new file mode 100644 index 00000000..4d2b5d09 --- /dev/null +++ b/docs/_docs/production-planning.md @@ -0,0 +1,9 @@ +Every morning Raffaella has to create a production plan which contains how many of each product to produce that day. + +She makes this plan taking into account the following factors: +- the production plan for the same day of the previous year +- the products that have to be manufactured that day to fulfil new orders considering the ripening time of each cheese type + For instance, a caciotta takes seven days to ripen, so the production has to start seven days before the order's deadline. +- the products needed to replenish the stock. + +> 💡 The completed production plan is sent to the production B.C. From 935e3628fd38adac531e5b2a66b1945aa6692ab9 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Fri, 29 Jul 2022 10:48:30 +0200 Subject: [PATCH 136/329] build: add dependencies to prod-planning bc --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 06e0aa4a..377bca0b 100644 --- a/build.sbt +++ b/build.sbt @@ -137,4 +137,4 @@ lazy val restocking = project lazy val `production-planning` = project .in(file("production-planning")) .settings(commonSettings) - .dependsOn(utils) + .dependsOn(utils, `products-shared-kernel`) From d2bd7fbf7db983df3d3de280852d7cf4f93f063a Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Fri, 29 Jul 2022 11:39:19 +0200 Subject: [PATCH 137/329] docs: add include for ul table --- docs/_docs/production-planning.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/_docs/production-planning.md b/docs/_docs/production-planning.md index 4d2b5d09..47475caa 100644 --- a/docs/_docs/production-planning.md +++ b/docs/_docs/production-planning.md @@ -2,8 +2,11 @@ Every morning Raffaella has to create a production plan which contains how many She makes this plan taking into account the following factors: - the production plan for the same day of the previous year -- the products that have to be manufactured that day to fulfil new orders considering the ripening time of each cheese type +- the products that have to be manufactured that day to fulfil new orders considering the ripening time of each cheese type. For instance, a caciotta takes seven days to ripen, so the production has to start seven days before the order's deadline. -- the products needed to replenish the stock. +- the products needed to replenish the stock > 💡 The completed production plan is sent to the production B.C. + + +% include production-planning-ul.md % From fbdcd837a169d53e414810b74003d6672d0118b4 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Mon, 1 Aug 2022 12:39:22 +0200 Subject: [PATCH 138/329] feat: bc types definition --- .../atedeg/mdm/productionplanning/Types.scala | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala new file mode 100644 index 00000000..ef29e207 --- /dev/null +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala @@ -0,0 +1,41 @@ +package dev.atedeg.mdm.productionplanning + +import dev.atedeg.mdm.utils.{NonNegativeNumber, NumberInClosedRange, PositiveNumber} +import dev.atedeg.mdm.products.{CheeseType, Product} +import cats.data.NonEmptyList + +import java.time.LocalDate + +/** + * All the [[ProductToProduce products to be produced]] in a day. + */ +final case class ProductionPlan(productsToProduce: NonEmptyList[ProductToProduce]) + +/** + * The [[Quantity quantity]] of each [[Product product]] to be produced. + */ +final case class ProductToProduce(product: Product, quantity: Quantity) + +/** + * A quantity of something. + * @example `Quantity(-2)` is not a valid quantity. + * @example `Quantity(5)` is a valida quantity. + */ +final case class Quantity(n: PositiveNumber) + +final case class Order(deadline: LocalDate, orderedProducts: NonEmptyList[OrderedProduct]) + +final case class OrderedProduct(product: Product, quantity: Quantity) + +/** + * Defines how many [[RipeningDays days of ripening]] are needed for a given [[CheeseType type of cheese]]. + */ +type CheeseTypeRipeningDays = CheeseType => RipeningDays + +/** + * The number of days needed for the ripening process to be done. + * @example `RipeningDays(7)` is a valid number of days. + * @example `RipeningDays(-2)` is not a valid number of days. + */ +final case class RipeningDays(days: NonNegativeNumber) + From 1ae213e4af772da429d072941a7844c7838a0a12 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Mon, 1 Aug 2022 12:39:59 +0200 Subject: [PATCH 139/329] feat: bc events definition --- .../scala/dev/atedeg/mdm/productionplanning/Events.scala | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala new file mode 100644 index 00000000..995d5f61 --- /dev/null +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala @@ -0,0 +1,7 @@ +package dev.atedeg.mdm.productionplanning + +enum IncomingEvent: + case NewOrderReceived(order: Order) + +enum OutgoingEvent: + case ProductionPlanReady(productionPlan: ProductionPlan) From 984a7ba6e6813d73e7285e4d6c4af9e756e645b2 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Mon, 1 Aug 2022 12:40:14 +0200 Subject: [PATCH 140/329] feat: bc actions definition --- .../dev/atedeg/mdm/productionplanning/Actions.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala new file mode 100644 index 00000000..541b61df --- /dev/null +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -0,0 +1,13 @@ +package dev.atedeg.mdm.productionplanning + +import cats.Monad + +import dev.atedeg.mdm.productionplanning.{ CheeseTypeRipeningDays, RipeningDays } +import dev.atedeg.mdm.productionplanning.OutgoingEvent.ProductionPlanReady +import dev.atedeg.mdm.products.CheeseType +import dev.atedeg.mdm.utils.monads.Emits + +private def createProductionPlan[M[_]: Emits[ProductionPlanReady]: Monad]: M[ProductionPlan] = ??? + +private def daysNeededForRipening(cheeseType: CheeseType, cheeseRipeningDays: CheeseTypeRipeningDays): RipeningDays = + ??? From 4b8d7b3cf0b33d9efc1c48ac66a31ad6752c492f Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 12:21:41 +0200 Subject: [PATCH 141/329] chore: add more events to the bc --- .../atedeg/mdm/productionplanning/Events.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala index 995d5f61..8c42b20f 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala @@ -1,7 +1,25 @@ package dev.atedeg.mdm.productionplanning +/** + * The events that have to be handled by the bounded context. + */ enum IncomingEvent: + /** + * Event representing an order placed used to create the [[ProductionPlan production plan]] of the day. + */ case NewOrderReceived(order: Order) +/** + * The events that may be produced by the bounded context. + */ enum OutgoingEvent: + /** + * Events that contains the [[ProductionPlan production plan]] of the day. + */ case ProductionPlanReady(productionPlan: ProductionPlan) + + /** + * If an order cannot be fulfilled since there are some products' ripening days takes more + * time than the order required date. + */ + case OrderDelayed() From 80cd7d30551be33afb5baafd592b90393511781a Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 15:33:54 +0200 Subject: [PATCH 142/329] docs: add tables placeolders --- docs/_docs/production-planning.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/_docs/production-planning.md b/docs/_docs/production-planning.md index 47475caa..082dee05 100644 --- a/docs/_docs/production-planning.md +++ b/docs/_docs/production-planning.md @@ -3,10 +3,23 @@ Every morning Raffaella has to create a production plan which contains how many She makes this plan taking into account the following factors: - the production plan for the same day of the previous year - the products that have to be manufactured that day to fulfil new orders considering the ripening time of each cheese type. - For instance, a caciotta takes seven days to ripen, so the production has to start seven days before the order's deadline. + > _e.g._ a caciotta takes seven days to ripen, so the production has to start seven days before the order's deadline. + + If an order contains a product with ripening days that takes more time than the order required date, it will be delayed. - the products needed to replenish the stock > 💡 The completed production plan is sent to the production B.C. +## Ubiquitous Language % include production-planning-ul.md % + +## Domain Events + +### Incoming Events + +{% include production-planning-incoming.md %} + +### Outgoing Events + +{% include production-planning-outgoing.md %} From 86c6e869432d3def47e7ff6f0a3bbbb63e4c3f5f Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 15:35:05 +0200 Subject: [PATCH 143/329] feat: implement bc actions --- .../mdm/productionplanning/Actions.scala | 61 +++++++++++++++++-- .../mdm/productionplanning/Events.scala | 2 +- .../atedeg/mdm/productionplanning/Types.scala | 35 +++++++++-- 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index 541b61df..c91e3a91 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -1,13 +1,64 @@ package dev.atedeg.mdm.productionplanning +import java.time.LocalDate + import cats.Monad +import cats.data.NonEmptyList +import cats.syntax.all.* import dev.atedeg.mdm.productionplanning.{ CheeseTypeRipeningDays, RipeningDays } -import dev.atedeg.mdm.productionplanning.OutgoingEvent.ProductionPlanReady +import dev.atedeg.mdm.productionplanning.OutgoingEvent.{ OrderDelayed, ProductionPlanReady } +import dev.atedeg.mdm.productionplanning.utils.given import dev.atedeg.mdm.products.CheeseType -import dev.atedeg.mdm.utils.monads.Emits +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.products.* + +/** + * Estimate how many [[Product products]] to produce that day. + * To do so, takes into account the [[ProductionPlan production plan]] for the same day of the previous year, + * the new [[Order orders]] considering the [[RipeningDays ripening time]] for each [[CheeseType cheese type]] and the + * [[Product products]] needed to replenish the stock. + * If an order cannot be fulfilled since there are some products' ripening days that takes more + * time than the order required date, it emits an [[OrderDelayed order delayed]] event. + */ +private def createProductionPlan[M[_]: Monad: Emits[ProductionPlanReady]: CanEmit[OrderDelayed]]( + stock: Stock, + cheeseTypeRipeningDays: CheeseTypeRipeningDays, +)( + previousProductionPlan: ProductionPlan, + orders: List[Order], +): M[ProductionPlan] = for { + _ <- orders.traverse(checkDeliverabilityOfOrder(cheeseTypeRipeningDays)) + productsToProduce = magicAIProductsToProduceEstimator(orders, previousProductionPlan, cheeseTypeRipeningDays, stock) + productionPlan = ProductionPlan(productsToProduce) + _ <- emit(ProductionPlanReady(productionPlan): ProductionPlanReady) +} yield productionPlan + +private def checkDeliverabilityOfOrder[M[_]: Monad: CanEmit[OrderDelayed]]( + cheeseTypeRipeningDays: CheeseTypeRipeningDays, +)(order: Order): M[Unit] = { + val ripeningDays = order.orderedProducts.map(_.product.cheeseType).map(cheeseTypeRipeningDays(_)) + val isDelayed = ripeningDays.map(productionInTime(_, order.requiredBy)).exists(_ == OrderStatus.Delayed) + when(isDelayed)(emit(OrderDelayed(order.orderdID): OrderDelayed)) +} + +private def magicAIProductsToProduceEstimator( + orders: List[Order], + previousProductionPlan: ProductionPlan, + cheeseTypeRipeningDays: CheeseTypeRipeningDays, + stock: Stock, +): NonEmptyList[ProductToProduce] = + // This is a mock, ideally that would be estimated by an intelligent agent or some heuristics. + NonEmptyList.of(ProductToProduce(Product.Caciotta(500), Quantity(5))) -private def createProductionPlan[M[_]: Emits[ProductionPlanReady]: Monad]: M[ProductionPlan] = ??? +private def productionInTime(ripeningDays: RipeningDays, requiredBy: LocalDate): OrderStatus = + val today = java.time.LocalDate.now + if today.plusDays(ripeningDays.days.value.toLong).isBefore(requiredBy) + then OrderStatus.NonDelayed + else OrderStatus.Delayed -private def daysNeededForRipening(cheeseType: CheeseType, cheeseRipeningDays: CheeseTypeRipeningDays): RipeningDays = - ??? +private enum OrderStatus: + case Delayed + case NonDelayed diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala index 8c42b20f..44745320 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala @@ -22,4 +22,4 @@ enum OutgoingEvent: * If an order cannot be fulfilled since there are some products' ripening days takes more * time than the order required date. */ - case OrderDelayed() + case OrderDelayed(orderID: OrderID) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala index ef29e207..b131dcf0 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala @@ -1,10 +1,13 @@ package dev.atedeg.mdm.productionplanning -import dev.atedeg.mdm.utils.{NonNegativeNumber, NumberInClosedRange, PositiveNumber} +import dev.atedeg.mdm.utils.{NonNegativeNumber, NumberInClosedRange, Plus, PositiveNumber, Times} import dev.atedeg.mdm.products.{CheeseType, Product} import cats.data.NonEmptyList +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given import java.time.LocalDate +import java.util.UUID /** * All the [[ProductToProduce products to be produced]] in a day. @@ -21,10 +24,35 @@ final case class ProductToProduce(product: Product, quantity: Quantity) * @example `Quantity(-2)` is not a valid quantity. * @example `Quantity(5)` is a valida quantity. */ -final case class Quantity(n: PositiveNumber) +final case class Quantity(n: PositiveNumber) derives Plus, Times -final case class Order(deadline: LocalDate, orderedProducts: NonEmptyList[OrderedProduct]) +/** + * It defines, for each [[Product product]], the [[StockedQuantity quantity in stock]]. + */ +type Stock = Product => StockedQuantity + +/** + * A quantity of a stocked [[Product product]], it may also be zero. + * @note it must be a [[NonNegativeNumber non-negative number]]. + * @example `StockedQuantity(0)` is valid. + * @example `StockedQuantity(-1)` is invalid. + */ +final case class StockedQuantity(n: NonNegativeNumber) + +/** + * A set of requested [[Product product]] with the [[Quantity quantities]] that have to be produced by the given + * [[LocalDate date]]. + */ +final case class Order(orderdID: OrderID, requiredBy: LocalDate, orderedProducts: NonEmptyList[OrderedProduct]) + +/** + * Uniquely identifies an [[Order order]]. + */ +final case class OrderID(id: UUID) +/** + * A [[Product product]] requested in a given [[Quantity quantity]]. + */ final case class OrderedProduct(product: Product, quantity: Quantity) /** @@ -38,4 +66,3 @@ type CheeseTypeRipeningDays = CheeseType => RipeningDays * @example `RipeningDays(-2)` is not a valid number of days. */ final case class RipeningDays(days: NonNegativeNumber) - From acdbd782dde096f03ce047fceac3d746a51f8110 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 15:35:47 +0200 Subject: [PATCH 144/329] test: add test template --- .../scala/dev.atedeg.mdm.productionplanning/Tests.scala | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala diff --git a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala new file mode 100644 index 00000000..d60fbb02 --- /dev/null +++ b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.productionplanning + +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +trait Mocks + +class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks From bda6e42722c53424a5fe734ee6f80ed9a0bedb15 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 15:41:36 +0200 Subject: [PATCH 145/329] style: scalafmt + scalafix --- .../mdm/productionplanning/Actions.scala | 127 ++++++++-------- .../mdm/productionplanning/Events.scala | 50 +++---- .../atedeg/mdm/productionplanning/Types.scala | 137 +++++++++--------- .../Tests.scala | 18 +-- 4 files changed, 166 insertions(+), 166 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index c91e3a91..f4a1b76b 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -1,64 +1,63 @@ -package dev.atedeg.mdm.productionplanning - -import java.time.LocalDate - -import cats.Monad -import cats.data.NonEmptyList -import cats.syntax.all.* - -import dev.atedeg.mdm.productionplanning.{ CheeseTypeRipeningDays, RipeningDays } -import dev.atedeg.mdm.productionplanning.OutgoingEvent.{ OrderDelayed, ProductionPlanReady } -import dev.atedeg.mdm.productionplanning.utils.given -import dev.atedeg.mdm.products.CheeseType -import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.given -import dev.atedeg.mdm.utils.monads.* -import dev.atedeg.mdm.products.* - -/** - * Estimate how many [[Product products]] to produce that day. - * To do so, takes into account the [[ProductionPlan production plan]] for the same day of the previous year, - * the new [[Order orders]] considering the [[RipeningDays ripening time]] for each [[CheeseType cheese type]] and the - * [[Product products]] needed to replenish the stock. - * If an order cannot be fulfilled since there are some products' ripening days that takes more - * time than the order required date, it emits an [[OrderDelayed order delayed]] event. - */ -private def createProductionPlan[M[_]: Monad: Emits[ProductionPlanReady]: CanEmit[OrderDelayed]]( - stock: Stock, - cheeseTypeRipeningDays: CheeseTypeRipeningDays, -)( - previousProductionPlan: ProductionPlan, - orders: List[Order], -): M[ProductionPlan] = for { - _ <- orders.traverse(checkDeliverabilityOfOrder(cheeseTypeRipeningDays)) - productsToProduce = magicAIProductsToProduceEstimator(orders, previousProductionPlan, cheeseTypeRipeningDays, stock) - productionPlan = ProductionPlan(productsToProduce) - _ <- emit(ProductionPlanReady(productionPlan): ProductionPlanReady) -} yield productionPlan - -private def checkDeliverabilityOfOrder[M[_]: Monad: CanEmit[OrderDelayed]]( - cheeseTypeRipeningDays: CheeseTypeRipeningDays, -)(order: Order): M[Unit] = { - val ripeningDays = order.orderedProducts.map(_.product.cheeseType).map(cheeseTypeRipeningDays(_)) - val isDelayed = ripeningDays.map(productionInTime(_, order.requiredBy)).exists(_ == OrderStatus.Delayed) - when(isDelayed)(emit(OrderDelayed(order.orderdID): OrderDelayed)) -} - -private def magicAIProductsToProduceEstimator( - orders: List[Order], - previousProductionPlan: ProductionPlan, - cheeseTypeRipeningDays: CheeseTypeRipeningDays, - stock: Stock, -): NonEmptyList[ProductToProduce] = - // This is a mock, ideally that would be estimated by an intelligent agent or some heuristics. - NonEmptyList.of(ProductToProduce(Product.Caciotta(500), Quantity(5))) - -private def productionInTime(ripeningDays: RipeningDays, requiredBy: LocalDate): OrderStatus = - val today = java.time.LocalDate.now - if today.plusDays(ripeningDays.days.value.toLong).isBefore(requiredBy) - then OrderStatus.NonDelayed - else OrderStatus.Delayed - -private enum OrderStatus: - case Delayed - case NonDelayed +package dev.atedeg.mdm.productionplanning + +import java.time.LocalDate + +import cats.Monad +import cats.data.NonEmptyList +import cats.syntax.all.* + +import dev.atedeg.mdm.productionplanning.{ CheeseTypeRipeningDays, RipeningDays } +import dev.atedeg.mdm.productionplanning.OutgoingEvent.{ OrderDelayed, ProductionPlanReady } +import dev.atedeg.mdm.products.* +import dev.atedeg.mdm.products.CheeseType +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given +import dev.atedeg.mdm.utils.monads.* + +/** + * Estimate how many [[Product products]] to produce that day. + * To do so, takes into account the [[ProductionPlan production plan]] for the same day of the previous year, + * the new [[Order orders]] considering the [[RipeningDays ripening time]] for each [[CheeseType cheese type]] and the + * [[Product products]] needed to replenish the stock. + * If an order cannot be fulfilled since there are some products' ripening days that takes more + * time than the order required date, it emits an [[OrderDelayed order delayed]] event. + */ +private def createProductionPlan[M[_]: Monad: Emits[ProductionPlanReady]: CanEmit[OrderDelayed]]( + stock: Stock, + cheeseTypeRipeningDays: CheeseTypeRipeningDays, +)( + previousProductionPlan: ProductionPlan, + orders: List[Order], +): M[ProductionPlan] = for { + _ <- orders.traverse(checkDeliverabilityOfOrder(cheeseTypeRipeningDays)) + productsToProduce = magicAIProductsToProduceEstimator(orders, previousProductionPlan, cheeseTypeRipeningDays, stock) + productionPlan = ProductionPlan(productsToProduce) + _ <- emit(ProductionPlanReady(productionPlan): ProductionPlanReady) +} yield productionPlan + +private def checkDeliverabilityOfOrder[M[_]: Monad: CanEmit[OrderDelayed]]( + cheeseTypeRipeningDays: CheeseTypeRipeningDays, +)(order: Order): M[Unit] = { + val ripeningDays = order.orderedProducts.map(_.product.cheeseType).map(cheeseTypeRipeningDays(_)) + val isDelayed = ripeningDays.map(productionInTime(_, order.requiredBy)).exists(_ == OrderStatus.Delayed) + when(isDelayed)(emit(OrderDelayed(order.orderdID): OrderDelayed)) +} + +private def magicAIProductsToProduceEstimator( + orders: List[Order], + previousProductionPlan: ProductionPlan, + cheeseTypeRipeningDays: CheeseTypeRipeningDays, + stock: Stock, +): NonEmptyList[ProductToProduce] = + // This is a mock, ideally that would be estimated by an intelligent agent or some heuristics. + NonEmptyList.of(ProductToProduce(Product.Caciotta(500), Quantity(5))) + +private def productionInTime(ripeningDays: RipeningDays, requiredBy: LocalDate): OrderStatus = + val today = java.time.LocalDate.now + if today.plusDays(ripeningDays.days.value.toLong).isBefore(requiredBy) + then OrderStatus.NonDelayed + else OrderStatus.Delayed + +private enum OrderStatus: + case Delayed + case NonDelayed diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala index 44745320..3d6ee1b9 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala @@ -1,25 +1,25 @@ -package dev.atedeg.mdm.productionplanning - -/** - * The events that have to be handled by the bounded context. - */ -enum IncomingEvent: - /** - * Event representing an order placed used to create the [[ProductionPlan production plan]] of the day. - */ - case NewOrderReceived(order: Order) - -/** - * The events that may be produced by the bounded context. - */ -enum OutgoingEvent: - /** - * Events that contains the [[ProductionPlan production plan]] of the day. - */ - case ProductionPlanReady(productionPlan: ProductionPlan) - - /** - * If an order cannot be fulfilled since there are some products' ripening days takes more - * time than the order required date. - */ - case OrderDelayed(orderID: OrderID) +package dev.atedeg.mdm.productionplanning + +/** + * The events that have to be handled by the bounded context. + */ +enum IncomingEvent: + /** + * Event representing an order placed used to create the [[ProductionPlan production plan]] of the day. + */ + case NewOrderReceived(order: Order) + +/** + * The events that may be produced by the bounded context. + */ +enum OutgoingEvent: + /** + * Events that contains the [[ProductionPlan production plan]] of the day. + */ + case ProductionPlanReady(productionPlan: ProductionPlan) + + /** + * If an order cannot be fulfilled since there are some products' ripening days takes more + * time than the order required date. + */ + case OrderDelayed(orderID: OrderID) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala index b131dcf0..222657ab 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala @@ -1,68 +1,69 @@ -package dev.atedeg.mdm.productionplanning - -import dev.atedeg.mdm.utils.{NonNegativeNumber, NumberInClosedRange, Plus, PositiveNumber, Times} -import dev.atedeg.mdm.products.{CheeseType, Product} -import cats.data.NonEmptyList -import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.given - -import java.time.LocalDate -import java.util.UUID - -/** - * All the [[ProductToProduce products to be produced]] in a day. - */ -final case class ProductionPlan(productsToProduce: NonEmptyList[ProductToProduce]) - -/** - * The [[Quantity quantity]] of each [[Product product]] to be produced. - */ -final case class ProductToProduce(product: Product, quantity: Quantity) - -/** - * A quantity of something. - * @example `Quantity(-2)` is not a valid quantity. - * @example `Quantity(5)` is a valida quantity. - */ -final case class Quantity(n: PositiveNumber) derives Plus, Times - -/** - * It defines, for each [[Product product]], the [[StockedQuantity quantity in stock]]. - */ -type Stock = Product => StockedQuantity - -/** - * A quantity of a stocked [[Product product]], it may also be zero. - * @note it must be a [[NonNegativeNumber non-negative number]]. - * @example `StockedQuantity(0)` is valid. - * @example `StockedQuantity(-1)` is invalid. - */ -final case class StockedQuantity(n: NonNegativeNumber) - -/** - * A set of requested [[Product product]] with the [[Quantity quantities]] that have to be produced by the given - * [[LocalDate date]]. - */ -final case class Order(orderdID: OrderID, requiredBy: LocalDate, orderedProducts: NonEmptyList[OrderedProduct]) - -/** - * Uniquely identifies an [[Order order]]. - */ -final case class OrderID(id: UUID) - -/** - * A [[Product product]] requested in a given [[Quantity quantity]]. - */ -final case class OrderedProduct(product: Product, quantity: Quantity) - -/** - * Defines how many [[RipeningDays days of ripening]] are needed for a given [[CheeseType type of cheese]]. - */ -type CheeseTypeRipeningDays = CheeseType => RipeningDays - -/** - * The number of days needed for the ripening process to be done. - * @example `RipeningDays(7)` is a valid number of days. - * @example `RipeningDays(-2)` is not a valid number of days. - */ -final case class RipeningDays(days: NonNegativeNumber) +package dev.atedeg.mdm.productionplanning + +import java.time.LocalDate +import java.util.UUID + +import cats.data.NonEmptyList + +import dev.atedeg.mdm.products.{ CheeseType, Product } +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.{ NonNegativeNumber, NumberInClosedRange, Plus, PositiveNumber, Times } +import dev.atedeg.mdm.utils.given + +/** + * All the [[ProductToProduce products to be produced]] in a day. + */ +final case class ProductionPlan(productsToProduce: NonEmptyList[ProductToProduce]) + +/** + * The [[Quantity quantity]] of each [[Product product]] to be produced. + */ +final case class ProductToProduce(product: Product, quantity: Quantity) + +/** + * A quantity of something. + * @example `Quantity(-2)` is not a valid quantity. + * @example `Quantity(5)` is a valida quantity. + */ +final case class Quantity(n: PositiveNumber) derives Plus, Times + +/** + * It defines, for each [[Product product]], the [[StockedQuantity quantity in stock]]. + */ +type Stock = Product => StockedQuantity + +/** + * A quantity of a stocked [[Product product]], it may also be zero. + * @note it must be a [[NonNegativeNumber non-negative number]]. + * @example `StockedQuantity(0)` is valid. + * @example `StockedQuantity(-1)` is invalid. + */ +final case class StockedQuantity(n: NonNegativeNumber) + +/** + * A set of requested [[Product product]] with the [[Quantity quantities]] that have to be produced by the given + * [[LocalDate date]]. + */ +final case class Order(orderdID: OrderID, requiredBy: LocalDate, orderedProducts: NonEmptyList[OrderedProduct]) + +/** + * Uniquely identifies an [[Order order]]. + */ +final case class OrderID(id: UUID) + +/** + * A [[Product product]] requested in a given [[Quantity quantity]]. + */ +final case class OrderedProduct(product: Product, quantity: Quantity) + +/** + * Defines how many [[RipeningDays days of ripening]] are needed for a given [[CheeseType type of cheese]]. + */ +type CheeseTypeRipeningDays = CheeseType => RipeningDays + +/** + * The number of days needed for the ripening process to be done. + * @example `RipeningDays(7)` is a valid number of days. + * @example `RipeningDays(-2)` is not a valid number of days. + */ +final case class RipeningDays(days: NonNegativeNumber) diff --git a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala index d60fbb02..4af77ee1 100644 --- a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala @@ -1,9 +1,9 @@ -package dev.atedeg.mdm.productionplanning - -import org.scalatest.GivenWhenThen -import org.scalatest.featurespec.AnyFeatureSpec -import org.scalatest.matchers.should.Matchers - -trait Mocks - -class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks +package dev.atedeg.mdm.productionplanning + +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +trait Mocks + +class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks From e5a9452477e8e2be9370e6ccdc4dd93c1bc434e8 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 20:10:30 +0200 Subject: [PATCH 146/329] chore: add ubidoc config for production planning --- .ubidoc.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.ubidoc.yml b/.ubidoc.yml index 34abb053..5899dd0d 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -82,5 +82,24 @@ tables: rows: - case: "IncomingEvent.OrderMilk" - case: "IncomingEvent.ProductionStarted" + - name: "production-planning-ul" + rows: + - class: "ProductionPlan" + - class: "ProductToProduce" + - class: "Quantity" + - type: "Stock" + - class: "StockedQuantity" + - class: "Order" + - class: "OrderedProduct" + - type: "CheeseTypeRipeningDays" + - class: "RipeningDays" + + - name: "production-planning-incoming" + rows: + - case: "IncomingEvent.NewOrderReceived" + - name: "production-planning-outgoing" + rows: + - case: "OutgoingEvent.ProductionPlanReady" + - case: "OutgoingEvent.OrderDelayed" ignored: [] From ca728a8bfab1f8716b10edbca2553e59247d0830 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 20:16:26 +0200 Subject: [PATCH 147/329] docs: fix include table --- docs/_docs/production-planning.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/_docs/production-planning.md b/docs/_docs/production-planning.md index 082dee05..605cfe81 100644 --- a/docs/_docs/production-planning.md +++ b/docs/_docs/production-planning.md @@ -1,3 +1,9 @@ +--- +title: Production Planning +--- + +# Production Planning + Every morning Raffaella has to create a production plan which contains how many of each product to produce that day. She makes this plan taking into account the following factors: @@ -12,7 +18,7 @@ She makes this plan taking into account the following factors: ## Ubiquitous Language -% include production-planning-ul.md % +{% include production-planning-ul.md %} ## Domain Events From 9a69f2a3f492c5f4111e5a8f05eaa96020a7089b Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 12:47:47 +0200 Subject: [PATCH 148/329] chore: add safe actions with two events --- .../scala/dev/atedeg/mdm/utils/monads/Stacks.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala index e77d3c62..6b982acb 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala @@ -21,6 +21,17 @@ type Action[Error, Event, Result] = ActionWithState[Error, Event, Result, Unit] */ type SafeAction[Event, Result] = Writer[List[Event], Result] +/** + * The same as a [[SafeAction safe action]] but with two events. + */ +type SafeActionTwoEvents[Event1, Event2, Result] = WriterT[[A] =>> Writer[List[Event2], A], List[Event1], Result] + +extension [Event1, Event2, Result](action: SafeActionTwoEvents[Event1, Event2, Result]) + + def execute: (List[Event1], List[Event2], Result) = + val (e2, (e1, res)) = action.run.run + (e1, e2, res) + extension [Event, Result](action: SafeAction[Event, Result]) def execute: (List[Event], Result) = action.run extension [Error, Event, Result, State](action: ActionWithState[Error, Event, Result, State]) From e931dc2e7cd95d07f308becd6d0cf36bb7cb1c62 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 12:48:28 +0200 Subject: [PATCH 149/329] test: tests for production planning bc --- .../Tests.scala | 93 ++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala index 4af77ee1..54e53434 100644 --- a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala @@ -1,9 +1,98 @@ package dev.atedeg.mdm.productionplanning +import java.util.UUID + +import cats.data.{ NonEmptyList, Writer } +import cats.syntax.all.* import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers -trait Mocks +import dev.atedeg.mdm.productionplanning.* +import dev.atedeg.mdm.productionplanning.OutgoingEvent.{ OrderDelayed, ProductionPlanReady } +import dev.atedeg.mdm.products.{ CheeseType, Product } +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given +import dev.atedeg.mdm.utils.monads.* + +trait Mocks { + + val cheeseTypeRipeningDays: CheeseTypeRipeningDays = Map( + CheeseType.Squacquerone -> RipeningDays(4), + CheeseType.Ricotta -> RipeningDays(0), + CheeseType.Caciotta -> RipeningDays(8), + CheeseType.Casatella -> RipeningDays(4), + CheeseType.Stracchino -> RipeningDays(5), + ) + + val prodToProd1: ProductToProduce = ProductToProduce(Product.Caciotta(500), Quantity(5)) + val prodToProd2: ProductToProduce = ProductToProduce(Product.Casatella(300), Quantity(10)) + val prodToProd3: ProductToProduce = ProductToProduce(Product.Squacquerone(250), Quantity(10)) + + val orderedProd1: OrderedProduct = OrderedProduct(Product.Caciotta(500), Quantity(5)) + val orderedProd2: OrderedProduct = OrderedProduct(Product.Casatella(300), Quantity(10)) + val orderedProd3: OrderedProduct = OrderedProduct(Product.Squacquerone(250), Quantity(10)) +} + +@SuppressWarnings(Array("org.wartremover.warts.Any")) +class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { + + Feature("Create the production plan for the day") { + Scenario("Raffaella wants to create the production plan") { + Given("a list of orders with deadline in 10 days") + val requiredBy = java.time.LocalDate.now.plusDays(10) + val orderedProducts = NonEmptyList.of(orderedProd1, orderedProd2, orderedProd3) + val orders = List( + Order(OrderID(UUID.randomUUID()), requiredBy, orderedProducts), + Order(OrderID(UUID.randomUUID()), requiredBy, orderedProducts), + Order(OrderID(UUID.randomUUID()), requiredBy, orderedProducts), + ) + And("the production plan of the previous year for the same day") + val productsToProduce = NonEmptyList.of(prodToProd1, prodToProd2, prodToProd3) + val previousProductionPlan = ProductionPlan(productsToProduce) + And("an empty stock") + val stock: Stock = _ => StockedQuantity(0) + When("creating the production plan") + val productionPlanCreation: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = + createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders) + val (events1, events2, productionPlan) = productionPlanCreation.execute + Then( + "the result is the mocked result of a magic AI products to produce estimator: " + + "NonEmptyList.of(ProductToProduce(Product.Caciotta(500), Quantity(5)))", + ) + val todaysProductionPlan: ProductionPlan = ProductionPlan( + NonEmptyList.of( + ProductToProduce(Product.Caciotta(500), Quantity(5)), + ), + ) + productionPlan shouldBe todaysProductionPlan + events1 should not be empty + events1.map(_.productionPlan) should contain(productionPlan) + events2 shouldBe empty -class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks + Given("a list of orders with deadline in 6 days (which is less than the ordered Caciotta's ripening time)") + val requiredBy2 = java.time.LocalDate.now.plusDays(6) + val orderedProducts1 = NonEmptyList.of(orderedProd1, orderedProd2, orderedProd3) + val orderedProducts2 = NonEmptyList.of(orderedProd2, orderedProd3) + val orderedProducts3 = NonEmptyList.of(orderedProd2, orderedProd3) + val order1ID = OrderID(UUID.randomUUID()) + val order2ID = OrderID(UUID.randomUUID()) + val order3ID = OrderID(UUID.randomUUID()) + val orders2 = List( + Order(order1ID, requiredBy2, orderedProducts1), + Order(order2ID, requiredBy2, orderedProducts2), + Order(order3ID, requiredBy2, orderedProducts3), + ) + And("the production plan of the previous year for the same day") + And("an empty stock") + When("creating the production plan") + val productionPlanCreation2: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = + createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders2) + val (e1, e2, pp) = productionPlanCreation2.execute + Then("Should delay the order containing the Cacciotta product") + pp shouldBe todaysProductionPlan + e1 should not be empty + e2 should contain(OrderDelayed(order1ID)) + } + } +} From 8aa246bfa2fcefdc962af2902f07855939990078 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 19:01:45 +0200 Subject: [PATCH 150/329] refactor: usign scala 3 syntax in tests --- .../scala/dev.atedeg.mdm.productionplanning/Tests.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala index 54e53434..cbd4f146 100644 --- a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala @@ -15,7 +15,7 @@ import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.* -trait Mocks { +trait Mocks: val cheeseTypeRipeningDays: CheeseTypeRipeningDays = Map( CheeseType.Squacquerone -> RipeningDays(4), @@ -32,10 +32,9 @@ trait Mocks { val orderedProd1: OrderedProduct = OrderedProduct(Product.Caciotta(500), Quantity(5)) val orderedProd2: OrderedProduct = OrderedProduct(Product.Casatella(300), Quantity(10)) val orderedProd3: OrderedProduct = OrderedProduct(Product.Squacquerone(250), Quantity(10)) -} @SuppressWarnings(Array("org.wartremover.warts.Any")) -class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { +class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Feature("Create the production plan for the day") { Scenario("Raffaella wants to create the production plan") { @@ -95,4 +94,3 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { e2 should contain(OrderDelayed(order1ID)) } } -} From 9a83b1c78ec15dfd8a29c65671bb5095c697d3c7 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 19:17:45 +0200 Subject: [PATCH 151/329] docs: refactor some sentences according to @giacomocavalieri suggestions --- .../scala/dev/atedeg/mdm/productionplanning/Actions.scala | 6 +++--- .../scala/dev/atedeg/mdm/productionplanning/Events.scala | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index f4a1b76b..25462bdb 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -17,10 +17,10 @@ import dev.atedeg.mdm.utils.monads.* /** * Estimate how many [[Product products]] to produce that day. * To do so, takes into account the [[ProductionPlan production plan]] for the same day of the previous year, - * the new [[Order orders]] considering the [[RipeningDays ripening time]] for each [[CheeseType cheese type]] and the + * the new [[Order orders]] considering the [[RipeningDays ripening days]] of each [[CheeseType cheese type]] and the * [[Product products]] needed to replenish the stock. - * If an order cannot be fulfilled since there are some products' ripening days that takes more - * time than the order required date, it emits an [[OrderDelayed order delayed]] event. + * If an order cannot be fulfilled since it contains products whose ripening days make it impossible + * to satisfy the order by the required date, it emits an [[OrderDelayed order delayed]] event. */ private def createProductionPlan[M[_]: Monad: Emits[ProductionPlanReady]: CanEmit[OrderDelayed]]( stock: Stock, diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala index 3d6ee1b9..d3f07347 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala @@ -5,7 +5,7 @@ package dev.atedeg.mdm.productionplanning */ enum IncomingEvent: /** - * Event representing an order placed used to create the [[ProductionPlan production plan]] of the day. + * Event representing an [[Order order]] placed, data about the orders is used to create the [[ProductionPlan production plan]]. */ case NewOrderReceived(order: Order) @@ -14,12 +14,12 @@ enum IncomingEvent: */ enum OutgoingEvent: /** - * Events that contains the [[ProductionPlan production plan]] of the day. + * Event that contains the [[ProductionPlan production plan]] of the day. */ case ProductionPlanReady(productionPlan: ProductionPlan) /** - * If an order cannot be fulfilled since there are some products' ripening days takes more - * time than the order required date. + * An event emitted if an [[Order order]] cannot be fulfilled since there are some [[Product products]] whose + * [[RipeningDays ripening days]] would make it impossible to fulfil the order by the required date. */ case OrderDelayed(orderID: OrderID) From 5bd02e0955dc9a8d446a770c02666970afe7dd6f Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 19:19:43 +0200 Subject: [PATCH 152/329] docs: remove useless examples in production planning bc --- .../scala/dev/atedeg/mdm/productionplanning/Types.scala | 7 ------- 1 file changed, 7 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala index 222657ab..b4e0ec8c 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala @@ -22,8 +22,6 @@ final case class ProductToProduce(product: Product, quantity: Quantity) /** * A quantity of something. - * @example `Quantity(-2)` is not a valid quantity. - * @example `Quantity(5)` is a valida quantity. */ final case class Quantity(n: PositiveNumber) derives Plus, Times @@ -34,9 +32,6 @@ type Stock = Product => StockedQuantity /** * A quantity of a stocked [[Product product]], it may also be zero. - * @note it must be a [[NonNegativeNumber non-negative number]]. - * @example `StockedQuantity(0)` is valid. - * @example `StockedQuantity(-1)` is invalid. */ final case class StockedQuantity(n: NonNegativeNumber) @@ -63,7 +58,5 @@ type CheeseTypeRipeningDays = CheeseType => RipeningDays /** * The number of days needed for the ripening process to be done. - * @example `RipeningDays(7)` is a valid number of days. - * @example `RipeningDays(-2)` is not a valid number of days. */ final case class RipeningDays(days: NonNegativeNumber) From 6ff13a68ee4595537d71e875b66c0042416ff2d6 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 19:25:51 +0200 Subject: [PATCH 153/329] refactor: use List.fill in test --- .../scala/dev.atedeg.mdm.productionplanning/Tests.scala | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala index cbd4f146..b297903e 100644 --- a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala @@ -41,11 +41,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Given("a list of orders with deadline in 10 days") val requiredBy = java.time.LocalDate.now.plusDays(10) val orderedProducts = NonEmptyList.of(orderedProd1, orderedProd2, orderedProd3) - val orders = List( - Order(OrderID(UUID.randomUUID()), requiredBy, orderedProducts), - Order(OrderID(UUID.randomUUID()), requiredBy, orderedProducts), - Order(OrderID(UUID.randomUUID()), requiredBy, orderedProducts), - ) + val orders = List.fill(3)(Order(OrderID(UUID.randomUUID), requiredBy, orderedProducts)) And("the production plan of the previous year for the same day") val productsToProduce = NonEmptyList.of(prodToProd1, prodToProd2, prodToProd3) val previousProductionPlan = ProductionPlan(productsToProduce) From b14b551de76f7a01f27da18161f3cd7eee728691 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 20:54:49 +0200 Subject: [PATCH 154/329] feat: add new delivery date for delayed orded in production planning bc --- .../mdm/productionplanning/Actions.scala | 11 ++++++++-- .../mdm/productionplanning/Events.scala | 5 ++++- .../atedeg/mdm/productionplanning/Types.scala | 2 +- .../Tests.scala | 21 +++++++------------ 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index 25462bdb..9894a748 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -40,7 +40,10 @@ private def checkDeliverabilityOfOrder[M[_]: Monad: CanEmit[OrderDelayed]]( )(order: Order): M[Unit] = { val ripeningDays = order.orderedProducts.map(_.product.cheeseType).map(cheeseTypeRipeningDays(_)) val isDelayed = ripeningDays.map(productionInTime(_, order.requiredBy)).exists(_ == OrderStatus.Delayed) - when(isDelayed)(emit(OrderDelayed(order.orderdID): OrderDelayed)) + when(isDelayed) { + val deliveryDate = newDeliveryDate(RipeningDays(ripeningDays.map(_.days).reduceLeft(max))) + emit(OrderDelayed(order.orderdID, deliveryDate): OrderDelayed) + } } private def magicAIProductsToProduceEstimator( @@ -53,11 +56,15 @@ private def magicAIProductsToProduceEstimator( NonEmptyList.of(ProductToProduce(Product.Caciotta(500), Quantity(5))) private def productionInTime(ripeningDays: RipeningDays, requiredBy: LocalDate): OrderStatus = - val today = java.time.LocalDate.now + val today = LocalDate.now if today.plusDays(ripeningDays.days.value.toLong).isBefore(requiredBy) then OrderStatus.NonDelayed else OrderStatus.Delayed +private def newDeliveryDate(ripeningDays: RipeningDays): LocalDate = + val today = LocalDate.now + today.plusDays(ripeningDays.days.value.toLong) + private enum OrderStatus: case Delayed case NonDelayed diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala index d3f07347..384b5fdb 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Events.scala @@ -1,5 +1,7 @@ package dev.atedeg.mdm.productionplanning +import java.time.LocalDate + /** * The events that have to be handled by the bounded context. */ @@ -21,5 +23,6 @@ enum OutgoingEvent: /** * An event emitted if an [[Order order]] cannot be fulfilled since there are some [[Product products]] whose * [[RipeningDays ripening days]] would make it impossible to fulfil the order by the required date. + * A new delivery date is also provided. */ - case OrderDelayed(orderID: OrderID) + case OrderDelayed(orderID: OrderID, newDeliveryDate: LocalDate) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala index b4e0ec8c..98d116f1 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala @@ -59,4 +59,4 @@ type CheeseTypeRipeningDays = CheeseType => RipeningDays /** * The number of days needed for the ripening process to be done. */ -final case class RipeningDays(days: NonNegativeNumber) +final case class RipeningDays(days: NonNegativeNumber) derives Plus diff --git a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala index b297903e..9cd827d1 100644 --- a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala @@ -1,5 +1,6 @@ package dev.atedeg.mdm.productionplanning +import java.time.LocalDate import java.util.UUID import cats.data.{ NonEmptyList, Writer } @@ -39,7 +40,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Feature("Create the production plan for the day") { Scenario("Raffaella wants to create the production plan") { Given("a list of orders with deadline in 10 days") - val requiredBy = java.time.LocalDate.now.plusDays(10) + val requiredBy = LocalDate.now.plusDays(10) val orderedProducts = NonEmptyList.of(orderedProd1, orderedProd2, orderedProd3) val orders = List.fill(3)(Order(OrderID(UUID.randomUUID), requiredBy, orderedProducts)) And("the production plan of the previous year for the same day") @@ -51,22 +52,13 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: val productionPlanCreation: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders) val (events1, events2, productionPlan) = productionPlanCreation.execute - Then( - "the result is the mocked result of a magic AI products to produce estimator: " + - "NonEmptyList.of(ProductToProduce(Product.Caciotta(500), Quantity(5)))", - ) - val todaysProductionPlan: ProductionPlan = ProductionPlan( - NonEmptyList.of( - ProductToProduce(Product.Caciotta(500), Quantity(5)), - ), - ) - productionPlan shouldBe todaysProductionPlan + Then("the result is the same production plan emitted in the event") events1 should not be empty events1.map(_.productionPlan) should contain(productionPlan) events2 shouldBe empty Given("a list of orders with deadline in 6 days (which is less than the ordered Caciotta's ripening time)") - val requiredBy2 = java.time.LocalDate.now.plusDays(6) + val requiredBy2 = LocalDate.now.plusDays(6) val orderedProducts1 = NonEmptyList.of(orderedProd1, orderedProd2, orderedProd3) val orderedProducts2 = NonEmptyList.of(orderedProd2, orderedProd3) val orderedProducts3 = NonEmptyList.of(orderedProd2, orderedProd3) @@ -85,8 +77,9 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders2) val (e1, e2, pp) = productionPlanCreation2.execute Then("Should delay the order containing the Cacciotta product") - pp shouldBe todaysProductionPlan e1 should not be empty - e2 should contain(OrderDelayed(order1ID)) + e1.map(_.productionPlan) should contain(pp) + val newDeliveryDate = LocalDate.now.plusDays(8) + e2 should contain(OrderDelayed(order1ID, newDeliveryDate)) } } From 69136763fcd2ab8001994a5fdebf08d58706c553 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 3 Aug 2022 21:15:10 +0200 Subject: [PATCH 155/329] fix: remove private modifier for production planning action --- .../main/scala/dev/atedeg/mdm/productionplanning/Actions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index 9894a748..8f9e802e 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -22,7 +22,7 @@ import dev.atedeg.mdm.utils.monads.* * If an order cannot be fulfilled since it contains products whose ripening days make it impossible * to satisfy the order by the required date, it emits an [[OrderDelayed order delayed]] event. */ -private def createProductionPlan[M[_]: Monad: Emits[ProductionPlanReady]: CanEmit[OrderDelayed]]( +def createProductionPlan[M[_]: Monad: Emits[ProductionPlanReady]: CanEmit[OrderDelayed]]( stock: Stock, cheeseTypeRipeningDays: CheeseTypeRipeningDays, )( From 7e0e15580be99f3359254bd258924f79945c0f08 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 4 Aug 2022 07:45:36 +0000 Subject: [PATCH 156/329] chore(release): 1.0.0-beta.6 [skip ci] # [1.0.0-beta.6](https://github.com/atedeg/mdm/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-04) ### Bug Fixes * remove private modifier for production planning action ([6913676](https://github.com/atedeg/mdm/commit/69136763fcd2ab8001994a5fdebf08d58706c553)) ### Features * add new delivery date for delayed orded in production planning bc ([b14b551](https://github.com/atedeg/mdm/commit/b14b551de76f7a01f27da18161f3cd7eee728691)) * bc actions definition ([984a7ba](https://github.com/atedeg/mdm/commit/984a7ba6e6813d73e7285e4d6c4af9e756e645b2)) * bc events definition ([1ae213e](https://github.com/atedeg/mdm/commit/1ae213e4af772da429d072941a7844c7838a0a12)) * bc types definition ([fbdcd83](https://github.com/atedeg/mdm/commit/fbdcd837a169d53e414810b74003d6672d0118b4)) * implement bc actions ([86c6e86](https://github.com/atedeg/mdm/commit/86c6e869432d3def47e7ff6f0a3bbbb63e4c3f5f)) --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7f86f01..f8129bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# [1.0.0-beta.6](https://github.com/atedeg/mdm/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-04) + + +### Bug Fixes + +* remove private modifier for production planning action ([6913676](https://github.com/atedeg/mdm/commit/69136763fcd2ab8001994a5fdebf08d58706c553)) + + +### Features + +* add new delivery date for delayed orded in production planning bc ([b14b551](https://github.com/atedeg/mdm/commit/b14b551de76f7a01f27da18161f3cd7eee728691)) +* bc actions definition ([984a7ba](https://github.com/atedeg/mdm/commit/984a7ba6e6813d73e7285e4d6c4af9e756e645b2)) +* bc events definition ([1ae213e](https://github.com/atedeg/mdm/commit/1ae213e4af772da429d072941a7844c7838a0a12)) +* bc types definition ([fbdcd83](https://github.com/atedeg/mdm/commit/fbdcd837a169d53e414810b74003d6672d0118b4)) +* implement bc actions ([86c6e86](https://github.com/atedeg/mdm/commit/86c6e869432d3def47e7ff6f0a3bbbb63e4c3f5f)) + # [1.0.0-beta.5](https://github.com/atedeg/mdm/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-03) From 117e727510155885571b1b9085dc8a06297965e1 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:22:23 +0200 Subject: [PATCH 157/329] build: add client orders subproject --- build.sbt | 8 +- .../atedeg/mdm/clientorders/ActionsTest.scala | 160 ++++++++++++++++++ 2 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala diff --git a/build.sbt b/build.sbt index 377bca0b..15a95c3d 100644 --- a/build.sbt +++ b/build.sbt @@ -126,8 +126,12 @@ lazy val `products-shared-kernel` = project lazy val stocking = project .in(file("stocking")) .settings(commonSettings) - .dependsOn(utils) - .dependsOn(`products-shared-kernel`) + .dependsOn(utils, `products-shared-kernel`) + +lazy val `client-orders` = project + .in(file("client-orders")) + .settings(commonSettings) + .dependsOn(utils, `products-shared-kernel`) lazy val restocking = project .in(file("restocking")) diff --git a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala new file mode 100644 index 00000000..544e0441 --- /dev/null +++ b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala @@ -0,0 +1,160 @@ +package dev.atedeg.mdm.clientorders + +import java.time.LocalDateTime +import java.util.UUID + +import cats.data.{ NonEmptyList, NonEmptyMap, Writer } +import cats.implicits.catsKernelOrderingForOrder +import cats.syntax.all.* +import eu.timepit.refined.predicates.all.NonNegative +import org.scalactic.{ Explicitly, Normalization, Uniformity } +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } +import dev.atedeg.mdm.products.Product.* +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given +import dev.atedeg.mdm.utils.intToNonNegativeNumber +import dev.atedeg.mdm.utils.monads.* + +extension (n: PositiveNumber) + def of(p: Product): IncomingOrderLine = IncomingOrderLine(Quantity(n), p) + def cents: PriceInEuroCents = PriceInEuroCents(n) + +trait CustomerMock: + private val customerId: CustomerID = CustomerID(UUID.randomUUID) + private val customerName: CustomerName = CustomerName("Giovanni Molari") + private val vatNumber: VATNumber = VATNumber(???) + val customer: Customer = Customer(customerId, customerName, vatNumber) + +trait LocationMock: + private val latitude: Latitude = Latitude(-90) + private val longitude: Longitude = Longitude(180) + val location: Location = Location(latitude, longitude) + +trait PriceListMock: + + val priceList: Product => PriceInEuroCents = Map( + Product.Caciotta(1000) -> 100.cents, + Product.Caciotta(500) -> 50.cents, + ) + +trait OrderMocks extends CustomerMock, LocationMock: + private val orderId: OrderID = OrderID(UUID.randomUUID) + + private val orderLines: NonEmptyList[IncomingOrderLine] = NonEmptyList.of[IncomingOrderLine]( + 100 of Product.Caciotta(1000), + 100 of Product.Caciotta(500), + ) + private val date: LocalDateTime = LocalDateTime.now + val incomingOrder: IncomingOrder = IncomingOrder(orderId, orderLines, customer, date, location) + +@SuppressWarnings(Array("org.wartremover.warts.Any")) +class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Matchers with OrderMocks with PriceListMock: + + Feature("Order pricing") { + Scenario("Operator prices an order") { + Given("an incoming order") + When("the order is priced") + val pricedOrder = priceOrder(priceList)(incomingOrder) + Then("the priced is computed correctly") + val expectedPrice = + incomingOrder.orderLines.map(ol => priceList(ol.product).n * ol.quantity.n).reduce(_ + _).cents + pricedOrder.totalPrice shouldBe expectedPrice + } + } + + Feature("Order preparation") { + Scenario("An order is prepared") { + Given("a priced order") + val pricedOrder = priceOrder(priceList)(incomingOrder) + When("the order is marked as in progress") + val inProgressOrder = startPreparingOrder(pricedOrder) + Then("it should not contain any palletized product") + val nonEmptyLines = inProgressOrder.orderLines.filter(ol => + ol match + case _: InProgressOrderLine.Complete => false + case ol: InProgressOrderLine.Incomplete => ol.actual.n === 0, + ) + nonEmptyLines shouldBe empty + } + + Scenario("A product is palletized for an order that does not require it") { + Given("an in-progress order") + val inProgressOrder = startPreparingOrder(priceOrder(priceList)(incomingOrder)) + And("a product that is not requested by the order") + val productNotInOrder = Ricotta(350) + When("the operator tries to palletize it") + val palletizeAction = palletizeProductForOrder(inProgressOrder)(Quantity(10), productNotInOrder) + Then("a ProductNotInOrder error should be raised") + // val result = palletizeAction.execute + ??? + } + + Scenario("A product is palletized in a quantity greater than the required one") { + Given("an in-progress order") + val inProgressOrder = startPreparingOrder(priceOrder(priceList)(incomingOrder)) + And("a product requested by the order") + val productInOrder = Caciotta(500) + When("the operator tries to palletize it in a quantity greater than the required one") + val palletizeAction = palletizeProductForOrder(inProgressOrder)(Quantity(1000), productInOrder) + Then("a PalletizedMoreThanRequired error should be raised") + // val result = palletizeAction.execute + ??? + } + + Scenario("A product is palletized in the exact quantity") { + Given("an in-progress order") + val inProgressOrder = startPreparingOrder(priceOrder(priceList)(incomingOrder)) + And("a product requested by the order") + val productInOrder = Caciotta(500) + When("the operator palletizes it in the exact required quantity") + val palletizeAction = palletizeProductForOrder(inProgressOrder)(Quantity(100), productInOrder) + Then("the corresponding order line is marked as completed") + ??? + } + + Scenario("A product is palletized in a quantity lower than the required one") { + Given("an in-progress order") + val inProgressOrder = startPreparingOrder(priceOrder(priceList)(incomingOrder)) + And("a product requested by the order") + val productInOrder = Caciotta(500) + When("the operator palletizes it in a quantity lower than the required one") + val palletizeAction = palletizeProductForOrder(inProgressOrder)(Quantity(20), productInOrder) + Then("the corresponding order line is updated") + ??? + } + } + + Feature("Order completion") { + Scenario("An incomplete order is completed") { + Given("an incomplete in-progress order") + val inProgressOrder = startPreparingOrder(priceOrder(priceList)(incomingOrder)) + When("one tries to mark it as completed") + val completeAction = completeOrder(inProgressOrder) + Then("an OrderCompletionError is raised") + ??? + } + + Scenario("A complete order is completed") { + Given("a complete in-progress order") + When("one marks it as completed") + Then("it is completed correctly") + } + } + + Feature("Order transportation") { + Scenario("The order weight is computed") { + Given("an order") + When("the weight is computed") + Then("it is the exact sum of the weights of its products") + } + + Scenario("A transport document is printed") { + Given("an order") + When("one requests the transport document") + Then("the correct transport document is generated") + } + } From aabf087b4250a52316f3ec4f63562240e577c8b8 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 18:35:15 +0200 Subject: [PATCH 158/329] docs: add client orders description --- docs/_docs/client-orders.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/_docs/client-orders.md diff --git a/docs/_docs/client-orders.md b/docs/_docs/client-orders.md new file mode 100644 index 00000000..13408cf7 --- /dev/null +++ b/docs/_docs/client-orders.md @@ -0,0 +1,34 @@ +--- +title: Client Orders +--- + +# Client Orders +The system receives all incoming orders from the clients; each incoming order +line specifies a product and the required quantity. Moreover, the incoming order +contains information about the customer, the expected delivery date and the +delivery location. +Finally, each order is identified by a unique ID in order to allow order tracking. + +After an order is received it has to be priced: a priced order has the same structure +of an incoming order but each order line also specifies the total price. +It is computed by multiplying the ordered quantity and the product's price (which is taken +from a price list); in addition, it may also be reduced by specific discounts +offered to the client for that product. The priced order also specifies the +total price which is obtained from the sum of the lines' prices. + +After the order is priced it considered in progress, and it is the operator's job +to palletize the products needed for each order. +The order is considered incomplete until all the required products are palletized. + +When an order is completed by the operator (i.e. all the products are taken from the +stock and palletized) it is weighted; then, the operator prints the order's transport +document and attach it to the pallet which is loaded onto a truck. A notification is sent +to the customer notifying them the order has been shipped. + +# Ubiquitous Language + +# Domain Events + +## Incoming Events + +## Outgoing Events From d27a1491f5a727c813657c498ae3102e3f9b7c1b Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 19:49:05 +0200 Subject: [PATCH 159/329] docs: add tables placeholders --- docs/_docs/client-orders.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/_docs/client-orders.md b/docs/_docs/client-orders.md index 13408cf7..74282ef5 100644 --- a/docs/_docs/client-orders.md +++ b/docs/_docs/client-orders.md @@ -12,8 +12,8 @@ Finally, each order is identified by a unique ID in order to allow order trackin After an order is received it has to be priced: a priced order has the same structure of an incoming order but each order line also specifies the total price. It is computed by multiplying the ordered quantity and the product's price (which is taken -from a price list); in addition, it may also be reduced by specific discounts -offered to the client for that product. The priced order also specifies the +from a price list). +The priced order also specifies the total price which is obtained from the sum of the lines' prices. After the order is priced it considered in progress, and it is the operator's job @@ -27,8 +27,11 @@ to the customer notifying them the order has been shipped. # Ubiquitous Language +{% include client-orders-ul.md %} + # Domain Events ## Incoming Events -## Outgoing Events +{% include client-orders-incoming.md %} + From 0167e8139855e656517ea89422bedad5a6ef228e Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 19:49:50 +0200 Subject: [PATCH 160/329] feat: add client orders types --- .../dev/atedeg/mdm/clientorders/Types.scala | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala new file mode 100644 index 00000000..0122e5dc --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala @@ -0,0 +1,191 @@ +package dev.atedeg.mdm.clientorders + +import java.time.LocalDateTime +import java.util.UUID + +import cats.data.NonEmptyList +import eu.timepit.refined.api.Refined +import eu.timepit.refined.numeric.Interval +import eu.timepit.refined.string.MatchesRegex + +import dev.atedeg.mdm.products.Product +import dev.atedeg.mdm.utils.{ NonNegativeNumber, PositiveDecimal, PositiveNumber } + +/** + * A set of [[IncomingOrderLine order lines]] with their respective [[Quantity quantity]] (e.g. 1000 ricotte of 0.5kg, + * 50 squacqueroni of 1 kg), it also contains data about the [[Customer customer]], an expected + * [[DateTime delivery date]] and the [[Location delivery location]]. + */ +final case class IncomingOrder( + id: OrderID, + orderLines: NonEmptyList[IncomingOrderLine], + customer: Customer, + deliveryDate: LocalDateTime, + deliveryLocation: Location, +) + +/** + * The unique identifier of an order which allows one to track the order during its lifecycle. + */ +final case class OrderID(id: UUID) + +/** + * A [[Product product]] with its ordered [[Quantity quantity]]. + */ +final case class IncomingOrderLine(quantity: Quantity, product: Product) + +/** + * A quantity of something. + */ +final case class Quantity(n: PositiveNumber) + +/** + * A physical or legal entity that places [[IncomingOrder orders]]. + */ +final case class Customer(code: CustomerID, name: CustomerName, vatNumber: VATNumber) + +/** + * An id which uniquely identifies a [[Customer customer]]. + */ +final case class CustomerID(id: UUID) + +/** + * A human readable name used to refer to a [[Customer customer]]. + */ +final case class CustomerName(name: String) + +/** + * An alphanumeric code for value-added tax purposes. + * + * @see + * [[https://en.wikipedia.org/wiki/VAT_identification_number VAT identification number on Wikipedia]] for further + * details. + */ +final case class VATNumber(number: String Refined MatchesRegex[VATRegex]) +private[clientorders] type VATRegex = "[A-Z]{2}[0-9A-Z]{2,13}" + +/** + * The location where an order has to be shipped to. + */ +final case class Location(latitude: Latitude, longitude: Longitude) + +/** + * A latitude specified in degrees. + */ +final case class Latitude(value: Double Refined Interval.Closed[-90, 90]) + +/** + * A longitude specified in degrees. + */ +final case class Longitude(value: Double Refined Interval.Closed[-180, 180]) + +/** + * Associates to each [[Product product]] its [[PriceInEuroCents unitary price]]. + */ +type PriceList = Product => PriceInEuroCents + +/** + * A price expressed in cents, the smallest currency unit for euros. + */ +final case class PriceInEuroCents(n: PositiveNumber) + +/** + * An order where each [[PricedOrderLine line]] has an associated [[PriceInEuroCents price]] and, optionally, an applied + * [[Discount discount]]. It also has the total [[PriceInEuroCents price]]. Its structure resembles the + * [[IncomingOrder incoming order]]'s with the difference that each [[PricedOrderLine line]] has been priced. + */ +final case class PricedOrder( + id: OrderID, + orderLines: NonEmptyList[PricedOrderLine], + customer: Customer, + deliveryDate: LocalDateTime, + deliveryLocation: Location, + totalPrice: PriceInEuroCents, +) + +/** + * A [[Product product]] with its [[Quantity quantity]], a [[PriceInEuroCents price]] and, optionally, a + * [[Discount discount]]. + */ +final case class PricedOrderLine(quantity: Quantity, product: Product, totalPrice: PriceInEuroCents) + +/** + * An order that is being fulfilled by an operator. Its structure resembles the [[PricedOrder priced order]]'s with the + * difference that each [[PricedOrderLine line]] can specify whether it is fulfilled or not. + */ +final case class InProgressOrder( + id: OrderID, + orderLines: NonEmptyList[InProgressOrderLine], + customer: Customer, + deliveryDate: LocalDateTime, + deliveryLocation: Location, + totalPrice: PriceInEuroCents, +) + +/** + * A [[Product product]] with its [[Quantity quantity]] and a [[PriceInEuroCents price]]. + * It may be in two different states: [[InProgressOrderLine.Complete complete]] if the + * [[Product product]] has already been palletized and is ready in the required [[Quantity quantity]]; + * [[InProgressOrderLine.Incomplete incomplete]] if the [[Product product]] is not present in the required + * [[Quantity quantity]]. + */ +enum InProgressOrderLine: + + /** + * A [[InProgressOrderLine line]] of an [[InProgressOrder in-progress order]] where the [[Product product]] is ready + * in the required [[Quantity quantity]] and has been palletized. + */ + case Complete(quantity: Quantity, product: Product, price: PriceInEuroCents) + + /** + * A [[InProgressOrderLine line]] of an [[InProgressOrder in-progress order]] where the [[Product product]] is still + * not available in the required [[Quantity quantity]] but [[PalletizedQuantity a part or none of it]] may have + * already been palletized. + */ + case Incomplete(actual: PalletizedQuantity, required: Quantity, product: Product, price: PriceInEuroCents) + +/** + * A quantity (possibly 0) of a palletized [[Product product]]. + */ +final case class PalletizedQuantity(n: NonNegativeNumber) + +/** + * An order that has been fulfilled by the operator and is ready to be shipped. + */ +final case class CompletedOrder( + id: OrderID, + orderLines: NonEmptyList[CompleteOrderLine], + customer: Customer, + deliveryDate: LocalDateTime, + deliveryLocation: Location, + totalPrice: PriceInEuroCents, +) + +/** + * A [[Product product]] with its [[Quantity quantity]], a [[PriceInEuroCents price]]. + */ +final case class CompleteOrderLine(quantity: Quantity, product: Product, price: PriceInEuroCents) + +/** + * A document that has to specify: a [[Location delivery location]], a [[Location shipping location]], the + * [[Customer customer]]'s info, the [[DateTime shipping date]], the total [[WeightInKilograms weight]] of the pallet, + * and a list of [[TransportDocumentLine transport document lines]]. + */ +final case class TransportDocument( + deliveryLocation: Location, + shippingLocation: Location, + customer: Customer, + shippingDate: LocalDateTime, + transportDocumentLines: NonEmptyList[TransportDocumentLine], + totalWeight: WeightInKilograms, +) + +/** + * A [[Product product]] with its respective shipped [[Quantity quantity]]. + */ +final case class TransportDocumentLine(quantity: Quantity, product: Product) + +/** + * A weight expressed in kilograms. + */ +final case class WeightInKilograms(n: PositiveDecimal) From fd66d8422201db21d56a1675a44ab25d1eef8473 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 19:50:03 +0200 Subject: [PATCH 161/329] feat: add client orders events --- .../dev/atedeg/mdm/clientorders/Events.scala | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala new file mode 100644 index 00000000..f6725671 --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala @@ -0,0 +1,24 @@ +package dev.atedeg.mdm.clientorders + +import dev.atedeg.mdm.products.Product + +/** + * The events that have to be handled by the bounded context. + */ +enum IncomingEvent: + /** + * An [[IncomingEvent event]] which is received when an [[order.IncomingOrder order]] is made. + */ + case OrderReceived() + + /** + * An [[IncomingEvent event]] received when an operator takes a [[Product product]] from the stock and palletizes it + * for the given [[Order.InProgressOrder order]]. + */ + case ProductPalletizedForOrder(orderID: OrderID, quantity: Quantity, product: Product) + + /** + * An [[IncomingEvent event]] received when an [[order.InProgressOrder order in progress]] is marked as + * [[order.CompletedOrder ready to be shipped]]. + */ + case OrderCompleted(orderID: OrderID) From 7b0eb5464032dd5f91f43ee7d5a1bc8c2f3b6f69 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 2 Aug 2022 19:50:17 +0200 Subject: [PATCH 162/329] feat: add client orders errors --- .../dev/atedeg/mdm/clientorders/Errors.scala | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala new file mode 100644 index 00000000..41715c6b --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala @@ -0,0 +1,28 @@ +package dev.atedeg.mdm.clientorders + +import dev.atedeg.mdm.products.Product + +/** + * An error that may be produced by the [[palletizeProductForOrder palletization action]]. + */ +enum PalletizationError: + /** + * Raised when trying to palletize a [[Product product]] for an [[Order.InProgressOrder order]] that does not require + * it. + */ + case ProductNotInOrder() + + /** + * Raised when trying to palletize more of a [[Product product]] than required. + */ + case PalletizedMoreThanRequired(requiredQuantity: Quantity) + +/** + * An error that may be produced by the [[completeOrder() order completion action]]. + */ +enum OrderCompletionError: + /** + * Raised when trying to mark an [[Order.InProgressOrder order]] as [[Order.CompletedOrder completed]] when any of its + * [[Order.InProgressOrderLine lines]] are not completed. + */ + case OrderNotComplete() From a0f624037198bbff0fd3102936122e6165aba943 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 3 Aug 2022 16:38:17 +0200 Subject: [PATCH 163/329] feat: implement client-order actions Co-authored-by: ndido98 --- .../dev/atedeg/mdm/clientorders/Actions.scala | 170 ++++++++++++++++++ .../dev/atedeg/mdm/clientorders/Types.scala | 14 +- .../mdm/clientorders/utils/TypesOps.scala | 20 +++ 3 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala new file mode 100644 index 00000000..65d065ee --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -0,0 +1,170 @@ +package dev.atedeg.mdm.clientorders + +import java.time.LocalDateTime + +import cats.Monad +import cats.data.NonEmptyList +import cats.syntax.all.* + +import dev.atedeg.mdm.clientorders.InProgressOrderLine.* +import dev.atedeg.mdm.clientorders.OrderCompletionError.* +import dev.atedeg.mdm.clientorders.PalletizationError.* +import dev.atedeg.mdm.clientorders.utils.* +import dev.atedeg.mdm.clientorders.utils.QuantityOps.* +import dev.atedeg.mdm.clientorders.utils.QuantityOps.given +import dev.atedeg.mdm.products.Product +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given +import dev.atedeg.mdm.utils.monads.* + +/** + * Turns an [[IncomingOrder incoming order]] into a [[PricedOrder priced order]] by computing the price + * of each [[IncomingOrderLine line]] using a [[PriceList price list]]. + */ +def priceOrder(priceList: PriceList)(incomingOrder: IncomingOrder): PricedOrder = + val pricedOrderLines = incomingOrder.orderLines.map { case iol @ IncomingOrderLine(quantity, product) => + val price = priceList(product).n * quantity.n + PricedOrderLine(quantity, product, price.euroCents) + } + val totalPrice = pricedOrderLines.map(_.totalPrice).reduce(_ + _) + PricedOrder( + incomingOrder.id, + pricedOrderLines, + incomingOrder.customer, + incomingOrder.deliveryDate, + incomingOrder.deliveryLocation, + totalPrice, + ) + +/** + * Turns a [[order.PricedOrder priced order]] into an [[order.InProgressOrder in-progress order]] that can then be + * fulfilled by operators. + * + * @param pricedOrder + * the priced order to be marked as in progress. + */ +def startPreparingOrder(pricedOrder: PricedOrder): InProgressOrder = + val PricedOrder(id, ol, customer, deliveryDate, deliveryLocation, totalPrice) = pricedOrder + val newOrderLine = ol.map { case PricedOrderLine(quantity, product, price) => + Incomplete(0.palletizedQuantity, quantity, product, price) + } + InProgressOrder(id, newOrderLine, customer, deliveryDate, deliveryLocation, totalPrice) + +/** + * Palletizes a [[Product product]] in the specified [[order.Quantity quantity]]. + * + * @note + * It can raise a [[PalletizationError palletization error]]. + * @param inProgressOrder + * the order for which the product needs to be palletized. + * @param quantity + * the quantity of product to be palletized. + * @param product + * the product to be palletized. + * @return + * an [[order.InProgressOrder in-progress order]] where the corresponding [[order.InProgressOrderLine line]] has been + * updated with the [[order.Quantity specified quantity]]. + */ +def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad]( + inProgressOrder: InProgressOrder, +)(quantity: Quantity, product: Product): M[InProgressOrder] = + val InProgressOrder(id, ol, customer, dd, dl, totalPrice) = inProgressOrder + for { + orderLine <- findOrderLine(product, ol).ifMissingRaise(ProductNotInOrder()) + requiredQuantity = getRequiredQuantity(orderLine) + totalPriceForProduct = getTotalPriceForProduct(orderLine) + _ <- (quantity <= requiredQuantity).otherwiseRaise(PalletizedMoreThanRequired(requiredQuantity)) + updatedLine = updateLine(product, quantity, requiredQuantity, totalPriceForProduct) + newOrderLine = ol.map { + case i @ Incomplete(_, _, `product`, _) => updatedLine + case l @ _ => l + } + } yield InProgressOrder(id, newOrderLine, customer, dd, dl, totalPrice) + +private def isProductInOrder(orderLines: NonEmptyList[InProgressOrderLine], product: Product): Boolean = + orderLines.map { + case Complete(_, prod, _) => prod + case Incomplete(_, _, prod, _) => prod + }.exists(_ == product) + +private def findOrderLine(product: Product, ol: NonEmptyList[InProgressOrderLine]): Option[InProgressOrderLine] = + ol.find(_ == product) + +private def getRequiredQuantity(orderLine: InProgressOrderLine): Quantity = orderLine match { + case Complete(quantity, _, _) => quantity + case Incomplete(_, requiredQuantity, _, _) => requiredQuantity +} + +private def getTotalPriceForProduct(orderLine: InProgressOrderLine): PriceInEuroCents = orderLine match { + case Complete(_, _, price) => price + case Incomplete(_, _, _, price) => price +} + +private def updateLine( + product: Product, + quantity: Quantity, + requiredQuantity: Quantity, + totalPrice: PriceInEuroCents, +): InProgressOrderLine = + if quantity == requiredQuantity then Complete(quantity, product, totalPrice) + else Incomplete(quantity.toPalletizedQuantity, requiredQuantity, product, totalPrice) + +/** + * Completes an [[order.InProgressOrder in-progress order]]. + * + * @note + * It can raise an [[OrderCompletionError order completion error]]. + * @param inProgressOrder + * the in-progress order to be marked as complete. + */ +def completeOrder[Result[_]: CanRaise[OrderCompletionError]: Monad]( + inProgressOrder: InProgressOrder, +): Result[CompletedOrder] = + val InProgressOrder(id, ol, customer, dd, dl, totalPrice) = inProgressOrder + for { + completedOrderLines <- getCompletedOrderLines(ol).ifMissingRaise(OrderNotComplete()) + completeOrderLines = ol.map(o => CompleteOrderLine(o.quantity, o.product, o.price)) + } yield CompletedOrder(id, completeOrderLines, customer, dd, dl, totalPrice) + +private def getCompletedOrderLines(orderLines: NonEmptyList[InProgressOrderLine]): Option[NonEmptyList[Complete]] = + def prova(acc: Option[List[Complete]])(l: List[InProgressOrderLine]): Option[List[Complete]] = l match + case (c @ _: Complete) :: tail => prova(acc.map(c :: _))(tail) + case (_: Incomplete) :: _ => None + case Nil => acc + + prova(Some(Nil))(orderLines.toList).flatMap(_.toNel).map(_.reverse) + +/** + * Computes the total [[order.WeightInKilograms weight]] of a [[order.CompletedOrder complete order]]. + * + * @param completeOrder + * the order whose weight has to be computed. + */ +def weightOrder(completeOrder: CompletedOrder): WeightInKilograms = + completeOrder.orderLines + .map(r => (r.product, r.quantity)) + .map { case (product, quantity) => (product.weight.n * quantity.n).toDecimal / 1_000 } + .map(WeightInKilograms(_)) + .reduce(_ + _) + +/** + * Creates a [[order.TransportDocument transport document]] from a [[order.CompletedOrder complete order]]. + * + * @param completeOrder + * the order for which the transport document has to be created. + * @param weight + * the weight of the order. + */ +def createTransportDocument(completeOrder: CompletedOrder, weight: WeightInKilograms): TransportDocument = + val CompletedOrder(_, orderLines, customer, _, deliveryLocation, _) = completeOrder + val transportDocumentLines = orderLines.map(l => TransportDocumentLine(l.quantity, l.product)) + TransportDocument( + deliveryLocation, + mambelliDeliveryLocation, + customer, + LocalDateTime.now(), + transportDocumentLines, + weight, + ) + +private val mambelliDeliveryLocation = Location(Latitude(12), Longitude(44)) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala index 0122e5dc..56f98802 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala @@ -9,7 +9,8 @@ import eu.timepit.refined.numeric.Interval import eu.timepit.refined.string.MatchesRegex import dev.atedeg.mdm.products.Product -import dev.atedeg.mdm.utils.{ NonNegativeNumber, PositiveDecimal, PositiveNumber } +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given /** * A set of [[IncomingOrderLine order lines]] with their respective [[Quantity quantity]] (e.g. 1000 ricotte of 0.5kg, @@ -72,12 +73,12 @@ final case class Location(latitude: Latitude, longitude: Longitude) /** * A latitude specified in degrees. */ -final case class Latitude(value: Double Refined Interval.Closed[-90, 90]) +final case class Latitude(value: NumberInClosedRange[-90, 90]) /** * A longitude specified in degrees. */ -final case class Longitude(value: Double Refined Interval.Closed[-180, 180]) +final case class Longitude(value: NumberInClosedRange[-180, 180]) /** * Associates to each [[Product product]] its [[PriceInEuroCents unitary price]]. @@ -87,7 +88,7 @@ type PriceList = Product => PriceInEuroCents /** * A price expressed in cents, the smallest currency unit for euros. */ -final case class PriceInEuroCents(n: PositiveNumber) +final case class PriceInEuroCents(n: PositiveNumber) derives Plus /** * An order where each [[PricedOrderLine line]] has an associated [[PriceInEuroCents price]] and, optionally, an applied @@ -104,8 +105,7 @@ final case class PricedOrder( ) /** - * A [[Product product]] with its [[Quantity quantity]], a [[PriceInEuroCents price]] and, optionally, a - * [[Discount discount]]. + * A [[Product product]] with its [[Quantity quantity]] and a [[PriceInEuroCents price]]. */ final case class PricedOrderLine(quantity: Quantity, product: Product, totalPrice: PriceInEuroCents) @@ -188,4 +188,4 @@ final case class TransportDocumentLine(quantity: Quantity, product: Product) /** * A weight expressed in kilograms. */ -final case class WeightInKilograms(n: PositiveDecimal) +final case class WeightInKilograms(n: PositiveDecimal) derives Plus diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala new file mode 100644 index 00000000..938c52c9 --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala @@ -0,0 +1,20 @@ +package dev.atedeg.mdm.clientorders.utils + +import cats.kernel.Order +import eu.timepit.refined.auto.autoUnwrap + +import dev.atedeg.mdm.clientorders.PalletizedQuantity +import dev.atedeg.mdm.clientorders.PalletizedQuantity.apply +import dev.atedeg.mdm.clientorders.PriceInEuroCents +import dev.atedeg.mdm.clientorders.Quantity +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given + +extension (n: PositiveNumber) def euroCents: PriceInEuroCents = PriceInEuroCents(n) +extension (i: NonNegativeNumber) def palletizedQuantity: PalletizedQuantity = PalletizedQuantity(i) + +object QuantityOps: + extension (q: Quantity) def toPalletizedQuantity: PalletizedQuantity = PalletizedQuantity(q.n) + + given Order[Quantity] with + override def compare(x: Quantity, y: Quantity): Int = Order[Int].compare(x.n, y.n) From c414592beec21923279842d906ec032134ba287f Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 3 Aug 2022 17:53:40 +0200 Subject: [PATCH 164/329] build: remove utils and production shared kernel from aggregate --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 15a95c3d..ded2677b 100644 --- a/build.sbt +++ b/build.sbt @@ -90,12 +90,12 @@ lazy val root = project ) .aggregate( stocking, - utils, production, `milk-planning`, `production-planning`, `products-shared-kernel`, restocking, + `client-orders`, ) lazy val utils = project From f1f9c530ed2be46b117616c0a8a118e4dff6a17c Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 3 Aug 2022 17:54:58 +0200 Subject: [PATCH 165/329] refactor: refactor some methods Co-Authored-by: ndido98 --- .../dev/atedeg/mdm/clientorders/Actions.scala | 64 ++++++++----------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 65d065ee..2b6b2624 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -1,9 +1,11 @@ package dev.atedeg.mdm.clientorders import java.time.LocalDateTime +import scala.annotation.tailrec import cats.Monad import cats.data.NonEmptyList +import cats.kernel.Comparison.* import cats.syntax.all.* import dev.atedeg.mdm.clientorders.InProgressOrderLine.* @@ -53,8 +55,6 @@ def startPreparingOrder(pricedOrder: PricedOrder): InProgressOrder = /** * Palletizes a [[Product product]] in the specified [[order.Quantity quantity]]. * - * @note - * It can raise a [[PalletizationError palletization error]]. * @param inProgressOrder * the order for which the product needs to be palletized. * @param quantity @@ -70,16 +70,13 @@ def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad]( )(quantity: Quantity, product: Product): M[InProgressOrder] = val InProgressOrder(id, ol, customer, dd, dl, totalPrice) = inProgressOrder for { - orderLine <- findOrderLine(product, ol).ifMissingRaise(ProductNotInOrder()) - requiredQuantity = getRequiredQuantity(orderLine) - totalPriceForProduct = getTotalPriceForProduct(orderLine) - _ <- (quantity <= requiredQuantity).otherwiseRaise(PalletizedMoreThanRequired(requiredQuantity)) - updatedLine = updateLine(product, quantity, requiredQuantity, totalPriceForProduct) - newOrderLine = ol.map { + orderLine <- ol.find(_ == product).ifMissingRaise(ProductNotInOrder()) + updatedLine <- addToLine(orderLine)(quantity) + newOrderLines = ol.map { case i @ Incomplete(_, _, `product`, _) => updatedLine case l @ _ => l } - } yield InProgressOrder(id, newOrderLine, customer, dd, dl, totalPrice) + } yield InProgressOrder(id, newOrderLines, customer, dd, dl, totalPrice) private def isProductInOrder(orderLines: NonEmptyList[InProgressOrderLine], product: Product): Boolean = orderLines.map { @@ -87,33 +84,20 @@ private def isProductInOrder(orderLines: NonEmptyList[InProgressOrderLine], prod case Incomplete(_, _, prod, _) => prod }.exists(_ == product) -private def findOrderLine(product: Product, ol: NonEmptyList[InProgressOrderLine]): Option[InProgressOrderLine] = - ol.find(_ == product) - -private def getRequiredQuantity(orderLine: InProgressOrderLine): Quantity = orderLine match { - case Complete(quantity, _, _) => quantity - case Incomplete(_, requiredQuantity, _, _) => requiredQuantity -} - -private def getTotalPriceForProduct(orderLine: InProgressOrderLine): PriceInEuroCents = orderLine match { - case Complete(_, _, price) => price - case Incomplete(_, _, _, price) => price -} - -private def updateLine( - product: Product, - quantity: Quantity, - requiredQuantity: Quantity, - totalPrice: PriceInEuroCents, -): InProgressOrderLine = - if quantity == requiredQuantity then Complete(quantity, product, totalPrice) - else Incomplete(quantity.toPalletizedQuantity, requiredQuantity, product, totalPrice) +private def addToLine[M[_]: Monad: CanRaise[PalletizedMoreThanRequired]](ol: InProgressOrderLine)( + quantityToAdd: Quantity, +): M[InProgressOrderLine] = ol match + case _: Complete => raise(PalletizedMoreThanRequired(0.missingQuantity): PalletizedMoreThanRequired) + case Incomplete(palletized, required, product, price) => + val missingQuantity = (required.n.toNonNegative - palletized.n).missingQuantity + missingQuantity.n.comparison(quantityToAdd.n.toNonNegative) match + case GreaterThan => Incomplete(palletized + quantityToAdd.toPalletizedQuantity, required, product, price).pure + case EqualTo => Complete(required, product, price).pure + case LessThan => raise(PalletizedMoreThanRequired(missingQuantity): PalletizedMoreThanRequired) /** * Completes an [[order.InProgressOrder in-progress order]]. * - * @note - * It can raise an [[OrderCompletionError order completion error]]. * @param inProgressOrder * the in-progress order to be marked as complete. */ @@ -123,16 +107,18 @@ def completeOrder[Result[_]: CanRaise[OrderCompletionError]: Monad]( val InProgressOrder(id, ol, customer, dd, dl, totalPrice) = inProgressOrder for { completedOrderLines <- getCompletedOrderLines(ol).ifMissingRaise(OrderNotComplete()) - completeOrderLines = ol.map(o => CompleteOrderLine(o.quantity, o.product, o.price)) + completeOrderLines = completedOrderLines.map(o => CompleteOrderLine(o.quantity, o.product, o.price)) } yield CompletedOrder(id, completeOrderLines, customer, dd, dl, totalPrice) private def getCompletedOrderLines(orderLines: NonEmptyList[InProgressOrderLine]): Option[NonEmptyList[Complete]] = - def prova(acc: Option[List[Complete]])(l: List[InProgressOrderLine]): Option[List[Complete]] = l match - case (c @ _: Complete) :: tail => prova(acc.map(c :: _))(tail) - case (_: Incomplete) :: _ => None - case Nil => acc - - prova(Some(Nil))(orderLines.toList).flatMap(_.toNel).map(_.reverse) + @tailrec + def _getCompletedOrderLines(acc: Option[List[Complete]])(l: List[InProgressOrderLine]): Option[List[Complete]] = + l match + case (c @ _: Complete) :: tail => _getCompletedOrderLines(acc.map(c :: _))(tail) + case (_: Incomplete) :: _ => None + case Nil => acc + + _getCompletedOrderLines(Some(Nil))(orderLines.toList).flatMap(_.toNel).map(_.reverse) /** * Computes the total [[order.WeightInKilograms weight]] of a [[order.CompletedOrder complete order]]. From 8fa1f2d10f3c140eb0f98234793708073fb24a7b Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 3 Aug 2022 17:55:57 +0200 Subject: [PATCH 166/329] fix: replace quantity with missing quantity Co-Authored-By: ndido98 --- .../src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala index 41715c6b..8c599765 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala @@ -15,7 +15,7 @@ enum PalletizationError: /** * Raised when trying to palletize more of a [[Product product]] than required. */ - case PalletizedMoreThanRequired(requiredQuantity: Quantity) + case PalletizedMoreThanRequired(requiredQuantity: MissingQuantity) /** * An error that may be produced by the [[completeOrder() order completion action]]. From e7a23f72e9c4babdcd89e9688fa5539cc59e8e53 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 3 Aug 2022 17:56:53 +0200 Subject: [PATCH 167/329] feat: add MissingQuantity class Co-Authored-By: ndido98 --- .../src/main/scala/dev/atedeg/mdm/clientorders/Types.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala index 56f98802..5af5fd1c 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala @@ -40,6 +40,11 @@ final case class IncomingOrderLine(quantity: Quantity, product: Product) */ final case class Quantity(n: PositiveNumber) +/** + * The missing quantity of a [[Product product]] necessary to fullfil an [[InProgressOrderLine in-progress order line]]. + */ +final case class MissingQuantity(n: NonNegativeNumber) + /** * A physical or legal entity that places [[IncomingOrder orders]]. */ @@ -147,7 +152,7 @@ enum InProgressOrderLine: /** * A quantity (possibly 0) of a palletized [[Product product]]. */ -final case class PalletizedQuantity(n: NonNegativeNumber) +final case class PalletizedQuantity(n: NonNegativeNumber) derives Plus /** * An order that has been fulfilled by the operator and is ready to be shipped. From c3cc0e04c1087c380a99f66a5bae97b425e2c29d Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 3 Aug 2022 17:57:46 +0200 Subject: [PATCH 168/329] chore: add some utility extension methods Co-Authored-By: ndido98 --- .../mdm/clientorders/utils/TypesOps.scala | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala index 938c52c9..0190fa97 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala @@ -3,18 +3,24 @@ package dev.atedeg.mdm.clientorders.utils import cats.kernel.Order import eu.timepit.refined.auto.autoUnwrap -import dev.atedeg.mdm.clientorders.PalletizedQuantity -import dev.atedeg.mdm.clientorders.PalletizedQuantity.apply -import dev.atedeg.mdm.clientorders.PriceInEuroCents -import dev.atedeg.mdm.clientorders.Quantity +import dev.atedeg.mdm.clientorders.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given -extension (n: PositiveNumber) def euroCents: PriceInEuroCents = PriceInEuroCents(n) -extension (i: NonNegativeNumber) def palletizedQuantity: PalletizedQuantity = PalletizedQuantity(i) +extension (n: PositiveNumber) + def euroCents: PriceInEuroCents = PriceInEuroCents(n) + def quantity: Quantity = Quantity(n) + +extension (i: NonNegativeNumber) + def palletizedQuantity: PalletizedQuantity = PalletizedQuantity(i) + def missingQuantity: MissingQuantity = MissingQuantity(i) object QuantityOps: - extension (q: Quantity) def toPalletizedQuantity: PalletizedQuantity = PalletizedQuantity(q.n) + + extension (q: Quantity) + def toPalletizedQuantity: PalletizedQuantity = PalletizedQuantity(q.n) + def toMissingQuantity: MissingQuantity = MissingQuantity(q.n) given Order[Quantity] with override def compare(x: Quantity, y: Quantity): Int = Order[Int].compare(x.n, y.n) + From 6b43f2d9ab78d84487b3de2f0ae9d8864586cae5 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 3 Aug 2022 17:58:15 +0200 Subject: [PATCH 169/329] docs: fix paragrph size --- docs/_docs/client-orders.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/_docs/client-orders.md b/docs/_docs/client-orders.md index 74282ef5..d0d05c83 100644 --- a/docs/_docs/client-orders.md +++ b/docs/_docs/client-orders.md @@ -3,35 +3,35 @@ title: Client Orders --- # Client Orders -The system receives all incoming orders from the clients; each incoming order -line specifies a product and the required quantity. Moreover, the incoming order + +The system receives all incoming orders from the clients; each incoming order +line specifies a product and the required quantity. Moreover, the incoming order contains information about the customer, the expected delivery date and the -delivery location. +delivery location. Finally, each order is identified by a unique ID in order to allow order tracking. After an order is received it has to be priced: a priced order has the same structure -of an incoming order but each order line also specifies the total price. -It is computed by multiplying the ordered quantity and the product's price (which is taken +of an incoming order but each order line also specifies the total price. +It is computed by multiplying the ordered quantity and the product's price (which is taken from a price list). -The priced order also specifies the +The priced order also specifies the total price which is obtained from the sum of the lines' prices. After the order is priced it considered in progress, and it is the operator's job to palletize the products needed for each order. The order is considered incomplete until all the required products are palletized. -When an order is completed by the operator (i.e. all the products are taken from the +When an order is completed by the operator (i.e. all the products are taken from the stock and palletized) it is weighted; then, the operator prints the order's transport -document and attach it to the pallet which is loaded onto a truck. A notification is sent +document and attach it to the pallet which is loaded onto a truck. A notification is sent to the customer notifying them the order has been shipped. -# Ubiquitous Language +## Ubiquitous Language {% include client-orders-ul.md %} -# Domain Events +## Domain Events -## Incoming Events +### Incoming Events {% include client-orders-incoming.md %} - From a7ef5dff26587c4bc97778ecdf9e4a3d3c0b63b9 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 3 Aug 2022 18:06:59 +0200 Subject: [PATCH 170/329] style: reformat with scalafmt --- .../dev/atedeg/mdm/clientorders/Errors.scala | 56 +-- .../dev/atedeg/mdm/clientorders/Events.scala | 48 +-- .../dev/atedeg/mdm/clientorders/Types.scala | 392 +++++++++--------- .../mdm/clientorders/utils/TypesOps.scala | 1 - 4 files changed, 248 insertions(+), 249 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala index 8c599765..08974551 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala @@ -1,28 +1,28 @@ -package dev.atedeg.mdm.clientorders - -import dev.atedeg.mdm.products.Product - -/** - * An error that may be produced by the [[palletizeProductForOrder palletization action]]. - */ -enum PalletizationError: - /** - * Raised when trying to palletize a [[Product product]] for an [[Order.InProgressOrder order]] that does not require - * it. - */ - case ProductNotInOrder() - - /** - * Raised when trying to palletize more of a [[Product product]] than required. - */ - case PalletizedMoreThanRequired(requiredQuantity: MissingQuantity) - -/** - * An error that may be produced by the [[completeOrder() order completion action]]. - */ -enum OrderCompletionError: - /** - * Raised when trying to mark an [[Order.InProgressOrder order]] as [[Order.CompletedOrder completed]] when any of its - * [[Order.InProgressOrderLine lines]] are not completed. - */ - case OrderNotComplete() +package dev.atedeg.mdm.clientorders + +import dev.atedeg.mdm.products.Product + +/** + * An error that may be produced by the [[palletizeProductForOrder palletization action]]. + */ +enum PalletizationError: + /** + * Raised when trying to palletize a [[Product product]] for an [[Order.InProgressOrder order]] that does not require + * it. + */ + case ProductNotInOrder() + + /** + * Raised when trying to palletize more of a [[Product product]] than required. + */ + case PalletizedMoreThanRequired(requiredQuantity: MissingQuantity) + +/** + * An error that may be produced by the [[completeOrder() order completion action]]. + */ +enum OrderCompletionError: + /** + * Raised when trying to mark an [[Order.InProgressOrder order]] as [[Order.CompletedOrder completed]] when any of its + * [[Order.InProgressOrderLine lines]] are not completed. + */ + case OrderNotComplete() diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala index f6725671..5d021164 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala @@ -1,24 +1,24 @@ -package dev.atedeg.mdm.clientorders - -import dev.atedeg.mdm.products.Product - -/** - * The events that have to be handled by the bounded context. - */ -enum IncomingEvent: - /** - * An [[IncomingEvent event]] which is received when an [[order.IncomingOrder order]] is made. - */ - case OrderReceived() - - /** - * An [[IncomingEvent event]] received when an operator takes a [[Product product]] from the stock and palletizes it - * for the given [[Order.InProgressOrder order]]. - */ - case ProductPalletizedForOrder(orderID: OrderID, quantity: Quantity, product: Product) - - /** - * An [[IncomingEvent event]] received when an [[order.InProgressOrder order in progress]] is marked as - * [[order.CompletedOrder ready to be shipped]]. - */ - case OrderCompleted(orderID: OrderID) +package dev.atedeg.mdm.clientorders + +import dev.atedeg.mdm.products.Product + +/** + * The events that have to be handled by the bounded context. + */ +enum IncomingEvent: + /** + * An [[IncomingEvent event]] which is received when an [[order.IncomingOrder order]] is made. + */ + case OrderReceived() + + /** + * An [[IncomingEvent event]] received when an operator takes a [[Product product]] from the stock and palletizes it + * for the given [[Order.InProgressOrder order]]. + */ + case ProductPalletizedForOrder(orderID: OrderID, quantity: Quantity, product: Product) + + /** + * An [[IncomingEvent event]] received when an [[order.InProgressOrder order in progress]] is marked as + * [[order.CompletedOrder ready to be shipped]]. + */ + case OrderCompleted(orderID: OrderID) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala index 5af5fd1c..d22dcd65 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala @@ -1,196 +1,196 @@ -package dev.atedeg.mdm.clientorders - -import java.time.LocalDateTime -import java.util.UUID - -import cats.data.NonEmptyList -import eu.timepit.refined.api.Refined -import eu.timepit.refined.numeric.Interval -import eu.timepit.refined.string.MatchesRegex - -import dev.atedeg.mdm.products.Product -import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.given - -/** - * A set of [[IncomingOrderLine order lines]] with their respective [[Quantity quantity]] (e.g. 1000 ricotte of 0.5kg, - * 50 squacqueroni of 1 kg), it also contains data about the [[Customer customer]], an expected - * [[DateTime delivery date]] and the [[Location delivery location]]. - */ -final case class IncomingOrder( - id: OrderID, - orderLines: NonEmptyList[IncomingOrderLine], - customer: Customer, - deliveryDate: LocalDateTime, - deliveryLocation: Location, -) - -/** - * The unique identifier of an order which allows one to track the order during its lifecycle. - */ -final case class OrderID(id: UUID) - -/** - * A [[Product product]] with its ordered [[Quantity quantity]]. - */ -final case class IncomingOrderLine(quantity: Quantity, product: Product) - -/** - * A quantity of something. - */ -final case class Quantity(n: PositiveNumber) - -/** - * The missing quantity of a [[Product product]] necessary to fullfil an [[InProgressOrderLine in-progress order line]]. - */ -final case class MissingQuantity(n: NonNegativeNumber) - -/** - * A physical or legal entity that places [[IncomingOrder orders]]. - */ -final case class Customer(code: CustomerID, name: CustomerName, vatNumber: VATNumber) - -/** - * An id which uniquely identifies a [[Customer customer]]. - */ -final case class CustomerID(id: UUID) - -/** - * A human readable name used to refer to a [[Customer customer]]. - */ -final case class CustomerName(name: String) - -/** - * An alphanumeric code for value-added tax purposes. - * - * @see - * [[https://en.wikipedia.org/wiki/VAT_identification_number VAT identification number on Wikipedia]] for further - * details. - */ -final case class VATNumber(number: String Refined MatchesRegex[VATRegex]) -private[clientorders] type VATRegex = "[A-Z]{2}[0-9A-Z]{2,13}" - -/** - * The location where an order has to be shipped to. - */ -final case class Location(latitude: Latitude, longitude: Longitude) - -/** - * A latitude specified in degrees. - */ -final case class Latitude(value: NumberInClosedRange[-90, 90]) - -/** - * A longitude specified in degrees. - */ -final case class Longitude(value: NumberInClosedRange[-180, 180]) - -/** - * Associates to each [[Product product]] its [[PriceInEuroCents unitary price]]. - */ -type PriceList = Product => PriceInEuroCents - -/** - * A price expressed in cents, the smallest currency unit for euros. - */ -final case class PriceInEuroCents(n: PositiveNumber) derives Plus - -/** - * An order where each [[PricedOrderLine line]] has an associated [[PriceInEuroCents price]] and, optionally, an applied - * [[Discount discount]]. It also has the total [[PriceInEuroCents price]]. Its structure resembles the - * [[IncomingOrder incoming order]]'s with the difference that each [[PricedOrderLine line]] has been priced. - */ -final case class PricedOrder( - id: OrderID, - orderLines: NonEmptyList[PricedOrderLine], - customer: Customer, - deliveryDate: LocalDateTime, - deliveryLocation: Location, - totalPrice: PriceInEuroCents, -) - -/** - * A [[Product product]] with its [[Quantity quantity]] and a [[PriceInEuroCents price]]. - */ -final case class PricedOrderLine(quantity: Quantity, product: Product, totalPrice: PriceInEuroCents) - -/** - * An order that is being fulfilled by an operator. Its structure resembles the [[PricedOrder priced order]]'s with the - * difference that each [[PricedOrderLine line]] can specify whether it is fulfilled or not. - */ -final case class InProgressOrder( - id: OrderID, - orderLines: NonEmptyList[InProgressOrderLine], - customer: Customer, - deliveryDate: LocalDateTime, - deliveryLocation: Location, - totalPrice: PriceInEuroCents, -) - -/** - * A [[Product product]] with its [[Quantity quantity]] and a [[PriceInEuroCents price]]. - * It may be in two different states: [[InProgressOrderLine.Complete complete]] if the - * [[Product product]] has already been palletized and is ready in the required [[Quantity quantity]]; - * [[InProgressOrderLine.Incomplete incomplete]] if the [[Product product]] is not present in the required - * [[Quantity quantity]]. - */ -enum InProgressOrderLine: - - /** - * A [[InProgressOrderLine line]] of an [[InProgressOrder in-progress order]] where the [[Product product]] is ready - * in the required [[Quantity quantity]] and has been palletized. - */ - case Complete(quantity: Quantity, product: Product, price: PriceInEuroCents) - - /** - * A [[InProgressOrderLine line]] of an [[InProgressOrder in-progress order]] where the [[Product product]] is still - * not available in the required [[Quantity quantity]] but [[PalletizedQuantity a part or none of it]] may have - * already been palletized. - */ - case Incomplete(actual: PalletizedQuantity, required: Quantity, product: Product, price: PriceInEuroCents) - -/** - * A quantity (possibly 0) of a palletized [[Product product]]. - */ -final case class PalletizedQuantity(n: NonNegativeNumber) derives Plus - -/** - * An order that has been fulfilled by the operator and is ready to be shipped. - */ -final case class CompletedOrder( - id: OrderID, - orderLines: NonEmptyList[CompleteOrderLine], - customer: Customer, - deliveryDate: LocalDateTime, - deliveryLocation: Location, - totalPrice: PriceInEuroCents, -) - -/** - * A [[Product product]] with its [[Quantity quantity]], a [[PriceInEuroCents price]]. - */ -final case class CompleteOrderLine(quantity: Quantity, product: Product, price: PriceInEuroCents) - -/** - * A document that has to specify: a [[Location delivery location]], a [[Location shipping location]], the - * [[Customer customer]]'s info, the [[DateTime shipping date]], the total [[WeightInKilograms weight]] of the pallet, - * and a list of [[TransportDocumentLine transport document lines]]. - */ -final case class TransportDocument( - deliveryLocation: Location, - shippingLocation: Location, - customer: Customer, - shippingDate: LocalDateTime, - transportDocumentLines: NonEmptyList[TransportDocumentLine], - totalWeight: WeightInKilograms, -) - -/** - * A [[Product product]] with its respective shipped [[Quantity quantity]]. - */ -final case class TransportDocumentLine(quantity: Quantity, product: Product) - -/** - * A weight expressed in kilograms. - */ -final case class WeightInKilograms(n: PositiveDecimal) derives Plus +package dev.atedeg.mdm.clientorders + +import java.time.LocalDateTime +import java.util.UUID + +import cats.data.NonEmptyList +import eu.timepit.refined.api.Refined +import eu.timepit.refined.numeric.Interval +import eu.timepit.refined.string.MatchesRegex + +import dev.atedeg.mdm.products.Product +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.given + +/** + * A set of [[IncomingOrderLine order lines]] with their respective [[Quantity quantity]] (e.g. 1000 ricotte of 0.5kg, + * 50 squacqueroni of 1 kg), it also contains data about the [[Customer customer]], an expected + * [[DateTime delivery date]] and the [[Location delivery location]]. + */ +final case class IncomingOrder( + id: OrderID, + orderLines: NonEmptyList[IncomingOrderLine], + customer: Customer, + deliveryDate: LocalDateTime, + deliveryLocation: Location, +) + +/** + * The unique identifier of an order which allows one to track the order during its lifecycle. + */ +final case class OrderID(id: UUID) + +/** + * A [[Product product]] with its ordered [[Quantity quantity]]. + */ +final case class IncomingOrderLine(quantity: Quantity, product: Product) + +/** + * A quantity of something. + */ +final case class Quantity(n: PositiveNumber) + +/** + * The missing quantity of a [[Product product]] necessary to fullfil an [[InProgressOrderLine in-progress order line]]. + */ +final case class MissingQuantity(n: NonNegativeNumber) + +/** + * A physical or legal entity that places [[IncomingOrder orders]]. + */ +final case class Customer(code: CustomerID, name: CustomerName, vatNumber: VATNumber) + +/** + * An id which uniquely identifies a [[Customer customer]]. + */ +final case class CustomerID(id: UUID) + +/** + * A human readable name used to refer to a [[Customer customer]]. + */ +final case class CustomerName(name: String) + +/** + * An alphanumeric code for value-added tax purposes. + * + * @see + * [[https://en.wikipedia.org/wiki/VAT_identification_number VAT identification number on Wikipedia]] for further + * details. + */ +final case class VATNumber(number: String Refined MatchesRegex[VATRegex]) +private[clientorders] type VATRegex = "[A-Z]{2}[0-9A-Z]{2,13}" + +/** + * The location where an order has to be shipped to. + */ +final case class Location(latitude: Latitude, longitude: Longitude) + +/** + * A latitude specified in degrees. + */ +final case class Latitude(value: NumberInClosedRange[-90, 90]) + +/** + * A longitude specified in degrees. + */ +final case class Longitude(value: NumberInClosedRange[-180, 180]) + +/** + * Associates to each [[Product product]] its [[PriceInEuroCents unitary price]]. + */ +type PriceList = Product => PriceInEuroCents + +/** + * A price expressed in cents, the smallest currency unit for euros. + */ +final case class PriceInEuroCents(n: PositiveNumber) derives Plus + +/** + * An order where each [[PricedOrderLine line]] has an associated [[PriceInEuroCents price]] and, optionally, an applied + * [[Discount discount]]. It also has the total [[PriceInEuroCents price]]. Its structure resembles the + * [[IncomingOrder incoming order]]'s with the difference that each [[PricedOrderLine line]] has been priced. + */ +final case class PricedOrder( + id: OrderID, + orderLines: NonEmptyList[PricedOrderLine], + customer: Customer, + deliveryDate: LocalDateTime, + deliveryLocation: Location, + totalPrice: PriceInEuroCents, +) + +/** + * A [[Product product]] with its [[Quantity quantity]] and a [[PriceInEuroCents price]]. + */ +final case class PricedOrderLine(quantity: Quantity, product: Product, totalPrice: PriceInEuroCents) + +/** + * An order that is being fulfilled by an operator. Its structure resembles the [[PricedOrder priced order]]'s with the + * difference that each [[PricedOrderLine line]] can specify whether it is fulfilled or not. + */ +final case class InProgressOrder( + id: OrderID, + orderLines: NonEmptyList[InProgressOrderLine], + customer: Customer, + deliveryDate: LocalDateTime, + deliveryLocation: Location, + totalPrice: PriceInEuroCents, +) + +/** + * A [[Product product]] with its [[Quantity quantity]] and a [[PriceInEuroCents price]]. + * It may be in two different states: [[InProgressOrderLine.Complete complete]] if the + * [[Product product]] has already been palletized and is ready in the required [[Quantity quantity]]; + * [[InProgressOrderLine.Incomplete incomplete]] if the [[Product product]] is not present in the required + * [[Quantity quantity]]. + */ +enum InProgressOrderLine: + + /** + * A [[InProgressOrderLine line]] of an [[InProgressOrder in-progress order]] where the [[Product product]] is ready + * in the required [[Quantity quantity]] and has been palletized. + */ + case Complete(quantity: Quantity, product: Product, price: PriceInEuroCents) + + /** + * A [[InProgressOrderLine line]] of an [[InProgressOrder in-progress order]] where the [[Product product]] is still + * not available in the required [[Quantity quantity]] but [[PalletizedQuantity a part or none of it]] may have + * already been palletized. + */ + case Incomplete(actual: PalletizedQuantity, required: Quantity, product: Product, price: PriceInEuroCents) + +/** + * A quantity (possibly 0) of a palletized [[Product product]]. + */ +final case class PalletizedQuantity(n: NonNegativeNumber) derives Plus + +/** + * An order that has been fulfilled by the operator and is ready to be shipped. + */ +final case class CompletedOrder( + id: OrderID, + orderLines: NonEmptyList[CompleteOrderLine], + customer: Customer, + deliveryDate: LocalDateTime, + deliveryLocation: Location, + totalPrice: PriceInEuroCents, +) + +/** + * A [[Product product]] with its [[Quantity quantity]], a [[PriceInEuroCents price]]. + */ +final case class CompleteOrderLine(quantity: Quantity, product: Product, price: PriceInEuroCents) + +/** + * A document that has to specify: a [[Location delivery location]], a [[Location shipping location]], the + * [[Customer customer]]'s info, the [[DateTime shipping date]], the total [[WeightInKilograms weight]] of the pallet, + * and a list of [[TransportDocumentLine transport document lines]]. + */ +final case class TransportDocument( + deliveryLocation: Location, + shippingLocation: Location, + customer: Customer, + shippingDate: LocalDateTime, + transportDocumentLines: NonEmptyList[TransportDocumentLine], + totalWeight: WeightInKilograms, +) + +/** + * A [[Product product]] with its respective shipped [[Quantity quantity]]. + */ +final case class TransportDocumentLine(quantity: Quantity, product: Product) + +/** + * A weight expressed in kilograms. + */ +final case class WeightInKilograms(n: PositiveDecimal) derives Plus diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala index 0190fa97..3b7079bc 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala @@ -23,4 +23,3 @@ object QuantityOps: given Order[Quantity] with override def compare(x: Quantity, y: Quantity): Int = Order[Int].compare(x.n, y.n) - From 454d26426b47436f6e2bf1add6ed4402be90c5c2 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 4 Aug 2022 09:38:46 +0200 Subject: [PATCH 171/329] refactor: remove unused method --- .../dev/atedeg/mdm/clientorders/Actions.scala | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 2b6b2624..7da30aed 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -78,12 +78,6 @@ def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad]( } } yield InProgressOrder(id, newOrderLines, customer, dd, dl, totalPrice) -private def isProductInOrder(orderLines: NonEmptyList[InProgressOrderLine], product: Product): Boolean = - orderLines.map { - case Complete(_, prod, _) => prod - case Incomplete(_, _, prod, _) => prod - }.exists(_ == product) - private def addToLine[M[_]: Monad: CanRaise[PalletizedMoreThanRequired]](ol: InProgressOrderLine)( quantityToAdd: Quantity, ): M[InProgressOrderLine] = ol match @@ -144,13 +138,7 @@ def weightOrder(completeOrder: CompletedOrder): WeightInKilograms = def createTransportDocument(completeOrder: CompletedOrder, weight: WeightInKilograms): TransportDocument = val CompletedOrder(_, orderLines, customer, _, deliveryLocation, _) = completeOrder val transportDocumentLines = orderLines.map(l => TransportDocumentLine(l.quantity, l.product)) - TransportDocument( - deliveryLocation, - mambelliDeliveryLocation, - customer, - LocalDateTime.now(), - transportDocumentLines, - weight, - ) + val date = LocalDateTime.now + TransportDocument(deliveryLocation, mambelliDeliveryLocation, customer, date, transportDocumentLines, weight) private val mambelliDeliveryLocation = Location(Latitude(12), Longitude(44)) From 578474033e5b05a9b8e0175fe0fdbef06a9fe6b3 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 4 Aug 2022 11:46:40 +0200 Subject: [PATCH 172/329] style(scalafix): ignore noValPattern and re-add noUniversalEquality --- .scalafix.conf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.scalafix.conf b/.scalafix.conf index af244273..d9b062bc 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -18,6 +18,5 @@ DisableSyntax { noDefaultArgs = true noFinalVal = true noFinalize = true - noValPatterns = true - noUniversalEquality = false # Disabled because of -language:strictEquality compiler flag + noUniversalEquality = true } From cf6746d24d639940cafa72dda9268d82f421fdbd Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 4 Aug 2022 11:51:53 +0200 Subject: [PATCH 173/329] refactor: fix scalafix errors --- .../main/scala/dev/atedeg/mdm/milkplanning/Actions.scala | 1 - .../scala/dev/atedeg/mdm/productionplanning/Actions.scala | 6 +++++- .../src/test/scala/dev/atedeg/mdm/production/Tests.scala | 1 - .../src/main/scala/dev/atedeg/mdm/products/Products.scala | 3 +++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala index adc5a276..e81b1296 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala @@ -39,7 +39,6 @@ private def milkNeededForProducts( .map(milkNeededForProduct(_, stock, recipeBook)) .foldLeft(0.quintalsOfMilk)(_ + _) -@SuppressWarnings(Array("scalafix:DisableSyntax.noValPatterns")) private def milkNeededForProduct( product: RequestedProduct, stock: Stock, diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index 8f9e802e..594e3d0f 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -4,6 +4,7 @@ import java.time.LocalDate import cats.Monad import cats.data.NonEmptyList +import cats.kernel.Eq import cats.syntax.all.* import dev.atedeg.mdm.productionplanning.{ CheeseTypeRipeningDays, RipeningDays } @@ -39,7 +40,7 @@ private def checkDeliverabilityOfOrder[M[_]: Monad: CanEmit[OrderDelayed]]( cheeseTypeRipeningDays: CheeseTypeRipeningDays, )(order: Order): M[Unit] = { val ripeningDays = order.orderedProducts.map(_.product.cheeseType).map(cheeseTypeRipeningDays(_)) - val isDelayed = ripeningDays.map(productionInTime(_, order.requiredBy)).exists(_ == OrderStatus.Delayed) + val isDelayed = ripeningDays.map(productionInTime(_, order.requiredBy)).exists(_ === OrderStatus.Delayed) when(isDelayed) { val deliveryDate = newDeliveryDate(RipeningDays(ripeningDays.map(_.days).reduceLeft(max))) emit(OrderDelayed(order.orderdID, deliveryDate): OrderDelayed) @@ -68,3 +69,6 @@ private def newDeliveryDate(ripeningDays: RipeningDays): LocalDate = private enum OrderStatus: case Delayed case NonDelayed + +object OrderStatus: + given Eq[OrderStatus] = Eq.fromUniversalEquals diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala index 6506eedd..e269d73c 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala @@ -28,7 +28,6 @@ trait Mocks { val allIngredients: NonEmptyList[Ingredient] = NonEmptyList.of(Milk, Cream, Rennet, Salt, Probiotics) } -@SuppressWarnings(Array("scalafix:DisableSyntax.noValPatterns")) class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { Feature("Production management") { diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala index a9d79638..45e27dec 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/Products.scala @@ -1,5 +1,7 @@ package dev.atedeg.mdm.products +import cats.kernel.Eq + import dev.atedeg.mdm.products.utils.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given @@ -51,3 +53,4 @@ val allCaciottaWeights = all[CaciottaWeightsInGrams] object Product: def unapply(prod: Product): (CheeseType, Grams) = (prod.cheeseType, prod.weight) + given Eq[Product] = Eq.fromUniversalEquals From e11bc6041ba2c85f1c80049891ff00e14ff09e02 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 4 Aug 2022 11:52:12 +0200 Subject: [PATCH 174/329] test: complete tests --- .../dev/atedeg/mdm/clientorders/Actions.scala | 15 ++- .../dev/atedeg/mdm/clientorders/Errors.scala | 2 +- .../mdm/clientorders/utils/TypesOps.scala | 2 + .../atedeg/mdm/clientorders/ActionsTest.scala | 115 +++++++++++++----- .../scala/dev/atedeg/mdm/utils/Refined.scala | 5 +- 5 files changed, 102 insertions(+), 37 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 7da30aed..62c9de52 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -24,7 +24,7 @@ import dev.atedeg.mdm.utils.monads.* * of each [[IncomingOrderLine line]] using a [[PriceList price list]]. */ def priceOrder(priceList: PriceList)(incomingOrder: IncomingOrder): PricedOrder = - val pricedOrderLines = incomingOrder.orderLines.map { case iol @ IncomingOrderLine(quantity, product) => + val pricedOrderLines = incomingOrder.orderLines.map { case IncomingOrderLine(quantity, product) => val price = priceList(product).n * quantity.n PricedOrderLine(quantity, product, price.euroCents) } @@ -65,19 +65,24 @@ def startPreparingOrder(pricedOrder: PricedOrder): InProgressOrder = * an [[order.InProgressOrder in-progress order]] where the corresponding [[order.InProgressOrderLine line]] has been * updated with the [[order.Quantity specified quantity]]. */ -def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad]( + +def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad](quantity: Quantity, product: Product)( inProgressOrder: InProgressOrder, -)(quantity: Quantity, product: Product): M[InProgressOrder] = +): M[InProgressOrder] = val InProgressOrder(id, ol, customer, dd, dl, totalPrice) = inProgressOrder for { - orderLine <- ol.find(_ == product).ifMissingRaise(ProductNotInOrder()) + orderLine <- ol.find(hasProduct(product)).ifMissingRaise(ProductNotInOrder(product)) updatedLine <- addToLine(orderLine)(quantity) newOrderLines = ol.map { - case i @ Incomplete(_, _, `product`, _) => updatedLine + case Incomplete(_, _, `product`, _) => updatedLine case l @ _ => l } } yield InProgressOrder(id, newOrderLines, customer, dd, dl, totalPrice) +private def hasProduct(product: Product)(ol: InProgressOrderLine): Boolean = ol match + case Incomplete(_, _, p, _) => p === product + case Complete(_, p, _) => p === product + private def addToLine[M[_]: Monad: CanRaise[PalletizedMoreThanRequired]](ol: InProgressOrderLine)( quantityToAdd: Quantity, ): M[InProgressOrderLine] = ol match diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala index 08974551..acf56cfb 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Errors.scala @@ -10,7 +10,7 @@ enum PalletizationError: * Raised when trying to palletize a [[Product product]] for an [[Order.InProgressOrder order]] that does not require * it. */ - case ProductNotInOrder() + case ProductNotInOrder(productNotInOrder: Product) /** * Raised when trying to palletize more of a [[Product product]] than required. diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala index 3b7079bc..067142fc 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/utils/TypesOps.scala @@ -4,11 +4,13 @@ import cats.kernel.Order import eu.timepit.refined.auto.autoUnwrap import dev.atedeg.mdm.clientorders.* +import dev.atedeg.mdm.products.Product import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given extension (n: PositiveNumber) def euroCents: PriceInEuroCents = PriceInEuroCents(n) + def of(p: Product): IncomingOrderLine = IncomingOrderLine(Quantity(n), p) def quantity: Quantity = Quantity(n) extension (i: NonNegativeNumber) diff --git a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala index 544e0441..b97ec001 100644 --- a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala +++ b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala @@ -3,15 +3,21 @@ package dev.atedeg.mdm.clientorders import java.time.LocalDateTime import java.util.UUID +import OrderCompletionError.* +import PalletizationError.* +import cats.Monad import cats.data.{ NonEmptyList, NonEmptyMap, Writer } import cats.implicits.catsKernelOrderingForOrder import cats.syntax.all.* import eu.timepit.refined.predicates.all.NonNegative +import eu.timepit.refined.string.MatchesRegex import org.scalactic.{ Explicitly, Normalization, Uniformity } +import org.scalatest.EitherValues.* import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers +import dev.atedeg.mdm.clientorders.utils.* import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } import dev.atedeg.mdm.products.Product.* import dev.atedeg.mdm.utils.* @@ -19,14 +25,10 @@ import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.intToNonNegativeNumber import dev.atedeg.mdm.utils.monads.* -extension (n: PositiveNumber) - def of(p: Product): IncomingOrderLine = IncomingOrderLine(Quantity(n), p) - def cents: PriceInEuroCents = PriceInEuroCents(n) - trait CustomerMock: private val customerId: CustomerID = CustomerID(UUID.randomUUID) private val customerName: CustomerName = CustomerName("Giovanni Molari") - private val vatNumber: VATNumber = VATNumber(???) + private val vatNumber: VATNumber = VATNumber(coerce("IT12345678901")) val customer: Customer = Customer(customerId, customerName, vatNumber) trait LocationMock: @@ -37,22 +39,35 @@ trait LocationMock: trait PriceListMock: val priceList: Product => PriceInEuroCents = Map( - Product.Caciotta(1000) -> 100.cents, - Product.Caciotta(500) -> 50.cents, + Caciotta(1000) -> 100.euroCents, + Caciotta(500) -> 50.euroCents, ) -trait OrderMocks extends CustomerMock, LocationMock: +trait OrderMocks extends PriceListMock, CustomerMock, LocationMock: private val orderId: OrderID = OrderID(UUID.randomUUID) private val orderLines: NonEmptyList[IncomingOrderLine] = NonEmptyList.of[IncomingOrderLine]( - 100 of Product.Caciotta(1000), - 100 of Product.Caciotta(500), + 100 of Caciotta(1000), + 100 of Caciotta(500), ) private val date: LocalDateTime = LocalDateTime.now val incomingOrder: IncomingOrder = IncomingOrder(orderId, orderLines, customer, date, location) + val inProgressCompleteOrder: InProgressOrder = + def palletizeAll[M[_]: Monad: CanRaise[PalletizationError]](inProgressOrder: InProgressOrder): M[InProgressOrder] = + palletizeProductForOrder(Quantity(100), Caciotta(500))(inProgressOrder) + >>= palletizeProductForOrder(Quantity(100), Caciotta(1000)) + + val inProgressOrder = startPreparingOrder(priceOrder(priceList)(incomingOrder)) + val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = palletizeAll(inProgressOrder) + palletizeAction.execute._2.value + + val completedOrder: CompletedOrder = + val completeAction: Action[OrderCompletionError, Unit, CompletedOrder] = completeOrder(inProgressCompleteOrder) + completeAction.execute._2.value + @SuppressWarnings(Array("org.wartremover.warts.Any")) -class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Matchers with OrderMocks with PriceListMock: +class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Matchers with OrderMocks: Feature("Order pricing") { Scenario("Operator prices an order") { @@ -61,7 +76,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match val pricedOrder = priceOrder(priceList)(incomingOrder) Then("the priced is computed correctly") val expectedPrice = - incomingOrder.orderLines.map(ol => priceList(ol.product).n * ol.quantity.n).reduce(_ + _).cents + incomingOrder.orderLines.map(ol => priceList(ol.product).n * ol.quantity.n).reduce(_ + _).euroCents pricedOrder.totalPrice shouldBe expectedPrice } } @@ -73,12 +88,10 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match When("the order is marked as in progress") val inProgressOrder = startPreparingOrder(pricedOrder) Then("it should not contain any palletized product") - val nonEmptyLines = inProgressOrder.orderLines.filter(ol => - ol match - case _: InProgressOrderLine.Complete => false - case ol: InProgressOrderLine.Incomplete => ol.actual.n === 0, + inProgressOrder.orderLines shouldBe NonEmptyList.of( + InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(1000), 10_000.euroCents), + InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(500), 5000.euroCents), ) - nonEmptyLines shouldBe empty } Scenario("A product is palletized for an order that does not require it") { @@ -87,10 +100,11 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match And("a product that is not requested by the order") val productNotInOrder = Ricotta(350) When("the operator tries to palletize it") - val palletizeAction = palletizeProductForOrder(inProgressOrder)(Quantity(10), productNotInOrder) + val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = + palletizeProductForOrder(Quantity(10), productNotInOrder)(inProgressOrder) Then("a ProductNotInOrder error should be raised") - // val result = palletizeAction.execute - ??? + val (_, result) = palletizeAction.execute + result.left.value shouldBe ProductNotInOrder(productNotInOrder) } Scenario("A product is palletized in a quantity greater than the required one") { @@ -99,10 +113,11 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match And("a product requested by the order") val productInOrder = Caciotta(500) When("the operator tries to palletize it in a quantity greater than the required one") - val palletizeAction = palletizeProductForOrder(inProgressOrder)(Quantity(1000), productInOrder) + val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = + palletizeProductForOrder(Quantity(1000), productInOrder)(inProgressOrder) Then("a PalletizedMoreThanRequired error should be raised") - // val result = palletizeAction.execute - ??? + val (_, result) = palletizeAction.execute + result.left.value shouldBe PalletizedMoreThanRequired(MissingQuantity(100)) } Scenario("A product is palletized in the exact quantity") { @@ -111,9 +126,14 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match And("a product requested by the order") val productInOrder = Caciotta(500) When("the operator palletizes it in the exact required quantity") - val palletizeAction = palletizeProductForOrder(inProgressOrder)(Quantity(100), productInOrder) + val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = + palletizeProductForOrder(Quantity(100), productInOrder)(inProgressOrder) Then("the corresponding order line is marked as completed") - ??? + val (_, result) = palletizeAction.execute + result.value.orderLines shouldBe NonEmptyList.of( + InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(1000), 10_000.euroCents), + InProgressOrderLine.Complete(Quantity(100), Caciotta(500), 5000.euroCents), + ) } Scenario("A product is palletized in a quantity lower than the required one") { @@ -122,9 +142,14 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match And("a product requested by the order") val productInOrder = Caciotta(500) When("the operator palletizes it in a quantity lower than the required one") - val palletizeAction = palletizeProductForOrder(inProgressOrder)(Quantity(20), productInOrder) + val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = + palletizeProductForOrder(Quantity(20), productInOrder)(inProgressOrder) Then("the corresponding order line is updated") - ??? + val (_, result) = palletizeAction.execute + result.value.orderLines shouldBe NonEmptyList.of( + InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(1000), 10_000.euroCents), + InProgressOrderLine.Incomplete(PalletizedQuantity(20), Quantity(100), Caciotta(500), 5000.euroCents), + ) } } @@ -133,28 +158,58 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match Given("an incomplete in-progress order") val inProgressOrder = startPreparingOrder(priceOrder(priceList)(incomingOrder)) When("one tries to mark it as completed") - val completeAction = completeOrder(inProgressOrder) + val completeAction: Action[OrderCompletionError, Unit, CompletedOrder] = completeOrder(inProgressOrder) Then("an OrderCompletionError is raised") - ??? + val (_, result) = completeAction.execute + result.left.value shouldBe OrderNotComplete() } Scenario("A complete order is completed") { Given("a complete in-progress order") + val order = inProgressCompleteOrder When("one marks it as completed") + val completeAction: Action[OrderCompletionError, Unit, CompletedOrder] = completeOrder(order) Then("it is completed correctly") + val (_, completed) = completeAction.execute + completed.value shouldBe CompletedOrder( + order.id, + NonEmptyList.of( + CompleteOrderLine(Quantity(100), Caciotta(1000), 10_000.euroCents), + CompleteOrderLine(Quantity(100), Caciotta(500), 5000.euroCents), + ), + order.customer, + order.deliveryDate, + order.deliveryLocation, + order.totalPrice, + ) } } Feature("Order transportation") { Scenario("The order weight is computed") { Given("an order") + val order = completedOrder When("the weight is computed") + val weight = weightOrder(order) Then("it is the exact sum of the weights of its products") + val expectedGrams = order.orderLines.map(ol => ol.quantity.n * ol.product.weight.n).reduce(_ + _) + val expectedKilograms = WeightInKilograms(expectedGrams.toDecimal / 1000) + weight shouldBe expectedKilograms } - Scenario("A transport document is printed") { + Scenario("A transport document is created") { Given("an order") + val order = completedOrder + val weight = weightOrder(order) When("one requests the transport document") + val td = createTransportDocument(order, weight) Then("the correct transport document is generated") + td.customer shouldBe order.customer + td.deliveryLocation shouldBe order.deliveryLocation + td.totalWeight shouldBe weight + td.transportDocumentLines shouldBe NonEmptyList.of( + TransportDocumentLine(Quantity(100), Caciotta(1000)), + TransportDocumentLine(Quantity(100), Caciotta(500)), + ) } } diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala index b5b32a3e..786df74a 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala @@ -44,8 +44,11 @@ given refinedEq[N: Eq, P]: Eq[N Refined P] with override def eqv(x: N Refined P, y: N Refined P): Boolean = Eq[N].eqv(x.value, y.value) // Instances for the various numeric ops + +extension [N](n: N) def refined[P: ValidFor[N]]: Option[N Refined P] = refineV[P](n).toOption + @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) -def coerce[A, P](a: A)(using Validate[A, P]): A Refined P = refineV[P](a).toOption.get +def coerce[A, P](a: A)(using Validate[A, P]): A Refined P = a.refined.get private type ValidFor[N] = [P] =>> Validate[N, P] From 2e7dd91592e68e741c13f0bff16034f95a49d8f2 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 4 Aug 2022 12:17:28 +0200 Subject: [PATCH 175/329] docs: remove redundant descriptions --- .../dev/atedeg/mdm/clientorders/Actions.scala | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 62c9de52..52977974 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -41,9 +41,6 @@ def priceOrder(priceList: PriceList)(incomingOrder: IncomingOrder): PricedOrder /** * Turns a [[order.PricedOrder priced order]] into an [[order.InProgressOrder in-progress order]] that can then be * fulfilled by operators. - * - * @param pricedOrder - * the priced order to be marked as in progress. */ def startPreparingOrder(pricedOrder: PricedOrder): InProgressOrder = val PricedOrder(id, ol, customer, deliveryDate, deliveryLocation, totalPrice) = pricedOrder @@ -53,19 +50,11 @@ def startPreparingOrder(pricedOrder: PricedOrder): InProgressOrder = InProgressOrder(id, newOrderLine, customer, deliveryDate, deliveryLocation, totalPrice) /** - * Palletizes a [[Product product]] in the specified [[order.Quantity quantity]]. - * - * @param inProgressOrder - * the order for which the product needs to be palletized. - * @param quantity - * the quantity of product to be palletized. - * @param product - * the product to be palletized. - * @return - * an [[order.InProgressOrder in-progress order]] where the corresponding [[order.InProgressOrderLine line]] has been - * updated with the [[order.Quantity specified quantity]]. + * Palletizes a [[Product product]] in the specified [[order.Quantity quantity]] for a given + * [[InProgressOrder order in progress]]. The result is an [[order.InProgressOrder in-progress order]] + * where the corresponding [[order.InProgressOrderLine line]] has been updated with the + * [[order.Quantity specified quantity]]. */ - def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad](quantity: Quantity, product: Product)( inProgressOrder: InProgressOrder, ): M[InProgressOrder] = @@ -96,9 +85,6 @@ private def addToLine[M[_]: Monad: CanRaise[PalletizedMoreThanRequired]](ol: InP /** * Completes an [[order.InProgressOrder in-progress order]]. - * - * @param inProgressOrder - * the in-progress order to be marked as complete. */ def completeOrder[Result[_]: CanRaise[OrderCompletionError]: Monad]( inProgressOrder: InProgressOrder, @@ -121,9 +107,6 @@ private def getCompletedOrderLines(orderLines: NonEmptyList[InProgressOrderLine] /** * Computes the total [[order.WeightInKilograms weight]] of a [[order.CompletedOrder complete order]]. - * - * @param completeOrder - * the order whose weight has to be computed. */ def weightOrder(completeOrder: CompletedOrder): WeightInKilograms = completeOrder.orderLines @@ -134,11 +117,6 @@ def weightOrder(completeOrder: CompletedOrder): WeightInKilograms = /** * Creates a [[order.TransportDocument transport document]] from a [[order.CompletedOrder complete order]]. - * - * @param completeOrder - * the order for which the transport document has to be created. - * @param weight - * the weight of the order. */ def createTransportDocument(completeOrder: CompletedOrder, weight: WeightInKilograms): TransportDocument = val CompletedOrder(_, orderLines, customer, _, deliveryLocation, _) = completeOrder From 498bb39de6160c2edf224d0eaccc57b3c9d83510 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 4 Aug 2022 12:30:05 +0200 Subject: [PATCH 176/329] feat: add new outgoing event --- .../dev/atedeg/mdm/clientorders/Events.scala | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala index 5d021164..453c64ba 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala @@ -1,15 +1,26 @@ package dev.atedeg.mdm.clientorders +import java.time.LocalDateTime + +import cats.data.NonEmptyList + import dev.atedeg.mdm.products.Product /** * The events that have to be handled by the bounded context. */ enum IncomingEvent: + /** - * An [[IncomingEvent event]] which is received when an [[order.IncomingOrder order]] is made. + * An [[IncomingEvent event]] which is received when an [[Order.IncomingOrder order]] is made. */ - case OrderReceived() + case OrderReceived( + id: OrderID, + orderLines: NonEmptyList[IncomingOrderLine], + customer: Customer, + deliveryDate: LocalDateTime, + deliveryLocation: Location, + ) /** * An [[IncomingEvent event]] received when an operator takes a [[Product product]] from the stock and palletizes it @@ -18,7 +29,16 @@ enum IncomingEvent: case ProductPalletizedForOrder(orderID: OrderID, quantity: Quantity, product: Product) /** - * An [[IncomingEvent event]] received when an [[order.InProgressOrder order in progress]] is marked as + * An [[IncomingEvent event]] received when an [[Order.InProgressOrder order in progress]] is marked as * [[order.CompletedOrder ready to be shipped]]. */ case OrderCompleted(orderID: OrderID) + +/** + * The events that may be produced by the bounded context. + */ +enum OutgoingEvent: + /** + * An event emitted when a new [[IncomingOrder incoming order]] is received and processed. + */ + case OrderReceived(incomingOrder: IncomingOrder) From dac64834f8bae9b8271f935b70f61ca5a46cb2ff Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 4 Aug 2022 12:52:10 +0200 Subject: [PATCH 177/329] feat: emit processed order event --- .../main/scala/dev/atedeg/mdm/clientorders/Actions.scala | 9 ++++++++- .../main/scala/dev/atedeg/mdm/clientorders/Events.scala | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 52977974..4fddbee3 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -10,6 +10,7 @@ import cats.syntax.all.* import dev.atedeg.mdm.clientorders.InProgressOrderLine.* import dev.atedeg.mdm.clientorders.OrderCompletionError.* +import dev.atedeg.mdm.clientorders.OutgoingEvent.OrderProcessed import dev.atedeg.mdm.clientorders.PalletizationError.* import dev.atedeg.mdm.clientorders.utils.* import dev.atedeg.mdm.clientorders.utils.QuantityOps.* @@ -23,7 +24,13 @@ import dev.atedeg.mdm.utils.monads.* * Turns an [[IncomingOrder incoming order]] into a [[PricedOrder priced order]] by computing the price * of each [[IncomingOrderLine line]] using a [[PriceList price list]]. */ -def priceOrder(priceList: PriceList)(incomingOrder: IncomingOrder): PricedOrder = +def processIncomingOrder[M[_]: Monad: Emits[OrderProcessed]](priceList: PriceList)( + incomingOrder: IncomingOrder, +): M[PricedOrder] = + val pricedOrder = priceOrder(priceList)(incomingOrder) + emit(OrderProcessed(incomingOrder): OrderProcessed).thenReturn(pricedOrder) + +private def priceOrder(priceList: PriceList)(incomingOrder: IncomingOrder): PricedOrder = val pricedOrderLines = incomingOrder.orderLines.map { case IncomingOrderLine(quantity, product) => val price = priceList(product).n * quantity.n PricedOrderLine(quantity, product, price.euroCents) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala index 453c64ba..bc3bf342 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala @@ -41,4 +41,4 @@ enum OutgoingEvent: /** * An event emitted when a new [[IncomingOrder incoming order]] is received and processed. */ - case OrderReceived(incomingOrder: IncomingOrder) + case OrderProcessed(incomingOrder: IncomingOrder) From ef6c1b813e9757e2329f5dcbff50c6ce5366f40b Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 4 Aug 2022 12:52:29 +0200 Subject: [PATCH 178/329] test: processed order event --- .../scala/dev/atedeg/mdm/clientorders/ActionsTest.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala index b97ec001..11e78f11 100644 --- a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala +++ b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala @@ -17,6 +17,7 @@ import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers +import dev.atedeg.mdm.clientorders.OutgoingEvent.OrderProcessed import dev.atedeg.mdm.clientorders.utils.* import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } import dev.atedeg.mdm.products.Product.* @@ -72,12 +73,16 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match Feature("Order pricing") { Scenario("Operator prices an order") { Given("an incoming order") - When("the order is priced") - val pricedOrder = priceOrder(priceList)(incomingOrder) + val order = incomingOrder + When("the order is processed") + val priceAction: SafeAction[OrderProcessed, PricedOrder] = processIncomingOrder(priceList)(incomingOrder) + val (events, pricedOrder) = priceAction.execute Then("the priced is computed correctly") val expectedPrice = incomingOrder.orderLines.map(ol => priceList(ol.product).n * ol.quantity.n).reduce(_ + _).euroCents pricedOrder.totalPrice shouldBe expectedPrice + And("an event is emitted") + events shouldBe List(OrderProcessed(incomingOrder)) } } From 16ed8f0ff66b475c31d1ee3529359948e989b94a Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 4 Aug 2022 13:05:46 +0200 Subject: [PATCH 179/329] docs: add ubidoc configuration --- .ubidoc.yml | 26 ++++++++++++++++++++++++++ docs/_docs/client-orders.md | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/.ubidoc.yml b/.ubidoc.yml index 5899dd0d..88fbebda 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -102,4 +102,30 @@ tables: - case: "OutgoingEvent.ProductionPlanReady" - case: "OutgoingEvent.OrderDelayed" + - name: "client-orders-ul" + rows: + - class: "IncomingOrder" + - class: "IncomingOrderLine" + - class: "Customer" + - class: "Location" + - type: "PriceList" + - class: "PricedOrder" + - class: "PricedOrderLine" + - class: "InProgressOrder" + - enum: "InProgressOrderLine" + - class: "CompletedOrder" + - class: "CompleteOrderLine" + - class: "TransportDocument" + - class: "TransportDocumentLine" + + - name: "client-orders-incoming" + rows: + - case: "IncomingEvent.OrderReceived" + - case: "IncomingEvent.ProductPalletizedForOrder" + - case: "IncomingEvent.OrderCompleted" + + - name: "client-orders-outgoing" + rows: + - case: "OutgoingEvent.OrderProcessed" + ignored: [] diff --git a/docs/_docs/client-orders.md b/docs/_docs/client-orders.md index d0d05c83..a7b4ea00 100644 --- a/docs/_docs/client-orders.md +++ b/docs/_docs/client-orders.md @@ -35,3 +35,7 @@ to the customer notifying them the order has been shipped. ### Incoming Events {% include client-orders-incoming.md %} + +### Outgoing Events + +{% include client-orders-outgoing.md %} From 40d5661e63acb13247f7347f899a8b8a93b55c53 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 4 Aug 2022 14:36:49 +0200 Subject: [PATCH 180/329] docs: fix doc --- .../dev/atedeg/mdm/clientorders/Actions.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 4fddbee3..9f43f376 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -46,7 +46,7 @@ private def priceOrder(priceList: PriceList)(incomingOrder: IncomingOrder): Pric ) /** - * Turns a [[order.PricedOrder priced order]] into an [[order.InProgressOrder in-progress order]] that can then be + * Turns a [[Order.PricedOrder priced order]] into an [[Order.InProgressOrder in-progress order]] that can then be * fulfilled by operators. */ def startPreparingOrder(pricedOrder: PricedOrder): InProgressOrder = @@ -57,10 +57,10 @@ def startPreparingOrder(pricedOrder: PricedOrder): InProgressOrder = InProgressOrder(id, newOrderLine, customer, deliveryDate, deliveryLocation, totalPrice) /** - * Palletizes a [[Product product]] in the specified [[order.Quantity quantity]] for a given - * [[InProgressOrder order in progress]]. The result is an [[order.InProgressOrder in-progress order]] - * where the corresponding [[order.InProgressOrderLine line]] has been updated with the - * [[order.Quantity specified quantity]]. + * Palletizes a [[Product product]] in the specified [[Order.Quantity quantity]] for a given + * [[InProgressOrder order in progress]]. The result is an [[Order.InProgressOrder in-progress order]] + * where the corresponding [[Order.InProgressOrderLine line]] has been updated with the + * [[Order.Quantity specified quantity]]. */ def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad](quantity: Quantity, product: Product)( inProgressOrder: InProgressOrder, @@ -91,7 +91,7 @@ private def addToLine[M[_]: Monad: CanRaise[PalletizedMoreThanRequired]](ol: InP case LessThan => raise(PalletizedMoreThanRequired(missingQuantity): PalletizedMoreThanRequired) /** - * Completes an [[order.InProgressOrder in-progress order]]. + * Completes an [[Order.InProgressOrder in-progress order]]. */ def completeOrder[Result[_]: CanRaise[OrderCompletionError]: Monad]( inProgressOrder: InProgressOrder, @@ -113,7 +113,7 @@ private def getCompletedOrderLines(orderLines: NonEmptyList[InProgressOrderLine] _getCompletedOrderLines(Some(Nil))(orderLines.toList).flatMap(_.toNel).map(_.reverse) /** - * Computes the total [[order.WeightInKilograms weight]] of a [[order.CompletedOrder complete order]]. + * Computes the total [[Order.WeightInKilograms weight]] of a [[Order.CompletedOrder complete order]]. */ def weightOrder(completeOrder: CompletedOrder): WeightInKilograms = completeOrder.orderLines @@ -123,7 +123,7 @@ def weightOrder(completeOrder: CompletedOrder): WeightInKilograms = .reduce(_ + _) /** - * Creates a [[order.TransportDocument transport document]] from a [[order.CompletedOrder complete order]]. + * Creates a [[Order.TransportDocument transport document]] from a [[Order.CompletedOrder complete order]]. */ def createTransportDocument(completeOrder: CompletedOrder, weight: WeightInKilograms): TransportDocument = val CompletedOrder(_, orderLines, customer, _, deliveryLocation, _) = completeOrder From 0c279177114953f7d420cb0b4b5054f4f31670f7 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 4 Aug 2022 14:46:22 +0200 Subject: [PATCH 181/329] refactor: minor changes --- .../dev/atedeg/mdm/clientorders/Actions.scala | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 9f43f376..1766ac9e 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -50,10 +50,8 @@ private def priceOrder(priceList: PriceList)(incomingOrder: IncomingOrder): Pric * fulfilled by operators. */ def startPreparingOrder(pricedOrder: PricedOrder): InProgressOrder = - val PricedOrder(id, ol, customer, deliveryDate, deliveryLocation, totalPrice) = pricedOrder - val newOrderLine = ol.map { case PricedOrderLine(quantity, product, price) => - Incomplete(0.palletizedQuantity, quantity, product, price) - } + val PricedOrder(id, ols, customer, deliveryDate, deliveryLocation, totalPrice) = pricedOrder + val newOrderLine = ols.map(ol => Incomplete(0.palletizedQuantity, ol.quantity, ol.product, ol.totalPrice)) InProgressOrder(id, newOrderLine, customer, deliveryDate, deliveryLocation, totalPrice) /** @@ -96,21 +94,21 @@ private def addToLine[M[_]: Monad: CanRaise[PalletizedMoreThanRequired]](ol: InP def completeOrder[Result[_]: CanRaise[OrderCompletionError]: Monad]( inProgressOrder: InProgressOrder, ): Result[CompletedOrder] = - val InProgressOrder(id, ol, customer, dd, dl, totalPrice) = inProgressOrder + val InProgressOrder(id, ols, customer, dd, dl, totalPrice) = inProgressOrder for { - completedOrderLines <- getCompletedOrderLines(ol).ifMissingRaise(OrderNotComplete()) - completeOrderLines = completedOrderLines.map(o => CompleteOrderLine(o.quantity, o.product, o.price)) + completedOrderLines <- getCompletedOrderLines(ols).ifMissingRaise(OrderNotComplete()) + completeOrderLines = completedOrderLines.map(ol => CompleteOrderLine(ol.quantity, ol.product, ol.price)) } yield CompletedOrder(id, completeOrderLines, customer, dd, dl, totalPrice) private def getCompletedOrderLines(orderLines: NonEmptyList[InProgressOrderLine]): Option[NonEmptyList[Complete]] = @tailrec - def _getCompletedOrderLines(acc: Option[List[Complete]])(l: List[InProgressOrderLine]): Option[List[Complete]] = + def go(acc: Option[List[Complete]])(l: List[InProgressOrderLine]): Option[List[Complete]] = l match - case (c @ _: Complete) :: tail => _getCompletedOrderLines(acc.map(c :: _))(tail) + case (c @ _: Complete) :: tail => go(acc.map(c :: _))(tail) case (_: Incomplete) :: _ => None case Nil => acc - _getCompletedOrderLines(Some(Nil))(orderLines.toList).flatMap(_.toNel).map(_.reverse) + go(Some(Nil))(orderLines.toList).flatMap(_.toNel).map(_.reverse) /** * Computes the total [[Order.WeightInKilograms weight]] of a [[Order.CompletedOrder complete order]]. From 57493d1c045e583d6b6371164a0f3e1524533648 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 4 Aug 2022 14:52:28 +0200 Subject: [PATCH 182/329] test: improve list tests --- .../atedeg/mdm/clientorders/ActionsTest.scala | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala index 11e78f11..07d6734d 100644 --- a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala +++ b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala @@ -93,7 +93,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match When("the order is marked as in progress") val inProgressOrder = startPreparingOrder(pricedOrder) Then("it should not contain any palletized product") - inProgressOrder.orderLines shouldBe NonEmptyList.of( + inProgressOrder.orderLines.toList should contain allOf ( InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(1000), 10_000.euroCents), InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(500), 5000.euroCents), ) @@ -135,7 +135,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match palletizeProductForOrder(Quantity(100), productInOrder)(inProgressOrder) Then("the corresponding order line is marked as completed") val (_, result) = palletizeAction.execute - result.value.orderLines shouldBe NonEmptyList.of( + result.value.orderLines.toList should contain allOf ( InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(1000), 10_000.euroCents), InProgressOrderLine.Complete(Quantity(100), Caciotta(500), 5000.euroCents), ) @@ -151,7 +151,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match palletizeProductForOrder(Quantity(20), productInOrder)(inProgressOrder) Then("the corresponding order line is updated") val (_, result) = palletizeAction.execute - result.value.orderLines shouldBe NonEmptyList.of( + result.value.orderLines.toList should contain allOf ( InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(1000), 10_000.euroCents), InProgressOrderLine.Incomplete(PalletizedQuantity(20), Quantity(100), Caciotta(500), 5000.euroCents), ) @@ -171,48 +171,45 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match Scenario("A complete order is completed") { Given("a complete in-progress order") - val order = inProgressCompleteOrder When("one marks it as completed") - val completeAction: Action[OrderCompletionError, Unit, CompletedOrder] = completeOrder(order) + val completeAction: Action[OrderCompletionError, Unit, CompletedOrder] = completeOrder(inProgressCompleteOrder) Then("it is completed correctly") val (_, completed) = completeAction.execute completed.value shouldBe CompletedOrder( - order.id, + inProgressCompleteOrder.id, NonEmptyList.of( CompleteOrderLine(Quantity(100), Caciotta(1000), 10_000.euroCents), CompleteOrderLine(Quantity(100), Caciotta(500), 5000.euroCents), ), - order.customer, - order.deliveryDate, - order.deliveryLocation, - order.totalPrice, + inProgressCompleteOrder.customer, + inProgressCompleteOrder.deliveryDate, + inProgressCompleteOrder.deliveryLocation, + inProgressCompleteOrder.totalPrice, ) } } Feature("Order transportation") { Scenario("The order weight is computed") { - Given("an order") - val order = completedOrder + Given("a completed order") When("the weight is computed") - val weight = weightOrder(order) + val weight = weightOrder(completedOrder) Then("it is the exact sum of the weights of its products") - val expectedGrams = order.orderLines.map(ol => ol.quantity.n * ol.product.weight.n).reduce(_ + _) + val expectedGrams = completedOrder.orderLines.map(ol => ol.quantity.n * ol.product.weight.n).reduce(_ + _) val expectedKilograms = WeightInKilograms(expectedGrams.toDecimal / 1000) weight shouldBe expectedKilograms } Scenario("A transport document is created") { - Given("an order") - val order = completedOrder - val weight = weightOrder(order) + Given("a completed order") + val weight = weightOrder(completedOrder) When("one requests the transport document") - val td = createTransportDocument(order, weight) + val td = createTransportDocument(completedOrder, weight) Then("the correct transport document is generated") - td.customer shouldBe order.customer - td.deliveryLocation shouldBe order.deliveryLocation + td.customer shouldBe completedOrder.customer + td.deliveryLocation shouldBe completedOrder.deliveryLocation td.totalWeight shouldBe weight - td.transportDocumentLines shouldBe NonEmptyList.of( + td.transportDocumentLines.toList should contain allOf ( TransportDocumentLine(Quantity(100), Caciotta(1000)), TransportDocumentLine(Quantity(100), Caciotta(500)), ) From e58c81436ce1bca5ab75a377262c3e59afa95737 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 4 Aug 2022 15:07:18 +0200 Subject: [PATCH 183/329] fix: location uses decimals --- build.sbt | 1 + .../main/scala/dev/atedeg/mdm/clientorders/Actions.scala | 2 +- .../main/scala/dev/atedeg/mdm/clientorders/Types.scala | 4 ++-- .../scala/dev/atedeg/mdm/productionplanning/Types.scala | 2 +- .../scala/dev/atedeg/mdm/utils/LiteralConversions.scala | 8 ++++---- utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/build.sbt b/build.sbt index ded2677b..5c5d3bca 100644 --- a/build.sbt +++ b/build.sbt @@ -44,6 +44,7 @@ ThisBuild / scalafixDependencies ++= Seq( ) ThisBuild / semanticdbEnabled := true +ThisBuild / scalacOptions ++= Seq("-language:implicitConversions") lazy val startupTransition: State => State = "conventionalCommits" :: _ Global / onLoad := { startupTransition compose (Global / onLoad).value } diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 1766ac9e..87ba58f7 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -129,4 +129,4 @@ def createTransportDocument(completeOrder: CompletedOrder, weight: WeightInKilog val date = LocalDateTime.now TransportDocument(deliveryLocation, mambelliDeliveryLocation, customer, date, transportDocumentLines, weight) -private val mambelliDeliveryLocation = Location(Latitude(12), Longitude(44)) +private val mambelliDeliveryLocation = Location(Latitude(12.0), Longitude(44.0)) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala index d22dcd65..28107426 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala @@ -78,12 +78,12 @@ final case class Location(latitude: Latitude, longitude: Longitude) /** * A latitude specified in degrees. */ -final case class Latitude(value: NumberInClosedRange[-90, 90]) +final case class Latitude(value: DecimalInClosedRange[-90.0, 90.0]) /** * A longitude specified in degrees. */ -final case class Longitude(value: NumberInClosedRange[-180, 180]) +final case class Longitude(value: DecimalInClosedRange[-180.0, 180.0]) /** * Associates to each [[Product product]] its [[PriceInEuroCents unitary price]]. diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala index 98d116f1..8427c624 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala @@ -7,7 +7,7 @@ import cats.data.NonEmptyList import dev.atedeg.mdm.products.{ CheeseType, Product } import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.{ NonNegativeNumber, NumberInClosedRange, Plus, PositiveNumber, Times } +import dev.atedeg.mdm.utils.{ DecimalInClosedRange, NonNegativeNumber, Plus, PositiveNumber, Times } import dev.atedeg.mdm.utils.given /** diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala index f0cdd828..d06d5884 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/LiteralConversions.scala @@ -8,10 +8,10 @@ import eu.timepit.refined.numeric.Interval.Closed import eu.timepit.refined.predicates.all.{ NonNegative, Positive } @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion", "org.wartremover.warts.OptionPartial")) -inline implicit def intToNumberInRange[L <: Int & Singleton, U <: Int & Singleton]( - inline i: Int, -): NumberInClosedRange[L, U] = - inline if constValue[L] <= i && i <= constValue[U] then coerce(i) else compiletime.error("Not in the desired range") +inline implicit def doubleToNumberInRange[L <: Double & Singleton, U <: Double & Singleton]( + inline d: Double, +): DecimalInClosedRange[L, U] = + inline if constValue[L] <= d && d <= constValue[U] then coerce(d) else compiletime.error("Not in the desired range") @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion", "org.wartremover.warts.OptionPartial")) inline implicit def intToPositiveNumber(inline i: Int): PositiveNumber = diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala index 786df74a..25d0482f 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala @@ -11,7 +11,7 @@ import math.Ordering.Implicits.infixOrderingOps type PositiveNumber = Int Refined Positive type PositiveDecimal = Double Refined Positive -type NumberInClosedRange[L, U] = Int Refined Interval.Closed[L, U] +type DecimalInClosedRange[L, U] = Double Refined Interval.Closed[L, U] type NonNegativeNumber = Int Refined NonNegative type NonNegativeDecimal = Double Refined NonNegative From db4fda00a1096e94407776ab71805f3b9709071e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 4 Aug 2022 13:31:38 +0000 Subject: [PATCH 184/329] chore(release): 1.0.0-beta.7 [skip ci] # [1.0.0-beta.7](https://github.com/atedeg/mdm/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2022-08-04) ### Bug Fixes * location uses decimals ([e58c814](https://github.com/atedeg/mdm/commit/e58c81436ce1bca5ab75a377262c3e59afa95737)) * replace quantity with missing quantity ([8fa1f2d](https://github.com/atedeg/mdm/commit/8fa1f2d10f3c140eb0f98234793708073fb24a7b)) ### Features * add client orders errors ([7b0eb54](https://github.com/atedeg/mdm/commit/7b0eb5464032dd5f91f43ee7d5a1bc8c2f3b6f69)) * add client orders events ([fd66d84](https://github.com/atedeg/mdm/commit/fd66d8422201db21d56a1675a44ab25d1eef8473)) * add client orders types ([0167e81](https://github.com/atedeg/mdm/commit/0167e8139855e656517ea89422bedad5a6ef228e)) * add MissingQuantity class ([e7a23f7](https://github.com/atedeg/mdm/commit/e7a23f72e9c4babdcd89e9688fa5539cc59e8e53)) * add new outgoing event ([498bb39](https://github.com/atedeg/mdm/commit/498bb39de6160c2edf224d0eaccc57b3c9d83510)) * emit processed order event ([dac6483](https://github.com/atedeg/mdm/commit/dac64834f8bae9b8271f935b70f61ca5a46cb2ff)) * implement client-order actions ([a0f6240](https://github.com/atedeg/mdm/commit/a0f624037198bbff0fd3102936122e6165aba943)) --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8129bc4..7e676c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# [1.0.0-beta.7](https://github.com/atedeg/mdm/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2022-08-04) + + +### Bug Fixes + +* location uses decimals ([e58c814](https://github.com/atedeg/mdm/commit/e58c81436ce1bca5ab75a377262c3e59afa95737)) +* replace quantity with missing quantity ([8fa1f2d](https://github.com/atedeg/mdm/commit/8fa1f2d10f3c140eb0f98234793708073fb24a7b)) + + +### Features + +* add client orders errors ([7b0eb54](https://github.com/atedeg/mdm/commit/7b0eb5464032dd5f91f43ee7d5a1bc8c2f3b6f69)) +* add client orders events ([fd66d84](https://github.com/atedeg/mdm/commit/fd66d8422201db21d56a1675a44ab25d1eef8473)) +* add client orders types ([0167e81](https://github.com/atedeg/mdm/commit/0167e8139855e656517ea89422bedad5a6ef228e)) +* add MissingQuantity class ([e7a23f7](https://github.com/atedeg/mdm/commit/e7a23f72e9c4babdcd89e9688fa5539cc59e8e53)) +* add new outgoing event ([498bb39](https://github.com/atedeg/mdm/commit/498bb39de6160c2edf224d0eaccc57b3c9d83510)) +* emit processed order event ([dac6483](https://github.com/atedeg/mdm/commit/dac64834f8bae9b8271f935b70f61ca5a46cb2ff)) +* implement client-order actions ([a0f6240](https://github.com/atedeg/mdm/commit/a0f624037198bbff0fd3102936122e6165aba943)) + # [1.0.0-beta.6](https://github.com/atedeg/mdm/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-04) From adabeb5dc7ba3bad8053a34be707d33c1fe7c29f Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Thu, 4 Aug 2022 16:53:24 +0200 Subject: [PATCH 185/329] chore: return an either instead an option --- utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala index 25d0482f..9203d64f 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/Refined.scala @@ -45,10 +45,10 @@ given refinedEq[N: Eq, P]: Eq[N Refined P] with // Instances for the various numeric ops -extension [N](n: N) def refined[P: ValidFor[N]]: Option[N Refined P] = refineV[P](n).toOption +extension [N](n: N) def refined[P: ValidFor[N]]: Either[String, N Refined P] = refineV[P](n) @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) -def coerce[A, P](a: A)(using Validate[A, P]): A Refined P = a.refined.get +def coerce[A, P](a: A)(using Validate[A, P]): A Refined P = a.refined.toOption.get private type ValidFor[N] = [P] =>> Validate[N, P] From e5a36bef7114a655eb1d28fdb8b55795ed33cfed Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Thu, 4 Aug 2022 16:54:03 +0200 Subject: [PATCH 186/329] chore: define Read and Show type classes --- .../dev/atedeg/mdm/utils/serialization/StringOps.scala | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala new file mode 100644 index 00000000..9f9d5030 --- /dev/null +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.utils.serialization + +trait Show[A]: + def toShow(a: A): String + extension (a: A) def show: String = toShow(a) + +trait Read[A]: + def fromString(s: String): Either[String, A] + extension (s: String) def read: Either[String, A] = fromString(s) From 0aa51ba9700dcec387ad6a99e8787970c0e9adc6 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Thu, 4 Aug 2022 16:55:01 +0200 Subject: [PATCH 187/329] chore: implement given instance of Read and Show for Ingredient class --- .../products/utils/ReadShowInstances.scala | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala new file mode 100644 index 00000000..321b2a3f --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala @@ -0,0 +1,28 @@ +package dev.atedeg.mdm.products.utils + +import cats.syntax.all.* + +import dev.atedeg.mdm.products.Ingredient +import dev.atedeg.mdm.products.Ingredient.* +import dev.atedeg.mdm.utils.serialization.* + +object ReadShowInstancesOps: + + given Show[Ingredient] with + + override def toShow(i: Ingredient): String = i match + case Probiotics => "probiotics" + case Rennet => "rennet" + case Cream => "cream" + case Milk => "milk" + case Salt => "salt" + + given Read[Ingredient] with + + override def fromString(s: String): Either[String, Ingredient] = s match + case "probiotics" => Probiotics.asRight[String] + case "rennet" => Rennet.asRight[String] + case "cream" => Cream.asRight[String] + case "milk" => Milk.asRight[String] + case "salt" => Salt.asRight[String] + case _ => s"Unknown `Ingredient`: '$s'".asLeft[Ingredient] From 8035a9c0e4b74ad10eb5b265ea10baa9d63accbc Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Thu, 4 Aug 2022 16:55:22 +0200 Subject: [PATCH 188/329] feat: define DTOs for restocking b.c. --- .../dev/atedeg/mdm/restocking/dto/DTOs.scala | 5 +++ .../atedeg/mdm/restocking/dto/FromDTO.scala | 32 +++++++++++++++++++ .../dev/atedeg/mdm/restocking/dto/ToDTO.scala | 13 ++++++++ 3 files changed, 50 insertions(+) create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala new file mode 100644 index 00000000..c265079c --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala @@ -0,0 +1,5 @@ +package dev.atedeg.mdm.restocking.dto + +final case class OrderMilkDTO(quintalsOfMilk: Int) +final case class ProductionStartedDTO(quintalsOfIngredients: List[QuintalsOfIngredientDTO]) +final case class QuintalsOfIngredientDTO(quintals: Double, ingredient: String) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala new file mode 100644 index 00000000..1dfb2384 --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala @@ -0,0 +1,32 @@ +package dev.atedeg.mdm.restocking.dto + +import cats.syntax.all.* +import eu.timepit.refined.numeric.Positive +import eu.timepit.refined.predicates.all.NonNegative +import eu.timepit.refined.refineV + +import dev.atedeg.mdm.products.utils.ReadShowInstancesOps.given +import dev.atedeg.mdm.restocking.{ QuintalsOfIngredient, QuintalsOfMilk, WeightInQuintals } +import dev.atedeg.mdm.restocking.IncomingEvent.* +import dev.atedeg.mdm.utils.* + +extension (omDTO: OrderMilkDTO) + + def toDomain: Either[String, OrderMilk] = + omDTO.quintalsOfMilk.refined[Positive].map(QuintalsOfMilk.apply).map(OrderMilk.apply) + +extension (psDTO: ProductionStartedDTO) + + def toDomain: Either[String, ProductionStarted] = + psDTO.quintalsOfIngredients.toNel + .toRight("The quintals of ingredients list is empty") + .flatMap(_.traverse(_.toDomain)) + .map(ProductionStarted.apply) + +extension (qoiDTO: QuintalsOfIngredientDTO) + + def toDomain: Either[String, QuintalsOfIngredient] = + for { + weight <- qoiDTO.quintals.refined[Positive].map(WeightInQuintals.apply) + ingredient <- qoiDTO.ingredient.read + } yield QuintalsOfIngredient(weight, ingredient) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala new file mode 100644 index 00000000..d2ce4f23 --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala @@ -0,0 +1,13 @@ +package dev.atedeg.mdm.restocking.dto + +import dev.atedeg.mdm.products.utils.ReadShowInstancesOps.given +import dev.atedeg.mdm.restocking.IncomingEvent.* +import dev.atedeg.mdm.restocking.QuintalsOfIngredient + +extension (om: OrderMilk) def toDTO: OrderMilkDTO = OrderMilkDTO(om.quintals.quintals.value) + +extension (ps: ProductionStarted) + def toDTO: ProductionStartedDTO = ProductionStartedDTO(ps.ingredients.toList.map(_.toDTO)) + +extension (qoi: QuintalsOfIngredient) + def toDTO: QuintalsOfIngredientDTO = QuintalsOfIngredientDTO(qoi.quintals.n.value, qoi.ingredient.show) From 57b88479cafbc2eef73c1ffa77bdfec3fd61446b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 6 Aug 2022 13:36:45 +0000 Subject: [PATCH 189/329] chore(release): 1.0.0-beta.8 [skip ci] # [1.0.0-beta.8](https://github.com/atedeg/mdm/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2022-08-06) ### Features * define DTOs for restocking b.c. ([8035a9c](https://github.com/atedeg/mdm/commit/8035a9c0e4b74ad10eb5b265ea10baa9d63accbc)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e676c90..14fb35cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.0.0-beta.8](https://github.com/atedeg/mdm/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2022-08-06) + + +### Features + +* define DTOs for restocking b.c. ([8035a9c](https://github.com/atedeg/mdm/commit/8035a9c0e4b74ad10eb5b265ea10baa9d63accbc)) + # [1.0.0-beta.7](https://github.com/atedeg/mdm/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2022-08-04) From 39c965ad2e30d91ea0ae98033cba4ffa72d09b08 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 15:34:12 +0200 Subject: [PATCH 190/329] chore: add more rules for better syntax --- .scalafmt.conf | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 80256b53..3dd67bcc 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -5,12 +5,8 @@ maxColumn = 120 includeCurlyBraceInSelectChains = false # Newlines -newlines.penalizeSingleSelectMultiArgList = false -newlines.topLevelStatementBlankLines = [ - { - blanks { before = 1 } - } -] +newlines.alwaysBeforeElseAfterCurlyIf = false +newlines.beforeCurlyLambdaParams = never # Docstring docstrings.style = Asterisk @@ -29,7 +25,10 @@ align.preset = none align.openParenDefnSite = false # Rewrite -rewrite.rules = [SortModifiers, PreferCurlyFors, Imports] +rewrite.scala3.convertToNewSyntax = true +rewrite.scala3.removeOptionalBraces = yes +rewrite.rules = [SortModifiers, PreferCurlyFors, Imports, RedundantBraces] +rewrite.redundantBraces.stringInterpolation = true rewrite.imports.sort = scalastyle rewrite.imports.groups = [ ["javax?\\..*", "scala\\..*"], @@ -45,5 +44,3 @@ rewrite.sortModifiers.order = [ spaces.inImportCurlyBraces = true trailingCommas = always - -rewrite.scala3.convertToNewSyntax = true From 13f7289d95ed6064edddd81c2f99783e63697a61 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 15:34:42 +0200 Subject: [PATCH 191/329] build: add quality assurance command alias --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index 5c5d3bca..0a35f55d 100644 --- a/build.sbt +++ b/build.sbt @@ -61,6 +61,7 @@ val commonSettings = Seq( ) addCommandAlias("ubidocGenerate", "clean; unidoc; ubidoc; clean; unidoc") +addCommandAlias("qaCheck", "scalafmtCheckAll; scalafixAll --check; wartremoverInspect") lazy val root = project .in(file(".")) From d6633ee82db1158cf6378906a251f7349e29b77b Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 15:35:42 +0200 Subject: [PATCH 192/329] ci: collapse steps for quality assurance in order to reduce CI times --- .github/workflows/ci.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8f1dd96..f1debef4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,14 +35,8 @@ jobs: export SCALA_VERSION=`sbt --no-colors --error 'set aggregate := false; print scalaVersion'` echo "::set-output name=scala-version::$SCALA_VERSION" - - name: Scalafmt - run: sbt scalafmtCheckAll - - - name: Scalafix - run: sbt 'scalafixAll --check' - - - name: WartRemover - run: sbt wartremoverInspect + - name: Quality Assurance + run: sbt qaCheck - name: Test run: sbt test From cc408f1b636deea4795cdfb6f4c16c438d7934f5 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 15:36:03 +0200 Subject: [PATCH 193/329] style: reformat files with new scalafmt rules --- .../main/scala/dev/atedeg/mdm/clientorders/Actions.scala | 8 ++++---- .../dev/atedeg/mdm/milkplanning/types/ActionsTest.scala | 6 ++---- .../scala/dev/atedeg/mdm/productionplanning/Actions.scala | 7 +++---- .../main/scala/dev/atedeg/mdm/production/Actions.scala | 4 ++-- .../mdm/production/utils/QuintalsOfIngredientOps.scala | 1 - .../src/test/scala/dev/atedeg/mdm/production/Tests.scala | 6 ++---- .../src/main/scala/dev/atedeg/mdm/stocking/Actions.scala | 4 ++-- .../src/test/scala/dev/atedeg/mdm/stocking/Tests.scala | 6 ++---- 8 files changed, 17 insertions(+), 25 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 87ba58f7..9f0add4c 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -64,14 +64,14 @@ def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad](quantity inProgressOrder: InProgressOrder, ): M[InProgressOrder] = val InProgressOrder(id, ol, customer, dd, dl, totalPrice) = inProgressOrder - for { + for orderLine <- ol.find(hasProduct(product)).ifMissingRaise(ProductNotInOrder(product)) updatedLine <- addToLine(orderLine)(quantity) newOrderLines = ol.map { case Incomplete(_, _, `product`, _) => updatedLine case l @ _ => l } - } yield InProgressOrder(id, newOrderLines, customer, dd, dl, totalPrice) + yield InProgressOrder(id, newOrderLines, customer, dd, dl, totalPrice) private def hasProduct(product: Product)(ol: InProgressOrderLine): Boolean = ol match case Incomplete(_, _, p, _) => p === product @@ -95,10 +95,10 @@ def completeOrder[Result[_]: CanRaise[OrderCompletionError]: Monad]( inProgressOrder: InProgressOrder, ): Result[CompletedOrder] = val InProgressOrder(id, ols, customer, dd, dl, totalPrice) = inProgressOrder - for { + for completedOrderLines <- getCompletedOrderLines(ols).ifMissingRaise(OrderNotComplete()) completeOrderLines = completedOrderLines.map(ol => CompleteOrderLine(ol.quantity, ol.product, ol.price)) - } yield CompletedOrder(id, completeOrderLines, customer, dd, dl, totalPrice) + yield CompletedOrder(id, completeOrderLines, customer, dd, dl, totalPrice) private def getCompletedOrderLines(orderLines: NonEmptyList[InProgressOrderLine]): Option[NonEmptyList[Complete]] = @tailrec diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index 0abdb53e..3c6d3bca 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -19,7 +19,7 @@ import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given import dev.atedeg.mdm.utils.monads.* -trait Mocks { +trait Mocks: val recipeBook: RecipeBook = Map( CheeseType.Squacquerone -> Yield(5.55), @@ -28,10 +28,9 @@ trait Mocks { CheeseType.Stracchino -> Yield(6.55), CheeseType.Caciotta -> Yield(8.33), ) -} @SuppressWarnings(Array("org.wartremover.warts.Any")) -class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { +class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Feature("Estimate the quintals of milk needed for the following week") { Scenario("Raffaella wants to estimate the quintals of milk") { @@ -68,4 +67,3 @@ class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with M events.map(_.n) should contain(estimation) } } -} diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index 594e3d0f..1863b0ef 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -29,23 +29,22 @@ def createProductionPlan[M[_]: Monad: Emits[ProductionPlanReady]: CanEmit[OrderD )( previousProductionPlan: ProductionPlan, orders: List[Order], -): M[ProductionPlan] = for { +): M[ProductionPlan] = for _ <- orders.traverse(checkDeliverabilityOfOrder(cheeseTypeRipeningDays)) productsToProduce = magicAIProductsToProduceEstimator(orders, previousProductionPlan, cheeseTypeRipeningDays, stock) productionPlan = ProductionPlan(productsToProduce) _ <- emit(ProductionPlanReady(productionPlan): ProductionPlanReady) -} yield productionPlan +yield productionPlan private def checkDeliverabilityOfOrder[M[_]: Monad: CanEmit[OrderDelayed]]( cheeseTypeRipeningDays: CheeseTypeRipeningDays, -)(order: Order): M[Unit] = { +)(order: Order): M[Unit] = val ripeningDays = order.orderedProducts.map(_.product.cheeseType).map(cheeseTypeRipeningDays(_)) val isDelayed = ripeningDays.map(productionInTime(_, order.requiredBy)).exists(_ === OrderStatus.Delayed) when(isDelayed) { val deliveryDate = newDeliveryDate(RipeningDays(ripeningDays.map(_.days).reduceLeft(max))) emit(OrderDelayed(order.orderdID, deliveryDate): OrderDelayed) } -} private def magicAIProductsToProduceEstimator( orders: List[Order], diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index 1893fdde..40393646 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -31,12 +31,12 @@ def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[StartProduction] ): M[Production.InProgress] = val typeToProduce = production.productToProduce.cheeseType val gramsOfSingleUnit = production.productToProduce.weight - for { + for recipe <- recipeBook(typeToProduce) ifMissingRaise MissingRecipe(typeToProduce) quintalsToProduce = (production.unitsToProduce.n * gramsOfSingleUnit.n).toDecimal / 100_000 neededIngredients = recipe.lines.map(_ * quintalsToProduce) _ <- emit(StartProduction(neededIngredients): StartProduction) - } yield Production.InProgress(production.ID, production.productToProduce, production.unitsToProduce) + yield Production.InProgress(production.ID, production.productToProduce, production.unitsToProduce) /** * Ends a [[Production.InProgress production]] by assigning it a [[BatchID batch ID]]. diff --git a/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala index 752c3e5a..1dad5604 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/utils/QuintalsOfIngredientOps.scala @@ -8,7 +8,6 @@ import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.given extension (q: QuintalsOfIngredient) - @targetName("quintalsOfIngredientTimesDecimal") def *(n: PositiveDecimal) = QuintalsOfIngredient(q.quintals * n.quintals, q.ingredient) diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala index e269d73c..9784f874 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala @@ -22,13 +22,12 @@ import dev.atedeg.mdm.utils.monads.given extension (n: PositiveNumber) def ofProd(p: Product): ProductionPlanItem = ProductionPlanItem(p, NumberOfUnits(n)) -trait Mocks { +trait Mocks: private val productionID = ProductionID(UUID.randomUUID) val production: Production.ToStart = Production.ToStart(productionID, Product.Caciotta(500), NumberOfUnits(10_000)) val allIngredients: NonEmptyList[Ingredient] = NonEmptyList.of(Milk, Cream, Rennet, Salt, Probiotics) -} -class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { +class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Feature("Production management") { Scenario("A production plan is handled") { @@ -95,4 +94,3 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { events should contain(ProductionEnded(result.ID, result.batchID)) } } -} diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 2cfb129e..586ef287 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -54,13 +54,13 @@ def labelProduct[M[_]: Monad: CanRaise[WeightNotInRange]: CanEmit[ProductStocked actualWeight: Grams, ): M[LabelledProduct] = val closestAllowedWeight = batch.cheeseType.allowedWeights.closestTo(actualWeight) - for { + for product <- batch.cheeseType .withWeight(closeTo(actualWeight)) .ifMissingRaise(WeightNotInRange(closestAllowedWeight, actualWeight): WeightNotInRange) labelledProduct = LabelledProduct(product, AvailableQuantity(1), batch.id) _ <- emit(ProductStocked(labelledProduct): ProductStocked) - } yield labelledProduct + yield labelledProduct private def closeTo(weight: Grams)(n: Int): Boolean = weight.n.value.toDouble isInRange (n.toDouble +- 5.percent) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index d8477e8f..48af3395 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -15,7 +15,7 @@ import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.monads.* -trait Mocks { +trait Mocks: val batchID: BatchID = BatchID(UUID.randomUUID()) val cheeseType: CheeseType = CheeseType.Squacquerone val squacquerone: Product = Product.Squacquerone(100) @@ -24,9 +24,8 @@ trait Mocks { val stracchino: Product = Product.Stracchino(250) val caciotta: Product = Product.Caciotta(500) val readyForQA: Batch.ReadyForQualityAssurance = Batch.ReadyForQualityAssurance(batchID, cheeseType) -} -class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { +class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Feature("Missing stock") { Scenario("There are missing products from the desired stock") { @@ -144,4 +143,3 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks { events shouldBe empty } } -} From b64c00d7a0aafd0a7c52c5347b1e2ea3d18c8d20 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sat, 6 Aug 2022 18:20:15 +0200 Subject: [PATCH 194/329] chore(utils): add DTO typeclass --- .../mdm/utils/serialization/DTOOps.scala | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTOOps.scala diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTOOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTOOps.scala new file mode 100644 index 00000000..a453c181 --- /dev/null +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTOOps.scala @@ -0,0 +1,58 @@ +package dev.atedeg.mdm.utils.serialization + +import java.time.{ LocalDate, LocalDateTime } +import java.time.format.DateTimeFormatter +import java.util.UUID +import scala.util.Try + +import cats.data.NonEmptyList +import cats.syntax.all.* +import eu.timepit.refined.api.Refined + +import dev.atedeg.mdm.utils.* + +trait DTO[E, D]: + def elemToDto(e: E): D + def dtoToElem(dto: D): Either[String, E] + + extension (e: E) def toDTO: D = elemToDto(e) + extension (dto: D) def toDomain: Either[String, E] = dtoToElem(dto) + +object DTO: + given listDTO[E, D](using DTO[E, D]): DTO[List[E], List[D]] with + override def dtoToElem(dto: List[D]): Either[String, List[E]] = dto.traverse(_.toDomain) + override def elemToDto(e: List[E]): List[D] = e.map(_.toDTO) + + given nonEmptyListDTO[E, D](using DTO[E, D]): DTO[NonEmptyList[E], List[D]] with + override def dtoToElem(dto: List[D]): Either[String, NonEmptyList[E]] = + dto.toNel.toRight("Got an empty list, it should contain at least one element").flatMap(_.traverse(_.toDomain)) + + override def elemToDto(e: NonEmptyList[E]): List[D] = e.toList.map(_.toDTO) + + given DTO[UUID, String] with + override def dtoToElem(dto: String): Either[String, UUID] = + Try(UUID.fromString(dto)).toEither.leftMap(_ => s"Invalid UUID: $dto") + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + override def elemToDto(e: UUID): String = e.toString + + given DTO[LocalDate, String] with + override def dtoToElem(dto: String): Either[String, LocalDate] = + Try(LocalDate.parse(dto, DateTimeFormatter.ISO_LOCAL_DATE)).toEither.leftMap(_ => s"Invalid date: $dto") + override def elemToDto(e: LocalDate): String = e.format(DateTimeFormatter.ISO_LOCAL_DATE) + + given DTO[LocalDateTime, String] with + override def dtoToElem(dto: String): Either[String, LocalDateTime] = + Try(LocalDateTime.parse(dto, DateTimeFormatter.ISO_LOCAL_DATE_TIME)).toEither.leftMap(_ => s"Invalid date: $dto") + override def elemToDto(e: LocalDateTime): String = e.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + given refinedDTO[E, D, P: ValidFor[E]](using d: DTO[E, D]): DTO[E Refined P, D] with + override def dtoToElem(dto: D): Either[String, E Refined P] = d.dtoToElem(dto).flatMap(_.refined[P]) + override def elemToDto(e: E Refined P): D = e.value.toDTO + + given DTO[Int, Int] = idDTO + given DTO[Double, Double] = idDTO + given DTO[String, String] = idDTO + + private def idDTO[T]: DTO[T, T] = new DTO[T, T]: + override def dtoToElem(dto: T): Either[String, T] = dto.asRight[String] + override def elemToDto(e: T): T = e From be3bc33bad8139bcc4c9b752bec8b6b892facead Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sat, 6 Aug 2022 23:11:38 +0200 Subject: [PATCH 195/329] style: reformat build.sbt --- build.sbt | 2 +- ...howInstances.scala => IngredientDTO.scala} | 24 ++++++-------- .../atedeg/mdm/restocking/dto/FromDTO.scala | 32 ------------------- .../dev/atedeg/mdm/restocking/dto/ToDTO.scala | 13 -------- .../serialization/{DTOOps.scala => DTO.scala} | 4 +-- .../mdm/utils/serialization/StringOps.scala | 9 ------ 6 files changed, 13 insertions(+), 71 deletions(-) rename products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/{ReadShowInstances.scala => IngredientDTO.scala} (73%) delete mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala delete mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala rename utils/src/main/scala/dev/atedeg/mdm/utils/serialization/{DTOOps.scala => DTO.scala} (93%) delete mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala diff --git a/build.sbt b/build.sbt index 0a35f55d..b416229c 100644 --- a/build.sbt +++ b/build.sbt @@ -47,7 +47,7 @@ ThisBuild / semanticdbEnabled := true ThisBuild / scalacOptions ++= Seq("-language:implicitConversions") lazy val startupTransition: State => State = "conventionalCommits" :: _ -Global / onLoad := { startupTransition compose (Global / onLoad).value } +Global / onLoad := startupTransition compose (Global / onLoad).value val commonSettings = Seq( libraryDependencies ++= Seq( diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala similarity index 73% rename from products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala rename to products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala index 321b2a3f..4d952b51 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala @@ -6,23 +6,19 @@ import dev.atedeg.mdm.products.Ingredient import dev.atedeg.mdm.products.Ingredient.* import dev.atedeg.mdm.utils.serialization.* -object ReadShowInstancesOps: - - given Show[Ingredient] with - - override def toShow(i: Ingredient): String = i match - case Probiotics => "probiotics" - case Rennet => "rennet" - case Cream => "cream" - case Milk => "milk" - case Salt => "salt" - - given Read[Ingredient] with - - override def fromString(s: String): Either[String, Ingredient] = s match +object IngredientDTO: + given DTO[Ingredient, String] with + override def dtoToElem(dto: String): Either[String, Ingredient] = dto match case "probiotics" => Probiotics.asRight[String] case "rennet" => Rennet.asRight[String] case "cream" => Cream.asRight[String] case "milk" => Milk.asRight[String] case "salt" => Salt.asRight[String] case _ => s"Unknown `Ingredient`: '$s'".asLeft[Ingredient] + + override def elemToDto(e: Ingredient): String = e match + case Probiotics => "probiotics" + case Rennet => "rennet" + case Cream => "cream" + case Milk => "milk" + case Salt => "salt" diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala deleted file mode 100644 index 1dfb2384..00000000 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala +++ /dev/null @@ -1,32 +0,0 @@ -package dev.atedeg.mdm.restocking.dto - -import cats.syntax.all.* -import eu.timepit.refined.numeric.Positive -import eu.timepit.refined.predicates.all.NonNegative -import eu.timepit.refined.refineV - -import dev.atedeg.mdm.products.utils.ReadShowInstancesOps.given -import dev.atedeg.mdm.restocking.{ QuintalsOfIngredient, QuintalsOfMilk, WeightInQuintals } -import dev.atedeg.mdm.restocking.IncomingEvent.* -import dev.atedeg.mdm.utils.* - -extension (omDTO: OrderMilkDTO) - - def toDomain: Either[String, OrderMilk] = - omDTO.quintalsOfMilk.refined[Positive].map(QuintalsOfMilk.apply).map(OrderMilk.apply) - -extension (psDTO: ProductionStartedDTO) - - def toDomain: Either[String, ProductionStarted] = - psDTO.quintalsOfIngredients.toNel - .toRight("The quintals of ingredients list is empty") - .flatMap(_.traverse(_.toDomain)) - .map(ProductionStarted.apply) - -extension (qoiDTO: QuintalsOfIngredientDTO) - - def toDomain: Either[String, QuintalsOfIngredient] = - for { - weight <- qoiDTO.quintals.refined[Positive].map(WeightInQuintals.apply) - ingredient <- qoiDTO.ingredient.read - } yield QuintalsOfIngredient(weight, ingredient) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala deleted file mode 100644 index d2ce4f23..00000000 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala +++ /dev/null @@ -1,13 +0,0 @@ -package dev.atedeg.mdm.restocking.dto - -import dev.atedeg.mdm.products.utils.ReadShowInstancesOps.given -import dev.atedeg.mdm.restocking.IncomingEvent.* -import dev.atedeg.mdm.restocking.QuintalsOfIngredient - -extension (om: OrderMilk) def toDTO: OrderMilkDTO = OrderMilkDTO(om.quintals.quintals.value) - -extension (ps: ProductionStarted) - def toDTO: ProductionStartedDTO = ProductionStartedDTO(ps.ingredients.toList.map(_.toDTO)) - -extension (qoi: QuintalsOfIngredient) - def toDTO: QuintalsOfIngredientDTO = QuintalsOfIngredientDTO(qoi.quintals.n.value, qoi.ingredient.show) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTOOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala similarity index 93% rename from utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTOOps.scala rename to utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala index a453c181..3bfb9e58 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTOOps.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala @@ -15,8 +15,8 @@ trait DTO[E, D]: def elemToDto(e: E): D def dtoToElem(dto: D): Either[String, E] - extension (e: E) def toDTO: D = elemToDto(e) - extension (dto: D) def toDomain: Either[String, E] = dtoToElem(dto) +extension [E](e: E) def toDTO[D](using d: DTO[E, D]) = d.elemToDto(e) +extension [D](dto: D) def toDomain[E](using d: DTO[E, D]): Either[String, E] = d.dtoToElem(dto) object DTO: given listDTO[E, D](using DTO[E, D]): DTO[List[E], List[D]] with diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala deleted file mode 100644 index 9f9d5030..00000000 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala +++ /dev/null @@ -1,9 +0,0 @@ -package dev.atedeg.mdm.utils.serialization - -trait Show[A]: - def toShow(a: A): String - extension (a: A) def show: String = toShow(a) - -trait Read[A]: - def fromString(s: String): Either[String, A] - extension (s: String) def read: Either[String, A] = fromString(s) From 7a27cf6e7c1d8cc9009ce2cac3a5400a57fc3c7f Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sat, 6 Aug 2022 23:13:51 +0200 Subject: [PATCH 196/329] chore: fix string interpolation --- .../scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala index 4d952b51..8d19a504 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala @@ -14,7 +14,7 @@ object IngredientDTO: case "cream" => Cream.asRight[String] case "milk" => Milk.asRight[String] case "salt" => Salt.asRight[String] - case _ => s"Unknown `Ingredient`: '$s'".asLeft[Ingredient] + case _ => s"Unknown `Ingredient`: '$dto'".asLeft[Ingredient] override def elemToDto(e: Ingredient): String = e match case Probiotics => "probiotics" From 509ab9abfc25d0031d1fdfddd30a89886b36392d Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sat, 6 Aug 2022 23:14:29 +0200 Subject: [PATCH 197/329] chore: add autogeneration for case classes --- .../atedeg/mdm/utils/serialization/DTO.scala | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala index 3bfb9e58..5a71d480 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala @@ -3,6 +3,8 @@ package dev.atedeg.mdm.utils.serialization import java.time.{ LocalDate, LocalDateTime } import java.time.format.DateTimeFormatter import java.util.UUID +import scala.compiletime.* +import scala.deriving.* import scala.util.Try import cats.data.NonEmptyList @@ -15,10 +17,10 @@ trait DTO[E, D]: def elemToDto(e: E): D def dtoToElem(dto: D): Either[String, E] -extension [E](e: E) def toDTO[D](using d: DTO[E, D]) = d.elemToDto(e) -extension [D](dto: D) def toDomain[E](using d: DTO[E, D]): Either[String, E] = d.dtoToElem(dto) - object DTO: + extension [E](e: E) def toDTO[D](using d: DTO[E, D]) = d.elemToDto(e) + extension [D](dto: D) def toDomain[E](using d: DTO[E, D]): Either[String, E] = d.dtoToElem(dto) + given listDTO[E, D](using DTO[E, D]): DTO[List[E], List[D]] with override def dtoToElem(dto: List[D]): Either[String, List[E]] = dto.traverse(_.toDomain) override def elemToDto(e: List[E]): List[D] = e.map(_.toDTO) @@ -53,6 +55,42 @@ object DTO: given DTO[Double, Double] = idDTO given DTO[String, String] = idDTO - private def idDTO[T]: DTO[T, T] = new DTO[T, T]: + def idDTO[T]: DTO[T, T] = new DTO[T, T]: override def dtoToElem(dto: T): Either[String, T] = dto.asRight[String] override def elemToDto(e: T): T = e + + inline private def numberOfFields[A](inline p: Mirror.ProductOf[A]): Int = constValue[Tuple.Size[p.MirroredElemTypes]] + @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) + private def firstField[T, X](t: T): X = t.asInstanceOf[Product].productElement(0).asInstanceOf[X] + + private type First[T <: Tuple] = Tuple.Elem[T, 0] + + inline def caseClassDTO[E, D](using p: Mirror.ProductOf[E]): DTO[E, D] = + inline numberOfFields(p) match + case 1 => + type t = First[p.MirroredElemTypes] + val instance: DTO[t, D] = summonInline[DTO[t, D]] + new DTO[E, D]: + def elemToDto(e: E): D = instance.elemToDto(firstField(e)) + def dtoToElem(dto: D): Either[String, E] = instance.dtoToElem(dto).map(e => p.fromProduct(e *: EmptyTuple)) + case _ => compiletime.error("Can only derive for case classes with only one field") + + inline def interCaseClassDTO[C1, C2](using p1: Mirror.ProductOf[C1])(using p2: Mirror.ProductOf[C2]): DTO[C1, C2] = + inline if numberOfFields(p1) != numberOfFields(p2) + then compiletime.error("Can only derive DTO for case classes with same number of fields") + else + type DTOFromTuple[T] = T match + case (t1 *: t2 *: EmptyTuple) => DTO[t1, t2] + type DTOs = Tuple.Map[Tuple.Zip[p1.MirroredElemTypes, p2.MirroredElemTypes], DTOFromTuple] + val instances = summonAll[DTOs].toList.asInstanceOf[List[DTO[Any, Any]]] + new DTO[C1, C2]: + override def elemToDto(e: C1): C2 = + val fields = e.asInstanceOf[Product].productIterator.zip(instances).map(fi => fi._2.elemToDto(fi._1)).toArray + p2.fromProduct(Tuple.fromArray(fields)) + override def dtoToElem(dto: C2): Either[String, C1] = dto + .asInstanceOf[Product] + .productIterator + .zip(instances) + .toList + .traverse(fi => fi._2.dtoToElem(fi._1)) + .map(l => p1.fromProduct(Tuple.fromArray(l.toArray))) From 015e4d14b1ca060f5f3d3daa5869543886237f7a Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sat, 6 Aug 2022 23:15:01 +0200 Subject: [PATCH 198/329] chore: auto generate DTO instances for restocking DTOs --- .../dev/atedeg/mdm/restocking/dto/DTOs.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala index c265079c..9509a8fc 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala @@ -1,5 +1,21 @@ package dev.atedeg.mdm.restocking.dto +import dev.atedeg.mdm.products.utils.IngredientDTO.given +import dev.atedeg.mdm.restocking.* +import dev.atedeg.mdm.restocking.IncomingEvent.* +import dev.atedeg.mdm.utils.serialization.DTO + final case class OrderMilkDTO(quintalsOfMilk: Int) final case class ProductionStartedDTO(quintalsOfIngredients: List[QuintalsOfIngredientDTO]) final case class QuintalsOfIngredientDTO(quintals: Double, ingredient: String) + +object OrderMilkDTO: + given DTO[OrderMilk, OrderMilkDTO] = DTO.interCaseClassDTO + private given DTO[QuintalsOfMilk, Int] = DTO.caseClassDTO + +object ProductionStartedDTO: + given DTO[ProductionStarted, ProductionStartedDTO] = DTO.interCaseClassDTO + +object QuintalsOfIngredientDTO: + given DTO[QuintalsOfIngredient, QuintalsOfIngredientDTO] = DTO.interCaseClassDTO + private given DTO[WeightInQuintals, Double] = DTO.caseClassDTO From 83848acefc49abe12e45db5298f2f7c08f63ebb3 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sat, 6 Aug 2022 23:26:55 +0200 Subject: [PATCH 199/329] refactor: change DTO structure --- .../dev/atedeg/mdm/restocking/dto/DTOs.scala | 12 +++--- .../atedeg/mdm/utils/serialization/DTO.scala | 37 +++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala index 9509a8fc..ceb90fb3 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala @@ -4,18 +4,20 @@ import dev.atedeg.mdm.products.utils.IngredientDTO.given import dev.atedeg.mdm.restocking.* import dev.atedeg.mdm.restocking.IncomingEvent.* import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOGenerators.* +import dev.atedeg.mdm.utils.serialization.DTOOps.* final case class OrderMilkDTO(quintalsOfMilk: Int) final case class ProductionStartedDTO(quintalsOfIngredients: List[QuintalsOfIngredientDTO]) final case class QuintalsOfIngredientDTO(quintals: Double, ingredient: String) object OrderMilkDTO: - given DTO[OrderMilk, OrderMilkDTO] = DTO.interCaseClassDTO - private given DTO[QuintalsOfMilk, Int] = DTO.caseClassDTO + given DTO[OrderMilk, OrderMilkDTO] = interCaseClassDTO + private given DTO[QuintalsOfMilk, Int] = caseClassDTO object ProductionStartedDTO: - given DTO[ProductionStarted, ProductionStartedDTO] = DTO.interCaseClassDTO + given DTO[ProductionStarted, ProductionStartedDTO] = interCaseClassDTO object QuintalsOfIngredientDTO: - given DTO[QuintalsOfIngredient, QuintalsOfIngredientDTO] = DTO.interCaseClassDTO - private given DTO[WeightInQuintals, Double] = DTO.caseClassDTO + given DTO[QuintalsOfIngredient, QuintalsOfIngredientDTO] = interCaseClassDTO + private given DTO[WeightInQuintals, Double] = caseClassDTO diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala index 5a71d480..a45cab1c 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala @@ -17,53 +17,56 @@ trait DTO[E, D]: def elemToDto(e: E): D def dtoToElem(dto: D): Either[String, E] -object DTO: - extension [E](e: E) def toDTO[D](using d: DTO[E, D]) = d.elemToDto(e) +object DTOOps: extension [D](dto: D) def toDomain[E](using d: DTO[E, D]): Either[String, E] = d.dtoToElem(dto) + extension [E](e: E) def toDTO[D](using d: DTO[E, D]) = d.elemToDto(e) + +object DTO: + import DTOGenerators.* + import DTOOps.* + + given DTO[Int, Int] = idDTO + given DTO[Double, Double] = idDTO + given DTO[String, String] = idDTO given listDTO[E, D](using DTO[E, D]): DTO[List[E], List[D]] with override def dtoToElem(dto: List[D]): Either[String, List[E]] = dto.traverse(_.toDomain) override def elemToDto(e: List[E]): List[D] = e.map(_.toDTO) given nonEmptyListDTO[E, D](using DTO[E, D]): DTO[NonEmptyList[E], List[D]] with + override def elemToDto(e: NonEmptyList[E]): List[D] = e.toList.map(_.toDTO) override def dtoToElem(dto: List[D]): Either[String, NonEmptyList[E]] = dto.toNel.toRight("Got an empty list, it should contain at least one element").flatMap(_.traverse(_.toDomain)) - override def elemToDto(e: NonEmptyList[E]): List[D] = e.toList.map(_.toDTO) - given DTO[UUID, String] with - override def dtoToElem(dto: String): Either[String, UUID] = - Try(UUID.fromString(dto)).toEither.leftMap(_ => s"Invalid UUID: $dto") @SuppressWarnings(Array("org.wartremover.warts.ToString")) override def elemToDto(e: UUID): String = e.toString + override def dtoToElem(dto: String): Either[String, UUID] = + Try(UUID.fromString(dto)).toEither.leftMap(_ => s"Invalid UUID: $dto") given DTO[LocalDate, String] with + override def elemToDto(e: LocalDate): String = e.format(DateTimeFormatter.ISO_LOCAL_DATE) override def dtoToElem(dto: String): Either[String, LocalDate] = Try(LocalDate.parse(dto, DateTimeFormatter.ISO_LOCAL_DATE)).toEither.leftMap(_ => s"Invalid date: $dto") - override def elemToDto(e: LocalDate): String = e.format(DateTimeFormatter.ISO_LOCAL_DATE) given DTO[LocalDateTime, String] with + override def elemToDto(e: LocalDateTime): String = e.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) override def dtoToElem(dto: String): Either[String, LocalDateTime] = Try(LocalDateTime.parse(dto, DateTimeFormatter.ISO_LOCAL_DATE_TIME)).toEither.leftMap(_ => s"Invalid date: $dto") - override def elemToDto(e: LocalDateTime): String = e.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) given refinedDTO[E, D, P: ValidFor[E]](using d: DTO[E, D]): DTO[E Refined P, D] with - override def dtoToElem(dto: D): Either[String, E Refined P] = d.dtoToElem(dto).flatMap(_.refined[P]) override def elemToDto(e: E Refined P): D = e.value.toDTO + override def dtoToElem(dto: D): Either[String, E Refined P] = d.dtoToElem(dto).flatMap(_.refined[P]) - given DTO[Int, Int] = idDTO - given DTO[Double, Double] = idDTO - given DTO[String, String] = idDTO - +object DTOGenerators: def idDTO[T]: DTO[T, T] = new DTO[T, T]: - override def dtoToElem(dto: T): Either[String, T] = dto.asRight[String] override def elemToDto(e: T): T = e + override def dtoToElem(dto: T): Either[String, T] = dto.asRight[String] - inline private def numberOfFields[A](inline p: Mirror.ProductOf[A]): Int = constValue[Tuple.Size[p.MirroredElemTypes]] @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) private def firstField[T, X](t: T): X = t.asInstanceOf[Product].productElement(0).asInstanceOf[X] - private type First[T <: Tuple] = Tuple.Elem[T, 0] + inline private def numberOfFields[A](inline p: Mirror.ProductOf[A]): Int = constValue[Tuple.Size[p.MirroredElemTypes]] inline def caseClassDTO[E, D](using p: Mirror.ProductOf[E]): DTO[E, D] = inline numberOfFields(p) match @@ -72,6 +75,7 @@ object DTO: val instance: DTO[t, D] = summonInline[DTO[t, D]] new DTO[E, D]: def elemToDto(e: E): D = instance.elemToDto(firstField(e)) + def dtoToElem(dto: D): Either[String, E] = instance.dtoToElem(dto).map(e => p.fromProduct(e *: EmptyTuple)) case _ => compiletime.error("Can only derive for case classes with only one field") @@ -87,6 +91,7 @@ object DTO: override def elemToDto(e: C1): C2 = val fields = e.asInstanceOf[Product].productIterator.zip(instances).map(fi => fi._2.elemToDto(fi._1)).toArray p2.fromProduct(Tuple.fromArray(fields)) + override def dtoToElem(dto: C2): Either[String, C1] = dto .asInstanceOf[Product] .productIterator From 639659c49e51c0e545665d7cb1e9c1dc92b76672 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sun, 7 Aug 2022 11:24:37 +0200 Subject: [PATCH 200/329] build: add scalacheck --- build.sbt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sbt b/build.sbt index b416229c..a15645df 100644 --- a/build.sbt +++ b/build.sbt @@ -53,6 +53,8 @@ val commonSettings = Seq( libraryDependencies ++= Seq( "org.scalactic" %% "scalactic" % scalaTestVersion, "org.scalatest" %% "scalatest" % scalaTestVersion % "test", + "org.scalatest" %% "scalatest-propspec" % "3.2.13" % "test", + "org.scalatestplus" %% "scalacheck-1-16" % "3.2.12.0" % "test", "eu.timepit" %% "refined" % "0.10.1", "org.typelevel" %% "cats-core" % "2.8.0", "org.typelevel" %% "cats-mtl" % "1.3.0", From 3e1321ad15ae0c1fdca14a16ca93782cd5c54c01 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sun, 7 Aug 2022 11:24:54 +0200 Subject: [PATCH 201/329] test: add tests --- .../mdm/utils/serialization/DTOTest.scala | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 utils/src/test/scala/dev/atedeg/mdm/utils/serialization/DTOTest.scala diff --git a/utils/src/test/scala/dev/atedeg/mdm/utils/serialization/DTOTest.scala b/utils/src/test/scala/dev/atedeg/mdm/utils/serialization/DTOTest.scala new file mode 100644 index 00000000..33772c8e --- /dev/null +++ b/utils/src/test/scala/dev/atedeg/mdm/utils/serialization/DTOTest.scala @@ -0,0 +1,139 @@ +package dev.atedeg.mdm.utils.serialization + +import java.time.{ LocalDate, LocalDateTime, ZoneId } +import java.time.format.DateTimeFormatter +import java.util.UUID +import scala.collection.mutable +import scala.language.postfixOps + +import cats.data.NonEmptyList +import cats.syntax.all.* +import eu.timepit.refined.api.Refined +import eu.timepit.refined.predicates.all.Positive +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen +import org.scalacheck.Gen.uuid +import org.scalacheck.Prop.forAll +import org.scalacheck.util.Buildable +import org.scalatest.* +import org.scalatest.EitherValues.* +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers +import org.scalatest.propspec.AnyPropSpec +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +import dev.atedeg.mdm.utils.coerce +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +final case class Test1(n: Int) +final case class Test2(t: Test1, s: String) +final case class Test2DTO(t: Int, s: String) + +trait Generators: + val test1: Gen[Test1] = arbitrary[Int].map(Test1.apply) + val test1DTO: Gen[Int] = arbitrary[Int] + val test2: Gen[Test2] = test1.flatMap(t1 => arbitrary[String].map(Test2(t1, _))) + val test2DTO: Gen[Test2DTO] = arbitrary[Int].flatMap(i => arbitrary[String].map(Test2DTO(i, _))) + @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) + val nonEmpty: Gen[NonEmptyList[Int]] = Gen.nonEmptyListOf(arbitrary[Int]).map(_.toNel.get) + val nonEmptyDTO: Gen[List[Int]] = nonEmpty.map(_.toList) + val localDateTime: Gen[LocalDateTime] = Gen.calendar.map(_.toInstant.atZone(ZoneId.systemDefault).toLocalDateTime) + val localDateTimeDTO: Gen[String] = localDateTime.map(_.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + val localDate: Gen[LocalDate] = localDateTime.map(_.toLocalDate) + val localDateDTO: Gen[String] = localDate.map(_.format(DateTimeFormatter.ISO_LOCAL_DATE)) + val positiveNumber: Gen[Int Refined Positive] = Gen.posNum[Int].map(coerce) + val positiveNumberDTO: Gen[Int] = Gen.posNum[Int] + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val uuidDTO: Gen[String] = uuid.map(_.toString) + +@SuppressWarnings(Array("org.wartremover.warts.Any")) +class Tests extends AnyFeatureSpec with GivenWhenThen with ScalaCheckDrivenPropertyChecks with Matchers with Generators: + extension [D](dto: D) def decodeAndEncode[E](using DTO[E, D]): D = dto.toDomain[E].value.toDTO[D] + extension [E](elem: E) def encodeAndDecode[D](using DTO[E, D]): E = elem.toDTO[D].toDomain[E].value + def encodingInverseOfDecoding[E, D](dto: D)(using DTO[E, D]): Assertion = dto.decodeAndEncode[E] shouldBe dto + def decodingInverseOfEncoding[E, D](e: E)(using DTO[E, D]): Assertion = e.encodeAndDecode[D] shouldBe e + extension [A, B](e: Either[A, B]) def shouldBeLeft: Assertion = e should matchPattern { case Left(_) => } + + Feature("Default instances' decoding is the inverse of encoding") { + Scenario("Int")(forAll(decodingInverseOfEncoding[Int, Int])) + Scenario("String")(forAll(decodingInverseOfEncoding[String, String])) + Scenario("Double")(forAll(decodingInverseOfEncoding[Double, Double])) + Scenario("List")(forAll(decodingInverseOfEncoding[List[Int], List[Int]])) + Scenario("NonEmptyList")(forAll(nonEmpty)(decodingInverseOfEncoding[NonEmptyList[Int], List[Int]])) + Scenario("UUID")(forAll(uuid)(decodingInverseOfEncoding[UUID, String])) + Scenario("LocalDate")(forAll(localDate)(decodingInverseOfEncoding[LocalDate, String])) + Scenario("LocalDateTime")(forAll(localDateTime)(decodingInverseOfEncoding[LocalDateTime, String])) + Scenario("Refined positive")(forAll(positiveNumber)(decodingInverseOfEncoding[Int Refined Positive, Int])) + } + + Feature("Default instances' encoding is the inverse of decoding") { + Scenario("Int")(forAll(encodingInverseOfDecoding[Int, Int])) + Scenario("String")(forAll(encodingInverseOfDecoding[String, String])) + Scenario("Double")(forAll(encodingInverseOfDecoding[Double, Double])) + Scenario("List")(forAll(encodingInverseOfDecoding[List[Int], List[Int]])) + Scenario("NonEmptyList")(forAll(nonEmptyDTO)(encodingInverseOfDecoding[NonEmptyList[Int], List[Int]])) + Scenario("UUID")(forAll(uuidDTO)(encodingInverseOfDecoding[UUID, String])) + Scenario("LocalDate")(forAll(localDateDTO)(encodingInverseOfDecoding[LocalDate, String])) + Scenario("LocalDateTime")(forAll(localDateTimeDTO)(encodingInverseOfDecoding[LocalDateTime, String])) + Scenario("Refined positive")(forAll(positiveNumberDTO)(encodingInverseOfDecoding[Int Refined Positive, Int])) + } + + Feature("Default instances' invariants are checked when decoding") { + Scenario("Refined positive")(0.toDomain[Int Refined Positive] shouldBeLeft) + Scenario("UUID")("invalid uuid".toDomain[UUID] shouldBeLeft) + Scenario("LocalDate")("invalid date".toDomain[LocalDate] shouldBeLeft) + Scenario("LocalDateTime")("invalid date time".toDomain[LocalDateTime] shouldBeLeft) + Scenario("NonEmptyList")(List[Int]().toDomain[NonEmptyList[Int]] shouldBeLeft) + } + + Feature("DTO auto generation") { + Scenario("Generating a DTO for a case class with one field") { + Given("a case class") + When("it has only one field") + And("it has a DTO instance") + Then("it should be possible to auto derive a DTO instance") + given DTO[Test1, Int] = DTOGenerators.caseClassDTO + And("decoding is the inverse of encoding") + forAll(test1)(decodingInverseOfEncoding[Test1, Int]) + And("vice versa") + forAll(arbitrary[Int])(encodingInverseOfDecoding[Test1, Int]) + } + + Scenario("Generating a DTO between two compatible case classes") { + Given("two case classes") + When("they have the same number of fields") + And("there are instances to convert between fields") + given DTO[Test1, Int] = DTOGenerators.caseClassDTO + Then("it should be possible to auto derive a DTO instance") + given DTO[Test2, Test2DTO] = DTOGenerators.interCaseClassDTO + And("decoding is the inverse of encoding") + forAll(test2)(decodingInverseOfEncoding[Test2, Test2DTO]) + And("vice versa") + forAll(test2DTO)(encodingInverseOfDecoding[Test2, Test2DTO]) + } + } + + Feature("Compile-time checks during auto generation") { + Scenario("Generating a DTO between case classes with a different number of fields") { + Given("two case classes") + When("they have a different number of fields") + Then("it should be impossible to auto derive a DTO instance") + "val instance = DTOGenerators.interCaseClassDTO[Test1, Test2]" shouldNot compile + } + + Scenario("Generating a DTO between case classes with fields that can not converted with a DTO instance") { + Given("two case classes") + When("they have the same number of fields") + And("there is no DTO instance to convert between fields") + Then("it should be impossible to auto derive a DTO instance") + "val instance = DTOGenerators.interCaseClassDTO[Test2, Test2DTO]" shouldNot compile + } + + Scenario("Generating a DTO for a case class with one field that can not be converted with a DTO instance") { + Given("a case class") + When("it has only one field") + And("it does not have a DTO instance for that field") + Then("it should be impossible to auto derive a DTO instance") + "val instance = DTOGenerators.caseClassDTO[Test1, Test2]" shouldNot compile + } + } From f0eb87eed119dc432f5922e203b09ebdee0f5f06 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sun, 7 Aug 2022 13:45:12 +0200 Subject: [PATCH 202/329] refactor: change tests' style --- .../mdm/utils/serialization/DTOTest.scala | 129 ++++++++---------- 1 file changed, 54 insertions(+), 75 deletions(-) diff --git a/utils/src/test/scala/dev/atedeg/mdm/utils/serialization/DTOTest.scala b/utils/src/test/scala/dev/atedeg/mdm/utils/serialization/DTOTest.scala index 33772c8e..07df186d 100644 --- a/utils/src/test/scala/dev/atedeg/mdm/utils/serialization/DTOTest.scala +++ b/utils/src/test/scala/dev/atedeg/mdm/utils/serialization/DTOTest.scala @@ -19,7 +19,7 @@ import org.scalatest.* import org.scalatest.EitherValues.* import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers -import org.scalatest.propspec.AnyPropSpec +import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import dev.atedeg.mdm.utils.coerce @@ -47,93 +47,72 @@ trait Generators: val uuidDTO: Gen[String] = uuid.map(_.toString) @SuppressWarnings(Array("org.wartremover.warts.Any")) -class Tests extends AnyFeatureSpec with GivenWhenThen with ScalaCheckDrivenPropertyChecks with Matchers with Generators: +class Tests extends AnyWordSpec with ScalaCheckDrivenPropertyChecks with Matchers with Generators: extension [D](dto: D) def decodeAndEncode[E](using DTO[E, D]): D = dto.toDomain[E].value.toDTO[D] extension [E](elem: E) def encodeAndDecode[D](using DTO[E, D]): E = elem.toDTO[D].toDomain[E].value def encodingInverseOfDecoding[E, D](dto: D)(using DTO[E, D]): Assertion = dto.decodeAndEncode[E] shouldBe dto def decodingInverseOfEncoding[E, D](e: E)(using DTO[E, D]): Assertion = e.encodeAndDecode[D] shouldBe e extension [A, B](e: Either[A, B]) def shouldBeLeft: Assertion = e should matchPattern { case Left(_) => } - Feature("Default instances' decoding is the inverse of encoding") { - Scenario("Int")(forAll(decodingInverseOfEncoding[Int, Int])) - Scenario("String")(forAll(decodingInverseOfEncoding[String, String])) - Scenario("Double")(forAll(decodingInverseOfEncoding[Double, Double])) - Scenario("List")(forAll(decodingInverseOfEncoding[List[Int], List[Int]])) - Scenario("NonEmptyList")(forAll(nonEmpty)(decodingInverseOfEncoding[NonEmptyList[Int], List[Int]])) - Scenario("UUID")(forAll(uuid)(decodingInverseOfEncoding[UUID, String])) - Scenario("LocalDate")(forAll(localDate)(decodingInverseOfEncoding[LocalDate, String])) - Scenario("LocalDateTime")(forAll(localDateTime)(decodingInverseOfEncoding[LocalDateTime, String])) - Scenario("Refined positive")(forAll(positiveNumber)(decodingInverseOfEncoding[Int Refined Positive, Int])) - } - - Feature("Default instances' encoding is the inverse of decoding") { - Scenario("Int")(forAll(encodingInverseOfDecoding[Int, Int])) - Scenario("String")(forAll(encodingInverseOfDecoding[String, String])) - Scenario("Double")(forAll(encodingInverseOfDecoding[Double, Double])) - Scenario("List")(forAll(encodingInverseOfDecoding[List[Int], List[Int]])) - Scenario("NonEmptyList")(forAll(nonEmptyDTO)(encodingInverseOfDecoding[NonEmptyList[Int], List[Int]])) - Scenario("UUID")(forAll(uuidDTO)(encodingInverseOfDecoding[UUID, String])) - Scenario("LocalDate")(forAll(localDateDTO)(encodingInverseOfDecoding[LocalDate, String])) - Scenario("LocalDateTime")(forAll(localDateTimeDTO)(encodingInverseOfDecoding[LocalDateTime, String])) - Scenario("Refined positive")(forAll(positiveNumberDTO)(encodingInverseOfDecoding[Int Refined Positive, Int])) - } - - Feature("Default instances' invariants are checked when decoding") { - Scenario("Refined positive")(0.toDomain[Int Refined Positive] shouldBeLeft) - Scenario("UUID")("invalid uuid".toDomain[UUID] shouldBeLeft) - Scenario("LocalDate")("invalid date".toDomain[LocalDate] shouldBeLeft) - Scenario("LocalDateTime")("invalid date time".toDomain[LocalDateTime] shouldBeLeft) - Scenario("NonEmptyList")(List[Int]().toDomain[NonEmptyList[Int]] shouldBeLeft) - } - - Feature("DTO auto generation") { - Scenario("Generating a DTO for a case class with one field") { - Given("a case class") - When("it has only one field") - And("it has a DTO instance") - Then("it should be possible to auto derive a DTO instance") - given DTO[Test1, Int] = DTOGenerators.caseClassDTO - And("decoding is the inverse of encoding") - forAll(test1)(decodingInverseOfEncoding[Test1, Int]) - And("vice versa") - forAll(arbitrary[Int])(encodingInverseOfDecoding[Test1, Int]) + "Default instances' decoding" should { + "be the inverse of encoding" in { + forAll(decodingInverseOfEncoding[Int, Int]) + forAll(decodingInverseOfEncoding[String, String]) + forAll(decodingInverseOfEncoding[Double, Double]) + forAll(decodingInverseOfEncoding[List[Int], List[Int]]) + forAll(nonEmpty)(decodingInverseOfEncoding[NonEmptyList[Int], List[Int]]) + forAll(uuid)(decodingInverseOfEncoding[UUID, String]) + forAll(localDate)(decodingInverseOfEncoding[LocalDate, String]) + forAll(localDateTime)(decodingInverseOfEncoding[LocalDateTime, String]) + forAll(positiveNumber)(decodingInverseOfEncoding[Int Refined Positive, Int]) } + } - Scenario("Generating a DTO between two compatible case classes") { - Given("two case classes") - When("they have the same number of fields") - And("there are instances to convert between fields") - given DTO[Test1, Int] = DTOGenerators.caseClassDTO - Then("it should be possible to auto derive a DTO instance") - given DTO[Test2, Test2DTO] = DTOGenerators.interCaseClassDTO - And("decoding is the inverse of encoding") - forAll(test2)(decodingInverseOfEncoding[Test2, Test2DTO]) - And("vice versa") - forAll(test2DTO)(encodingInverseOfDecoding[Test2, Test2DTO]) + "Default instances' encoding" should { + "be the inverse of decoding" in { + forAll(encodingInverseOfDecoding[Int, Int]) + forAll(encodingInverseOfDecoding[String, String]) + forAll(encodingInverseOfDecoding[Double, Double]) + forAll(encodingInverseOfDecoding[List[Int], List[Int]]) + forAll(nonEmptyDTO)(encodingInverseOfDecoding[NonEmptyList[Int], List[Int]]) + forAll(uuidDTO)(encodingInverseOfDecoding[UUID, String]) + forAll(localDateDTO)(encodingInverseOfDecoding[LocalDate, String]) + forAll(localDateTimeDTO)(encodingInverseOfDecoding[LocalDateTime, String]) + forAll(positiveNumberDTO)(encodingInverseOfDecoding[Int Refined Positive, Int]) } } - Feature("Compile-time checks during auto generation") { - Scenario("Generating a DTO between case classes with a different number of fields") { - Given("two case classes") - When("they have a different number of fields") - Then("it should be impossible to auto derive a DTO instance") - "val instance = DTOGenerators.interCaseClassDTO[Test1, Test2]" shouldNot compile + "Default instances' invariants" should { + "be checked when decoding" in { + 0.toDomain[Int Refined Positive].shouldBeLeft + "invalid uuid".toDomain[UUID].shouldBeLeft + "invalid date".toDomain[LocalDate].shouldBeLeft + "invalid date time".toDomain[LocalDateTime].shouldBeLeft + List[Int]().toDomain[NonEmptyList[Int]].shouldBeLeft } + } - Scenario("Generating a DTO between case classes with fields that can not converted with a DTO instance") { - Given("two case classes") - When("they have the same number of fields") - And("there is no DTO instance to convert between fields") - Then("it should be impossible to auto derive a DTO instance") - "val instance = DTOGenerators.interCaseClassDTO[Test2, Test2DTO]" shouldNot compile + "DTO auto-generation" when { + "used for case class with one field" should { + "generate a correct instance" in { + given DTO[Test1, Int] = DTOGenerators.caseClassDTO + forAll(test1)(decodingInverseOfEncoding[Test1, Int]) + forAll(arbitrary[Int])(encodingInverseOfDecoding[Test1, Int]) + } + "be checked at compile-time" in { + "val instance = DTOGenerators.caseClassDTO[Test1, Test2]" shouldNot compile + } } - - Scenario("Generating a DTO for a case class with one field that can not be converted with a DTO instance") { - Given("a case class") - When("it has only one field") - And("it does not have a DTO instance for that field") - Then("it should be impossible to auto derive a DTO instance") - "val instance = DTOGenerators.caseClassDTO[Test1, Test2]" shouldNot compile + "used with a pair of compatible case classes" should { + "generate a correct instance" in { + given DTO[Test1, Int] = DTOGenerators.caseClassDTO + given DTO[Test2, Test2DTO] = DTOGenerators.interCaseClassDTO + forAll(test2)(decodingInverseOfEncoding[Test2, Test2DTO]) + forAll(test2DTO)(encodingInverseOfDecoding[Test2, Test2DTO]) + } + "be checked at compile-time" in { + "val instance = DTOGenerators.interCaseClassDTO[Test1, Test2]" shouldNot compile + "val instance = DTOGenerators.interCaseClassDTO[Test2, Test2DTO]" shouldNot compile + } } } From 5f1c40ad23c26a15b9904f4e02740f77c90d44f4 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 17:28:20 +0200 Subject: [PATCH 203/329] feat: add DTOs for product and cheesetype --- .../dev/atedeg/mdm/products/dto/DTOs.scala | 3 +++ .../dev/atedeg/mdm/products/dto/FromDTO.scala | 20 +++++++++++++++++++ .../dev/atedeg/mdm/products/dto/ToDTO.scala | 6 ++++++ 3 files changed, 29 insertions(+) create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala new file mode 100644 index 00000000..9e153795 --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala @@ -0,0 +1,3 @@ +package dev.atedeg.mdm.products.dto + +final case class ProductDTO(cheeseType: String, weight: Int) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala new file mode 100644 index 00000000..1d4d2d34 --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala @@ -0,0 +1,20 @@ +package dev.atedeg.mdm.products.dto + +import cats.syntax.all.* +import eu.timepit.refined.numeric.Positive + +import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } +import dev.atedeg.mdm.products.utils.* +import dev.atedeg.mdm.products.utils.ReadShowInstances.given +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.serialization.* + +extension (p: ProductDTO) + def toDomain: Either[String, Product] = + for + weight <- p.weight.refined[Positive].map(Grams.apply) + cheeseType <- p.cheeseType.read[CheeseType] + product <- cheeseType + .withWeight(_ === weight.n.value) + .toRight(s"Couldn't find a `$cheeseType` with weight $weight") + yield product diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala new file mode 100644 index 00000000..9fda511c --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala @@ -0,0 +1,6 @@ +package dev.atedeg.mdm.products.dto + +import dev.atedeg.mdm.products.Product +import dev.atedeg.mdm.products.utils.ReadShowInstances.given + +extension (p: Product) def toDTO: ProductDTO = ProductDTO(p.cheeseType.show, p.weight.n.value) From c41bcea4af51fa5814369fa025c2de5eec298b2d Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Sun, 7 Aug 2022 13:58:58 +0200 Subject: [PATCH 204/329] feat: add prod planning dtos --- .../atedeg/mdm/productionplanning/dto/DTOs.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala new file mode 100644 index 00000000..01e452fd --- /dev/null +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala @@ -0,0 +1,14 @@ +package dev.atedeg.mdm.productionplanning.dto + +type ProductDTO +// incoming events +final case class NewOrderReceivedDTO(order: OrderDTO) +final case class OrderDTO(orderID: String, requiredBy: String, orderedProducts: List[OrderedProductDTO]) +final case class OrderedProductDTO(product: ProductDTO, quantity: Int) + +// outgoing events +final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) +final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) +final case class ProductToProduceDTO(product: ProductDTO, quantity: Int) + +final case class OrderDelayed(orderID: String, newDeliveryDate: String) From 9e977505636b351c476cb9cdb180b5c0d9a7aa0d Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Sun, 7 Aug 2022 15:14:42 +0200 Subject: [PATCH 205/329] refactor: automatically generate products and cheese type DTOs --- .../dev/atedeg/mdm/products/dto/DTOs.scala | 60 +++++++++++++++++++ .../dev/atedeg/mdm/products/dto/FromDTO.scala | 20 ------- .../dev/atedeg/mdm/products/dto/ToDTO.scala | 6 -- .../mdm/products/utils/IngredientDTO.scala | 24 -------- .../dev/atedeg/mdm/restocking/dto/DTOs.scala | 2 +- 5 files changed, 61 insertions(+), 51 deletions(-) delete mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala delete mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala delete mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala index 9e153795..0164ce85 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala @@ -1,3 +1,63 @@ package dev.atedeg.mdm.products.dto +import cats.syntax.all.* + +import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } +import dev.atedeg.mdm.products.CheeseType.* +import dev.atedeg.mdm.products.Ingredient +import dev.atedeg.mdm.products.Ingredient.* +import dev.atedeg.mdm.products.utils.withWeight +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOGenerators.* +import dev.atedeg.mdm.utils.serialization.DTOOps.* + final case class ProductDTO(cheeseType: String, weight: Int) + +object ProductDTO: + import dev.atedeg.mdm.products.dto.CheeseTypeDTO.given + given DTO[Product, ProductDTO] with + override def dtoToElem(dto: ProductDTO): Either[String, Product] = + for + cheeseType <- dto.cheeseType.toDomain[CheeseType] + weight <- dto.weight.toDomain[Grams] + product <- cheeseType + .withWeight(_ === weight.n.value) + .toRight(s"No product of type $cheeseType with weight $weight") + yield product + + override def elemToDto(e: Product): ProductDTO = ProductDTO(e.cheeseType.toDTO, e.weight.toDTO) + private given DTO[Grams, Int] = caseClassDTO + +object CheeseTypeDTO: + given DTO[CheeseType, String] with + override def dtoToElem(dto: String): Either[String, CheeseType] = dto match + case "squacquerone" => Squacquerone.asRight[String] + case "stracchino" => Stracchino.asRight[String] + case "casatella" => Casatella.asRight[String] + case "caciotta" => Caciotta.asRight[String] + case "ricotta" => Ricotta.asRight[String] + case _ => "Unknown `CheeseType`: '$s'".asLeft[CheeseType] + + override def elemToDto(e: CheeseType): String = e match + case Squacquerone => "squacquerone" + case Stracchino => "stracchino" + case Casatella => "casatella" + case Caciotta => "caciotta" + case Ricotta => "ricotta" + +object IngredientDTO: + given DTO[Ingredient, String] with + override def dtoToElem(dto: String): Either[String, Ingredient] = dto match + case "probiotics" => Probiotics.asRight[String] + case "rennet" => Rennet.asRight[String] + case "cream" => Cream.asRight[String] + case "milk" => Milk.asRight[String] + case "salt" => Salt.asRight[String] + case _ => s"Unknown `Ingredient`: '$dto'".asLeft[Ingredient] + + override def elemToDto(e: Ingredient): String = e match + case Probiotics => "probiotics" + case Rennet => "rennet" + case Cream => "cream" + case Milk => "milk" + case Salt => "salt" diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala deleted file mode 100644 index 1d4d2d34..00000000 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala +++ /dev/null @@ -1,20 +0,0 @@ -package dev.atedeg.mdm.products.dto - -import cats.syntax.all.* -import eu.timepit.refined.numeric.Positive - -import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } -import dev.atedeg.mdm.products.utils.* -import dev.atedeg.mdm.products.utils.ReadShowInstances.given -import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.serialization.* - -extension (p: ProductDTO) - def toDomain: Either[String, Product] = - for - weight <- p.weight.refined[Positive].map(Grams.apply) - cheeseType <- p.cheeseType.read[CheeseType] - product <- cheeseType - .withWeight(_ === weight.n.value) - .toRight(s"Couldn't find a `$cheeseType` with weight $weight") - yield product diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala deleted file mode 100644 index 9fda511c..00000000 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala +++ /dev/null @@ -1,6 +0,0 @@ -package dev.atedeg.mdm.products.dto - -import dev.atedeg.mdm.products.Product -import dev.atedeg.mdm.products.utils.ReadShowInstances.given - -extension (p: Product) def toDTO: ProductDTO = ProductDTO(p.cheeseType.show, p.weight.n.value) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala deleted file mode 100644 index 8d19a504..00000000 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/IngredientDTO.scala +++ /dev/null @@ -1,24 +0,0 @@ -package dev.atedeg.mdm.products.utils - -import cats.syntax.all.* - -import dev.atedeg.mdm.products.Ingredient -import dev.atedeg.mdm.products.Ingredient.* -import dev.atedeg.mdm.utils.serialization.* - -object IngredientDTO: - given DTO[Ingredient, String] with - override def dtoToElem(dto: String): Either[String, Ingredient] = dto match - case "probiotics" => Probiotics.asRight[String] - case "rennet" => Rennet.asRight[String] - case "cream" => Cream.asRight[String] - case "milk" => Milk.asRight[String] - case "salt" => Salt.asRight[String] - case _ => s"Unknown `Ingredient`: '$dto'".asLeft[Ingredient] - - override def elemToDto(e: Ingredient): String = e match - case Probiotics => "probiotics" - case Rennet => "rennet" - case Cream => "cream" - case Milk => "milk" - case Salt => "salt" diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala index ceb90fb3..2dfca756 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala @@ -1,6 +1,6 @@ package dev.atedeg.mdm.restocking.dto -import dev.atedeg.mdm.products.utils.IngredientDTO.given +import dev.atedeg.mdm.products.dto.IngredientDTO.given import dev.atedeg.mdm.restocking.* import dev.atedeg.mdm.restocking.IncomingEvent.* import dev.atedeg.mdm.utils.serialization.DTO From d3ae700dc71a14edb7a2853d998f6e7ecea944c4 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Sun, 7 Aug 2022 15:35:48 +0200 Subject: [PATCH 206/329] feat: add production planning DTOs --- .../mdm/productionplanning/dto/DTOs.scala | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala index 01e452fd..426e9819 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala @@ -1,14 +1,44 @@ -package dev.atedeg.mdm.productionplanning.dto - -type ProductDTO -// incoming events -final case class NewOrderReceivedDTO(order: OrderDTO) -final case class OrderDTO(orderID: String, requiredBy: String, orderedProducts: List[OrderedProductDTO]) -final case class OrderedProductDTO(product: ProductDTO, quantity: Int) - -// outgoing events -final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) -final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) -final case class ProductToProduceDTO(product: ProductDTO, quantity: Int) - -final case class OrderDelayed(orderID: String, newDeliveryDate: String) +package dev.atedeg.mdm.productionplanning.dto + +import java.time.LocalDate + +import dev.atedeg.mdm.productionplanning.* +import dev.atedeg.mdm.productionplanning.IncomingEvent.* +import dev.atedeg.mdm.productionplanning.OutgoingEvent.* +import dev.atedeg.mdm.productionplanning.dto.OrderIDDTO.given +import dev.atedeg.mdm.productionplanning.dto.QuantityDTO.given +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.products.dto.ProductDTO.given +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOGenerators.* +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +// incoming events +final case class NewOrderReceivedDTO(order: OrderDTO) +final case class OrderDTO(orderID: String, requiredBy: String, orderedProducts: List[OrderedProductDTO]) +final case class OrderedProductDTO(product: ProductDTO, quantity: Int) + +object NewOrderReceivedDTO: + given DTO[NewOrderReceived, NewOrderReceivedDTO] = interCaseClassDTO + private given DTO[Order, OrderDTO] = interCaseClassDTO + private given DTO[OrderedProduct, OrderedProductDTO] = interCaseClassDTO + +// outgoing events +final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) +final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) +final case class ProductToProduceDTO(product: ProductDTO, quantity: Int) + +object ProductionPlanReadyDTO: + given DTO[ProductionPlanReady, ProductionPlanReadyDTO] = interCaseClassDTO + given DTO[ProductionPlan, ProductionPlanDTO] = interCaseClassDTO + given DTO[ProductToProduce, ProductToProduceDTO] = interCaseClassDTO + +final case class OrderDelayedDTO(orderID: String, newDeliveryDate: String) +object OrderDelayedDTO: + given DTO[OrderDelayed, OrderDelayedDTO] = interCaseClassDTO + +private object QuantityDTO: + given DTO[Quantity, Int] = caseClassDTO + +private object OrderIDDTO: + given DTO[OrderID, String] = caseClassDTO From 17d94eee469c5429855325b0bb88b4312cb3c6ac Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Sun, 7 Aug 2022 15:48:30 +0200 Subject: [PATCH 207/329] refactor: remove useless comments --- .../main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala index 426e9819..82650d69 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala @@ -13,7 +13,6 @@ import dev.atedeg.mdm.utils.serialization.DTO import dev.atedeg.mdm.utils.serialization.DTOGenerators.* import dev.atedeg.mdm.utils.serialization.DTOOps.* -// incoming events final case class NewOrderReceivedDTO(order: OrderDTO) final case class OrderDTO(orderID: String, requiredBy: String, orderedProducts: List[OrderedProductDTO]) final case class OrderedProductDTO(product: ProductDTO, quantity: Int) @@ -23,7 +22,6 @@ object NewOrderReceivedDTO: private given DTO[Order, OrderDTO] = interCaseClassDTO private given DTO[OrderedProduct, OrderedProductDTO] = interCaseClassDTO -// outgoing events final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) final case class ProductToProduceDTO(product: ProductDTO, quantity: Int) From 0fc3c4532b928f0e2820673fc712ec5e91b06805 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 7 Aug 2022 14:06:50 +0000 Subject: [PATCH 208/329] chore(release): 1.0.0-beta.9 [skip ci] # [1.0.0-beta.9](https://github.com/atedeg/mdm/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2022-08-07) ### Features * add DTOs for product and cheesetype ([5f1c40a](https://github.com/atedeg/mdm/commit/5f1c40ad23c26a15b9904f4e02740f77c90d44f4)) * add prod planning dtos ([c41bcea](https://github.com/atedeg/mdm/commit/c41bcea4af51fa5814369fa025c2de5eec298b2d)) * add production planning DTOs ([d3ae700](https://github.com/atedeg/mdm/commit/d3ae700dc71a14edb7a2853d998f6e7ecea944c4)) --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14fb35cf..2f8c43cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [1.0.0-beta.9](https://github.com/atedeg/mdm/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2022-08-07) + + +### Features + +* add DTOs for product and cheesetype ([5f1c40a](https://github.com/atedeg/mdm/commit/5f1c40ad23c26a15b9904f4e02740f77c90d44f4)) +* add prod planning dtos ([c41bcea](https://github.com/atedeg/mdm/commit/c41bcea4af51fa5814369fa025c2de5eec298b2d)) +* add production planning DTOs ([d3ae700](https://github.com/atedeg/mdm/commit/d3ae700dc71a14edb7a2853d998f6e7ecea944c4)) + # [1.0.0-beta.8](https://github.com/atedeg/mdm/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2022-08-06) From c146b489d0fbdaf803bc415c4520f00ae16450c6 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sat, 6 Aug 2022 17:08:55 +0200 Subject: [PATCH 209/329] feat: add production DTOs --- .../dev/atedeg/mdm/production/Events.scala | 2 +- .../dev/atedeg/mdm/production/dto/DTOs.scala | 10 ++++ .../atedeg/mdm/production/dto/FromDTO.scala | 46 +++++++++++++++++++ .../dev/atedeg/mdm/production/dto/ToDTO.scala | 22 +++++++++ .../production/utils/ReadShowInstances.scala | 24 ++++++++++ .../atedeg/mdm/restocking/dto/FromDTO.scala | 0 .../mdm/utils/serialization/StringOps.scala | 0 7 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/dto/FromDTO.scala create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/dto/ToDTO.scala create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/utils/ReadShowInstances.scala create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala index 671a8151..2345a534 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala @@ -11,7 +11,7 @@ enum OutgoingEvent: * [[QuintalsOfIngredient needed ingredients and the quantity]] necessary to sustain the * production. */ - case StartProduction(neededIngredient: NonEmptyList[QuintalsOfIngredient]) + case StartProduction(neededIngredients: NonEmptyList[QuintalsOfIngredient]) /** * Fired when a [[Production.InProgress production]] is terminated, given a diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala new file mode 100644 index 00000000..26c41a84 --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala @@ -0,0 +1,10 @@ +package dev.atedeg.mdm.production.dto + +type ProductDTO = String + +final case class StartProductionDTO(neededIngredients: List[QuintalsOfIngredientDTO]) +final case class QuintalsOfIngredientDTO(quintals: Double, ingredient: String) +final case class ProductionEndedDTO(productionID: String, batchID: String) +final case class ProductionPlanReadyDTO(productionPlan: List[ProductionPlanItemDTO]) +final case class ProductionPlanItemDTO(product: ProductDTO, units: Int) + diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/FromDTO.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/FromDTO.scala new file mode 100644 index 00000000..de5742c9 --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/dto/FromDTO.scala @@ -0,0 +1,46 @@ +package dev.atedeg.mdm.production.dto +import cats.syntax.all.* +import eu.timepit.refined.numeric.Positive + +import dev.atedeg.mdm.production.* +import dev.atedeg.mdm.production.IncomingEvent.* +import dev.atedeg.mdm.production.OutgoingEvent.* +import dev.atedeg.mdm.production.utils.ReadShowInstances.given +import dev.atedeg.mdm.products.{ Ingredient, Product } +import dev.atedeg.mdm.products.utils.ReadShowInstancesOps.given +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.serialization.* +import dev.atedeg.mdm.utils.serialization.given + +extension (sp: StartProductionDTO) + def toDomain: Either[String, StartProduction] = sp.neededIngredients.toNel + .toRight("Needed ingredients is empty") + .flatMap(_.traverse(_.toDomain)) + .map(StartProduction.apply) + +extension (qoi: QuintalsOfIngredientDTO) + def toDomain: Either[String, QuintalsOfIngredient] = for + quintals <- qoi.quintals.refined[Positive].map(WeightInQuintals.apply) + ingredient <- qoi.ingredient.read[Ingredient] + yield QuintalsOfIngredient(quintals, ingredient) + +extension (pe: ProductionEndedDTO) + def toDomain: Either[String, ProductionEnded] = for + productionID <- pe.productionID.read[ProductionID] + batchID <- pe.batchID.read[BatchID] + yield ProductionEnded(productionID, batchID) + +extension (ppr: ProductionPlanReadyDTO) + def toDomain: Either[String, ProductionPlanReady] = ppr.productionPlan.toNel + .toRight("The production plan item list is empty") + .flatMap(_.traverse(_.toDomain)) + .map(ProductionPlan.apply) + .map(ProductionPlanReady.apply) + +extension (ppi: ProductionPlanItemDTO) + def toDomain: Either[String, ProductionPlanItem] = for + product <- ppi.product.toDomain + units <- ppi.units.refined[Positive].map(NumberOfUnits.apply) + yield ProductionPlanItem(product, units) + +extension (p: ProductDTO) def toDomain: Either[String, Product] = ??? diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/ToDTO.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/ToDTO.scala new file mode 100644 index 00000000..7f1cb8eb --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/dto/ToDTO.scala @@ -0,0 +1,22 @@ +package dev.atedeg.mdm.production.dto + +import dev.atedeg.mdm.production.* +import dev.atedeg.mdm.production.IncomingEvent.* +import dev.atedeg.mdm.production.OutgoingEvent.* +import dev.atedeg.mdm.products.utils.ReadShowInstancesOps.given + +extension (sp: StartProduction) + def toDTO: StartProductionDTO = StartProductionDTO(sp.neededIngredients.map(_.toDTO).toList) + +extension (qoi: QuintalsOfIngredient) + def toDTO: QuintalsOfIngredientDTO = QuintalsOfIngredientDTO(qoi.quintals.n.value, qoi.ingredient.show) + +extension (pe: ProductionEnded) + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + def toDTO: ProductionEndedDTO = ProductionEndedDTO(pe.productionID.ID.toString, pe.batchID.ID.toString) + +extension (ppr: ProductionPlanReady) + def toDTO: ProductionPlanReadyDTO = ProductionPlanReadyDTO(ppr.productionPlan.plan.map(_.toDTO).toList) + +extension (ppi: ProductionPlanItem) + def toDTO: ProductionPlanItemDTO = ProductionPlanItemDTO(??? /*ppi.productToProduce.toDTO*/, ppi.units.n.value) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/utils/ReadShowInstances.scala b/production/src/main/scala/dev/atedeg/mdm/production/utils/ReadShowInstances.scala new file mode 100644 index 00000000..f6a76d91 --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/utils/ReadShowInstances.scala @@ -0,0 +1,24 @@ +package dev.atedeg.mdm.production.utils + +import java.util.UUID +import scala.util.Try + +import cats.syntax.all.* + +import dev.atedeg.mdm.production.{ BatchID, ProductionID } +import dev.atedeg.mdm.utils.serialization.* + +object ReadShowInstances: + given Read[BatchID] with + override def fromString(s: String): Either[String, BatchID] = + Try(UUID.fromString(s)) + .map(BatchID.apply) + .toEither + .leftMap(_ => s"Not a valid BatchID: $s") + + given Read[ProductionID] with + override def fromString(s: String): Either[String, ProductionID] = + Try(UUID.fromString(s)) + .map(ProductionID.apply) + .toEither + .leftMap(_ => s"Not a valid ProductionID: $s") diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala new file mode 100644 index 00000000..e69de29b diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala new file mode 100644 index 00000000..e69de29b From e4967365dc70cba518110016fe81ee3b55378ef6 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sun, 7 Aug 2022 19:00:18 +0200 Subject: [PATCH 210/329] refactor: autogenerate all DTOs implementations --- .../dev/atedeg/mdm/production/dto/DTOs.scala | 25 +++++++++- .../atedeg/mdm/production/dto/FromDTO.scala | 46 ------------------- .../dev/atedeg/mdm/production/dto/ToDTO.scala | 22 --------- .../production/utils/ReadShowInstances.scala | 24 ---------- .../atedeg/mdm/restocking/dto/FromDTO.scala | 0 .../mdm/utils/serialization/StringOps.scala | 0 6 files changed, 24 insertions(+), 93 deletions(-) delete mode 100644 production/src/main/scala/dev/atedeg/mdm/production/dto/FromDTO.scala delete mode 100644 production/src/main/scala/dev/atedeg/mdm/production/dto/ToDTO.scala delete mode 100644 production/src/main/scala/dev/atedeg/mdm/production/utils/ReadShowInstances.scala delete mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala delete mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala index 26c41a84..5df5d7dc 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala @@ -1,6 +1,14 @@ package dev.atedeg.mdm.production.dto -type ProductDTO = String +import dev.atedeg.mdm.production.* +import dev.atedeg.mdm.production.IncomingEvent.* +import dev.atedeg.mdm.production.OutgoingEvent.* +import dev.atedeg.mdm.products.dto.IngredientDTO.given +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.products.dto.ProductDTO.given +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOGenerators.* +import dev.atedeg.mdm.utils.serialization.DTOOps final case class StartProductionDTO(neededIngredients: List[QuintalsOfIngredientDTO]) final case class QuintalsOfIngredientDTO(quintals: Double, ingredient: String) @@ -8,3 +16,18 @@ final case class ProductionEndedDTO(productionID: String, batchID: String) final case class ProductionPlanReadyDTO(productionPlan: List[ProductionPlanItemDTO]) final case class ProductionPlanItemDTO(product: ProductDTO, units: Int) +object StartProductionDTO: + given DTO[StartProduction, StartProductionDTO] = interCaseClassDTO + private given DTO[QuintalsOfIngredient, QuintalsOfIngredientDTO] = interCaseClassDTO + private given DTO[WeightInQuintals, Double] = caseClassDTO + +object ProductionEndedDTO: + given DTO[ProductionEnded, ProductionEndedDTO] = interCaseClassDTO + private given DTO[ProductionID, String] = caseClassDTO + private given DTO[BatchID, String] = caseClassDTO + +object ProductionPlanReadyDTO: + given DTO[ProductionPlanReady, ProductionPlanReadyDTO] = interCaseClassDTO + private given DTO[ProductionPlan, List[ProductionPlanItemDTO]] = caseClassDTO + private given DTO[ProductionPlanItem, ProductionPlanItemDTO] = interCaseClassDTO + private given DTO[NumberOfUnits, Int] = caseClassDTO diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/FromDTO.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/FromDTO.scala deleted file mode 100644 index de5742c9..00000000 --- a/production/src/main/scala/dev/atedeg/mdm/production/dto/FromDTO.scala +++ /dev/null @@ -1,46 +0,0 @@ -package dev.atedeg.mdm.production.dto -import cats.syntax.all.* -import eu.timepit.refined.numeric.Positive - -import dev.atedeg.mdm.production.* -import dev.atedeg.mdm.production.IncomingEvent.* -import dev.atedeg.mdm.production.OutgoingEvent.* -import dev.atedeg.mdm.production.utils.ReadShowInstances.given -import dev.atedeg.mdm.products.{ Ingredient, Product } -import dev.atedeg.mdm.products.utils.ReadShowInstancesOps.given -import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.serialization.* -import dev.atedeg.mdm.utils.serialization.given - -extension (sp: StartProductionDTO) - def toDomain: Either[String, StartProduction] = sp.neededIngredients.toNel - .toRight("Needed ingredients is empty") - .flatMap(_.traverse(_.toDomain)) - .map(StartProduction.apply) - -extension (qoi: QuintalsOfIngredientDTO) - def toDomain: Either[String, QuintalsOfIngredient] = for - quintals <- qoi.quintals.refined[Positive].map(WeightInQuintals.apply) - ingredient <- qoi.ingredient.read[Ingredient] - yield QuintalsOfIngredient(quintals, ingredient) - -extension (pe: ProductionEndedDTO) - def toDomain: Either[String, ProductionEnded] = for - productionID <- pe.productionID.read[ProductionID] - batchID <- pe.batchID.read[BatchID] - yield ProductionEnded(productionID, batchID) - -extension (ppr: ProductionPlanReadyDTO) - def toDomain: Either[String, ProductionPlanReady] = ppr.productionPlan.toNel - .toRight("The production plan item list is empty") - .flatMap(_.traverse(_.toDomain)) - .map(ProductionPlan.apply) - .map(ProductionPlanReady.apply) - -extension (ppi: ProductionPlanItemDTO) - def toDomain: Either[String, ProductionPlanItem] = for - product <- ppi.product.toDomain - units <- ppi.units.refined[Positive].map(NumberOfUnits.apply) - yield ProductionPlanItem(product, units) - -extension (p: ProductDTO) def toDomain: Either[String, Product] = ??? diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/ToDTO.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/ToDTO.scala deleted file mode 100644 index 7f1cb8eb..00000000 --- a/production/src/main/scala/dev/atedeg/mdm/production/dto/ToDTO.scala +++ /dev/null @@ -1,22 +0,0 @@ -package dev.atedeg.mdm.production.dto - -import dev.atedeg.mdm.production.* -import dev.atedeg.mdm.production.IncomingEvent.* -import dev.atedeg.mdm.production.OutgoingEvent.* -import dev.atedeg.mdm.products.utils.ReadShowInstancesOps.given - -extension (sp: StartProduction) - def toDTO: StartProductionDTO = StartProductionDTO(sp.neededIngredients.map(_.toDTO).toList) - -extension (qoi: QuintalsOfIngredient) - def toDTO: QuintalsOfIngredientDTO = QuintalsOfIngredientDTO(qoi.quintals.n.value, qoi.ingredient.show) - -extension (pe: ProductionEnded) - @SuppressWarnings(Array("org.wartremover.warts.ToString")) - def toDTO: ProductionEndedDTO = ProductionEndedDTO(pe.productionID.ID.toString, pe.batchID.ID.toString) - -extension (ppr: ProductionPlanReady) - def toDTO: ProductionPlanReadyDTO = ProductionPlanReadyDTO(ppr.productionPlan.plan.map(_.toDTO).toList) - -extension (ppi: ProductionPlanItem) - def toDTO: ProductionPlanItemDTO = ProductionPlanItemDTO(??? /*ppi.productToProduce.toDTO*/, ppi.units.n.value) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/utils/ReadShowInstances.scala b/production/src/main/scala/dev/atedeg/mdm/production/utils/ReadShowInstances.scala deleted file mode 100644 index f6a76d91..00000000 --- a/production/src/main/scala/dev/atedeg/mdm/production/utils/ReadShowInstances.scala +++ /dev/null @@ -1,24 +0,0 @@ -package dev.atedeg.mdm.production.utils - -import java.util.UUID -import scala.util.Try - -import cats.syntax.all.* - -import dev.atedeg.mdm.production.{ BatchID, ProductionID } -import dev.atedeg.mdm.utils.serialization.* - -object ReadShowInstances: - given Read[BatchID] with - override def fromString(s: String): Either[String, BatchID] = - Try(UUID.fromString(s)) - .map(BatchID.apply) - .toEither - .leftMap(_ => s"Not a valid BatchID: $s") - - given Read[ProductionID] with - override def fromString(s: String): Either[String, ProductionID] = - Try(UUID.fromString(s)) - .map(ProductionID.apply) - .toEither - .leftMap(_ => s"Not a valid ProductionID: $s") diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala deleted file mode 100644 index e69de29b..00000000 diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala deleted file mode 100644 index e69de29b..00000000 From df514fb1060c034eb6e0cf2ebe9595cb152ec96f Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sun, 7 Aug 2022 19:26:15 +0200 Subject: [PATCH 211/329] refactor: rename parameter --- stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index 068f389f..801a4e14 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -79,4 +79,4 @@ final case class BatchID(id: UUID) /** * A [[Product product]] with its respective [[Quantity quantity]] and the [[BatchID ID of the batch]] it belongs to. */ -final case class LabelledProduct(cheeseType: Product, quantity: AvailableQuantity, batchID: BatchID) +final case class LabelledProduct(product: Product, quantity: AvailableQuantity, batchID: BatchID) From 25896d185ebf77cf8b9d4109e346eeac92805387 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sun, 7 Aug 2022 19:26:26 +0200 Subject: [PATCH 212/329] chore: add DTOs --- .../dev/atedeg/mdm/stocking/dto/DTOs.scala | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala new file mode 100644 index 00000000..65f2b54f --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala @@ -0,0 +1,32 @@ +package dev.atedeg.mdm.stocking.dto + +import dev.atedeg.mdm.products.dto.CheeseTypeDTO.given +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.stocking.* +import dev.atedeg.mdm.stocking.IncomingEvent.* +import dev.atedeg.mdm.stocking.LabelledProduct +import dev.atedeg.mdm.stocking.OutgoingEvent.* +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOGenerators.* + +final case class ProductStockedDTO(labelledProduct: LabelledProductDTO) +final case class LabelledProductDTO(product: ProductDTO, quantity: Int, batchID: String) +object ProductStockedDTO: + given DTO[ProductStocked, ProductStockedDTO] = interCaseClassDTO + private given DTO[LabelledProduct, LabelledProductDTO] = interCaseClassDTO + private given DTO[AvailableQuantity, Int] = caseClassDTO + +private given DTO[BatchID, String] = caseClassDTO + +final case class BatchReadyForQualityAssuranceDTO(batch: String) +object BatchReadyForQualityAssuranceDTO: + given DTO[BatchReadyForQualityAssurance, BatchReadyForQualityAssuranceDTO] = interCaseClassDTO + +final case class ProductRemovedFromStockDTO(quantity: Int, product: ProductDTO) +object ProductRemovedFromStockDTO: + given DTO[ProductRemovedFromStock, ProductRemovedFromStockDTO] = interCaseClassDTO + given DTO[DesiredQuantity, Int] = caseClassDTO + +final case class NewBatchDTO(batchID: String, cheeseType: String, readyFrom: String) +object NewBatchDTO: + given DTO[NewBatch, NewBatchDTO] = interCaseClassDTO From 4695c67e2c44c6950fc8e8f86211b17a29c5004a Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sun, 7 Aug 2022 19:52:52 +0200 Subject: [PATCH 213/329] chore: add DTOs --- .../atedeg/mdm/clientorders/dto/DTOs.scala | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala new file mode 100644 index 00000000..df05a252 --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -0,0 +1,57 @@ +package dev.atedeg.mdm.clientorders.dto + +import dev.atedeg.mdm.clientorders.* +import dev.atedeg.mdm.clientorders.IncomingEvent.* +import dev.atedeg.mdm.clientorders.OutgoingEvent.* +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOGenerators.* + +private object Commons: + final case class CustomerDTO(code: String, name: String, vatNumber: String) + final case class LocationDTO(latitude: Double, longitude: Double) + final case class IncomingOrderLineDTO(quantity: Int, product: ProductDTO) + + given DTO[OrderID, String] = caseClassDTO + given DTO[Customer, CustomerDTO] = interCaseClassDTO + given DTO[Location, LocationDTO] = interCaseClassDTO + given DTO[Latitude, Double] = caseClassDTO + given DTO[Longitude, Double] = caseClassDTO + given DTO[Quantity, Int] = caseClassDTO + given DTO[CustomerID, String] = caseClassDTO + given DTO[CustomerName, String] = caseClassDTO + given DTO[VATNumber, String] = caseClassDTO + given DTO[IncomingOrderLine, IncomingOrderLineDTO] = interCaseClassDTO + +import Commons.* +import Commons.given + +final case class OrderReceivedDTO( + id: String, + orderLines: List[IncomingOrderLineDTO], + customer: CustomerDTO, + deliveryDate: String, + deliveryLocation: LocationDTO, +) +object OrderReceivedDTO: + given DTO[OrderReceived, OrderReceivedDTO] = interCaseClassDTO + +final case class ProductPalletizedForOrderDTO(orderID: String, quantity: Int, product: ProductDTO) +object ProductPalletizedForOrderDTO: + given DTO[ProductPalletizedForOrder, ProductPalletizedForOrderDTO] = interCaseClassDTO + +final case class OrderCompletedDTO(orderID: String) +object OrderCompletedDTO: + given DTO[OrderCompleted, OrderCompletedDTO] = interCaseClassDTO + +final case class OrderProcessedDTO(incomingOrder: IncomingOrderDTO) +final case class IncomingOrderDTO( + id: String, + orderLines: List[IncomingOrderLineDTO], + customer: CustomerDTO, + deliveryDate: String, + deliveryLocation: LocationDTO, +) +object OrderProcessedDTO: + given DTO[OrderProcessed, OrderProcessedDTO] = interCaseClassDTO + private given DTO[IncomingOrder, IncomingOrderDTO] = interCaseClassDTO From ddcc814356fb7ec91da89a554166de7b2cfbc071 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 7 Aug 2022 18:42:48 +0000 Subject: [PATCH 214/329] chore(release): 1.0.0-beta.10 [skip ci] # [1.0.0-beta.10](https://github.com/atedeg/mdm/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-08-07) ### Features * add production DTOs ([c146b48](https://github.com/atedeg/mdm/commit/c146b489d0fbdaf803bc415c4520f00ae16450c6)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f8c43cd..5badac39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.0.0-beta.10](https://github.com/atedeg/mdm/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-08-07) + + +### Features + +* add production DTOs ([c146b48](https://github.com/atedeg/mdm/commit/c146b489d0fbdaf803bc415c4520f00ae16450c6)) + # [1.0.0-beta.9](https://github.com/atedeg/mdm/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2022-08-07) From bf3711a0fdc9bc6e9c98f0bc068d241b9643b737 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 17:28:20 +0200 Subject: [PATCH 215/329] feat: add DTOs for product and cheesetype --- .../dev/atedeg/mdm/products/dto/FromDTO.scala | 20 +++++++++ .../dev/atedeg/mdm/products/dto/ToDTO.scala | 6 +++ .../products/utils/ReadShowInstances.scala | 44 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala create mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala new file mode 100644 index 00000000..1d4d2d34 --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala @@ -0,0 +1,20 @@ +package dev.atedeg.mdm.products.dto + +import cats.syntax.all.* +import eu.timepit.refined.numeric.Positive + +import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } +import dev.atedeg.mdm.products.utils.* +import dev.atedeg.mdm.products.utils.ReadShowInstances.given +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.serialization.* + +extension (p: ProductDTO) + def toDomain: Either[String, Product] = + for + weight <- p.weight.refined[Positive].map(Grams.apply) + cheeseType <- p.cheeseType.read[CheeseType] + product <- cheeseType + .withWeight(_ === weight.n.value) + .toRight(s"Couldn't find a `$cheeseType` with weight $weight") + yield product diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala new file mode 100644 index 00000000..9fda511c --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala @@ -0,0 +1,6 @@ +package dev.atedeg.mdm.products.dto + +import dev.atedeg.mdm.products.Product +import dev.atedeg.mdm.products.utils.ReadShowInstances.given + +extension (p: Product) def toDTO: ProductDTO = ProductDTO(p.cheeseType.show, p.weight.n.value) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala new file mode 100644 index 00000000..c8ce3e29 --- /dev/null +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala @@ -0,0 +1,44 @@ +package dev.atedeg.mdm.products.utils + +import cats.syntax.all.* + +import dev.atedeg.mdm.products.CheeseType +import dev.atedeg.mdm.products.CheeseType.* +import dev.atedeg.mdm.products.Ingredient +import dev.atedeg.mdm.products.Ingredient.* +import dev.atedeg.mdm.utils.serialization.* + +object ReadShowInstances: + given Show[CheeseType] with + override def toShow(a: CheeseType): String = a match + case Squacquerone => "squacquerone" + case Stracchino => "stracchino" + case Casatella => "casatella" + case Caciotta => "caciotta" + case Ricotta => "ricotta" + + given Read[CheeseType] with + override def fromString(s: String): Either[String, CheeseType] = s match + case "squacquerone" => Squacquerone.asRight[String] + case "stracchino" => Stracchino.asRight[String] + case "casatella" => Casatella.asRight[String] + case "caciotta" => Caciotta.asRight[String] + case "ricotta" => Ricotta.asRight[String] + case _ => "Unknown `CheeseType`: '$s'".asLeft[CheeseType] + + given Show[Ingredient] with + override def toShow(i: Ingredient): String = i match + case Probiotics => "probiotics" + case Rennet => "rennet" + case Cream => "cream" + case Milk => "milk" + case Salt => "salt" + + given Read[Ingredient] with + override def fromString(s: String): Either[String, Ingredient] = s match + case "probiotics" => Probiotics.asRight[String] + case "rennet" => Rennet.asRight[String] + case "cream" => Cream.asRight[String] + case "milk" => Milk.asRight[String] + case "salt" => Salt.asRight[String] + case _ => s"Unknown `Ingredient`: '$s'".asLeft[Ingredient] From 7edbfd669449a9cff441b1d2a34d6efca7576a86 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 17:31:44 +0200 Subject: [PATCH 216/329] style: reformat files --- .../atedeg/mdm/restocking/dto/FromDTO.scala | 31 +++++++++++++++++++ .../dev/atedeg/mdm/restocking/dto/ToDTO.scala | 13 ++++++++ 2 files changed, 44 insertions(+) create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala new file mode 100644 index 00000000..551ad6ea --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala @@ -0,0 +1,31 @@ +package dev.atedeg.mdm.restocking.dto + +import cats.syntax.all.* +import eu.timepit.refined.numeric.Positive +import eu.timepit.refined.predicates.all.NonNegative +import eu.timepit.refined.refineV + +import dev.atedeg.mdm.products.Ingredient +import dev.atedeg.mdm.products.utils.ReadShowInstances.given +import dev.atedeg.mdm.restocking.{ QuintalsOfIngredient, QuintalsOfMilk, WeightInQuintals } +import dev.atedeg.mdm.restocking.IncomingEvent.* +import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.serialization.* + +extension (omDTO: OrderMilkDTO) + def toDomain: Either[String, OrderMilk] = + omDTO.quintalsOfMilk.refined[Positive].map(QuintalsOfMilk.apply).map(OrderMilk.apply) + +extension (psDTO: ProductionStartedDTO) + def toDomain: Either[String, ProductionStarted] = + psDTO.quintalsOfIngredients.toNel + .toRight("The quintals of ingredients list is empty") + .flatMap(_.traverse(_.toDomain)) + .map(ProductionStarted.apply) + +extension (qoiDTO: QuintalsOfIngredientDTO) + def toDomain: Either[String, QuintalsOfIngredient] = + for + weight <- qoiDTO.quintals.refined[Positive].map(WeightInQuintals.apply) + ingredient <- qoiDTO.ingredient.read[Ingredient] + yield QuintalsOfIngredient(weight, ingredient) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala new file mode 100644 index 00000000..a9d67cba --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala @@ -0,0 +1,13 @@ +package dev.atedeg.mdm.restocking.dto + +import dev.atedeg.mdm.products.utils.ReadShowInstances.given +import dev.atedeg.mdm.restocking.IncomingEvent.* +import dev.atedeg.mdm.restocking.QuintalsOfIngredient + +extension (om: OrderMilk) def toDTO: OrderMilkDTO = OrderMilkDTO(om.quintals.quintals.value) + +extension (ps: ProductionStarted) + def toDTO: ProductionStartedDTO = ProductionStartedDTO(ps.ingredients.toList.map(_.toDTO)) + +extension (qoi: QuintalsOfIngredient) + def toDTO: QuintalsOfIngredientDTO = QuintalsOfIngredientDTO(qoi.quintals.n.value, qoi.ingredient.show) From 1f94f0dc061eabc1622d22fbe17a4083e3ec1bb8 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 17:32:19 +0200 Subject: [PATCH 217/329] refactor: move extension method out from trait --- .../dev/atedeg/mdm/utils/serialization/StringOps.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala new file mode 100644 index 00000000..c6d762fc --- /dev/null +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala @@ -0,0 +1,10 @@ +package dev.atedeg.mdm.utils.serialization + +trait Show[A]: + def toShow(a: A): String + extension (a: A) def show: String = toShow(a) + +trait Read[A]: + def fromString(s: String): Either[String, A] + +extension (s: String) def read[A](using r: Read[A]): Either[String, A] = r.fromString(s) From 84ff35ce01367bc561c09db785170f5691f28ac2 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 17:32:48 +0200 Subject: [PATCH 218/329] feat: add DTOs for milk-planning BC --- .../dev/atedeg/mdm/milkplanning/Events.scala | 2 +- .../atedeg/mdm/milkplanning/dto/DTOs.scala | 7 ++++ .../atedeg/mdm/milkplanning/dto/FromDTO.scala | 34 +++++++++++++++++++ .../atedeg/mdm/milkplanning/dto/ToDTO.scala | 21 ++++++++++++ .../mdm/milkplanning/types/ActionsTest.scala | 2 +- 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala index 61af4b67..afafab01 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Events.scala @@ -21,4 +21,4 @@ enum OutgoingEvent: * Event to order the [[QuintalsOfMilk quintals of milk]] needed for the next week. * This event is emitted every week on saturday. */ - case OrderMilk(n: QuintalsOfMilk) + case OrderMilk(quintalsOfMilk: QuintalsOfMilk) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala new file mode 100644 index 00000000..fc176b0b --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala @@ -0,0 +1,7 @@ +package dev.atedeg.mdm.milkplanning.dto + +import dev.atedeg.mdm.products.dto.ProductDTO + +final case class ReceivedOrderDTO(products: List[RequestedProductDTO]) +final case class RequestedProductDTO(product: ProductDTO, quantity: Int, requiredBy: String) +final case class OrderMilkDTO(quintals: Int) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala new file mode 100644 index 00000000..fbf9c999 --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala @@ -0,0 +1,34 @@ +package dev.atedeg.mdm.milkplanning.dto + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +import cats.syntax.all.* +import eu.timepit.refined.numeric.{ NonNegative, Positive } + +import dev.atedeg.mdm.milkplanning.{ Quantity, QuintalsOfMilk, RequestedProduct } +import dev.atedeg.mdm.milkplanning.IncomingEvent.* +import dev.atedeg.mdm.milkplanning.OutgoingEvent.* +import dev.atedeg.mdm.products.Product +import dev.atedeg.mdm.products.dto.toDomain as toProductDomain +import dev.atedeg.mdm.utils.* + +extension (ro: ReceivedOrderDTO) + def toDomain: Either[String, ReceivedOrder] = + ro.products.toNel + .toRight("The received order list is empty") + .flatMap(_.traverse(_.toDomain)) + .map(ReceivedOrder.apply) + +extension (rp: RequestedProductDTO) + def toDomain: Either[String, RequestedProduct] = + val formatter = DateTimeFormatter.ISO_DATE_TIME + for + quantity <- rp.quantity.refined[Positive].map(Quantity.apply) + product <- rp.product.toProductDomain + yield RequestedProduct(product, quantity, LocalDateTime.parse(rp.requiredBy, formatter)) + +extension (om: OrderMilkDTO) + def toDomain: Either[String, OrderMilk] = + for quintalsOfMilk <- om.quintals.refined[NonNegative].map(QuintalsOfMilk.apply) + yield OrderMilk(quintalsOfMilk) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala new file mode 100644 index 00000000..c631387d --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala @@ -0,0 +1,21 @@ +package dev.atedeg.mdm.milkplanning.dto + +import java.time.format.DateTimeFormatter + +import dev.atedeg.mdm.milkplanning.IncomingEvent.* +import dev.atedeg.mdm.milkplanning.OutgoingEvent.* +import dev.atedeg.mdm.milkplanning.RequestedProduct +import dev.atedeg.mdm.products.dto.* +import dev.atedeg.mdm.products.dto.toDTO as toProductDTO + +extension (ro: ReceivedOrder) def toDTO: ReceivedOrderDTO = ReceivedOrderDTO(ro.products.map(_.toDTO).toList) + +extension (rp: RequestedProduct) + def toDTO: RequestedProductDTO = + RequestedProductDTO( + rp.product.toProductDTO, + rp.quantity.n.value, + rp.requiredBy.format(DateTimeFormatter.ISO_DATE_TIME), + ) + +extension (om: OrderMilk) def toDTO: OrderMilkDTO = OrderMilkDTO(om.quintalsOfMilk.quintals.value) diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index 3c6d3bca..76da6952 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -64,6 +64,6 @@ class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with M .foldLeft(0.quintalsOfMilk)(_ + _) estimation should be > quintalsForRequestedProducts events should not be empty - events.map(_.n) should contain(estimation) + events.map(_.quintalsOfMilk) should contain(estimation) } } From 73de3fdff5e37030aa2006e3073e2e0aa7dc0a1c Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Sat, 6 Aug 2022 17:52:42 +0200 Subject: [PATCH 219/329] fix: fix date time formatter --- .../main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala | 2 +- .../src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala index fbf9c999..0e361b8f 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala @@ -22,7 +22,7 @@ extension (ro: ReceivedOrderDTO) extension (rp: RequestedProductDTO) def toDomain: Either[String, RequestedProduct] = - val formatter = DateTimeFormatter.ISO_DATE_TIME + val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME for quantity <- rp.quantity.refined[Positive].map(Quantity.apply) product <- rp.product.toProductDomain diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala index c631387d..cc7b350b 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala @@ -15,7 +15,7 @@ extension (rp: RequestedProduct) RequestedProductDTO( rp.product.toProductDTO, rp.quantity.n.value, - rp.requiredBy.format(DateTimeFormatter.ISO_DATE_TIME), + rp.requiredBy.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), ) extension (om: OrderMilk) def toDTO: OrderMilkDTO = OrderMilkDTO(om.quintalsOfMilk.quintals.value) From 17e3cec38fd020b878b2d026bc35e739de5f30a2 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 09:19:49 +0200 Subject: [PATCH 220/329] refactor: use new utilities to generate milk-planning DTOs --- .../atedeg/mdm/milkplanning/dto/DTOs.scala | 18 ++++++++++ .../atedeg/mdm/milkplanning/dto/FromDTO.scala | 34 ------------------- .../atedeg/mdm/milkplanning/dto/ToDTO.scala | 21 ------------ 3 files changed, 18 insertions(+), 55 deletions(-) delete mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala delete mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala index fc176b0b..cbd5d960 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala @@ -1,7 +1,25 @@ package dev.atedeg.mdm.milkplanning.dto +import dev.atedeg.mdm.milkplanning.{ Quantity, QuintalsOfMilk, RequestedProduct } +import dev.atedeg.mdm.milkplanning.IncomingEvent.ReceivedOrder +import dev.atedeg.mdm.milkplanning.OutgoingEvent.OrderMilk import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOGenerators.* +import dev.atedeg.mdm.utils.serialization.DTOOps.* final case class ReceivedOrderDTO(products: List[RequestedProductDTO]) final case class RequestedProductDTO(product: ProductDTO, quantity: Int, requiredBy: String) final case class OrderMilkDTO(quintals: Int) + +object ReceivedOrderDTO: + import dev.atedeg.mdm.milkplanning.dto.RequestedProductDTO.given + given DTO[ReceivedOrder, ReceivedOrderDTO] = interCaseClassDTO + +object RequestedProductDTO: + given DTO[RequestedProduct, RequestedProductDTO] = interCaseClassDTO + private given DTO[Quantity, Int] = caseClassDTO + +object OrderMilkDTO: + given DTO[OrderMilk, OrderMilkDTO] = interCaseClassDTO + private given DTO[QuintalsOfMilk, Int] = caseClassDTO diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala deleted file mode 100644 index 0e361b8f..00000000 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/FromDTO.scala +++ /dev/null @@ -1,34 +0,0 @@ -package dev.atedeg.mdm.milkplanning.dto - -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -import cats.syntax.all.* -import eu.timepit.refined.numeric.{ NonNegative, Positive } - -import dev.atedeg.mdm.milkplanning.{ Quantity, QuintalsOfMilk, RequestedProduct } -import dev.atedeg.mdm.milkplanning.IncomingEvent.* -import dev.atedeg.mdm.milkplanning.OutgoingEvent.* -import dev.atedeg.mdm.products.Product -import dev.atedeg.mdm.products.dto.toDomain as toProductDomain -import dev.atedeg.mdm.utils.* - -extension (ro: ReceivedOrderDTO) - def toDomain: Either[String, ReceivedOrder] = - ro.products.toNel - .toRight("The received order list is empty") - .flatMap(_.traverse(_.toDomain)) - .map(ReceivedOrder.apply) - -extension (rp: RequestedProductDTO) - def toDomain: Either[String, RequestedProduct] = - val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME - for - quantity <- rp.quantity.refined[Positive].map(Quantity.apply) - product <- rp.product.toProductDomain - yield RequestedProduct(product, quantity, LocalDateTime.parse(rp.requiredBy, formatter)) - -extension (om: OrderMilkDTO) - def toDomain: Either[String, OrderMilk] = - for quintalsOfMilk <- om.quintals.refined[NonNegative].map(QuintalsOfMilk.apply) - yield OrderMilk(quintalsOfMilk) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala deleted file mode 100644 index cc7b350b..00000000 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/ToDTO.scala +++ /dev/null @@ -1,21 +0,0 @@ -package dev.atedeg.mdm.milkplanning.dto - -import java.time.format.DateTimeFormatter - -import dev.atedeg.mdm.milkplanning.IncomingEvent.* -import dev.atedeg.mdm.milkplanning.OutgoingEvent.* -import dev.atedeg.mdm.milkplanning.RequestedProduct -import dev.atedeg.mdm.products.dto.* -import dev.atedeg.mdm.products.dto.toDTO as toProductDTO - -extension (ro: ReceivedOrder) def toDTO: ReceivedOrderDTO = ReceivedOrderDTO(ro.products.map(_.toDTO).toList) - -extension (rp: RequestedProduct) - def toDTO: RequestedProductDTO = - RequestedProductDTO( - rp.product.toProductDTO, - rp.quantity.n.value, - rp.requiredBy.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), - ) - -extension (om: OrderMilk) def toDTO: OrderMilkDTO = OrderMilkDTO(om.quintalsOfMilk.quintals.value) From 81e0a527a8b3a18232fbbda37a6573ebef467f03 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 09:23:47 +0200 Subject: [PATCH 221/329] refactor: remove no more needed files --- .../dev/atedeg/mdm/products/dto/FromDTO.scala | 20 --------- .../dev/atedeg/mdm/products/dto/ToDTO.scala | 6 --- .../products/utils/ReadShowInstances.scala | 44 ------------------- .../atedeg/mdm/restocking/dto/FromDTO.scala | 31 ------------- .../dev/atedeg/mdm/restocking/dto/ToDTO.scala | 13 ------ .../mdm/utils/serialization/StringOps.scala | 10 ----- 6 files changed, 124 deletions(-) delete mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala delete mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala delete mode 100644 products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala delete mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala delete mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala delete mode 100644 utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala deleted file mode 100644 index 1d4d2d34..00000000 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/FromDTO.scala +++ /dev/null @@ -1,20 +0,0 @@ -package dev.atedeg.mdm.products.dto - -import cats.syntax.all.* -import eu.timepit.refined.numeric.Positive - -import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } -import dev.atedeg.mdm.products.utils.* -import dev.atedeg.mdm.products.utils.ReadShowInstances.given -import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.serialization.* - -extension (p: ProductDTO) - def toDomain: Either[String, Product] = - for - weight <- p.weight.refined[Positive].map(Grams.apply) - cheeseType <- p.cheeseType.read[CheeseType] - product <- cheeseType - .withWeight(_ === weight.n.value) - .toRight(s"Couldn't find a `$cheeseType` with weight $weight") - yield product diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala deleted file mode 100644 index 9fda511c..00000000 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/ToDTO.scala +++ /dev/null @@ -1,6 +0,0 @@ -package dev.atedeg.mdm.products.dto - -import dev.atedeg.mdm.products.Product -import dev.atedeg.mdm.products.utils.ReadShowInstances.given - -extension (p: Product) def toDTO: ProductDTO = ProductDTO(p.cheeseType.show, p.weight.n.value) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala deleted file mode 100644 index c8ce3e29..00000000 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/utils/ReadShowInstances.scala +++ /dev/null @@ -1,44 +0,0 @@ -package dev.atedeg.mdm.products.utils - -import cats.syntax.all.* - -import dev.atedeg.mdm.products.CheeseType -import dev.atedeg.mdm.products.CheeseType.* -import dev.atedeg.mdm.products.Ingredient -import dev.atedeg.mdm.products.Ingredient.* -import dev.atedeg.mdm.utils.serialization.* - -object ReadShowInstances: - given Show[CheeseType] with - override def toShow(a: CheeseType): String = a match - case Squacquerone => "squacquerone" - case Stracchino => "stracchino" - case Casatella => "casatella" - case Caciotta => "caciotta" - case Ricotta => "ricotta" - - given Read[CheeseType] with - override def fromString(s: String): Either[String, CheeseType] = s match - case "squacquerone" => Squacquerone.asRight[String] - case "stracchino" => Stracchino.asRight[String] - case "casatella" => Casatella.asRight[String] - case "caciotta" => Caciotta.asRight[String] - case "ricotta" => Ricotta.asRight[String] - case _ => "Unknown `CheeseType`: '$s'".asLeft[CheeseType] - - given Show[Ingredient] with - override def toShow(i: Ingredient): String = i match - case Probiotics => "probiotics" - case Rennet => "rennet" - case Cream => "cream" - case Milk => "milk" - case Salt => "salt" - - given Read[Ingredient] with - override def fromString(s: String): Either[String, Ingredient] = s match - case "probiotics" => Probiotics.asRight[String] - case "rennet" => Rennet.asRight[String] - case "cream" => Cream.asRight[String] - case "milk" => Milk.asRight[String] - case "salt" => Salt.asRight[String] - case _ => s"Unknown `Ingredient`: '$s'".asLeft[Ingredient] diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala deleted file mode 100644 index 551ad6ea..00000000 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/FromDTO.scala +++ /dev/null @@ -1,31 +0,0 @@ -package dev.atedeg.mdm.restocking.dto - -import cats.syntax.all.* -import eu.timepit.refined.numeric.Positive -import eu.timepit.refined.predicates.all.NonNegative -import eu.timepit.refined.refineV - -import dev.atedeg.mdm.products.Ingredient -import dev.atedeg.mdm.products.utils.ReadShowInstances.given -import dev.atedeg.mdm.restocking.{ QuintalsOfIngredient, QuintalsOfMilk, WeightInQuintals } -import dev.atedeg.mdm.restocking.IncomingEvent.* -import dev.atedeg.mdm.utils.* -import dev.atedeg.mdm.utils.serialization.* - -extension (omDTO: OrderMilkDTO) - def toDomain: Either[String, OrderMilk] = - omDTO.quintalsOfMilk.refined[Positive].map(QuintalsOfMilk.apply).map(OrderMilk.apply) - -extension (psDTO: ProductionStartedDTO) - def toDomain: Either[String, ProductionStarted] = - psDTO.quintalsOfIngredients.toNel - .toRight("The quintals of ingredients list is empty") - .flatMap(_.traverse(_.toDomain)) - .map(ProductionStarted.apply) - -extension (qoiDTO: QuintalsOfIngredientDTO) - def toDomain: Either[String, QuintalsOfIngredient] = - for - weight <- qoiDTO.quintals.refined[Positive].map(WeightInQuintals.apply) - ingredient <- qoiDTO.ingredient.read[Ingredient] - yield QuintalsOfIngredient(weight, ingredient) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala deleted file mode 100644 index a9d67cba..00000000 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/ToDTO.scala +++ /dev/null @@ -1,13 +0,0 @@ -package dev.atedeg.mdm.restocking.dto - -import dev.atedeg.mdm.products.utils.ReadShowInstances.given -import dev.atedeg.mdm.restocking.IncomingEvent.* -import dev.atedeg.mdm.restocking.QuintalsOfIngredient - -extension (om: OrderMilk) def toDTO: OrderMilkDTO = OrderMilkDTO(om.quintals.quintals.value) - -extension (ps: ProductionStarted) - def toDTO: ProductionStartedDTO = ProductionStartedDTO(ps.ingredients.toList.map(_.toDTO)) - -extension (qoi: QuintalsOfIngredient) - def toDTO: QuintalsOfIngredientDTO = QuintalsOfIngredientDTO(qoi.quintals.n.value, qoi.ingredient.show) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala deleted file mode 100644 index c6d762fc..00000000 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/StringOps.scala +++ /dev/null @@ -1,10 +0,0 @@ -package dev.atedeg.mdm.utils.serialization - -trait Show[A]: - def toShow(a: A): String - extension (a: A) def show: String = toShow(a) - -trait Read[A]: - def fromString(s: String): Either[String, A] - -extension (s: String) def read[A](using r: Read[A]): Either[String, A] = r.fromString(s) From 3ddd426135e32a78f50f6ef8ba9ad619073caa57 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 09:35:26 +0200 Subject: [PATCH 222/329] refactor: address @giacomocavalieri suggestions --- .../main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala index cbd5d960..7ac4647d 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala @@ -13,11 +13,8 @@ final case class RequestedProductDTO(product: ProductDTO, quantity: Int, require final case class OrderMilkDTO(quintals: Int) object ReceivedOrderDTO: - import dev.atedeg.mdm.milkplanning.dto.RequestedProductDTO.given given DTO[ReceivedOrder, ReceivedOrderDTO] = interCaseClassDTO - -object RequestedProductDTO: - given DTO[RequestedProduct, RequestedProductDTO] = interCaseClassDTO + private given DTO[RequestedProduct, RequestedProductDTO] = interCaseClassDTO private given DTO[Quantity, Int] = caseClassDTO object OrderMilkDTO: From 7a24444f078aaf0d72728d3a57634676d77cb3a6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 8 Aug 2022 09:09:57 +0000 Subject: [PATCH 223/329] chore(release): 1.0.0-beta.11 [skip ci] # [1.0.0-beta.11](https://github.com/atedeg/mdm/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-08-08) ### Bug Fixes * fix date time formatter ([73de3fd](https://github.com/atedeg/mdm/commit/73de3fdff5e37030aa2006e3073e2e0aa7dc0a1c)) ### Features * add DTOs for milk-planning BC ([84ff35c](https://github.com/atedeg/mdm/commit/84ff35ce01367bc561c09db785170f5691f28ac2)) * add DTOs for product and cheesetype ([bf3711a](https://github.com/atedeg/mdm/commit/bf3711a0fdc9bc6e9c98f0bc068d241b9643b737)) --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5badac39..255a8a0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [1.0.0-beta.11](https://github.com/atedeg/mdm/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-08-08) + + +### Bug Fixes + +* fix date time formatter ([73de3fd](https://github.com/atedeg/mdm/commit/73de3fdff5e37030aa2006e3073e2e0aa7dc0a1c)) + + +### Features + +* add DTOs for milk-planning BC ([84ff35c](https://github.com/atedeg/mdm/commit/84ff35ce01367bc561c09db785170f5691f28ac2)) +* add DTOs for product and cheesetype ([bf3711a](https://github.com/atedeg/mdm/commit/bf3711a0fdc9bc6e9c98f0bc068d241b9643b737)) + # [1.0.0-beta.10](https://github.com/atedeg/mdm/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-08-07) From 0074fa63bf811481e3dbc1c4edd183c0130d7710 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 14:29:01 +0200 Subject: [PATCH 224/329] build: add tapir and https4s dependencies --- build.sbt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/build.sbt b/build.sbt index a15645df..1ac93731 100644 --- a/build.sbt +++ b/build.sbt @@ -59,6 +59,16 @@ val commonSettings = Seq( "org.typelevel" %% "cats-core" % "2.8.0", "org.typelevel" %% "cats-mtl" % "1.3.0", "org.typelevel" %% "shapeless3-deriving" % "3.1.0", + "org.http4s" %% "http4s-blaze-server" % "0.23.12", + "org.http4s" %% "http4s-circe" % "0.23.14", + "org.http4s" %% "http4s-dsl" % "0.23.14", + "io.circe" %% "circe-generic" % "0.14.2", + "io.circe" %% "circe-core" % "0.14.2", + "io.circe" %% "circe-generic" % "0.14.2", + "io.circe" %% "circe-parser" % "0.14.2", + "com.softwaremill.sttp.tapir" %% "tapir-core" % "1.0.3", + "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.0.3", + "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.0.3", ), ) From 02ffaef1f576b3569e7777ed93072e78b1e2fa30 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 14:29:35 +0200 Subject: [PATCH 225/329] chore(utils): DTO for generic Map --- .../scala/dev/atedeg/mdm/utils/serialization/DTO.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala index a45cab1c..c6729f9e 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala @@ -29,6 +29,14 @@ object DTO: given DTO[Double, Double] = idDTO given DTO[String, String] = idDTO + given mapDTO[KE, KD, VE, VD](using DTO[KE, KD])(using DTO[VE, VD]): DTO[Map[KE, VE], Map[KD, VD]] with + override def elemToDto(e: Map[KE, VE]): Map[KD, VD] = e.map(_.bimap(_.toDTO[KD], _.toDTO[VD])) + override def dtoToElem(dto: Map[KD, VD]): Either[String, Map[KE, VE]] = + for + keys <- dto.keys.toList.traverse(_.toDomain[KE]) + values <- dto.values.toList.traverse(_.toDomain[VE]) + yield keys.zip(values).toMap + given listDTO[E, D](using DTO[E, D]): DTO[List[E], List[D]] with override def dtoToElem(dto: List[D]): Either[String, List[E]] = dto.traverse(_.toDomain) override def elemToDto(e: List[E]): List[D] = e.map(_.toDTO) From cffaa9f4bb42e000e27aa4905daeb6c47b0c59c5 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 14:30:02 +0200 Subject: [PATCH 226/329] feat: add StockDTO --- .../src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala index 2dfca756..875d09e7 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/dto/DTOs.scala @@ -10,6 +10,7 @@ import dev.atedeg.mdm.utils.serialization.DTOOps.* final case class OrderMilkDTO(quintalsOfMilk: Int) final case class ProductionStartedDTO(quintalsOfIngredients: List[QuintalsOfIngredientDTO]) final case class QuintalsOfIngredientDTO(quintals: Double, ingredient: String) +type StockDTO = Map[String, Double] object OrderMilkDTO: given DTO[OrderMilk, OrderMilkDTO] = interCaseClassDTO @@ -21,3 +22,8 @@ object ProductionStartedDTO: object QuintalsOfIngredientDTO: given DTO[QuintalsOfIngredient, QuintalsOfIngredientDTO] = interCaseClassDTO private given DTO[WeightInQuintals, Double] = caseClassDTO + +object StockDTO: + import dev.atedeg.mdm.products.dto.IngredientDTO.given + given DTO[Stock, StockDTO] = DTO.mapDTO + private given DTO[StockedQuantity, Double] = caseClassDTO From 16b6648ac9c1680529e4e6bee089be02f41448e4 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 14:30:22 +0200 Subject: [PATCH 227/329] feat: create logic handlers --- .../atedeg/mdm/restocking/api/Handlers.scala | 43 +++++++++++++++++++ .../dev/atedeg/mdm/restocking/api/Types.scala | 5 +++ .../atedeg/mdm/restocking/api/dto/DTOs.scala | 12 ++++++ 3 files changed, 60 insertions(+) create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Types.scala create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/api/dto/DTOs.scala diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala new file mode 100644 index 00000000..637ad9cd --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala @@ -0,0 +1,43 @@ +package dev.atedeg.mdm.restocking.api + +import cats.Monad +import cats.data.{ EitherT, Kleisli, ReaderT } +import cats.effect.IO +import cats.syntax.all.* + +import dev.atedeg.mdm.products.dto.IngredientDTO.given +import dev.atedeg.mdm.restocking.* +import dev.atedeg.mdm.restocking.IncomingEvent.* +import dev.atedeg.mdm.restocking.Stock +import dev.atedeg.mdm.restocking.api.dto.RemainingMilkDTO +import dev.atedeg.mdm.restocking.dto.{ OrderMilkDTO, ProductionStartedDTO, StockDTO } +import dev.atedeg.mdm.restocking.dto.StockDTO.given +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +type App[C, E, R] = EitherT[[A] =>> ReaderT[IO, C, A], E, R] +final case class DBClient(cs: String) + +def remaningQuintalsOfMilkHandler: App[DBClient, String, RemainingMilkDTO] = + for + remainingMilkDTO <- readQuintalsFromDB + remainingMilk <- remainingMilkDTO.toDomain[RemainingMilk].toEitherT + yield remainingMilk.toDTO[RemainingMilkDTO] + +def orderMilkHandler(orderMilkDTO: OrderMilkDTO): EitherT[IO, String, Unit] = + for + orderMilk <- orderMilkDTO.toDomain[OrderMilk].toEitherT + _ <- makeMilkOrder(orderMilk.toDTO[OrderMilkDTO]) + yield () + +def productionStartedHandler(productionStartedDTO: ProductionStartedDTO): EitherT[IO, String, Unit] = + for + productionStarted <- productionStartedDTO.toDomain[ProductionStarted].toEitherT + stock <- readStockFromDB >>= (_.toDomain[Stock].toEitherT) + newStock = consumeIngredients(stock)(productionStarted.ingredients) + _ <- writeStockToDB(newStock.toDTO[StockDTO]) + yield () + +private def readQuintalsFromDB: App[DBClient, String, RemainingMilkDTO] = ??? +private def makeMilkOrder(orderMilkDTO: OrderMilkDTO): EitherT[IO, String, Unit] = ??? +private def readStockFromDB: EitherT[IO, String, StockDTO] = ??? +private def writeStockToDB(newStock: StockDTO): EitherT[IO, String, Unit] = ??? diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Types.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Types.scala new file mode 100644 index 00000000..351721d1 --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Types.scala @@ -0,0 +1,5 @@ +package dev.atedeg.mdm.restocking.api + +import dev.atedeg.mdm.restocking.QuintalsOfMilk + +final case class RemainingMilk(quintalsOfMilk: QuintalsOfMilk) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/dto/DTOs.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/dto/DTOs.scala new file mode 100644 index 00000000..ee12aab3 --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/dto/DTOs.scala @@ -0,0 +1,12 @@ +package dev.atedeg.mdm.restocking.api.dto + +import dev.atedeg.mdm.restocking.QuintalsOfMilk +import dev.atedeg.mdm.restocking.api.RemainingMilk +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOGenerators.* + +final case class RemainingMilkDTO(quintalsOfMilk: Int) + +object RemainingMilkDTO: + given DTO[RemainingMilk, RemainingMilkDTO] = interCaseClassDTO + private given DTO[QuintalsOfMilk, Int] = caseClassDTO From 7804e5ad01d645d7d115f35223cc5e42bb85b37f Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 8 Aug 2022 15:13:35 +0200 Subject: [PATCH 228/329] build: add cats effects dependency --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index 1ac93731..7e9a5331 100644 --- a/build.sbt +++ b/build.sbt @@ -58,6 +58,7 @@ val commonSettings = Seq( "eu.timepit" %% "refined" % "0.10.1", "org.typelevel" %% "cats-core" % "2.8.0", "org.typelevel" %% "cats-mtl" % "1.3.0", + "org.typelevel" %% "cats-effect" % "3.3.14", "org.typelevel" %% "shapeless3-deriving" % "3.1.0", "org.http4s" %% "http4s-blaze-server" % "0.23.12", "org.http4s" %% "http4s-circe" % "0.23.14", From 82d561e222779b6a4f844c427ca4af1ad0996dd8 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 8 Aug 2022 15:14:26 +0200 Subject: [PATCH 229/329] chore: add new kind of side effect --- .../dev/atedeg/mdm/utils/monads/Monads.scala | 22 ++++++++++++++++--- .../dev/atedeg/mdm/utils/monads/Stacks.scala | 1 - 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala index 70910006..87c6c71f 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala @@ -2,7 +2,8 @@ package dev.atedeg.mdm.utils.monads import cats.{ Monad, Traverse } import cats.data.NonEmptyList -import cats.mtl.{ Raise, Tell } +import cats.effect.{ IO, LiftIO } +import cats.mtl.{ Ask, Raise, Tell } import cats.syntax.all.* /** @@ -22,6 +23,11 @@ type Emits[Emitted] = [M[_]] =>> Tell[M, List[Emitted]] */ type CanRaise[Raised] = [M[_]] =>> Raise[M, Raised] +/** + * Signals that a method can read a state `C`. + */ +type CanRead[C] = [F[_]] =>> Ask[F, C] + /** * Emits an element of type `E` in a context `M[_]` that accumulates emitted elements in a list. */ @@ -32,6 +38,11 @@ def emit[M[_], E](e: E)(using T: Tell[M, List[E]]): M[Unit] = T.tell(List(e)) */ def raise[M[_], E, A](e: E)(using R: Raise[M, E]): M[A] = R.raise(e) +/** + * Reads the current global state in a context `M[_]` with a global state `C`. + */ +def readState[C, M[_]: Monad: CanRead[C]](implicit A: Ask[M, C]): M[C] = A.ask + /** * `unless(cond)(a)` performs the monadic action `a` if the condition `cond` is false. */ @@ -42,6 +53,13 @@ def unless[M[_], A](cond: => Boolean)(action: => M[A])(using M: Monad[M]): M[Uni */ def when[M[_], A](cond: => Boolean)(action: => M[A])(using M: Monad[M]): M[Unit] = M.whenA(cond)(action) +extension [A](a: IO[A]) def liftIO[F[_]](using L: LiftIO[F]): F[A] = L.liftIO(a) +extension [A](a: => A) def performSyncIO[F[_]](using L: LiftIO[F]): F[A] = IO(a).liftIO +extension [A, B](e: Either[A, B]) + def getOrRaise[M[_]: Monad: CanRaise[A]]: M[B] = e match + case Left(err) => raise(err) + case Right(res) => res.pure + extension [M[_]: Monad, A](ma: M[A]) /** * `ma.thenReturn(b)` performs the monadic action `ma`, ignores its return value @@ -62,7 +80,6 @@ extension [M[_]: Monad, A](ma: M[A]) def ignore: M[Unit] = ma *> ().pure extension (condition: Boolean) - /** * `cond.otherwiseRaise(err)` [[raise() raises]] the error `err` if the condition `cond` is false. */ @@ -70,7 +87,6 @@ extension (condition: Boolean) unless[M, Boolean](condition)(raise(error)).map(_ => condition) extension [A](a: Option[A]) - /** * `opt.ifMissingRaise(err)` [[raise() raises]] the error `err` if `opt` is `None`, * otherwise returns its value inside the context `M[_]`. diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala index 6b982acb..aa089151 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala @@ -27,7 +27,6 @@ type SafeAction[Event, Result] = Writer[List[Event], Result] type SafeActionTwoEvents[Event1, Event2, Result] = WriterT[[A] =>> Writer[List[Event2], A], List[Event1], Result] extension [Event1, Event2, Result](action: SafeActionTwoEvents[Event1, Event2, Result]) - def execute: (List[Event1], List[Event2], Result) = val (e2, (e1, res)) = action.run.run (e1, e2, res) From 8e1f2494293bdf9d9c4ad0b494ee860aa5e8a909 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 8 Aug 2022 15:14:54 +0200 Subject: [PATCH 230/329] refactor: change apis so that they are generic on the used monad stack --- .../atedeg/mdm/restocking/api/Handlers.scala | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala index 637ad9cd..0908d998 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala @@ -1,8 +1,7 @@ package dev.atedeg.mdm.restocking.api import cats.Monad -import cats.data.{ EitherT, Kleisli, ReaderT } -import cats.effect.IO +import cats.effect.LiftIO import cats.syntax.all.* import dev.atedeg.mdm.products.dto.IngredientDTO.given @@ -12,32 +11,33 @@ import dev.atedeg.mdm.restocking.Stock import dev.atedeg.mdm.restocking.api.dto.RemainingMilkDTO import dev.atedeg.mdm.restocking.dto.{ OrderMilkDTO, ProductionStartedDTO, StockDTO } import dev.atedeg.mdm.restocking.dto.StockDTO.given +import dev.atedeg.mdm.utils.monads.* import dev.atedeg.mdm.utils.serialization.DTOOps.* -type App[C, E, R] = EitherT[[A] =>> ReaderT[IO, C, A], E, R] -final case class DBClient(cs: String) - -def remaningQuintalsOfMilkHandler: App[DBClient, String, RemainingMilkDTO] = +final case class DBClient() +def remaningQuintalsOfMilkHandler[M[_]: Monad: LiftIO: CanRead[DBClient]: CanRaise[String]]: M[RemainingMilkDTO] = for remainingMilkDTO <- readQuintalsFromDB - remainingMilk <- remainingMilkDTO.toDomain[RemainingMilk].toEitherT + remainingMilk <- remainingMilkDTO.toDomain[RemainingMilk].getOrRaise yield remainingMilk.toDTO[RemainingMilkDTO] -def orderMilkHandler(orderMilkDTO: OrderMilkDTO): EitherT[IO, String, Unit] = +def orderMilkHandler[M[_]: Monad: LiftIO: CanRaise[String]](orderMilkDTO: OrderMilkDTO): M[Unit] = for - orderMilk <- orderMilkDTO.toDomain[OrderMilk].toEitherT + orderMilk <- orderMilkDTO.toDomain[OrderMilk].getOrRaise _ <- makeMilkOrder(orderMilk.toDTO[OrderMilkDTO]) yield () -def productionStartedHandler(productionStartedDTO: ProductionStartedDTO): EitherT[IO, String, Unit] = +def productionStartedHandler[M[_]: Monad: LiftIO: CanRaise[String]]( + productionStartedDTO: ProductionStartedDTO, +): M[Unit] = for - productionStarted <- productionStartedDTO.toDomain[ProductionStarted].toEitherT - stock <- readStockFromDB >>= (_.toDomain[Stock].toEitherT) + productionStarted <- productionStartedDTO.toDomain[ProductionStarted].getOrRaise + stock <- readStockFromDB >>= (_.toDomain[Stock].getOrRaise) newStock = consumeIngredients(stock)(productionStarted.ingredients) _ <- writeStockToDB(newStock.toDTO[StockDTO]) yield () -private def readQuintalsFromDB: App[DBClient, String, RemainingMilkDTO] = ??? -private def makeMilkOrder(orderMilkDTO: OrderMilkDTO): EitherT[IO, String, Unit] = ??? -private def readStockFromDB: EitherT[IO, String, StockDTO] = ??? -private def writeStockToDB(newStock: StockDTO): EitherT[IO, String, Unit] = ??? +private def readQuintalsFromDB[M[_]: Monad: LiftIO]: M[RemainingMilkDTO] = ??? +private def makeMilkOrder[M[_]: Monad: LiftIO](orderMilkDTO: OrderMilkDTO): M[Unit] = ??? +private def readStockFromDB[M[_]: Monad: LiftIO]: M[StockDTO] = ??? +private def writeStockToDB[M[_]: Monad: LiftIO](newStock: StockDTO): M[Unit] = ??? From 7f9b994b99941443a15e682b4e43f255c53ebd8a Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 16:29:14 +0200 Subject: [PATCH 231/329] chore(utils): add stack for server actions --- .../main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala index aa089151..baad043e 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala @@ -1,6 +1,7 @@ package dev.atedeg.mdm.utils.monads -import cats.data.{ EitherT, Reader, Writer, WriterT } +import cats.data.{ EitherT, Reader, ReaderT, Writer, WriterT } +import cats.effect.IO private type Reading[State] = [A] =>> Reader[State, A] private type EmittingT[M[_], Event] = [A] =>> WriterT[M, List[Event], A] @@ -26,6 +27,12 @@ type SafeAction[Event, Result] = Writer[List[Event], Result] */ type SafeActionTwoEvents[Event1, Event2, Result] = WriterT[[A] =>> Writer[List[Event2], A], List[Event1], Result] +/** + * An action which performs `IO`, reads an immutable state `C` and can either fail with an error `E` + * or produce a result `R`. + */ +type ServerAction[C, E, R] = EitherT[[A] =>> ReaderT[IO, C, A], E, R] + extension [Event1, Event2, Result](action: SafeActionTwoEvents[Event1, Event2, Result]) def execute: (List[Event1], List[Event2], Result) = val (e2, (e1, res)) = action.run.run From 59fa6210c778119477445709bc761e7fb519e2b0 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 16:30:07 +0200 Subject: [PATCH 232/329] build: add tapir dependencies for circe --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 7e9a5331..6c302f22 100644 --- a/build.sbt +++ b/build.sbt @@ -67,7 +67,7 @@ val commonSettings = Seq( "io.circe" %% "circe-core" % "0.14.2", "io.circe" %% "circe-generic" % "0.14.2", "io.circe" %% "circe-parser" % "0.14.2", - "com.softwaremill.sttp.tapir" %% "tapir-core" % "1.0.3", + "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.0.3", "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.0.3", "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.0.3", ), From 6ff71ab44f86aa59df6049928f6dd115c452b60a Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 16:31:05 +0200 Subject: [PATCH 233/329] feat: creates api endpoints specifications --- .../atedeg/mdm/restocking/api/Handlers.scala | 8 +++--- .../restocking/api/endpoints/Endpoints.scala | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala index 0908d998..26091aa4 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala @@ -37,7 +37,7 @@ def productionStartedHandler[M[_]: Monad: LiftIO: CanRaise[String]]( _ <- writeStockToDB(newStock.toDTO[StockDTO]) yield () -private def readQuintalsFromDB[M[_]: Monad: LiftIO]: M[RemainingMilkDTO] = ??? -private def makeMilkOrder[M[_]: Monad: LiftIO](orderMilkDTO: OrderMilkDTO): M[Unit] = ??? -private def readStockFromDB[M[_]: Monad: LiftIO]: M[StockDTO] = ??? -private def writeStockToDB[M[_]: Monad: LiftIO](newStock: StockDTO): M[Unit] = ??? +private def readQuintalsFromDB[M[_]: Monad: LiftIO]: M[RemainingMilkDTO] = RemainingMilkDTO(10).pure +private def makeMilkOrder[M[_]: Monad: LiftIO](orderMilkDTO: OrderMilkDTO): M[Unit] = ().pure +private def readStockFromDB[M[_]: Monad: LiftIO]: M[StockDTO] = Map("Milk" -> 10.2).pure +private def writeStockToDB[M[_]: Monad: LiftIO](newStock: StockDTO): M[Unit] = ().pure diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala new file mode 100644 index 00000000..f7a4ee53 --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala @@ -0,0 +1,28 @@ +package dev.atedeg.mdm.restocking.api.endpoints + +import cats.data.{ EitherT, ReaderT } +import cats.effect.IO +import io.circe.generic.auto.* +import org.http4s.HttpRoutes +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.jsonBody +import sttp.tapir.server.http4s.Http4sServerInterpreter + +import dev.atedeg.mdm.restocking.api.{ remaningQuintalsOfMilkHandler, DBClient } +import dev.atedeg.mdm.restocking.api.dto.RemainingMilkDTO +import dev.atedeg.mdm.utils.monads.ServerAction + +object RemainingQuintalsOfMilkEndpoint: + private val handler: ServerAction[DBClient, String, RemainingMilkDTO] = remaningQuintalsOfMilkHandler + + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val remainingQuintalsOfMilkEndpoint: PublicEndpoint[Unit, String, RemainingMilkDTO, Any] = + endpoint.get + .in("remaining-quintals-of-milk") + .out(jsonBody[RemainingMilkDTO].description("The quintals of milk remaining in stock")) + .errorOut(stringBody) + + val remainingQuintalsOfMilkRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( + remainingQuintalsOfMilkEndpoint.serverLogic(_ => handler.value.run(DBClient())), + ) From 3647c21f3e0875d5888846eee3511b31d7fbd05b Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 16:31:23 +0200 Subject: [PATCH 234/329] chore: create application entrypoint --- .../dev/atedeg/mdm/restocking/Main.scala | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala new file mode 100644 index 00000000..9abea15f --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala @@ -0,0 +1,36 @@ +package dev.atedeg.mdm.restocking + +import scala.concurrent.ExecutionContext +import scala.util.Properties + +import cats.effect.{ ExitCode, IO, IOApp } +import cats.syntax.all.* +import org.http4s.HttpRoutes +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router +import sttp.tapir.server.http4s.Http4sServerInterpreter +import sttp.tapir.swagger.bundle.SwaggerInterpreter + +import dev.atedeg.mdm.restocking.api.endpoints.RemainingQuintalsOfMilkEndpoint + +object Main extends IOApp: + private val swaggerEndpoint = SwaggerInterpreter().fromEndpoints[IO]( + RemainingQuintalsOfMilkEndpoint.remainingQuintalsOfMilkEndpoint :: Nil, + "Restocking", + "1.0.0-beta.11", // TODO: dynamic version from build.sbt (?) + ) + private val swaggerRoute = Http4sServerInterpreter[IO]().toRoutes(swaggerEndpoint) + + private val routes: HttpRoutes[IO] = RemainingQuintalsOfMilkEndpoint.remainingQuintalsOfMilkRoute <+> swaggerRoute + + @SuppressWarnings(Array("org.wartremover.warts.GlobalExecutionContext")) + implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global + + override def run(args: List[String]): IO[ExitCode] = + BlazeServerBuilder[IO] + .bindHttp(Properties.envOrElse("PORT", "8080").toInt, Properties.envOrElse("HOST", "localhost")) + .withExecutionContext(ec) + .withHttpApp(Router("/" -> routes).orNotFound) + .resource + .use(_ => IO.println("Started") >> IO.never[Unit]) + .as(ExitCode.Success) From 01b831f5a9630665bc2b303d4d2962f46e9af140 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 16:36:09 +0200 Subject: [PATCH 235/329] refactor: remove explicit execution context --- .../src/main/scala/dev/atedeg/mdm/restocking/Main.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala index 9abea15f..ea1fc04e 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala @@ -23,13 +23,9 @@ object Main extends IOApp: private val routes: HttpRoutes[IO] = RemainingQuintalsOfMilkEndpoint.remainingQuintalsOfMilkRoute <+> swaggerRoute - @SuppressWarnings(Array("org.wartremover.warts.GlobalExecutionContext")) - implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO] .bindHttp(Properties.envOrElse("PORT", "8080").toInt, Properties.envOrElse("HOST", "localhost")) - .withExecutionContext(ec) .withHttpApp(Router("/" -> routes).orNotFound) .resource .use(_ => IO.println("Started") >> IO.never[Unit]) From b907503c34ca2a56015ea8f34aeb371c73bb8579 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 16:40:16 +0200 Subject: [PATCH 236/329] chore: set API version --- restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala index ea1fc04e..e09eb2d3 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala @@ -17,7 +17,7 @@ object Main extends IOApp: private val swaggerEndpoint = SwaggerInterpreter().fromEndpoints[IO]( RemainingQuintalsOfMilkEndpoint.remainingQuintalsOfMilkEndpoint :: Nil, "Restocking", - "1.0.0-beta.11", // TODO: dynamic version from build.sbt (?) + Properties.envOrElse("VERSION", "v1-beta"), ) private val swaggerRoute = Http4sServerInterpreter[IO]().toRoutes(swaggerEndpoint) From 6d62e93c119e25a592e896373df813df67b14782 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 17:14:15 +0200 Subject: [PATCH 237/329] feat: add stock repository --- .../api/repositories/StockRepository.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 restocking/src/main/scala/dev/atedeg/mdm/restocking/api/repositories/StockRepository.scala diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/repositories/StockRepository.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/repositories/StockRepository.scala new file mode 100644 index 00000000..8d76c1b2 --- /dev/null +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/repositories/StockRepository.scala @@ -0,0 +1,17 @@ +package dev.atedeg.mdm.restocking.api.repositories + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.restocking.api.dto.RemainingMilkDTO +import dev.atedeg.mdm.restocking.dto.{ OrderMilkDTO, StockDTO } + +trait StockRepository: + def getQuintals[M[_]: Monad: LiftIO]: M[RemainingMilkDTO] + def getStock[M[_]: Monad: LiftIO]: M[StockDTO] + def writeStock[M[_]: Monad: LiftIO](newStock: StockDTO): M[Unit] + +final case class DBStockRepository(connection: String) extends StockRepository: + override def getQuintals[M[_]: Monad: LiftIO]: M[RemainingMilkDTO] = ??? + override def getStock[M[_]: Monad: LiftIO]: M[StockDTO] = ??? + override def writeStock[M[_]: Monad: LiftIO](newStock: StockDTO): M[Unit] = ??? From e5668693eb6d788d86a31cb655786f94e7517b3e Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 17:14:27 +0200 Subject: [PATCH 238/329] chore: use stock repository --- .../atedeg/mdm/restocking/api/Handlers.scala | 17 ++++++++--------- .../restocking/api/endpoints/Endpoints.scala | 7 ++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala index 26091aa4..c4a6fc7a 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala @@ -9,15 +9,16 @@ import dev.atedeg.mdm.restocking.* import dev.atedeg.mdm.restocking.IncomingEvent.* import dev.atedeg.mdm.restocking.Stock import dev.atedeg.mdm.restocking.api.dto.RemainingMilkDTO +import dev.atedeg.mdm.restocking.api.repositories.StockRepository import dev.atedeg.mdm.restocking.dto.{ OrderMilkDTO, ProductionStartedDTO, StockDTO } import dev.atedeg.mdm.restocking.dto.StockDTO.given import dev.atedeg.mdm.utils.monads.* import dev.atedeg.mdm.utils.serialization.DTOOps.* -final case class DBClient() -def remaningQuintalsOfMilkHandler[M[_]: Monad: LiftIO: CanRead[DBClient]: CanRaise[String]]: M[RemainingMilkDTO] = +def remaningQuintalsOfMilkHandler[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]] + : M[RemainingMilkDTO] = for - remainingMilkDTO <- readQuintalsFromDB + remainingMilkDTO <- readState >>= (_.getQuintals) remainingMilk <- remainingMilkDTO.toDomain[RemainingMilk].getOrRaise yield remainingMilk.toDTO[RemainingMilkDTO] @@ -27,17 +28,15 @@ def orderMilkHandler[M[_]: Monad: LiftIO: CanRaise[String]](orderMilkDTO: OrderM _ <- makeMilkOrder(orderMilk.toDTO[OrderMilkDTO]) yield () -def productionStartedHandler[M[_]: Monad: LiftIO: CanRaise[String]]( +def productionStartedHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[StockRepository]]( productionStartedDTO: ProductionStartedDTO, ): M[Unit] = for + stockRepository <- readState productionStarted <- productionStartedDTO.toDomain[ProductionStarted].getOrRaise - stock <- readStockFromDB >>= (_.toDomain[Stock].getOrRaise) + stock <- stockRepository.getStock >>= (_.toDomain[Stock].getOrRaise) newStock = consumeIngredients(stock)(productionStarted.ingredients) - _ <- writeStockToDB(newStock.toDTO[StockDTO]) + _ <- stockRepository.writeStock(newStock.toDTO[StockDTO]) yield () -private def readQuintalsFromDB[M[_]: Monad: LiftIO]: M[RemainingMilkDTO] = RemainingMilkDTO(10).pure private def makeMilkOrder[M[_]: Monad: LiftIO](orderMilkDTO: OrderMilkDTO): M[Unit] = ().pure -private def readStockFromDB[M[_]: Monad: LiftIO]: M[StockDTO] = Map("Milk" -> 10.2).pure -private def writeStockToDB[M[_]: Monad: LiftIO](newStock: StockDTO): M[Unit] = ().pure diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala index f7a4ee53..ac34ab09 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala @@ -9,12 +9,13 @@ import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.jsonBody import sttp.tapir.server.http4s.Http4sServerInterpreter -import dev.atedeg.mdm.restocking.api.{ remaningQuintalsOfMilkHandler, DBClient } import dev.atedeg.mdm.restocking.api.dto.RemainingMilkDTO +import dev.atedeg.mdm.restocking.api.remaningQuintalsOfMilkHandler +import dev.atedeg.mdm.restocking.api.repositories.{ DBStockRepository, StockRepository } import dev.atedeg.mdm.utils.monads.ServerAction object RemainingQuintalsOfMilkEndpoint: - private val handler: ServerAction[DBClient, String, RemainingMilkDTO] = remaningQuintalsOfMilkHandler + private val handler: ServerAction[StockRepository, String, RemainingMilkDTO] = remaningQuintalsOfMilkHandler @SuppressWarnings(Array("org.wartremover.warts.Any")) val remainingQuintalsOfMilkEndpoint: PublicEndpoint[Unit, String, RemainingMilkDTO, Any] = @@ -24,5 +25,5 @@ object RemainingQuintalsOfMilkEndpoint: .errorOut(stringBody) val remainingQuintalsOfMilkRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( - remainingQuintalsOfMilkEndpoint.serverLogic(_ => handler.value.run(DBClient())), + remainingQuintalsOfMilkEndpoint.serverLogic(_ => handler.value.run(DBStockRepository("conn-string"))), ) From 16c2dc9fafce9b378fc76359efdf44593572bb3b Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 17:44:25 +0200 Subject: [PATCH 239/329] chore(utils): add extension method in server action --- .../scala/dev/atedeg/mdm/utils/monads/Stacks.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala index baad043e..1873c119 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Stacks.scala @@ -31,7 +31,7 @@ type SafeActionTwoEvents[Event1, Event2, Result] = WriterT[[A] =>> Writer[List[E * An action which performs `IO`, reads an immutable state `C` and can either fail with an error `E` * or produce a result `R`. */ -type ServerAction[C, E, R] = EitherT[[A] =>> ReaderT[IO, C, A], E, R] +type ServerAction[Config, Error, Result] = EitherT[[A] =>> ReaderT[IO, Config, A], Error, Result] extension [Event1, Event2, Result](action: SafeActionTwoEvents[Event1, Event2, Result]) def execute: (List[Event1], List[Event2], Result) = @@ -49,6 +49,14 @@ extension [Error, Event, Result, State](action: ActionWithState[Error, Event, Re extension [Error, Event, Result](action: Action[Error, Event, Result]) /** - * `a.execute(s)` runs the [[Action action]] a returning all the emitted events and its return value. + * `a.execute(s)` runs the [[Action action]] `a` returning all the emitted events and its return value. */ def execute: (List[Event], Either[Error, Result]) = action.value.run(()) + +import cats.effect.unsafe.implicits.global +extension [Config, Error, Result](sa: ServerAction[Config, Error, Result]) + /** + * `sa.unsafeExecute(config)` runs the [[ServerAction server action]] with a configuration. + * @note Don't use it in production! + */ + def unsafeExecute(config: Config): Either[Error, Result] = sa.value.run(config).unsafeRunSync() From 4ffc1ba1973c97ad285f312ae4fafbdae62c32f1 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 17:44:41 +0200 Subject: [PATCH 240/329] test: add endpoint handler's tests --- .../mdm/restocking/api/HandlersTest.scala | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala diff --git a/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala b/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala new file mode 100644 index 00000000..d1c48963 --- /dev/null +++ b/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala @@ -0,0 +1,44 @@ +package dev.atedeg.mdm.restocking.api + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* +import org.scalatest.EitherValues.* +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import dev.atedeg.mdm.restocking.api.dto.RemainingMilkDTO +import dev.atedeg.mdm.restocking.api.repositories.StockRepository +import dev.atedeg.mdm.restocking.dto.{ ProductionStartedDTO, QuintalsOfIngredientDTO, StockDTO } +import dev.atedeg.mdm.utils.monads.* + +trait Mocks: + @SuppressWarnings(Array("org.wartremover.warts.Var")) + var inMemoryStockDTO: Option[StockDTO] = None + val stockRepository: StockRepository = new StockRepository: + override def getQuintals[M[_]: Monad: LiftIO]: M[RemainingMilkDTO] = RemainingMilkDTO(10).pure + override def getStock[M[_]: Monad: LiftIO]: M[StockDTO] = Map("milk" -> 20.0, "salt" -> 3.0, "rennet" -> 30.5).pure + override def writeStock[M[_]: Monad: LiftIO](newStock: StockDTO): M[Unit] = + inMemoryStockDTO = Some(newStock) + ().pure + +class HandlersTest extends AnyWordSpec, Matchers, Mocks: + "The `remaningQuintalsOfMilkHandler`" should { + "return the same value it reads from the DB" in { + val handler: ServerAction[StockRepository, String, RemainingMilkDTO] = remaningQuintalsOfMilkHandler + val result = handler.unsafeExecute(stockRepository) + result.value shouldBe RemainingMilkDTO(10) + } + } + + "The `productionStartedHandler`" should { + "write the new stock to DB" in { + val handler: ServerAction[StockRepository, String, Unit] = productionStartedHandler( + ProductionStartedDTO( + List(QuintalsOfIngredientDTO(10.0, "milk")), + ), + ) + handler.unsafeExecute(stockRepository) + inMemoryStockDTO should contain(Map("milk" -> 10.0, "salt" -> 3.0, "rennet" -> 30.5)) + } + } From 3112920270f41396565a296843fa7e668736a53c Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Mon, 8 Aug 2022 18:04:34 +0200 Subject: [PATCH 241/329] build: add docker configuration for restocking --- build.sbt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.sbt b/build.sbt index 6c302f22..b43357e3 100644 --- a/build.sbt +++ b/build.sbt @@ -71,6 +71,7 @@ val commonSettings = Seq( "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.0.3", "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.0.3", ), + dockerEnvVars := Map("PORT" -> "8080", "HOST" -> "0.0.0.0"), ) addCommandAlias("ubidocGenerate", "clean; unidoc; ubidoc; clean; unidoc") @@ -149,8 +150,14 @@ lazy val `client-orders` = project .dependsOn(utils, `products-shared-kernel`) lazy val restocking = project + .enablePlugins(DockerPlugin, JavaAppPackaging) .in(file("restocking")) .settings(commonSettings) + .settings( + Docker / packageName := packageName.value, + Docker / version := version.value, + dockerExposedPorts := Seq(8080), + ) .dependsOn(utils, `products-shared-kernel`) lazy val `production-planning` = project From 44e9b0241f35e0352fa4a54d605a77fca61c9536 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 10:12:53 +0200 Subject: [PATCH 242/329] chore: disable scalafix var warning --- .../test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala b/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala index d1c48963..3e3d661b 100644 --- a/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala +++ b/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala @@ -13,7 +13,7 @@ import dev.atedeg.mdm.restocking.dto.{ ProductionStartedDTO, QuintalsOfIngredien import dev.atedeg.mdm.utils.monads.* trait Mocks: - @SuppressWarnings(Array("org.wartremover.warts.Var")) + @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) var inMemoryStockDTO: Option[StockDTO] = None val stockRepository: StockRepository = new StockRepository: override def getQuintals[M[_]: Monad: LiftIO]: M[RemainingMilkDTO] = RemainingMilkDTO(10).pure From f84dedc3d76cc315bfdb79a6ffce553a26fa9df9 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 10:15:52 +0200 Subject: [PATCH 243/329] refactor: address @giacomocavalieri suggestions --- .../mdm/restocking/api/repositories/StockRepository.scala | 2 +- .../dev/atedeg/mdm/restocking/api/HandlersTest.scala | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/repositories/StockRepository.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/repositories/StockRepository.scala index 8d76c1b2..24ec171e 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/repositories/StockRepository.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/repositories/StockRepository.scala @@ -11,7 +11,7 @@ trait StockRepository: def getStock[M[_]: Monad: LiftIO]: M[StockDTO] def writeStock[M[_]: Monad: LiftIO](newStock: StockDTO): M[Unit] -final case class DBStockRepository(connection: String) extends StockRepository: +final case class DBStockRepository(connectionString: String) extends StockRepository: override def getQuintals[M[_]: Monad: LiftIO]: M[RemainingMilkDTO] = ??? override def getStock[M[_]: Monad: LiftIO]: M[StockDTO] = ??? override def writeStock[M[_]: Monad: LiftIO](newStock: StockDTO): M[Unit] = ??? diff --git a/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala b/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala index 3e3d661b..b916adc3 100644 --- a/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala +++ b/restocking/src/test/scala/dev/atedeg/mdm/restocking/api/HandlersTest.scala @@ -33,11 +33,9 @@ class HandlersTest extends AnyWordSpec, Matchers, Mocks: "The `productionStartedHandler`" should { "write the new stock to DB" in { - val handler: ServerAction[StockRepository, String, Unit] = productionStartedHandler( - ProductionStartedDTO( - List(QuintalsOfIngredientDTO(10.0, "milk")), - ), - ) + val consumedIngredients = List(QuintalsOfIngredientDTO(10.0, "milk")) + val productionStartedDTO = ProductionStartedDTO(consumedIngredients) + val handler: ServerAction[StockRepository, String, Unit] = productionStartedHandler(productionStartedDTO) handler.unsafeExecute(stockRepository) inMemoryStockDTO should contain(Map("milk" -> 10.0, "salt" -> 3.0, "rennet" -> 30.5)) } From bc293f99e8ae6c1e525c42621367c2142facc9fe Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 9 Aug 2022 08:35:13 +0000 Subject: [PATCH 244/329] chore(release): 1.0.0-beta.12 [skip ci] # [1.0.0-beta.12](https://github.com/atedeg/mdm/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-08-09) ### Features * add stock repository ([6d62e93](https://github.com/atedeg/mdm/commit/6d62e93c119e25a592e896373df813df67b14782)) * add StockDTO ([cffaa9f](https://github.com/atedeg/mdm/commit/cffaa9f4bb42e000e27aa4905daeb6c47b0c59c5)) * create logic handlers ([16b6648](https://github.com/atedeg/mdm/commit/16b6648ac9c1680529e4e6bee089be02f41448e4)) * creates api endpoints specifications ([6ff71ab](https://github.com/atedeg/mdm/commit/6ff71ab44f86aa59df6049928f6dd115c452b60a)) --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 255a8a0d..4f737be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# [1.0.0-beta.12](https://github.com/atedeg/mdm/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-08-09) + + +### Features + +* add stock repository ([6d62e93](https://github.com/atedeg/mdm/commit/6d62e93c119e25a592e896373df813df67b14782)) +* add StockDTO ([cffaa9f](https://github.com/atedeg/mdm/commit/cffaa9f4bb42e000e27aa4905daeb6c47b0c59c5)) +* create logic handlers ([16b6648](https://github.com/atedeg/mdm/commit/16b6648ac9c1680529e4e6bee089be02f41448e4)) +* creates api endpoints specifications ([6ff71ab](https://github.com/atedeg/mdm/commit/6ff71ab44f86aa59df6049928f6dd115c452b60a)) + # [1.0.0-beta.11](https://github.com/atedeg/mdm/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-08-08) From 6f162333174ea82687a3e1fd6ac887369cadad4c Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 16:53:45 +0200 Subject: [PATCH 245/329] chore(utils): add validation utility --- .../main/scala/dev/atedeg/mdm/utils/monads/Monads.scala | 5 +++++ .../scala/dev/atedeg/mdm/utils/serialization/DTO.scala | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala index 87c6c71f..f6a139e1 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/monads/Monads.scala @@ -43,6 +43,11 @@ def raise[M[_], E, A](e: E)(using R: Raise[M, E]): M[A] = R.raise(e) */ def readState[C, M[_]: Monad: CanRead[C]](implicit A: Ask[M, C]): M[C] = A.ask +/** + * Gets a view of the current state applying a function `f` to it. + */ +def readStateView[C, M[_]: Monad: CanRead[C], C1](f: C => C1): M[C1] = readState.map(f) + /** * `unless(cond)(a)` performs the monadic action `a` if the condition `cond` is false. */ diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala index c6729f9e..ff8749e9 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala @@ -1,17 +1,18 @@ package dev.atedeg.mdm.utils.serialization -import java.time.{ LocalDate, LocalDateTime } +import cats.Monad + +import java.time.{LocalDate, LocalDateTime} import java.time.format.DateTimeFormatter import java.util.UUID import scala.compiletime.* import scala.deriving.* import scala.util.Try - import cats.data.NonEmptyList import cats.syntax.all.* import eu.timepit.refined.api.Refined - import dev.atedeg.mdm.utils.* +import dev.atedeg.mdm.utils.monads.* trait DTO[E, D]: def elemToDto(e: E): D @@ -20,6 +21,7 @@ trait DTO[E, D]: object DTOOps: extension [D](dto: D) def toDomain[E](using d: DTO[E, D]): Either[String, E] = d.dtoToElem(dto) extension [E](e: E) def toDTO[D](using d: DTO[E, D]) = d.elemToDto(e) + def validate[D, E, M[_]: Monad: CanRaise[String]](dto: D)(using DTO[E, D]): M[E] = dto.toDomain[E].getOrRaise object DTO: import DTOGenerators.* From c2553f65aabce8dfaaf92ad32547e7dc5e467ae0 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 16:55:18 +0200 Subject: [PATCH 246/329] chore: use validate utility --- .../dev/atedeg/mdm/restocking/api/Handlers.scala | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala index c4a6fc7a..a1a2892c 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/Handlers.scala @@ -16,25 +16,18 @@ import dev.atedeg.mdm.utils.monads.* import dev.atedeg.mdm.utils.serialization.DTOOps.* def remaningQuintalsOfMilkHandler[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]] - : M[RemainingMilkDTO] = - for - remainingMilkDTO <- readState >>= (_.getQuintals) - remainingMilk <- remainingMilkDTO.toDomain[RemainingMilk].getOrRaise - yield remainingMilk.toDTO[RemainingMilkDTO] + : M[RemainingMilkDTO] = (readState >>= (_.getQuintals) >>= validate).map(_.toDTO[RemainingMilkDTO]) def orderMilkHandler[M[_]: Monad: LiftIO: CanRaise[String]](orderMilkDTO: OrderMilkDTO): M[Unit] = - for - orderMilk <- orderMilkDTO.toDomain[OrderMilk].getOrRaise - _ <- makeMilkOrder(orderMilk.toDTO[OrderMilkDTO]) - yield () + validate(orderMilkDTO) >>= (o => makeMilkOrder(o.toDTO[OrderMilkDTO])) def productionStartedHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[StockRepository]]( productionStartedDTO: ProductionStartedDTO, ): M[Unit] = for stockRepository <- readState - productionStarted <- productionStartedDTO.toDomain[ProductionStarted].getOrRaise - stock <- stockRepository.getStock >>= (_.toDomain[Stock].getOrRaise) + productionStarted <- validate(productionStartedDTO) + stock <- stockRepository.getStock >>= validate newStock = consumeIngredients(stock)(productionStarted.ingredients) _ <- stockRepository.writeStock(newStock.toDTO[StockDTO]) yield () From cafb5be9fbf252a06e1e93dbb59269248d4f783f Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 16:55:59 +0200 Subject: [PATCH 247/329] fix: fix format string --- .../src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala index 0164ce85..63b2ca32 100644 --- a/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala +++ b/products-shared-kernel/src/main/scala/dev/atedeg/mdm/products/dto/DTOs.scala @@ -36,7 +36,7 @@ object CheeseTypeDTO: case "casatella" => Casatella.asRight[String] case "caciotta" => Caciotta.asRight[String] case "ricotta" => Ricotta.asRight[String] - case _ => "Unknown `CheeseType`: '$s'".asLeft[CheeseType] + case _ => s"Unknown `CheeseType`: '$dto'".asLeft[CheeseType] override def elemToDto(e: CheeseType): String = e match case Squacquerone => "squacquerone" From 640f00c651b20847bef97a8ff0b15a277c26f45b Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 16:56:20 +0200 Subject: [PATCH 248/329] feat: implement milk-planning API --- .../dev/atedeg/mdm/milkplanning/Actions.scala | 4 +- .../dev/atedeg/mdm/milkplanning/Types.scala | 4 +- .../mdm/milkplanning/api/Configuration.scala | 10 ++++ .../mdm/milkplanning/api/Handlers.scala | 47 ++++++++++++++++++ .../mdm/milkplanning/api/acl/Types.scala | 27 +++++++++++ .../api/emitters/OrderMilkEmitter.scala | 9 ++++ .../api/repositories/Repositories.scala | 14 ++++++ .../atedeg/mdm/milkplanning/dto/DTOs.scala | 31 +++++++++++- .../mdm/milkplanning/api/HandlersTest.scala | 48 +++++++++++++++++++ .../mdm/milkplanning/types/ActionsTest.scala | 4 +- 10 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/Configuration.scala create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/Handlers.scala create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/acl/Types.scala create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/emitters/OrderMilkEmitter.scala create mode 100644 milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/repositories/Repositories.scala create mode 100644 milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/api/HandlersTest.scala diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala index e81b1296..506a0a94 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Actions.scala @@ -21,7 +21,7 @@ import dev.atedeg.mdm.utils.monads.{ emit, thenReturn, when, Emits } */ def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( milkOfPreviousYear: QuintalsOfMilk, - requestedProductsForWeek: NonEmptyList[RequestedProduct], + requestedProductsForWeek: List[RequestedProduct], currentStock: Stock, recipeBook: RecipeBook, stockedMilk: QuintalsOfMilk, @@ -31,7 +31,7 @@ def estimateQuintalsOfMilk[M[_]: Emits[OrderMilk]: Monad]( when(estimatedMilk.quintals > 0)(emit(OrderMilk(estimatedMilk): OrderMilk)).thenReturn(estimatedMilk) private def milkNeededForProducts( - requestedProducts: NonEmptyList[RequestedProduct], + requestedProducts: List[RequestedProduct], stock: Stock, recipeBook: RecipeBook, ): QuintalsOfMilk = diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala index f4fa9952..75a6deeb 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/Types.scala @@ -31,12 +31,12 @@ final case class Yield(n: PositiveDecimal) /** * It defines, for each [[Product product]], the [[Yield yield]] of the milk. */ -type RecipeBook = CheeseType => Yield +type RecipeBook = Map[CheeseType, Yield] /** * It defines, for each [[Product product]], the [[StockedQuantity quantity in stock]]. */ -type Stock = Product => StockedQuantity +type Stock = Map[Product, StockedQuantity] /** * A quantity of a stocked [[Product product]], it may also be zero. diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/Configuration.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/Configuration.scala new file mode 100644 index 00000000..f199a369 --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/Configuration.scala @@ -0,0 +1,10 @@ +package dev.atedeg.mdm.milkplanning.api + +import dev.atedeg.mdm.milkplanning.api.emitters.OrderMilkEmitter +import dev.atedeg.mdm.milkplanning.api.repositories.* + +final case class Configuration( + receivedOrderRepository: ReceivedOrderRepository, + recipeBookRepository: RecipeBookRepository, + emitter: OrderMilkEmitter, +) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/Handlers.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/Handlers.scala new file mode 100644 index 00000000..6ea3078d --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/Handlers.scala @@ -0,0 +1,47 @@ +package dev.atedeg.mdm.milkplanning.api + +import cats.Monad +import cats.data.NonEmptyList +import cats.effect.LiftIO +import cats.syntax.all.* + +import dev.atedeg.mdm.milkplanning.* +import dev.atedeg.mdm.milkplanning.IncomingEvent.ReceivedOrder +import dev.atedeg.mdm.milkplanning.OutgoingEvent.OrderMilk +import dev.atedeg.mdm.milkplanning.api.acl.* +import dev.atedeg.mdm.milkplanning.api.repositories.* +import dev.atedeg.mdm.milkplanning.dto.* +import dev.atedeg.mdm.milkplanning.dto.QuintalsOfMilkDTO.given +import dev.atedeg.mdm.milkplanning.dto.RecipeBookDTO.given +import dev.atedeg.mdm.milkplanning.dto.StockDTO.given +import dev.atedeg.mdm.milkplanning.estimateQuintalsOfMilk +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +def receivedOrderHandler[M[_]: Monad: LiftIO: CanRead[ReceivedOrderRepository]: CanRaise[String]]( + incomingOrderDTO: IncomingOrderDTO, +): M[Unit] = + for + receivedOrder <- validate(incomingOrderDTO.toReceivedOrderDTO) + _ <- readState >>= (_.save(receivedOrder.products.toDTO[List[RequestedProductDTO]])) + yield () + +def orderMilkHandler[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]: M[Unit] = + for + config <- readState + milkPrevYear <- getQuintalsOfThePreviousYear >>= validate + currentStock <- getCurrentStock >>= validate + stockedMilk <- getStockedMilk >>= validate + orderedProducts <- config.receivedOrderRepository.getRequestedProducts + >>= validate[List[RequestedProductDTO], List[RequestedProduct], M] + recipeBook <- config.recipeBookRepository.getRecipeBook >>= validate + action: SafeAction[OrderMilk, QuintalsOfMilk] = + estimateQuintalsOfMilk(milkPrevYear, orderedProducts, currentStock, recipeBook, stockedMilk) + (events, _) = action.execute + _ <- events.map(_.toDTO[OrderMilkDTO]).traverse(config.emitter.emit) + yield () + +private def getQuintalsOfThePreviousYear[M[_]: Monad: LiftIO]: M[QuintalsOfMilkDTO] = ??? +private def getStockedMilk[M[_]: Monad: LiftIO]: M[QuintalsOfMilkDTO] = ??? +private def getCurrentStock[M[_]: Monad: LiftIO]: M[StockDTO] = ??? diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/acl/Types.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/acl/Types.scala new file mode 100644 index 00000000..89c7df06 --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/acl/Types.scala @@ -0,0 +1,27 @@ +package dev.atedeg.mdm.milkplanning.api.acl + +import dev.atedeg.mdm.milkplanning.dto.* +import dev.atedeg.mdm.products.dto.ProductDTO + +final case class IncomingOrderLineDTO(quantity: Int, product: ProductDTO) +final case class CustomerDTO(code: String, name: String, vatNumber: String) +final case class LocationDTO(latitude: Double, longitude: Double) +final case class IncomingOrderDTO( + id: String, + orderLines: List[IncomingOrderLineDTO], + customer: CustomerDTO, + deliveryDate: String, + deliveryLocation: LocationDTO, +) + +extension (iol: IncomingOrderLineDTO) + def toRequestedProductDTO(requiredBy: String): RequestedProductDTO = + RequestedProductDTO(iol.product, iol.quantity, requiredBy) + +extension (io: IncomingOrderDTO) + def toReceivedOrderDTO: ReceivedOrderDTO = ReceivedOrderDTO( + io.orderLines.map(_.toRequestedProductDTO(io.deliveryDate)), + ) + +final case class OrderedMilkDTO(quintals: Int, orderPlacedAt: String) +extension (om: OrderedMilkDTO) def toQuintalsOfMilkDTO: QuintalsOfMilkDTO = QuintalsOfMilkDTO(om.quintals) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/emitters/OrderMilkEmitter.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/emitters/OrderMilkEmitter.scala new file mode 100644 index 00000000..e2c4ad3f --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/emitters/OrderMilkEmitter.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.milkplanning.api.emitters + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.milkplanning.dto.OrderMilkDTO + +trait OrderMilkEmitter: + def emit[M[_]: Monad: LiftIO](orderMilkDTO: OrderMilkDTO): M[Unit] diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/repositories/Repositories.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/repositories/Repositories.scala new file mode 100644 index 00000000..1cebdeb4 --- /dev/null +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/repositories/Repositories.scala @@ -0,0 +1,14 @@ +package dev.atedeg.mdm.milkplanning.api.repositories + +import cats.Monad +import cats.effect.LiftIO +import dev.atedeg.mdm.milkplanning.QuintalsOfMilk +import dev.atedeg.mdm.milkplanning.dto.{ReceivedOrderDTO, RecipeBookDTO, RequestedProductDTO} + +trait RecipeBookRepository: + def getRecipeBook[M[_]: Monad: LiftIO]: M[RecipeBookDTO] + +trait ReceivedOrderRepository: + def save[M[_]: Monad: LiftIO](requestedProducts: List[RequestedProductDTO]): M[Unit] + def getRequestedProducts[M[_]: Monad: LiftIO]: M[List[RequestedProductDTO]] + diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala index 7ac4647d..08c925fa 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala @@ -1,6 +1,14 @@ package dev.atedeg.mdm.milkplanning.dto -import dev.atedeg.mdm.milkplanning.{ Quantity, QuintalsOfMilk, RequestedProduct } +import dev.atedeg.mdm.milkplanning.{ + Quantity, + QuintalsOfMilk, + RecipeBook, + RequestedProduct, + Stock, + StockedQuantity, + Yield, +} import dev.atedeg.mdm.milkplanning.IncomingEvent.ReceivedOrder import dev.atedeg.mdm.milkplanning.OutgoingEvent.OrderMilk import dev.atedeg.mdm.products.dto.ProductDTO @@ -11,12 +19,33 @@ import dev.atedeg.mdm.utils.serialization.DTOOps.* final case class ReceivedOrderDTO(products: List[RequestedProductDTO]) final case class RequestedProductDTO(product: ProductDTO, quantity: Int, requiredBy: String) final case class OrderMilkDTO(quintals: Int) +final case class QuintalsOfMilkDTO(quintals: Int) +type StockDTO = Map[ProductDTO, Int] +type RecipeBookDTO = Map[String, Double] object ReceivedOrderDTO: given DTO[ReceivedOrder, ReceivedOrderDTO] = interCaseClassDTO private given DTO[RequestedProduct, RequestedProductDTO] = interCaseClassDTO private given DTO[Quantity, Int] = caseClassDTO + +object RequestedProductDTO: + given DTO[RequestedProduct, RequestedProductDTO] = interCaseClassDTO + private given DTO[Quantity, Int] = caseClassDTO object OrderMilkDTO: given DTO[OrderMilk, OrderMilkDTO] = interCaseClassDTO private given DTO[QuintalsOfMilk, Int] = caseClassDTO + +object StockDTO: + import dev.atedeg.mdm.products.dto.ProductDTO.given + given DTO[Stock, StockDTO] = DTO.mapDTO + private given DTO[StockedQuantity, Int] = caseClassDTO + +object RecipeBookDTO: + import dev.atedeg.mdm.products.dto.CheeseTypeDTO.given + import dev.atedeg.mdm.products.dto.ProductDTO.given + given DTO[RecipeBook, RecipeBookDTO] = DTO.mapDTO + private given DTO[Yield, Double] = caseClassDTO + +object QuintalsOfMilkDTO: + given DTO[QuintalsOfMilk, QuintalsOfMilkDTO] = interCaseClassDTO diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/api/HandlersTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/api/HandlersTest.scala new file mode 100644 index 00000000..11641b46 --- /dev/null +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/api/HandlersTest.scala @@ -0,0 +1,48 @@ +package dev.atedeg.mdm.milkplanning.api + +import java.time.LocalDateTime +import java.util.UUID + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import dev.atedeg.mdm.milkplanning.QuintalsOfMilk +import dev.atedeg.mdm.milkplanning.api.acl.{ CustomerDTO, IncomingOrderDTO, IncomingOrderLineDTO, LocationDTO } +import dev.atedeg.mdm.milkplanning.api.emitters.OrderMilkEmitter +import dev.atedeg.mdm.milkplanning.api.repositories.{ ReceivedOrderRepository, RecipeBookRepository } +import dev.atedeg.mdm.milkplanning.dto.{ OrderMilkDTO, RecipeBookDTO, RequestedProductDTO } +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTOOps.toDTO + +trait Mocks: + @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) + var orderHistory: List[RequestedProductDTO] = Nil + private val orderMilkEmitter: OrderMilkEmitter = new OrderMilkEmitter: + override def emit[M[_]: Monad: LiftIO](orderMilkDTO: OrderMilkDTO): M[Unit] = ??? + val receivedOrderRepository: ReceivedOrderRepository = new ReceivedOrderRepository: + override def save[M[_]: Monad: LiftIO](requestedProducts: List[RequestedProductDTO]): M[Unit] = + orderHistory = requestedProducts + ().pure + override def getRequestedProducts[M[_]: Monad: LiftIO]: M[List[RequestedProductDTO]] = ??? + private val recipeBookRepository: RecipeBookRepository = new RecipeBookRepository: + override def getRecipeBook[M[_]: Monad: LiftIO]: M[RecipeBookDTO] = ??? + val config: Configuration = Configuration(receivedOrderRepository, recipeBookRepository, orderMilkEmitter) + +class HandlersTest extends AnyWordSpec, Matchers, Mocks: + "The `receivedOrderHandler`" should { + "save the received order in the DB" in { + val product = ProductDTO("squacquerone", 250) + val date = LocalDateTime.now().toDTO[String] + val orderLines = List(IncomingOrderLineDTO(50, product)) + val uuid = UUID.randomUUID().toDTO[String] + val incomingOrder = + IncomingOrderDTO(uuid, orderLines, CustomerDTO(uuid, "Foo", "IT01088260409"), date, LocationDTO(12.6, 44.6)) + val action: ServerAction[ReceivedOrderRepository, String, Unit] = receivedOrderHandler(incomingOrder) + action.unsafeExecute(receivedOrderRepository) + orderHistory should contain(RequestedProductDTO(product, 50, date)) + } + } diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala index 76da6952..20184404 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala @@ -37,14 +37,14 @@ class ActionsTest extends AnyFeatureSpec with GivenWhenThen with Matchers with M Given("the quintals of milk of the previous year for the same period") val qomPreviousYear = 12.quintalsOfMilk And("a list of products to be produced") - val requestedProducts = NonEmptyList.of( + val requestedProducts = List( RequestedProduct(Squacquerone(100), Quantity(500), LocalDateTime.now()), RequestedProduct(Squacquerone(250), Quantity(300), LocalDateTime.now()), RequestedProduct(Ricotta(350), Quantity(50), LocalDateTime.now()), RequestedProduct(Caciotta(500), Quantity(100), LocalDateTime.now()), ) And("an empty stock") - val currentStock: Stock = _ => StockedQuantity(0) + val currentStock: Stock = Map.empty.withDefaultValue(StockedQuantity(0)) And("there is no milk in stock") val stockedMilk = QuintalsOfMilk(0) When("estimating the necessary quintals of milk") From b8558951ea3119f45f53bd39f7e9f6a05ea3e006 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 17:21:52 +0200 Subject: [PATCH 249/329] style: reformat files --- .../mdm/milkplanning/api/repositories/Repositories.scala | 4 ++-- .../src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/repositories/Repositories.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/repositories/Repositories.scala index 1cebdeb4..047418e9 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/repositories/Repositories.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/api/repositories/Repositories.scala @@ -2,8 +2,9 @@ package dev.atedeg.mdm.milkplanning.api.repositories import cats.Monad import cats.effect.LiftIO + import dev.atedeg.mdm.milkplanning.QuintalsOfMilk -import dev.atedeg.mdm.milkplanning.dto.{ReceivedOrderDTO, RecipeBookDTO, RequestedProductDTO} +import dev.atedeg.mdm.milkplanning.dto.{ ReceivedOrderDTO, RecipeBookDTO, RequestedProductDTO } trait RecipeBookRepository: def getRecipeBook[M[_]: Monad: LiftIO]: M[RecipeBookDTO] @@ -11,4 +12,3 @@ trait RecipeBookRepository: trait ReceivedOrderRepository: def save[M[_]: Monad: LiftIO](requestedProducts: List[RequestedProductDTO]): M[Unit] def getRequestedProducts[M[_]: Monad: LiftIO]: M[List[RequestedProductDTO]] - diff --git a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala index 08c925fa..4875ca99 100644 --- a/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala +++ b/milk-planning/src/main/scala/dev/atedeg/mdm/milkplanning/dto/DTOs.scala @@ -27,7 +27,7 @@ object ReceivedOrderDTO: given DTO[ReceivedOrder, ReceivedOrderDTO] = interCaseClassDTO private given DTO[RequestedProduct, RequestedProductDTO] = interCaseClassDTO private given DTO[Quantity, Int] = caseClassDTO - + object RequestedProductDTO: given DTO[RequestedProduct, RequestedProductDTO] = interCaseClassDTO private given DTO[Quantity, Int] = caseClassDTO From 18f3c13f8e0a34f98716c16bf826014cc9e029ec Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 17:24:00 +0200 Subject: [PATCH 250/329] build: add docker configuration for milk-planning --- build.sbt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.sbt b/build.sbt index b43357e3..fbf30f93 100644 --- a/build.sbt +++ b/build.sbt @@ -122,8 +122,14 @@ lazy val utils = project ) lazy val `milk-planning` = project + .enablePlugins(DockerPlugin, JavaAppPackaging) .in(file("milk-planning")) .settings(commonSettings) + .settings( + Docker / packageName := packageName.value, + Docker / version := version.value, + dockerExposedPorts := Seq(8080), + ) .dependsOn(utils, `products-shared-kernel`) lazy val production = project From 49b395a65cf92a344985adb210c1ccac8c71fa0d Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 9 Aug 2022 15:08:34 +0200 Subject: [PATCH 251/329] docs: add acl between milk planning and stocking bcs --- docs/_assets/images/contextMap.svg | 238 +++++++++++++++-------------- docs/_docs/context-map.md | 3 + docs/mdm.cml | 1 + 3 files changed, 130 insertions(+), 112 deletions(-) diff --git a/docs/_assets/images/contextMap.svg b/docs/_assets/images/contextMap.svg index cc2c90c9..05341ade 100644 --- a/docs/_assets/images/contextMap.svg +++ b/docs/_assets/images/contextMap.svg @@ -1,155 +1,169 @@ - - + + ContextMapGraph - + ClientOrders - -ClientOrders + +ClientOrders - + +MilkPlanning + +MilkPlanning + + + +ClientOrders->MilkPlanning + +                                         + + +D + + +ACL + +U + + + ProductionPlanning - -ProductionPlanning + +ProductionPlanning - + ClientOrders->ProductionPlanning - -                                         - - -D - - -ACL - -U + +                                         + + +D + + +ACL + +U - + Stocking - -Stocking + +Stocking - -ClientOrders->Stocking - -                                         - - -D - - -ACL - -U - - - -MilkPlanning - -MilkPlanning - - -ClientOrders->MilkPlanning - -                                         - - -D - - -ACL - -U +ClientOrders->Stocking + +                                         + + +D + + +ACL + +U - + Production - -Production + +Production ProductionPlanning->Production - -                                         - - -D - - -CF - -U + +                                         + + +D + + +CF + +U Production->Stocking - -                                         - - -D - - -CF - -U + +                                         + + +D + + +CF + +U - + Restocking - -Restocking + +Restocking Production->Restocking - -                                         - -D - - -U - - -OHS, PL + +                                         + +D + + +U + + +OHS, PL - + +Stocking->MilkPlanning + +                                         + + +D + + +ACL + +U + + + Stocking->ProductionPlanning - -                                         - - -D - - -ACL - -U + +                                         + + +D + + +ACL + +U - + Restocking->MilkPlanning - -                                         - - -D - - -ACL - -U + +                                         + + +D + + +ACL + +U diff --git a/docs/_docs/context-map.md b/docs/_docs/context-map.md index b7f3d1d4..ca10eb0f 100644 --- a/docs/_docs/context-map.md +++ b/docs/_docs/context-map.md @@ -32,6 +32,9 @@ title: Context Map `Production` informs `Restocking` when some raw materials are consumed. `Production` is an upstream Open-Host Service and must expose a published language as the `Restocking` downstream bounded context is going to be generic and we will not be able to freely change its API. +- `MilkPlanning [D, ACL] <- [U] Stocking` + `MilkPlanning` asks `Stocking` for the amount of products in stock. + Since `MilkPlanning` is a downstream core bounded context, and Anti-Corruption Layer is required. There is a *Shared Kernel* among the bounded contexts which contains the definitions for **product** and **cheese type**. This choice was taken as the two aforementioned concepts are crucial for the cheese factory and a change in any of the definitions must be reflected in all diff --git a/docs/mdm.cml b/docs/mdm.cml index 1ffac3d6..dadca805 100644 --- a/docs/mdm.cml +++ b/docs/mdm.cml @@ -14,6 +14,7 @@ ContextMap MDM { ProductionPlanning [D, ACL] <- [U] Stocking Stocking [D, CF] <- [U] Production Restocking [D] <- [U, OHS, PL] Production + MilkPlanning [D, ACL] <- [U] Stocking } BoundedContext MilkPlanning From f1878fb8ee97a748fe09296053989a00c9e89e0a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 9 Aug 2022 15:44:42 +0000 Subject: [PATCH 252/329] chore(release): 1.0.0-beta.13 [skip ci] # [1.0.0-beta.13](https://github.com/atedeg/mdm/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2022-08-09) ### Bug Fixes * fix format string ([cafb5be](https://github.com/atedeg/mdm/commit/cafb5be9fbf252a06e1e93dbb59269248d4f783f)) ### Features * implement milk-planning API ([640f00c](https://github.com/atedeg/mdm/commit/640f00c651b20847bef97a8ff0b15a277c26f45b)) --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f737be6..b01567e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [1.0.0-beta.13](https://github.com/atedeg/mdm/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2022-08-09) + + +### Bug Fixes + +* fix format string ([cafb5be](https://github.com/atedeg/mdm/commit/cafb5be9fbf252a06e1e93dbb59269248d4f783f)) + + +### Features + +* implement milk-planning API ([640f00c](https://github.com/atedeg/mdm/commit/640f00c651b20847bef97a8ff0b15a277c26f45b)) + # [1.0.0-beta.12](https://github.com/atedeg/mdm/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-08-09) From aaf7e54e4388209afc0c39cd92a8c595eded1642 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Wed, 10 Aug 2022 09:16:44 +0200 Subject: [PATCH 253/329] refactor: change recipe book to be a case class --- .../dev/atedeg/mdm/production/Actions.scala | 2 +- .../dev/atedeg/mdm/production/Types.scala | 2 +- .../dev/atedeg/mdm/production/dto/DTOs.scala | 21 +++++++++++++------ .../dev/atedeg/mdm/production/Tests.scala | 4 ++-- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index 40393646..13d809c2 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -32,7 +32,7 @@ def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[StartProduction] val typeToProduce = production.productToProduce.cheeseType val gramsOfSingleUnit = production.productToProduce.weight for - recipe <- recipeBook(typeToProduce) ifMissingRaise MissingRecipe(typeToProduce) + recipe <- recipeBook.recipeBook.get(typeToProduce) ifMissingRaise MissingRecipe(typeToProduce) quintalsToProduce = (production.unitsToProduce.n * gramsOfSingleUnit.n).toDecimal / 100_000 neededIngredients = recipe.lines.map(_ * quintalsToProduce) _ <- emit(StartProduction(neededIngredients): StartProduction) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index ff1b8214..e27d02ce 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -57,7 +57,7 @@ final case class BatchID(ID: UUID) /** * Associates to each [[CheeseType cheese type]] the [[Recipe recipe]] to produce a quintal of it. */ -type RecipeBook = CheeseType => Option[Recipe] +final case class RecipeBook(recipeBook: Map[CheeseType, Recipe]) /** * A list of [[QuintalsOfIngredient ingredients and the respective quintals]] needed to produce a quintal of a product. diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala index 5df5d7dc..0806ed40 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala @@ -3,6 +3,7 @@ package dev.atedeg.mdm.production.dto import dev.atedeg.mdm.production.* import dev.atedeg.mdm.production.IncomingEvent.* import dev.atedeg.mdm.production.OutgoingEvent.* +import dev.atedeg.mdm.products.dto.CheeseTypeDTO.given import dev.atedeg.mdm.products.dto.IngredientDTO.given import dev.atedeg.mdm.products.dto.ProductDTO import dev.atedeg.mdm.products.dto.ProductDTO.given @@ -12,22 +13,30 @@ import dev.atedeg.mdm.utils.serialization.DTOOps final case class StartProductionDTO(neededIngredients: List[QuintalsOfIngredientDTO]) final case class QuintalsOfIngredientDTO(quintals: Double, ingredient: String) -final case class ProductionEndedDTO(productionID: String, batchID: String) -final case class ProductionPlanReadyDTO(productionPlan: List[ProductionPlanItemDTO]) -final case class ProductionPlanItemDTO(product: ProductDTO, units: Int) - object StartProductionDTO: given DTO[StartProduction, StartProductionDTO] = interCaseClassDTO private given DTO[QuintalsOfIngredient, QuintalsOfIngredientDTO] = interCaseClassDTO private given DTO[WeightInQuintals, Double] = caseClassDTO +final case class ProductionEndedDTO(productionID: String, batchID: String) object ProductionEndedDTO: given DTO[ProductionEnded, ProductionEndedDTO] = interCaseClassDTO private given DTO[ProductionID, String] = caseClassDTO private given DTO[BatchID, String] = caseClassDTO +final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) +final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) +final case class ProductToProduceDTO(product: ProductDTO, units: Int) object ProductionPlanReadyDTO: given DTO[ProductionPlanReady, ProductionPlanReadyDTO] = interCaseClassDTO - private given DTO[ProductionPlan, List[ProductionPlanItemDTO]] = caseClassDTO - private given DTO[ProductionPlanItem, ProductionPlanItemDTO] = interCaseClassDTO + private given DTO[ProductionPlan, ProductionPlanDTO] = interCaseClassDTO + private given DTO[ProductionPlanItem, ProductToProduceDTO] = interCaseClassDTO private given DTO[NumberOfUnits, Int] = caseClassDTO + +final case class RecipeBookDTO(recipeBook: Map[String, RecipeDTO]) +final case class RecipeDTO(recipe: List[QuintalsOfIngredientDTO]) +object RecipeBookDTO: + given DTO[RecipeBook, RecipeBookDTO] = interCaseClassDTO + private given DTO[Recipe, RecipeDTO] = interCaseClassDTO + private given DTO[QuintalsOfIngredient, QuintalsOfIngredientDTO] = interCaseClassDTO + private given DTO[WeightInQuintals, Double] = caseClassDTO diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala index 9784f874..a75184c9 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala @@ -49,7 +49,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Scenario("A production is started") { Given("a production that has to be started") And("a recipe book") - val recipeBook = Map(CheeseType.Caciotta -> Recipe(allIngredients.map(10 of _))).get + val recipeBook = RecipeBook(Map(CheeseType.Caciotta -> Recipe(allIngredients.map(10 of _)))) When("it is started") val startAction: Action[MissingRecipe, StartProduction, Production.InProgress] = startProduction(recipeBook)(production) @@ -65,7 +65,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Scenario("A production is started with no recipe") { Given("a production that has to be started") And("has no recipe") - val emptyRecipeBook = Map[CheeseType, Recipe]().get + val emptyRecipeBook = RecipeBook(Map[CheeseType, Recipe]()) When("it is started") val startAction: Action[MissingRecipe, StartProduction, Production.InProgress] = startProduction(emptyRecipeBook)(production) From 40c17471562520c887a29120b90ba05c8c6d122f Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Wed, 10 Aug 2022 13:39:07 +0200 Subject: [PATCH 254/329] chore: add api layer and repositories --- .../mdm/production/api/Configuration.scala | 9 ++++++ .../atedeg/mdm/production/api/Handlers.scala | 29 +++++++++++++++++++ .../api/repositories/Repositories.scala | 12 ++++++++ 3 files changed, 50 insertions(+) create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala new file mode 100644 index 00000000..990b0ab2 --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.production.api + +import dev.atedeg.mdm.production.api.repositories.RecipeBookRepository + +case class Configuration( + recipeBookRepository: RecipeBookRepository, + productionsRepository: ProductionsRepository, + emitter: StartProductionEmitter, +) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala new file mode 100644 index 00000000..dec15c19 --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala @@ -0,0 +1,29 @@ +package dev.atedeg.mdm.production.api + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* + +import dev.atedeg.mdm.production.* +import dev.atedeg.mdm.production.IncomingEvent.* +import dev.atedeg.mdm.production.OutgoingEvent.StartProduction +import dev.atedeg.mdm.production.api.repositories.RecipeBookRepository +import dev.atedeg.mdm.production.dto.ProductionPlanReadyDTO +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +def handleProductionPlanReady[M[_]: Monad: LiftIO: CanRead[RecipeBookRepository]: CanRaise[String]]( + ppr: ProductionPlanReadyDTO, +): M[Unit] = + for + config <- readState + recipeBook <- config.recipeBookRepository.read >>= validate + productionPlan <- validate(ppr).map(_.productionPlan) + productions = setupProductions(productionPlan) + val action: Action[MissingRecipe, StartProduction, NonEmptyList[Production.InProgress]] = + productions.traverse(startProduction(recipeBook)) + (events, res) = action.execute + _ <- events.traverse(config.emitter.emit) + productions <- res.leftMap(_.toString).getOrRaise + _ <- config.productionsRepository.write(productions.toDTO) + yield () diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala new file mode 100644 index 00000000..774619a3 --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala @@ -0,0 +1,12 @@ +package dev.atedeg.mdm.production.api.repositories + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.production.dto.RecipeBookDTO + +trait RecipeBookRepository: + def read[M[_]: Monad: LiftIO]: M[RecipeBookDTO] + +trait ProductionsRepository: + def write[M[_]: Monad: LiftIO](production: ProductionDTO): M[?] From aa7d01921197ea6bfd66727606be2835b1cdee1a Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Wed, 10 Aug 2022 14:56:54 +0200 Subject: [PATCH 255/329] chore: add missing DTOs --- .../dev/atedeg/mdm/production/dto/DTOs.scala | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala index 0806ed40..24d9f80f 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala @@ -1,5 +1,7 @@ package dev.atedeg.mdm.production.dto +import cats.syntax.all.* + import dev.atedeg.mdm.production.* import dev.atedeg.mdm.production.IncomingEvent.* import dev.atedeg.mdm.production.OutgoingEvent.* @@ -9,7 +11,14 @@ import dev.atedeg.mdm.products.dto.ProductDTO import dev.atedeg.mdm.products.dto.ProductDTO.given import dev.atedeg.mdm.utils.serialization.DTO import dev.atedeg.mdm.utils.serialization.DTOGenerators.* -import dev.atedeg.mdm.utils.serialization.DTOOps +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +private object Common: + given DTO[ProductionID, String] = caseClassDTO + given DTO[BatchID, String] = caseClassDTO + given DTO[NumberOfUnits, Int] = caseClassDTO + +import Common.given final case class StartProductionDTO(neededIngredients: List[QuintalsOfIngredientDTO]) final case class QuintalsOfIngredientDTO(quintals: Double, ingredient: String) @@ -21,8 +30,6 @@ object StartProductionDTO: final case class ProductionEndedDTO(productionID: String, batchID: String) object ProductionEndedDTO: given DTO[ProductionEnded, ProductionEndedDTO] = interCaseClassDTO - private given DTO[ProductionID, String] = caseClassDTO - private given DTO[BatchID, String] = caseClassDTO final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) @@ -31,7 +38,6 @@ object ProductionPlanReadyDTO: given DTO[ProductionPlanReady, ProductionPlanReadyDTO] = interCaseClassDTO private given DTO[ProductionPlan, ProductionPlanDTO] = interCaseClassDTO private given DTO[ProductionPlanItem, ProductToProduceDTO] = interCaseClassDTO - private given DTO[NumberOfUnits, Int] = caseClassDTO final case class RecipeBookDTO(recipeBook: Map[String, RecipeDTO]) final case class RecipeDTO(recipe: List[QuintalsOfIngredientDTO]) @@ -40,3 +46,15 @@ object RecipeBookDTO: private given DTO[Recipe, RecipeDTO] = interCaseClassDTO private given DTO[QuintalsOfIngredient, QuintalsOfIngredientDTO] = interCaseClassDTO private given DTO[WeightInQuintals, Double] = caseClassDTO + +final case class ToStartDTO(id: String, product: ProductDTO, units: Int) +object ToStartDTO: + given DTO[Production.ToStart, ToStartDTO] = interCaseClassDTO + +final case class InProgressDTO(id: String, product: ProductDTO, units: Int) +object InProgressDTO: + given DTO[Production.InProgress, InProgressDTO] = interCaseClassDTO + +final case class EndedDTO(id: String, batchID: String, product: ProductDTO, units: Int) +object EndedDTO: + given DTO[Production.Ended, EndedDTO] = interCaseClassDTO From b8bcf7812cdef282d88d22f9510e753f4de93f38 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Wed, 10 Aug 2022 14:58:08 +0200 Subject: [PATCH 256/329] refactor: fix handler --- .../atedeg/mdm/production/api/Configuration.scala | 5 +++-- .../dev/atedeg/mdm/production/api/Handlers.scala | 13 +++++++------ .../api/emitters/StartProductionEmitter.scala | 9 +++++++++ .../production/api/repositories/Repositories.scala | 4 ++-- 4 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala index 990b0ab2..da15ee5a 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala @@ -1,8 +1,9 @@ package dev.atedeg.mdm.production.api -import dev.atedeg.mdm.production.api.repositories.RecipeBookRepository +import dev.atedeg.mdm.production.api.emitters.* +import dev.atedeg.mdm.production.api.repositories.* -case class Configuration( +final case class Configuration( recipeBookRepository: RecipeBookRepository, productionsRepository: ProductionsRepository, emitter: StartProductionEmitter, diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala index dec15c19..fa0b3695 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala @@ -1,6 +1,7 @@ package dev.atedeg.mdm.production.api import cats.Monad +import cats.data.NonEmptyList import cats.effect.LiftIO import cats.syntax.all.* @@ -8,11 +9,11 @@ import dev.atedeg.mdm.production.* import dev.atedeg.mdm.production.IncomingEvent.* import dev.atedeg.mdm.production.OutgoingEvent.StartProduction import dev.atedeg.mdm.production.api.repositories.RecipeBookRepository -import dev.atedeg.mdm.production.dto.ProductionPlanReadyDTO +import dev.atedeg.mdm.production.dto.{ ProductionPlanReadyDTO, StartProductionDTO } import dev.atedeg.mdm.utils.monads.* import dev.atedeg.mdm.utils.serialization.DTOOps.* -def handleProductionPlanReady[M[_]: Monad: LiftIO: CanRead[RecipeBookRepository]: CanRaise[String]]( +def handleProductionPlanReady[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]( ppr: ProductionPlanReadyDTO, ): M[Unit] = for @@ -20,10 +21,10 @@ def handleProductionPlanReady[M[_]: Monad: LiftIO: CanRead[RecipeBookRepository] recipeBook <- config.recipeBookRepository.read >>= validate productionPlan <- validate(ppr).map(_.productionPlan) productions = setupProductions(productionPlan) - val action: Action[MissingRecipe, StartProduction, NonEmptyList[Production.InProgress]] = + action: Action[MissingRecipe, StartProduction, NonEmptyList[Production.InProgress]] = productions.traverse(startProduction(recipeBook)) (events, res) = action.execute - _ <- events.traverse(config.emitter.emit) - productions <- res.leftMap(_.toString).getOrRaise - _ <- config.productionsRepository.write(productions.toDTO) + _ <- events.map(_.toDTO[StartProductionDTO]).traverse(config.emitter.emit) + productions <- res.leftMap(m => s"Missing recipe: $m").getOrRaise + _ <- config.productionsRepository.writeInProgressProductions(productions.toDTO) yield () diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala new file mode 100644 index 00000000..0ca1d66c --- /dev/null +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.production.api.emitters + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.production.dto.StartProductionDTO + +trait StartProductionEmitter: + def emit[M[_]: Monad: LiftIO](message: StartProductionDTO): M[Unit] diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala index 774619a3..068e1a95 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala @@ -3,10 +3,10 @@ package dev.atedeg.mdm.production.api.repositories import cats.Monad import cats.effect.LiftIO -import dev.atedeg.mdm.production.dto.RecipeBookDTO +import dev.atedeg.mdm.production.dto.* trait RecipeBookRepository: def read[M[_]: Monad: LiftIO]: M[RecipeBookDTO] trait ProductionsRepository: - def write[M[_]: Monad: LiftIO](production: ProductionDTO): M[?] + def writeInProgressProductions[M[_]: Monad: LiftIO](productions: List[InProgressDTO]): M[Unit] From 871d7271801ee3f5622605961350dc2b5cfb2f72 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Wed, 10 Aug 2022 15:53:53 +0200 Subject: [PATCH 257/329] refactor: add missing event --- .../src/main/scala/dev/atedeg/mdm/production/Events.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala index 2345a534..d0f5fcdf 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala @@ -25,3 +25,8 @@ enum IncomingEvent: * the [[Product products]] that need to be produced. */ case ProductionPlanReady(productionPlan: ProductionPlan) + + /** + * Fired when a [[Production.InProgress production]] is terminated by a smart machine. + */ + case ProductionEnded(productionID: ProductionID) From dd84953890c7966fbf0a4301fc03602734715e03 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Wed, 10 Aug 2022 15:54:52 +0200 Subject: [PATCH 258/329] refactor: add missing handler --- .../mdm/production/api/Configuration.scala | 2 +- .../atedeg/mdm/production/api/Handlers.scala | 20 ++++++++++++++++--- .../api/emitters/StartProductionEmitter.scala | 7 ++++--- .../api/repositories/Repositories.scala | 3 +++ .../dev/atedeg/mdm/production/dto/DTOs.scala | 9 +++++++-- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala index da15ee5a..1809de04 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala @@ -6,5 +6,5 @@ import dev.atedeg.mdm.production.api.repositories.* final case class Configuration( recipeBookRepository: RecipeBookRepository, productionsRepository: ProductionsRepository, - emitter: StartProductionEmitter, + emitter: Emitter, ) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala index fa0b3695..aba14d47 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala @@ -7,9 +7,10 @@ import cats.syntax.all.* import dev.atedeg.mdm.production.* import dev.atedeg.mdm.production.IncomingEvent.* -import dev.atedeg.mdm.production.OutgoingEvent.StartProduction +import dev.atedeg.mdm.production.OutgoingEvent.{ ProductionEnded, StartProduction } import dev.atedeg.mdm.production.api.repositories.RecipeBookRepository -import dev.atedeg.mdm.production.dto.{ ProductionPlanReadyDTO, StartProductionDTO } +import dev.atedeg.mdm.production.dto.* +import dev.atedeg.mdm.production.dto.given import dev.atedeg.mdm.utils.monads.* import dev.atedeg.mdm.utils.serialization.DTOOps.* @@ -24,7 +25,20 @@ def handleProductionPlanReady[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRa action: Action[MissingRecipe, StartProduction, NonEmptyList[Production.InProgress]] = productions.traverse(startProduction(recipeBook)) (events, res) = action.execute - _ <- events.map(_.toDTO[StartProductionDTO]).traverse(config.emitter.emit) + _ <- events.map(_.toDTO[StartProductionDTO]).traverse(config.emitter.emitStart) productions <- res.leftMap(m => s"Missing recipe: $m").getOrRaise _ <- config.productionsRepository.writeInProgressProductions(productions.toDTO) yield () + +def handleProductionEnded[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]( + productionEnded: IncomingProductionEndedDTO, +): M[Unit] = + for + config <- readState + productionID <- validate(productionEnded).map(_.productionID) + production <- config.productionsRepository.readInProgressProduction(productionID.toDTO) >>= validate + action: SafeAction[ProductionEnded, Production.Ended] = endProduction(production) + (events, result) = action.execute + _ <- events.map(_.toDTO[ProductionEndedDTO]).traverse(config.emitter.emitEnded) + _ <- config.productionsRepository.updateToEnded(result.toDTO[EndedDTO]) + yield () diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala index 0ca1d66c..9472ce77 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala @@ -3,7 +3,8 @@ package dev.atedeg.mdm.production.api.emitters import cats.Monad import cats.effect.LiftIO -import dev.atedeg.mdm.production.dto.StartProductionDTO +import dev.atedeg.mdm.production.dto.* -trait StartProductionEmitter: - def emit[M[_]: Monad: LiftIO](message: StartProductionDTO): M[Unit] +trait Emitter: + def emitStart[M[_]: Monad: LiftIO](message: StartProductionDTO): M[Unit] + def emitEnded[M[_]: Monad: LiftIO](message: ProductionEndedDTO): M[Unit] diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala index 068e1a95..db79fb8d 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala @@ -4,9 +4,12 @@ import cats.Monad import cats.effect.LiftIO import dev.atedeg.mdm.production.dto.* +import dev.atedeg.mdm.utils.monads.CanRaise trait RecipeBookRepository: def read[M[_]: Monad: LiftIO]: M[RecipeBookDTO] trait ProductionsRepository: def writeInProgressProductions[M[_]: Monad: LiftIO](productions: List[InProgressDTO]): M[Unit] + def readInProgressProduction[M[_]: Monad: LiftIO: CanRaise[String]](productionID: String): M[InProgressDTO] + def updateToEnded[M[_]: Monad: LiftIO](production: EndedDTO): M[Unit] diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala index 24d9f80f..03860a64 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala @@ -13,8 +13,9 @@ import dev.atedeg.mdm.utils.serialization.DTO import dev.atedeg.mdm.utils.serialization.DTOGenerators.* import dev.atedeg.mdm.utils.serialization.DTOOps.* +given DTO[ProductionID, String] = caseClassDTO + private object Common: - given DTO[ProductionID, String] = caseClassDTO given DTO[BatchID, String] = caseClassDTO given DTO[NumberOfUnits, Int] = caseClassDTO @@ -29,7 +30,11 @@ object StartProductionDTO: final case class ProductionEndedDTO(productionID: String, batchID: String) object ProductionEndedDTO: - given DTO[ProductionEnded, ProductionEndedDTO] = interCaseClassDTO + given DTO[OutgoingEvent.ProductionEnded, ProductionEndedDTO] = interCaseClassDTO + +final case class IncomingProductionEndedDTO(productionID: String) +object IncomingProductionEndedDTO: + given DTO[IncomingEvent.ProductionEnded, IncomingProductionEndedDTO] = interCaseClassDTO final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) From 567bce2b7fff647b8e78ebf1500f5396e7dc7e2a Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Wed, 10 Aug 2022 16:02:23 +0200 Subject: [PATCH 259/329] docs: add non-documented interaction --- docs/_docs/context-map.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_docs/context-map.md b/docs/_docs/context-map.md index ca10eb0f..05101351 100644 --- a/docs/_docs/context-map.md +++ b/docs/_docs/context-map.md @@ -12,6 +12,7 @@ title: Context Map Moreover, `ClientOrders` is going to be a *generic* bounded context, as reported in the Core Domain Chart. - `MilkPlanning [D, ACL] <- [U] Restocking` `MilkPlanning` asks to `Restocking` the remaining quantity of milk and informs `Restocking` to place an order for the required amount of milk. + In addition it asks to `Restocking` the quantity of milk used in the previous year. `MilkPlanning` is a downstream core domain since `Restocking` provides a service to it and the latter is going to be a generic bounded context, as reported in the Core Domain Chart. For all these reasons `MilkPlanning` has an Anti-Corruption Layer on its side. - `Production [D, CF] <- [U] ProductionPlanning` From ba9ff9046f6da0b5959154139c6e598eb6573df1 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Wed, 10 Aug 2022 16:51:58 +0200 Subject: [PATCH 260/329] refactor: change NewBatch event to better reflect how the domain works --- .../scala/dev/atedeg/mdm/production/Actions.scala | 10 ++++++++-- .../scala/dev/atedeg/mdm/production/Events.scala | 6 +++++- .../scala/dev/atedeg/mdm/production/Types.scala | 10 ++++++++++ .../dev/atedeg/mdm/production/api/Handlers.scala | 8 ++++---- .../api/emitters/StartProductionEmitter.scala | 2 +- .../dev/atedeg/mdm/production/dto/DTOs.scala | 15 +++++++++------ .../scala/dev/atedeg/mdm/production/Tests.scala | 8 ++++++-- 7 files changed, 43 insertions(+), 16 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala index 13d809c2..9793d85f 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Actions.scala @@ -1,5 +1,6 @@ package dev.atedeg.mdm.production +import java.time.{ LocalDate, LocalDateTime } import java.util.UUID import OutgoingEvent.* @@ -41,11 +42,16 @@ def startProduction[M[_]: Monad: CanRaise[MissingRecipe]: Emits[StartProduction] /** * Ends a [[Production.InProgress production]] by assigning it a [[BatchID batch ID]]. */ -def endProduction[M[_]: Monad: Emits[ProductionEnded]](production: Production.InProgress): M[Production.Ended] = +def endProduction[M[_]: Monad: Emits[NewBatch]](ripeningDays: CheeseTypeRipeningDays)( + production: Production.InProgress, +): M[Production.Ended] = val batchID = generateBatchID val producedProduct = production.productInProduction val unitsProduced = production.unitsInProduction - emit(ProductionEnded(production.ID, batchID): ProductionEnded) + val cheeseType = production.productInProduction.cheeseType + val days = ripeningDays.value(cheeseType).days.value.toLong + val readyBy = LocalDate.now.plusDays(days) + emit(NewBatch(batchID, cheeseType, readyBy): NewBatch) .thenReturn(Production.Ended(production.ID, batchID, producedProduct, unitsProduced)) private def generateBatchID: BatchID = BatchID(UUID.randomUUID) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala index d0f5fcdf..77ddfc27 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Events.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Events.scala @@ -1,7 +1,11 @@ package dev.atedeg.mdm.production +import java.time.{ LocalDate, LocalDateTime } + import cats.data.NonEmptyList +import dev.atedeg.mdm.products.CheeseType + /** * The events that may be produced by the bounded context. */ @@ -17,7 +21,7 @@ enum OutgoingEvent: * Fired when a [[Production.InProgress production]] is terminated, given a * [[BatchID batch ID]] and sent to the refrigeration room. */ - case ProductionEnded(productionID: ProductionID, batchID: BatchID) + case NewBatch(batchID: BatchID, cheeseType: CheeseType, readyFrom: LocalDate) enum IncomingEvent: /** diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index e27d02ce..5495e68e 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -73,3 +73,13 @@ final case class QuintalsOfIngredient(quintals: WeightInQuintals, ingredient: In * A weight expressed in quintals. */ final case class WeightInQuintals(n: PositiveDecimal) derives Times + +/** + * For each [[CheeseType cheese type]] associates it its ripening days. + */ +final case class CheeseTypeRipeningDays(value: Map[CheeseType, RipeningDays]) + +/** + * The number of days a [[CheeseType cheese type]] has to ripend before being ready. + */ +final case class RipeningDays(days: NonNegativeNumber) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala index aba14d47..e32ac6af 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala @@ -7,7 +7,7 @@ import cats.syntax.all.* import dev.atedeg.mdm.production.* import dev.atedeg.mdm.production.IncomingEvent.* -import dev.atedeg.mdm.production.OutgoingEvent.{ ProductionEnded, StartProduction } +import dev.atedeg.mdm.production.OutgoingEvent.* import dev.atedeg.mdm.production.api.repositories.RecipeBookRepository import dev.atedeg.mdm.production.dto.* import dev.atedeg.mdm.production.dto.given @@ -31,14 +31,14 @@ def handleProductionPlanReady[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRa yield () def handleProductionEnded[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]( - productionEnded: IncomingProductionEndedDTO, + productionEnded: ProductionEndedDTO, ): M[Unit] = for config <- readState productionID <- validate(productionEnded).map(_.productionID) production <- config.productionsRepository.readInProgressProduction(productionID.toDTO) >>= validate - action: SafeAction[ProductionEnded, Production.Ended] = endProduction(production) + action: SafeAction[NewBatch, Production.Ended] = endProduction(???)(production) // FIXME: ripening days (events, result) = action.execute - _ <- events.map(_.toDTO[ProductionEndedDTO]).traverse(config.emitter.emitEnded) + _ <- events.map(_.toDTO[NewBatchDTO]).traverse(config.emitter.emitNewBatch) _ <- config.productionsRepository.updateToEnded(result.toDTO[EndedDTO]) yield () diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala index 9472ce77..8523ca06 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala @@ -7,4 +7,4 @@ import dev.atedeg.mdm.production.dto.* trait Emitter: def emitStart[M[_]: Monad: LiftIO](message: StartProductionDTO): M[Unit] - def emitEnded[M[_]: Monad: LiftIO](message: ProductionEndedDTO): M[Unit] + def emitNewBatch[M[_]: Monad: LiftIO](message: NewBatchDTO): M[Unit] diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala index 03860a64..c046f959 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala @@ -1,10 +1,13 @@ package dev.atedeg.mdm.production.dto +import java.time.LocalDateTime + import cats.syntax.all.* import dev.atedeg.mdm.production.* import dev.atedeg.mdm.production.IncomingEvent.* import dev.atedeg.mdm.production.OutgoingEvent.* +import dev.atedeg.mdm.products.CheeseType import dev.atedeg.mdm.products.dto.CheeseTypeDTO.given import dev.atedeg.mdm.products.dto.IngredientDTO.given import dev.atedeg.mdm.products.dto.ProductDTO @@ -28,13 +31,13 @@ object StartProductionDTO: private given DTO[QuintalsOfIngredient, QuintalsOfIngredientDTO] = interCaseClassDTO private given DTO[WeightInQuintals, Double] = caseClassDTO -final case class ProductionEndedDTO(productionID: String, batchID: String) -object ProductionEndedDTO: - given DTO[OutgoingEvent.ProductionEnded, ProductionEndedDTO] = interCaseClassDTO +final case class NewBatchDTO(batchID: String, cheeseType: String, readyFrom: String) +object NewBatchDTO: + given DTO[NewBatch, NewBatchDTO] = interCaseClassDTO -final case class IncomingProductionEndedDTO(productionID: String) -object IncomingProductionEndedDTO: - given DTO[IncomingEvent.ProductionEnded, IncomingProductionEndedDTO] = interCaseClassDTO +final case class ProductionEndedDTO(productionID: String) +object ProductionEndedDTO: + given DTO[ProductionEnded, ProductionEndedDTO] = interCaseClassDTO final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) diff --git a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala index a75184c9..f47ac24e 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/Tests.scala @@ -1,5 +1,6 @@ package dev.atedeg.mdm.production +import java.time.LocalDate import java.util.UUID import OutgoingEvent.* @@ -26,6 +27,9 @@ trait Mocks: private val productionID = ProductionID(UUID.randomUUID) val production: Production.ToStart = Production.ToStart(productionID, Product.Caciotta(500), NumberOfUnits(10_000)) val allIngredients: NonEmptyList[Ingredient] = NonEmptyList.of(Milk, Cream, Rennet, Salt, Probiotics) + val ripeningDays: CheeseTypeRipeningDays = CheeseTypeRipeningDays( + Map.empty[CheeseType, RipeningDays].withDefaultValue(RipeningDays(0)), + ) class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: @@ -84,13 +88,13 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: production.unitsToProduce, ) When("it is ended") - val endAction: SafeAction[ProductionEnded, Production.Ended] = endProduction(productionInProgress) + val endAction: SafeAction[NewBatch, Production.Ended] = endProduction(ripeningDays)(productionInProgress) val (events, result) = endAction.execute Then("it should emit an event to notify that the production ended") result shouldBe a[Production.Ended] result.ID shouldBe productionInProgress.ID result.producedUnits shouldBe productionInProgress.unitsInProduction result.producedProduct shouldBe productionInProgress.productInProduction - events should contain(ProductionEnded(result.ID, result.batchID)) + events should contain(NewBatch(result.batchID, production.productToProduce.cheeseType, LocalDate.now)) } } From 70c22bed68c707e84dcba523d840ee1f54d4fb88 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Wed, 10 Aug 2022 18:08:48 +0200 Subject: [PATCH 261/329] chore: read from DB the cheese ripening days --- .../scala/dev/atedeg/mdm/production/api/Configuration.scala | 1 + .../main/scala/dev/atedeg/mdm/production/api/Handlers.scala | 3 ++- .../mdm/production/api/repositories/Repositories.scala | 3 +++ .../src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala | 5 +++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala index 1809de04..3fc75385 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Configuration.scala @@ -6,5 +6,6 @@ import dev.atedeg.mdm.production.api.repositories.* final case class Configuration( recipeBookRepository: RecipeBookRepository, productionsRepository: ProductionsRepository, + ripeningDaysRepository: CheeseTypeRipeningDaysRepository, emitter: Emitter, ) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala index e32ac6af..8a958360 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala @@ -36,8 +36,9 @@ def handleProductionEnded[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[ for config <- readState productionID <- validate(productionEnded).map(_.productionID) + cheeseTypeRipeningDays <- config.ripeningDaysRepository.read >>= validate production <- config.productionsRepository.readInProgressProduction(productionID.toDTO) >>= validate - action: SafeAction[NewBatch, Production.Ended] = endProduction(???)(production) // FIXME: ripening days + action: SafeAction[NewBatch, Production.Ended] = endProduction(cheeseTypeRipeningDays)(production) (events, result) = action.execute _ <- events.map(_.toDTO[NewBatchDTO]).traverse(config.emitter.emitNewBatch) _ <- config.productionsRepository.updateToEnded(result.toDTO[EndedDTO]) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala index db79fb8d..534e4e35 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/repositories/Repositories.scala @@ -13,3 +13,6 @@ trait ProductionsRepository: def writeInProgressProductions[M[_]: Monad: LiftIO](productions: List[InProgressDTO]): M[Unit] def readInProgressProduction[M[_]: Monad: LiftIO: CanRaise[String]](productionID: String): M[InProgressDTO] def updateToEnded[M[_]: Monad: LiftIO](production: EndedDTO): M[Unit] + +trait CheeseTypeRipeningDaysRepository: + def read[M[_]: Monad: LiftIO]: M[CheeseTypeRipeningDaysDTO] diff --git a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala index c046f959..e2955658 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/dto/DTOs.scala @@ -66,3 +66,8 @@ object InProgressDTO: final case class EndedDTO(id: String, batchID: String, product: ProductDTO, units: Int) object EndedDTO: given DTO[Production.Ended, EndedDTO] = interCaseClassDTO + +final case class CheeseTypeRipeningDaysDTO(value: Map[String, Int]) +object CheeseTypeRipeningDaysDTO: + given DTO[CheeseTypeRipeningDays, CheeseTypeRipeningDaysDTO] = interCaseClassDTO + private given DTO[RipeningDays, Int] = caseClassDTO From 4a104dcee2d86ce1f6ea3f7427407ecb49e40a5c Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 09:25:37 +0200 Subject: [PATCH 262/329] build: add fatal warnings compiler optionn --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index fbf30f93..ac181c9d 100644 --- a/build.sbt +++ b/build.sbt @@ -44,7 +44,7 @@ ThisBuild / scalafixDependencies ++= Seq( ) ThisBuild / semanticdbEnabled := true -ThisBuild / scalacOptions ++= Seq("-language:implicitConversions") +ThisBuild / scalacOptions ++= Seq("-language:implicitConversions", "-feature", "-Xfatal-warnings") lazy val startupTransition: State => State = "conventionalCommits" :: _ Global / onLoad := startupTransition compose (Global / onLoad).value From a8f8510b0daf58ed55c1ae2cb13569501edddae9 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 09:26:07 +0200 Subject: [PATCH 263/329] test: add production api handlers --- .../mdm/production/api/HandlersTests.scala | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala diff --git a/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala b/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala new file mode 100644 index 00000000..4010121e --- /dev/null +++ b/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala @@ -0,0 +1,101 @@ +package dev.atedeg.mdm.production.api + +import java.time.LocalDate +import java.util.UUID + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import dev.atedeg.mdm.production.api.emitters.Emitter +import dev.atedeg.mdm.production.api.repositories.{ + CheeseTypeRipeningDaysRepository, + ProductionsRepository, + RecipeBookRepository, +} +import dev.atedeg.mdm.production.dto.* +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.monads.ServerAction +import dev.atedeg.mdm.utils.serialization.DTOOps.toDTO + +@SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) +trait Mocks: + var emittedStarts: List[StartProductionDTO] = Nil + var emittedNews: List[NewBatchDTO] = Nil + var savedInProgressProductions: List[InProgressDTO] = Nil + var ended: Option[EndedDTO] = None + + val productionsRepository: ProductionsRepository = new ProductionsRepository: + override def writeInProgressProductions[M[_]: Monad: LiftIO](productions: List[InProgressDTO]): M[Unit] = + savedInProgressProductions = productions + ().pure + override def readInProgressProduction[M[_]: Monad: LiftIO: CanRaise[String]]( + productionID: String, + ): M[InProgressDTO] = InProgressDTO(productionID, ProductDTO("ricotta", 350), 30).pure + override def updateToEnded[M[_]: Monad: LiftIO](production: EndedDTO): M[Unit] = + ended = Some(production) + ().pure + + val recipeBookRepository: RecipeBookRepository = new RecipeBookRepository: + override def read[M[_]: Monad: LiftIO]: M[RecipeBookDTO] = + RecipeBookDTO(Map("ricotta" -> RecipeDTO(List(QuintalsOfIngredientDTO(10, "milk"))))).pure + + val ripeningDaysRepository: CheeseTypeRipeningDaysRepository = new CheeseTypeRipeningDaysRepository: + override def read[M[_]: Monad: LiftIO]: M[CheeseTypeRipeningDaysDTO] = + CheeseTypeRipeningDaysDTO(Map("ricotta" -> 0)).pure + + val emitter: Emitter = new Emitter: + override def emitStart[M[_]: Monad: LiftIO](message: StartProductionDTO): M[Unit] = + emittedStarts = message :: emittedStarts + ().pure + override def emitNewBatch[M[_]: Monad: LiftIO](message: NewBatchDTO): M[Unit] = + emittedNews = message :: emittedNews + ().pure + + val configuration: Configuration = + Configuration(recipeBookRepository, productionsRepository, ripeningDaysRepository, emitter) + +class HandlersTest extends AnyWordSpec, Matchers, Mocks: + "The `handleProductionPlanReady` handler" should { + val productsToProduce = List(ProductToProduceDTO(ProductDTO("ricotta", 350), 1000)) + val productionPlan = ProductionPlanDTO(productsToProduce) + val productionPlanReady = ProductionPlanReadyDTO(productionPlan) + val action: ServerAction[Configuration, String, Unit] = handleProductionPlanReady(productionPlanReady) + action.unsafeExecute(configuration) + + "write the new productions to the DB" in { + savedInProgressProductions.size shouldBe 1 + @SuppressWarnings(Array("org.wartremover.warts.IterableOps")) + val savedProduction = savedInProgressProductions.head + savedProduction.product shouldBe ProductDTO("ricotta", 350) + savedProduction.units shouldBe 1000 + } + "emit all the domain events" in { + emittedStarts shouldBe List(StartProductionDTO(List(QuintalsOfIngredientDTO(35, "milk")))) + } + } + + "The `handleProductionPlanEnded` handler" should { + val productionID = UUID.randomUUID.toDTO[String] + val productionEndedDTO = ProductionEndedDTO(productionID) + val action: ServerAction[Configuration, String, Unit] = handleProductionEnded(productionEndedDTO) + action.unsafeExecute(configuration) + "update the production in the DB" in { + ended shouldBe defined + @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) + val updatedProduction = ended.get + updatedProduction.id shouldBe productionID + updatedProduction.product shouldBe ProductDTO("ricotta", 350) + updatedProduction.units shouldBe 30 + } + "emit all the domain events" in { + emittedNews.size shouldBe 1 + @SuppressWarnings(Array("org.wartremover.warts.IterableOps")) + val emitted = emittedNews.head + emitted.cheeseType shouldBe "ricotta" + emitted.readyFrom shouldBe LocalDate.now.toDTO[String] + } + } From 48eea572d3c0c87bd3b54d1ce4247ef58c2c8bea Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 09:46:25 +0200 Subject: [PATCH 264/329] docs: fix ubiquitous language tables --- .ubidoc.yml | 5 ++++- .../src/main/scala/dev/atedeg/mdm/production/Types.scala | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index 88fbebda..55c77daf 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -57,17 +57,20 @@ tables: - type: "RecipeBook" - class: "Recipe" - class: "QuintalsOfIngredient" + - class: "CheeseTypeRipeningDays" + - class: "RipeningDays" - name: "production-outgoing" termName: "Event" definitionName: "Description" rows: - case: "OutgoingEvent.StartProduction" - - case: "OutgoingEvent.ProductionEnded" + - case: "OutgoingEvent.NewBatch" - name: "production-incoming" termName: "Event" definitionName: "Description" rows: - case: "IncomingEvent.ProductionPlanReady" + - case: "IncomingEvent.ProductionEnded" - name: "restocking-ul" rows: diff --git a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala index 5495e68e..ac17d951 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/Types.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/Types.scala @@ -75,11 +75,11 @@ final case class QuintalsOfIngredient(quintals: WeightInQuintals, ingredient: In final case class WeightInQuintals(n: PositiveDecimal) derives Times /** - * For each [[CheeseType cheese type]] associates it its ripening days. + * For each [[CheeseType cheese type]] associates it its [[RipeningDays ripening days]]. */ final case class CheeseTypeRipeningDays(value: Map[CheeseType, RipeningDays]) /** - * The number of days a [[CheeseType cheese type]] has to ripend before being ready. + * The number of days a [[CheeseType cheese type]] has to ripen before being ready. */ final case class RipeningDays(days: NonNegativeNumber) From 6e75a648ca72dee873d56243872fe5abda20cdf4 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 09:52:28 +0200 Subject: [PATCH 265/329] test: refactor tests to address @nicolasfara 's comment --- .../mdm/production/api/HandlersTests.scala | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala b/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala index 4010121e..7fb10561 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala @@ -67,11 +67,12 @@ class HandlersTest extends AnyWordSpec, Matchers, Mocks: action.unsafeExecute(configuration) "write the new productions to the DB" in { - savedInProgressProductions.size shouldBe 1 - @SuppressWarnings(Array("org.wartremover.warts.IterableOps")) - val savedProduction = savedInProgressProductions.head - savedProduction.product shouldBe ProductDTO("ricotta", 350) - savedProduction.units shouldBe 1000 + savedInProgressProductions match + case Nil => fail("No productions were saved") + case List(saved) => + saved.product shouldBe ProductDTO("ricotta", 350) + saved.units shouldBe 1000 + case _ => fail("Saved more productions than expected") } "emit all the domain events" in { emittedStarts shouldBe List(StartProductionDTO(List(QuintalsOfIngredientDTO(35, "milk")))) @@ -84,18 +85,19 @@ class HandlersTest extends AnyWordSpec, Matchers, Mocks: val action: ServerAction[Configuration, String, Unit] = handleProductionEnded(productionEndedDTO) action.unsafeExecute(configuration) "update the production in the DB" in { - ended shouldBe defined - @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) - val updatedProduction = ended.get - updatedProduction.id shouldBe productionID - updatedProduction.product shouldBe ProductDTO("ricotta", 350) - updatedProduction.units shouldBe 30 + ended match + case None => fail("The production was not updated") + case Some(updated) => + updated.id shouldBe productionID + updated.product shouldBe ProductDTO("ricotta", 350) + updated.units shouldBe 30 } "emit all the domain events" in { - emittedNews.size shouldBe 1 - @SuppressWarnings(Array("org.wartremover.warts.IterableOps")) - val emitted = emittedNews.head - emitted.cheeseType shouldBe "ricotta" - emitted.readyFrom shouldBe LocalDate.now.toDTO[String] + emittedNews match + case Nil => fail("No event was emitted") + case List(emitted) => + emitted.cheeseType shouldBe "ricotta" + emitted.readyFrom shouldBe LocalDate.now.toDTO[String] + case _ => fail("Emitted more events than expected") } } From 24897a32f0017e324cfebc57d22704453392ee72 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 09:56:41 +0200 Subject: [PATCH 266/329] chore: rename emitter method --- .../src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala | 2 +- .../mdm/production/api/emitters/StartProductionEmitter.scala | 2 +- .../scala/dev/atedeg/mdm/production/api/HandlersTests.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala index 8a958360..2023d025 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/Handlers.scala @@ -25,7 +25,7 @@ def handleProductionPlanReady[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRa action: Action[MissingRecipe, StartProduction, NonEmptyList[Production.InProgress]] = productions.traverse(startProduction(recipeBook)) (events, res) = action.execute - _ <- events.map(_.toDTO[StartProductionDTO]).traverse(config.emitter.emitStart) + _ <- events.map(_.toDTO[StartProductionDTO]).traverse(config.emitter.emitStartProduction) productions <- res.leftMap(m => s"Missing recipe: $m").getOrRaise _ <- config.productionsRepository.writeInProgressProductions(productions.toDTO) yield () diff --git a/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala b/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala index 8523ca06..6d3a25aa 100644 --- a/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala +++ b/production/src/main/scala/dev/atedeg/mdm/production/api/emitters/StartProductionEmitter.scala @@ -6,5 +6,5 @@ import cats.effect.LiftIO import dev.atedeg.mdm.production.dto.* trait Emitter: - def emitStart[M[_]: Monad: LiftIO](message: StartProductionDTO): M[Unit] + def emitStartProduction[M[_]: Monad: LiftIO](message: StartProductionDTO): M[Unit] def emitNewBatch[M[_]: Monad: LiftIO](message: NewBatchDTO): M[Unit] diff --git a/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala b/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala index 7fb10561..96ace84a 100644 --- a/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala +++ b/production/src/test/scala/dev/atedeg/mdm/production/api/HandlersTests.scala @@ -48,7 +48,7 @@ trait Mocks: CheeseTypeRipeningDaysDTO(Map("ricotta" -> 0)).pure val emitter: Emitter = new Emitter: - override def emitStart[M[_]: Monad: LiftIO](message: StartProductionDTO): M[Unit] = + override def emitStartProduction[M[_]: Monad: LiftIO](message: StartProductionDTO): M[Unit] = emittedStarts = message :: emittedStarts ().pure override def emitNewBatch[M[_]: Monad: LiftIO](message: NewBatchDTO): M[Unit] = From 4b8474c797f54bc8e7ddb7b25df37fd82844cfea Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Tue, 9 Aug 2022 18:03:35 +0200 Subject: [PATCH 267/329] feat: add product palletized event to client orders bc --- .../main/scala/dev/atedeg/mdm/clientorders/Actions.scala | 8 ++++++-- .../main/scala/dev/atedeg/mdm/clientorders/Events.scala | 5 +++++ .../main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala | 5 +++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 9f0add4c..9a6b4f68 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -10,7 +10,7 @@ import cats.syntax.all.* import dev.atedeg.mdm.clientorders.InProgressOrderLine.* import dev.atedeg.mdm.clientorders.OrderCompletionError.* -import dev.atedeg.mdm.clientorders.OutgoingEvent.OrderProcessed +import dev.atedeg.mdm.clientorders.OutgoingEvent.{ OrderProcessed, ProductPalletized } import dev.atedeg.mdm.clientorders.PalletizationError.* import dev.atedeg.mdm.clientorders.utils.* import dev.atedeg.mdm.clientorders.utils.QuantityOps.* @@ -60,7 +60,10 @@ def startPreparingOrder(pricedOrder: PricedOrder): InProgressOrder = * where the corresponding [[Order.InProgressOrderLine line]] has been updated with the * [[Order.Quantity specified quantity]]. */ -def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad](quantity: Quantity, product: Product)( +def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad: Emits[ProductPalletized]]( + quantity: Quantity, + product: Product, +)( inProgressOrder: InProgressOrder, ): M[InProgressOrder] = val InProgressOrder(id, ol, customer, dd, dl, totalPrice) = inProgressOrder @@ -71,6 +74,7 @@ def palletizeProductForOrder[M[_]: CanRaise[PalletizationError]: Monad](quantity case Incomplete(_, _, `product`, _) => updatedLine case l @ _ => l } + _ <- emit(ProductPalletized(product, quantity): ProductPalletized) yield InProgressOrder(id, newOrderLines, customer, dd, dl, totalPrice) private def hasProduct(product: Product)(ol: InProgressOrderLine): Boolean = ol match diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala index bc3bf342..a2b6973d 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala @@ -42,3 +42,8 @@ enum OutgoingEvent: * An event emitted when a new [[IncomingOrder incoming order]] is received and processed. */ case OrderProcessed(incomingOrder: IncomingOrder) + + /** + * An event emitted when a [[Product product]] is successfully palletized for an [[Order.InProgressoOrder order]]. + */ + case ProductPalletized(product: Product, quantity: Quantity) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala index df05a252..3911a28a 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -55,3 +55,8 @@ final case class IncomingOrderDTO( object OrderProcessedDTO: given DTO[OrderProcessed, OrderProcessedDTO] = interCaseClassDTO private given DTO[IncomingOrder, IncomingOrderDTO] = interCaseClassDTO + +final case class ProductPalletizedDTO(product: ProductDTO, quantity: Int) +object ProductPalletizedDTO: + given DTO[ProductPalletized, ProductPalletizedDTO] = interCaseClassDTO + private given DTO[Quantity, Int] = caseClassDTO \ No newline at end of file From 44ec36436e2cc05b3ec604b6413529bea9c0b62b Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 15:50:51 +0200 Subject: [PATCH 268/329] feat: available stock and desired stock are now case classes --- .../dev/atedeg/mdm/stocking/Actions.scala | 11 +++--- .../scala/dev/atedeg/mdm/stocking/Types.scala | 4 +- .../scala/dev/atedeg/mdm/stocking/Tests.scala | 38 ++++++++++--------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 586ef287..da02ff86 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -7,6 +7,7 @@ import cats.syntax.all.* import dev.atedeg.mdm.products.* import dev.atedeg.mdm.products.utils.* import dev.atedeg.mdm.products.utils.given +import dev.atedeg.mdm.stocking.* import dev.atedeg.mdm.stocking.Error.* import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.utils.* @@ -20,8 +21,8 @@ def getMissingCountFromProductStock( availableStock: AvailableStock, desiredStock: DesiredStock, )(product: Product): MissingQuantity = - if availableStock(product).n >= desiredStock(product).n then MissingQuantity(0) - else MissingQuantity(desiredStock(product).n.toNonNegative - availableStock(product).n) + if availableStock.as(product).n >= desiredStock.ds(product).n then MissingQuantity(0) + else MissingQuantity(desiredStock.ds(product).n.toNonNegative - availableStock.as(product).n) /** * Removes the given quantity of a certain [[Product product]] from the [[AvailableStock stock]], giving the new current [[AvailableStock stock]]. @@ -29,9 +30,9 @@ def getMissingCountFromProductStock( def removeFromStock[M[_]: Monad: CanRaise[NotEnoughStock]]( stock: AvailableStock, )(product: Product, quantity: Quantity): M[AvailableStock] = - (stock(product).n > quantity.n) - .otherwiseRaise(NotEnoughStock(product, quantity, stock(product)): NotEnoughStock) - .thenReturn(stock + (product -> AvailableQuantity(stock(product).n - quantity.n))) + (stock.as(product).n > quantity.n) + .otherwiseRaise(NotEnoughStock(product, quantity, stock.as(product)): NotEnoughStock) + .thenReturn(AvailableStock(stock.as + (product -> AvailableQuantity(stock.as(product).n - quantity.n)))) /** * Approves a batch after quality assurance. diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index 801a4e14..b2f632fa 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -33,13 +33,13 @@ final case class MissingQuantity(n: NonNegativeNumber) * in a certain [[AvailableQuantity quantity]] (that could also be zero if the * product is out-of-stock). */ -type AvailableStock = Map[Product, AvailableQuantity] +final case class AvailableStock(as: Map[Product, AvailableQuantity]) /** * The [[DesiredQuantity desired quantity]] of each [[Product product]] that should * always be in stock in order to have a safe margin to keep order fulfillment going. */ -type DesiredStock = Map[Product, DesiredQuantity] +final case class DesiredStock(ds: Map[Product, DesiredQuantity]) /** * A batch of products of a certain [[CheeseType type]], uniquely identified by an [[BatchID ID]], diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index 48af3395..ff22624d 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -30,20 +30,24 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Feature("Missing stock") { Scenario("There are missing products from the desired stock") { Given("An available stock") - val available = Map( - squacquerone -> AvailableQuantity(10), - casatella -> AvailableQuantity(20), - ricotta -> AvailableQuantity(30), - stracchino -> AvailableQuantity(40), - caciotta -> AvailableQuantity(50), + val available = AvailableStock( + Map( + squacquerone -> AvailableQuantity(10), + casatella -> AvailableQuantity(20), + ricotta -> AvailableQuantity(30), + stracchino -> AvailableQuantity(40), + caciotta -> AvailableQuantity(50), + ), ) And("a desired stock") - val desired = Map( - squacquerone -> DesiredQuantity(20), - casatella -> DesiredQuantity(30), - ricotta -> DesiredQuantity(40), - stracchino -> DesiredQuantity(50), - caciotta -> DesiredQuantity(60), + val desired = DesiredStock( + Map( + squacquerone -> DesiredQuantity(20), + casatella -> DesiredQuantity(30), + ricotta -> DesiredQuantity(40), + stracchino -> DesiredQuantity(50), + caciotta -> DesiredQuantity(60), + ), ) When("someone asks how many products are missing to reach the desired stock") val missingSquacquerone = getMissingCountFromProductStock(available, desired)(squacquerone) @@ -60,9 +64,9 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: } Scenario("There are more products than needed in the desired stock") { Given("An available stock") - val available = Map(squacquerone -> AvailableQuantity(20)) + val available = AvailableStock(Map(squacquerone -> AvailableQuantity(20))) And("a desired stock") - val desired = Map(squacquerone -> DesiredQuantity(10)) + val desired = DesiredStock(Map(squacquerone -> DesiredQuantity(10))) When("someone asks how many products are missing to reach the desired stock") val missing = getMissingCountFromProductStock(available, desired)(squacquerone) Then("the missing quantity should be zero") @@ -70,18 +74,18 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: } Scenario("Removal from stock with enough available products") { Given("An available stock") - val available = Map(squacquerone -> AvailableQuantity(10)) + val available = AvailableStock(Map(squacquerone -> AvailableQuantity(10))) And("a quantity to remove from stock") val toRemove = Quantity(5) When("someone removes the product from the stock") val action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(available)(squacquerone, toRemove) Then("the stock should be updated") val (_, result) = action.execute - result.value shouldEqual Map(squacquerone -> AvailableQuantity(5)) + result.value shouldEqual AvailableStock(Map(squacquerone -> AvailableQuantity(5))) } Scenario("Removal from stock with not enough available products") { Given("An available stock") - val available = Map(squacquerone -> AvailableQuantity(10)) + val available = AvailableStock(Map(squacquerone -> AvailableQuantity(10))) And("a quantity to remove from stock that is greater than the available one") val toRemove = Quantity(50) When("someone removes the product from the stock") From 84386756b00911c91d8c9a8ce3d7719f83692d00 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 18:37:05 +0200 Subject: [PATCH 269/329] chore: generate list of tuple dtos --- .../atedeg/mdm/utils/serialization/DTO.scala | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala index ff8749e9..6f856990 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala @@ -1,16 +1,17 @@ package dev.atedeg.mdm.utils.serialization -import cats.Monad - -import java.time.{LocalDate, LocalDateTime} +import java.time.{ LocalDate, LocalDateTime } import java.time.format.DateTimeFormatter import java.util.UUID import scala.compiletime.* import scala.deriving.* import scala.util.Try + +import cats.Monad import cats.data.NonEmptyList import cats.syntax.all.* import eu.timepit.refined.api.Refined + import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.monads.* @@ -21,7 +22,7 @@ trait DTO[E, D]: object DTOOps: extension [D](dto: D) def toDomain[E](using d: DTO[E, D]): Either[String, E] = d.dtoToElem(dto) extension [E](e: E) def toDTO[D](using d: DTO[E, D]) = d.elemToDto(e) - def validate[D, E, M[_]: Monad: CanRaise[String]](dto: D)(using DTO[E, D]): M[E] = dto.toDomain[E].getOrRaise + def validate[D, E, M[_]: Monad: CanRaise[String]](dto: D)(using DTO[E, D]): M[E] = dto.toDomain[E].getOrRaise object DTO: import DTOGenerators.* @@ -31,6 +32,14 @@ object DTO: given DTO[Double, Double] = idDTO given DTO[String, String] = idDTO + given mapListDTO[KE, KD, VE, VD](using DTO[KE, KD])(using DTO[VE, VD]): DTO[Map[KE, VE], List[(KD, VD)]] with + override def elemToDto(e: Map[KE, VE]): List[(KD, VD)] = e.toList.map(_.bimap(_.toDTO[KD], _.toDTO[VD])) + override def dtoToElem(dto: List[(KD, VD)]): Either[String, Map[KE, VE]] = + for + keys <- dto.map(_._1).traverse(_.toDomain[KE]) + values <- dto.map(_._2).traverse(_.toDomain[VE]) + yield keys.zip(values).toMap + given mapDTO[KE, KD, VE, VD](using DTO[KE, KD])(using DTO[VE, VD]): DTO[Map[KE, VE], Map[KD, VD]] with override def elemToDto(e: Map[KE, VE]): Map[KD, VD] = e.map(_.bimap(_.toDTO[KD], _.toDTO[VD])) override def dtoToElem(dto: Map[KD, VD]): Either[String, Map[KE, VE]] = From a3ca54cdf4cb0bb40f4c5074c697c61b518de059 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 18:41:36 +0200 Subject: [PATCH 270/329] refactor: stock names --- .../scala/dev/atedeg/mdm/stocking/Actions.scala | 17 +++++++++++------ .../scala/dev/atedeg/mdm/stocking/Events.scala | 2 +- .../scala/dev/atedeg/mdm/stocking/Types.scala | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index da02ff86..4a859c5b 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -21,18 +21,23 @@ def getMissingCountFromProductStock( availableStock: AvailableStock, desiredStock: DesiredStock, )(product: Product): MissingQuantity = - if availableStock.as(product).n >= desiredStock.ds(product).n then MissingQuantity(0) - else MissingQuantity(desiredStock.ds(product).n.toNonNegative - availableStock.as(product).n) + if availableStock.availableStock(product).n >= desiredStock.desiredStock(product).n then MissingQuantity(0) + else MissingQuantity(desiredStock.desiredStock(product).n.toNonNegative - availableStock.availableStock(product).n) /** - * Removes the given quantity of a certain [[Product product]] from the [[AvailableStock stock]], giving the new current [[AvailableStock stock]]. + * Removes the given quantity of a certain [[Product product]] from the [[AvailableStock stock]], giving the + * new current [[AvailableStock stock]]. */ def removeFromStock[M[_]: Monad: CanRaise[NotEnoughStock]]( stock: AvailableStock, )(product: Product, quantity: Quantity): M[AvailableStock] = - (stock.as(product).n > quantity.n) - .otherwiseRaise(NotEnoughStock(product, quantity, stock.as(product)): NotEnoughStock) - .thenReturn(AvailableStock(stock.as + (product -> AvailableQuantity(stock.as(product).n - quantity.n)))) + (stock.availableStock(product).n > quantity.n) + .otherwiseRaise(NotEnoughStock(product, quantity, stock.availableStock(product)): NotEnoughStock) + .thenReturn( + AvailableStock( + stock.availableStock + (product -> AvailableQuantity(stock.availableStock(product).n - quantity.n)), + ), + ) /** * Approves a batch after quality assurance. diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala index f483b65b..20df444b 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Events.scala @@ -25,7 +25,7 @@ enum IncomingEvent: /** * Received when a [[Product product]] is removed from the stock. */ - case ProductRemovedFromStock(quantity: DesiredQuantity, product: Product) + case ProductRemovedFromStock(quantity: Quantity, product: Product) /** * Received when a [[Batch.Aging batch]] is created. diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala index b2f632fa..e1d23f2c 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Types.scala @@ -33,13 +33,13 @@ final case class MissingQuantity(n: NonNegativeNumber) * in a certain [[AvailableQuantity quantity]] (that could also be zero if the * product is out-of-stock). */ -final case class AvailableStock(as: Map[Product, AvailableQuantity]) +final case class AvailableStock(availableStock: Map[Product, AvailableQuantity]) /** * The [[DesiredQuantity desired quantity]] of each [[Product product]] that should * always be in stock in order to have a safe margin to keep order fulfillment going. */ -final case class DesiredStock(ds: Map[Product, DesiredQuantity]) +final case class DesiredStock(desiredStock: Map[Product, DesiredQuantity]) /** * A batch of products of a certain [[CheeseType type]], uniquely identified by an [[BatchID ID]], From 0a9ddffb61ebeae9b9abeb07cc1fd9e57d63e4b4 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 18:42:19 +0200 Subject: [PATCH 271/329] chore: more stocking dtos --- .../dev/atedeg/mdm/stocking/dto/DTOs.scala | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala index 65f2b54f..d4632369 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala @@ -18,15 +18,41 @@ object ProductStockedDTO: private given DTO[BatchID, String] = caseClassDTO -final case class BatchReadyForQualityAssuranceDTO(batch: String) -object BatchReadyForQualityAssuranceDTO: - given DTO[BatchReadyForQualityAssurance, BatchReadyForQualityAssuranceDTO] = interCaseClassDTO +final case class BatchReadyForQualityAssuranceEventDTO(batch: String) +object BatchReadyForQualityAssuranceEventDTO: + given DTO[BatchReadyForQualityAssurance, BatchReadyForQualityAssuranceEventDTO] = interCaseClassDTO final case class ProductRemovedFromStockDTO(quantity: Int, product: ProductDTO) object ProductRemovedFromStockDTO: given DTO[ProductRemovedFromStock, ProductRemovedFromStockDTO] = interCaseClassDTO - given DTO[DesiredQuantity, Int] = caseClassDTO + given DTO[Quantity, Int] = caseClassDTO final case class NewBatchDTO(batchID: String, cheeseType: String, readyFrom: String) object NewBatchDTO: given DTO[NewBatch, NewBatchDTO] = interCaseClassDTO + +final case class AvailableStockDTO(as: List[(ProductDTO, Int)]) +object AvailableStockDTO: + given DTO[AvailableStock, AvailableStockDTO] = interCaseClassDTO + private given DTO[AvailableQuantity, Int] = caseClassDTO + +final case class DesiredStockDTO(ds: List[(ProductDTO, Int)]) +object DesiredStockDTO: + given DTO[DesiredStock, DesiredStockDTO] = interCaseClassDTO + private given DTO[DesiredQuantity, Int] = caseClassDTO + +final case class AgingBatchDTO(batchID: String, cheeseType: String, readyFrom: String) +object AgingBatchDTO: + given DTO[Batch.Aging, AgingBatchDTO] = interCaseClassDTO + +final case class QualityAssuredBatchPassedDTO(id: String, cheeseType: String) +object QualityAssuredBatchPassedDTO: + given DTO[QualityAssuredBatch.Passed, QualityAssuredBatchPassedDTO] = interCaseClassDTO + +final case class QualityAssuredBatchFailedDTO(id: String, cheeseType: String) +object QualityAssuredBatchFailedDTO: + given DTO[QualityAssuredBatch.Failed, QualityAssuredBatchFailedDTO] = interCaseClassDTO + +final case class BatchReadyForQualityAssuranceDTO(id: String, cheeseType: String) +object BatchReadyForQualityAssuranceDTO: + given DTO[Batch.ReadyForQualityAssurance, BatchReadyForQualityAssuranceDTO] = interCaseClassDTO From ed8a27d634df701d5a51125b2823e2b75fae28d1 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 18:43:36 +0200 Subject: [PATCH 272/329] feat: add handlers --- .../atedeg/mdm/stocking/api/Handlers.scala | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/api/Handlers.scala diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/Handlers.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/Handlers.scala new file mode 100644 index 00000000..dd920c96 --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/Handlers.scala @@ -0,0 +1,61 @@ +package dev.atedeg.mdm.stocking.api + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* + +import dev.atedeg.mdm.stocking.* +import dev.atedeg.mdm.stocking.Error.NotEnoughStock +import dev.atedeg.mdm.stocking.IncomingEvent.ProductRemovedFromStock +import dev.atedeg.mdm.stocking.api.acl.ProductPalletizedDTO +import dev.atedeg.mdm.stocking.api.acl.toProductRemovedFromStockDTO +import dev.atedeg.mdm.stocking.api.repositories.{ BatchesRepository, StockRepository } +import dev.atedeg.mdm.stocking.dto.* +import dev.atedeg.mdm.stocking.dto.AvailableStockDTO.given +import dev.atedeg.mdm.stocking.dto.DesiredStockDTO.given +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +def handleRemovalFromStock[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]]( + productPalletized: ProductPalletizedDTO, +): M[Unit] = + for + repository <- readState + removed <- validate(productPalletized.toProductRemovedFromStockDTO) + stock <- repository.readStock >>= validate + action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(stock)(removed.product, removed.quantity) + (_, res) = action.execute + updatedStock <- res.leftMap(e => s"Not enough in stock: $e").getOrRaise + _ <- repository.writeStock(updatedStock.toDTO[AvailableStockDTO]) + yield () + +def handleNewBatch[M[_]: Monad: LiftIO: CanRead[BatchesRepository]: CanRaise[String]]( + newBatchDTO: NewBatchDTO, +): M[Unit] = + for + newBatch <- validate(newBatchDTO) + batch: Batch.Aging = Batch.Aging(newBatch.batchID, newBatch.cheeseType, newBatch.readyFrom) + _ <- readState >>= (_.addNewBatch(batch.toDTO[AgingBatchDTO])) + yield () + +def handleDesiredStockRequest[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]]: M[DesiredStockDTO] = + (readState >>= (_.readDesiredStock) >>= validate).map(_.toDTO) + +def handleProductsInStockRequest[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]] + : M[AvailableStockDTO] = (readState >>= (_.readStock) >>= validate).map(_.toDTO) + +def approveBatchHandler[M[_]: Monad: LiftIO: CanRead[BatchesRepository]: CanRaise[String]](batchID: String): M[Unit] = + for + repository <- readState + batchReady <- repository.readReadyForQA(batchID) >>= validate + approvedBatch = approveBatch(batchReady) + _ <- repository.approveBatch(approvedBatch.toDTO) + yield () + +def rejectBatchHandler[M[_]: Monad: LiftIO: CanRead[BatchesRepository]: CanRaise[String]](batchID: String): M[Unit] = + for + repository <- readState + batchReady <- repository.readReadyForQA(batchID) >>= validate + rejectedBatch = rejectBatch(batchReady) + _ <- repository.rejectBatch(rejectedBatch.toDTO) + yield () \ No newline at end of file From d85d6c2eb4ecbd1df20ec3b9f941ee1283a6d9a6 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 18:44:08 +0200 Subject: [PATCH 273/329] feat: add acl dtos --- .../scala/dev/atedeg/mdm/stocking/api/acl/DTOs.scala | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/api/acl/DTOs.scala diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/acl/DTOs.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/acl/DTOs.scala new file mode 100644 index 00000000..a83ffaf0 --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/acl/DTOs.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.stocking.api.acl + +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.stocking.dto.ProductRemovedFromStockDTO + +final case class ProductPalletizedDTO(product: ProductDTO, quantity: Int) +extension (ppDTO: ProductPalletizedDTO) + def toProductRemovedFromStockDTO: ProductRemovedFromStockDTO = + ProductRemovedFromStockDTO(ppDTO.quantity, ppDTO.product) From 344f2666adeefc268b16d7ab6d98f08ce94bed68 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 18:44:41 +0200 Subject: [PATCH 274/329] feat: add repositories --- .../api/repositories/Repositories.scala | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/api/repositories/Repositories.scala diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/repositories/Repositories.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/repositories/Repositories.scala new file mode 100644 index 00000000..1fd75818 --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/repositories/Repositories.scala @@ -0,0 +1,30 @@ +package dev.atedeg.mdm.stocking.api.repositories + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.stocking.QualityAssuredBatch +import dev.atedeg.mdm.stocking.dto.* +import dev.atedeg.mdm.utils.monads.CanRaise + +trait StockRepository: + def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] + def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] + def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] + +trait BatchesRepository: + def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] + def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]](id: String): M[BatchReadyForQualityAssuranceDTO] + def approveBatch[M[_]: Monad: LiftIO](passedBatch: QualityAssuredBatchPassedDTO): M[Unit] + def rejectBatch[M[_]: Monad: LiftIO](failedBatch: QualityAssuredBatchFailedDTO): M[Unit] + +final case class StockRepositoryDB(connectionString: String) extends StockRepository: + def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] = ??? + def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] = ??? + def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] = ??? + +final case class BatchesRepositoryDB(connectionString: String) extends BatchesRepository: + def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] = ??? + def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]](id: String): M[BatchReadyForQualityAssuranceDTO] = ??? + def approveBatch[M[_]: Monad: LiftIO](passedBatch: QualityAssuredBatchPassedDTO): M[Unit] = ??? + def rejectBatch[M[_]: Monad: LiftIO](failedBatch: QualityAssuredBatchFailedDTO): M[Unit] = ??? From 938dae62581a74aca245b689f074ef3cda9e4517 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 18:45:10 +0200 Subject: [PATCH 275/329] feat: add endpoints --- .../scala/dev/atedeg/mdm/stocking/Main.scala | 32 ++++++++ .../stocking/api/endpoints/Endpoints.scala | 78 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala create mode 100644 stocking/src/main/scala/dev/atedeg/mdm/stocking/api/endpoints/Endpoints.scala diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala new file mode 100644 index 00000000..87283b43 --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala @@ -0,0 +1,32 @@ +package dev.atedeg.mdm.stocking + +import scala.util.Properties + +import cats.effect.{ ExitCode, IO, IOApp } +import cats.syntax.all.* +import org.http4s.HttpRoutes +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router +import sttp.tapir.server.http4s.Http4sServerInterpreter +import sttp.tapir.swagger.bundle.SwaggerInterpreter + +import dev.atedeg.mdm.stocking.api.endpoints.BatchesRequests.* +import dev.atedeg.mdm.stocking.api.endpoints.StockRequests.* + +object Main extends IOApp: + private val swaggerEndpoint = SwaggerInterpreter().fromEndpoints[IO]( + desiredStockRequestEndpoint :: availableStockRequestEndpoint :: approveBatchRequestEndpoint :: rejectBatchRequestEndpoint :: Nil, + "stocking", + Properties.envOrElse("VERSION", "v1-beta"), + ) + private val swaggerRoute = Http4sServerInterpreter[IO]().toRoutes(swaggerEndpoint) + private val routes: HttpRoutes[IO] = + desiredStockRoute <+> availableStockRoute <+> approveBatchRoute <+> rejectBatchRoute <+> swaggerRoute + + override def run(args: List[String]): IO[ExitCode] = + BlazeServerBuilder[IO] + .bindHttp(Properties.envOrElse("PORT", "8080").toInt, Properties.envOrElse("HOST", "localhost")) + .withHttpApp(Router("/" -> routes).orNotFound) + .resource + .use(_ => IO.never[Unit]) + .as(ExitCode.Success) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/endpoints/Endpoints.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/endpoints/Endpoints.scala new file mode 100644 index 00000000..50482a9a --- /dev/null +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/endpoints/Endpoints.scala @@ -0,0 +1,78 @@ +package dev.atedeg.mdm.stocking.api.endpoints + +import cats.effect.IO +import io.circe.generic.auto.* +import org.http4s.HttpRoutes +import sttp.tapir.* +import sttp.tapir.PublicEndpoint +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.jsonBody +import sttp.tapir.server.http4s.Http4sServerInterpreter + +import dev.atedeg.mdm.stocking.api.* +import dev.atedeg.mdm.stocking.api.repositories.{ + BatchesRepository, + BatchesRepositoryDB, + StockRepository, + StockRepositoryDB, +} +import dev.atedeg.mdm.stocking.dto.{ AvailableStockDTO, DesiredStockDTO } +import dev.atedeg.mdm.utils.monads.ServerAction + +object StockRequests: + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val desiredStockRequestEndpoint: PublicEndpoint[Unit, String, DesiredStockDTO, Any] = + endpoint.get + .in("stock" / "desired") + .out(jsonBody[DesiredStockDTO].description("The products missing from the stock")) + .errorOut(stringBody) + + val desiredStockRoute: HttpRoutes[IO] = + val handler: ServerAction[StockRepository, String, DesiredStockDTO] = handleDesiredStockRequest + Http4sServerInterpreter[IO]().toRoutes( + desiredStockRequestEndpoint.serverLogic(_ => handler.value.run(StockRepositoryDB("conn-string"))), + ) + + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val availableStockRequestEndpoint: PublicEndpoint[Unit, String, AvailableStockDTO, Any] = + endpoint.get + .in("stock") + .out(jsonBody[AvailableStockDTO].description("The current stock")) + .errorOut(stringBody) + + val availableStockRoute: HttpRoutes[IO] = + val handler: ServerAction[StockRepository, String, AvailableStockDTO] = handleProductsInStockRequest + Http4sServerInterpreter[IO]().toRoutes( + availableStockRequestEndpoint.serverLogic(_ => handler.value.run(StockRepositoryDB("conn-string"))), + ) + +object BatchesRequests: + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val approveBatchRequestEndpoint: PublicEndpoint[String, String, Unit, Any] = + endpoint.post + .in("batch" / "approve") + .in(stringBody.description("The id of the batch to approve")) + .errorOut(stringBody) + + val approveBatchRoute: HttpRoutes[IO] = + Http4sServerInterpreter[IO]().toRoutes( + approveBatchRequestEndpoint.serverLogic { request => + val handler: ServerAction[BatchesRepository, String, Unit] = approveBatchHandler(request) + handler.value.run(BatchesRepositoryDB("conn-string")) + }, + ) + + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val rejectBatchRequestEndpoint: PublicEndpoint[String, String, Unit, Any] = + endpoint.post + .in("batch" / "reject") + .in(stringBody.description("The id of the batch to reject")) + .errorOut(stringBody) + + val rejectBatchRoute: HttpRoutes[IO] = + Http4sServerInterpreter[IO]().toRoutes( + rejectBatchRequestEndpoint.serverLogic { request => + val handler: ServerAction[BatchesRepository, String, Unit] = rejectBatchHandler(request) + handler.value.run(BatchesRepositoryDB("conn-string")) + }, + ) From 12c769541c67d38798c537ffc5bdc8a5a90d26e0 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 18:51:33 +0200 Subject: [PATCH 276/329] test: add events check in client orders --- .../atedeg/mdm/clientorders/ActionsTest.scala | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala index 07d6734d..cdb05419 100644 --- a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala +++ b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala @@ -17,7 +17,8 @@ import org.scalatest.GivenWhenThen import org.scalatest.featurespec.AnyFeatureSpec import org.scalatest.matchers.should.Matchers -import dev.atedeg.mdm.clientorders.OutgoingEvent.OrderProcessed +import dev.atedeg.mdm.clientorders.OutgoingEvent.{ OrderProcessed, ProductPalletized } +import dev.atedeg.mdm.clientorders.dto.ProductPalletizedDTO import dev.atedeg.mdm.clientorders.utils.* import dev.atedeg.mdm.products.{ CheeseType, Grams, Product } import dev.atedeg.mdm.products.Product.* @@ -55,12 +56,14 @@ trait OrderMocks extends PriceListMock, CustomerMock, LocationMock: val incomingOrder: IncomingOrder = IncomingOrder(orderId, orderLines, customer, date, location) val inProgressCompleteOrder: InProgressOrder = - def palletizeAll[M[_]: Monad: CanRaise[PalletizationError]](inProgressOrder: InProgressOrder): M[InProgressOrder] = + def palletizeAll[M[_]: Monad: CanRaise[PalletizationError]: Emits[ProductPalletized]]( + inProgressOrder: InProgressOrder, + ): M[InProgressOrder] = palletizeProductForOrder(Quantity(100), Caciotta(500))(inProgressOrder) >>= palletizeProductForOrder(Quantity(100), Caciotta(1000)) val inProgressOrder = startPreparingOrder(priceOrder(priceList)(incomingOrder)) - val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = palletizeAll(inProgressOrder) + val palletizeAction: Action[PalletizationError, ProductPalletized, InProgressOrder] = palletizeAll(inProgressOrder) palletizeAction.execute._2.value val completedOrder: CompletedOrder = @@ -105,10 +108,11 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match And("a product that is not requested by the order") val productNotInOrder = Ricotta(350) When("the operator tries to palletize it") - val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = + val palletizeAction: Action[PalletizationError, ProductPalletized, InProgressOrder] = palletizeProductForOrder(Quantity(10), productNotInOrder)(inProgressOrder) Then("a ProductNotInOrder error should be raised") - val (_, result) = palletizeAction.execute + val (events, result) = palletizeAction.execute + events shouldBe empty result.left.value shouldBe ProductNotInOrder(productNotInOrder) } @@ -118,10 +122,11 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match And("a product requested by the order") val productInOrder = Caciotta(500) When("the operator tries to palletize it in a quantity greater than the required one") - val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = + val palletizeAction: Action[PalletizationError, ProductPalletized, InProgressOrder] = palletizeProductForOrder(Quantity(1000), productInOrder)(inProgressOrder) Then("a PalletizedMoreThanRequired error should be raised") - val (_, result) = palletizeAction.execute + val (events, result) = palletizeAction.execute + events shouldBe empty result.left.value shouldBe PalletizedMoreThanRequired(MissingQuantity(100)) } @@ -131,10 +136,11 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match And("a product requested by the order") val productInOrder = Caciotta(500) When("the operator palletizes it in the exact required quantity") - val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = + val palletizeAction: Action[PalletizationError, ProductPalletized, InProgressOrder] = palletizeProductForOrder(Quantity(100), productInOrder)(inProgressOrder) Then("the corresponding order line is marked as completed") - val (_, result) = palletizeAction.execute + val (events, result) = palletizeAction.execute + events should contain(ProductPalletized(productInOrder, Quantity(100))) result.value.orderLines.toList should contain allOf ( InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(1000), 10_000.euroCents), InProgressOrderLine.Complete(Quantity(100), Caciotta(500), 5000.euroCents), @@ -147,10 +153,11 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match And("a product requested by the order") val productInOrder = Caciotta(500) When("the operator palletizes it in a quantity lower than the required one") - val palletizeAction: Action[PalletizationError, Unit, InProgressOrder] = + val palletizeAction: Action[PalletizationError, ProductPalletized, InProgressOrder] = palletizeProductForOrder(Quantity(20), productInOrder)(inProgressOrder) Then("the corresponding order line is updated") - val (_, result) = palletizeAction.execute + val (events, result) = palletizeAction.execute + events should contain(ProductPalletized(productInOrder, Quantity(20))) result.value.orderLines.toList should contain allOf ( InProgressOrderLine.Incomplete(PalletizedQuantity(0), Quantity(100), Caciotta(1000), 10_000.euroCents), InProgressOrderLine.Incomplete(PalletizedQuantity(20), Quantity(100), Caciotta(500), 5000.euroCents), From 16dbeb40b05874ea0c3bb3fd2e84abfccf65192e Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 18:54:35 +0200 Subject: [PATCH 277/329] build: add docker to stocking bc --- build.sbt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.sbt b/build.sbt index ac181c9d..839df725 100644 --- a/build.sbt +++ b/build.sbt @@ -148,6 +148,11 @@ lazy val `products-shared-kernel` = project lazy val stocking = project .in(file("stocking")) .settings(commonSettings) + .settings( + Docker / packageName := packageName.value, + Docker / version := version.value, + dockerExposedPorts := Seq(8080), + ) .dependsOn(utils, `products-shared-kernel`) lazy val `client-orders` = project From 2cb0cb7fb175b903b5214c31ee280c0f3c3b8b1e Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Wed, 10 Aug 2022 09:06:10 +0200 Subject: [PATCH 278/329] build(deps): add log dependencies --- build.sbt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.sbt b/build.sbt index 839df725..94b5373c 100644 --- a/build.sbt +++ b/build.sbt @@ -70,6 +70,10 @@ val commonSettings = Seq( "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.0.3", "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.0.3", "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.0.3", + "org.typelevel" %% "log4cats-core" % "2.4.0", + "org.typelevel" %% "log4cats-slf4j" % "2.4.0", + "org.slf4j" % "slf4j-api" % "1.7.36", + "org.slf4j" % "slf4j-simple" % "1.7.36", ), dockerEnvVars := Map("PORT" -> "8080", "HOST" -> "0.0.0.0"), ) @@ -146,6 +150,7 @@ lazy val `products-shared-kernel` = project .dependsOn(utils) lazy val stocking = project + .enablePlugins(DockerPlugin, JavaAppPackaging) .in(file("stocking")) .settings(commonSettings) .settings( From 42712a2dc9842485a21ed84ac52d3577a964d322 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 19:10:01 +0200 Subject: [PATCH 279/329] chore: add logger --- .../src/main/scala/dev/atedeg/mdm/stocking/Main.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala index 87283b43..f5657fa9 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala @@ -7,6 +7,9 @@ import cats.syntax.all.* import org.http4s.HttpRoutes import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.Router +import org.typelevel.log4cats.* +import org.typelevel.log4cats.slf4j.Slf4jFactory +import org.typelevel.log4cats.slf4j.loggerFactoryforSync import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.tapir.swagger.bundle.SwaggerInterpreter @@ -23,10 +26,13 @@ object Main extends IOApp: private val routes: HttpRoutes[IO] = desiredStockRoute <+> availableStockRoute <+> approveBatchRoute <+> rejectBatchRoute <+> swaggerRoute + implicit val logging: LoggerFactory[IO] = Slf4jFactory[IO] + val logger: SelfAwareStructuredLogger[IO] = LoggerFactory[IO].getLogger + override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO] .bindHttp(Properties.envOrElse("PORT", "8080").toInt, Properties.envOrElse("HOST", "localhost")) .withHttpApp(Router("/" -> routes).orNotFound) .resource - .use(_ => IO.never[Unit]) + .use(_ => logger.info("Server started") >> IO.never[Unit]) .as(ExitCode.Success) From cc57cca49b8dde3d9ba8ff705e23dc09c1d52222 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 22:25:01 +0200 Subject: [PATCH 280/329] =?UTF-8?q?fix:=20use=20to=20string=20for=20local?= =?UTF-8?q?=20date=20time=20conversion=C3=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala index 6f856990..3d8c99d0 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala @@ -69,7 +69,7 @@ object DTO: Try(LocalDate.parse(dto, DateTimeFormatter.ISO_LOCAL_DATE)).toEither.leftMap(_ => s"Invalid date: $dto") given DTO[LocalDateTime, String] with - override def elemToDto(e: LocalDateTime): String = e.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + override def elemToDto(e: LocalDateTime): String = s"$e" override def dtoToElem(dto: String): Either[String, LocalDateTime] = Try(LocalDateTime.parse(dto, DateTimeFormatter.ISO_LOCAL_DATE_TIME)).toEither.leftMap(_ => s"Invalid date: $dto") From 484abce81ef883d0ed41812900c6401fe058549a Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 22:25:42 +0200 Subject: [PATCH 281/329] test: add stocking handlers tests --- .../mdm/stocking/api/HandlersTest.scala | 73 +++++++++++++++++++ .../mdm/stocking/{ => types}/Tests.scala | 18 ++--- 2 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala rename stocking/src/test/scala/dev/atedeg/mdm/stocking/{ => types}/Tests.scala (98%) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala new file mode 100644 index 00000000..dbcd3b1b --- /dev/null +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala @@ -0,0 +1,73 @@ +package dev.atedeg.mdm.stocking.api + +import java.time.{ LocalDate, LocalDateTime } +import java.time.format.DateTimeFormatter +import java.util.UUID +import scala.collection.mutable + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* +import org.scalatest.EitherValues.* +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.stocking.AvailableStock +import dev.atedeg.mdm.stocking.api.acl.ProductPalletizedDTO +import dev.atedeg.mdm.stocking.api.repositories.{ BatchesRepository, StockRepository } +import dev.atedeg.mdm.stocking.dto.* +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.monads.ServerAction + +trait Mocks: + val product: ProductDTO = ProductDTO("caciotta", 500) + @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) + var availableStock: AvailableStockDTO = AvailableStockDTO(List((product, 5))) + val desiredStock: DesiredStockDTO = DesiredStockDTO(List((product, 2))) + val stockRepository: StockRepository = new StockRepository: + override def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] = availableStock.pure + override def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] = + availableStock = updatedStock + ().pure + override def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] = desiredStock.pure + + @SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures")) + val agingBatches: mutable.ListBuffer[AgingBatchDTO] = mutable.ListBuffer() + val batchesRepository: BatchesRepository = new BatchesRepository: + override def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] = + agingBatches.addOne(agingBatch) + ().pure + + override def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]]( + id: String, + ): M[BatchReadyForQualityAssuranceDTO] = ??? + override def approveBatch[M[_]: Monad: LiftIO](passedBatch: QualityAssuredBatchPassedDTO): M[Unit] = ??? + override def rejectBatch[M[_]: Monad: LiftIO](failedBatch: QualityAssuredBatchFailedDTO): M[Unit] = ??? + +class HandlersTest extends AnyWordSpec, Matchers, Mocks: + "The `handleRemovalFromStock`" should { + "correctly update the available stock when a product has been palletized" in { + val productPalletized = ProductPalletizedDTO(product, 1) + val handler: ServerAction[StockRepository, String, Unit] = handleRemovalFromStock(productPalletized) + handler.unsafeExecute(stockRepository) + availableStock shouldBe AvailableStockDTO(List((product, 4))) + } + } + "The `handleDesiredStockRequest`" should { + "return the same value it reads from the DB" in { + val handler: ServerAction[StockRepository, String, DesiredStockDTO] = handleDesiredStockRequest + val res = handler.unsafeExecute(stockRepository) + res.value shouldBe desiredStock + } + } + + "The `handleNewBatch`" should { + "add a batch to the aging ones" in { + val newBatch = NewBatchDTO(s"${UUID.randomUUID}", "caciotta", s"${LocalDateTime.now}") + val handler: ServerAction[BatchesRepository, String, Unit] = handleNewBatch(newBatch) + handler.unsafeExecute(batchesRepository) + val agingBatchRes = AgingBatchDTO(newBatch.batchID, newBatch.cheeseType, newBatch.readyFrom) + agingBatches.toList should contain(agingBatchRes) + } + } diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala similarity index 98% rename from stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala rename to stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala index ff22624d..1106b2e4 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala @@ -1,19 +1,19 @@ -package dev.atedeg.mdm.stocking - -import java.util.UUID - -import cats.data.{ Writer, WriterT } -import org.scalatest.EitherValues.* -import org.scalatest.GivenWhenThen -import org.scalatest.featurespec.AnyFeatureSpec -import org.scalatest.matchers.should.Matchers +package dev.atedeg.mdm.stocking.types +import cats.data.{Writer, WriterT} import dev.atedeg.mdm.products.* import dev.atedeg.mdm.products.utils.* +import dev.atedeg.mdm.stocking.* import dev.atedeg.mdm.stocking.Error.* import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.monads.* +import org.scalatest.EitherValues.* +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +import java.util.UUID trait Mocks: val batchID: BatchID = BatchID(UUID.randomUUID()) From d3c427860a9fc12eb8210563306b380cdedb90a2 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Wed, 10 Aug 2022 22:25:58 +0200 Subject: [PATCH 282/329] style: scalafmt and scalafix --- .../atedeg/mdm/clientorders/dto/DTOs.scala | 2 +- .../scala/dev/atedeg/mdm/stocking/Main.scala | 76 ++++----- .../atedeg/mdm/stocking/api/Handlers.scala | 122 +++++++------- .../atedeg/mdm/stocking/api/acl/DTOs.scala | 18 +- .../stocking/api/endpoints/Endpoints.scala | 156 +++++++++--------- .../api/repositories/Repositories.scala | 60 +++---- .../dev/atedeg/mdm/stocking/dto/DTOs.scala | 6 +- .../mdm/stocking/api/HandlersTest.scala | 146 ++++++++-------- .../dev/atedeg/mdm/stocking/types/Tests.scala | 15 +- 9 files changed, 301 insertions(+), 300 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala index 3911a28a..ecaf0536 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -59,4 +59,4 @@ object OrderProcessedDTO: final case class ProductPalletizedDTO(product: ProductDTO, quantity: Int) object ProductPalletizedDTO: given DTO[ProductPalletized, ProductPalletizedDTO] = interCaseClassDTO - private given DTO[Quantity, Int] = caseClassDTO \ No newline at end of file + private given DTO[Quantity, Int] = caseClassDTO diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala index f5657fa9..372e309a 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Main.scala @@ -1,38 +1,38 @@ -package dev.atedeg.mdm.stocking - -import scala.util.Properties - -import cats.effect.{ ExitCode, IO, IOApp } -import cats.syntax.all.* -import org.http4s.HttpRoutes -import org.http4s.blaze.server.BlazeServerBuilder -import org.http4s.server.Router -import org.typelevel.log4cats.* -import org.typelevel.log4cats.slf4j.Slf4jFactory -import org.typelevel.log4cats.slf4j.loggerFactoryforSync -import sttp.tapir.server.http4s.Http4sServerInterpreter -import sttp.tapir.swagger.bundle.SwaggerInterpreter - -import dev.atedeg.mdm.stocking.api.endpoints.BatchesRequests.* -import dev.atedeg.mdm.stocking.api.endpoints.StockRequests.* - -object Main extends IOApp: - private val swaggerEndpoint = SwaggerInterpreter().fromEndpoints[IO]( - desiredStockRequestEndpoint :: availableStockRequestEndpoint :: approveBatchRequestEndpoint :: rejectBatchRequestEndpoint :: Nil, - "stocking", - Properties.envOrElse("VERSION", "v1-beta"), - ) - private val swaggerRoute = Http4sServerInterpreter[IO]().toRoutes(swaggerEndpoint) - private val routes: HttpRoutes[IO] = - desiredStockRoute <+> availableStockRoute <+> approveBatchRoute <+> rejectBatchRoute <+> swaggerRoute - - implicit val logging: LoggerFactory[IO] = Slf4jFactory[IO] - val logger: SelfAwareStructuredLogger[IO] = LoggerFactory[IO].getLogger - - override def run(args: List[String]): IO[ExitCode] = - BlazeServerBuilder[IO] - .bindHttp(Properties.envOrElse("PORT", "8080").toInt, Properties.envOrElse("HOST", "localhost")) - .withHttpApp(Router("/" -> routes).orNotFound) - .resource - .use(_ => logger.info("Server started") >> IO.never[Unit]) - .as(ExitCode.Success) +package dev.atedeg.mdm.stocking + +import scala.util.Properties + +import cats.effect.{ ExitCode, IO, IOApp } +import cats.syntax.all.* +import org.http4s.HttpRoutes +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router +import org.typelevel.log4cats.* +import org.typelevel.log4cats.slf4j.Slf4jFactory +import org.typelevel.log4cats.slf4j.loggerFactoryforSync +import sttp.tapir.server.http4s.Http4sServerInterpreter +import sttp.tapir.swagger.bundle.SwaggerInterpreter + +import dev.atedeg.mdm.stocking.api.endpoints.BatchesRequests.* +import dev.atedeg.mdm.stocking.api.endpoints.StockRequests.* + +object Main extends IOApp: + private val swaggerEndpoint = SwaggerInterpreter().fromEndpoints[IO]( + desiredStockRequestEndpoint :: availableStockRequestEndpoint :: approveBatchRequestEndpoint :: rejectBatchRequestEndpoint :: Nil, + "stocking", + Properties.envOrElse("VERSION", "v1-beta"), + ) + private val swaggerRoute = Http4sServerInterpreter[IO]().toRoutes(swaggerEndpoint) + private val routes: HttpRoutes[IO] = + desiredStockRoute <+> availableStockRoute <+> approveBatchRoute <+> rejectBatchRoute <+> swaggerRoute + + implicit val logging: LoggerFactory[IO] = Slf4jFactory[IO] + val logger: SelfAwareStructuredLogger[IO] = LoggerFactory[IO].getLogger + + override def run(args: List[String]): IO[ExitCode] = + BlazeServerBuilder[IO] + .bindHttp(Properties.envOrElse("PORT", "8080").toInt, Properties.envOrElse("HOST", "localhost")) + .withHttpApp(Router("/" -> routes).orNotFound) + .resource + .use(_ => logger.info("Server started") >> IO.never[Unit]) + .as(ExitCode.Success) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/Handlers.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/Handlers.scala index dd920c96..3fdba268 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/Handlers.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/Handlers.scala @@ -1,61 +1,61 @@ -package dev.atedeg.mdm.stocking.api - -import cats.Monad -import cats.effect.LiftIO -import cats.syntax.all.* - -import dev.atedeg.mdm.stocking.* -import dev.atedeg.mdm.stocking.Error.NotEnoughStock -import dev.atedeg.mdm.stocking.IncomingEvent.ProductRemovedFromStock -import dev.atedeg.mdm.stocking.api.acl.ProductPalletizedDTO -import dev.atedeg.mdm.stocking.api.acl.toProductRemovedFromStockDTO -import dev.atedeg.mdm.stocking.api.repositories.{ BatchesRepository, StockRepository } -import dev.atedeg.mdm.stocking.dto.* -import dev.atedeg.mdm.stocking.dto.AvailableStockDTO.given -import dev.atedeg.mdm.stocking.dto.DesiredStockDTO.given -import dev.atedeg.mdm.utils.monads.* -import dev.atedeg.mdm.utils.serialization.DTOOps.* - -def handleRemovalFromStock[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]]( - productPalletized: ProductPalletizedDTO, -): M[Unit] = - for - repository <- readState - removed <- validate(productPalletized.toProductRemovedFromStockDTO) - stock <- repository.readStock >>= validate - action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(stock)(removed.product, removed.quantity) - (_, res) = action.execute - updatedStock <- res.leftMap(e => s"Not enough in stock: $e").getOrRaise - _ <- repository.writeStock(updatedStock.toDTO[AvailableStockDTO]) - yield () - -def handleNewBatch[M[_]: Monad: LiftIO: CanRead[BatchesRepository]: CanRaise[String]]( - newBatchDTO: NewBatchDTO, -): M[Unit] = - for - newBatch <- validate(newBatchDTO) - batch: Batch.Aging = Batch.Aging(newBatch.batchID, newBatch.cheeseType, newBatch.readyFrom) - _ <- readState >>= (_.addNewBatch(batch.toDTO[AgingBatchDTO])) - yield () - -def handleDesiredStockRequest[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]]: M[DesiredStockDTO] = - (readState >>= (_.readDesiredStock) >>= validate).map(_.toDTO) - -def handleProductsInStockRequest[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]] - : M[AvailableStockDTO] = (readState >>= (_.readStock) >>= validate).map(_.toDTO) - -def approveBatchHandler[M[_]: Monad: LiftIO: CanRead[BatchesRepository]: CanRaise[String]](batchID: String): M[Unit] = - for - repository <- readState - batchReady <- repository.readReadyForQA(batchID) >>= validate - approvedBatch = approveBatch(batchReady) - _ <- repository.approveBatch(approvedBatch.toDTO) - yield () - -def rejectBatchHandler[M[_]: Monad: LiftIO: CanRead[BatchesRepository]: CanRaise[String]](batchID: String): M[Unit] = - for - repository <- readState - batchReady <- repository.readReadyForQA(batchID) >>= validate - rejectedBatch = rejectBatch(batchReady) - _ <- repository.rejectBatch(rejectedBatch.toDTO) - yield () \ No newline at end of file +package dev.atedeg.mdm.stocking.api + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* + +import dev.atedeg.mdm.stocking.* +import dev.atedeg.mdm.stocking.Error.NotEnoughStock +import dev.atedeg.mdm.stocking.IncomingEvent.ProductRemovedFromStock +import dev.atedeg.mdm.stocking.api.acl.ProductPalletizedDTO +import dev.atedeg.mdm.stocking.api.acl.toProductRemovedFromStockDTO +import dev.atedeg.mdm.stocking.api.repositories.{ BatchesRepository, StockRepository } +import dev.atedeg.mdm.stocking.dto.* +import dev.atedeg.mdm.stocking.dto.AvailableStockDTO.given +import dev.atedeg.mdm.stocking.dto.DesiredStockDTO.given +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +def handleRemovalFromStock[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]]( + productPalletized: ProductPalletizedDTO, +): M[Unit] = + for + repository <- readState + removed <- validate(productPalletized.toProductRemovedFromStockDTO) + stock <- repository.readStock >>= validate + action: Action[NotEnoughStock, Unit, AvailableStock] = removeFromStock(stock)(removed.product, removed.quantity) + (_, res) = action.execute + updatedStock <- res.leftMap(e => s"Not enough in stock: $e").getOrRaise + _ <- repository.writeStock(updatedStock.toDTO[AvailableStockDTO]) + yield () + +def handleNewBatch[M[_]: Monad: LiftIO: CanRead[BatchesRepository]: CanRaise[String]]( + newBatchDTO: NewBatchDTO, +): M[Unit] = + for + newBatch <- validate(newBatchDTO) + batch: Batch.Aging = Batch.Aging(newBatch.batchID, newBatch.cheeseType, newBatch.readyFrom) + _ <- readState >>= (_.addNewBatch(batch.toDTO[AgingBatchDTO])) + yield () + +def handleDesiredStockRequest[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]]: M[DesiredStockDTO] = + (readState >>= (_.readDesiredStock) >>= validate).map(_.toDTO) + +def handleProductsInStockRequest[M[_]: Monad: LiftIO: CanRead[StockRepository]: CanRaise[String]] + : M[AvailableStockDTO] = (readState >>= (_.readStock) >>= validate).map(_.toDTO) + +def approveBatchHandler[M[_]: Monad: LiftIO: CanRead[BatchesRepository]: CanRaise[String]](batchID: String): M[Unit] = + for + repository <- readState + batchReady <- repository.readReadyForQA(batchID) >>= validate + approvedBatch = approveBatch(batchReady) + _ <- repository.approveBatch(approvedBatch.toDTO) + yield () + +def rejectBatchHandler[M[_]: Monad: LiftIO: CanRead[BatchesRepository]: CanRaise[String]](batchID: String): M[Unit] = + for + repository <- readState + batchReady <- repository.readReadyForQA(batchID) >>= validate + rejectedBatch = rejectBatch(batchReady) + _ <- repository.rejectBatch(rejectedBatch.toDTO) + yield () diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/acl/DTOs.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/acl/DTOs.scala index a83ffaf0..b957fab5 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/acl/DTOs.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/acl/DTOs.scala @@ -1,9 +1,9 @@ -package dev.atedeg.mdm.stocking.api.acl - -import dev.atedeg.mdm.products.dto.ProductDTO -import dev.atedeg.mdm.stocking.dto.ProductRemovedFromStockDTO - -final case class ProductPalletizedDTO(product: ProductDTO, quantity: Int) -extension (ppDTO: ProductPalletizedDTO) - def toProductRemovedFromStockDTO: ProductRemovedFromStockDTO = - ProductRemovedFromStockDTO(ppDTO.quantity, ppDTO.product) +package dev.atedeg.mdm.stocking.api.acl + +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.stocking.dto.ProductRemovedFromStockDTO + +final case class ProductPalletizedDTO(product: ProductDTO, quantity: Int) +extension (ppDTO: ProductPalletizedDTO) + def toProductRemovedFromStockDTO: ProductRemovedFromStockDTO = + ProductRemovedFromStockDTO(ppDTO.quantity, ppDTO.product) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/endpoints/Endpoints.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/endpoints/Endpoints.scala index 50482a9a..815c5b0b 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/endpoints/Endpoints.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/endpoints/Endpoints.scala @@ -1,78 +1,78 @@ -package dev.atedeg.mdm.stocking.api.endpoints - -import cats.effect.IO -import io.circe.generic.auto.* -import org.http4s.HttpRoutes -import sttp.tapir.* -import sttp.tapir.PublicEndpoint -import sttp.tapir.generic.auto.* -import sttp.tapir.json.circe.jsonBody -import sttp.tapir.server.http4s.Http4sServerInterpreter - -import dev.atedeg.mdm.stocking.api.* -import dev.atedeg.mdm.stocking.api.repositories.{ - BatchesRepository, - BatchesRepositoryDB, - StockRepository, - StockRepositoryDB, -} -import dev.atedeg.mdm.stocking.dto.{ AvailableStockDTO, DesiredStockDTO } -import dev.atedeg.mdm.utils.monads.ServerAction - -object StockRequests: - @SuppressWarnings(Array("org.wartremover.warts.Any")) - val desiredStockRequestEndpoint: PublicEndpoint[Unit, String, DesiredStockDTO, Any] = - endpoint.get - .in("stock" / "desired") - .out(jsonBody[DesiredStockDTO].description("The products missing from the stock")) - .errorOut(stringBody) - - val desiredStockRoute: HttpRoutes[IO] = - val handler: ServerAction[StockRepository, String, DesiredStockDTO] = handleDesiredStockRequest - Http4sServerInterpreter[IO]().toRoutes( - desiredStockRequestEndpoint.serverLogic(_ => handler.value.run(StockRepositoryDB("conn-string"))), - ) - - @SuppressWarnings(Array("org.wartremover.warts.Any")) - val availableStockRequestEndpoint: PublicEndpoint[Unit, String, AvailableStockDTO, Any] = - endpoint.get - .in("stock") - .out(jsonBody[AvailableStockDTO].description("The current stock")) - .errorOut(stringBody) - - val availableStockRoute: HttpRoutes[IO] = - val handler: ServerAction[StockRepository, String, AvailableStockDTO] = handleProductsInStockRequest - Http4sServerInterpreter[IO]().toRoutes( - availableStockRequestEndpoint.serverLogic(_ => handler.value.run(StockRepositoryDB("conn-string"))), - ) - -object BatchesRequests: - @SuppressWarnings(Array("org.wartremover.warts.Any")) - val approveBatchRequestEndpoint: PublicEndpoint[String, String, Unit, Any] = - endpoint.post - .in("batch" / "approve") - .in(stringBody.description("The id of the batch to approve")) - .errorOut(stringBody) - - val approveBatchRoute: HttpRoutes[IO] = - Http4sServerInterpreter[IO]().toRoutes( - approveBatchRequestEndpoint.serverLogic { request => - val handler: ServerAction[BatchesRepository, String, Unit] = approveBatchHandler(request) - handler.value.run(BatchesRepositoryDB("conn-string")) - }, - ) - - @SuppressWarnings(Array("org.wartremover.warts.Any")) - val rejectBatchRequestEndpoint: PublicEndpoint[String, String, Unit, Any] = - endpoint.post - .in("batch" / "reject") - .in(stringBody.description("The id of the batch to reject")) - .errorOut(stringBody) - - val rejectBatchRoute: HttpRoutes[IO] = - Http4sServerInterpreter[IO]().toRoutes( - rejectBatchRequestEndpoint.serverLogic { request => - val handler: ServerAction[BatchesRepository, String, Unit] = rejectBatchHandler(request) - handler.value.run(BatchesRepositoryDB("conn-string")) - }, - ) +package dev.atedeg.mdm.stocking.api.endpoints + +import cats.effect.IO +import io.circe.generic.auto.* +import org.http4s.HttpRoutes +import sttp.tapir.* +import sttp.tapir.PublicEndpoint +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.jsonBody +import sttp.tapir.server.http4s.Http4sServerInterpreter + +import dev.atedeg.mdm.stocking.api.* +import dev.atedeg.mdm.stocking.api.repositories.{ + BatchesRepository, + BatchesRepositoryDB, + StockRepository, + StockRepositoryDB, +} +import dev.atedeg.mdm.stocking.dto.{ AvailableStockDTO, DesiredStockDTO } +import dev.atedeg.mdm.utils.monads.ServerAction + +object StockRequests: + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val desiredStockRequestEndpoint: PublicEndpoint[Unit, String, DesiredStockDTO, Any] = + endpoint.get + .in("stock" / "desired") + .out(jsonBody[DesiredStockDTO].description("The products missing from the stock")) + .errorOut(stringBody) + + val desiredStockRoute: HttpRoutes[IO] = + val handler: ServerAction[StockRepository, String, DesiredStockDTO] = handleDesiredStockRequest + Http4sServerInterpreter[IO]().toRoutes( + desiredStockRequestEndpoint.serverLogic(_ => handler.value.run(StockRepositoryDB("conn-string"))), + ) + + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val availableStockRequestEndpoint: PublicEndpoint[Unit, String, AvailableStockDTO, Any] = + endpoint.get + .in("stock") + .out(jsonBody[AvailableStockDTO].description("The current stock")) + .errorOut(stringBody) + + val availableStockRoute: HttpRoutes[IO] = + val handler: ServerAction[StockRepository, String, AvailableStockDTO] = handleProductsInStockRequest + Http4sServerInterpreter[IO]().toRoutes( + availableStockRequestEndpoint.serverLogic(_ => handler.value.run(StockRepositoryDB("conn-string"))), + ) + +object BatchesRequests: + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val approveBatchRequestEndpoint: PublicEndpoint[String, String, Unit, Any] = + endpoint.post + .in("batch" / "approve") + .in(stringBody.description("The id of the batch to approve")) + .errorOut(stringBody) + + val approveBatchRoute: HttpRoutes[IO] = + Http4sServerInterpreter[IO]().toRoutes( + approveBatchRequestEndpoint.serverLogic { request => + val handler: ServerAction[BatchesRepository, String, Unit] = approveBatchHandler(request) + handler.value.run(BatchesRepositoryDB("conn-string")) + }, + ) + + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val rejectBatchRequestEndpoint: PublicEndpoint[String, String, Unit, Any] = + endpoint.post + .in("batch" / "reject") + .in(stringBody.description("The id of the batch to reject")) + .errorOut(stringBody) + + val rejectBatchRoute: HttpRoutes[IO] = + Http4sServerInterpreter[IO]().toRoutes( + rejectBatchRequestEndpoint.serverLogic { request => + val handler: ServerAction[BatchesRepository, String, Unit] = rejectBatchHandler(request) + handler.value.run(BatchesRepositoryDB("conn-string")) + }, + ) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/repositories/Repositories.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/repositories/Repositories.scala index 1fd75818..ca76511a 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/repositories/Repositories.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/api/repositories/Repositories.scala @@ -1,30 +1,30 @@ -package dev.atedeg.mdm.stocking.api.repositories - -import cats.Monad -import cats.effect.LiftIO - -import dev.atedeg.mdm.stocking.QualityAssuredBatch -import dev.atedeg.mdm.stocking.dto.* -import dev.atedeg.mdm.utils.monads.CanRaise - -trait StockRepository: - def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] - def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] - def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] - -trait BatchesRepository: - def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] - def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]](id: String): M[BatchReadyForQualityAssuranceDTO] - def approveBatch[M[_]: Monad: LiftIO](passedBatch: QualityAssuredBatchPassedDTO): M[Unit] - def rejectBatch[M[_]: Monad: LiftIO](failedBatch: QualityAssuredBatchFailedDTO): M[Unit] - -final case class StockRepositoryDB(connectionString: String) extends StockRepository: - def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] = ??? - def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] = ??? - def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] = ??? - -final case class BatchesRepositoryDB(connectionString: String) extends BatchesRepository: - def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] = ??? - def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]](id: String): M[BatchReadyForQualityAssuranceDTO] = ??? - def approveBatch[M[_]: Monad: LiftIO](passedBatch: QualityAssuredBatchPassedDTO): M[Unit] = ??? - def rejectBatch[M[_]: Monad: LiftIO](failedBatch: QualityAssuredBatchFailedDTO): M[Unit] = ??? +package dev.atedeg.mdm.stocking.api.repositories + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.stocking.QualityAssuredBatch +import dev.atedeg.mdm.stocking.dto.* +import dev.atedeg.mdm.utils.monads.CanRaise + +trait StockRepository: + def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] + def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] + def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] + +trait BatchesRepository: + def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] + def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]](id: String): M[BatchReadyForQualityAssuranceDTO] + def approveBatch[M[_]: Monad: LiftIO](passedBatch: QualityAssuredBatchPassedDTO): M[Unit] + def rejectBatch[M[_]: Monad: LiftIO](failedBatch: QualityAssuredBatchFailedDTO): M[Unit] + +final case class StockRepositoryDB(connectionString: String) extends StockRepository: + def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] = ??? + def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] = ??? + def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] = ??? + +final case class BatchesRepositoryDB(connectionString: String) extends BatchesRepository: + def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] = ??? + def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]](id: String): M[BatchReadyForQualityAssuranceDTO] = ??? + def approveBatch[M[_]: Monad: LiftIO](passedBatch: QualityAssuredBatchPassedDTO): M[Unit] = ??? + def rejectBatch[M[_]: Monad: LiftIO](failedBatch: QualityAssuredBatchFailedDTO): M[Unit] = ??? diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala index d4632369..f28d14ec 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala @@ -31,12 +31,12 @@ final case class NewBatchDTO(batchID: String, cheeseType: String, readyFrom: Str object NewBatchDTO: given DTO[NewBatch, NewBatchDTO] = interCaseClassDTO -final case class AvailableStockDTO(as: List[(ProductDTO, Int)]) +final case class AvailableStockDTO(availableStock: List[(ProductDTO, Int)]) object AvailableStockDTO: given DTO[AvailableStock, AvailableStockDTO] = interCaseClassDTO private given DTO[AvailableQuantity, Int] = caseClassDTO -final case class DesiredStockDTO(ds: List[(ProductDTO, Int)]) +final case class DesiredStockDTO(desiredStock: List[(ProductDTO, Int)]) object DesiredStockDTO: given DTO[DesiredStock, DesiredStockDTO] = interCaseClassDTO private given DTO[DesiredQuantity, Int] = caseClassDTO @@ -51,7 +51,7 @@ object QualityAssuredBatchPassedDTO: final case class QualityAssuredBatchFailedDTO(id: String, cheeseType: String) object QualityAssuredBatchFailedDTO: - given DTO[QualityAssuredBatch.Failed, QualityAssuredBatchFailedDTO] = interCaseClassDTO + given DTO[QualityAssuredBatch.Failed, QualityAssuredBatchFailedDTO] = interCaseClassDTO final case class BatchReadyForQualityAssuranceDTO(id: String, cheeseType: String) object BatchReadyForQualityAssuranceDTO: diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala index dbcd3b1b..54783337 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala @@ -1,73 +1,73 @@ -package dev.atedeg.mdm.stocking.api - -import java.time.{ LocalDate, LocalDateTime } -import java.time.format.DateTimeFormatter -import java.util.UUID -import scala.collection.mutable - -import cats.Monad -import cats.effect.LiftIO -import cats.syntax.all.* -import org.scalatest.EitherValues.* -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import dev.atedeg.mdm.products.dto.ProductDTO -import dev.atedeg.mdm.stocking.AvailableStock -import dev.atedeg.mdm.stocking.api.acl.ProductPalletizedDTO -import dev.atedeg.mdm.stocking.api.repositories.{ BatchesRepository, StockRepository } -import dev.atedeg.mdm.stocking.dto.* -import dev.atedeg.mdm.utils.monads.* -import dev.atedeg.mdm.utils.monads.ServerAction - -trait Mocks: - val product: ProductDTO = ProductDTO("caciotta", 500) - @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) - var availableStock: AvailableStockDTO = AvailableStockDTO(List((product, 5))) - val desiredStock: DesiredStockDTO = DesiredStockDTO(List((product, 2))) - val stockRepository: StockRepository = new StockRepository: - override def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] = availableStock.pure - override def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] = - availableStock = updatedStock - ().pure - override def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] = desiredStock.pure - - @SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures")) - val agingBatches: mutable.ListBuffer[AgingBatchDTO] = mutable.ListBuffer() - val batchesRepository: BatchesRepository = new BatchesRepository: - override def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] = - agingBatches.addOne(agingBatch) - ().pure - - override def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]]( - id: String, - ): M[BatchReadyForQualityAssuranceDTO] = ??? - override def approveBatch[M[_]: Monad: LiftIO](passedBatch: QualityAssuredBatchPassedDTO): M[Unit] = ??? - override def rejectBatch[M[_]: Monad: LiftIO](failedBatch: QualityAssuredBatchFailedDTO): M[Unit] = ??? - -class HandlersTest extends AnyWordSpec, Matchers, Mocks: - "The `handleRemovalFromStock`" should { - "correctly update the available stock when a product has been palletized" in { - val productPalletized = ProductPalletizedDTO(product, 1) - val handler: ServerAction[StockRepository, String, Unit] = handleRemovalFromStock(productPalletized) - handler.unsafeExecute(stockRepository) - availableStock shouldBe AvailableStockDTO(List((product, 4))) - } - } - "The `handleDesiredStockRequest`" should { - "return the same value it reads from the DB" in { - val handler: ServerAction[StockRepository, String, DesiredStockDTO] = handleDesiredStockRequest - val res = handler.unsafeExecute(stockRepository) - res.value shouldBe desiredStock - } - } - - "The `handleNewBatch`" should { - "add a batch to the aging ones" in { - val newBatch = NewBatchDTO(s"${UUID.randomUUID}", "caciotta", s"${LocalDateTime.now}") - val handler: ServerAction[BatchesRepository, String, Unit] = handleNewBatch(newBatch) - handler.unsafeExecute(batchesRepository) - val agingBatchRes = AgingBatchDTO(newBatch.batchID, newBatch.cheeseType, newBatch.readyFrom) - agingBatches.toList should contain(agingBatchRes) - } - } +package dev.atedeg.mdm.stocking.api + +import java.time.{ LocalDate, LocalDateTime } +import java.time.format.DateTimeFormatter +import java.util.UUID +import scala.collection.mutable + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* +import org.scalatest.EitherValues.* +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.stocking.AvailableStock +import dev.atedeg.mdm.stocking.api.acl.ProductPalletizedDTO +import dev.atedeg.mdm.stocking.api.repositories.{ BatchesRepository, StockRepository } +import dev.atedeg.mdm.stocking.dto.* +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.monads.ServerAction + +trait Mocks: + val product: ProductDTO = ProductDTO("caciotta", 500) + @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) + var availableStock: AvailableStockDTO = AvailableStockDTO(List((product, 5))) + val desiredStock: DesiredStockDTO = DesiredStockDTO(List((product, 2))) + val stockRepository: StockRepository = new StockRepository: + override def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] = availableStock.pure + override def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] = + availableStock = updatedStock + ().pure + override def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] = desiredStock.pure + + @SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures")) + val agingBatches: mutable.ListBuffer[AgingBatchDTO] = mutable.ListBuffer() + val batchesRepository: BatchesRepository = new BatchesRepository: + override def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] = + agingBatches.addOne(agingBatch) + ().pure + + override def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]]( + id: String, + ): M[BatchReadyForQualityAssuranceDTO] = ??? + override def approveBatch[M[_]: Monad: LiftIO](passedBatch: QualityAssuredBatchPassedDTO): M[Unit] = ??? + override def rejectBatch[M[_]: Monad: LiftIO](failedBatch: QualityAssuredBatchFailedDTO): M[Unit] = ??? + +class HandlersTest extends AnyWordSpec, Matchers, Mocks: + "The `handleRemovalFromStock`" should { + "correctly update the available stock when a product has been palletized" in { + val productPalletized = ProductPalletizedDTO(product, 1) + val handler: ServerAction[StockRepository, String, Unit] = handleRemovalFromStock(productPalletized) + handler.unsafeExecute(stockRepository) + availableStock shouldBe AvailableStockDTO(List((product, 4))) + } + } + "The `handleDesiredStockRequest`" should { + "return the same value it reads from the DB" in { + val handler: ServerAction[StockRepository, String, DesiredStockDTO] = handleDesiredStockRequest + val res = handler.unsafeExecute(stockRepository) + res.value shouldBe desiredStock + } + } + + "The `handleNewBatch`" should { + "add a batch to the aging ones" in { + val newBatch = NewBatchDTO(s"${UUID.randomUUID}", "caciotta", s"${LocalDateTime.now}") + val handler: ServerAction[BatchesRepository, String, Unit] = handleNewBatch(newBatch) + handler.unsafeExecute(batchesRepository) + val agingBatchRes = AgingBatchDTO(newBatch.batchID, newBatch.cheeseType, newBatch.readyFrom) + agingBatches.toList should contain(agingBatchRes) + } + } diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala index 1106b2e4..558f5c7e 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala @@ -1,6 +1,13 @@ package dev.atedeg.mdm.stocking.types -import cats.data.{Writer, WriterT} +import java.util.UUID + +import cats.data.{ Writer, WriterT } +import org.scalatest.EitherValues.* +import org.scalatest.GivenWhenThen +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + import dev.atedeg.mdm.products.* import dev.atedeg.mdm.products.utils.* import dev.atedeg.mdm.stocking.* @@ -8,12 +15,6 @@ import dev.atedeg.mdm.stocking.Error.* import dev.atedeg.mdm.stocking.OutgoingEvent.* import dev.atedeg.mdm.utils.* import dev.atedeg.mdm.utils.monads.* -import org.scalatest.EitherValues.* -import org.scalatest.GivenWhenThen -import org.scalatest.featurespec.AnyFeatureSpec -import org.scalatest.matchers.should.Matchers - -import java.util.UUID trait Mocks: val batchID: BatchID = BatchID(UUID.randomUUID()) From 330bd0211bfeb1d6ebe51bccddfa727b6db4683a Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 11:13:56 +0200 Subject: [PATCH 283/329] fix: use format instead of to string --- .../src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala index 3d8c99d0..6f856990 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala @@ -69,7 +69,7 @@ object DTO: Try(LocalDate.parse(dto, DateTimeFormatter.ISO_LOCAL_DATE)).toEither.leftMap(_ => s"Invalid date: $dto") given DTO[LocalDateTime, String] with - override def elemToDto(e: LocalDateTime): String = s"$e" + override def elemToDto(e: LocalDateTime): String = e.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) override def dtoToElem(dto: String): Either[String, LocalDateTime] = Try(LocalDateTime.parse(dto, DateTimeFormatter.ISO_LOCAL_DATE_TIME)).toEither.leftMap(_ => s"Invalid date: $dto") From f5d9faa220a63cbeaa21cab61fda38a0d9d98271 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 11:14:34 +0200 Subject: [PATCH 284/329] docs: update ubidoc according to new elements --- .ubidoc.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index 55c77daf..ea2a93f1 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -1,8 +1,8 @@ tables: - name: "stocking-ul" rows: - - type: "AvailableStock" - - type: "DesiredStock" + - class: "AvailableStock" + - class: "DesiredStock" - class: "AvailableQuantity" - class: "DesiredQuantity" - class: "MissingQuantity" @@ -14,6 +14,7 @@ tables: - name: "stocking-outgoing" rows: - case: "OutgoingEvent.ProductStocked" + - case: "OutgoingEvent.ProductPalletized" - name: "stocking-incoming" rows: - case: "IncomingEvent.BatchReadyForQualityAssurance" From 9a04f5008d909c7780a6a3f3ad2b62c92a38bc6e Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 11:28:57 +0200 Subject: [PATCH 285/329] test: add test for handle products in stock --- .../mdm/stocking/api/HandlersTest.scala | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala index 54783337..0f4fb618 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala @@ -19,6 +19,7 @@ import dev.atedeg.mdm.stocking.api.repositories.{ BatchesRepository, StockReposi import dev.atedeg.mdm.stocking.dto.* import dev.atedeg.mdm.utils.monads.* import dev.atedeg.mdm.utils.monads.ServerAction +import dev.atedeg.mdm.utils.serialization.DTOOps.* trait Mocks: val product: ProductDTO = ProductDTO("caciotta", 500) @@ -32,11 +33,11 @@ trait Mocks: ().pure override def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] = desiredStock.pure - @SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures")) - val agingBatches: mutable.ListBuffer[AgingBatchDTO] = mutable.ListBuffer() + @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) + var agingBatches: List[AgingBatchDTO] = List[AgingBatchDTO]() val batchesRepository: BatchesRepository = new BatchesRepository: override def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] = - agingBatches.addOne(agingBatch) + agingBatches = agingBatch :: agingBatches ().pure override def readReadyForQA[M[_]: Monad: LiftIO: CanRaise[String]]( @@ -64,10 +65,20 @@ class HandlersTest extends AnyWordSpec, Matchers, Mocks: "The `handleNewBatch`" should { "add a batch to the aging ones" in { - val newBatch = NewBatchDTO(s"${UUID.randomUUID}", "caciotta", s"${LocalDateTime.now}") + val id = UUID.randomUUID.toDTO[String] + val date = LocalDateTime.now.toDTO[String] + val newBatch = NewBatchDTO(id, "caciotta", date) val handler: ServerAction[BatchesRepository, String, Unit] = handleNewBatch(newBatch) handler.unsafeExecute(batchesRepository) val agingBatchRes = AgingBatchDTO(newBatch.batchID, newBatch.cheeseType, newBatch.readyFrom) - agingBatches.toList should contain(agingBatchRes) + agingBatches should contain(agingBatchRes) + } + } + + "The `handleProductsInStockRequest`" should { + "return the same value it reads from the DB" in { + val handler: ServerAction[StockRepository, String, AvailableStockDTO] = handleProductsInStockRequest + val res = handler.unsafeExecute(stockRepository) + res.value shouldBe availableStock } } From e70709ed036a10392f5e901a3804715e034db76f Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 11:31:14 +0200 Subject: [PATCH 286/329] refactor: use @giacomocavalieri for comprehension suggestion --- .../main/scala/dev/atedeg/mdm/stocking/Actions.scala | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala index 4a859c5b..99f24a64 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/Actions.scala @@ -31,13 +31,11 @@ def getMissingCountFromProductStock( def removeFromStock[M[_]: Monad: CanRaise[NotEnoughStock]]( stock: AvailableStock, )(product: Product, quantity: Quantity): M[AvailableStock] = - (stock.availableStock(product).n > quantity.n) - .otherwiseRaise(NotEnoughStock(product, quantity, stock.availableStock(product)): NotEnoughStock) - .thenReturn( - AvailableStock( - stock.availableStock + (product -> AvailableQuantity(stock.availableStock(product).n - quantity.n)), - ), - ) + for + _ <- (stock.availableStock(product).n > quantity.n) + .otherwiseRaise(NotEnoughStock(product, quantity, stock.availableStock(product)): NotEnoughStock) + updatedStock = stock.availableStock.updatedWith(product)(_.map(q => AvailableQuantity(q.n - quantity.n))) + yield AvailableStock(updatedStock) /** * Approves a batch after quality assurance. From 38d0c4c327b7a2ff53f4886be11f43f1e7cfa5a1 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 12:55:32 +0200 Subject: [PATCH 287/329] refactor: adress review comment --- .../test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala index 0f4fb618..11d499af 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala @@ -34,7 +34,7 @@ trait Mocks: override def readDesiredStock[M[_]: Monad: LiftIO]: M[DesiredStockDTO] = desiredStock.pure @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) - var agingBatches: List[AgingBatchDTO] = List[AgingBatchDTO]() + var agingBatches: List[AgingBatchDTO] = Nil val batchesRepository: BatchesRepository = new BatchesRepository: override def addNewBatch[M[_]: Monad: LiftIO](agingBatch: AgingBatchDTO): M[Unit] = agingBatches = agingBatch :: agingBatches From e25d48f8f95cf2d9ce689c3aaa86fa6508ef3ce8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 11 Aug 2022 11:26:15 +0000 Subject: [PATCH 288/329] chore(release): 1.0.0-beta.14 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [1.0.0-beta.14](https://github.com/atedeg/mdm/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2022-08-11) ### Bug Fixes * use format instead of to string ([330bd02](https://github.com/atedeg/mdm/commit/330bd0211bfeb1d6ebe51bccddfa727b6db4683a)) * use to string for local date time conversionù ([cc57cca](https://github.com/atedeg/mdm/commit/cc57cca49b8dde3d9ba8ff705e23dc09c1d52222)) ### Features * add acl dtos ([d85d6c2](https://github.com/atedeg/mdm/commit/d85d6c2eb4ecbd1df20ec3b9f941ee1283a6d9a6)) * add endpoints ([938dae6](https://github.com/atedeg/mdm/commit/938dae62581a74aca245b689f074ef3cda9e4517)) * add handlers ([ed8a27d](https://github.com/atedeg/mdm/commit/ed8a27d634df701d5a51125b2823e2b75fae28d1)) * add product palletized event to client orders bc ([4b8474c](https://github.com/atedeg/mdm/commit/4b8474c797f54bc8e7ddb7b25df37fd82844cfea)) * add repositories ([344f266](https://github.com/atedeg/mdm/commit/344f2666adeefc268b16d7ab6d98f08ce94bed68)) * available stock and desired stock are now case classes ([44ec364](https://github.com/atedeg/mdm/commit/44ec36436e2cc05b3ec604b6413529bea9c0b62b)) --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b01567e6..04c913c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# [1.0.0-beta.14](https://github.com/atedeg/mdm/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2022-08-11) + + +### Bug Fixes + +* use format instead of to string ([330bd02](https://github.com/atedeg/mdm/commit/330bd0211bfeb1d6ebe51bccddfa727b6db4683a)) +* use to string for local date time conversionù ([cc57cca](https://github.com/atedeg/mdm/commit/cc57cca49b8dde3d9ba8ff705e23dc09c1d52222)) + + +### Features + +* add acl dtos ([d85d6c2](https://github.com/atedeg/mdm/commit/d85d6c2eb4ecbd1df20ec3b9f941ee1283a6d9a6)) +* add endpoints ([938dae6](https://github.com/atedeg/mdm/commit/938dae62581a74aca245b689f074ef3cda9e4517)) +* add handlers ([ed8a27d](https://github.com/atedeg/mdm/commit/ed8a27d634df701d5a51125b2823e2b75fae28d1)) +* add product palletized event to client orders bc ([4b8474c](https://github.com/atedeg/mdm/commit/4b8474c797f54bc8e7ddb7b25df37fd82844cfea)) +* add repositories ([344f266](https://github.com/atedeg/mdm/commit/344f2666adeefc268b16d7ab6d98f08ce94bed68)) +* available stock and desired stock are now case classes ([44ec364](https://github.com/atedeg/mdm/commit/44ec36436e2cc05b3ec604b6413529bea9c0b62b)) + # [1.0.0-beta.13](https://github.com/atedeg/mdm/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2022-08-09) From 28c70a0b9e5a6cf8b60843ccfd166d7791308e47 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Thu, 11 Aug 2022 17:27:19 +0200 Subject: [PATCH 289/329] feat!: rename api endpoint to 'milk' --- .../dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala index ac34ab09..1e37d298 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/api/endpoints/Endpoints.scala @@ -20,7 +20,8 @@ object RemainingQuintalsOfMilkEndpoint: @SuppressWarnings(Array("org.wartremover.warts.Any")) val remainingQuintalsOfMilkEndpoint: PublicEndpoint[Unit, String, RemainingMilkDTO, Any] = endpoint.get - .in("remaining-quintals-of-milk") + .in("milk") + .description("Get the remaining quintals of milk") .out(jsonBody[RemainingMilkDTO].description("The quintals of milk remaining in stock")) .errorOut(stringBody) From 8f3603a4b8e92926668f5930fd894a855120f072 Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Thu, 11 Aug 2022 17:27:34 +0200 Subject: [PATCH 290/329] chore: add logging capability --- .../src/main/scala/dev/atedeg/mdm/restocking/Main.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala index e09eb2d3..a16ac26c 100644 --- a/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala +++ b/restocking/src/main/scala/dev/atedeg/mdm/restocking/Main.scala @@ -8,6 +8,10 @@ import cats.syntax.all.* import org.http4s.HttpRoutes import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.Router +import org.typelevel.log4cats.LoggerFactory +import org.typelevel.log4cats.SelfAwareStructuredLogger +import org.typelevel.log4cats.slf4j.Slf4jFactory +import org.typelevel.log4cats.slf4j.loggerFactoryforSync import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.tapir.swagger.bundle.SwaggerInterpreter @@ -23,10 +27,13 @@ object Main extends IOApp: private val routes: HttpRoutes[IO] = RemainingQuintalsOfMilkEndpoint.remainingQuintalsOfMilkRoute <+> swaggerRoute + implicit val logging: LoggerFactory[IO] = Slf4jFactory[IO] + private val logger: SelfAwareStructuredLogger[IO] = LoggerFactory[IO].getLogger + override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO] .bindHttp(Properties.envOrElse("PORT", "8080").toInt, Properties.envOrElse("HOST", "localhost")) .withHttpApp(Router("/" -> routes).orNotFound) .resource - .use(_ => IO.println("Started") >> IO.never[Unit]) + .use(_ => logger.info("Server started") >> IO.never[Unit]) .as(ExitCode.Success) From 99d48511ef353df3f8cf48f241c5ea2795062c1b Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 11:02:13 +0200 Subject: [PATCH 291/329] refactor: remove orderID from ReceivedOrder event since it will be generated by the system --- .../src/main/scala/dev/atedeg/mdm/clientorders/Events.scala | 1 - .../src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala | 1 - 2 files changed, 2 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala index a2b6973d..4a52d6ab 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Events.scala @@ -15,7 +15,6 @@ enum IncomingEvent: * An [[IncomingEvent event]] which is received when an [[Order.IncomingOrder order]] is made. */ case OrderReceived( - id: OrderID, orderLines: NonEmptyList[IncomingOrderLine], customer: Customer, deliveryDate: LocalDateTime, diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala index ecaf0536..9511d9a9 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -27,7 +27,6 @@ import Commons.* import Commons.given final case class OrderReceivedDTO( - id: String, orderLines: List[IncomingOrderLineDTO], customer: CustomerDTO, deliveryDate: String, From f6233cd8df342b09d1a7a55f47ab19e984344131 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 11:45:30 +0200 Subject: [PATCH 292/329] refactor: change price list modelling using a simple case class instead of a type alias --- .../dev/atedeg/mdm/clientorders/Actions.scala | 2 +- .../dev/atedeg/mdm/clientorders/Types.scala | 2 +- .../atedeg/mdm/clientorders/dto/DTOs.scala | 19 +++++++++++++++++++ .../atedeg/mdm/clientorders/ActionsTest.scala | 10 ++++++---- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala index 9a6b4f68..30b314e6 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Actions.scala @@ -32,7 +32,7 @@ def processIncomingOrder[M[_]: Monad: Emits[OrderProcessed]](priceList: PriceLis private def priceOrder(priceList: PriceList)(incomingOrder: IncomingOrder): PricedOrder = val pricedOrderLines = incomingOrder.orderLines.map { case IncomingOrderLine(quantity, product) => - val price = priceList(product).n * quantity.n + val price = priceList.priceList(product).n * quantity.n PricedOrderLine(quantity, product, price.euroCents) } val totalPrice = pricedOrderLines.map(_.totalPrice).reduce(_ + _) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala index 28107426..d52d4eb8 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Types.scala @@ -88,7 +88,7 @@ final case class Longitude(value: DecimalInClosedRange[-180.0, 180.0]) /** * Associates to each [[Product product]] its [[PriceInEuroCents unitary price]]. */ -type PriceList = Product => PriceInEuroCents +final case class PriceList(priceList: Map[Product, PriceInEuroCents]) /** * A price expressed in cents, the smallest currency unit for euros. diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala index 9511d9a9..25ddf236 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -22,6 +22,7 @@ private object Commons: given DTO[CustomerName, String] = caseClassDTO given DTO[VATNumber, String] = caseClassDTO given DTO[IncomingOrderLine, IncomingOrderLineDTO] = interCaseClassDTO + given DTO[PriceInEuroCents, Int] = caseClassDTO import Commons.* import Commons.given @@ -55,6 +56,24 @@ object OrderProcessedDTO: given DTO[OrderProcessed, OrderProcessedDTO] = interCaseClassDTO private given DTO[IncomingOrder, IncomingOrderDTO] = interCaseClassDTO +final case class PriceListDTO(priceList: Map[ProductDTO, Int]) +object PriceListDTO: + given DTO[PriceList, PriceListDTO] = interCaseClassDTO + +final case class PricedOrderDTO( + id: String, + orderLines: List[PricedOrderLineDTO], + customer: CustomerDTO, + deliveryDate: String, + deliveryLocation: LocationDTO, + totalPrice: Int, +) +final case class PricedOrderLineDTO(quantity: Int, product: ProductDTO, totalPrice: Int) + +object PricedOrderDTO: + given DTO[PricedOrder, PricedOrderDTO] = interCaseClassDTO + private given DTO[PricedOrderLine, PricedOrderLineDTO] = interCaseClassDTO + final case class ProductPalletizedDTO(product: ProductDTO, quantity: Int) object ProductPalletizedDTO: given DTO[ProductPalletized, ProductPalletizedDTO] = interCaseClassDTO diff --git a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala index cdb05419..f044c79f 100644 --- a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala +++ b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/ActionsTest.scala @@ -40,9 +40,11 @@ trait LocationMock: trait PriceListMock: - val priceList: Product => PriceInEuroCents = Map( - Caciotta(1000) -> 100.euroCents, - Caciotta(500) -> 50.euroCents, + val priceList: PriceList = PriceList( + Map( + Caciotta(1000) -> 100.euroCents, + Caciotta(500) -> 50.euroCents, + ), ) trait OrderMocks extends PriceListMock, CustomerMock, LocationMock: @@ -82,7 +84,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Explicitly with Match val (events, pricedOrder) = priceAction.execute Then("the priced is computed correctly") val expectedPrice = - incomingOrder.orderLines.map(ol => priceList(ol.product).n * ol.quantity.n).reduce(_ + _).euroCents + incomingOrder.orderLines.map(ol => priceList.priceList(ol.product).n * ol.quantity.n).reduce(_ + _).euroCents pricedOrder.totalPrice shouldBe expectedPrice And("an event is emitted") events shouldBe List(OrderProcessed(incomingOrder)) From 2536958bd3c3edc5e0f5e35b2e0e79cd39947862 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 12:02:27 +0200 Subject: [PATCH 293/329] refactor: add DTO for in progress orders --- .../atedeg/mdm/clientorders/dto/DTOs.scala | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala index 25ddf236..8e9b6008 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -1,11 +1,14 @@ package dev.atedeg.mdm.clientorders.dto +import cats.syntax.all.* + import dev.atedeg.mdm.clientorders.* import dev.atedeg.mdm.clientorders.IncomingEvent.* import dev.atedeg.mdm.clientorders.OutgoingEvent.* import dev.atedeg.mdm.products.dto.ProductDTO import dev.atedeg.mdm.utils.serialization.DTO import dev.atedeg.mdm.utils.serialization.DTOGenerators.* +import dev.atedeg.mdm.utils.serialization.DTOOps.* private object Commons: final case class CustomerDTO(code: String, name: String, vatNumber: String) @@ -23,6 +26,7 @@ private object Commons: given DTO[VATNumber, String] = caseClassDTO given DTO[IncomingOrderLine, IncomingOrderLineDTO] = interCaseClassDTO given DTO[PriceInEuroCents, Int] = caseClassDTO + given DTO[PalletizedQuantity, Int] = caseClassDTO import Commons.* import Commons.given @@ -60,19 +64,41 @@ final case class PriceListDTO(priceList: Map[ProductDTO, Int]) object PriceListDTO: given DTO[PriceList, PriceListDTO] = interCaseClassDTO -final case class PricedOrderDTO( +final case class InProgressOrderDTO( id: String, - orderLines: List[PricedOrderLineDTO], + orderLines: List[InProgressOrderLineDTO], customer: CustomerDTO, deliveryDate: String, deliveryLocation: LocationDTO, totalPrice: Int, ) -final case class PricedOrderLineDTO(quantity: Int, product: ProductDTO, totalPrice: Int) +final case class InProgressOrderLineDTO( + tag: String, + completeDTO: Option[CompleteOrderLineDTO], + incompleteDTO: Option[IncompleteOrderLineDTO], +) + +final case class CompleteOrderLineDTO(quantity: Int, product: ProductDTO, price: Int) +final case class IncompleteOrderLineDTO(actual: Int, required: Int, product: ProductDTO, price: Int) -object PricedOrderDTO: - given DTO[PricedOrder, PricedOrderDTO] = interCaseClassDTO - private given DTO[PricedOrderLine, PricedOrderLineDTO] = interCaseClassDTO +object InProgressOrderDTO: + given DTO[InProgressOrder, InProgressOrderDTO] = interCaseClassDTO + private given DTO[InProgressOrderLine.Complete, CompleteOrderLineDTO] = interCaseClassDTO + private given DTO[InProgressOrderLine.Incomplete, IncompleteOrderLineDTO] = interCaseClassDTO + private given DTO[InProgressOrderLine, InProgressOrderLineDTO] = new DTO: + override def elemToDto(e: InProgressOrderLine): InProgressOrderLineDTO = e match + case c: InProgressOrderLine.Complete => InProgressOrderLineDTO("complete", Some(c.toDTO), None) + case i: InProgressOrderLine.Incomplete => InProgressOrderLineDTO("incomplete", None, Some(i.toDTO)) + override def dtoToElem(dto: InProgressOrderLineDTO): Either[String, InProgressOrderLine] = dto.tag match + case "complete" => + dto.completeDTO match + case Some(dto) => dto.toDomain[InProgressOrderLine.Complete] + case None => "Found tag 'complete' but complete data is missing".asLeft[InProgressOrderLine] + case "incomplete" => + dto.incompleteDTO match + case Some(dto) => dto.toDomain[InProgressOrderLine.Incomplete] + case None => "Found tag 'incomplete' but incomplete data is missing".asLeft[InProgressOrderLine] + case s => s"Unknown tag: $s".asLeft[InProgressOrderLine] final case class ProductPalletizedDTO(product: ProductDTO, quantity: Int) object ProductPalletizedDTO: From c07674154332e641e8adec4c2a85d80d71e55707 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 12:05:59 +0200 Subject: [PATCH 294/329] feat: add handler for incoming orders --- .../mdm/clientorders/api/Configuration.scala | 9 ++++ .../atedeg/mdm/clientorders/api/Emitter.scala | 9 ++++ .../mdm/clientorders/api/Handlers.scala | 45 +++++++++++++++++++ .../api/repositories/Repositories.scala | 12 +++++ 4 files changed, 75 insertions(+) create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Configuration.scala create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Configuration.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Configuration.scala new file mode 100644 index 00000000..c1f161e5 --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Configuration.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.clientorders.api + +import dev.atedeg.mdm.clientorders.api.repositories.* + +final case class Configuration( + priceListRepository: PriceListRepository, + orderRepository: OrderRepository, + emitter: Emitter, +) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala new file mode 100644 index 00000000..deec6258 --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala @@ -0,0 +1,9 @@ +package dev.atedeg.mdm.clientorders.api + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.clientorders.dto.OrderProcessedDTO + +trait Emitter: + def emitOrderProcessed[M[_]: Monad: LiftIO](orderProcessed: OrderProcessedDTO): M[Unit] diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala new file mode 100644 index 00000000..93d05047 --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala @@ -0,0 +1,45 @@ +package dev.atedeg.mdm.clientorders.api + +import java.util.UUID + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* + +import dev.atedeg.mdm.clientorders.* +import dev.atedeg.mdm.clientorders.IncomingEvent.* +import dev.atedeg.mdm.clientorders.OutgoingEvent.* +import dev.atedeg.mdm.clientorders.dto.* +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +def newOrderHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configuration]]( + orderReceivedDTO: OrderReceivedDTO, +): M[String] = + for + config <- readState + priceList <- config.priceListRepository.read >>= validate + orderData <- validate(orderReceivedDTO) + incomingOrder = IncomingOrder( + OrderID(UUID.randomUUID), + orderData.orderLines, + orderData.customer, + orderData.deliveryDate, + orderData.deliveryLocation, + ) + action: SafeAction[OrderProcessed, PricedOrder] = processIncomingOrder(priceList)(incomingOrder) + (events, pricedOrder) = action.execute + _ <- events.map(_.toDTO[OrderProcessedDTO]).traverse(config.emitter.emitOrderProcessed) + inProgressOrder = startPreparingOrder(pricedOrder) + _ <- config.orderRepository.writeInProgressOrder(inProgressOrder.toDTO) + yield pricedOrder.id.id.toDTO[String] + +/* +def productPalletizedForOrderHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configuration]]( + productPalletizedForOrderDTO: ProductPalletizedForOrderDTO, +): M[Unit] = + for + config <- readState + + yield () + */ diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala new file mode 100644 index 00000000..e96f735c --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala @@ -0,0 +1,12 @@ +package dev.atedeg.mdm.clientorders.api.repositories + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.clientorders.dto.* + +trait PriceListRepository: + def read[M[_]: Monad: LiftIO]: M[PriceListDTO] + +trait OrderRepository: + def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] From 8a34e02ff7ec01217018d6e8b67740439d3aeacf Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 12:36:48 +0200 Subject: [PATCH 295/329] test: add new order handler test --- .../atedeg/mdm/clientorders/dto/DTOs.scala | 7 +- .../mdm/clientorders/api/HandlersTests.scala | 67 +++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala index 8e9b6008..57d182f3 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -11,10 +11,6 @@ import dev.atedeg.mdm.utils.serialization.DTOGenerators.* import dev.atedeg.mdm.utils.serialization.DTOOps.* private object Commons: - final case class CustomerDTO(code: String, name: String, vatNumber: String) - final case class LocationDTO(latitude: Double, longitude: Double) - final case class IncomingOrderLineDTO(quantity: Int, product: ProductDTO) - given DTO[OrderID, String] = caseClassDTO given DTO[Customer, CustomerDTO] = interCaseClassDTO given DTO[Location, LocationDTO] = interCaseClassDTO @@ -37,6 +33,9 @@ final case class OrderReceivedDTO( deliveryDate: String, deliveryLocation: LocationDTO, ) +final case class IncomingOrderLineDTO(quantity: Int, product: ProductDTO) +final case class CustomerDTO(code: String, name: String, vatNumber: String) +final case class LocationDTO(latitude: Double, longitude: Double) object OrderReceivedDTO: given DTO[OrderReceived, OrderReceivedDTO] = interCaseClassDTO diff --git a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala new file mode 100644 index 00000000..17539391 --- /dev/null +++ b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala @@ -0,0 +1,67 @@ +package dev.atedeg.mdm.clientorders.api + +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* +import cats.syntax.validated +import org.scalatest.EitherValues.* +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import dev.atedeg.mdm.clientorders.api.repositories.* +import dev.atedeg.mdm.clientorders.dto.* +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTOOps.toDTO + +@SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) +trait Mocks: + var savedOrder: Option[InProgressOrderDTO] = None + var emittedOrderProcessed: List[OrderProcessedDTO] = Nil + + val priceListRepository: PriceListRepository = new PriceListRepository: + override def read[M[_]: Monad: LiftIO]: M[PriceListDTO] = PriceListDTO(Map(ProductDTO("ricotta", 350) -> 100)).pure + + val orderRepository: OrderRepository = new OrderRepository: + override def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] = + savedOrder = Some(inProgressOrder) + ().pure + + val emitter: Emitter = new Emitter: + override def emitOrderProcessed[M[_]: Monad: LiftIO](orderProcessed: OrderProcessedDTO): M[Unit] = + emittedOrderProcessed = orderProcessed :: emittedOrderProcessed + ().pure + + val config: Configuration = Configuration(priceListRepository, orderRepository, emitter) + +class HandlersTest extends AnyWordSpec, Matchers, Mocks: + "The `newOrderHandler`" should { + val orderLines = List(IncomingOrderLineDTO(10, ProductDTO("ricotta", 350))) + val customer = CustomerDTO(UUID.randomUUID.toDTO[String], "foo", "IT01088260409") + val deliveryDate = LocalDateTime.now.toDTO[String] + val deliveryLocation = LocationDTO(12, 41) + val orderReceivedDTO = OrderReceivedDTO(orderLines, customer, deliveryDate, deliveryLocation) + val action: ServerAction[Configuration, String, String] = newOrderHandler(orderReceivedDTO) + val res = action.unsafeExecute(config) + + "emit all the events" in { + emittedOrderProcessed match + case Nil => fail("No events were emitted") + case List(e) => + e.incomingOrder shouldBe IncomingOrderDTO(res.value, orderLines, customer, deliveryDate, deliveryLocation) + case _ => fail("Emitted more events than expected") + } + + "save the new in progress order to the DB" in { + savedOrder match + case None => fail("The order was not saved in the DB") + case Some(o) => + val incomplete = IncompleteOrderLineDTO(0, 10, ProductDTO("ricotta", 350), 1000) + val orderLines = List(InProgressOrderLineDTO("incomplete", None, Some(incomplete))) + o shouldBe InProgressOrderDTO(res.value, orderLines, customer, deliveryDate, deliveryLocation, 1000) + } + } From 70ba52cd9afb33239512eca94170e401f7844b7d Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 13:16:09 +0200 Subject: [PATCH 296/329] chore: add mock emitter and repositories --- .../scala/dev/atedeg/mdm/clientorders/api/Emitter.scala | 3 +++ .../mdm/clientorders/api/repositories/Repositories.scala | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala index deec6258..9d342f94 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala @@ -7,3 +7,6 @@ import dev.atedeg.mdm.clientorders.dto.OrderProcessedDTO trait Emitter: def emitOrderProcessed[M[_]: Monad: LiftIO](orderProcessed: OrderProcessedDTO): M[Unit] + +final case class EmitterMQ() extends Emitter: + override def emitOrderProcessed[M[_]: Monad: LiftIO](orderProcessed: OrderProcessedDTO): M[Unit] = ??? diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala index e96f735c..bfd1f484 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala @@ -10,3 +10,9 @@ trait PriceListRepository: trait OrderRepository: def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] + +final case class PriceListRepositoryDB(connectionString: String) extends PriceListRepository: + override def read[M[_]: Monad: LiftIO]: M[PriceListDTO] = ??? + +final case class OrderRepositoryDB(connectionString: String) extends OrderRepository: + override def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] = ??? From 5a6c2b03eb05eeef0673ceaa41cf170c14e488cd Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 13:16:32 +0200 Subject: [PATCH 297/329] feat: add endpoint to place new orders --- .../api/endpoints/Endpoints.scala | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala new file mode 100644 index 00000000..dac33b39 --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala @@ -0,0 +1,30 @@ +package dev.atedeg.mdm.clientorders.api.endpoints + +import cats.effect.IO +import io.circe.generic.auto.* +import org.http4s.HttpRoutes +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.jsonBody +import sttp.tapir.server.http4s.Http4sServerInterpreter + +import dev.atedeg.mdm.clientorders.api.* +import dev.atedeg.mdm.clientorders.api.repositories.* +import dev.atedeg.mdm.clientorders.dto.* +import dev.atedeg.mdm.utils.monads.* + +object OrdersEndpoint: + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val newOrderEndpoint: PublicEndpoint[OrderReceivedDTO, String, String, Any] = + endpoint.post + .in("order") + .in(jsonBody[OrderReceivedDTO].description("TODO")) + .out(stringBody) + .errorOut(stringBody) + + val newOrderRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( + newOrderEndpoint.serverLogic { o => + val action: ServerAction[Configuration, String, String] = newOrderHandler(o) + action.value.run(Configuration(PriceListRepositoryDB("foo"), OrderRepositoryDB("bar"), EmitterMQ())) + }, + ) From 867508fd30be7e9f63a0ceeb976fb1ff78769e4a Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 15:19:13 +0200 Subject: [PATCH 298/329] chore: add handler for productPalletizedForOrder --- .../dev/atedeg/mdm/clientorders/api/Emitter.scala | 4 +++- .../dev/atedeg/mdm/clientorders/api/Handlers.scala | 11 ++++++++--- .../clientorders/api/repositories/Repositories.scala | 4 ++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala index 9d342f94..ced1ee2d 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala @@ -3,10 +3,12 @@ package dev.atedeg.mdm.clientorders.api import cats.Monad import cats.effect.LiftIO -import dev.atedeg.mdm.clientorders.dto.OrderProcessedDTO +import dev.atedeg.mdm.clientorders.dto.* trait Emitter: def emitOrderProcessed[M[_]: Monad: LiftIO](orderProcessed: OrderProcessedDTO): M[Unit] + def emitProductPalletized[M[_]: Monad: LiftIO](orderPalletized: ProductPalletizedDTO): M[Unit] final case class EmitterMQ() extends Emitter: override def emitOrderProcessed[M[_]: Monad: LiftIO](orderProcessed: OrderProcessedDTO): M[Unit] = ??? + override def emitProductPalletized[M[_]: Monad: LiftIO](orderPalletized: ProductPalletizedDTO): M[Unit] = ??? diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala index 93d05047..41019c16 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala @@ -34,12 +34,17 @@ def newOrderHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configuration _ <- config.orderRepository.writeInProgressOrder(inProgressOrder.toDTO) yield pricedOrder.id.id.toDTO[String] -/* def productPalletizedForOrderHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configuration]]( productPalletizedForOrderDTO: ProductPalletizedForOrderDTO, ): M[Unit] = for config <- readState - + productPallettizedForOrder <- validate(productPalletizedForOrderDTO) + order <- config.orderRepository.readInProgressOrder(productPallettizedForOrder.orderID.id.toDTO) + action: Action[PalletizationError, ProductPalletized, InProgressOrder] = + palletizeProductForOrder(productPallettizedForOrder.quantity, productPallettizedForOrder.product)(order) + (events, result) = action.execute + _ <- events.map(_.toDTO[ProductPalletizedDTO]).traverse(config.emitter.emitProductPalletized) + updatedOrder <- result.leftMap(e => s"Palletization error: $e").getOrRaise + _ <- config.orderRepository.writeInProgressOrder(updatedOrder.toDTO) yield () - */ diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala index bfd1f484..2c4740e1 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala @@ -3,16 +3,20 @@ package dev.atedeg.mdm.clientorders.api.repositories import cats.Monad import cats.effect.LiftIO +import dev.atedeg.mdm.clientorders.InProgressOrder import dev.atedeg.mdm.clientorders.dto.* +import dev.atedeg.mdm.utils.monads.* trait PriceListRepository: def read[M[_]: Monad: LiftIO]: M[PriceListDTO] trait OrderRepository: def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] + def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrder] final case class PriceListRepositoryDB(connectionString: String) extends PriceListRepository: override def read[M[_]: Monad: LiftIO]: M[PriceListDTO] = ??? final case class OrderRepositoryDB(connectionString: String) extends OrderRepository: override def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] = ??? + override def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrder] = ??? From dadaf7a3f9f6798d7180677101b3929ef822b70d Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 16:03:42 +0200 Subject: [PATCH 299/329] test: add test for ProductPalletizedForOrderHandler --- .../atedeg/mdm/clientorders/api/Emitter.scala | 2 +- .../mdm/clientorders/api/Handlers.scala | 2 +- .../api/repositories/Repositories.scala | 4 +- .../mdm/clientorders/api/HandlersTests.scala | 48 ++++++++++++++++++- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala index ced1ee2d..1d80801a 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Emitter.scala @@ -11,4 +11,4 @@ trait Emitter: final case class EmitterMQ() extends Emitter: override def emitOrderProcessed[M[_]: Monad: LiftIO](orderProcessed: OrderProcessedDTO): M[Unit] = ??? - override def emitProductPalletized[M[_]: Monad: LiftIO](orderPalletized: ProductPalletizedDTO): M[Unit] = ??? + override def emitProductPalletized[M[_]: Monad: LiftIO](productPalletized: ProductPalletizedDTO): M[Unit] = ??? diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala index 41019c16..b5f2b7ed 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala @@ -40,7 +40,7 @@ def productPalletizedForOrderHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanR for config <- readState productPallettizedForOrder <- validate(productPalletizedForOrderDTO) - order <- config.orderRepository.readInProgressOrder(productPallettizedForOrder.orderID.id.toDTO) + order <- config.orderRepository.readInProgressOrder(productPallettizedForOrder.orderID.id.toDTO) >>= validate action: Action[PalletizationError, ProductPalletized, InProgressOrder] = palletizeProductForOrder(productPallettizedForOrder.quantity, productPallettizedForOrder.product)(order) (events, result) = action.execute diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala index 2c4740e1..febe8642 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala @@ -12,11 +12,11 @@ trait PriceListRepository: trait OrderRepository: def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] - def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrder] + def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrderDTO] final case class PriceListRepositoryDB(connectionString: String) extends PriceListRepository: override def read[M[_]: Monad: LiftIO]: M[PriceListDTO] = ??? final case class OrderRepositoryDB(connectionString: String) extends OrderRepository: override def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] = ??? - override def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrder] = ??? + override def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrderDTO] = ??? diff --git a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala index 17539391..fbaf80ff 100644 --- a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala +++ b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala @@ -8,10 +8,12 @@ import cats.Monad import cats.effect.LiftIO import cats.syntax.all.* import cats.syntax.validated +import org.scalatest.* import org.scalatest.EitherValues.* import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +import dev.atedeg.mdm.clientorders.InProgressOrder import dev.atedeg.mdm.clientorders.api.repositories.* import dev.atedeg.mdm.clientorders.dto.* import dev.atedeg.mdm.products.dto.ProductDTO @@ -20,8 +22,19 @@ import dev.atedeg.mdm.utils.serialization.DTOOps.toDTO @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) trait Mocks: - var savedOrder: Option[InProgressOrderDTO] = None + var emittedProductPalletized: List[ProductPalletizedDTO] = Nil var emittedOrderProcessed: List[OrderProcessedDTO] = Nil + var savedOrder: Option[InProgressOrderDTO] = None + + val incompleteOrderLine: IncompleteOrderLineDTO = IncompleteOrderLineDTO(0, 100, ProductDTO("ricotta", 350), 1000) + val oldInProgressOrder: InProgressOrderDTO = InProgressOrderDTO( + UUID.randomUUID.toDTO, + List(InProgressOrderLineDTO("incomplete", None, Some(incompleteOrderLine))), + CustomerDTO(UUID.randomUUID.toDTO, "foo", "IT01088260409"), + LocalDateTime.now.toDTO, + LocationDTO(12, 42), + 1000, + ) val priceListRepository: PriceListRepository = new PriceListRepository: override def read[M[_]: Monad: LiftIO]: M[PriceListDTO] = PriceListDTO(Map(ProductDTO("ricotta", 350) -> 100)).pure @@ -30,15 +43,20 @@ trait Mocks: override def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] = savedOrder = Some(inProgressOrder) ().pure + override def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrderDTO] = + oldInProgressOrder.pure val emitter: Emitter = new Emitter: override def emitOrderProcessed[M[_]: Monad: LiftIO](orderProcessed: OrderProcessedDTO): M[Unit] = emittedOrderProcessed = orderProcessed :: emittedOrderProcessed ().pure + override def emitProductPalletized[M[_]: Monad: LiftIO](productPalletized: ProductPalletizedDTO): M[Unit] = + emittedProductPalletized = productPalletized :: emittedProductPalletized + ().pure val config: Configuration = Configuration(priceListRepository, orderRepository, emitter) -class HandlersTest extends AnyWordSpec, Matchers, Mocks: +class NewOrderHandler extends AnyWordSpec, Matchers, Mocks: "The `newOrderHandler`" should { val orderLines = List(IncomingOrderLineDTO(10, ProductDTO("ricotta", 350))) val customer = CustomerDTO(UUID.randomUUID.toDTO[String], "foo", "IT01088260409") @@ -65,3 +83,29 @@ class HandlersTest extends AnyWordSpec, Matchers, Mocks: o shouldBe InProgressOrderDTO(res.value, orderLines, customer, deliveryDate, deliveryLocation, 1000) } } + +class ProductPalletizedForOrderHandler extends AnyWordSpec, Matchers, Mocks: + "The `ProductPalletizedForOrderHandler`" should { + val dto = ProductPalletizedForOrderDTO(oldInProgressOrder.id, 10, ProductDTO("ricotta", 350)) + val action: ServerAction[Configuration, String, Unit] = productPalletizedForOrderHandler(dto) + action.unsafeExecute(config) + + "save the updated order" in { + savedOrder match + case None => fail("The order was not updated") + case Some(o) => + val orderLine = o.orderLines(0) + orderLine.incompleteDTO match + case None => fail("The order was not updated correctly") + case Some(i) => i.actual shouldBe 10 + } + + "emit all the events" in { + emittedProductPalletized match + case Nil => fail("No events were emitted") + case List(e) => + e.product shouldBe ProductDTO("ricotta", 350) + e.quantity shouldBe 10 + case _ => fail("Emitted more events than expected") + } + } From c1b1afdc1fc63dd5e7940e39d46645e67cecde66 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 16:13:30 +0200 Subject: [PATCH 300/329] feat: add endpoint to palletize product for order --- .../mdm/clientorders/api/endpoints/Endpoints.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala index dac33b39..68c5f72f 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala @@ -28,3 +28,17 @@ object OrdersEndpoint: action.value.run(Configuration(PriceListRepositoryDB("foo"), OrderRepositoryDB("bar"), EmitterMQ())) }, ) + + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val palletizeProductForOrder: PublicEndpoint[ProductPalletizedForOrderDTO, String, Unit, Any] = + endpoint.post + .in("order" / "palletize") + .in(jsonBody[ProductPalletizedForOrderDTO].description("TODO")) + .errorOut(stringBody) + + val palletizeProductRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( + palletizeProductForOrder.serverLogic { p => + val action: ServerAction[Configuration, String, Unit] = productPalletizedForOrderHandler(p) + action.value.run(Configuration(PriceListRepositoryDB("foo"), OrderRepositoryDB("bar"), EmitterMQ())) + }, + ) From 10510ba535a2816d349d32762269d47dc2059269 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 16:26:15 +0200 Subject: [PATCH 301/329] chore: add handler for order completion --- .../dev/atedeg/mdm/clientorders/api/Handlers.scala | 14 ++++++++++++++ .../api/repositories/Repositories.scala | 2 ++ .../dev/atedeg/mdm/clientorders/dto/DTOs.scala | 13 +++++++++++++ 3 files changed, 29 insertions(+) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala index b5f2b7ed..6d3fce79 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala @@ -4,6 +4,7 @@ import java.util.UUID import cats.Monad import cats.effect.LiftIO +import cats.instances.ordering import cats.syntax.all.* import dev.atedeg.mdm.clientorders.* @@ -48,3 +49,16 @@ def productPalletizedForOrderHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanR updatedOrder <- result.leftMap(e => s"Palletization error: $e").getOrRaise _ <- config.orderRepository.writeInProgressOrder(updatedOrder.toDTO) yield () + +def orderCompletedHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configuration]]( + orderCompletedDTO: OrderCompletedDTO, +): M[Unit] = + for + config <- readState + orderCompleted <- validate(orderCompletedDTO) + order <- config.orderRepository.readInProgressOrder(orderCompleted.orderID.id.toDTO) >>= validate + action: Action[OrderCompletionError, Unit, CompletedOrder] = completeOrder(order) + (_, result) = action.execute + completedOrder <- result.leftMap(e => s"Order completion error: $e").getOrRaise + _ <- config.orderRepository.updateOrderToCompleted(completedOrder.toDTO) + yield () diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala index febe8642..ce35a6a9 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala @@ -13,6 +13,7 @@ trait PriceListRepository: trait OrderRepository: def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrderDTO] + def updateOrderToCompleted[M[_]: Monad: LiftIO: CanRaise[String]](completedOrder: CompletedOrderDTO): M[Unit] final case class PriceListRepositoryDB(connectionString: String) extends PriceListRepository: override def read[M[_]: Monad: LiftIO]: M[PriceListDTO] = ??? @@ -20,3 +21,4 @@ final case class PriceListRepositoryDB(connectionString: String) extends PriceLi final case class OrderRepositoryDB(connectionString: String) extends OrderRepository: override def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] = ??? override def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrderDTO] = ??? + override def updateOrderToCompleted[M[_]: Monad: LiftIO: CanRaise[String]](order: CompletedOrderDTO): M[Unit] = ??? diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala index 57d182f3..f3a16be2 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -103,3 +103,16 @@ final case class ProductPalletizedDTO(product: ProductDTO, quantity: Int) object ProductPalletizedDTO: given DTO[ProductPalletized, ProductPalletizedDTO] = interCaseClassDTO private given DTO[Quantity, Int] = caseClassDTO + +final case class CompletedOrderDTO( + id: String, + orderLines: List[CompletedOrderLineDTO], + customer: CustomerDTO, + deliveryDate: String, + deliveryLocation: LocationDTO, + totalPrice: Int, +) +final case class CompletedOrderLineDTO(quantity: Int, product: ProductDTO, price: Int) +object CompletedOrderDTO: + given DTO[CompletedOrder, CompletedOrderDTO] = interCaseClassDTO + private given DTO[CompleteOrderLine, CompletedOrderLineDTO] = interCaseClassDTO From 9cae769f55ac6d185448b4c2663a50096642c2ad Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 16:32:58 +0200 Subject: [PATCH 302/329] chore: add handlers to complete order and get its DDT --- .../main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala | 4 ++++ .../mdm/clientorders/api/repositories/Repositories.scala | 2 ++ 2 files changed, 6 insertions(+) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala index 6d3fce79..542965c0 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala @@ -62,3 +62,7 @@ def orderCompletedHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configu completedOrder <- result.leftMap(e => s"Order completion error: $e").getOrRaise _ <- config.orderRepository.updateOrderToCompleted(completedOrder.toDTO) yield () + +def getDDTHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configuration]](orderID: String): M[Unit] = + (readState >>= (_.orderRepository.readCompletedOrder(orderID)) >>= validate) + .map(o => createTransportDocument(o, weightOrder(o))) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala index ce35a6a9..d7804d5a 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/repositories/Repositories.scala @@ -14,6 +14,7 @@ trait OrderRepository: def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrderDTO] def updateOrderToCompleted[M[_]: Monad: LiftIO: CanRaise[String]](completedOrder: CompletedOrderDTO): M[Unit] + def readCompletedOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[CompletedOrderDTO] final case class PriceListRepositoryDB(connectionString: String) extends PriceListRepository: override def read[M[_]: Monad: LiftIO]: M[PriceListDTO] = ??? @@ -22,3 +23,4 @@ final case class OrderRepositoryDB(connectionString: String) extends OrderReposi override def writeInProgressOrder[M[_]: Monad: LiftIO](inProgressOrder: InProgressOrderDTO): M[Unit] = ??? override def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrderDTO] = ??? override def updateOrderToCompleted[M[_]: Monad: LiftIO: CanRaise[String]](order: CompletedOrderDTO): M[Unit] = ??? + override def readCompletedOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[CompletedOrderDTO] = ??? From 27973fed4ec344b5827cb6dbf6e396813b24fac8 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 16:48:33 +0200 Subject: [PATCH 303/329] feat: add endpoints to complete orders and get their transport document --- .../mdm/clientorders/api/Handlers.scala | 6 ++-- .../api/endpoints/Endpoints.scala | 36 +++++++++++++++++-- .../atedeg/mdm/clientorders/dto/DTOs.scala | 13 +++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala index 542965c0..d17bcd52 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/Handlers.scala @@ -63,6 +63,8 @@ def orderCompletedHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configu _ <- config.orderRepository.updateOrderToCompleted(completedOrder.toDTO) yield () -def getDDTHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configuration]](orderID: String): M[Unit] = +def getTransportDocumentHandler[M[_]: Monad: LiftIO: CanRaise[String]: CanRead[Configuration]]( + orderID: String, +): M[TransportDocumentDTO] = (readState >>= (_.orderRepository.readCompletedOrder(orderID)) >>= validate) - .map(o => createTransportDocument(o, weightOrder(o))) + .map(o => createTransportDocument(o, weightOrder(o)).toDTO) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala index 68c5f72f..b4163bf3 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala @@ -18,8 +18,8 @@ object OrdersEndpoint: val newOrderEndpoint: PublicEndpoint[OrderReceivedDTO, String, String, Any] = endpoint.post .in("order") - .in(jsonBody[OrderReceivedDTO].description("TODO")) - .out(stringBody) + .in(jsonBody[OrderReceivedDTO].description("The order that needs to be placed")) + .out(stringBody.description("The ID assigned to the placed order")) .errorOut(stringBody) val newOrderRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( @@ -33,7 +33,7 @@ object OrdersEndpoint: val palletizeProductForOrder: PublicEndpoint[ProductPalletizedForOrderDTO, String, Unit, Any] = endpoint.post .in("order" / "palletize") - .in(jsonBody[ProductPalletizedForOrderDTO].description("TODO")) + .in(jsonBody[ProductPalletizedForOrderDTO].description("The product and quantity palletized for the given order")) .errorOut(stringBody) val palletizeProductRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( @@ -42,3 +42,33 @@ object OrdersEndpoint: action.value.run(Configuration(PriceListRepositoryDB("foo"), OrderRepositoryDB("bar"), EmitterMQ())) }, ) + + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val orderCompletedEndpoint: PublicEndpoint[OrderCompletedDTO, String, Unit, Any] = + endpoint.post + .in("order" / "complete") + .in(jsonBody[OrderCompletedDTO].description("The ID of the order that has been completed")) + .errorOut(stringBody) + + val orderCompletedRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( + orderCompletedEndpoint.serverLogic { o => + val action: ServerAction[Configuration, String, Unit] = orderCompletedHandler(o) + action.value.run(Configuration(PriceListRepositoryDB("foo"), OrderRepositoryDB("bar"), EmitterMQ())) + }, + ) + + @SuppressWarnings(Array("org.wartremover.warts.Any")) + val getTransportDocumentEndpoint: PublicEndpoint[String, String, TransportDocumentDTO, Any] = + endpoint.get + .in("order") + .in(path[String].description("The ID of the order for which the transport document is requested")) + .in("ddt") + .out(jsonBody[TransportDocumentDTO].description("The transport document for the given order")) + .errorOut(stringBody) + + val getTransportDocumentRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( + getTransportDocumentEndpoint.serverLogic { o => + val action: ServerAction[Configuration, String, TransportDocumentDTO] = getTransportDocumentHandler(o) + action.value.run(Configuration(PriceListRepositoryDB("foo"), OrderRepositoryDB("bar"), EmitterMQ())) + }, + ) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala index f3a16be2..df76b785 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -116,3 +116,16 @@ final case class CompletedOrderLineDTO(quantity: Int, product: ProductDTO, price object CompletedOrderDTO: given DTO[CompletedOrder, CompletedOrderDTO] = interCaseClassDTO private given DTO[CompleteOrderLine, CompletedOrderLineDTO] = interCaseClassDTO + +final case class TransportDocumentDTO( + deliveryLocation: LocationDTO, + shippingLocation: LocationDTO, + customer: CustomerDTO, + shippingDate: String, + transportDocumentLines: List[TransportDocumentLineDTO], + totalWeight: Double, +) +final case class TransportDocumentLineDTO(quantity: Int, product: ProductDTO, price: Int) +object TransportDocumentDTO: + given DTO[TransportDocument, TransportDocumentDTO] = interCaseClassDTO + private given DTO[TransportDocumentLine, TransportDocumentLineDTO] = interCaseClassDTO From 08380a04f9b6339544d86c6115b5f80ee71e7b5e Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 17:03:52 +0200 Subject: [PATCH 304/329] chore: add main --- .../dev/atedeg/mdm/clientorders/Main.scala | 42 +++++++++++++++++++ .../api/endpoints/Endpoints.scala | 6 +-- 2 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Main.scala diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Main.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Main.scala new file mode 100644 index 00000000..4c8a0837 --- /dev/null +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/Main.scala @@ -0,0 +1,42 @@ +package dev.atedeg + +import scala.util.Properties + +import cats.effect.ExitCode +import cats.effect.IO +import cats.effect.IOApp +import cats.syntax.all.* +import org.http4s.HttpRoutes +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router +import org.typelevel.log4cats.* +import org.typelevel.log4cats.slf4j.Slf4jFactory +import org.typelevel.log4cats.slf4j.loggerFactoryforSync +import sttp.tapir.server.http4s.Http4sServerInterpreter +import sttp.tapir.swagger.bundle.SwaggerInterpreter + +import dev.atedeg.mdm.clientorders.api.endpoints.OrdersEndpoint.* + +object Main extends IOApp: + private val swaggerEndpoint = SwaggerInterpreter().fromEndpoints[IO]( + newOrderEndpoint :: palletizeProductForOrderEndpoint :: orderCompletedEndpoint :: getTransportDocumentEndpoint :: Nil, + "Client orders", + Properties.envOrElse("VERSION", "v1-beta"), + ) + + private val swaggerRoute = Http4sServerInterpreter[IO]().toRoutes(swaggerEndpoint) + private val routes: HttpRoutes[IO] = newOrderRoute + <+> palletizeProductForOrderRoute + <+> orderCompletedRoute + <+> getTransportDocumentRoute + <+> swaggerRoute + + implicit val logging: LoggerFactory[IO] = Slf4jFactory[IO] + private val logger: SelfAwareStructuredLogger[IO] = LoggerFactory[IO].getLogger + + override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO] + .bindHttp(Properties.envOrElse("PORT", "8080").toInt, Properties.envOrElse("HOST", "localhost")) + .withHttpApp(Router("/" -> routes).orNotFound) + .resource + .use(_ => logger.info("server started") >> IO.never[Unit]) + .as(ExitCode.Success) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala index b4163bf3..e160cb52 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala @@ -30,14 +30,14 @@ object OrdersEndpoint: ) @SuppressWarnings(Array("org.wartremover.warts.Any")) - val palletizeProductForOrder: PublicEndpoint[ProductPalletizedForOrderDTO, String, Unit, Any] = + val palletizeProductForOrderEndpoint: PublicEndpoint[ProductPalletizedForOrderDTO, String, Unit, Any] = endpoint.post .in("order" / "palletize") .in(jsonBody[ProductPalletizedForOrderDTO].description("The product and quantity palletized for the given order")) .errorOut(stringBody) - val palletizeProductRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( - palletizeProductForOrder.serverLogic { p => + val palletizeProductForOrderRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes( + palletizeProductForOrderEndpoint.serverLogic { p => val action: ServerAction[Configuration, String, Unit] = productPalletizedForOrderHandler(p) action.value.run(Configuration(PriceListRepositoryDB("foo"), OrderRepositoryDB("bar"), EmitterMQ())) }, From ca433f8dada9f961c80b7539774216fbc21cf4f7 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 17:06:49 +0200 Subject: [PATCH 305/329] chore: fix minor code errors --- .../src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala | 3 ++- .../scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala index df76b785..25c42111 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/dto/DTOs.scala @@ -23,6 +23,7 @@ private object Commons: given DTO[IncomingOrderLine, IncomingOrderLineDTO] = interCaseClassDTO given DTO[PriceInEuroCents, Int] = caseClassDTO given DTO[PalletizedQuantity, Int] = caseClassDTO + given DTO[WeightInKilograms, Double] = caseClassDTO import Commons.* import Commons.given @@ -125,7 +126,7 @@ final case class TransportDocumentDTO( transportDocumentLines: List[TransportDocumentLineDTO], totalWeight: Double, ) -final case class TransportDocumentLineDTO(quantity: Int, product: ProductDTO, price: Int) +final case class TransportDocumentLineDTO(quantity: Int, product: ProductDTO) object TransportDocumentDTO: given DTO[TransportDocument, TransportDocumentDTO] = interCaseClassDTO private given DTO[TransportDocumentLine, TransportDocumentLineDTO] = interCaseClassDTO diff --git a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala index fbaf80ff..e14d323c 100644 --- a/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala +++ b/client-orders/src/test/scala/dev/atedeg/mdm/clientorders/api/HandlersTests.scala @@ -45,6 +45,8 @@ trait Mocks: ().pure override def readInProgressOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[InProgressOrderDTO] = oldInProgressOrder.pure + override def readCompletedOrder[M[_]: Monad: LiftIO: CanRaise[String]](orderID: String): M[CompletedOrderDTO] = ??? + override def updateOrderToCompleted[M[_]: Monad: LiftIO: CanRaise[String]](order: CompletedOrderDTO): M[Unit] = ??? val emitter: Emitter = new Emitter: override def emitOrderProcessed[M[_]: Monad: LiftIO](orderProcessed: OrderProcessedDTO): M[Unit] = From 7290b6a53dce5082f2737b50c71cb759133b9497 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 17:10:31 +0200 Subject: [PATCH 306/329] fix: change endpoint path parameter name --- .../atedeg/mdm/clientorders/api/endpoints/Endpoints.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala index e160cb52..cb697b1b 100644 --- a/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala +++ b/client-orders/src/main/scala/dev/atedeg/mdm/clientorders/api/endpoints/Endpoints.scala @@ -61,7 +61,11 @@ object OrdersEndpoint: val getTransportDocumentEndpoint: PublicEndpoint[String, String, TransportDocumentDTO, Any] = endpoint.get .in("order") - .in(path[String].description("The ID of the order for which the transport document is requested")) + .in( + path[String] + .description("The ID of the order for which the transport document is requested") + .name("order-id"), + ) .in("ddt") .out(jsonBody[TransportDocumentDTO].description("The transport document for the given order")) .errorOut(stringBody) From e567f8cf498e936a6124b2d8abb0a1b7222e85d0 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 17:30:09 +0200 Subject: [PATCH 307/329] docs: fix ubidoc table --- .ubidoc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index ea2a93f1..1a21b9b5 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -112,7 +112,7 @@ tables: - class: "IncomingOrderLine" - class: "Customer" - class: "Location" - - type: "PriceList" + - class: "PriceList" - class: "PricedOrder" - class: "PricedOrderLine" - class: "InProgressOrder" From 67703a7d33d3eb9459936dafdd59746051634326 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 11 Aug 2022 16:13:06 +0000 Subject: [PATCH 308/329] chore(release): 1.0.0-beta.15 [skip ci] # [1.0.0-beta.15](https://github.com/atedeg/mdm/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2022-08-11) ### Bug Fixes * change endpoint path parameter name ([7290b6a](https://github.com/atedeg/mdm/commit/7290b6a53dce5082f2737b50c71cb759133b9497)) ### Features * add endpoint to palletize product for order ([c1b1afd](https://github.com/atedeg/mdm/commit/c1b1afdc1fc63dd5e7940e39d46645e67cecde66)) * add endpoint to place new orders ([5a6c2b0](https://github.com/atedeg/mdm/commit/5a6c2b03eb05eeef0673ceaa41cf170c14e488cd)) * add endpoints to complete orders and get their transport document ([27973fe](https://github.com/atedeg/mdm/commit/27973fed4ec344b5827cb6dbf6e396813b24fac8)) * add handler for incoming orders ([c076741](https://github.com/atedeg/mdm/commit/c07674154332e641e8adec4c2a85d80d71e55707)) --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04c913c5..651df18c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# [1.0.0-beta.15](https://github.com/atedeg/mdm/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2022-08-11) + + +### Bug Fixes + +* change endpoint path parameter name ([7290b6a](https://github.com/atedeg/mdm/commit/7290b6a53dce5082f2737b50c71cb759133b9497)) + + +### Features + +* add endpoint to palletize product for order ([c1b1afd](https://github.com/atedeg/mdm/commit/c1b1afdc1fc63dd5e7940e39d46645e67cecde66)) +* add endpoint to place new orders ([5a6c2b0](https://github.com/atedeg/mdm/commit/5a6c2b03eb05eeef0673ceaa41cf170c14e488cd)) +* add endpoints to complete orders and get their transport document ([27973fe](https://github.com/atedeg/mdm/commit/27973fed4ec344b5827cb6dbf6e396813b24fac8)) +* add handler for incoming orders ([c076741](https://github.com/atedeg/mdm/commit/c07674154332e641e8adec4c2a85d80d71e55707)) + # [1.0.0-beta.14](https://github.com/atedeg/mdm/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2022-08-11) From d27bf4dcdd88f88e1337e625088b1d15ad200b99 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 17:52:39 +0200 Subject: [PATCH 309/329] refactor: change map DTO creation logic --- .../dev/atedeg/mdm/utils/serialization/DTO.scala | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala index 6f856990..14ab0113 100644 --- a/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala +++ b/utils/src/main/scala/dev/atedeg/mdm/utils/serialization/DTO.scala @@ -32,13 +32,9 @@ object DTO: given DTO[Double, Double] = idDTO given DTO[String, String] = idDTO - given mapListDTO[KE, KD, VE, VD](using DTO[KE, KD])(using DTO[VE, VD]): DTO[Map[KE, VE], List[(KD, VD)]] with - override def elemToDto(e: Map[KE, VE]): List[(KD, VD)] = e.toList.map(_.bimap(_.toDTO[KD], _.toDTO[VD])) - override def dtoToElem(dto: List[(KD, VD)]): Either[String, Map[KE, VE]] = - for - keys <- dto.map(_._1).traverse(_.toDomain[KE]) - values <- dto.map(_._2).traverse(_.toDomain[VE]) - yield keys.zip(values).toMap + given mapListDTO[KE, VE, D](using DTO[(KE, VE), D]): DTO[Map[KE, VE], List[D]] with + override def elemToDto(e: Map[KE, VE]): List[D] = e.toList.map(_.toDTO[D]) + override def dtoToElem(dto: List[D]): Either[String, Map[KE, VE]] = dto.traverse(_.toDomain[(KE, VE)]).map(_.toMap) given mapDTO[KE, KD, VE, VD](using DTO[KE, KD])(using DTO[VE, VD]): DTO[Map[KE, VE], Map[KD, VD]] with override def elemToDto(e: Map[KE, VE]): Map[KD, VD] = e.map(_.bimap(_.toDTO[KD], _.toDTO[VD])) @@ -94,7 +90,6 @@ object DTOGenerators: val instance: DTO[t, D] = summonInline[DTO[t, D]] new DTO[E, D]: def elemToDto(e: E): D = instance.elemToDto(firstField(e)) - def dtoToElem(dto: D): Either[String, E] = instance.dtoToElem(dto).map(e => p.fromProduct(e *: EmptyTuple)) case _ => compiletime.error("Can only derive for case classes with only one field") From c6d9ea2d56ff31160a59c40cf1404f58e28c9590 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 11 Aug 2022 17:53:01 +0200 Subject: [PATCH 310/329] refactor: adapt stocking to new map DTO logic creation --- .../main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala | 9 +++++++-- .../scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala b/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala index f28d14ec..4ce8c695 100644 --- a/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala +++ b/stocking/src/main/scala/dev/atedeg/mdm/stocking/dto/DTOs.scala @@ -1,5 +1,6 @@ package dev.atedeg.mdm.stocking.dto +import dev.atedeg.mdm.products.* import dev.atedeg.mdm.products.dto.CheeseTypeDTO.given import dev.atedeg.mdm.products.dto.ProductDTO import dev.atedeg.mdm.stocking.* @@ -31,14 +32,18 @@ final case class NewBatchDTO(batchID: String, cheeseType: String, readyFrom: Str object NewBatchDTO: given DTO[NewBatch, NewBatchDTO] = interCaseClassDTO -final case class AvailableStockDTO(availableStock: List[(ProductDTO, Int)]) +final case class AvailableStockDTO(availableStock: List[ProductAvailableQuantityDTO]) +final case class ProductAvailableQuantityDTO(product: ProductDTO, availableQuantity: Int) object AvailableStockDTO: given DTO[AvailableStock, AvailableStockDTO] = interCaseClassDTO + private given DTO[(Product, AvailableQuantity), ProductAvailableQuantityDTO] = interCaseClassDTO private given DTO[AvailableQuantity, Int] = caseClassDTO -final case class DesiredStockDTO(desiredStock: List[(ProductDTO, Int)]) +final case class DesiredStockDTO(desiredStock: List[ProductDesiredQuantityDTO]) +final case class ProductDesiredQuantityDTO(product: ProductDTO, desiredQuantity: Int) object DesiredStockDTO: given DTO[DesiredStock, DesiredStockDTO] = interCaseClassDTO + private given DTO[(Product, DesiredQuantity), ProductDesiredQuantityDTO] = interCaseClassDTO private given DTO[DesiredQuantity, Int] = caseClassDTO final case class AgingBatchDTO(batchID: String, cheeseType: String, readyFrom: String) diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala index 11d499af..7c3b9464 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/api/HandlersTest.scala @@ -24,8 +24,8 @@ import dev.atedeg.mdm.utils.serialization.DTOOps.* trait Mocks: val product: ProductDTO = ProductDTO("caciotta", 500) @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) - var availableStock: AvailableStockDTO = AvailableStockDTO(List((product, 5))) - val desiredStock: DesiredStockDTO = DesiredStockDTO(List((product, 2))) + var availableStock: AvailableStockDTO = AvailableStockDTO(List(ProductAvailableQuantityDTO(product, 5))) + val desiredStock: DesiredStockDTO = DesiredStockDTO(List(ProductDesiredQuantityDTO(product, 2))) val stockRepository: StockRepository = new StockRepository: override def readStock[M[_]: Monad: LiftIO]: M[AvailableStockDTO] = availableStock.pure override def writeStock[M[_]: Monad: LiftIO](updatedStock: AvailableStockDTO): M[Unit] = @@ -52,7 +52,7 @@ class HandlersTest extends AnyWordSpec, Matchers, Mocks: val productPalletized = ProductPalletizedDTO(product, 1) val handler: ServerAction[StockRepository, String, Unit] = handleRemovalFromStock(productPalletized) handler.unsafeExecute(stockRepository) - availableStock shouldBe AvailableStockDTO(List((product, 4))) + availableStock shouldBe AvailableStockDTO(List(ProductAvailableQuantityDTO(product, 4))) } } "The `handleDesiredStockRequest`" should { From b6e7c1751d18037c292414f924f670cf4cfffccf Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 13:11:55 +0200 Subject: [PATCH 311/329] refactor: cheese type ripening days as a case class --- .ubidoc.yml | 2 +- .../mdm/productionplanning/Actions.scala | 2 +- .../atedeg/mdm/productionplanning/Types.scala | 2 +- .../mdm/productionplanning/dto/DTOs.scala | 20 +++++++++++++------ .../Tests.scala | 14 +++++++------ 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index 1a21b9b5..4673b485 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -95,7 +95,7 @@ tables: - class: "StockedQuantity" - class: "Order" - class: "OrderedProduct" - - type: "CheeseTypeRipeningDays" + - class: "CheeseTypeRipeningDays" - class: "RipeningDays" - name: "production-planning-incoming" diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index 1863b0ef..3eea4f6f 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -39,7 +39,7 @@ yield productionPlan private def checkDeliverabilityOfOrder[M[_]: Monad: CanEmit[OrderDelayed]]( cheeseTypeRipeningDays: CheeseTypeRipeningDays, )(order: Order): M[Unit] = - val ripeningDays = order.orderedProducts.map(_.product.cheeseType).map(cheeseTypeRipeningDays(_)) + val ripeningDays = order.orderedProducts.map(_.product.cheeseType).map(cheeseTypeRipeningDays.value(_)) val isDelayed = ripeningDays.map(productionInTime(_, order.requiredBy)).exists(_ === OrderStatus.Delayed) when(isDelayed) { val deliveryDate = newDeliveryDate(RipeningDays(ripeningDays.map(_.days).reduceLeft(max))) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala index 8427c624..319f0870 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala @@ -54,7 +54,7 @@ final case class OrderedProduct(product: Product, quantity: Quantity) /** * Defines how many [[RipeningDays days of ripening]] are needed for a given [[CheeseType type of cheese]]. */ -type CheeseTypeRipeningDays = CheeseType => RipeningDays +final case class CheeseTypeRipeningDays(value: Map[CheeseType, RipeningDays]) /** * The number of days needed for the ripening process to be done. diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala index 82650d69..9f730a00 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala @@ -7,6 +7,7 @@ import dev.atedeg.mdm.productionplanning.IncomingEvent.* import dev.atedeg.mdm.productionplanning.OutgoingEvent.* import dev.atedeg.mdm.productionplanning.dto.OrderIDDTO.given import dev.atedeg.mdm.productionplanning.dto.QuantityDTO.given +import dev.atedeg.mdm.products.dto.CheeseTypeDTO.given import dev.atedeg.mdm.products.dto.ProductDTO import dev.atedeg.mdm.products.dto.ProductDTO.given import dev.atedeg.mdm.utils.serialization.DTO @@ -17,10 +18,18 @@ final case class NewOrderReceivedDTO(order: OrderDTO) final case class OrderDTO(orderID: String, requiredBy: String, orderedProducts: List[OrderedProductDTO]) final case class OrderedProductDTO(product: ProductDTO, quantity: Int) +private object QuantityDTO: + given DTO[Quantity, Int] = caseClassDTO + +private object OrderIDDTO: + given DTO[OrderID, String] = caseClassDTO + object NewOrderReceivedDTO: given DTO[NewOrderReceived, NewOrderReceivedDTO] = interCaseClassDTO private given DTO[Order, OrderDTO] = interCaseClassDTO - private given DTO[OrderedProduct, OrderedProductDTO] = interCaseClassDTO + +object OrderedProductDTO: + given DTO[OrderedProduct, OrderedProductDTO] = interCaseClassDTO final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) @@ -35,8 +44,7 @@ final case class OrderDelayedDTO(orderID: String, newDeliveryDate: String) object OrderDelayedDTO: given DTO[OrderDelayed, OrderDelayedDTO] = interCaseClassDTO -private object QuantityDTO: - given DTO[Quantity, Int] = caseClassDTO - -private object OrderIDDTO: - given DTO[OrderID, String] = caseClassDTO +final case class CheeseTypeRipeningDaysDTO(value: Map[String, Int]) +object CheeseTypeRipeningDaysDTO: + given DTO[CheeseTypeRipeningDays, CheeseTypeRipeningDaysDTO] = interCaseClassDTO + private given DTO[RipeningDays, Int] = caseClassDTO diff --git a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala index 9cd827d1..61a1b655 100644 --- a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala @@ -18,12 +18,14 @@ import dev.atedeg.mdm.utils.monads.* trait Mocks: - val cheeseTypeRipeningDays: CheeseTypeRipeningDays = Map( - CheeseType.Squacquerone -> RipeningDays(4), - CheeseType.Ricotta -> RipeningDays(0), - CheeseType.Caciotta -> RipeningDays(8), - CheeseType.Casatella -> RipeningDays(4), - CheeseType.Stracchino -> RipeningDays(5), + val cheeseTypeRipeningDays: CheeseTypeRipeningDays = CheeseTypeRipeningDays( + Map( + CheeseType.Squacquerone -> RipeningDays(4), + CheeseType.Ricotta -> RipeningDays(0), + CheeseType.Caciotta -> RipeningDays(8), + CheeseType.Casatella -> RipeningDays(4), + CheeseType.Stracchino -> RipeningDays(5), + ), ) val prodToProd1: ProductToProduce = ProductToProduce(Product.Caciotta(500), Quantity(5)) From 2f97b4803d95484b8e3cfde4ad261a379eefae4a Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 15:47:18 +0200 Subject: [PATCH 312/329] refactor: stock as a case class --- .../main/scala/dev/atedeg/mdm/productionplanning/Types.scala | 2 +- .../test/scala/dev.atedeg.mdm.productionplanning/Tests.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala index 319f0870..95d820e0 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala @@ -28,7 +28,7 @@ final case class Quantity(n: PositiveNumber) derives Plus, Times /** * It defines, for each [[Product product]], the [[StockedQuantity quantity in stock]]. */ -type Stock = Product => StockedQuantity +final case class Stock(stock: Map[Product, StockedQuantity]) /** * A quantity of a stocked [[Product product]], it may also be zero. diff --git a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala index 61a1b655..71ff136f 100644 --- a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala @@ -49,7 +49,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: val productsToProduce = NonEmptyList.of(prodToProd1, prodToProd2, prodToProd3) val previousProductionPlan = ProductionPlan(productsToProduce) And("an empty stock") - val stock: Stock = _ => StockedQuantity(0) + val stock: Stock = Stock(Map.empty) When("creating the production plan") val productionPlanCreation: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders) From 69c62b976772010398fb5a789394d44626c4232e Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 15:47:38 +0200 Subject: [PATCH 313/329] chore: add dtos --- .../mdm/productionplanning/dto/DTOs.scala | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala index 9f730a00..ea7b0285 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala @@ -7,6 +7,7 @@ import dev.atedeg.mdm.productionplanning.IncomingEvent.* import dev.atedeg.mdm.productionplanning.OutgoingEvent.* import dev.atedeg.mdm.productionplanning.dto.OrderIDDTO.given import dev.atedeg.mdm.productionplanning.dto.QuantityDTO.given +import dev.atedeg.mdm.products.Product import dev.atedeg.mdm.products.dto.CheeseTypeDTO.given import dev.atedeg.mdm.products.dto.ProductDTO import dev.atedeg.mdm.products.dto.ProductDTO.given @@ -14,9 +15,9 @@ import dev.atedeg.mdm.utils.serialization.DTO import dev.atedeg.mdm.utils.serialization.DTOGenerators.* import dev.atedeg.mdm.utils.serialization.DTOOps.* -final case class NewOrderReceivedDTO(order: OrderDTO) final case class OrderDTO(orderID: String, requiredBy: String, orderedProducts: List[OrderedProductDTO]) -final case class OrderedProductDTO(product: ProductDTO, quantity: Int) +object OrderDTO: + given DTO[Order, OrderDTO] = interCaseClassDTO private object QuantityDTO: given DTO[Quantity, Int] = caseClassDTO @@ -24,22 +25,25 @@ private object QuantityDTO: private object OrderIDDTO: given DTO[OrderID, String] = caseClassDTO +final case class NewOrderReceivedDTO(order: OrderDTO) object NewOrderReceivedDTO: given DTO[NewOrderReceived, NewOrderReceivedDTO] = interCaseClassDTO - private given DTO[Order, OrderDTO] = interCaseClassDTO +final case class OrderedProductDTO(product: ProductDTO, quantity: Int) object OrderedProductDTO: given DTO[OrderedProduct, OrderedProductDTO] = interCaseClassDTO final case class ProductionPlanReadyDTO(productionPlan: ProductionPlanDTO) -final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) final case class ProductToProduceDTO(product: ProductDTO, quantity: Int) -object ProductionPlanReadyDTO: - given DTO[ProductionPlanReady, ProductionPlanReadyDTO] = interCaseClassDTO +final case class ProductionPlanDTO(productsToProduce: List[ProductToProduceDTO]) +object ProductionPlanDTO: given DTO[ProductionPlan, ProductionPlanDTO] = interCaseClassDTO given DTO[ProductToProduce, ProductToProduceDTO] = interCaseClassDTO +object ProductionPlanReadyDTO: + given DTO[ProductionPlanReady, ProductionPlanReadyDTO] = interCaseClassDTO + final case class OrderDelayedDTO(orderID: String, newDeliveryDate: String) object OrderDelayedDTO: given DTO[OrderDelayed, OrderDelayedDTO] = interCaseClassDTO @@ -48,3 +52,8 @@ final case class CheeseTypeRipeningDaysDTO(value: Map[String, Int]) object CheeseTypeRipeningDaysDTO: given DTO[CheeseTypeRipeningDays, CheeseTypeRipeningDaysDTO] = interCaseClassDTO private given DTO[RipeningDays, Int] = caseClassDTO + +final case class StockDTO(value: Map[ProductDTO, Int]) +object StockDTO: + given DTO[Stock, StockDTO] = interCaseClassDTO + private given DTO[StockedQuantity, Int] = caseClassDTO From 2893046c2221774f577cd824e784853f77f31ea3 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 15:48:39 +0200 Subject: [PATCH 314/329] feat: add order received and production plan send handlers --- .../api/Configuration.scala | 11 +++++ .../mdm/productionplanning/api/Handlers.scala | 45 +++++++++++++++++++ .../mdm/productionplanning/api/acl/DTOs.scala | 28 ++++++++++++ .../api/emitters/Emitters.scala | 11 +++++ .../api/repositories/Repositories.scala | 13 ++++++ 5 files changed, 108 insertions(+) create mode 100644 production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala create mode 100644 production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala create mode 100644 production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala create mode 100644 production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/emitters/Emitters.scala create mode 100644 production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala new file mode 100644 index 00000000..1f24f755 --- /dev/null +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala @@ -0,0 +1,11 @@ +package dev.atedeg.mdm.productionplanning.api + +import dev.atedeg.mdm.productionplanning.api.emitters.* +import dev.atedeg.mdm.productionplanning.api.repositories.* + +final case class Configuration( + receivedOrderRepository: ReceivedOrderRepository, + ripeningDaysRepository: RipeningDaysRepository, + productionPlanReadyEmitter: ProductionPlanReadyEmitter, + orderDelayedEmitter: OrderDelayedEmitter, +) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala new file mode 100644 index 00000000..f933742c --- /dev/null +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala @@ -0,0 +1,45 @@ +package dev.atedeg.mdm.productionplanning.api + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* + +import dev.atedeg.mdm.productionplanning.{ createProductionPlan, Order, OrderedProduct, ProductionPlan } +import dev.atedeg.mdm.productionplanning.OutgoingEvent +import dev.atedeg.mdm.productionplanning.OutgoingEvent.{ OrderDelayed, ProductionPlanReady } +import dev.atedeg.mdm.productionplanning.api.Configuration.* +import dev.atedeg.mdm.productionplanning.api.acl.IncomingOrderDTO +import dev.atedeg.mdm.productionplanning.api.acl.toNewOrderReceivedDTO +import dev.atedeg.mdm.productionplanning.api.repositories.ReceivedOrderRepository +import dev.atedeg.mdm.productionplanning.dto.* +import dev.atedeg.mdm.productionplanning.dto.NewOrderReceivedDTO.given +import dev.atedeg.mdm.productionplanning.dto.OrderDTO.given +import dev.atedeg.mdm.productionplanning.dto.OrderedProductDTO.given +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +def handleOrderReceived[M[_]: Monad: LiftIO: CanRead[ReceivedOrderRepository]: CanRaise[String]]( + incomingOrderDTO: IncomingOrderDTO, +): M[Unit] = + for + newOrder <- validate(incomingOrderDTO.toNewOrderReceivedDTO) + _ <- readState >>= (_.saveNewOrder(newOrder.order.orderedProducts.toDTO[List[OrderedProductDTO]])) + yield () + +def handleProductionPlanSend[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]: M[Unit] = + for + config <- readState + stock <- getCurrentStock >>= validate + cheeseTypeRipeningDays <- config.ripeningDaysRepository.getRipeningDays >>= validate + previousProductionPlan <- getPreviousYearProductionPlan >>= validate + orders <- config.receivedOrderRepository.getOrders >>= validate[List[OrderDTO], List[Order], M] + action: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = + createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders) + (events1, events2, _) = action.execute + _ <- events2.map(_.toDTO[OrderDelayedDTO]).traverse(config.orderDelayedEmitter.emit) + _ <- events1.map(_.toDTO[ProductionPlanReadyDTO]).traverse(config.productionPlanReadyEmitter.emit) + yield () + +private def getCurrentStock[M[_]: Monad: LiftIO]: M[StockDTO] = ??? +private def getPreviousYearProductionPlan[M[_]: Monad: LiftIO]: M[ProductionPlanDTO] = ??? diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala new file mode 100644 index 00000000..1aa916a8 --- /dev/null +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala @@ -0,0 +1,28 @@ +package dev.atedeg.mdm.productionplanning.api.acl + +import java.time.LocalDate + +import cats.data.NonEmptyList + +import dev.atedeg.mdm.productionplanning.dto.* +import dev.atedeg.mdm.products.dto.ProductDTO + +final case class IncomingOrderLineDTO(quantity: Int, product: ProductDTO) +final case class CustomerDTO(code: String, name: String, vatNumber: String) +final case class LocationDTO(latitude: Double, longitude: Double) +final case class IncomingOrderDTO( + id: String, + orderLines: List[IncomingOrderLineDTO], + customer: CustomerDTO, + deliveryDate: String, + deliveryLocation: LocationDTO, +) + +extension (iol: IncomingOrderLineDTO) + def toOrderedProductDTO: OrderedProductDTO = OrderedProductDTO(iol.product, iol.quantity) + +extension (io: IncomingOrderDTO) + def toNewOrderDTO: OrderDTO = + OrderDTO(io.id, io.deliveryDate, io.orderLines.map(_.toOrderedProductDTO)) + +extension (io: IncomingOrderDTO) def toNewOrderReceivedDTO: NewOrderReceivedDTO = NewOrderReceivedDTO(io.toNewOrderDTO) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/emitters/Emitters.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/emitters/Emitters.scala new file mode 100644 index 00000000..5f39b816 --- /dev/null +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/emitters/Emitters.scala @@ -0,0 +1,11 @@ +package dev.atedeg.mdm.productionplanning.api.emitters + +import cats.Monad +import cats.effect.LiftIO +import dev.atedeg.mdm.productionplanning.dto.{OrderDelayedDTO, ProductionPlanReadyDTO} + +trait ProductionPlanReadyEmitter: + def emit[M[_]: Monad: LiftIO](productionPlanReady: ProductionPlanReadyDTO): M[Unit] + +trait OrderDelayedEmitter: + def emit[M[_]: Monad: LiftIO](orderDelayed: OrderDelayedDTO): M[Unit] diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala new file mode 100644 index 00000000..495f03e0 --- /dev/null +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala @@ -0,0 +1,13 @@ +package dev.atedeg.mdm.productionplanning.api.repositories + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.productionplanning.dto.{ CheeseTypeRipeningDaysDTO, OrderDTO, OrderedProductDTO } + +trait ReceivedOrderRepository: + def saveNewOrder[M[_]: Monad: LiftIO](orderedProducts: List[OrderedProductDTO]): M[Unit] + def getOrders[M[_]: Monad: LiftIO]: M[List[OrderDTO]] + +trait RipeningDaysRepository: + def getRipeningDays[M[_]: Monad: LiftIO]: M[CheeseTypeRipeningDaysDTO] From 9b4c9c559d893851760a16496b6ba7f539cad0ab Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 16:22:42 +0200 Subject: [PATCH 315/329] feat: add desired stock to dto stock dto in prod plan acl --- .../dev/atedeg/mdm/productionplanning/api/Handlers.scala | 6 +++--- .../dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala index f933742c..8cb9d1af 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala @@ -27,10 +27,10 @@ def handleOrderReceived[M[_]: Monad: LiftIO: CanRead[ReceivedOrderRepository]: C _ <- readState >>= (_.saveNewOrder(newOrder.order.orderedProducts.toDTO[List[OrderedProductDTO]])) yield () -def handleProductionPlanSend[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]: M[Unit] = +def handleSendProductionPlan[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]: M[Unit] = for config <- readState - stock <- getCurrentStock >>= validate + stock <- getMissingProductsFromStock >>= validate cheeseTypeRipeningDays <- config.ripeningDaysRepository.getRipeningDays >>= validate previousProductionPlan <- getPreviousYearProductionPlan >>= validate orders <- config.receivedOrderRepository.getOrders >>= validate[List[OrderDTO], List[Order], M] @@ -41,5 +41,5 @@ def handleProductionPlanSend[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRai _ <- events1.map(_.toDTO[ProductionPlanReadyDTO]).traverse(config.productionPlanReadyEmitter.emit) yield () -private def getCurrentStock[M[_]: Monad: LiftIO]: M[StockDTO] = ??? +private def getMissingProductsFromStock[M[_]: Monad: LiftIO]: M[StockDTO] = ??? private def getPreviousYearProductionPlan[M[_]: Monad: LiftIO]: M[ProductionPlanDTO] = ??? diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala index 1aa916a8..3af8cb9f 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala @@ -26,3 +26,6 @@ extension (io: IncomingOrderDTO) OrderDTO(io.id, io.deliveryDate, io.orderLines.map(_.toOrderedProductDTO)) extension (io: IncomingOrderDTO) def toNewOrderReceivedDTO: NewOrderReceivedDTO = NewOrderReceivedDTO(io.toNewOrderDTO) + +final case class DesiredStockDTO(desiredStock: List[(ProductDTO, Int)]) +extension (ds: DesiredStockDTO) def toMissingProductsInStock: StockDTO = StockDTO(ds.desiredStock.toMap) From facbaba0a5425d995487316220dfd0088262efe8 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 17:14:37 +0200 Subject: [PATCH 316/329] fix: in handleOrderReceived use order instead of ordered product --- .../dev/atedeg/mdm/productionplanning/api/Handlers.scala | 3 +-- .../productionplanning/api/repositories/Repositories.scala | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala index 8cb9d1af..fc74894f 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala @@ -14,7 +14,6 @@ import dev.atedeg.mdm.productionplanning.api.repositories.ReceivedOrderRepositor import dev.atedeg.mdm.productionplanning.dto.* import dev.atedeg.mdm.productionplanning.dto.NewOrderReceivedDTO.given import dev.atedeg.mdm.productionplanning.dto.OrderDTO.given -import dev.atedeg.mdm.productionplanning.dto.OrderedProductDTO.given import dev.atedeg.mdm.utils.monads.* import dev.atedeg.mdm.utils.serialization.DTO import dev.atedeg.mdm.utils.serialization.DTOOps.* @@ -24,7 +23,7 @@ def handleOrderReceived[M[_]: Monad: LiftIO: CanRead[ReceivedOrderRepository]: C ): M[Unit] = for newOrder <- validate(incomingOrderDTO.toNewOrderReceivedDTO) - _ <- readState >>= (_.saveNewOrder(newOrder.order.orderedProducts.toDTO[List[OrderedProductDTO]])) + _ <- readState >>= (_.saveNewOrder(newOrder.order.toDTO[OrderDTO])) yield () def handleSendProductionPlan[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]: M[Unit] = diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala index 495f03e0..2c4bcc31 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala @@ -3,10 +3,10 @@ package dev.atedeg.mdm.productionplanning.api.repositories import cats.Monad import cats.effect.LiftIO -import dev.atedeg.mdm.productionplanning.dto.{ CheeseTypeRipeningDaysDTO, OrderDTO, OrderedProductDTO } +import dev.atedeg.mdm.productionplanning.dto.{ CheeseTypeRipeningDaysDTO, OrderDTO } trait ReceivedOrderRepository: - def saveNewOrder[M[_]: Monad: LiftIO](orderedProducts: List[OrderedProductDTO]): M[Unit] + def saveNewOrder[M[_]: Monad: LiftIO](order: OrderDTO): M[Unit] def getOrders[M[_]: Monad: LiftIO]: M[List[OrderDTO]] trait RipeningDaysRepository: From aad6bfffabedd03b771e2cb4d7d837fbebde09cb Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 17:15:39 +0200 Subject: [PATCH 317/329] fix: date dto in prod planning acl --- .../dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala index 3af8cb9f..2b60b2ee 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala @@ -1,11 +1,13 @@ package dev.atedeg.mdm.productionplanning.api.acl -import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import cats.data.NonEmptyList import dev.atedeg.mdm.productionplanning.dto.* import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.utils.serialization.DTOOps.toDTO final case class IncomingOrderLineDTO(quantity: Int, product: ProductDTO) final case class CustomerDTO(code: String, name: String, vatNumber: String) @@ -23,7 +25,8 @@ extension (iol: IncomingOrderLineDTO) extension (io: IncomingOrderDTO) def toNewOrderDTO: OrderDTO = - OrderDTO(io.id, io.deliveryDate, io.orderLines.map(_.toOrderedProductDTO)) + val date = LocalDateTime.parse(io.deliveryDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toLocalDate + OrderDTO(io.id, date.toDTO[String], io.orderLines.map(_.toOrderedProductDTO)) extension (io: IncomingOrderDTO) def toNewOrderReceivedDTO: NewOrderReceivedDTO = NewOrderReceivedDTO(io.toNewOrderDTO) From d6bf5ca410df2c6edc711504d35227ec5883c616 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 17:16:12 +0200 Subject: [PATCH 318/329] test: add prod planning handler tests --- .../productionplanning/api/HandlersTest.scala | 44 +++++++++++++++++++ .../mdm/productionplanning/types}/Tests.scala | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/api/HandlersTest.scala rename production-planning/src/test/scala/{dev.atedeg.mdm.productionplanning => dev/atedeg/mdm/productionplanning/types}/Tests.scala (98%) diff --git a/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/api/HandlersTest.scala b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/api/HandlersTest.scala new file mode 100644 index 00000000..3816d7fb --- /dev/null +++ b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/api/HandlersTest.scala @@ -0,0 +1,44 @@ +package dev.atedeg.mdm.productionplanning.api + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.UUID + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import dev.atedeg.mdm.productionplanning.api.acl.{ CustomerDTO, IncomingOrderDTO, IncomingOrderLineDTO, LocationDTO } +import dev.atedeg.mdm.productionplanning.api.repositories.ReceivedOrderRepository +import dev.atedeg.mdm.productionplanning.dto.{ OrderDTO, OrderedProductDTO } +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTOOps.toDTO + +trait Mocks: + @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) + var orders: List[OrderDTO] = Nil + val receivedOrderRepository: ReceivedOrderRepository = new ReceivedOrderRepository: + override def saveNewOrder[M[_]: Monad: LiftIO](order: OrderDTO): M[Unit] = + orders = order :: orders + ().pure + + override def getOrders[M[_]: Monad: LiftIO]: M[List[OrderDTO]] = ??? + +class HandlersTest extends AnyWordSpec, Matchers, Mocks: + "The `handleOrderReceived`" should { + "save the new order in the DB" in { + val product = ProductDTO("caciotta", 500) + val date = LocalDateTime.now().toDTO[String] + val orderLines = List(IncomingOrderLineDTO(10, product)) + val id = UUID.randomUUID().toDTO[String] + val incomingOrder = + IncomingOrderDTO(id, orderLines, CustomerDTO(id, "Pippo", "IT12345678910"), date, LocationDTO(12.6, 44.6)) + val action: ServerAction[ReceivedOrderRepository, String, Unit] = handleOrderReceived(incomingOrder) + action.unsafeExecute(receivedOrderRepository) + val localDate = LocalDateTime.parse(date, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toLocalDate + orders should contain(OrderDTO(id, localDate.toDTO[String], List(OrderedProductDTO(product, 10)))) + } + } diff --git a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/types/Tests.scala similarity index 98% rename from production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala rename to production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/types/Tests.scala index 71ff136f..684449e2 100644 --- a/production-planning/src/test/scala/dev.atedeg.mdm.productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/types/Tests.scala @@ -1,4 +1,4 @@ -package dev.atedeg.mdm.productionplanning +package dev.atedeg.mdm.productionplanning.types import java.time.LocalDate import java.util.UUID From fab4411b0ae6e06bf5c80f93e10f791ea00bc911 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 17:18:59 +0200 Subject: [PATCH 319/329] style: scalafmt --- .../api/Configuration.scala | 22 ++--- .../mdm/productionplanning/api/Handlers.scala | 88 +++++++++---------- .../api/emitters/Emitters.scala | 23 ++--- .../productionplanning/api/HandlersTest.scala | 88 +++++++++---------- 4 files changed, 111 insertions(+), 110 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala index 1f24f755..f399637d 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala @@ -1,11 +1,11 @@ -package dev.atedeg.mdm.productionplanning.api - -import dev.atedeg.mdm.productionplanning.api.emitters.* -import dev.atedeg.mdm.productionplanning.api.repositories.* - -final case class Configuration( - receivedOrderRepository: ReceivedOrderRepository, - ripeningDaysRepository: RipeningDaysRepository, - productionPlanReadyEmitter: ProductionPlanReadyEmitter, - orderDelayedEmitter: OrderDelayedEmitter, -) +package dev.atedeg.mdm.productionplanning.api + +import dev.atedeg.mdm.productionplanning.api.emitters.* +import dev.atedeg.mdm.productionplanning.api.repositories.* + +final case class Configuration( + receivedOrderRepository: ReceivedOrderRepository, + ripeningDaysRepository: RipeningDaysRepository, + productionPlanReadyEmitter: ProductionPlanReadyEmitter, + orderDelayedEmitter: OrderDelayedEmitter, +) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala index fc74894f..dac1d26e 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala @@ -1,44 +1,44 @@ -package dev.atedeg.mdm.productionplanning.api - -import cats.Monad -import cats.effect.LiftIO -import cats.syntax.all.* - -import dev.atedeg.mdm.productionplanning.{ createProductionPlan, Order, OrderedProduct, ProductionPlan } -import dev.atedeg.mdm.productionplanning.OutgoingEvent -import dev.atedeg.mdm.productionplanning.OutgoingEvent.{ OrderDelayed, ProductionPlanReady } -import dev.atedeg.mdm.productionplanning.api.Configuration.* -import dev.atedeg.mdm.productionplanning.api.acl.IncomingOrderDTO -import dev.atedeg.mdm.productionplanning.api.acl.toNewOrderReceivedDTO -import dev.atedeg.mdm.productionplanning.api.repositories.ReceivedOrderRepository -import dev.atedeg.mdm.productionplanning.dto.* -import dev.atedeg.mdm.productionplanning.dto.NewOrderReceivedDTO.given -import dev.atedeg.mdm.productionplanning.dto.OrderDTO.given -import dev.atedeg.mdm.utils.monads.* -import dev.atedeg.mdm.utils.serialization.DTO -import dev.atedeg.mdm.utils.serialization.DTOOps.* - -def handleOrderReceived[M[_]: Monad: LiftIO: CanRead[ReceivedOrderRepository]: CanRaise[String]]( - incomingOrderDTO: IncomingOrderDTO, -): M[Unit] = - for - newOrder <- validate(incomingOrderDTO.toNewOrderReceivedDTO) - _ <- readState >>= (_.saveNewOrder(newOrder.order.toDTO[OrderDTO])) - yield () - -def handleSendProductionPlan[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]: M[Unit] = - for - config <- readState - stock <- getMissingProductsFromStock >>= validate - cheeseTypeRipeningDays <- config.ripeningDaysRepository.getRipeningDays >>= validate - previousProductionPlan <- getPreviousYearProductionPlan >>= validate - orders <- config.receivedOrderRepository.getOrders >>= validate[List[OrderDTO], List[Order], M] - action: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = - createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders) - (events1, events2, _) = action.execute - _ <- events2.map(_.toDTO[OrderDelayedDTO]).traverse(config.orderDelayedEmitter.emit) - _ <- events1.map(_.toDTO[ProductionPlanReadyDTO]).traverse(config.productionPlanReadyEmitter.emit) - yield () - -private def getMissingProductsFromStock[M[_]: Monad: LiftIO]: M[StockDTO] = ??? -private def getPreviousYearProductionPlan[M[_]: Monad: LiftIO]: M[ProductionPlanDTO] = ??? +package dev.atedeg.mdm.productionplanning.api + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* + +import dev.atedeg.mdm.productionplanning.{ createProductionPlan, Order, OrderedProduct, ProductionPlan } +import dev.atedeg.mdm.productionplanning.OutgoingEvent +import dev.atedeg.mdm.productionplanning.OutgoingEvent.{ OrderDelayed, ProductionPlanReady } +import dev.atedeg.mdm.productionplanning.api.Configuration.* +import dev.atedeg.mdm.productionplanning.api.acl.IncomingOrderDTO +import dev.atedeg.mdm.productionplanning.api.acl.toNewOrderReceivedDTO +import dev.atedeg.mdm.productionplanning.api.repositories.ReceivedOrderRepository +import dev.atedeg.mdm.productionplanning.dto.* +import dev.atedeg.mdm.productionplanning.dto.NewOrderReceivedDTO.given +import dev.atedeg.mdm.productionplanning.dto.OrderDTO.given +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTO +import dev.atedeg.mdm.utils.serialization.DTOOps.* + +def handleOrderReceived[M[_]: Monad: LiftIO: CanRead[ReceivedOrderRepository]: CanRaise[String]]( + incomingOrderDTO: IncomingOrderDTO, +): M[Unit] = + for + newOrder <- validate(incomingOrderDTO.toNewOrderReceivedDTO) + _ <- readState >>= (_.saveNewOrder(newOrder.order.toDTO[OrderDTO])) + yield () + +def handleSendProductionPlan[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]: M[Unit] = + for + config <- readState + stock <- getMissingProductsFromStock >>= validate + cheeseTypeRipeningDays <- config.ripeningDaysRepository.getRipeningDays >>= validate + previousProductionPlan <- getPreviousYearProductionPlan >>= validate + orders <- config.receivedOrderRepository.getOrders >>= validate[List[OrderDTO], List[Order], M] + action: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = + createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders) + (events1, events2, _) = action.execute + _ <- events2.map(_.toDTO[OrderDelayedDTO]).traverse(config.orderDelayedEmitter.emit) + _ <- events1.map(_.toDTO[ProductionPlanReadyDTO]).traverse(config.productionPlanReadyEmitter.emit) + yield () + +private def getMissingProductsFromStock[M[_]: Monad: LiftIO]: M[StockDTO] = ??? +private def getPreviousYearProductionPlan[M[_]: Monad: LiftIO]: M[ProductionPlanDTO] = ??? diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/emitters/Emitters.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/emitters/Emitters.scala index 5f39b816..3cbfa514 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/emitters/Emitters.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/emitters/Emitters.scala @@ -1,11 +1,12 @@ -package dev.atedeg.mdm.productionplanning.api.emitters - -import cats.Monad -import cats.effect.LiftIO -import dev.atedeg.mdm.productionplanning.dto.{OrderDelayedDTO, ProductionPlanReadyDTO} - -trait ProductionPlanReadyEmitter: - def emit[M[_]: Monad: LiftIO](productionPlanReady: ProductionPlanReadyDTO): M[Unit] - -trait OrderDelayedEmitter: - def emit[M[_]: Monad: LiftIO](orderDelayed: OrderDelayedDTO): M[Unit] +package dev.atedeg.mdm.productionplanning.api.emitters + +import cats.Monad +import cats.effect.LiftIO + +import dev.atedeg.mdm.productionplanning.dto.{ OrderDelayedDTO, ProductionPlanReadyDTO } + +trait ProductionPlanReadyEmitter: + def emit[M[_]: Monad: LiftIO](productionPlanReady: ProductionPlanReadyDTO): M[Unit] + +trait OrderDelayedEmitter: + def emit[M[_]: Monad: LiftIO](orderDelayed: OrderDelayedDTO): M[Unit] diff --git a/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/api/HandlersTest.scala b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/api/HandlersTest.scala index 3816d7fb..1862bd0f 100644 --- a/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/api/HandlersTest.scala +++ b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/api/HandlersTest.scala @@ -1,44 +1,44 @@ -package dev.atedeg.mdm.productionplanning.api - -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.UUID - -import cats.Monad -import cats.effect.LiftIO -import cats.syntax.all.* -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import dev.atedeg.mdm.productionplanning.api.acl.{ CustomerDTO, IncomingOrderDTO, IncomingOrderLineDTO, LocationDTO } -import dev.atedeg.mdm.productionplanning.api.repositories.ReceivedOrderRepository -import dev.atedeg.mdm.productionplanning.dto.{ OrderDTO, OrderedProductDTO } -import dev.atedeg.mdm.products.dto.ProductDTO -import dev.atedeg.mdm.utils.monads.* -import dev.atedeg.mdm.utils.serialization.DTOOps.toDTO - -trait Mocks: - @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) - var orders: List[OrderDTO] = Nil - val receivedOrderRepository: ReceivedOrderRepository = new ReceivedOrderRepository: - override def saveNewOrder[M[_]: Monad: LiftIO](order: OrderDTO): M[Unit] = - orders = order :: orders - ().pure - - override def getOrders[M[_]: Monad: LiftIO]: M[List[OrderDTO]] = ??? - -class HandlersTest extends AnyWordSpec, Matchers, Mocks: - "The `handleOrderReceived`" should { - "save the new order in the DB" in { - val product = ProductDTO("caciotta", 500) - val date = LocalDateTime.now().toDTO[String] - val orderLines = List(IncomingOrderLineDTO(10, product)) - val id = UUID.randomUUID().toDTO[String] - val incomingOrder = - IncomingOrderDTO(id, orderLines, CustomerDTO(id, "Pippo", "IT12345678910"), date, LocationDTO(12.6, 44.6)) - val action: ServerAction[ReceivedOrderRepository, String, Unit] = handleOrderReceived(incomingOrder) - action.unsafeExecute(receivedOrderRepository) - val localDate = LocalDateTime.parse(date, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toLocalDate - orders should contain(OrderDTO(id, localDate.toDTO[String], List(OrderedProductDTO(product, 10)))) - } - } +package dev.atedeg.mdm.productionplanning.api + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.UUID + +import cats.Monad +import cats.effect.LiftIO +import cats.syntax.all.* +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import dev.atedeg.mdm.productionplanning.api.acl.{ CustomerDTO, IncomingOrderDTO, IncomingOrderLineDTO, LocationDTO } +import dev.atedeg.mdm.productionplanning.api.repositories.ReceivedOrderRepository +import dev.atedeg.mdm.productionplanning.dto.{ OrderDTO, OrderedProductDTO } +import dev.atedeg.mdm.products.dto.ProductDTO +import dev.atedeg.mdm.utils.monads.* +import dev.atedeg.mdm.utils.serialization.DTOOps.toDTO + +trait Mocks: + @SuppressWarnings(Array("org.wartremover.warts.Var", "scalafix:DisableSyntax.var")) + var orders: List[OrderDTO] = Nil + val receivedOrderRepository: ReceivedOrderRepository = new ReceivedOrderRepository: + override def saveNewOrder[M[_]: Monad: LiftIO](order: OrderDTO): M[Unit] = + orders = order :: orders + ().pure + + override def getOrders[M[_]: Monad: LiftIO]: M[List[OrderDTO]] = ??? + +class HandlersTest extends AnyWordSpec, Matchers, Mocks: + "The `handleOrderReceived`" should { + "save the new order in the DB" in { + val product = ProductDTO("caciotta", 500) + val date = LocalDateTime.now().toDTO[String] + val orderLines = List(IncomingOrderLineDTO(10, product)) + val id = UUID.randomUUID().toDTO[String] + val incomingOrder = + IncomingOrderDTO(id, orderLines, CustomerDTO(id, "Pippo", "IT12345678910"), date, LocationDTO(12.6, 44.6)) + val action: ServerAction[ReceivedOrderRepository, String, Unit] = handleOrderReceived(incomingOrder) + action.unsafeExecute(receivedOrderRepository) + val localDate = LocalDateTime.parse(date, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toLocalDate + orders should contain(OrderDTO(id, localDate.toDTO[String], List(OrderedProductDTO(product, 10)))) + } + } From 3740f91dd846f0b512512dfe4b8fa09ddbdf0687 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 17:26:10 +0200 Subject: [PATCH 320/329] fix: progate date error in prod plan acl --- .../dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala index 2b60b2ee..8a4132f9 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala @@ -2,6 +2,7 @@ package dev.atedeg.mdm.productionplanning.api.acl import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import scala.util.Try import cats.data.NonEmptyList @@ -25,8 +26,10 @@ extension (iol: IncomingOrderLineDTO) extension (io: IncomingOrderDTO) def toNewOrderDTO: OrderDTO = - val date = LocalDateTime.parse(io.deliveryDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toLocalDate - OrderDTO(io.id, date.toDTO[String], io.orderLines.map(_.toOrderedProductDTO)) + val date = + Try(LocalDateTime.parse(io.deliveryDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toLocalDate.toDTO[String]) + .getOrElse(io.deliveryDate) + OrderDTO(io.id, date, io.orderLines.map(_.toOrderedProductDTO)) extension (io: IncomingOrderDTO) def toNewOrderReceivedDTO: NewOrderReceivedDTO = NewOrderReceivedDTO(io.toNewOrderDTO) From bcaf8c1eeaf75b392022ab03b1b699945f981689 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 17:54:00 +0200 Subject: [PATCH 321/329] refactor: tests packages --- .../dev/atedeg/mdm/milkplanning/{types => }/ActionsTest.scala | 2 +- .../dev/atedeg/mdm/productionplanning/{types => }/Tests.scala | 2 +- .../test/scala/dev/atedeg/mdm/stocking/{types => }/Tests.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/{types => }/ActionsTest.scala (98%) rename production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/{types => }/Tests.scala (98%) rename stocking/src/test/scala/dev/atedeg/mdm/stocking/{types => }/Tests.scala (99%) diff --git a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/ActionsTest.scala similarity index 98% rename from milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala rename to milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/ActionsTest.scala index 20184404..75833185 100644 --- a/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/types/ActionsTest.scala +++ b/milk-planning/src/test/scala/dev/atedeg/mdm/milkplanning/ActionsTest.scala @@ -1,4 +1,4 @@ -package dev.atedeg.mdm.milkplanning.types +package dev.atedeg.mdm.milkplanning import java.time.LocalDateTime diff --git a/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/types/Tests.scala b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala similarity index 98% rename from production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/types/Tests.scala rename to production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala index 684449e2..71ff136f 100644 --- a/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/types/Tests.scala +++ b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala @@ -1,4 +1,4 @@ -package dev.atedeg.mdm.productionplanning.types +package dev.atedeg.mdm.productionplanning import java.time.LocalDate import java.util.UUID diff --git a/stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala similarity index 99% rename from stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala rename to stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala index 558f5c7e..88da7e67 100644 --- a/stocking/src/test/scala/dev/atedeg/mdm/stocking/types/Tests.scala +++ b/stocking/src/test/scala/dev/atedeg/mdm/stocking/Tests.scala @@ -1,4 +1,4 @@ -package dev.atedeg.mdm.stocking.types +package dev.atedeg.mdm.stocking import java.util.UUID From c6fb1da887085adf1586b73dec19dfa61000a30d Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 18:19:28 +0200 Subject: [PATCH 322/329] refactor: stock becomes missing products --- .ubidoc.yml | 4 ++-- .../dev/atedeg/mdm/productionplanning/Actions.scala | 11 ++++++++--- .../dev/atedeg/mdm/productionplanning/Types.scala | 8 ++++---- .../atedeg/mdm/productionplanning/api/Handlers.scala | 6 +++--- .../atedeg/mdm/productionplanning/api/acl/DTOs.scala | 3 ++- .../dev/atedeg/mdm/productionplanning/dto/DTOs.scala | 8 ++++---- .../dev/atedeg/mdm/productionplanning/Tests.scala | 10 +++++----- 7 files changed, 28 insertions(+), 22 deletions(-) diff --git a/.ubidoc.yml b/.ubidoc.yml index 4673b485..b83df581 100644 --- a/.ubidoc.yml +++ b/.ubidoc.yml @@ -91,8 +91,8 @@ tables: - class: "ProductionPlan" - class: "ProductToProduce" - class: "Quantity" - - type: "Stock" - - class: "StockedQuantity" + - class: "MissingProducts" + - class: "MissingQuantity" - class: "Order" - class: "OrderedProduct" - class: "CheeseTypeRipeningDays" diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index 3eea4f6f..22fc8d1a 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -24,14 +24,19 @@ import dev.atedeg.mdm.utils.monads.* * to satisfy the order by the required date, it emits an [[OrderDelayed order delayed]] event. */ def createProductionPlan[M[_]: Monad: Emits[ProductionPlanReady]: CanEmit[OrderDelayed]]( - stock: Stock, + missingProducts: MissingProducts, cheeseTypeRipeningDays: CheeseTypeRipeningDays, )( previousProductionPlan: ProductionPlan, orders: List[Order], ): M[ProductionPlan] = for _ <- orders.traverse(checkDeliverabilityOfOrder(cheeseTypeRipeningDays)) - productsToProduce = magicAIProductsToProduceEstimator(orders, previousProductionPlan, cheeseTypeRipeningDays, stock) + productsToProduce = magicAIProductsToProduceEstimator( + orders, + previousProductionPlan, + cheeseTypeRipeningDays, + missingProducts, + ) productionPlan = ProductionPlan(productsToProduce) _ <- emit(ProductionPlanReady(productionPlan): ProductionPlanReady) yield productionPlan @@ -50,7 +55,7 @@ private def magicAIProductsToProduceEstimator( orders: List[Order], previousProductionPlan: ProductionPlan, cheeseTypeRipeningDays: CheeseTypeRipeningDays, - stock: Stock, + missingProducts: MissingProducts, ): NonEmptyList[ProductToProduce] = // This is a mock, ideally that would be estimated by an intelligent agent or some heuristics. NonEmptyList.of(ProductToProduce(Product.Caciotta(500), Quantity(5))) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala index 95d820e0..8215289e 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Types.scala @@ -26,14 +26,14 @@ final case class ProductToProduce(product: Product, quantity: Quantity) final case class Quantity(n: PositiveNumber) derives Plus, Times /** - * It defines, for each [[Product product]], the [[StockedQuantity quantity in stock]]. + * The [[Product products]] missing from the stock in a given [[MissingQuantity quantity]]. */ -final case class Stock(stock: Map[Product, StockedQuantity]) +final case class MissingProducts(missingProducts: Map[Product, MissingQuantity]) /** - * A quantity of a stocked [[Product product]], it may also be zero. + * A quantity of a missing [[Product product]]. */ -final case class StockedQuantity(n: NonNegativeNumber) +final case class MissingQuantity(n: NonNegativeNumber) /** * A set of requested [[Product product]] with the [[Quantity quantities]] that have to be produced by the given diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala index dac1d26e..f0a6ebb7 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala @@ -29,16 +29,16 @@ def handleOrderReceived[M[_]: Monad: LiftIO: CanRead[ReceivedOrderRepository]: C def handleSendProductionPlan[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRaise[String]]: M[Unit] = for config <- readState - stock <- getMissingProductsFromStock >>= validate + missingProducts <- getMissingProductsFromStock >>= validate cheeseTypeRipeningDays <- config.ripeningDaysRepository.getRipeningDays >>= validate previousProductionPlan <- getPreviousYearProductionPlan >>= validate orders <- config.receivedOrderRepository.getOrders >>= validate[List[OrderDTO], List[Order], M] action: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = - createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders) + createProductionPlan(missingProducts, cheeseTypeRipeningDays)(previousProductionPlan, orders) (events1, events2, _) = action.execute _ <- events2.map(_.toDTO[OrderDelayedDTO]).traverse(config.orderDelayedEmitter.emit) _ <- events1.map(_.toDTO[ProductionPlanReadyDTO]).traverse(config.productionPlanReadyEmitter.emit) yield () -private def getMissingProductsFromStock[M[_]: Monad: LiftIO]: M[StockDTO] = ??? +private def getMissingProductsFromStock[M[_]: Monad: LiftIO]: M[MissingProductsDTO] = ??? private def getPreviousYearProductionPlan[M[_]: Monad: LiftIO]: M[ProductionPlanDTO] = ??? diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala index 8a4132f9..a6bc6963 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala @@ -34,4 +34,5 @@ extension (io: IncomingOrderDTO) extension (io: IncomingOrderDTO) def toNewOrderReceivedDTO: NewOrderReceivedDTO = NewOrderReceivedDTO(io.toNewOrderDTO) final case class DesiredStockDTO(desiredStock: List[(ProductDTO, Int)]) -extension (ds: DesiredStockDTO) def toMissingProductsInStock: StockDTO = StockDTO(ds.desiredStock.toMap) +extension (ds: DesiredStockDTO) + def toMissingProductsInStock: MissingProductsDTO = MissingProductsDTO(ds.desiredStock.toMap) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala index ea7b0285..ce74d57c 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/dto/DTOs.scala @@ -53,7 +53,7 @@ object CheeseTypeRipeningDaysDTO: given DTO[CheeseTypeRipeningDays, CheeseTypeRipeningDaysDTO] = interCaseClassDTO private given DTO[RipeningDays, Int] = caseClassDTO -final case class StockDTO(value: Map[ProductDTO, Int]) -object StockDTO: - given DTO[Stock, StockDTO] = interCaseClassDTO - private given DTO[StockedQuantity, Int] = caseClassDTO +final case class MissingProductsDTO(missingProducts: Map[ProductDTO, Int]) +object MissingProductsDTO: + given DTO[MissingProducts, MissingProductsDTO] = interCaseClassDTO + private given DTO[MissingQuantity, Int] = caseClassDTO diff --git a/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala index 71ff136f..3ac010c9 100644 --- a/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala @@ -48,11 +48,11 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: And("the production plan of the previous year for the same day") val productsToProduce = NonEmptyList.of(prodToProd1, prodToProd2, prodToProd3) val previousProductionPlan = ProductionPlan(productsToProduce) - And("an empty stock") - val stock: Stock = Stock(Map.empty) + And("no missing products") + val missingProducts: MissingProducts = MissingProducts(Map.empty) When("creating the production plan") val productionPlanCreation: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = - createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders) + createProductionPlan(missingProducts, cheeseTypeRipeningDays)(previousProductionPlan, orders) val (events1, events2, productionPlan) = productionPlanCreation.execute Then("the result is the same production plan emitted in the event") events1 should not be empty @@ -73,10 +73,10 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: Order(order3ID, requiredBy2, orderedProducts3), ) And("the production plan of the previous year for the same day") - And("an empty stock") + And("no missing products") When("creating the production plan") val productionPlanCreation2: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = - createProductionPlan(stock, cheeseTypeRipeningDays)(previousProductionPlan, orders2) + createProductionPlan(missingProducts, cheeseTypeRipeningDays)(previousProductionPlan, orders2) val (e1, e2, pp) = productionPlanCreation2.execute Then("Should delay the order containing the Cacciotta product") e1 should not be empty From 3457eaae70eb52288aaaf5df77593e2435c8c288 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 18:40:42 +0200 Subject: [PATCH 323/329] feat: save the production plan --- .../scala/dev/atedeg/mdm/productionplanning/Actions.scala | 4 ++-- .../atedeg/mdm/productionplanning/api/Configuration.scala | 1 + .../dev/atedeg/mdm/productionplanning/api/Handlers.scala | 7 ++++--- .../dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala | 3 +-- .../productionplanning/api/repositories/Repositories.scala | 6 +++++- .../scala/dev/atedeg/mdm/productionplanning/Tests.scala | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala index 22fc8d1a..ed0be0a3 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/Actions.scala @@ -27,7 +27,7 @@ def createProductionPlan[M[_]: Monad: Emits[ProductionPlanReady]: CanEmit[OrderD missingProducts: MissingProducts, cheeseTypeRipeningDays: CheeseTypeRipeningDays, )( - previousProductionPlan: ProductionPlan, + previousProductionPlan: Option[ProductionPlan], orders: List[Order], ): M[ProductionPlan] = for _ <- orders.traverse(checkDeliverabilityOfOrder(cheeseTypeRipeningDays)) @@ -53,7 +53,7 @@ private def checkDeliverabilityOfOrder[M[_]: Monad: CanEmit[OrderDelayed]]( private def magicAIProductsToProduceEstimator( orders: List[Order], - previousProductionPlan: ProductionPlan, + previousProductionPlan: Option[ProductionPlan], cheeseTypeRipeningDays: CheeseTypeRipeningDays, missingProducts: MissingProducts, ): NonEmptyList[ProductToProduce] = diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala index f399637d..cce5e9eb 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Configuration.scala @@ -6,6 +6,7 @@ import dev.atedeg.mdm.productionplanning.api.repositories.* final case class Configuration( receivedOrderRepository: ReceivedOrderRepository, ripeningDaysRepository: RipeningDaysRepository, + productionPlanRepository: ProductionPlanRepository, productionPlanReadyEmitter: ProductionPlanReadyEmitter, orderDelayedEmitter: OrderDelayedEmitter, ) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala index f0a6ebb7..0f15d2af 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/Handlers.scala @@ -14,6 +14,7 @@ import dev.atedeg.mdm.productionplanning.api.repositories.ReceivedOrderRepositor import dev.atedeg.mdm.productionplanning.dto.* import dev.atedeg.mdm.productionplanning.dto.NewOrderReceivedDTO.given import dev.atedeg.mdm.productionplanning.dto.OrderDTO.given +import dev.atedeg.mdm.productionplanning.dto.ProductionPlanDTO.given import dev.atedeg.mdm.utils.monads.* import dev.atedeg.mdm.utils.serialization.DTO import dev.atedeg.mdm.utils.serialization.DTOOps.* @@ -31,14 +32,14 @@ def handleSendProductionPlan[M[_]: Monad: LiftIO: CanRead[Configuration]: CanRai config <- readState missingProducts <- getMissingProductsFromStock >>= validate cheeseTypeRipeningDays <- config.ripeningDaysRepository.getRipeningDays >>= validate - previousProductionPlan <- getPreviousYearProductionPlan >>= validate + previousProductionPlan <- config.productionPlanRepository.getPreviuosYearProductionPlan >>= (_.traverse(validate)) orders <- config.receivedOrderRepository.getOrders >>= validate[List[OrderDTO], List[Order], M] action: SafeActionTwoEvents[ProductionPlanReady, OrderDelayed, ProductionPlan] = createProductionPlan(missingProducts, cheeseTypeRipeningDays)(previousProductionPlan, orders) - (events1, events2, _) = action.execute + (events1, events2, plan) = action.execute + _ <- config.productionPlanRepository.saveProductionPlan(plan.toDTO[ProductionPlanDTO]) _ <- events2.map(_.toDTO[OrderDelayedDTO]).traverse(config.orderDelayedEmitter.emit) _ <- events1.map(_.toDTO[ProductionPlanReadyDTO]).traverse(config.productionPlanReadyEmitter.emit) yield () private def getMissingProductsFromStock[M[_]: Monad: LiftIO]: M[MissingProductsDTO] = ??? -private def getPreviousYearProductionPlan[M[_]: Monad: LiftIO]: M[ProductionPlanDTO] = ??? diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala index a6bc6963..fd956f87 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/acl/DTOs.scala @@ -34,5 +34,4 @@ extension (io: IncomingOrderDTO) extension (io: IncomingOrderDTO) def toNewOrderReceivedDTO: NewOrderReceivedDTO = NewOrderReceivedDTO(io.toNewOrderDTO) final case class DesiredStockDTO(desiredStock: List[(ProductDTO, Int)]) -extension (ds: DesiredStockDTO) - def toMissingProductsInStock: MissingProductsDTO = MissingProductsDTO(ds.desiredStock.toMap) +extension (ds: DesiredStockDTO) def toMissingProductsDTO: MissingProductsDTO = MissingProductsDTO(ds.desiredStock.toMap) diff --git a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala index 2c4bcc31..c92e379b 100644 --- a/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala +++ b/production-planning/src/main/scala/dev/atedeg/mdm/productionplanning/api/repositories/Repositories.scala @@ -3,7 +3,7 @@ package dev.atedeg.mdm.productionplanning.api.repositories import cats.Monad import cats.effect.LiftIO -import dev.atedeg.mdm.productionplanning.dto.{ CheeseTypeRipeningDaysDTO, OrderDTO } +import dev.atedeg.mdm.productionplanning.dto.{ CheeseTypeRipeningDaysDTO, OrderDTO, ProductionPlanDTO } trait ReceivedOrderRepository: def saveNewOrder[M[_]: Monad: LiftIO](order: OrderDTO): M[Unit] @@ -11,3 +11,7 @@ trait ReceivedOrderRepository: trait RipeningDaysRepository: def getRipeningDays[M[_]: Monad: LiftIO]: M[CheeseTypeRipeningDaysDTO] + +trait ProductionPlanRepository: + def getPreviuosYearProductionPlan[M[_]: Monad: LiftIO]: M[Option[ProductionPlanDTO]] + def saveProductionPlan[M[_]: Monad: LiftIO](productionPlanDTO: ProductionPlanDTO): M[Unit] diff --git a/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala index 3ac010c9..eb2ea4f7 100644 --- a/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala +++ b/production-planning/src/test/scala/dev/atedeg/mdm/productionplanning/Tests.scala @@ -47,7 +47,7 @@ class Tests extends AnyFeatureSpec with GivenWhenThen with Matchers with Mocks: val orders = List.fill(3)(Order(OrderID(UUID.randomUUID), requiredBy, orderedProducts)) And("the production plan of the previous year for the same day") val productsToProduce = NonEmptyList.of(prodToProd1, prodToProd2, prodToProd3) - val previousProductionPlan = ProductionPlan(productsToProduce) + val previousProductionPlan = Some(ProductionPlan(productsToProduce)) And("no missing products") val missingProducts: MissingProducts = MissingProducts(Map.empty) When("creating the production plan") From 6e8b69e79c983fe34a828a0ee6c1ea6fdcca4697 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 11 Aug 2022 17:08:41 +0000 Subject: [PATCH 324/329] chore(release): 1.0.0-beta.16 [skip ci] # [1.0.0-beta.16](https://github.com/atedeg/mdm/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2022-08-11) ### Bug Fixes * date dto in prod planning acl ([aad6bff](https://github.com/atedeg/mdm/commit/aad6bfffabedd03b771e2cb4d7d837fbebde09cb)) * in handleOrderReceived use order instead of ordered product ([facbaba](https://github.com/atedeg/mdm/commit/facbaba0a5425d995487316220dfd0088262efe8)) * progate date error in prod plan acl ([3740f91](https://github.com/atedeg/mdm/commit/3740f91dd846f0b512512dfe4b8fa09ddbdf0687)) ### Features * add desired stock to dto stock dto in prod plan acl ([9b4c9c5](https://github.com/atedeg/mdm/commit/9b4c9c559d893851760a16496b6ba7f539cad0ab)) * add order received and production plan send handlers ([2893046](https://github.com/atedeg/mdm/commit/2893046c2221774f577cd824e784853f77f31ea3)) * save the production plan ([3457eaa](https://github.com/atedeg/mdm/commit/3457eaae70eb52288aaaf5df77593e2435c8c288)) --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 651df18c..95651575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# [1.0.0-beta.16](https://github.com/atedeg/mdm/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2022-08-11) + + +### Bug Fixes + +* date dto in prod planning acl ([aad6bff](https://github.com/atedeg/mdm/commit/aad6bfffabedd03b771e2cb4d7d837fbebde09cb)) +* in handleOrderReceived use order instead of ordered product ([facbaba](https://github.com/atedeg/mdm/commit/facbaba0a5425d995487316220dfd0088262efe8)) +* progate date error in prod plan acl ([3740f91](https://github.com/atedeg/mdm/commit/3740f91dd846f0b512512dfe4b8fa09ddbdf0687)) + + +### Features + +* add desired stock to dto stock dto in prod plan acl ([9b4c9c5](https://github.com/atedeg/mdm/commit/9b4c9c559d893851760a16496b6ba7f539cad0ab)) +* add order received and production plan send handlers ([2893046](https://github.com/atedeg/mdm/commit/2893046c2221774f577cd824e784853f77f31ea3)) +* save the production plan ([3457eaa](https://github.com/atedeg/mdm/commit/3457eaae70eb52288aaaf5df77593e2435c8c288)) + # [1.0.0-beta.15](https://github.com/atedeg/mdm/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2022-08-11) From 5588f935689768bd94b7035f30b1c0ef8abf2090 Mon Sep 17 00:00:00 2001 From: Linda Vitali Date: Thu, 11 Aug 2022 18:55:21 +0200 Subject: [PATCH 325/329] ci: create workflow for documentation site --- .github/workflows/ci.yml | 8 ------- .github/workflows/publish-site.yml | 38 ++++++++++++++++++++++++++++++ .releaserc.yml | 1 - 3 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/publish-site.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1debef4..bc361cbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,11 +83,3 @@ jobs: sonatype-username: ${{ secrets.SONATYPE_USERNAME }} sonatype-password: ${{ secrets.SONATYPE_PASSWORD }} github-token: ${{ steps.atedeg-bot.outputs.token }} - - - name: Deploy site - if: github.ref == 'refs/heads/main' - uses: JamesIves/github-pages-deploy-action@v4.4.0 - with: - GITHUB_TOKEN: ${{ steps.atedeg-bot.outputs.token }} - BRANCH: gh-pages - FOLDER: target/site diff --git a/.github/workflows/publish-site.yml b/.github/workflows/publish-site.yml new file mode 100644 index 00000000..fd3b0223 --- /dev/null +++ b/.github/workflows/publish-site.yml @@ -0,0 +1,38 @@ +name: Publish site + +on: + workflow_run: + workflows: [Build test and deploy] + types: + - completed + +jobs: + publish-site: + runs-on: ubuntu-22.04 + steps: + - name: Setup atedeg-bot + id: atedeg-bot + uses: tibdex/github-app-token@v1.6.0 + with: + app_id: ${{ secrets.ATEDEG_BOT_APP_ID }} + private_key: ${{ secrets.ATEDEG_BOT_PRIVATE_KEY }} + + - name: Checkout current branch + uses: actions/checkout@v3 + with: + token: ${{ steps.atedeg-bot.outputs.token }} + fetch-depth: 0 + + - name: Build documentation site + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + run: | + export LAST_VERSION=`git describe --tags --abbrev=0 | sed -e 's/^v*//'` + sbt 'set ThisBuild / version := System.getenv("LAST_VERSION")' ubidocGenerate + + - name: Deploy site + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + uses: JamesIves/github-pages-deploy-action@v4.4.0 + with: + GITHUB_TOKEN: ${{ steps.atedeg-bot.outputs.token }} + BRANCH: gh-pages + FOLDER: target/site diff --git a/.releaserc.yml b/.releaserc.yml index 246eaae7..587a8348 100644 --- a/.releaserc.yml +++ b/.releaserc.yml @@ -15,7 +15,6 @@ plugins: - publishCmd: | export CI_COMMIT_TAG="true" sbt ci-release - sbt 'set ThisBuild / version := "${nextRelease.version}"' ubidocGenerate - - '@semantic-release/git' - assets: - CHANGELOG.md From 01e1b751f73f82b3a809105ff98603f9b43d72de Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 18:27:43 +0200 Subject: [PATCH 326/329] build(docker): refactor docker settings --- build.sbt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 94b5373c..9fb8c97b 100644 --- a/build.sbt +++ b/build.sbt @@ -75,7 +75,13 @@ val commonSettings = Seq( "org.slf4j" % "slf4j-api" % "1.7.36", "org.slf4j" % "slf4j-simple" % "1.7.36", ), +) + +val commonDockerSettings = Seq( + dockerBaseImage := "eclipse-temurin:18.0.2_9-jre", dockerEnvVars := Map("PORT" -> "8080", "HOST" -> "0.0.0.0"), + Docker / packageName := packageName.value, + Docker / version := version.value, ) addCommandAlias("ubidocGenerate", "clean; unidoc; ubidoc; clean; unidoc") @@ -129,9 +135,8 @@ lazy val `milk-planning` = project .enablePlugins(DockerPlugin, JavaAppPackaging) .in(file("milk-planning")) .settings(commonSettings) + .settings(commonDockerSettings) .settings( - Docker / packageName := packageName.value, - Docker / version := version.value, dockerExposedPorts := Seq(8080), ) .dependsOn(utils, `products-shared-kernel`) @@ -169,9 +174,8 @@ lazy val restocking = project .enablePlugins(DockerPlugin, JavaAppPackaging) .in(file("restocking")) .settings(commonSettings) + .settings(commonDockerSettings) .settings( - Docker / packageName := packageName.value, - Docker / version := version.value, dockerExposedPorts := Seq(8080), ) .dependsOn(utils, `products-shared-kernel`) From b8fd1fd11c3c51e17ec837099e566ffa648b893c Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 18:28:39 +0200 Subject: [PATCH 327/329] chore(docker): add step in order to publishing docker images to docker hub --- .releaserc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.releaserc.yml b/.releaserc.yml index 587a8348..67b562f3 100644 --- a/.releaserc.yml +++ b/.releaserc.yml @@ -15,6 +15,7 @@ plugins: - publishCmd: | export CI_COMMIT_TAG="true" sbt ci-release + sbt 'set ThisBuild / version := "${nextRelease.version}"' docker:publish - - '@semantic-release/git' - assets: - CHANGELOG.md From 3605f702248b1759f52b8dc59f58c8542da46b9a Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Tue, 9 Aug 2022 18:29:01 +0200 Subject: [PATCH 328/329] ci(docker): setup docker action in order to login to docker hub --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc361cbc..b338d15f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,12 @@ jobs: token: ${{ steps.atedeg-bot.outputs.token }} fetch-depth: 0 + - name: Login to Docker Hub + uses: docker/login-action@v2.0.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Release uses: atedeg/scala-release@v1.0.3 with: From 678b24709a5a89b5a61a82cd565fe87f225f410d Mon Sep 17 00:00:00 2001 From: Nicolas Farabegoli Date: Fri, 12 Aug 2022 10:42:28 +0200 Subject: [PATCH 329/329] build: uniform docker configuration --- build.sbt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 9fb8c97b..72730258 100644 --- a/build.sbt +++ b/build.sbt @@ -142,9 +142,13 @@ lazy val `milk-planning` = project .dependsOn(utils, `products-shared-kernel`) lazy val production = project + .enablePlugins(DockerPlugin, JavaAppPackaging) .in(file("production")) - .dependsOn(utils, `products-shared-kernel`) .settings(commonSettings) + .settings( + dockerExposedPorts := Seq(8080), + ) + .dependsOn(utils, `products-shared-kernel`) lazy val `products-shared-kernel` = project .in(file("products-shared-kernel")) @@ -159,15 +163,17 @@ lazy val stocking = project .in(file("stocking")) .settings(commonSettings) .settings( - Docker / packageName := packageName.value, - Docker / version := version.value, dockerExposedPorts := Seq(8080), ) .dependsOn(utils, `products-shared-kernel`) lazy val `client-orders` = project + .enablePlugins(DockerPlugin, JavaAppPackaging) .in(file("client-orders")) .settings(commonSettings) + .settings( + dockerExposedPorts := Seq(8080), + ) .dependsOn(utils, `products-shared-kernel`) lazy val restocking = project @@ -181,6 +187,10 @@ lazy val restocking = project .dependsOn(utils, `products-shared-kernel`) lazy val `production-planning` = project + .enablePlugins(DockerPlugin, JavaAppPackaging) .in(file("production-planning")) .settings(commonSettings) + .settings( + dockerExposedPorts := Seq(8080), + ) .dependsOn(utils, `products-shared-kernel`)