Implement contracts
This commit is contained in:
parent
31014d1a0c
commit
efdc50eb1d
33 changed files with 879 additions and 173 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,5 +1,7 @@
|
||||||
.devenv
|
.devenv
|
||||||
.direnv
|
.direnv
|
||||||
out
|
out
|
||||||
|
.bsp
|
||||||
.bloop
|
.bloop
|
||||||
.metals
|
.metals
|
||||||
|
mill.*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package lu.foyer
|
package lu.foyer
|
||||||
|
|
||||||
import lu.foyer.clients.*
|
import lu.foyer.clients.*
|
||||||
|
import lu.foyer.contracts.*
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.Console.*
|
import zio.Console.*
|
||||||
import zio.http.*
|
import zio.http.*
|
||||||
|
|
@ -10,17 +11,26 @@ import zio.http.endpoint.*
|
||||||
import zio.http.endpoint.openapi.OpenAPIGen
|
import zio.http.endpoint.openapi.OpenAPIGen
|
||||||
import zio.http.endpoint.openapi.SwaggerUI
|
import zio.http.endpoint.openapi.SwaggerUI
|
||||||
import zio.schema.*
|
import zio.schema.*
|
||||||
|
import zio.http.Middleware.cors
|
||||||
|
import zio.http.Middleware.CorsConfig
|
||||||
|
import zio.http.Header.AccessControlAllowOrigin
|
||||||
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
object HttpServer:
|
object HttpServer:
|
||||||
|
|
||||||
|
val corsConfig = CorsConfig(_ => Some(AccessControlAllowOrigin.All))
|
||||||
|
|
||||||
def routes =
|
def routes =
|
||||||
for
|
for
|
||||||
client <- ZIO.service[ClientController]
|
client <- ZIO.service[ClientController]
|
||||||
openAPI = OpenAPIGen.fromEndpoints(client.endpoints)
|
contract <- ZIO.service[ContractController]
|
||||||
yield client.routes @@ Middleware.debug ++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
openAPI = OpenAPIGen.fromEndpoints(client.endpoints ++ contract.endpoints)
|
||||||
|
yield (client.routes ++ contract.routes)
|
||||||
|
@@ cors(corsConfig) @@ Middleware.debug
|
||||||
|
++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
||||||
|
|
||||||
object App extends ZIOAppDefault:
|
object App extends ZIOAppDefault:
|
||||||
val app =
|
val app =
|
||||||
|
|
@ -31,9 +41,17 @@ object App extends ZIOAppDefault:
|
||||||
|
|
||||||
override def run = app.provide(
|
override def run = app.provide(
|
||||||
CommandEngine.layer[ClientCommand, ClientEvent, ClientState],
|
CommandEngine.layer[ClientCommand, ClientEvent, ClientState],
|
||||||
|
CommandEngine.layer[ContractCommand, ContractEvent, ContractState],
|
||||||
ClientHandlers.layer,
|
ClientHandlers.layer,
|
||||||
|
ContractHandlers.layer,
|
||||||
ClientReducer.layer,
|
ClientReducer.layer,
|
||||||
|
ContractReducer.layer,
|
||||||
ClientEventRepositoryInMemory.layer,
|
ClientEventRepositoryInMemory.layer,
|
||||||
|
ContractEventRepositoryInMemory.layer,
|
||||||
ClientStateRepositoryInMemory.layer,
|
ClientStateRepositoryInMemory.layer,
|
||||||
ClientController.layer
|
ContractStateRepositoryInMemory.layer,
|
||||||
|
ClientController.layer,
|
||||||
|
ContractController.layer,
|
||||||
|
PremiumServiceImpl.layer,
|
||||||
|
EmployeeServiceImpl.layer
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package lu.foyer
|
package lu.foyer
|
||||||
|
|
||||||
|
import lu.foyer.JsonApiResponse.One
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.Console.*
|
import zio.Console.*
|
||||||
import zio.http.*
|
import zio.http.*
|
||||||
|
|
@ -11,34 +12,27 @@ import zio.schema.*
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import lu.foyer.JsonApiResponse.One
|
|
||||||
|
|
||||||
trait CommandEngineController[Command: Schema, Event: Schema, State: Schema](
|
trait CommandEngineController[Command: Schema, Event: Schema, State: Schema]
|
||||||
domain: String,
|
|
||||||
entityName: String)
|
|
||||||
extends JsonApiController:
|
extends JsonApiController:
|
||||||
|
|
||||||
def commandEngine: CommandEngine[Command, Event, State]
|
def commandEngine: CommandEngine[Command, Event, State]
|
||||||
|
|
||||||
val onthology = s"$domain:$entityName"
|
private lazy val fetchMany =
|
||||||
|
|
||||||
private val fetchMany =
|
|
||||||
Endpoint(Method.GET / entityName)
|
Endpoint(Method.GET / entityName)
|
||||||
.query(HttpCodec.query[Page])
|
// .query(HttpCodec.query[Page])
|
||||||
.jsonApiMany[State]
|
.jsonApiMany[State]
|
||||||
|
|
||||||
private val fetchOne =
|
private lazy val fetchOne =
|
||||||
Endpoint(Method.GET / entityName / uuid("entityId"))
|
Endpoint(Method.GET / entityName / uuid("entityId"))
|
||||||
.jsonApiOne[State]
|
.jsonApiOne[State]
|
||||||
|
|
||||||
private val fetchEventsMany =
|
private lazy val fetchEventsMany =
|
||||||
Endpoint(Method.GET / entityName / uuid("entityId") / "events")
|
Endpoint(Method.GET / entityName / uuid("entityId") / "events")
|
||||||
.query(HttpCodec.query[Page])
|
// .query(HttpCodec.query[Page])
|
||||||
.jsonApiMany[Event]
|
.jsonApiMany[Event]
|
||||||
|
|
||||||
private val fetchEventsOne: Endpoint[(UUID, UUID), (UUID, UUID), JsonApiResponse.Error, One[
|
private lazy val fetchEventsOne =
|
||||||
Event
|
|
||||||
], zio.http.endpoint.AuthType.None.type] =
|
|
||||||
Endpoint(Method.GET / entityName / uuid("entityId") / "events" / uuid("eventId"))
|
Endpoint(Method.GET / entityName / uuid("entityId") / "events" / uuid("eventId"))
|
||||||
.jsonApiOne[Event]
|
.jsonApiOne[Event]
|
||||||
|
|
||||||
|
|
@ -51,7 +45,7 @@ trait CommandEngineController[Command: Schema, Event: Schema, State: Schema](
|
||||||
given Schema[Command] = handler.commandSchema.asInstanceOf[Schema[Command]]
|
given Schema[Command] = handler.commandSchema.asInstanceOf[Schema[Command]]
|
||||||
val endpoint = Endpoint(Method.POST / entityName / "commands" / handler.name)
|
val endpoint = Endpoint(Method.POST / entityName / "commands" / handler.name)
|
||||||
.in[Command]
|
.in[Command]
|
||||||
.jsonApiOne[Event]
|
.jsonApiOneWithStatus[Event](Status.Created)
|
||||||
val route = endpoint.implementJsonApiOneEvent(command =>
|
val route = endpoint.implementJsonApiOneEvent(command =>
|
||||||
for
|
for
|
||||||
entityId <- Random.nextUUID
|
entityId <- Random.nextUUID
|
||||||
|
|
@ -73,32 +67,36 @@ trait CommandEngineController[Command: Schema, Event: Schema, State: Schema](
|
||||||
)
|
)
|
||||||
(endpoint, route)
|
(endpoint, route)
|
||||||
|
|
||||||
private val (commands, commandsRoutes) = generateCommands.unzip
|
private lazy val (commands, commandsRoutes) = generateCommands.unzip
|
||||||
|
|
||||||
private val fetchManyRoute =
|
private lazy val fetchManyRoute =
|
||||||
fetchMany.implementJsonApiManyEntity(commandEngine.stateRepo.fetchMany)
|
fetchMany.implementJsonApiManyEntity(_ =>
|
||||||
|
commandEngine.stateRepo.fetchMany(Page(None, None, totals = Some(true)))
|
||||||
|
)
|
||||||
|
|
||||||
private val fetchOneRoute =
|
private lazy val fetchOneRoute =
|
||||||
fetchOne.implementJsonApiOneEntity(commandEngine.stateRepo.fetchOne)
|
fetchOne.implementJsonApiOneEntity(commandEngine.stateRepo.fetchOne)
|
||||||
|
|
||||||
private val fetchEventsManyRoute =
|
private lazy val fetchEventsManyRoute =
|
||||||
fetchEventsMany.implementJsonApiManyEvent(commandEngine.eventRepo.fetchMany(_, _))
|
fetchEventsMany.implementJsonApiManyEvent(entityId =>
|
||||||
|
commandEngine.eventRepo.fetchMany(entityId, Page(None, None, totals = Some(true)))
|
||||||
|
)
|
||||||
|
|
||||||
private val fetchEventsOneRoute =
|
private lazy val fetchEventsOneRoute =
|
||||||
fetchEventsOne.implementJsonApiOneEvent(commandEngine.eventRepo.fetchOne(_, _))
|
fetchEventsOne.implementJsonApiOneEvent(commandEngine.eventRepo.fetchOne(_, _))
|
||||||
|
|
||||||
val endpoints = List(
|
lazy val endpoints = List(
|
||||||
fetchMany,
|
fetchMany,
|
||||||
fetchOne,
|
fetchOne,
|
||||||
fetchEventsMany,
|
fetchEventsMany,
|
||||||
fetchEventsOne
|
fetchEventsOne
|
||||||
) ++ commands
|
) ++ commands
|
||||||
|
|
||||||
val routes = Routes(
|
lazy val routes = (Routes(
|
||||||
fetchManyRoute,
|
fetchManyRoute,
|
||||||
fetchOneRoute,
|
fetchOneRoute,
|
||||||
fetchEventsManyRoute,
|
fetchEventsManyRoute,
|
||||||
fetchEventsOneRoute
|
fetchEventsOneRoute
|
||||||
) ++ Routes.fromIterable(commandsRoutes)
|
) ++ Routes.fromIterable(commandsRoutes)) @@ proxyHeadersAspect
|
||||||
|
|
||||||
end CommandEngineController
|
end CommandEngineController
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,45 @@
|
||||||
package lu.foyer
|
package lu.foyer
|
||||||
|
|
||||||
|
import lu.foyer.JsonApiResponse.Many
|
||||||
|
import lu.foyer.JsonApiResponse.One
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.schema.*
|
|
||||||
import zio.http.*
|
import zio.http.*
|
||||||
|
import zio.http.Header.Forwarded
|
||||||
import zio.http.codec.*
|
import zio.http.codec.*
|
||||||
import zio.http.codec.PathCodec.path
|
import zio.http.codec.PathCodec.path
|
||||||
import zio.http.endpoint.*
|
import zio.http.endpoint.*
|
||||||
import lu.foyer.JsonApiResponse.Many
|
import zio.schema.*
|
||||||
import lu.foyer.JsonApiResponse.One
|
import zio.schema.annotation.discriminatorName
|
||||||
|
import zio.schema.annotation.fieldName
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import scala.annotation.targetName
|
import scala.annotation.targetName
|
||||||
import zio.schema.annotation.discriminatorName
|
|
||||||
|
|
||||||
object JsonApiResponse:
|
object JsonApiResponse:
|
||||||
|
|
||||||
case class One[T](
|
case class One[T](data: Entity[T]) derives Schema
|
||||||
data: Entity[T],
|
|
||||||
links: Links)
|
case class Many[T](data: List[Entity[T]], links: Map[String, String], meta: Meta) derives Schema
|
||||||
|
|
||||||
|
case class Meta(totalRecords: Option[Long], totalPages: Option[Long], page: Page) derives Schema
|
||||||
|
case class Page(number: Int, size: Int) derives Schema
|
||||||
|
|
||||||
|
case class Entity[T](
|
||||||
|
`type`: String,
|
||||||
|
id: UUID,
|
||||||
|
attributes: T,
|
||||||
|
relationships: Option[Relationships] = None,
|
||||||
|
links: Map[String, String])
|
||||||
derives Schema
|
derives Schema
|
||||||
|
|
||||||
case class Many[T](
|
case class Relationships(_entity: RelationshipsEntity) derives Schema
|
||||||
data: List[Entity[T]],
|
object Relationships:
|
||||||
links: Links,
|
def apply(id: UUID, `type`: String, entityUrl: String): Relationships = Relationships(
|
||||||
meta: Meta)
|
RelationshipsEntity(RelationshipsData(id, `type`), RelationshipsLinks(entityUrl))
|
||||||
derives Schema
|
)
|
||||||
|
case class RelationshipsEntity(data: RelationshipsData, links: RelationshipsLinks) derives Schema
|
||||||
case class Links(
|
case class RelationshipsData(id: UUID, `type`: String) derives Schema
|
||||||
self: String,
|
case class RelationshipsLinks(related: String) derives Schema
|
||||||
first: Option[String] = None,
|
|
||||||
prev: Option[String] = None,
|
|
||||||
next: Option[String] = None,
|
|
||||||
last: Option[String] = None)
|
|
||||||
derives Schema
|
|
||||||
|
|
||||||
case class Meta(totalRecords: Option[Long], totalPages: Option[Long]) derives Schema
|
|
||||||
|
|
||||||
case class Entity[T](id: UUID, `type`: String, attributes: T) derives Schema
|
|
||||||
|
|
||||||
@discriminatorName("errorType")
|
@discriminatorName("errorType")
|
||||||
enum Error(title: String) derives Schema:
|
enum Error(title: String) derives Schema:
|
||||||
|
|
@ -46,16 +50,47 @@ object JsonApiResponse:
|
||||||
given Schema[Error.InternalServerError] = DeriveSchema.gen
|
given Schema[Error.InternalServerError] = DeriveSchema.gen
|
||||||
end JsonApiResponse
|
end JsonApiResponse
|
||||||
|
|
||||||
trait JsonApiController:
|
final case class ProxyHeaders(
|
||||||
|
protocol: Option[String],
|
||||||
|
host: Option[String],
|
||||||
|
prefix: Option[String])
|
||||||
|
derives Schema:
|
||||||
|
def rootUrl =
|
||||||
|
(for
|
||||||
|
proto <- protocol
|
||||||
|
host <- host
|
||||||
|
prefix <- prefix
|
||||||
|
yield s"$proto://$host$prefix")
|
||||||
|
.getOrElse("")
|
||||||
|
|
||||||
|
trait JsonApiController:
|
||||||
def onthology: String
|
def onthology: String
|
||||||
|
def entityName: String
|
||||||
|
def allEntities: List[String]
|
||||||
|
|
||||||
|
val proxyHeadersAspect = HandlerAspect.interceptIncomingHandler(
|
||||||
|
Handler.fromFunction[Request](request =>
|
||||||
|
(
|
||||||
|
request,
|
||||||
|
ProxyHeaders(
|
||||||
|
protocol = request.headers.get("X-Forwarded-Proto"),
|
||||||
|
host = request.headers.get("X-Forwarded-Host"),
|
||||||
|
prefix = request.headers.get("X-Forwarded-Prefix")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
extension [PathInput, Input, Auth <: AuthType](
|
extension [PathInput, Input, Auth <: AuthType](
|
||||||
endpoint: Endpoint[PathInput, Input, ZNothing, ZNothing, AuthType.None]
|
endpoint: Endpoint[PathInput, Input, ZNothing, ZNothing, AuthType.None]
|
||||||
)
|
)
|
||||||
def jsonApiOne[Output: Schema] =
|
inline def jsonApiOne[Output: Schema]
|
||||||
|
: Endpoint[PathInput, Input, JsonApiResponse.Error, One[Output], AuthType.None.type] =
|
||||||
|
jsonApiOneWithStatus(Status.Ok)
|
||||||
|
|
||||||
|
def jsonApiOneWithStatus[Output: Schema](status: Status) =
|
||||||
endpoint
|
endpoint
|
||||||
.out[JsonApiResponse.One[Output]]
|
.out[JsonApiResponse.One[Output]](status)
|
||||||
.outErrors[JsonApiResponse.Error](
|
.outErrors[JsonApiResponse.Error](
|
||||||
HttpCodec.error[JsonApiResponse.Error.NotFound](Status.NotFound),
|
HttpCodec.error[JsonApiResponse.Error.NotFound](Status.NotFound),
|
||||||
HttpCodec.error[JsonApiResponse.Error.InternalServerError](Status.InternalServerError)
|
HttpCodec.error[JsonApiResponse.Error.InternalServerError](Status.InternalServerError)
|
||||||
|
|
@ -71,21 +106,31 @@ trait JsonApiController:
|
||||||
def implementJsonApiOne[Env, A](
|
def implementJsonApiOne[Env, A](
|
||||||
f: Input => RIO[Env, Option[A]],
|
f: Input => RIO[Env, Option[A]],
|
||||||
getId: A => UUID,
|
getId: A => UUID,
|
||||||
getEntity: A => Output
|
getEntity: A => Output,
|
||||||
|
onthology: String = this.onthology,
|
||||||
|
links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty,
|
||||||
|
relationships: (A, ProxyHeaders) => Option[JsonApiResponse.Relationships] = (
|
||||||
|
_: A,
|
||||||
|
_: ProxyHeaders
|
||||||
|
) => None
|
||||||
)(implicit trace: Trace
|
)(implicit trace: Trace
|
||||||
): Route[Env, Nothing] =
|
): Route[Env & ProxyHeaders, Nothing] =
|
||||||
endpoint.implement(input =>
|
endpoint.implement(input =>
|
||||||
f(input)
|
for
|
||||||
|
item <- f(input)
|
||||||
.mapError(e => JsonApiResponse.Error.InternalServerError(e.getMessage))
|
.mapError(e => JsonApiResponse.Error.InternalServerError(e.getMessage))
|
||||||
.someOrFail(JsonApiResponse.Error.NotFound(input.toString))
|
.flatMap {
|
||||||
.map(item =>
|
case Some(paged) => ZIO.succeed(paged)
|
||||||
JsonApiResponse.One(
|
case None => ZIO.fail(JsonApiResponse.Error.NotFound(input.toString))
|
||||||
|
}
|
||||||
|
headers <- ZIO.service[ProxyHeaders]
|
||||||
|
yield JsonApiResponse.One(
|
||||||
JsonApiResponse.Entity(
|
JsonApiResponse.Entity(
|
||||||
id = getId(item),
|
id = getId(item),
|
||||||
`type` = onthology,
|
`type` = onthology,
|
||||||
attributes = getEntity(item)
|
attributes = getEntity(item),
|
||||||
),
|
relationships = relationships(item, headers),
|
||||||
JsonApiResponse.Links("https://api.example.org") // TODO
|
links = links(item, headers)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -93,14 +138,38 @@ trait JsonApiController:
|
||||||
def implementJsonApiOneEntity[Env](
|
def implementJsonApiOneEntity[Env](
|
||||||
f: Input => RIO[Env, Option[Entity[Output]]]
|
f: Input => RIO[Env, Option[Entity[Output]]]
|
||||||
)(implicit trace: Trace
|
)(implicit trace: Trace
|
||||||
): Route[Env, Nothing] =
|
): Route[Env & ProxyHeaders, Nothing] =
|
||||||
implementJsonApiOne(f, _.entityId, _.data)
|
implementJsonApiOne(
|
||||||
|
f,
|
||||||
|
_.entityId,
|
||||||
|
_.data,
|
||||||
|
links = (e, headers) =>
|
||||||
|
Map(
|
||||||
|
"self" -> s"${headers.rootUrl}/$entityName/${e.entityId}",
|
||||||
|
"events" -> s"${headers.rootUrl}/$entityName/${e.entityId}/events"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def implementJsonApiOneEvent[Env](
|
def implementJsonApiOneEvent[Env](
|
||||||
f: Input => RIO[Env, Option[Event[Output]]]
|
f: Input => RIO[Env, Option[Event[Output]]]
|
||||||
)(implicit trace: Trace
|
)(implicit trace: Trace
|
||||||
): Route[Env, Nothing] =
|
): Route[Env & ProxyHeaders, Nothing] =
|
||||||
implementJsonApiOne(f, _.eventId, _.data)
|
implementJsonApiOne(
|
||||||
|
f,
|
||||||
|
_.eventId,
|
||||||
|
_.data,
|
||||||
|
onthology + ":event",
|
||||||
|
links = (e, headers) =>
|
||||||
|
Map("self" -> s"${headers.rootUrl}/$entityName/${e.entityId}/events/${e.eventId}"),
|
||||||
|
relationships = (e, headers) =>
|
||||||
|
Some(
|
||||||
|
JsonApiResponse.Relationships(
|
||||||
|
e.entityId,
|
||||||
|
onthology,
|
||||||
|
s"${headers.rootUrl}/$entityName/${e.entityId}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
end extension
|
end extension
|
||||||
|
|
||||||
extension [PathInput, Input, Output, Auth <: AuthType](
|
extension [PathInput, Input, Output, Auth <: AuthType](
|
||||||
|
|
@ -109,33 +178,60 @@ trait JsonApiController:
|
||||||
def implementJsonApiMany[Env, A](
|
def implementJsonApiMany[Env, A](
|
||||||
f: Input => RIO[Env, Paged[A]],
|
f: Input => RIO[Env, Paged[A]],
|
||||||
getId: A => UUID,
|
getId: A => UUID,
|
||||||
getEntity: A => Output
|
getEntity: A => Output,
|
||||||
|
links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty
|
||||||
)(implicit trace: Trace
|
)(implicit trace: Trace
|
||||||
): Route[Env, Nothing] =
|
): Route[Env & ProxyHeaders, Nothing] =
|
||||||
endpoint.implement(
|
endpoint.implement(input =>
|
||||||
f(_)
|
(for
|
||||||
.map(paged =>
|
paged <- f(input)
|
||||||
JsonApiResponse.Many(
|
headers <- ZIO.service[ProxyHeaders]
|
||||||
|
yield JsonApiResponse.Many(
|
||||||
paged.items.map(item =>
|
paged.items.map(item =>
|
||||||
JsonApiResponse.Entity(id = getId(item), `type` = onthology, getEntity(item))
|
JsonApiResponse.Entity(
|
||||||
|
id = getId(item),
|
||||||
|
`type` = onthology,
|
||||||
|
attributes = getEntity(item),
|
||||||
|
links = links(item, headers)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
JsonApiResponse.Links("https://api.example.org"),
|
links = Map( // TODO compute proper pagination
|
||||||
meta = JsonApiResponse.Meta(paged.totals, None) // TODO
|
"self" -> s"${headers.rootUrl}/$entityName",
|
||||||
|
"prev" -> s"${headers.rootUrl}/$entityName?page[number]=0&page[size]=10",
|
||||||
|
"first" -> s"${headers.rootUrl}/$entityName?page[number]=0&page[size]=10",
|
||||||
|
"next" -> s"${headers.rootUrl}/$entityName?page[number]=0&page[size]=10",
|
||||||
|
"last" -> s"${headers.rootUrl}/$entityName?page[number]=0&page[size]=10"
|
||||||
)
|
)
|
||||||
|
++ allEntities.map(e => e -> s"${headers.rootUrl}/$e"),
|
||||||
|
meta = JsonApiResponse
|
||||||
|
.Meta(
|
||||||
|
totalRecords = paged.totals,
|
||||||
|
totalPages = Some(1),
|
||||||
|
page = JsonApiResponse.Page(number = 0, size = 10)
|
||||||
)
|
)
|
||||||
|
))
|
||||||
.mapError(e => JsonApiResponse.Error.InternalServerError(e.getMessage))
|
.mapError(e => JsonApiResponse.Error.InternalServerError(e.getMessage))
|
||||||
)
|
)
|
||||||
|
|
||||||
inline def implementJsonApiManyEntity[Env](
|
inline def implementJsonApiManyEntity[Env](
|
||||||
f: Input => RIO[Env, Paged[Entity[Output]]]
|
f: Input => RIO[Env, Paged[Entity[Output]]]
|
||||||
)(implicit trace: Trace
|
)(implicit trace: Trace
|
||||||
): Route[Env, Nothing] =
|
): Route[Env & ProxyHeaders, Nothing] =
|
||||||
implementJsonApiMany(f, _.entityId, _.data)
|
implementJsonApiMany(
|
||||||
|
f,
|
||||||
|
_.entityId,
|
||||||
|
_.data,
|
||||||
|
links = (e, headers) =>
|
||||||
|
Map(
|
||||||
|
"self" -> s"${headers.rootUrl}/$entityName/${e.entityId}",
|
||||||
|
"events" -> s"${headers.rootUrl}/$entityName/${e.entityId}/events"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
inline def implementJsonApiManyEvent[Env](
|
inline def implementJsonApiManyEvent[Env](
|
||||||
f: Input => RIO[Env, Paged[Event[Output]]]
|
f: Input => RIO[Env, Paged[Event[Output]]]
|
||||||
)(implicit trace: Trace
|
)(implicit trace: Trace
|
||||||
): Route[Env, Nothing] =
|
): Route[Env & ProxyHeaders, Nothing] =
|
||||||
implementJsonApiMany(f, _.eventId, _.data)
|
implementJsonApiMany(f, _.eventId, _.data)
|
||||||
|
|
||||||
end extension
|
end extension
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,11 @@ import java.net.URI
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class ClientController(
|
class ClientController(val commandEngine: CommandEngine[ClientCommand, ClientEvent, ClientState])
|
||||||
override val commandEngine: CommandEngine[ClientCommand, ClientEvent, ClientState])
|
extends CommandEngineController[ClientCommand, ClientEvent, ClientState]:
|
||||||
extends CommandEngineController[ClientCommand, ClientEvent, ClientState](
|
override val onthology = "org:example:insurance:client"
|
||||||
"api:example:insurance",
|
override val entityName = "clients"
|
||||||
"client"
|
override val allEntities = List("clients", "contracts")
|
||||||
)
|
|
||||||
|
|
||||||
object ClientController:
|
object ClientController:
|
||||||
val layer = ZLayer.fromFunction(ClientController.apply)
|
val layer = ZLayer.fromFunction(ClientController.apply)
|
||||||
|
|
|
||||||
24
api/src/lu/foyer/contracts/ContractController.scala
Normal file
24
api/src/lu/foyer/contracts/ContractController.scala
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
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])
|
||||||
|
extends CommandEngineController[ContractCommand, ContractEvent, ContractState]:
|
||||||
|
override val onthology = "org:example:insurance:contract"
|
||||||
|
override val entityName = "contracts"
|
||||||
|
override val allEntities = List("clients", "contracts")
|
||||||
|
|
||||||
|
object ContractController:
|
||||||
|
val layer = ZLayer.fromFunction(ContractController.apply)
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import zio.*
|
||||||
|
|
||||||
|
class ContractEventRepositoryInMemory(events: Ref[Map[UUID, Event[ContractEvent]]])
|
||||||
|
extends EventRepository[ContractEvent]
|
||||||
|
with InMemoryRepository[Event[ContractEvent]](events):
|
||||||
|
def fetchOne(entityId: UUID, eventId: UUID): Task[Option[Event[ContractEvent]]] =
|
||||||
|
events.get.map(_.get(eventId))
|
||||||
|
def fetchMany(entityId: UUID, 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
object ContractEventRepositoryInMemory:
|
||||||
|
val layer = ZLayer.fromZIO(Ref.make(Map.empty).map(ContractEventRepositoryInMemory(_)))
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import zio.*
|
||||||
|
|
||||||
|
class ContractStateRepositoryInMemory(clients: Ref[Map[UUID, Entity[ContractState]]])
|
||||||
|
extends StateRepository[ContractState]
|
||||||
|
with InMemoryRepository[Entity[ContractState]](clients)
|
||||||
|
|
||||||
|
object ContractStateRepositoryInMemory:
|
||||||
|
val layer = ZLayer.fromZIO(Ref.make(Map.empty).map(ContractStateRepositoryInMemory(_)))
|
||||||
24
api/src/lu/foyer/contracts/EmployeeServiceImpl.scala
Normal file
24
api/src/lu/foyer/contracts/EmployeeServiceImpl.scala
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
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:
|
||||||
|
|
||||||
|
def fetchOne(subject: String): Either[String, Employee] =
|
||||||
|
if subject == "usr:top" then
|
||||||
|
Right(
|
||||||
|
Employee(
|
||||||
|
subject,
|
||||||
|
EmployeeDisplayName.assume("MEIER Christoph"),
|
||||||
|
Email.assume("top@foyer.lu")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else Left("Invalid Employee")
|
||||||
|
|
||||||
|
val layer = ZLayer.succeed(EmployeeServiceImpl)
|
||||||
53
api/src/lu/foyer/contracts/PremiumServiceImpl.scala
Normal file
53
api/src/lu/foyer/contracts/PremiumServiceImpl.scala
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
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 PremiumServiceImpl extends PremiumService:
|
||||||
|
private val EUR = Currency.getInstance("EUR")
|
||||||
|
|
||||||
|
private val brands =
|
||||||
|
Map(
|
||||||
|
"renault" -> BigDecimal(0.5),
|
||||||
|
"fiat" -> BigDecimal(0.6),
|
||||||
|
"ford" -> BigDecimal(0.7),
|
||||||
|
"nissan" -> BigDecimal(0.75),
|
||||||
|
"peugeot" -> BigDecimal(0.8),
|
||||||
|
"volkswagen" -> BigDecimal(1.1),
|
||||||
|
"audi" -> BigDecimal(1.25),
|
||||||
|
"bmw" -> BigDecimal(1.25),
|
||||||
|
"mercedes" -> BigDecimal(1.25),
|
||||||
|
"ferrari" -> BigDecimal(2.3),
|
||||||
|
"bugatti" -> BigDecimal(2.6),
|
||||||
|
"tesla" -> BigDecimal(5.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val countries =
|
||||||
|
Map(Country.CH -> BigDecimal(0.8), Country.LU -> BigDecimal(1.2))
|
||||||
|
|
||||||
|
private def computeAmountEUR(base: BigDecimal, brand: VehicleBrand, country: Country)
|
||||||
|
: BigDecimal =
|
||||||
|
val brandMultiplier = brands.getOrElse(brand.toLowerCase, BigDecimal(1))
|
||||||
|
val countryMultiplier = countries.getOrElse(country, BigDecimal(1))
|
||||||
|
val amount = base * brandMultiplier * countryMultiplier
|
||||||
|
amount.setScale(EUR.getDefaultFractionDigits, RoundingMode.HALF_UP).rounded
|
||||||
|
|
||||||
|
override def computePremium(formula: FormulaType, vehicle: Vehicle, residentialAddress: Address)
|
||||||
|
: Either[String, Amount] =
|
||||||
|
if vehicle.insuredValue.value > formula.maxCoverage then
|
||||||
|
Left(s"The $formula formula only covers amounts up to ${formula.maxCoverage} EUR")
|
||||||
|
else
|
||||||
|
Right(
|
||||||
|
Amount(
|
||||||
|
computeAmountEUR(formula.basePremium, vehicle.brand, residentialAddress.country),
|
||||||
|
EUR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val layer = ZLayer.succeed(PremiumServiceImpl)
|
||||||
|
end PremiumServiceImpl
|
||||||
8
build.sc
8
build.sc
|
|
@ -11,7 +11,7 @@ object Versions {
|
||||||
val zio = "2.1.15"
|
val zio = "2.1.15"
|
||||||
val zioJson = "0.7.33"
|
val zioJson = "0.7.33"
|
||||||
val zioSchema = "1.6.3"
|
val zioSchema = "1.6.3"
|
||||||
val zioHttp = "3.0.1+97-29d12531-SNAPSHOT"
|
val zioHttp = "3.1.0"
|
||||||
val zioPrelude = "1.0.0-RC39"
|
val zioPrelude = "1.0.0-RC39"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,6 +19,8 @@ trait CommonModule extends ScalaModule {
|
||||||
def scalaVersion = "3.6.3"
|
def scalaVersion = "3.6.3"
|
||||||
def ivyDeps = Agg(
|
def ivyDeps = Agg(
|
||||||
ivy"dev.zio::zio:${Versions.zio}",
|
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:${Versions.zioSchema}",
|
||||||
ivy"dev.zio::zio-schema-derivation:${Versions.zioSchema}",
|
ivy"dev.zio::zio-schema-derivation:${Versions.zioSchema}",
|
||||||
ivy"dev.zio::zio-prelude:${Versions.zioPrelude}"
|
ivy"dev.zio::zio-prelude:${Versions.zioPrelude}"
|
||||||
|
|
@ -38,8 +40,6 @@ object api extends CommonModule {
|
||||||
def moduleDeps = Seq(core)
|
def moduleDeps = Seq(core)
|
||||||
def ivyDeps = Agg(
|
def ivyDeps = Agg(
|
||||||
ivy"dev.zio::zio:${Versions.zio}",
|
ivy"dev.zio::zio:${Versions.zio}",
|
||||||
ivy"dev.zio::zio-http:${Versions.zioHttp}",
|
ivy"dev.zio::zio-http:${Versions.zioHttp}"
|
||||||
ivy"dev.zio::zio-json:${Versions.zioJson}",
|
|
||||||
ivy"dev.zio::zio-schema-json:${Versions.zioSchema}"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package lu.foyer
|
package lu.foyer
|
||||||
|
|
||||||
import zio.*
|
import zio.*
|
||||||
import java.util.UUID
|
|
||||||
import zio.schema.Schema
|
import zio.schema.Schema
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
final case class Entity[T](entityId: UUID, data: T, version: Long)
|
final case class Entity[T](entityId: UUID, data: T, version: Long)
|
||||||
final case class Event[T](entityId: UUID, data: T, eventId: UUID)
|
final case class Event[T](entityId: UUID, data: T, eventId: UUID)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,17 @@ package lu.foyer
|
||||||
|
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.schema.*
|
import zio.schema.*
|
||||||
|
import zio.schema.annotation.fieldName
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
final case class Page(number: Option[Int], size: Option[Int], totals: Option[Boolean])
|
final case class Page(
|
||||||
|
@fieldName("page[number]")
|
||||||
|
number: Option[Int],
|
||||||
|
@fieldName("page[size]")
|
||||||
|
size: Option[Int],
|
||||||
|
@fieldName("page[totals]")
|
||||||
|
totals: Option[Boolean])
|
||||||
derives Schema
|
derives Schema
|
||||||
final case class Paged[T](items: List[T], totals: Option[Long])
|
final case class Paged[T](items: List[T], totals: Option[Long])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,17 @@ package lu.foyer
|
||||||
package clients
|
package clients
|
||||||
|
|
||||||
import zio.schema.*
|
import zio.schema.*
|
||||||
|
import zio.schema.annotation.caseName
|
||||||
import java.time.LocalDate
|
|
||||||
import zio.schema.annotation.discriminatorName
|
import zio.schema.annotation.discriminatorName
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
@discriminatorName("eventType")
|
@discriminatorName("eventType")
|
||||||
enum ClientEvent derives Schema:
|
sealed trait ClientEvent derives Schema
|
||||||
case Created(
|
object ClientEvent:
|
||||||
|
|
||||||
|
@caseName("created")
|
||||||
|
final case class Created(
|
||||||
lastName: ClientLastName,
|
lastName: ClientLastName,
|
||||||
firstName: ClientFirstName,
|
firstName: ClientFirstName,
|
||||||
birthDate: ClientBirthDate,
|
birthDate: ClientBirthDate,
|
||||||
|
|
@ -16,7 +20,10 @@ enum ClientEvent derives Schema:
|
||||||
phoneNumber: Option[PhoneNumberInput],
|
phoneNumber: Option[PhoneNumberInput],
|
||||||
email: Option[Email],
|
email: Option[Email],
|
||||||
address: Option[Address])
|
address: Option[Address])
|
||||||
case Updated(
|
extends ClientEvent derives Schema
|
||||||
|
|
||||||
|
@caseName("updated")
|
||||||
|
final case class Updated(
|
||||||
lastName: Option[ClientLastName],
|
lastName: Option[ClientLastName],
|
||||||
firstName: Option[ClientFirstName],
|
firstName: Option[ClientFirstName],
|
||||||
birthDate: Option[ClientBirthDate],
|
birthDate: Option[ClientBirthDate],
|
||||||
|
|
@ -24,9 +31,7 @@ enum ClientEvent derives Schema:
|
||||||
phoneNumber: Option[PhoneNumberInput],
|
phoneNumber: Option[PhoneNumberInput],
|
||||||
email: Option[Email],
|
email: Option[Email],
|
||||||
address: Option[Address])
|
address: Option[Address])
|
||||||
case Disabled(reason: ClientDisabledReason)
|
extends ClientEvent derives Schema
|
||||||
|
|
||||||
object ClientEvent:
|
@caseName("disabled")
|
||||||
given Schema[ClientEvent.Created] = DeriveSchema.gen
|
final case class Disabled(reason: ClientDisabledReason) extends ClientEvent derives Schema
|
||||||
given Schema[ClientEvent.Updated] = DeriveSchema.gen
|
|
||||||
given Schema[ClientEvent.Disabled] = DeriveSchema.gen
|
|
||||||
|
|
|
||||||
|
|
@ -2,30 +2,59 @@ package lu.foyer
|
||||||
package clients
|
package clients
|
||||||
|
|
||||||
import zio.schema.*
|
import zio.schema.*
|
||||||
|
import zio.schema.annotation.caseName
|
||||||
import zio.schema.annotation.discriminatorName
|
import zio.schema.annotation.discriminatorName
|
||||||
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
@discriminatorName("statusType")
|
@discriminatorName("statusType")
|
||||||
enum ClientState derives Schema:
|
sealed trait ClientState derives Schema
|
||||||
case Actif(
|
|
||||||
lastName: ClientLastName,
|
|
||||||
firstName: ClientFirstName,
|
|
||||||
birthDate: ClientBirthDate,
|
|
||||||
drivingLicenseDate: Option[ClientDrivingLicenseDate],
|
|
||||||
phoneNumber: Option[PhoneNumberInput],
|
|
||||||
email: Option[Email],
|
|
||||||
address: Option[Address])
|
|
||||||
case Inactif(
|
|
||||||
lastName: ClientLastName,
|
|
||||||
firstName: ClientFirstName,
|
|
||||||
birthDate: ClientBirthDate,
|
|
||||||
drivingLicenseDate: Option[ClientDrivingLicenseDate],
|
|
||||||
phoneNumber: Option[PhoneNumberInput],
|
|
||||||
email: Option[Email],
|
|
||||||
address: Option[Address])
|
|
||||||
|
|
||||||
object ClientState:
|
object ClientState:
|
||||||
|
|
||||||
|
@caseName("actif")
|
||||||
|
final case class Actif(
|
||||||
|
lastName: ClientLastName,
|
||||||
|
firstName: ClientFirstName,
|
||||||
|
birthDate: ClientBirthDate,
|
||||||
|
drivingLicenseDate: Option[ClientDrivingLicenseDate],
|
||||||
|
phoneNumber: Option[PhoneNumberInput],
|
||||||
|
email: Option[Email],
|
||||||
|
address: Option[Address])
|
||||||
|
extends ClientState derives Schema:
|
||||||
|
|
||||||
|
def update(e: ClientEvent.Updated) =
|
||||||
|
ClientState.Actif(
|
||||||
|
e.lastName.getOrElse(lastName),
|
||||||
|
e.firstName.getOrElse(firstName),
|
||||||
|
e.birthDate.getOrElse(birthDate),
|
||||||
|
e.drivingLicenseDate.orElse(drivingLicenseDate),
|
||||||
|
e.phoneNumber.orElse(phoneNumber),
|
||||||
|
e.email.orElse(email),
|
||||||
|
e.address.orElse(address)
|
||||||
|
)
|
||||||
|
def disable(e: ClientEvent.Disabled) =
|
||||||
|
ClientState.Inactif(
|
||||||
|
lastName,
|
||||||
|
firstName,
|
||||||
|
birthDate,
|
||||||
|
drivingLicenseDate,
|
||||||
|
phoneNumber,
|
||||||
|
email,
|
||||||
|
address
|
||||||
|
)
|
||||||
|
end Actif
|
||||||
|
|
||||||
|
@caseName("inactif")
|
||||||
|
final case class Inactif(
|
||||||
|
lastName: ClientLastName,
|
||||||
|
firstName: ClientFirstName,
|
||||||
|
birthDate: ClientBirthDate,
|
||||||
|
drivingLicenseDate: Option[ClientDrivingLicenseDate],
|
||||||
|
phoneNumber: Option[PhoneNumberInput],
|
||||||
|
email: Option[Email],
|
||||||
|
address: Option[Address])
|
||||||
|
extends ClientState derives Schema
|
||||||
|
|
||||||
def create(event: ClientEvent.Created) =
|
def create(event: ClientEvent.Created) =
|
||||||
ClientState.Actif(
|
ClientState.Actif(
|
||||||
event.lastName,
|
event.lastName,
|
||||||
|
|
@ -36,25 +65,4 @@ object ClientState:
|
||||||
event.email,
|
event.email,
|
||||||
event.address
|
event.address
|
||||||
)
|
)
|
||||||
|
end ClientState
|
||||||
extension (s: ClientState.Actif)
|
|
||||||
def update(e: ClientEvent.Updated) =
|
|
||||||
ClientState.Actif(
|
|
||||||
e.lastName.getOrElse(s.lastName),
|
|
||||||
e.firstName.getOrElse(s.firstName),
|
|
||||||
e.birthDate.getOrElse(s.birthDate),
|
|
||||||
e.drivingLicenseDate.orElse(s.drivingLicenseDate),
|
|
||||||
e.phoneNumber.orElse(s.phoneNumber),
|
|
||||||
e.email.orElse(s.email),
|
|
||||||
e.address.orElse(s.address)
|
|
||||||
)
|
|
||||||
def disable(e: ClientEvent.Disabled) =
|
|
||||||
ClientState.Inactif(
|
|
||||||
s.lastName,
|
|
||||||
s.firstName,
|
|
||||||
s.birthDate,
|
|
||||||
s.drivingLicenseDate,
|
|
||||||
s.phoneNumber,
|
|
||||||
s.email,
|
|
||||||
s.address
|
|
||||||
)
|
|
||||||
|
|
|
||||||
28
core/src/lu/foyer/contracts/ContractCommand.scala
Normal file
28
core/src/lu/foyer/contracts/ContractCommand.scala
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import zio.schema.*
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
enum ContractCommand derives Schema:
|
||||||
|
case Subscribe(
|
||||||
|
product: ProductType,
|
||||||
|
holder: ClientEntityId,
|
||||||
|
vehicle: Vehicle,
|
||||||
|
formula: FormulaType)
|
||||||
|
case Amend(
|
||||||
|
product: Option[ProductType],
|
||||||
|
vehicle: Option[Vehicle],
|
||||||
|
formula: Option[FormulaType])
|
||||||
|
case Approve()
|
||||||
|
case Reject(comment: String)
|
||||||
|
case Terminate(reason: TerminationReasonType)
|
||||||
|
|
||||||
|
object ContractCommand:
|
||||||
|
given Schema[ContractCommand.Subscribe] = DeriveSchema.gen
|
||||||
|
given Schema[ContractCommand.Amend] = DeriveSchema.gen
|
||||||
|
given Schema[ContractCommand.Approve] = DeriveSchema.gen
|
||||||
|
given Schema[ContractCommand.Reject] = DeriveSchema.gen
|
||||||
|
given Schema[ContractCommand.Terminate] = DeriveSchema.gen
|
||||||
39
core/src/lu/foyer/contracts/ContractEvent.scala
Normal file
39
core/src/lu/foyer/contracts/ContractEvent.scala
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
@caseName("created")
|
||||||
|
final case class Subscribed(
|
||||||
|
product: ProductType,
|
||||||
|
holder: ClientEntityId,
|
||||||
|
vehicle: Vehicle,
|
||||||
|
formula: FormulaType,
|
||||||
|
premium: Amount)
|
||||||
|
extends ContractEvent derives Schema
|
||||||
|
|
||||||
|
@caseName("amended")
|
||||||
|
final case class Amended(
|
||||||
|
product: ProductType,
|
||||||
|
vehicle: Vehicle,
|
||||||
|
formula: FormulaType,
|
||||||
|
premium: Amount)
|
||||||
|
extends ContractEvent derives Schema
|
||||||
|
|
||||||
|
@caseName("approved")
|
||||||
|
final case class Approved(approvedBy: Employee) extends ContractEvent derives Schema
|
||||||
|
|
||||||
|
@caseName("rejected")
|
||||||
|
final case class Rejected(rejectedBy: Employee, comment: String) extends ContractEvent
|
||||||
|
derives Schema
|
||||||
|
|
||||||
|
@caseName("terminated")
|
||||||
|
final case class Terminated(reason: TerminationReasonType) extends ContractEvent derives Schema
|
||||||
137
core/src/lu/foyer/contracts/ContractHandlers.scala
Normal file
137
core/src/lu/foyer/contracts/ContractHandlers.scala
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import lu.foyer.clients.*
|
||||||
|
import zio.*
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
object ContractHandlers:
|
||||||
|
val layer =
|
||||||
|
ZLayer.fromFunction(
|
||||||
|
(
|
||||||
|
clientStateRepo: StateRepository[ClientState],
|
||||||
|
premiumService: PremiumService,
|
||||||
|
employeeService: EmployeeService
|
||||||
|
) =>
|
||||||
|
List(
|
||||||
|
SubscribeHandler(clientStateRepo, premiumService),
|
||||||
|
AmendHandler(clientStateRepo, premiumService),
|
||||||
|
ApproveHandler(employeeService),
|
||||||
|
RejectHandler(employeeService),
|
||||||
|
TerminateHandler()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class SubscribeHandler(
|
||||||
|
clientStateRepo: StateRepository[ClientState],
|
||||||
|
premiumService: PremiumService)
|
||||||
|
extends CommandHandlerCreate[ContractCommand.Subscribe, ContractEvent.Subscribed]:
|
||||||
|
|
||||||
|
val name = "create"
|
||||||
|
|
||||||
|
def onCommand(entityId: UUID, command: ContractCommand.Subscribe)
|
||||||
|
: Task[ContractEvent.Subscribed] =
|
||||||
|
for
|
||||||
|
holder <- clientStateRepo.fetchOne(command.holder).someOrFailException
|
||||||
|
residentialAddress <-
|
||||||
|
holder.data match
|
||||||
|
case ClientState.Actif(_, _, _, _, _, _, Some(address)) =>
|
||||||
|
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(
|
||||||
|
premiumService.computePremium(command.formula, command.vehicle, residentialAddress)
|
||||||
|
)
|
||||||
|
.mapError(new IllegalArgumentException(_))
|
||||||
|
yield ContractEvent.Subscribed(
|
||||||
|
command.product,
|
||||||
|
command.holder,
|
||||||
|
command.vehicle,
|
||||||
|
command.formula,
|
||||||
|
premium
|
||||||
|
)
|
||||||
|
end SubscribeHandler
|
||||||
|
|
||||||
|
class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService: PremiumService)
|
||||||
|
extends CommandHandlerUpdate[ContractCommand.Amend, ContractEvent.Amended, ContractState]:
|
||||||
|
|
||||||
|
val name = "amend"
|
||||||
|
|
||||||
|
def onCommand(entityId: UUID, state: ContractState, command: ContractCommand.Amend)
|
||||||
|
: Task[ContractEvent.Amended] =
|
||||||
|
for
|
||||||
|
holder <- clientStateRepo.fetchOne(state.holder).someOrFailException
|
||||||
|
residentialAddress <-
|
||||||
|
holder.data match
|
||||||
|
case ClientState.Actif(_, _, _, _, _, _, Some(address)) =>
|
||||||
|
ZIO.succeed(address)
|
||||||
|
case _ =>
|
||||||
|
ZIO.fail(new IllegalArgumentException(s"No active holder found for ${state.holder}"))
|
||||||
|
premium <- ZIO
|
||||||
|
.fromEither(
|
||||||
|
premiumService.computePremium(
|
||||||
|
command.formula.getOrElse(state.formula),
|
||||||
|
command.vehicle.getOrElse(state.vehicle),
|
||||||
|
residentialAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.mapError(new IllegalArgumentException(_))
|
||||||
|
yield ContractEvent.Amended(
|
||||||
|
command.product.getOrElse(state.product),
|
||||||
|
command.vehicle.getOrElse(state.vehicle),
|
||||||
|
command.formula.getOrElse(state.formula),
|
||||||
|
premium
|
||||||
|
)
|
||||||
|
end AmendHandler
|
||||||
|
|
||||||
|
class ApproveHandler(employeeService: EmployeeService)
|
||||||
|
extends CommandHandlerUpdate[
|
||||||
|
ContractCommand.Approve,
|
||||||
|
ContractEvent.Approved,
|
||||||
|
ContractState.Pending
|
||||||
|
]:
|
||||||
|
|
||||||
|
val name = "approve"
|
||||||
|
|
||||||
|
def onCommand(entityId: UUID, state: ContractState.Pending, command: ContractCommand.Approve)
|
||||||
|
: Task[ContractEvent.Approved] =
|
||||||
|
for
|
||||||
|
user <- ZIO.succeed("") // TODO current user
|
||||||
|
employee <- ZIO
|
||||||
|
.fromEither(employeeService.fetchOne(user))
|
||||||
|
.mapError(new IllegalArgumentException(_))
|
||||||
|
yield ContractEvent.Approved(employee)
|
||||||
|
|
||||||
|
class RejectHandler(employeeService: EmployeeService)
|
||||||
|
extends CommandHandlerUpdate[
|
||||||
|
ContractCommand.Reject,
|
||||||
|
ContractEvent.Rejected,
|
||||||
|
ContractState.Pending
|
||||||
|
]:
|
||||||
|
|
||||||
|
val name = "reject"
|
||||||
|
|
||||||
|
def onCommand(entityId: UUID, state: ContractState.Pending, command: ContractCommand.Reject)
|
||||||
|
: Task[ContractEvent.Rejected] =
|
||||||
|
for
|
||||||
|
user <- ZIO.succeed("") // TODO current user
|
||||||
|
employee <- ZIO
|
||||||
|
.fromEither(employeeService.fetchOne(user))
|
||||||
|
.mapError(new IllegalArgumentException(_))
|
||||||
|
yield ContractEvent.Rejected(employee, command.comment)
|
||||||
|
|
||||||
|
class TerminateHandler()
|
||||||
|
extends CommandHandlerUpdate[
|
||||||
|
ContractCommand.Terminate,
|
||||||
|
ContractEvent.Terminated,
|
||||||
|
ContractState
|
||||||
|
]:
|
||||||
|
|
||||||
|
val name = "reject"
|
||||||
|
|
||||||
|
def onCommand(entityId: UUID, state: ContractState, command: ContractCommand.Terminate)
|
||||||
|
: Task[ContractEvent.Terminated] =
|
||||||
|
ZIO.succeed(ContractEvent.Terminated(command.reason))
|
||||||
26
core/src/lu/foyer/contracts/ContractReducer.scala
Normal file
26
core/src/lu/foyer/contracts/ContractReducer.scala
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import zio.*
|
||||||
|
|
||||||
|
class ContractReducer() extends Reducer[ContractEvent, ContractState]:
|
||||||
|
|
||||||
|
override val fromEmpty =
|
||||||
|
case e: ContractEvent.Subscribed => ContractState.subscribe(e)
|
||||||
|
|
||||||
|
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.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.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)
|
||||||
|
|
||||||
|
object ContractReducer:
|
||||||
|
val layer: ULayer[Reducer[ContractEvent, ContractState]] = ZLayer.succeed(ContractReducer())
|
||||||
106
core/src/lu/foyer/contracts/ContractState.scala
Normal file
106
core/src/lu/foyer/contracts/ContractState.scala
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
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
|
||||||
|
def holder: ClientEntityId
|
||||||
|
def vehicle: Vehicle
|
||||||
|
def formula: FormulaType
|
||||||
|
def premium: Amount
|
||||||
|
|
||||||
|
object ContractState:
|
||||||
|
|
||||||
|
def subscribe(e: ContractEvent.Subscribed) =
|
||||||
|
PendingSubscription(e.product, e.holder, e.vehicle, e.formula, e.premium)
|
||||||
|
|
||||||
|
@caseName("actif")
|
||||||
|
final case class Actif(
|
||||||
|
product: ProductType,
|
||||||
|
holder: ClientEntityId,
|
||||||
|
vehicle: Vehicle,
|
||||||
|
formula: FormulaType,
|
||||||
|
premium: Amount)
|
||||||
|
extends ContractState derives Schema:
|
||||||
|
|
||||||
|
def amend(e: ContractEvent.Amended) =
|
||||||
|
Actif(e.product, holder, e.vehicle, e.formula, e.premium)
|
||||||
|
|
||||||
|
def terminate(e: ContractEvent.Terminated) =
|
||||||
|
Terminated(product, holder, vehicle, formula, premium)
|
||||||
|
|
||||||
|
@discriminatorName("statusType")
|
||||||
|
sealed trait Pending extends ContractState
|
||||||
|
|
||||||
|
@caseName("pending-subscription")
|
||||||
|
final case class PendingSubscription(
|
||||||
|
product: ProductType,
|
||||||
|
holder: ClientEntityId,
|
||||||
|
vehicle: Vehicle,
|
||||||
|
formula: FormulaType,
|
||||||
|
premium: Amount)
|
||||||
|
extends Pending derives Schema:
|
||||||
|
|
||||||
|
def amend(e: ContractEvent.Amended) =
|
||||||
|
PendingSubscription(e.product, holder, e.vehicle, e.formula, e.premium)
|
||||||
|
|
||||||
|
def approve(e: ContractEvent.Approved) =
|
||||||
|
Actif(product, holder, vehicle, formula, premium)
|
||||||
|
|
||||||
|
def reject(e: ContractEvent.Rejected) =
|
||||||
|
Terminated(product, holder, vehicle, formula, premium)
|
||||||
|
|
||||||
|
def terminate(e: ContractEvent.Terminated) =
|
||||||
|
Terminated(product, holder, vehicle, formula, premium)
|
||||||
|
|
||||||
|
final case class PendingChanges(
|
||||||
|
product: ProductType,
|
||||||
|
vehicle: Vehicle,
|
||||||
|
formula: FormulaType,
|
||||||
|
premium: Amount)
|
||||||
|
derives Schema
|
||||||
|
|
||||||
|
@caseName("pending-amendment")
|
||||||
|
final case class PendingAmendment(
|
||||||
|
product: ProductType,
|
||||||
|
holder: ClientEntityId,
|
||||||
|
vehicle: Vehicle,
|
||||||
|
formula: FormulaType,
|
||||||
|
premium: Amount,
|
||||||
|
pendingChanges: PendingChanges)
|
||||||
|
extends Pending derives Schema:
|
||||||
|
|
||||||
|
def amend(e: ContractEvent.Amended) =
|
||||||
|
copy(pendingChanges = PendingChanges(e.product, e.vehicle, e.formula, e.premium))
|
||||||
|
|
||||||
|
def approve(e: ContractEvent.Approved) =
|
||||||
|
Actif(
|
||||||
|
pendingChanges.product,
|
||||||
|
holder,
|
||||||
|
pendingChanges.vehicle,
|
||||||
|
pendingChanges.formula,
|
||||||
|
pendingChanges.premium
|
||||||
|
)
|
||||||
|
|
||||||
|
def reject(e: ContractEvent.Rejected) =
|
||||||
|
Actif(product, holder, vehicle, formula, premium)
|
||||||
|
|
||||||
|
def terminate(e: ContractEvent.Terminated) =
|
||||||
|
ContractState.Terminated(product, holder, vehicle, formula, premium)
|
||||||
|
|
||||||
|
@caseName("terminated")
|
||||||
|
final case class Terminated(
|
||||||
|
product: ProductType,
|
||||||
|
holder: ClientEntityId,
|
||||||
|
vehicle: Vehicle,
|
||||||
|
formula: FormulaType,
|
||||||
|
premium: Amount)
|
||||||
|
extends ContractState derives Schema
|
||||||
|
|
||||||
|
end ContractState
|
||||||
7
core/src/lu/foyer/contracts/EmployeeService.scala
Normal file
7
core/src/lu/foyer/contracts/EmployeeService.scala
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import lu.foyer.clients.Address
|
||||||
|
|
||||||
|
trait EmployeeService:
|
||||||
|
def fetchOne(subject: String): Either[String, Employee]
|
||||||
8
core/src/lu/foyer/contracts/PremiumService.scala
Normal file
8
core/src/lu/foyer/contracts/PremiumService.scala
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import lu.foyer.clients.Address
|
||||||
|
|
||||||
|
trait PremiumService:
|
||||||
|
def computePremium(formula: FormulaType, vehicle: Vehicle, residentialAddress: Address)
|
||||||
|
: Either[String, Amount]
|
||||||
18
flake.lock
generated
18
flake.lock
generated
|
|
@ -39,11 +39,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1740460834,
|
"lastModified": 1742480343,
|
||||||
"narHash": "sha256-RUL1r8zH5wG5L1YipNj1bmt0Oi8L9qwzXsf/ww8WxBc=",
|
"narHash": "sha256-AN6X0t0pX0GLDh6CMG474aNffJUKPQtSEJ9aCZed474=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "devenv",
|
"repo": "devenv",
|
||||||
"rev": "9e4003b2702483bd962dac3d4ff43e8dafb93cda",
|
"rev": "56fe80518d1c949520a2cea8e9c2a83ec3f9bdc5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -102,11 +102,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1737465171,
|
"lastModified": 1740849354,
|
||||||
"narHash": "sha256-R10v2hoJRLq8jcL4syVFag7nIGE7m13qO48wRIukWNg=",
|
"narHash": "sha256-oy33+t09FraucSZ2rZ6qnD1Y1c8azKKmQuCvF2ytUko=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "git-hooks.nix",
|
"repo": "git-hooks.nix",
|
||||||
"rev": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17",
|
"rev": "4a709a8ce9f8c08fa7ddb86761fe488ff7858a07",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -172,11 +172,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1734114420,
|
"lastModified": 1741798497,
|
||||||
"narHash": "sha256-n52PUzub5jZWc8nI/sR7UICOheU8rNA+YZ73YaHeCBg=",
|
"narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=",
|
||||||
"owner": "domenkozar",
|
"owner": "domenkozar",
|
||||||
"repo": "nix",
|
"repo": "nix",
|
||||||
"rev": "bde6a1a0d1f2af86caa4d20d23eca019f3d57eee",
|
"rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
7
model/src/lu/foyer/AppError.scala
Normal file
7
model/src/lu/foyer/AppError.scala
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package lu.foyer
|
||||||
|
|
||||||
|
import zio.schema.*
|
||||||
|
|
||||||
|
enum AppError(description: String) extends Throwable:
|
||||||
|
case NotFound(desc: String) extends AppError(desc)
|
||||||
|
case Unexpected(desc: String) extends AppError(desc)
|
||||||
10
model/src/lu/foyer/CommonTypes.scala
Normal file
10
model/src/lu/foyer/CommonTypes.scala
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package lu.foyer
|
||||||
|
|
||||||
|
import zio.schema.*
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
opaque type ClientEntityId <: UUID = UUID
|
||||||
|
object ClientEntityId extends RefinedUUID[ClientEntityId]
|
||||||
|
|
||||||
|
opaque type Email <: String = String
|
||||||
|
object Email extends NonBlankString[Email]
|
||||||
|
|
@ -4,6 +4,7 @@ import zio.prelude.*
|
||||||
import zio.schema.Schema
|
import zio.schema.Schema
|
||||||
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
trait RefinedType[Base, New]:
|
trait RefinedType[Base, New]:
|
||||||
inline def assume(value: Base): New = value.asInstanceOf[New]
|
inline def assume(value: Base): New = value.asInstanceOf[New]
|
||||||
|
|
@ -15,6 +16,15 @@ trait RefinedString[New <: String] extends RefinedType[String, New]:
|
||||||
Right(_)
|
Right(_)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
trait RefinedUUID[New <: UUID] extends RefinedType[UUID, New]:
|
||||||
|
def apply(value: UUID): New = assume(value)
|
||||||
|
override def validation(value: UUID): Validation[String, New] =
|
||||||
|
Validation.succeed(assume(value))
|
||||||
|
given Schema[New] = Schema[UUID].transformOrFail(
|
||||||
|
validation(_).toEither.left.map(_.toList.mkString(", ")),
|
||||||
|
Right(_)
|
||||||
|
)
|
||||||
|
|
||||||
trait RefinedLocalDate[New <: LocalDate] extends RefinedType[LocalDate, New]:
|
trait RefinedLocalDate[New <: LocalDate] extends RefinedType[LocalDate, New]:
|
||||||
def apply(value: LocalDate): New = assume(value)
|
def apply(value: LocalDate): New = assume(value)
|
||||||
override def validation(value: LocalDate): Validation[String, New] =
|
override def validation(value: LocalDate): Validation[String, New] =
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,9 @@ object ClientBirthDate extends RefinedLocalDate[ClientBirthDate]
|
||||||
opaque type ClientDrivingLicenseDate <: LocalDate = LocalDate
|
opaque type ClientDrivingLicenseDate <: LocalDate = LocalDate
|
||||||
object ClientDrivingLicenseDate extends RefinedLocalDate[ClientDrivingLicenseDate]
|
object ClientDrivingLicenseDate extends RefinedLocalDate[ClientDrivingLicenseDate]
|
||||||
|
|
||||||
opaque type Email <: String = String
|
|
||||||
object Email extends NonBlankString[Email]
|
|
||||||
|
|
||||||
opaque type NationalNumber <: String = String
|
opaque type NationalNumber <: String = String
|
||||||
object NationalNumber extends NonBlankString[NationalNumber]
|
object NationalNumber extends NonBlankString[NationalNumber]
|
||||||
|
|
||||||
case class Client(
|
|
||||||
lastName: ClientFirstName,
|
|
||||||
firstName: ClientLastName,
|
|
||||||
drivingLicenseDate: ClientDrivingLicenseDate,
|
|
||||||
phoneNumber: PhoneNumber,
|
|
||||||
email: Email,
|
|
||||||
address: Address)
|
|
||||||
derives Schema
|
|
||||||
|
|
||||||
case class PhoneNumber(
|
case class PhoneNumber(
|
||||||
country: Country,
|
country: Country,
|
||||||
nationalNumber: NationalNumber,
|
nationalNumber: NationalNumber,
|
||||||
|
|
@ -45,4 +33,4 @@ case class PhoneNumberInput(
|
||||||
derives Schema
|
derives Schema
|
||||||
|
|
||||||
enum ClientDisabledReason derives Schema:
|
enum ClientDisabledReason derives Schema:
|
||||||
case GDPR, Death
|
case gdpr, death
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,4 @@ package clients
|
||||||
import zio.schema.*
|
import zio.schema.*
|
||||||
|
|
||||||
enum Country derives Schema:
|
enum Country derives Schema:
|
||||||
case LU, FR, BE
|
case LU, FR, BE, CH
|
||||||
|
|
|
||||||
11
model/src/lu/foyer/contracts/Employee.scala
Normal file
11
model/src/lu/foyer/contracts/Employee.scala
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import zio.schema.*
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
|
opaque type EmployeeDisplayName <: String = String
|
||||||
|
object EmployeeDisplayName extends NonBlankString[EmployeeDisplayName]
|
||||||
|
|
||||||
|
final case class Employee(uid: String, displayName: EmployeeDisplayName, email: Email)
|
||||||
|
derives Schema
|
||||||
27
model/src/lu/foyer/contracts/FormulaType.scala
Normal file
27
model/src/lu/foyer/contracts/FormulaType.scala
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import zio.schema.*
|
||||||
|
|
||||||
|
enum FormulaType(val maxCoverage: Long, val basePremium: BigDecimal):
|
||||||
|
case Bronze extends FormulaType(10_000, 600)
|
||||||
|
case Silver extends FormulaType(25_000, 900)
|
||||||
|
case Gold extends FormulaType(250_000, 1500)
|
||||||
|
|
||||||
|
object FormulaType:
|
||||||
|
enum Json:
|
||||||
|
case bronze, silver, gold
|
||||||
|
|
||||||
|
given Schema[FormulaType] = DeriveSchema
|
||||||
|
.gen[Json].transform(
|
||||||
|
{
|
||||||
|
case Json.bronze => FormulaType.Bronze
|
||||||
|
case Json.silver => FormulaType.Silver
|
||||||
|
case Json.gold => FormulaType.Gold
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case FormulaType.Bronze => Json.bronze
|
||||||
|
case FormulaType.Silver => Json.silver
|
||||||
|
case FormulaType.Gold => Json.gold
|
||||||
|
}
|
||||||
|
)
|
||||||
8
model/src/lu/foyer/contracts/ProductType.scala
Normal file
8
model/src/lu/foyer/contracts/ProductType.scala
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import zio.schema.*
|
||||||
|
import zio.schema.annotation.caseName
|
||||||
|
|
||||||
|
enum ProductType derives Schema:
|
||||||
|
@caseName("car") case Car
|
||||||
7
model/src/lu/foyer/contracts/TerminationReasonType.scala
Normal file
7
model/src/lu/foyer/contracts/TerminationReasonType.scala
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import zio.schema.*
|
||||||
|
|
||||||
|
enum TerminationReasonType derives Schema:
|
||||||
|
case Rejected, HolderDeceased, TerminatedByClient
|
||||||
16
model/src/lu/foyer/contracts/Vehicle.scala
Normal file
16
model/src/lu/foyer/contracts/Vehicle.scala
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package lu.foyer
|
||||||
|
package contracts
|
||||||
|
|
||||||
|
import zio.schema.*
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
|
opaque type VehiclePlate <: String = String
|
||||||
|
object VehiclePlate extends NonBlankString[VehiclePlate]
|
||||||
|
|
||||||
|
opaque type VehicleBrand <: String = String
|
||||||
|
object VehicleBrand extends NonBlankString[VehicleBrand]
|
||||||
|
|
||||||
|
final case class Amount(value: BigDecimal, currency: Currency) derives Schema
|
||||||
|
|
||||||
|
final case class Vehicle(plate: VehiclePlate, brand: VehicleBrand, insuredValue: Amount)
|
||||||
|
derives Schema
|
||||||
Loading…
Add table
Add a link
Reference in a new issue