Skip to content

Instantly share code, notes, and snippets.

@NikitaMelnikov
Created September 18, 2016 12:07
Show Gist options
  • Save NikitaMelnikov/74f21342a925d883578f7c36fa24c687 to your computer and use it in GitHub Desktop.
Save NikitaMelnikov/74f21342a925d883578f7c36fa24c687 to your computer and use it in GitHub Desktop.
package com.bitbucket.fsm
import akka.actor._
import akka.persistence.fsm.PersistentFSM
import akka.persistence.fsm.PersistentFSM._
import akka.stream.ActorMaterializer
import com.bitbucket.fsm.Order._
import scala.concurrent.Future
import scala.reflect._
import scala.util.Success
object Order {
type Timestamp = Long
sealed trait Command
case class AddItem(item: Item) extends Command
case class AddDiscount(discount: Discount) extends Command
case class AcceptDiscount(discount: Discount) extends Command
case class RejectDiscount(cause: Throwable) extends Command
case object Pay extends Command
case class RejectPayment(cause: Throwable) extends Command
case object CompletePayment extends Command
case class Item(name: String, cost: Int)
case class Discount(code: String, amount: Int)
sealed trait State extends FSMState
case object Empty extends State {
def identifier: String = "created"
}
case object Buying extends State {
def identifier: String = "buying"
}
case object WaitingPayment extends State {
def identifier: String = "payment"
}
case object Payed extends State {
def identifier: String = "payed"
}
case object Rejected extends State {
def identifier: String = "rejected"
}
sealed trait Data {
def items: Set[Item]
def discount: Option[Discount]
def payed: Boolean
def cost: Int = items.map(_.cost).sum
def amount: Int = cost - discount.map(_.amount).getOrElse(0)
}
case class EmptyOrder() extends Data {
def items: Set[Item] = Set.empty[Item]
def discount: Option[Discount] = None
def payed: Boolean = false
}
case class OrderData(items: Set[Item], discount: Option[Discount]) extends Data {
def payed: Boolean = false
}
case class PayedOrder(items: Set[Item], discount: Option[Discount], completedAt: Timestamp) extends Data {
def payed: Boolean = true
}
case class RejectedOrder(items: Set[Item], discount: Option[Discount], cause: Exception, payed: Boolean = false) extends Data
sealed trait DomainEvent
case class ItemAdded(item: Item) extends DomainEvent
case class DiscountAdded(discount: Discount) extends DomainEvent
case class DiscountRejected(discount: Discount) extends DomainEvent
case class DiscountApplied(discount: Discount) extends DomainEvent
case class PaymentStarted(amount: Int, at: Timestamp) extends DomainEvent
case class PaymentRejected(cause: Throwable) extends DomainEvent
case object PaymentComplete extends DomainEvent
}
class Order(id: String) extends PersistentFSM[Order.State, Data, DomainEvent] {
implicit def domainEventClassTag: ClassTag[DomainEvent] = ClassTag(classOf[DomainEvent])
def persistenceId: String = s"user-$id"
startWith(Empty, EmptyOrder())
when(Empty) {
case Event(AddItem(item), _) => goto(Buying) applying ItemAdded(item)
}
when(Buying) {
case Event(AddItem(item), _) => stay applying ItemAdded(item)
case Event(AddDiscount(discount), _) => stay andThen { _ =>
applyDiscount(discount) onComplete {
case Success(_) => self ! AcceptDiscount(discount)
case scala.util.Failure(cause) => self ! RejectDiscount(cause)
}
}
case Event(AcceptDiscount(discount), _) => stay applying DiscountAdded(discount)
case Event(RejectDiscount(cause), _ ) => stay // TODO: Notify
case Event(Pay, data: OrderData) => goto(WaitingPayment) applying PaymentStarted(data.cost, time) andThen { _ =>
pay(id, data.cost) onComplete {
case Success(_) => self ! PaymentComplete
case scala.util.Failure(cause) => self ! PaymentRejected(cause)
}
}
}
when(WaitingPayment) {
case Event(PaymentComplete, _) => goto(Payed) applying PaymentComplete
case Event(PaymentRejected, _) => ???
}
when(Payed) {
case Event(CompletePayment, _) => goto(Payed) applying PaymentComplete
case Event(RejectPayment(cause), _) => goto(Rejected) applying PaymentRejected(cause)
}
def applyEvent(domainEvent: DomainEvent, currentData: Data): Data = {
(domainEvent, currentData) match {
case (ItemAdded(item), data: EmptyOrder) => OrderData(items = Set(item), discount = None)
case (ItemAdded(item), data: OrderData) => data.copy(items = data.items + item)
???
}
}
// TODO:
def pay(id: String, amount: Int): Future[Unit] = ???
// TODO:
def applyDiscount(discount: Discount): Future[Unit] = ???
def time = System.currentTimeMillis()
}
object Boot extends App {
implicit val system = ActorSystem()
implicit val ec = system.dispatcher
implicit val materializer = ActorMaterializer()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment