Fix tests

This commit is contained in:
Paul-Henri Froidmont 2025-10-13 15:46:22 +02:00
parent efdc50eb1d
commit 87bd780f9f
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
34 changed files with 230 additions and 303 deletions

24
.scalafix.conf Normal file
View file

@ -0,0 +1,24 @@
rules = [
DisableSyntax,
LeakingImplicitClassVal,
NoAutoTupling,
NoValInForComprehension,
OrganizeImports
]
OrganizeImports {
targetDialect = Scala3
blankLines = Manual
expandRelative = true
removeUnused=false
groupedImports=Keep
groups = [
"---"
"re:(javax?|scala)\\."
"---"
"*"
"---"
"lu.foyer"
"---"
]
}

View file

@ -1,23 +1,18 @@
package lu.foyer
import zio.*
import zio.http.*
import zio.http.Header.AccessControlAllowOrigin
import zio.http.Middleware.CorsConfig
import zio.http.Middleware.cors
import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.openapi.OpenAPIGen
import zio.http.endpoint.openapi.SwaggerUI
import lu.foyer.clients.*
import lu.foyer.contracts.*
import zio.*
import zio.Console.*
import zio.http.*
import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.*
import zio.http.endpoint.openapi.OpenAPIGen
import zio.http.endpoint.openapi.SwaggerUI
import zio.schema.*
import zio.http.Middleware.cors
import zio.http.Middleware.CorsConfig
import zio.http.Header.AccessControlAllowOrigin
import java.net.URI
import java.time.LocalDate
import java.util.UUID
import zio.schema.codec.JsonCodec.ExplicitConfig
object HttpServer:
@ -33,6 +28,11 @@ object HttpServer:
++ SwaggerUI.routes("docs" / "openapi", openAPI)
object App extends ZIOAppDefault:
override val bootstrap = CodecConfig.configLayer(
CodecConfig(explicitNulls = ExplicitConfig(encoding = false, decoding = false))
)
val app =
for
routes <- HttpServer.routes

View file

@ -1,39 +1,32 @@
package lu.foyer
import lu.foyer.JsonApiResponse.One
import zio.*
import zio.Console.*
import zio.http.*
import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.*
import zio.schema.*
import java.net.URI
import java.time.LocalDate
import java.util.UUID
trait CommandEngineController[Command: Schema, Event: Schema, State: Schema]
extends JsonApiController:
trait CommandEngineController[Command, Event: Schema, State: Schema] extends JsonApiController:
def commandEngine: CommandEngine[Command, Event, State]
private lazy val fetchMany =
Endpoint(Method.GET / entityName)
// .query(HttpCodec.query[Page])
.query(HttpCodec.query[Page])
.jsonApiMany[State]
private lazy val fetchOne =
Endpoint(Method.GET / entityName / uuid("entityId"))
Endpoint(Method.GET / entityName / string("entityId"))
.jsonApiOne[State]
private lazy val fetchEventsMany =
Endpoint(Method.GET / entityName / uuid("entityId") / "events")
// .query(HttpCodec.query[Page])
Endpoint(Method.GET / entityName / string("entityId") / "events")
.query(HttpCodec.query[Page])
.jsonApiMany[Event]
private lazy val fetchEventsOne =
Endpoint(Method.GET / entityName / uuid("entityId") / "events" / uuid("eventId"))
Endpoint(Method.GET / entityName / string("entityId") / "events" / string("eventId"))
.jsonApiOne[Event]
private def generateCommands = commandEngine.handlers.map(handler =>
@ -49,15 +42,17 @@ trait CommandEngineController[Command: Schema, Event: Schema, State: Schema]
val route = endpoint.implementJsonApiOneEvent(command =>
for
entityId <- Random.nextUUID
(event, state) <- commandEngine
.handleCommand(command, handler.name, entityId)
(event, _) <- commandEngine
.handleCommand(command, handler.name, entityId.toString)
yield Some(event)
)
(endpoint, route)
private def generateUpdateCommand(handler: CommandHandler[Command, Event, State]) =
given Schema[Command] = handler.commandSchema.asInstanceOf[Schema[Command]]
val endpoint = Endpoint(Method.PUT / entityName / uuid("entityId") / "commands" / handler.name)
val endpoint = Endpoint(
Method.PUT / entityName / string("entityId") / "commands" / handler.name
)
.in[Command]
.jsonApiOne[Event]
val route = endpoint.implementJsonApiOneEvent((entityId, command) =>
@ -70,17 +65,13 @@ trait CommandEngineController[Command: Schema, Event: Schema, State: Schema]
private lazy val (commands, commandsRoutes) = generateCommands.unzip
private lazy val fetchManyRoute =
fetchMany.implementJsonApiManyEntity(_ =>
commandEngine.stateRepo.fetchMany(Page(None, None, totals = Some(true)))
)
fetchMany.implementJsonApiManyEntity(commandEngine.stateRepo.fetchMany)
private lazy val fetchOneRoute =
fetchOne.implementJsonApiOneEntity(commandEngine.stateRepo.fetchOne)
private lazy val fetchEventsManyRoute =
fetchEventsMany.implementJsonApiManyEvent(entityId =>
commandEngine.eventRepo.fetchMany(entityId, Page(None, None, totals = Some(true)))
)
fetchEventsMany.implementJsonApiManyEvent(commandEngine.eventRepo.fetchMany(_, _))
private lazy val fetchEventsOneRoute =
fetchEventsOne.implementJsonApiOneEvent(commandEngine.eventRepo.fetchOne(_, _))

View file

@ -1,19 +1,14 @@
package lu.foyer
import lu.foyer.JsonApiResponse.Many
import lu.foyer.JsonApiResponse.One
import zio.*
import zio.http.*
import zio.http.Header.Forwarded
import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.*
import zio.schema.*
import zio.schema.annotation.discriminatorName
import zio.schema.annotation.fieldName
import java.util.UUID
import scala.annotation.targetName
import lu.foyer.JsonApiResponse.Many
import lu.foyer.JsonApiResponse.One
object JsonApiResponse:
@ -26,7 +21,7 @@ object JsonApiResponse:
case class Entity[T](
`type`: String,
id: UUID,
id: String,
attributes: T,
relationships: Option[Relationships] = None,
links: Map[String, String])
@ -34,17 +29,17 @@ object JsonApiResponse:
case class Relationships(_entity: RelationshipsEntity) derives Schema
object Relationships:
def apply(id: UUID, `type`: String, entityUrl: String): Relationships = Relationships(
def apply(id: String, `type`: String, entityUrl: String): Relationships = Relationships(
RelationshipsEntity(RelationshipsData(id, `type`), RelationshipsLinks(entityUrl))
)
case class RelationshipsEntity(data: RelationshipsData, links: RelationshipsLinks) derives Schema
case class RelationshipsData(id: UUID, `type`: String) derives Schema
case class RelationshipsData(id: String, `type`: String) derives Schema
case class RelationshipsLinks(related: String) derives Schema
@discriminatorName("errorType")
enum Error(title: String) derives Schema:
case NotFound(id: String) extends Error(s"Entity $id not found")
case InternalServerError(title: String) extends Error(title)
enum Error derives Schema:
case NotFound(id: String)
case InternalServerError(title: String)
object Error:
given Schema[Error.NotFound] = DeriveSchema.gen
given Schema[Error.InternalServerError] = DeriveSchema.gen
@ -105,7 +100,7 @@ trait JsonApiController:
)
def implementJsonApiOne[Env, A](
f: Input => RIO[Env, Option[A]],
getId: A => UUID,
getId: A => String,
getEntity: A => Output,
onthology: String = this.onthology,
links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty,
@ -177,7 +172,7 @@ trait JsonApiController:
)
def implementJsonApiMany[Env, A](
f: Input => RIO[Env, Paged[A]],
getId: A => UUID,
getId: A => String,
getEntity: A => Output,
links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty
)(implicit trace: Trace

View file

@ -2,16 +2,6 @@ package lu.foyer
package clients
import zio.*
import zio.Console.*
import zio.http.*
import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.*
import zio.schema.*
import java.net.URI
import java.time.LocalDate
import java.util.UUID
class ClientController(val commandEngine: CommandEngine[ClientCommand, ClientEvent, ClientState])
extends CommandEngineController[ClientCommand, ClientEvent, ClientState]:

View file

@ -1,22 +1,21 @@
package lu.foyer
package clients
import java.util.UUID
import zio.*
class ClientEventRepositoryInMemory(events: Ref[Map[UUID, Event[ClientEvent]]])
class ClientEventRepositoryInMemory(events: Ref[Map[String, Event[ClientEvent]]])
extends EventRepository[ClientEvent]
with InMemoryRepository[Event[ClientEvent]](events):
def fetchOne(entityId: UUID, eventId: UUID): Task[Option[Event[ClientEvent]]] =
def fetchOne(entityId: String, eventId: String): Task[Option[Event[ClientEvent]]] =
events.get.map(_.get(eventId))
def fetchMany(entityId: UUID, page: Page): Task[Paged[Event[ClientEvent]]] =
def fetchMany(entityId: String, page: Page): Task[Paged[Event[ClientEvent]]] =
events.get
.map(entities =>
val items = entities.values
.filter(_.entityId == entityId)
.drop(page.number.getOrElse(0) * page.size.getOrElse(50))
.take(page.size.getOrElse(50))
Paged(items.toList, if page.totals.getOrElse(false) then Some(entities.size) else None)
Paged(items.toList, if page.totals.getOrElse(true) then Some(entities.size) else None)
)
object ClientEventRepositoryInMemory:

View file

@ -1,10 +1,9 @@
package lu.foyer
package clients
import java.util.UUID
import zio.*
class ClientStateRepositoryInMemory(clients: Ref[Map[UUID, Entity[ClientState]]])
class ClientStateRepositoryInMemory(clients: Ref[Map[String, Entity[ClientState]]])
extends StateRepository[ClientState]
with InMemoryRepository[Entity[ClientState]](clients)

View file

@ -2,16 +2,6 @@ package lu.foyer
package contracts
import zio.*
import zio.Console.*
import zio.http.*
import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.*
import zio.schema.*
import java.net.URI
import java.time.LocalDate
import java.util.UUID
class ContractController(
val commandEngine: CommandEngine[ContractCommand, ContractEvent, ContractState])

View file

@ -1,22 +1,21 @@
package lu.foyer
package contracts
import java.util.UUID
import zio.*
class ContractEventRepositoryInMemory(events: Ref[Map[UUID, Event[ContractEvent]]])
class ContractEventRepositoryInMemory(events: Ref[Map[String, Event[ContractEvent]]])
extends EventRepository[ContractEvent]
with InMemoryRepository[Event[ContractEvent]](events):
def fetchOne(entityId: UUID, eventId: UUID): Task[Option[Event[ContractEvent]]] =
def fetchOne(entityId: String, eventId: String): Task[Option[Event[ContractEvent]]] =
events.get.map(_.get(eventId))
def fetchMany(entityId: UUID, page: Page): Task[Paged[Event[ContractEvent]]] =
def fetchMany(entityId: String, page: Page): Task[Paged[Event[ContractEvent]]] =
events.get
.map(entities =>
val items = entities.values
.filter(_.entityId == entityId)
.drop(page.number.getOrElse(0) * page.size.getOrElse(50))
.take(page.size.getOrElse(50))
Paged(items.toList, if page.totals.getOrElse(false) then Some(entities.size) else None)
Paged(items.toList, if page.totals.getOrElse(true) then Some(entities.size) else None)
)
object ContractEventRepositoryInMemory:

View file

@ -1,10 +1,9 @@
package lu.foyer
package contracts
import java.util.UUID
import zio.*
class ContractStateRepositoryInMemory(clients: Ref[Map[UUID, Entity[ContractState]]])
class ContractStateRepositoryInMemory(clients: Ref[Map[String, Entity[ContractState]]])
extends StateRepository[ContractState]
with InMemoryRepository[Entity[ContractState]](clients)

View file

@ -1,12 +1,7 @@
package lu.foyer
package contracts
import java.util.UUID
import zio.*
import lu.foyer.clients.Address
import scala.math.BigDecimal.RoundingMode
import java.util.Currency
import lu.foyer.clients.Country
object EmployeeServiceImpl extends EmployeeService:

View file

@ -1,11 +1,12 @@
package lu.foyer
package contracts
import java.util.UUID
import zio.*
import lu.foyer.clients.Address
import scala.math.BigDecimal.RoundingMode
import java.util.Currency
import scala.math.BigDecimal.RoundingMode
import zio.*
import lu.foyer.clients.Address
import lu.foyer.clients.Country
object PremiumServiceImpl extends PremiumService:

48
build.mill Normal file
View file

@ -0,0 +1,48 @@
//| mvnDeps:
//| - com.goyeau::mill-scalafix::0.6.0
package build
import mill.*, scalalib.*
import com.goyeau.mill.scalafix.ScalafixModule
object Versions:
val zio = "2.1.21"
val zioJson = "0.7.44"
val zioSchema = "1.7.5"
val zioHttp = "3.5.1"
val zioPrelude = "1.0.0-RC42"
trait CommonModule extends ScalaModule with ScalafixModule:
def scalaVersion = "3.7.2"
def scalacOptions = Seq(
"-Wunused:all",
"-preview",
"-feature",
"-language:implicitConversions",
"-Wvalue-discard",
"-Wnonunit-statement"
)
def mvnDeps = Seq(
mvn"dev.zio::zio:${Versions.zio}",
mvn"dev.zio::zio-json:${Versions.zioJson}",
mvn"dev.zio::zio-schema-json:${Versions.zioSchema}",
mvn"dev.zio::zio-schema:${Versions.zioSchema}",
mvn"dev.zio::zio-schema-derivation:${Versions.zioSchema}",
mvn"dev.zio::zio-prelude:${Versions.zioPrelude}"
)
object model extends CommonModule
object core extends CommonModule :
def moduleDeps = Seq(model)
object api extends CommonModule :
def moduleDeps = Seq(core)
def mvnDeps = Seq(
mvn"dev.zio::zio:${Versions.zio}",
mvn"dev.zio::zio-http:${Versions.zioHttp}"
)

View file

@ -1,45 +0,0 @@
// scalafmt: { runner.dialect = scala213 }
package build
import mill._, scalalib._
import coursier.maven.MavenRepository
val sonatypeSnapshots = Seq(
MavenRepository("https://oss.sonatype.org/content/repositories/snapshots")
)
object Versions {
val zio = "2.1.15"
val zioJson = "0.7.33"
val zioSchema = "1.6.3"
val zioHttp = "3.1.0"
val zioPrelude = "1.0.0-RC39"
}
trait CommonModule extends ScalaModule {
def scalaVersion = "3.6.3"
def ivyDeps = Agg(
ivy"dev.zio::zio:${Versions.zio}",
ivy"dev.zio::zio-json:${Versions.zioJson}",
ivy"dev.zio::zio-schema-json:${Versions.zioSchema}",
ivy"dev.zio::zio-schema:${Versions.zioSchema}",
ivy"dev.zio::zio-schema-derivation:${Versions.zioSchema}",
ivy"dev.zio::zio-prelude:${Versions.zioPrelude}"
)
def repositoriesTask = Task.Anon {
super.repositoriesTask() ++ sonatypeSnapshots
}
}
object model extends CommonModule
object core extends CommonModule {
def moduleDeps = Seq(model)
}
object api extends CommonModule {
def moduleDeps = Seq(core)
def ivyDeps = Agg(
ivy"dev.zio::zio:${Versions.zio}",
ivy"dev.zio::zio-http:${Versions.zioHttp}"
)
}

View file

@ -3,15 +3,13 @@ package lu.foyer
import zio.*
import zio.schema.Schema
import java.util.UUID
final case class Entity[T](entityId: String, data: T, version: Long)
final case class Event[T](entityId: String, data: T, eventId: String)
final case class Entity[T](entityId: UUID, data: T, version: Long)
final case class Event[T](entityId: UUID, data: T, eventId: UUID)
trait StateRepository[Data] extends Repository[Entity[Data], UUID]
trait EventRepository[Data] extends Repository[Event[Data], UUID]:
def fetchOne(entityId: UUID, eventId: UUID): Task[Option[Event[Data]]]
def fetchMany(entityId: UUID, page: Page): Task[Paged[Event[Data]]]
trait StateRepository[Data] extends Repository[Entity[Data], String]
trait EventRepository[Data] extends Repository[Event[Data], String]:
def fetchOne(entityId: String, eventId: String): Task[Option[Event[Data]]]
def fetchMany(entityId: String, page: Page): Task[Paged[Event[Data]]]
trait Reducer[Event, State]:
def fromEmpty: PartialFunction[Event, State]
@ -30,13 +28,13 @@ trait CommandHandler[+Command, +Event, +State]:
def commandSchema: Schema[?]
trait CommandHandlerCreate[Command: Schema, Event] extends CommandHandler[Command, Event, Nothing]:
def onCommand(entityId: UUID, command: Command): Task[Event]
def onCommand(entityId: String, command: Command): Task[Event]
val isCreate = true
val commandSchema = summon[Schema[Command]]
trait CommandHandlerUpdate[Command: Schema, Event, State]
extends CommandHandler[Command, Event, State]:
def onCommand(entityId: UUID, state: State, command: Command): Task[Event]
def onCommand(entityId: String, state: State, command: Command): Task[Event]
val isCreate = false
val commandSchema = summon[Schema[Command]]
@ -46,7 +44,7 @@ class CommandEngine[Command, Event, State](
val eventRepo: EventRepository[Event],
val stateRepo: StateRepository[State]):
def handleCommand(command: Command, name: String, entityId: UUID)
def handleCommand(command: Command, name: String, entityId: String)
: Task[(lu.foyer.Event[Event], lu.foyer.Entity[State])] =
for
handler <- ZIO
@ -61,14 +59,14 @@ class CommandEngine[Command, Event, State](
newEntity = Entity(entityId, newState, entityOption.map(_.version).getOrElse(1))
_ <- if entityOption.isEmpty then stateRepo.insert(newEntity.entityId, newEntity)
else stateRepo.update(newEntity.entityId, newEntity)
eventEntity <- Random.nextUUID.map(Event(newEntity.entityId, event, _))
eventEntity <- Random.nextUUID.map(id => Event(newEntity.entityId, event, id.toString))
_ <- eventRepo.insert(eventEntity.eventId, eventEntity)
yield (eventEntity, newEntity)
private def transition(
command: Command,
name: String,
entityId: UUID,
entityId: String,
entityOption: Option[Entity[State]],
handler: CommandHandler[Command, Event, State]
): Task[(Event, Option[State])] = (entityOption, handler) match

View file

@ -4,8 +4,6 @@ import zio.*
import zio.schema.*
import zio.schema.annotation.fieldName
import java.util.UUID
final case class Page(
@fieldName("page[number]")
number: Option[Int],
@ -14,6 +12,9 @@ final case class Page(
@fieldName("page[totals]")
totals: Option[Boolean])
derives Schema
object Page:
val default = Page(None, None, totals = Some(true))
final case class Paged[T](items: List[T], totals: Option[Long])
trait Repository[Entity, Id]:
@ -22,15 +23,16 @@ trait Repository[Entity, Id]:
def insert(id: Id, entity: Entity): Task[Unit]
def update(id: Id, entity: Entity): Task[Unit]
trait InMemoryRepository[State](entities: Ref[Map[UUID, State]]) extends Repository[State, UUID]:
def fetchOne(id: UUID): Task[Option[State]] = entities.get.map(_.get(id))
trait InMemoryRepository[State](entities: Ref[Map[String, State]])
extends Repository[State, String]:
def fetchOne(id: String): Task[Option[State]] = entities.get.map(_.get(id))
def fetchMany(page: Page): Task[Paged[State]] = entities.get.map(entities =>
val items =
entities.values
.drop(page.number.getOrElse(0) * page.size.getOrElse(50)).take(page.size.getOrElse(50))
Paged(items.toList, if page.totals.getOrElse(false) then Some(entities.size) else None)
Paged(items.toList, if page.totals.getOrElse(true) then Some(entities.size) else None)
)
def insert(id: UUID, entity: State): Task[Unit] =
def insert(id: String, entity: State): Task[Unit] =
entities.update(entities => entities.updated(id, entity))
def update(id: UUID, entity: State): Task[Unit] =
def update(id: String, entity: State): Task[Unit] =
entities.update(entities => entities.updated(id, entity))

View file

@ -3,8 +3,6 @@ package clients
import zio.schema.*
import java.time.LocalDate
enum ClientCommand derives Schema:
case Create(
lastName: ClientLastName,

View file

@ -5,8 +5,6 @@ import zio.schema.*
import zio.schema.annotation.caseName
import zio.schema.annotation.discriminatorName
import java.time.LocalDate
@discriminatorName("eventType")
sealed trait ClientEvent derives Schema
object ClientEvent:

View file

@ -2,7 +2,6 @@ package lu.foyer
package clients
import zio.*
import java.util.UUID
object ClientHandlers:
val layer: ULayer[List[CommandHandler[ClientCommand, ClientEvent, ClientState]]] =
@ -16,7 +15,7 @@ object ClientHandlers:
object CreateHandler extends CommandHandlerCreate[ClientCommand.Create, ClientEvent.Created]:
val name = "create"
def onCommand(entityId: UUID, command: ClientCommand.Create): Task[ClientEvent.Created] =
def onCommand(entityId: String, command: ClientCommand.Create): Task[ClientEvent.Created] =
ZIO.succeed(
ClientEvent.Created(
command.lastName,
@ -32,7 +31,7 @@ object CreateHandler extends CommandHandlerCreate[ClientCommand.Create, ClientEv
object UpdateHandler
extends CommandHandlerUpdate[ClientCommand.Update, ClientEvent.Updated, ClientState.Actif]:
val name = "update"
def onCommand(entityId: UUID, state: ClientState.Actif, command: ClientCommand.Update)
def onCommand(entityId: String, state: ClientState.Actif, command: ClientCommand.Update)
: Task[ClientEvent.Updated] =
ZIO.succeed(
ClientEvent.Updated(
@ -49,6 +48,6 @@ object UpdateHandler
object DisableHandler
extends CommandHandlerUpdate[ClientCommand.Disable, ClientEvent.Disabled, ClientState.Actif]:
val name = "disable"
def onCommand(entityId: UUID, state: ClientState.Actif, command: ClientCommand.Disable)
def onCommand(entityId: String, state: ClientState.Actif, command: ClientCommand.Disable)
: Task[ClientEvent.Disabled] =
ZIO.succeed(ClientEvent.Disabled(command.reason))

View file

@ -10,7 +10,7 @@ class ClientReducer() extends Reducer[ClientEvent, ClientState]:
override val fromState =
case (s: ClientState.Actif, e: ClientEvent.Updated) => s.update(e)
case (s: ClientState.Actif, e: ClientEvent.Disabled) => s.disable(e)
case (s: ClientState.Actif, _: ClientEvent.Disabled) => s.disable()
object ClientReducer:
val layer: ULayer[Reducer[ClientEvent, ClientState]] = ZLayer.succeed(ClientReducer())

View file

@ -5,8 +5,6 @@ import zio.schema.*
import zio.schema.annotation.caseName
import zio.schema.annotation.discriminatorName
import java.time.LocalDate
@discriminatorName("statusType")
sealed trait ClientState derives Schema
object ClientState:
@ -32,7 +30,7 @@ object ClientState:
e.email.orElse(email),
e.address.orElse(address)
)
def disable(e: ClientEvent.Disabled) =
def disable() =
ClientState.Inactif(
lastName,
firstName,

View file

@ -3,9 +3,6 @@ package contracts
import zio.schema.*
import java.time.LocalDate
import java.util.UUID
enum ContractCommand derives Schema:
case Subscribe(
product: ProductType,

View file

@ -5,8 +5,6 @@ import zio.schema.*
import zio.schema.annotation.caseName
import zio.schema.annotation.discriminatorName
import java.time.LocalDate
@discriminatorName("eventType")
sealed trait ContractEvent derives Schema
object ContractEvent:

View file

@ -1,9 +1,9 @@
package lu.foyer
package contracts
import lu.foyer.clients.*
import zio.*
import java.util.UUID
import lu.foyer.clients.*
object ContractHandlers:
val layer =
@ -29,7 +29,7 @@ class SubscribeHandler(
val name = "create"
def onCommand(entityId: UUID, command: ContractCommand.Subscribe)
def onCommand(entityId: String, command: ContractCommand.Subscribe)
: Task[ContractEvent.Subscribed] =
for
holder <- clientStateRepo.fetchOne(command.holder).someOrFailException
@ -39,7 +39,6 @@ class SubscribeHandler(
ZIO.succeed(address)
case _ =>
ZIO.fail(new IllegalArgumentException(s"No active holder found for ${command.holder}"))
premium = premiumService.computePremium(command.formula, command.vehicle, residentialAddress)
premium <-
ZIO
.fromEither(
@ -60,7 +59,7 @@ class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService
val name = "amend"
def onCommand(entityId: UUID, state: ContractState, command: ContractCommand.Amend)
def onCommand(entityId: String, state: ContractState, command: ContractCommand.Amend)
: Task[ContractEvent.Amended] =
for
holder <- clientStateRepo.fetchOne(state.holder).someOrFailException
@ -96,7 +95,7 @@ class ApproveHandler(employeeService: EmployeeService)
val name = "approve"
def onCommand(entityId: UUID, state: ContractState.Pending, command: ContractCommand.Approve)
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Approve)
: Task[ContractEvent.Approved] =
for
user <- ZIO.succeed("") // TODO current user
@ -114,7 +113,7 @@ class RejectHandler(employeeService: EmployeeService)
val name = "reject"
def onCommand(entityId: UUID, state: ContractState.Pending, command: ContractCommand.Reject)
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Reject)
: Task[ContractEvent.Rejected] =
for
user <- ZIO.succeed("") // TODO current user
@ -132,6 +131,6 @@ class TerminateHandler()
val name = "reject"
def onCommand(entityId: UUID, state: ContractState, command: ContractCommand.Terminate)
def onCommand(entityId: String, state: ContractState, command: ContractCommand.Terminate)
: Task[ContractEvent.Terminated] =
ZIO.succeed(ContractEvent.Terminated(command.reason))

View file

@ -10,17 +10,17 @@ class ContractReducer() extends Reducer[ContractEvent, ContractState]:
override val fromState =
case (s: ContractState.Actif, e: ContractEvent.Amended) => s.amend(e)
case (s: ContractState.Actif, e: ContractEvent.Terminated) => s.terminate(e)
case (s: ContractState.Actif, _: ContractEvent.Terminated) => s.terminate()
case (s: ContractState.PendingSubscription, e: ContractEvent.Amended) => s.amend(e)
case (s: ContractState.PendingSubscription, e: ContractEvent.Approved) => s.approve(e)
case (s: ContractState.PendingSubscription, e: ContractEvent.Rejected) => s.reject(e)
case (s: ContractState.PendingSubscription, e: ContractEvent.Terminated) => s.terminate(e)
case (s: ContractState.PendingSubscription, _: ContractEvent.Approved) => s.approve()
case (s: ContractState.PendingSubscription, _: ContractEvent.Rejected) => s.reject()
case (s: ContractState.PendingSubscription, _: ContractEvent.Terminated) => s.terminate()
case (s: ContractState.PendingAmendment, e: ContractEvent.Amended) => s.amend(e)
case (s: ContractState.PendingAmendment, e: ContractEvent.Approved) => s.approve(e)
case (s: ContractState.PendingAmendment, e: ContractEvent.Rejected) => s.reject(e)
case (s: ContractState.PendingAmendment, e: ContractEvent.Terminated) => s.terminate(e)
case (s: ContractState.PendingAmendment, _: ContractEvent.Approved) => s.approve()
case (s: ContractState.PendingAmendment, _: ContractEvent.Rejected) => s.reject()
case (s: ContractState.PendingAmendment, _: ContractEvent.Terminated) => s.terminate()
object ContractReducer:
val layer: ULayer[Reducer[ContractEvent, ContractState]] = ZLayer.succeed(ContractReducer())

View file

@ -5,8 +5,6 @@ import zio.schema.*
import zio.schema.annotation.caseName
import zio.schema.annotation.discriminatorName
import java.time.LocalDate
@discriminatorName("statusType")
sealed trait ContractState derives Schema:
def product: ProductType
@ -32,7 +30,7 @@ object ContractState:
def amend(e: ContractEvent.Amended) =
Actif(e.product, holder, e.vehicle, e.formula, e.premium)
def terminate(e: ContractEvent.Terminated) =
def terminate() =
Terminated(product, holder, vehicle, formula, premium)
@discriminatorName("statusType")
@ -50,13 +48,13 @@ object ContractState:
def amend(e: ContractEvent.Amended) =
PendingSubscription(e.product, holder, e.vehicle, e.formula, e.premium)
def approve(e: ContractEvent.Approved) =
def approve() =
Actif(product, holder, vehicle, formula, premium)
def reject(e: ContractEvent.Rejected) =
def reject() =
Terminated(product, holder, vehicle, formula, premium)
def terminate(e: ContractEvent.Terminated) =
def terminate() =
Terminated(product, holder, vehicle, formula, premium)
final case class PendingChanges(
@ -79,7 +77,7 @@ object ContractState:
def amend(e: ContractEvent.Amended) =
copy(pendingChanges = PendingChanges(e.product, e.vehicle, e.formula, e.premium))
def approve(e: ContractEvent.Approved) =
def approve() =
Actif(
pendingChanges.product,
holder,
@ -88,10 +86,10 @@ object ContractState:
pendingChanges.premium
)
def reject(e: ContractEvent.Rejected) =
def reject() =
Actif(product, holder, vehicle, formula, premium)
def terminate(e: ContractEvent.Terminated) =
def terminate() =
ContractState.Terminated(product, holder, vehicle, formula, premium)
@caseName("terminated")

View file

@ -1,7 +1,5 @@
package lu.foyer
package contracts
import lu.foyer.clients.Address
trait EmployeeService:
def fetchOne(subject: String): Either[String, Employee]

132
flake.lock generated
View file

@ -9,16 +9,20 @@
"devenv"
],
"git-hooks": [
"devenv"
"devenv",
"git-hooks"
],
"nixpkgs": "nixpkgs"
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1737621947,
"narHash": "sha256-8HFvG7fvIFbgtaYAY2628Tb89fA55nPm2jSiNs0/Cws=",
"lastModified": 1752264895,
"narHash": "sha256-1zBPE/PNAkPNUsOWFET4J0cjlvziH8DOekesDmjND+w=",
"owner": "cachix",
"repo": "cachix",
"rev": "f65a3cd5e339c223471e64c051434616e18cc4f5",
"rev": "47053aef762f452e816e44eb9a23fbc3827b241a",
"type": "github"
},
"original": {
@ -32,6 +36,7 @@
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"git-hooks": "git-hooks",
"nix": "nix",
"nixpkgs": [
@ -39,11 +44,11 @@
]
},
"locked": {
"lastModified": 1742480343,
"narHash": "sha256-AN6X0t0pX0GLDh6CMG474aNffJUKPQtSEJ9aCZed474=",
"lastModified": 1759767645,
"narHash": "sha256-36Cm0InaiezqXYmzM7sNmLChWV2jfL4OWccIbL1n3nU=",
"owner": "cachix",
"repo": "devenv",
"rev": "56fe80518d1c949520a2cea8e9c2a83ec3f9bdc5",
"rev": "692a6db69ca067f9555f736b92c9d83468053f13",
"type": "github"
},
"original": {
@ -55,11 +60,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
@ -72,16 +77,15 @@
"inputs": {
"nixpkgs-lib": [
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712014858,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"lastModified": 1756770412,
"narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"rev": "4524271976b625a4a605beefd893f270620fd751",
"type": "github"
},
"original": {
@ -93,7 +97,8 @@
"git-hooks": {
"inputs": {
"flake-compat": [
"devenv"
"devenv",
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
@ -102,11 +107,11 @@
]
},
"locked": {
"lastModified": 1740849354,
"narHash": "sha256-oy33+t09FraucSZ2rZ6qnD1Y1c8azKKmQuCvF2ytUko=",
"lastModified": 1758108966,
"narHash": "sha256-ytw7ROXaWZ7OfwHrQ9xvjpUWeGVm86pwnEd1QhzawIo=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "4a709a8ce9f8c08fa7ddb86761fe488ff7858a07",
"rev": "54df955a695a84cd47d4a43e08e1feaf90b1fd9b",
"type": "github"
},
"original": {
@ -137,94 +142,53 @@
"type": "github"
}
},
"libgit2": {
"flake": false,
"locked": {
"lastModified": 1697646580,
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
"owner": "libgit2",
"repo": "libgit2",
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
"type": "github"
},
"original": {
"owner": "libgit2",
"repo": "libgit2",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": [
"devenv"
"devenv",
"flake-compat"
],
"flake-parts": [
"devenv",
"flake-parts"
],
"git-hooks-nix": [
"devenv",
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"flake-parts": "flake-parts",
"libgit2": "libgit2",
"nixpkgs": "nixpkgs_2",
"nixpkgs-23-11": [
"devenv"
],
"nixpkgs-regression": [
"devenv"
],
"pre-commit-hooks": [
"devenv"
]
},
"locked": {
"lastModified": 1741798497,
"narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=",
"owner": "domenkozar",
"lastModified": 1758763079,
"narHash": "sha256-Bx1A+lShhOWwMuy3uDzZQvYiBKBFcKwy6G6NEohhv6A=",
"owner": "cachix",
"repo": "nix",
"rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd",
"rev": "6f0140527c2b0346df4afad7497baa08decb929f",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.24",
"owner": "cachix",
"ref": "devenv-2.30.5",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1733212471,
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1717432640,
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1733477122,
"narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=",
"lastModified": 1758532697,
"narHash": "sha256-bhop0bR3u7DCw9/PtLCwr7GwEWDlBSxHp+eVQhCW9t4=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857",
"rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f",
"type": "github"
},
"original": {
@ -237,7 +201,7 @@
"root": {
"inputs": {
"devenv": "devenv",
"nixpkgs": "nixpkgs_3",
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},

View file

@ -1,7 +1,5 @@
package lu.foyer
import zio.schema.*
enum AppError(description: String) extends Throwable:
enum AppError(description: String) extends Throwable(description):
case NotFound(desc: String) extends AppError(desc)
case Unexpected(desc: String) extends AppError(desc)

View file

@ -1,10 +1,7 @@
package lu.foyer
import zio.schema.*
import java.util.UUID
opaque type ClientEntityId <: UUID = UUID
object ClientEntityId extends RefinedUUID[ClientEntityId]
opaque type ClientEntityId <: String = String
object ClientEntityId extends NonBlankString[ClientEntityId]
opaque type Email <: String = String
object Email extends NonBlankString[Email]

View file

@ -1,11 +1,11 @@
package lu.foyer
import zio.prelude.*
import zio.schema.Schema
import java.time.LocalDate
import java.util.UUID
import zio.prelude.*
import zio.schema.Schema
trait RefinedType[Base, New]:
inline def assume(value: Base): New = value.asInstanceOf[New]
def validation(value: Base): Validation[String, New]

View file

@ -1,11 +1,11 @@
package lu.foyer
package clients
import zio.schema.*
import java.time.Instant
import java.time.LocalDate
import zio.schema.*
opaque type ClientLastName <: String = String
object ClientLastName extends NonBlankString[ClientLastName]

View file

@ -2,7 +2,6 @@ package lu.foyer
package contracts
import zio.schema.*
import java.util.Currency
opaque type EmployeeDisplayName <: String = String
object EmployeeDisplayName extends NonBlankString[EmployeeDisplayName]

View file

@ -1,9 +1,10 @@
package lu.foyer
package contracts
import zio.schema.*
import java.util.Currency
import zio.schema.*
opaque type VehiclePlate <: String = String
object VehiclePlate extends NonBlankString[VehiclePlate]