diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala new file mode 100644 index 0000000..35a15af --- /dev/null +++ b/domain/proof/command/model/src/Command.scala @@ -0,0 +1,38 @@ +package mdr.pdb +package proof +package command + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +sealed trait AuthorizeOption +case object Unauthorized extends AuthorizeOption +case class Authorized(note: Option[String]) extends AuthorizeOption + +case class CreateProof( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef], + authorize: AuthorizeOption +) extends Command + +case class AuthorizeProof( + id: Proof.Id, + note: Option[String] +) extends Command + +case class UpdateProof( + id: Proof.Id, + documents: List[DocumentRef] +) extends Command + +case class RevokeProof( + id: Proof.Id, + reason: RevocationReason, + since: Instant, + documents: List[DocumentRef] +) extends Command diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala new file mode 100644 index 0000000..35a15af --- /dev/null +++ b/domain/proof/command/model/src/Command.scala @@ -0,0 +1,38 @@ +package mdr.pdb +package proof +package command + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +sealed trait AuthorizeOption +case object Unauthorized extends AuthorizeOption +case class Authorized(note: Option[String]) extends AuthorizeOption + +case class CreateProof( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef], + authorize: AuthorizeOption +) extends Command + +case class AuthorizeProof( + id: Proof.Id, + note: Option[String] +) extends Command + +case class UpdateProof( + id: Proof.Id, + documents: List[DocumentRef] +) extends Command + +case class RevokeProof( + id: Proof.Id, + reason: RevocationReason, + since: Instant, + documents: List[DocumentRef] +) extends Command diff --git a/domain/proof/command/model/src/State.scala b/domain/proof/command/model/src/State.scala new file mode 100644 index 0000000..47c5b36 --- /dev/null +++ b/domain/proof/command/model/src/State.scala @@ -0,0 +1,5 @@ +package mdr.pdb +package proof +package command + +type State = Option[Proof] diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala new file mode 100644 index 0000000..35a15af --- /dev/null +++ b/domain/proof/command/model/src/Command.scala @@ -0,0 +1,38 @@ +package mdr.pdb +package proof +package command + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +sealed trait AuthorizeOption +case object Unauthorized extends AuthorizeOption +case class Authorized(note: Option[String]) extends AuthorizeOption + +case class CreateProof( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef], + authorize: AuthorizeOption +) extends Command + +case class AuthorizeProof( + id: Proof.Id, + note: Option[String] +) extends Command + +case class UpdateProof( + id: Proof.Id, + documents: List[DocumentRef] +) extends Command + +case class RevokeProof( + id: Proof.Id, + reason: RevocationReason, + since: Instant, + documents: List[DocumentRef] +) extends Command diff --git a/domain/proof/command/model/src/State.scala b/domain/proof/command/model/src/State.scala new file mode 100644 index 0000000..47c5b36 --- /dev/null +++ b/domain/proof/command/model/src/State.scala @@ -0,0 +1,5 @@ +package mdr.pdb +package proof +package command + +type State = Option[Proof] diff --git a/domain/proof/shared/json/src/Codecs.scala b/domain/proof/shared/json/src/Codecs.scala index bef8a2b..251550e 100644 --- a/domain/proof/shared/json/src/Codecs.scala +++ b/domain/proof/shared/json/src/Codecs.scala @@ -6,6 +6,7 @@ trait Codecs extends mdr.pdb.json.Codecs: given JsonCodec[Authorization] = DeriveJsonCodec.gen + given JsonCodec[RevocationReason] = DeriveJsonCodec.gen given JsonCodec[Revocation] = DeriveJsonCodec.gen given JsonCodec[Proof] = DeriveJsonCodec.gen diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala new file mode 100644 index 0000000..35a15af --- /dev/null +++ b/domain/proof/command/model/src/Command.scala @@ -0,0 +1,38 @@ +package mdr.pdb +package proof +package command + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +sealed trait AuthorizeOption +case object Unauthorized extends AuthorizeOption +case class Authorized(note: Option[String]) extends AuthorizeOption + +case class CreateProof( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef], + authorize: AuthorizeOption +) extends Command + +case class AuthorizeProof( + id: Proof.Id, + note: Option[String] +) extends Command + +case class UpdateProof( + id: Proof.Id, + documents: List[DocumentRef] +) extends Command + +case class RevokeProof( + id: Proof.Id, + reason: RevocationReason, + since: Instant, + documents: List[DocumentRef] +) extends Command diff --git a/domain/proof/command/model/src/State.scala b/domain/proof/command/model/src/State.scala new file mode 100644 index 0000000..47c5b36 --- /dev/null +++ b/domain/proof/command/model/src/State.scala @@ -0,0 +1,5 @@ +package mdr.pdb +package proof +package command + +type State = Option[Proof] diff --git a/domain/proof/shared/json/src/Codecs.scala b/domain/proof/shared/json/src/Codecs.scala index bef8a2b..251550e 100644 --- a/domain/proof/shared/json/src/Codecs.scala +++ b/domain/proof/shared/json/src/Codecs.scala @@ -6,6 +6,7 @@ trait Codecs extends mdr.pdb.json.Codecs: given JsonCodec[Authorization] = DeriveJsonCodec.gen + given JsonCodec[RevocationReason] = DeriveJsonCodec.gen given JsonCodec[Revocation] = DeriveJsonCodec.gen given JsonCodec[Proof] = DeriveJsonCodec.gen diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala new file mode 100644 index 0000000..465d6fd --- /dev/null +++ b/domain/proof/shared/model/src/Event.scala @@ -0,0 +1,31 @@ +package mdr.pdb +package proof + +import java.time.Instant + +sealed trait Event + +case class ProofCreated( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef] +) extends Event + +case class ProofUpdated( + id: Proof.Id, + documents: List[DocumentRef] +) extends Event + +case class ProofAuthorized( + id: Proof.Id, + note: Option[String] +) extends Event + +case class ProofRevoked( + id: Proof.Id, + reason: RevocationReason, + revokedSince: Instant, + documents: List[DocumentRef] +) extends Event diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala new file mode 100644 index 0000000..35a15af --- /dev/null +++ b/domain/proof/command/model/src/Command.scala @@ -0,0 +1,38 @@ +package mdr.pdb +package proof +package command + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +sealed trait AuthorizeOption +case object Unauthorized extends AuthorizeOption +case class Authorized(note: Option[String]) extends AuthorizeOption + +case class CreateProof( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef], + authorize: AuthorizeOption +) extends Command + +case class AuthorizeProof( + id: Proof.Id, + note: Option[String] +) extends Command + +case class UpdateProof( + id: Proof.Id, + documents: List[DocumentRef] +) extends Command + +case class RevokeProof( + id: Proof.Id, + reason: RevocationReason, + since: Instant, + documents: List[DocumentRef] +) extends Command diff --git a/domain/proof/command/model/src/State.scala b/domain/proof/command/model/src/State.scala new file mode 100644 index 0000000..47c5b36 --- /dev/null +++ b/domain/proof/command/model/src/State.scala @@ -0,0 +1,5 @@ +package mdr.pdb +package proof +package command + +type State = Option[Proof] diff --git a/domain/proof/shared/json/src/Codecs.scala b/domain/proof/shared/json/src/Codecs.scala index bef8a2b..251550e 100644 --- a/domain/proof/shared/json/src/Codecs.scala +++ b/domain/proof/shared/json/src/Codecs.scala @@ -6,6 +6,7 @@ trait Codecs extends mdr.pdb.json.Codecs: given JsonCodec[Authorization] = DeriveJsonCodec.gen + given JsonCodec[RevocationReason] = DeriveJsonCodec.gen given JsonCodec[Revocation] = DeriveJsonCodec.gen given JsonCodec[Proof] = DeriveJsonCodec.gen diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala new file mode 100644 index 0000000..465d6fd --- /dev/null +++ b/domain/proof/shared/model/src/Event.scala @@ -0,0 +1,31 @@ +package mdr.pdb +package proof + +import java.time.Instant + +sealed trait Event + +case class ProofCreated( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef] +) extends Event + +case class ProofUpdated( + id: Proof.Id, + documents: List[DocumentRef] +) extends Event + +case class ProofAuthorized( + id: Proof.Id, + note: Option[String] +) extends Event + +case class ProofRevoked( + id: Proof.Id, + reason: RevocationReason, + revokedSince: Instant, + documents: List[DocumentRef] +) extends Event diff --git a/domain/proof/shared/model/src/Proof.scala b/domain/proof/shared/model/src/Proof.scala index 7052edc..593265d 100644 --- a/domain/proof/shared/model/src/Proof.scala +++ b/domain/proof/shared/model/src/Proof.scala @@ -4,29 +4,35 @@ import java.time.Instant import java.time.LocalDate +sealed abstract class RevocationReason(msg: String) +case object Expired extends RevocationReason("Vypršela platnost důkazu") +case class Other(msg: String) extends RevocationReason(msg) + case class Authorization( - time: Instant, - person: OsobniCislo + authorized: WW, + note: Option[String] ) case class Revocation( - time: Instant, - person: OsobniCislo, - explanation: String, + revoked: WW, + revokedSince: Instant, + reason: RevocationReason, documents: List[DocumentRef] ) case class Proof( - person: OsobniCislo, id: Proof.Id, + person: OsobniCislo, parameterId: String, criterionId: String, documents: List[DocumentRef], - note: String, authorizations: List[Authorization], - expiration: Option[LocalDate], - revocation: Option[Revocation] -) + revocations: List[Revocation], + created: WW +) { + def isAuthorized = authorizations.nonEmpty + def isRevoked = revocations.exists(_.revokedSince.isBefore(Instant.now())) +} object Proof: type Id = String diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala new file mode 100644 index 0000000..35a15af --- /dev/null +++ b/domain/proof/command/model/src/Command.scala @@ -0,0 +1,38 @@ +package mdr.pdb +package proof +package command + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +sealed trait AuthorizeOption +case object Unauthorized extends AuthorizeOption +case class Authorized(note: Option[String]) extends AuthorizeOption + +case class CreateProof( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef], + authorize: AuthorizeOption +) extends Command + +case class AuthorizeProof( + id: Proof.Id, + note: Option[String] +) extends Command + +case class UpdateProof( + id: Proof.Id, + documents: List[DocumentRef] +) extends Command + +case class RevokeProof( + id: Proof.Id, + reason: RevocationReason, + since: Instant, + documents: List[DocumentRef] +) extends Command diff --git a/domain/proof/command/model/src/State.scala b/domain/proof/command/model/src/State.scala new file mode 100644 index 0000000..47c5b36 --- /dev/null +++ b/domain/proof/command/model/src/State.scala @@ -0,0 +1,5 @@ +package mdr.pdb +package proof +package command + +type State = Option[Proof] diff --git a/domain/proof/shared/json/src/Codecs.scala b/domain/proof/shared/json/src/Codecs.scala index bef8a2b..251550e 100644 --- a/domain/proof/shared/json/src/Codecs.scala +++ b/domain/proof/shared/json/src/Codecs.scala @@ -6,6 +6,7 @@ trait Codecs extends mdr.pdb.json.Codecs: given JsonCodec[Authorization] = DeriveJsonCodec.gen + given JsonCodec[RevocationReason] = DeriveJsonCodec.gen given JsonCodec[Revocation] = DeriveJsonCodec.gen given JsonCodec[Proof] = DeriveJsonCodec.gen diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala new file mode 100644 index 0000000..465d6fd --- /dev/null +++ b/domain/proof/shared/model/src/Event.scala @@ -0,0 +1,31 @@ +package mdr.pdb +package proof + +import java.time.Instant + +sealed trait Event + +case class ProofCreated( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef] +) extends Event + +case class ProofUpdated( + id: Proof.Id, + documents: List[DocumentRef] +) extends Event + +case class ProofAuthorized( + id: Proof.Id, + note: Option[String] +) extends Event + +case class ProofRevoked( + id: Proof.Id, + reason: RevocationReason, + revokedSince: Instant, + documents: List[DocumentRef] +) extends Event diff --git a/domain/proof/shared/model/src/Proof.scala b/domain/proof/shared/model/src/Proof.scala index 7052edc..593265d 100644 --- a/domain/proof/shared/model/src/Proof.scala +++ b/domain/proof/shared/model/src/Proof.scala @@ -4,29 +4,35 @@ import java.time.Instant import java.time.LocalDate +sealed abstract class RevocationReason(msg: String) +case object Expired extends RevocationReason("Vypršela platnost důkazu") +case class Other(msg: String) extends RevocationReason(msg) + case class Authorization( - time: Instant, - person: OsobniCislo + authorized: WW, + note: Option[String] ) case class Revocation( - time: Instant, - person: OsobniCislo, - explanation: String, + revoked: WW, + revokedSince: Instant, + reason: RevocationReason, documents: List[DocumentRef] ) case class Proof( - person: OsobniCislo, id: Proof.Id, + person: OsobniCislo, parameterId: String, criterionId: String, documents: List[DocumentRef], - note: String, authorizations: List[Authorization], - expiration: Option[LocalDate], - revocation: Option[Revocation] -) + revocations: List[Revocation], + created: WW +) { + def isAuthorized = authorizations.nonEmpty + def isRevoked = revocations.exists(_.revokedSince.isBefore(Instant.now())) +} object Proof: type Id = String diff --git a/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala new file mode 100644 index 0000000..49a0e3d --- /dev/null +++ b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala @@ -0,0 +1,19 @@ +package fiftyforms.akka + +// Base class for all command-processing related exceptions from handlers +sealed abstract class CommandHandlerException( + msg: String, + cause: Option[Throwable] = None +) extends Exception(msg, cause.orNull) + +// TODO: use a typeclass like "Show" to create the error message +case class CommandNotAvailable[C, S](cmd: C, state: S) + extends CommandHandlerException( + s"Příkaz $cmd není dostupný ve stavu $state" + ) + +// TODO: use a typeclass like "Show" to create the error message +case class CommandRejected[C, S](reason: String, cmd: C, state: S) + extends CommandHandlerException( + s"Příkaz $cmd byl ve stavu $state odmítnut s odůvodněním $reason" + ) diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala new file mode 100644 index 0000000..35a15af --- /dev/null +++ b/domain/proof/command/model/src/Command.scala @@ -0,0 +1,38 @@ +package mdr.pdb +package proof +package command + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +sealed trait AuthorizeOption +case object Unauthorized extends AuthorizeOption +case class Authorized(note: Option[String]) extends AuthorizeOption + +case class CreateProof( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef], + authorize: AuthorizeOption +) extends Command + +case class AuthorizeProof( + id: Proof.Id, + note: Option[String] +) extends Command + +case class UpdateProof( + id: Proof.Id, + documents: List[DocumentRef] +) extends Command + +case class RevokeProof( + id: Proof.Id, + reason: RevocationReason, + since: Instant, + documents: List[DocumentRef] +) extends Command diff --git a/domain/proof/command/model/src/State.scala b/domain/proof/command/model/src/State.scala new file mode 100644 index 0000000..47c5b36 --- /dev/null +++ b/domain/proof/command/model/src/State.scala @@ -0,0 +1,5 @@ +package mdr.pdb +package proof +package command + +type State = Option[Proof] diff --git a/domain/proof/shared/json/src/Codecs.scala b/domain/proof/shared/json/src/Codecs.scala index bef8a2b..251550e 100644 --- a/domain/proof/shared/json/src/Codecs.scala +++ b/domain/proof/shared/json/src/Codecs.scala @@ -6,6 +6,7 @@ trait Codecs extends mdr.pdb.json.Codecs: given JsonCodec[Authorization] = DeriveJsonCodec.gen + given JsonCodec[RevocationReason] = DeriveJsonCodec.gen given JsonCodec[Revocation] = DeriveJsonCodec.gen given JsonCodec[Proof] = DeriveJsonCodec.gen diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala new file mode 100644 index 0000000..465d6fd --- /dev/null +++ b/domain/proof/shared/model/src/Event.scala @@ -0,0 +1,31 @@ +package mdr.pdb +package proof + +import java.time.Instant + +sealed trait Event + +case class ProofCreated( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef] +) extends Event + +case class ProofUpdated( + id: Proof.Id, + documents: List[DocumentRef] +) extends Event + +case class ProofAuthorized( + id: Proof.Id, + note: Option[String] +) extends Event + +case class ProofRevoked( + id: Proof.Id, + reason: RevocationReason, + revokedSince: Instant, + documents: List[DocumentRef] +) extends Event diff --git a/domain/proof/shared/model/src/Proof.scala b/domain/proof/shared/model/src/Proof.scala index 7052edc..593265d 100644 --- a/domain/proof/shared/model/src/Proof.scala +++ b/domain/proof/shared/model/src/Proof.scala @@ -4,29 +4,35 @@ import java.time.Instant import java.time.LocalDate +sealed abstract class RevocationReason(msg: String) +case object Expired extends RevocationReason("Vypršela platnost důkazu") +case class Other(msg: String) extends RevocationReason(msg) + case class Authorization( - time: Instant, - person: OsobniCislo + authorized: WW, + note: Option[String] ) case class Revocation( - time: Instant, - person: OsobniCislo, - explanation: String, + revoked: WW, + revokedSince: Instant, + reason: RevocationReason, documents: List[DocumentRef] ) case class Proof( - person: OsobniCislo, id: Proof.Id, + person: OsobniCislo, parameterId: String, criterionId: String, documents: List[DocumentRef], - note: String, authorizations: List[Authorization], - expiration: Option[LocalDate], - revocation: Option[Revocation] -) + revocations: List[Revocation], + created: WW +) { + def isAuthorized = authorizations.nonEmpty + def isRevoked = revocations.exists(_.revokedSince.isBefore(Instant.now())) +} object Proof: type Id = String diff --git a/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala new file mode 100644 index 0000000..49a0e3d --- /dev/null +++ b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala @@ -0,0 +1,19 @@ +package fiftyforms.akka + +// Base class for all command-processing related exceptions from handlers +sealed abstract class CommandHandlerException( + msg: String, + cause: Option[Throwable] = None +) extends Exception(msg, cause.orNull) + +// TODO: use a typeclass like "Show" to create the error message +case class CommandNotAvailable[C, S](cmd: C, state: S) + extends CommandHandlerException( + s"Příkaz $cmd není dostupný ve stavu $state" + ) + +// TODO: use a typeclass like "Show" to create the error message +case class CommandRejected[C, S](reason: String, cmd: C, state: S) + extends CommandHandlerException( + s"Příkaz $cmd byl ve stavu $state odmítnut s odůvodněním $reason" + ) diff --git a/fiftyforms/akka-persistence/src/main/scala/fiftyforms/EventHandlerException.scala b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/EventHandlerException.scala new file mode 100644 index 0000000..77172d0 --- /dev/null +++ b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/EventHandlerException.scala @@ -0,0 +1,11 @@ +package fiftyforms.akka + +sealed abstract class EventHandlerException( + msg: String, + cause: Option[Throwable] = None +) extends Exception(msg, cause.orNull) + +case class UnhandledEvent[Event, State](event: Event, state: State) + extends EventHandlerException( + s"Událost $event nastala ve stavu $state bez možnosti zpracování" + ) diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala new file mode 100644 index 0000000..35a15af --- /dev/null +++ b/domain/proof/command/model/src/Command.scala @@ -0,0 +1,38 @@ +package mdr.pdb +package proof +package command + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +sealed trait AuthorizeOption +case object Unauthorized extends AuthorizeOption +case class Authorized(note: Option[String]) extends AuthorizeOption + +case class CreateProof( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef], + authorize: AuthorizeOption +) extends Command + +case class AuthorizeProof( + id: Proof.Id, + note: Option[String] +) extends Command + +case class UpdateProof( + id: Proof.Id, + documents: List[DocumentRef] +) extends Command + +case class RevokeProof( + id: Proof.Id, + reason: RevocationReason, + since: Instant, + documents: List[DocumentRef] +) extends Command diff --git a/domain/proof/command/model/src/State.scala b/domain/proof/command/model/src/State.scala new file mode 100644 index 0000000..47c5b36 --- /dev/null +++ b/domain/proof/command/model/src/State.scala @@ -0,0 +1,5 @@ +package mdr.pdb +package proof +package command + +type State = Option[Proof] diff --git a/domain/proof/shared/json/src/Codecs.scala b/domain/proof/shared/json/src/Codecs.scala index bef8a2b..251550e 100644 --- a/domain/proof/shared/json/src/Codecs.scala +++ b/domain/proof/shared/json/src/Codecs.scala @@ -6,6 +6,7 @@ trait Codecs extends mdr.pdb.json.Codecs: given JsonCodec[Authorization] = DeriveJsonCodec.gen + given JsonCodec[RevocationReason] = DeriveJsonCodec.gen given JsonCodec[Revocation] = DeriveJsonCodec.gen given JsonCodec[Proof] = DeriveJsonCodec.gen diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala new file mode 100644 index 0000000..465d6fd --- /dev/null +++ b/domain/proof/shared/model/src/Event.scala @@ -0,0 +1,31 @@ +package mdr.pdb +package proof + +import java.time.Instant + +sealed trait Event + +case class ProofCreated( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef] +) extends Event + +case class ProofUpdated( + id: Proof.Id, + documents: List[DocumentRef] +) extends Event + +case class ProofAuthorized( + id: Proof.Id, + note: Option[String] +) extends Event + +case class ProofRevoked( + id: Proof.Id, + reason: RevocationReason, + revokedSince: Instant, + documents: List[DocumentRef] +) extends Event diff --git a/domain/proof/shared/model/src/Proof.scala b/domain/proof/shared/model/src/Proof.scala index 7052edc..593265d 100644 --- a/domain/proof/shared/model/src/Proof.scala +++ b/domain/proof/shared/model/src/Proof.scala @@ -4,29 +4,35 @@ import java.time.Instant import java.time.LocalDate +sealed abstract class RevocationReason(msg: String) +case object Expired extends RevocationReason("Vypršela platnost důkazu") +case class Other(msg: String) extends RevocationReason(msg) + case class Authorization( - time: Instant, - person: OsobniCislo + authorized: WW, + note: Option[String] ) case class Revocation( - time: Instant, - person: OsobniCislo, - explanation: String, + revoked: WW, + revokedSince: Instant, + reason: RevocationReason, documents: List[DocumentRef] ) case class Proof( - person: OsobniCislo, id: Proof.Id, + person: OsobniCislo, parameterId: String, criterionId: String, documents: List[DocumentRef], - note: String, authorizations: List[Authorization], - expiration: Option[LocalDate], - revocation: Option[Revocation] -) + revocations: List[Revocation], + created: WW +) { + def isAuthorized = authorizations.nonEmpty + def isRevoked = revocations.exists(_.revokedSince.isBefore(Instant.now())) +} object Proof: type Id = String diff --git a/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala new file mode 100644 index 0000000..49a0e3d --- /dev/null +++ b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala @@ -0,0 +1,19 @@ +package fiftyforms.akka + +// Base class for all command-processing related exceptions from handlers +sealed abstract class CommandHandlerException( + msg: String, + cause: Option[Throwable] = None +) extends Exception(msg, cause.orNull) + +// TODO: use a typeclass like "Show" to create the error message +case class CommandNotAvailable[C, S](cmd: C, state: S) + extends CommandHandlerException( + s"Příkaz $cmd není dostupný ve stavu $state" + ) + +// TODO: use a typeclass like "Show" to create the error message +case class CommandRejected[C, S](reason: String, cmd: C, state: S) + extends CommandHandlerException( + s"Příkaz $cmd byl ve stavu $state odmítnut s odůvodněním $reason" + ) diff --git a/fiftyforms/akka-persistence/src/main/scala/fiftyforms/EventHandlerException.scala b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/EventHandlerException.scala new file mode 100644 index 0000000..77172d0 --- /dev/null +++ b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/EventHandlerException.scala @@ -0,0 +1,11 @@ +package fiftyforms.akka + +sealed abstract class EventHandlerException( + msg: String, + cause: Option[Throwable] = None +) extends Exception(msg, cause.orNull) + +case class UnhandledEvent[Event, State](event: Event, state: State) + extends EventHandlerException( + s"Událost $event nastala ve stavu $state bez možnosti zpracování" + ) diff --git a/json/src/main/scala/mdr/pdb/json/Codecs.scala b/json/src/main/scala/mdr/pdb/json/Codecs.scala index e09617e..31c411f 100644 --- a/json/src/main/scala/mdr/pdb/json/Codecs.scala +++ b/json/src/main/scala/mdr/pdb/json/Codecs.scala @@ -5,6 +5,7 @@ trait Codecs: + given JsonCodec[WhoWhen] = DeriveJsonCodec.gen given JsonCodec[OsobniCislo] = JsonCodec.string.transform(OsobniCislo.apply, _.toString) given JsonFieldEncoder[OsobniCislo] = diff --git a/.sbtopts b/.sbtopts index 527d62a..8f73384 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 4096 +-mem 8192 diff --git a/core/src/main/scala/mdr/pdb/WhoWhen.scala b/core/src/main/scala/mdr/pdb/WhoWhen.scala new file mode 100644 index 0000000..85baf96 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/WhoWhen.scala @@ -0,0 +1,7 @@ +package mdr.pdb + +import java.time.Instant + +case class WhoWhen(user: OsobniCislo, time: Instant) + +type WW = WhoWhen diff --git a/domain/parameters/command/json/src/Codecs.scala b/domain/parameters/command/json/src/Codecs.scala deleted file mode 100644 index 2aaa7b8..0000000 --- a/domain/parameters/command/json/src/Codecs.scala +++ /dev/null @@ -1,9 +0,0 @@ -package mdr.pdb -package parameters.command -package json - -import zio.json.* - -trait Codecs extends mdr.pdb.json.Codecs: - - given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen diff --git a/domain/parameters/command/model/src/commands.scala b/domain/parameters/command/model/src/commands.scala deleted file mode 100644 index a030e98..0000000 --- a/domain/parameters/command/model/src/commands.scala +++ /dev/null @@ -1,16 +0,0 @@ -package mdr.pdb -package parameters -package command - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -case class AuthorizeProof( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriterion.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/domain/proof/command/entity/src/ProofBehaviour.scala b/domain/proof/command/entity/src/ProofBehaviour.scala index ebd2972..c682550 100644 --- a/domain/proof/command/entity/src/ProofBehaviour.scala +++ b/domain/proof/command/entity/src/ProofBehaviour.scala @@ -1,6 +1,159 @@ package mdr.pdb package proof -package command.entity +package command +package entity + +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.persistence.typed.PersistenceId +import akka.actor.typed.Behavior +import akka.actor.typed.ActorRef +import akka.pattern.StatusReply +import akka.Done + +import fiftyforms.akka.* +import akka.persistence.typed.scaladsl.Effect object ProofBehaviour: -end ProofBehaviour + + type ReplyTo = ActorRef[StatusReply[Done]] + + type Effect = akka.persistence.typed.scaladsl.Effect[Event, State] + + case class ProofCommand(command: Command, meta: WW, replyTo: ReplyTo) + case class ProofEvent(event: Event, meta: WW) + + type ProofReplyEffect = + akka.persistence.typed.scaladsl.ReplyEffect[ProofEvent, State] + + def apply(persistenceId: PersistenceId): Behavior[ProofCommand] = + EventSourcedBehavior + .withEnforcedReplies[ProofCommand, ProofEvent, State]( + persistenceId = persistenceId, + emptyState = None, + commandHandler = handleProofCommand, + eventHandler = handleProofEvent + ) + + def handleProofCommand( + state: State, + command: ProofCommand + ): ProofReplyEffect = + handleCommand(state, command.command) match + case Some(events) => persist(events, command.meta, command.replyTo) + case _ => unhandled(command.command, command.replyTo) + + type ProofHandler = WW ?=> PartialFunction[Event, Proof] + type ProofModHandler = WW ?=> PartialFunction[Event, Proof => Proof] + + def handleProofEvent(state: State, event: ProofEvent): State = + val ProofEvent(ev, ww) = event + + def handle(h: ProofHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))) + + def handleMod(p: Proof)(h: ProofModHandler): State = + Some(h(using ww).applyOrElse(ev, unhandledEvent(event, state))(p)) + + state match + case None => + handle(handleCreateProof) + case Some(proof) => + handleMod(proof) { + handleAuthorizeProof orElse handleUpdateProof orElse handleRevokeProof + } + + def handleCreateProof: ProofHandler = { + case ProofCreated(id, person, parameterId, criterionId, documents) => + Proof( + id, + person, + parameterId, + criterionId, + documents, + Nil, + Nil, + summon[WW] + ) + } + + def handleAuthorizeProof: ProofModHandler = { case AuthorizeProof(id, note) => + proof => + proof.copy(authorizations = + proof.authorizations :+ Authorization(summon[WW], note) + ) + } + + def handleUpdateProof: ProofModHandler = { case UpdateProof(id, documents) => + proof => proof.copy(documents = documents) + } + + def handleRevokeProof: ProofModHandler = { + case RevokeProof(id, reason, since, documents) => + proof => + proof.copy(revocations = + proof.revocations :+ Revocation(summon[WW], since, reason, documents) + ) + } + + def handleCommand(state: State, cmd: Command): Option[Seq[Event]] = + state match + case None => + cmd match + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + Authorized(note) + ) => + Some( + Seq( + ProofCreated(id, person, parameterId, criterionId, documents), + ProofAuthorized(id, note) + ) + ) + case CreateProof( + id, + person, + parameterId, + criterionId, + documents, + _ + ) => + Some( + Seq(ProofCreated(id, person, parameterId, criterionId, documents)) + ) + case _ => None + case Some(proof) if proof.isRevoked => None + case Some(proof) if proof.isAuthorized => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + case Some(proof) => + cmd match + case AuthorizeProof(id, note) => Some(Seq(ProofAuthorized(id, note))) + case UpdateProof(id, documents) => + Some(Seq(ProofUpdated(id, documents))) + case RevokeProof(id, reason, since, documents) => + Some(Seq((ProofRevoked(id, reason, since, documents)))) + case _ => None + + private def persist( + events: Seq[Event], + meta: WW, + replyTo: ReplyTo + ): ProofReplyEffect = + Effect + .persist(events.map(ProofEvent(_, meta))) + .thenReply(replyTo)(_ => StatusReply.Ack) + + private def unhandled(command: Command, replyTo: ReplyTo): ProofReplyEffect = + Effect.unhandled.thenReply(replyTo)(s => + StatusReply.error(CommandNotAvailable(command, s)) + ) + + private def unhandledEvent(event: ProofEvent, state: State): Nothing = + throw UnhandledEvent(event, state) diff --git a/domain/proof/command/json/src/Codecs.scala b/domain/proof/command/json/src/Codecs.scala new file mode 100644 index 0000000..6867615 --- /dev/null +++ b/domain/proof/command/json/src/Codecs.scala @@ -0,0 +1,13 @@ +package mdr.pdb +package proof.command +package json + +import zio.json.* + +trait Codecs extends proof.json.Codecs: + + given JsonCodec[AuthorizeOption] = DeriveJsonCodec.gen + given JsonCodec[CreateProof] = DeriveJsonCodec.gen + given JsonCodec[AuthorizeProof] = DeriveJsonCodec.gen + given JsonCodec[UpdateProof] = DeriveJsonCodec.gen + given JsonCodec[RevokeProof] = DeriveJsonCodec.gen diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala new file mode 100644 index 0000000..35a15af --- /dev/null +++ b/domain/proof/command/model/src/Command.scala @@ -0,0 +1,38 @@ +package mdr.pdb +package proof +package command + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +sealed trait AuthorizeOption +case object Unauthorized extends AuthorizeOption +case class Authorized(note: Option[String]) extends AuthorizeOption + +case class CreateProof( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef], + authorize: AuthorizeOption +) extends Command + +case class AuthorizeProof( + id: Proof.Id, + note: Option[String] +) extends Command + +case class UpdateProof( + id: Proof.Id, + documents: List[DocumentRef] +) extends Command + +case class RevokeProof( + id: Proof.Id, + reason: RevocationReason, + since: Instant, + documents: List[DocumentRef] +) extends Command diff --git a/domain/proof/command/model/src/State.scala b/domain/proof/command/model/src/State.scala new file mode 100644 index 0000000..47c5b36 --- /dev/null +++ b/domain/proof/command/model/src/State.scala @@ -0,0 +1,5 @@ +package mdr.pdb +package proof +package command + +type State = Option[Proof] diff --git a/domain/proof/shared/json/src/Codecs.scala b/domain/proof/shared/json/src/Codecs.scala index bef8a2b..251550e 100644 --- a/domain/proof/shared/json/src/Codecs.scala +++ b/domain/proof/shared/json/src/Codecs.scala @@ -6,6 +6,7 @@ trait Codecs extends mdr.pdb.json.Codecs: given JsonCodec[Authorization] = DeriveJsonCodec.gen + given JsonCodec[RevocationReason] = DeriveJsonCodec.gen given JsonCodec[Revocation] = DeriveJsonCodec.gen given JsonCodec[Proof] = DeriveJsonCodec.gen diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala new file mode 100644 index 0000000..465d6fd --- /dev/null +++ b/domain/proof/shared/model/src/Event.scala @@ -0,0 +1,31 @@ +package mdr.pdb +package proof + +import java.time.Instant + +sealed trait Event + +case class ProofCreated( + id: Proof.Id, + person: OsobniCislo, + parameterId: String, + criterionId: String, + documents: List[DocumentRef] +) extends Event + +case class ProofUpdated( + id: Proof.Id, + documents: List[DocumentRef] +) extends Event + +case class ProofAuthorized( + id: Proof.Id, + note: Option[String] +) extends Event + +case class ProofRevoked( + id: Proof.Id, + reason: RevocationReason, + revokedSince: Instant, + documents: List[DocumentRef] +) extends Event diff --git a/domain/proof/shared/model/src/Proof.scala b/domain/proof/shared/model/src/Proof.scala index 7052edc..593265d 100644 --- a/domain/proof/shared/model/src/Proof.scala +++ b/domain/proof/shared/model/src/Proof.scala @@ -4,29 +4,35 @@ import java.time.Instant import java.time.LocalDate +sealed abstract class RevocationReason(msg: String) +case object Expired extends RevocationReason("Vypršela platnost důkazu") +case class Other(msg: String) extends RevocationReason(msg) + case class Authorization( - time: Instant, - person: OsobniCislo + authorized: WW, + note: Option[String] ) case class Revocation( - time: Instant, - person: OsobniCislo, - explanation: String, + revoked: WW, + revokedSince: Instant, + reason: RevocationReason, documents: List[DocumentRef] ) case class Proof( - person: OsobniCislo, id: Proof.Id, + person: OsobniCislo, parameterId: String, criterionId: String, documents: List[DocumentRef], - note: String, authorizations: List[Authorization], - expiration: Option[LocalDate], - revocation: Option[Revocation] -) + revocations: List[Revocation], + created: WW +) { + def isAuthorized = authorizations.nonEmpty + def isRevoked = revocations.exists(_.revokedSince.isBefore(Instant.now())) +} object Proof: type Id = String diff --git a/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala new file mode 100644 index 0000000..49a0e3d --- /dev/null +++ b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/CommandHandlerException.scala @@ -0,0 +1,19 @@ +package fiftyforms.akka + +// Base class for all command-processing related exceptions from handlers +sealed abstract class CommandHandlerException( + msg: String, + cause: Option[Throwable] = None +) extends Exception(msg, cause.orNull) + +// TODO: use a typeclass like "Show" to create the error message +case class CommandNotAvailable[C, S](cmd: C, state: S) + extends CommandHandlerException( + s"Příkaz $cmd není dostupný ve stavu $state" + ) + +// TODO: use a typeclass like "Show" to create the error message +case class CommandRejected[C, S](reason: String, cmd: C, state: S) + extends CommandHandlerException( + s"Příkaz $cmd byl ve stavu $state odmítnut s odůvodněním $reason" + ) diff --git a/fiftyforms/akka-persistence/src/main/scala/fiftyforms/EventHandlerException.scala b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/EventHandlerException.scala new file mode 100644 index 0000000..77172d0 --- /dev/null +++ b/fiftyforms/akka-persistence/src/main/scala/fiftyforms/EventHandlerException.scala @@ -0,0 +1,11 @@ +package fiftyforms.akka + +sealed abstract class EventHandlerException( + msg: String, + cause: Option[Throwable] = None +) extends Exception(msg, cause.orNull) + +case class UnhandledEvent[Event, State](event: Event, state: State) + extends EventHandlerException( + s"Událost $event nastala ve stavu $state bez možnosti zpracování" + ) diff --git a/json/src/main/scala/mdr/pdb/json/Codecs.scala b/json/src/main/scala/mdr/pdb/json/Codecs.scala index e09617e..31c411f 100644 --- a/json/src/main/scala/mdr/pdb/json/Codecs.scala +++ b/json/src/main/scala/mdr/pdb/json/Codecs.scala @@ -5,6 +5,7 @@ trait Codecs: + given JsonCodec[WhoWhen] = DeriveJsonCodec.gen given JsonCodec[OsobniCislo] = JsonCodec.string.transform(OsobniCislo.apply, _.toString) given JsonFieldEncoder[OsobniCislo] = diff --git a/project/DomainProjectsPlugin.scala b/project/DomainProjectsPlugin.scala index 0228623..03941e8 100644 --- a/project/DomainProjectsPlugin.scala +++ b/project/DomainProjectsPlugin.scala @@ -238,7 +238,14 @@ def commandProjects = { val cb = pb("command") import cb._ - CommandProjects(commonProjects(cb), p("entity")) + val common = commonProjects(cb) + CommandProjects( + common, + p("entity").dependsOn( + common.model.projects(JVMPlatform), + common.json.projects(JVMPlatform) + ) + ) } EntityProject(