Improve error handling

This commit is contained in:
Paul-Henri Froidmont 2025-10-22 15:30:36 +02:00
parent 87bd780f9f
commit e6a8150483
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
7 changed files with 78 additions and 62 deletions

View file

@ -28,13 +28,14 @@ trait CommandHandler[+Command, +Event, +State]:
def commandSchema: Schema[?]
trait CommandHandlerCreate[Command: Schema, Event] extends CommandHandler[Command, Event, Nothing]:
def onCommand(entityId: String, command: Command): Task[Event]
def onCommand(entityId: String, command: Command): IO[JsonApiError | Throwable, Event]
val isCreate = true
val commandSchema = summon[Schema[Command]]
trait CommandHandlerUpdate[Command: Schema, Event, State]
extends CommandHandler[Command, Event, State]:
def onCommand(entityId: String, state: State, command: Command): Task[Event]
def onCommand(entityId: String, state: State, command: Command)
: IO[JsonApiError | Throwable, Event]
val isCreate = false
val commandSchema = summon[Schema[Command]]
@ -45,20 +46,21 @@ class CommandEngine[Command, Event, State](
val stateRepo: StateRepository[State]):
def handleCommand(command: Command, name: String, entityId: String)
: Task[(lu.foyer.Event[Event], lu.foyer.Entity[State])] =
: IO[JsonApiError | Throwable, (lu.foyer.Event[Event], lu.foyer.Entity[State])] =
for
handler <- ZIO
.succeed(handlers.find(_.name == name))
.someOrFail(new IllegalArgumentException(s"No handler found for command $name"))
entityOption <- stateRepo.fetchOne(entityId)
(event, newStateOption) <- transition(command, name, entityId, entityOption, handler)
newState <-
newState <-
ZIO
.succeed(newStateOption)
.someOrFail(new IllegalArgumentException("Reducer cannot resolve state transition"))
newEntity = Entity(entityId, newState, entityOption.map(_.version).getOrElse(1))
_ <- if entityOption.isEmpty then stateRepo.insert(newEntity.entityId, newEntity)
else stateRepo.update(newEntity.entityId, newEntity)
_ <-
if entityOption.isEmpty then stateRepo.insert(newEntity.entityId, newEntity)
else stateRepo.update(newEntity.entityId, newEntity)
eventEntity <- Random.nextUUID.map(id => Event(newEntity.entityId, event, id.toString))
_ <- eventRepo.insert(eventEntity.eventId, eventEntity)
yield (eventEntity, newEntity)
@ -69,7 +71,7 @@ class CommandEngine[Command, Event, State](
entityId: String,
entityOption: Option[Entity[State]],
handler: CommandHandler[Command, Event, State]
): Task[(Event, Option[State])] = (entityOption, handler) match
): IO[JsonApiError | Throwable, (Event, Option[State])] = (entityOption, handler) match
case (None, h) if !h.isUpdate =>
h.asInstanceOf[CommandHandlerCreate[Command, Event]]
.onCommand(entityId, command)

View file

@ -0,0 +1,12 @@
package lu.foyer
import zio.schema.*
import zio.schema.annotation.discriminatorName
@discriminatorName("errorType")
enum JsonApiError derives Schema:
case NotFound(id: String)
case InternalServerError(e: String)
object JsonApiError:
given Schema[JsonApiError.NotFound] = DeriveSchema.gen
given Schema[JsonApiError.InternalServerError] = DeriveSchema.gen

View file

@ -29,22 +29,21 @@ class SubscribeHandler(
val name = "create"
def onCommand(entityId: String, command: ContractCommand.Subscribe)
: Task[ContractEvent.Subscribed] =
override def onCommand(entityId: String, command: ContractCommand.Subscribe) =
for
holder <- clientStateRepo.fetchOne(command.holder).someOrFailException
holder <- clientStateRepo
.fetchOne(command.holder)
.someOrFail(JsonApiError.NotFound(command.holder))
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}"))
case ClientState.Actif(address = Some(address)) => ZIO.succeed(address)
case _ => ZIO.fail(JsonApiError.NotFound(command.holder))
premium <-
ZIO
.fromEither(
premiumService.computePremium(command.formula, command.vehicle, residentialAddress)
)
.mapError(new IllegalArgumentException(_))
.mapError(JsonApiError.InternalServerError(_))
yield ContractEvent.Subscribed(
command.product,
command.holder,
@ -52,7 +51,6 @@ class SubscribeHandler(
command.formula,
premium
)
end SubscribeHandler
class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService: PremiumService)
extends CommandHandlerUpdate[ContractCommand.Amend, ContractEvent.Amended, ContractState]:
@ -62,12 +60,11 @@ class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService
def onCommand(entityId: String, state: ContractState, command: ContractCommand.Amend)
: Task[ContractEvent.Amended] =
for
holder <- clientStateRepo.fetchOne(state.holder).someOrFailException
holder <- clientStateRepo.fetchOne(state.holder).someOrFailException
residentialAddress <-
holder.data match
case ClientState.Actif(_, _, _, _, _, _, Some(address)) =>
ZIO.succeed(address)
case _ =>
case ClientState.Actif(address = Some(address)) => ZIO.succeed(address)
case _ =>
ZIO.fail(new IllegalArgumentException(s"No active holder found for ${state.holder}"))
premium <- ZIO
.fromEither(
@ -84,7 +81,6 @@ class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService
command.formula.getOrElse(state.formula),
premium
)
end AmendHandler
class ApproveHandler(employeeService: EmployeeService)
extends CommandHandlerUpdate[
@ -98,7 +94,7 @@ class ApproveHandler(employeeService: EmployeeService)
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Approve)
: Task[ContractEvent.Approved] =
for
user <- ZIO.succeed("") // TODO current user
user <- ZIO.succeed("") // TODO current user
employee <- ZIO
.fromEither(employeeService.fetchOne(user))
.mapError(new IllegalArgumentException(_))
@ -116,7 +112,7 @@ class RejectHandler(employeeService: EmployeeService)
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Reject)
: Task[ContractEvent.Rejected] =
for
user <- ZIO.succeed("") // TODO current user
user <- ZIO.succeed("") // TODO current user
employee <- ZIO
.fromEither(employeeService.fetchOne(user))
.mapError(new IllegalArgumentException(_))