Implement contracts
This commit is contained in:
parent
31014d1a0c
commit
efdc50eb1d
33 changed files with 879 additions and 173 deletions
|
|
@ -1,9 +1,10 @@
|
|||
package lu.foyer
|
||||
|
||||
import zio.*
|
||||
import java.util.UUID
|
||||
import zio.schema.Schema
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
final case class Entity[T](entityId: UUID, data: T, version: Long)
|
||||
final case class Event[T](entityId: UUID, data: T, eventId: UUID)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,17 @@ package lu.foyer
|
|||
|
||||
import zio.*
|
||||
import zio.schema.*
|
||||
import zio.schema.annotation.fieldName
|
||||
|
||||
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
|
||||
final case class Paged[T](items: List[T], totals: Option[Long])
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ package lu.foyer
|
|||
package clients
|
||||
|
||||
import zio.schema.*
|
||||
|
||||
import java.time.LocalDate
|
||||
import zio.schema.annotation.caseName
|
||||
import zio.schema.annotation.discriminatorName
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
@discriminatorName("eventType")
|
||||
enum ClientEvent derives Schema:
|
||||
case Created(
|
||||
sealed trait ClientEvent derives Schema
|
||||
object ClientEvent:
|
||||
|
||||
@caseName("created")
|
||||
final case class Created(
|
||||
lastName: ClientLastName,
|
||||
firstName: ClientFirstName,
|
||||
birthDate: ClientBirthDate,
|
||||
|
|
@ -16,7 +20,10 @@ enum ClientEvent derives Schema:
|
|||
phoneNumber: Option[PhoneNumberInput],
|
||||
email: Option[Email],
|
||||
address: Option[Address])
|
||||
case Updated(
|
||||
extends ClientEvent derives Schema
|
||||
|
||||
@caseName("updated")
|
||||
final case class Updated(
|
||||
lastName: Option[ClientLastName],
|
||||
firstName: Option[ClientFirstName],
|
||||
birthDate: Option[ClientBirthDate],
|
||||
|
|
@ -24,9 +31,7 @@ enum ClientEvent derives Schema:
|
|||
phoneNumber: Option[PhoneNumberInput],
|
||||
email: Option[Email],
|
||||
address: Option[Address])
|
||||
case Disabled(reason: ClientDisabledReason)
|
||||
extends ClientEvent derives Schema
|
||||
|
||||
object ClientEvent:
|
||||
given Schema[ClientEvent.Created] = DeriveSchema.gen
|
||||
given Schema[ClientEvent.Updated] = DeriveSchema.gen
|
||||
given Schema[ClientEvent.Disabled] = DeriveSchema.gen
|
||||
@caseName("disabled")
|
||||
final case class Disabled(reason: ClientDisabledReason) extends ClientEvent derives Schema
|
||||
|
|
|
|||
|
|
@ -2,30 +2,59 @@ package lu.foyer
|
|||
package clients
|
||||
|
||||
import zio.schema.*
|
||||
import zio.schema.annotation.caseName
|
||||
import zio.schema.annotation.discriminatorName
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
@discriminatorName("statusType")
|
||||
enum 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])
|
||||
|
||||
sealed trait ClientState derives Schema
|
||||
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) =
|
||||
ClientState.Actif(
|
||||
event.lastName,
|
||||
|
|
@ -36,25 +65,4 @@ object ClientState:
|
|||
event.email,
|
||||
event.address
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
end ClientState
|
||||
|
|
|
|||
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]
|
||||
Loading…
Add table
Add a link
Reference in a new issue