Phase III

Eventify

  • Edit Lottery.ksmile
stream-module LotteryStream at com.metastay.lotterystream
  • $smile
  • Edit LotteryStream.kstream , add lottery related events
event LotteryCreated(lotteryName: String, amount: Int)
    event ParticipantAdded(lotteryName: String, participantName: String)
    event LotteryRan(lotteryName: String, winner: String)
  • $compile
  • Edit LotteryStreamTestSuite.scala
test("test1", Tag("event")) {
        val created = LotteryCreated("Loto", 2000);
        LotteryStreamWriter.appendEvent(created);
        val ramAdded = ParticipantAdded("Loto", "ram");
        LotteryStreamWriter.appendEvent(ramAdded);
        val johnAdded = ParticipantAdded("Loto", "john");
        LotteryStreamWriter.appendEvent(johnAdded);
        val ran = LotteryRan("Loto", "john");
        LotteryStreamWriter.appendEvent(ran);
     }

    test("test2", Tag("read")) {
        LotteryStreamReader.size.println
    }
  • See conf file, Database and how the events are getiing stored.

Events and Commands

  • Add stream dependency to Domain module in lottery.ksmile.
domain-module LotteryDomain(LotteryMongo,LotteryStream) at com.metastay.lotterydomain
  • add stream to aggregate,and raise events from commands in LotteryDomain.kdomain
aggregate[LotteryStream] Lottery {
     domain-ref lottery:Lottery
     command create {
        input(lotteryName:String, amount:Int)
        pre {
            require lotterMustNotExist "Lottery must not exist" => !(lottery.lotteryMustExist(input.lotteryName))
        }
        event-raised(created:LotteryCreated)
     }

     command addParticipant {
        input(lotteryName:String, participantName:String)
        pre {
            require lotteryMustExists "Lottery must exist" => lottery.lotteryMustExist(input.lotteryName)
            require newParticpant => !lottery.participantExist(input.lotteryName, input.participantName) failing "participant ${input.participantName} is    already add to this lottery"
        }
        event-raised(added:ParticipantAdded)
     }


    command run {

        input (lotteryName: String)
        pre{
            require notYetRun => lottery.lotteryOpen(input.lotteryName) failing "Lottery has already run!"
            require existingLotteryName => lottery.lotteryNameExists(input.lotteryName) failing "Lottery name does not exist"
                require atLeastTwoParticipants => existingLotteryName -> `domainRef.lottery.participantCount(input.lotteryName) > 1` failing "The lottery should have at least two participants"
        }
        event-raised(ran: LotteryRun)
        output(winner: String)
  • $smile
  • $compile
  • fix the compilation error
override def create = CreateCommand {
     import CreateCommand._
     input: Input =>
        Event(LotteryCreated(input.lotteryName, input.amount)) -> Output()
     }
    override def addParticipant = AddParticipantCommand {
     import AddParticipantCommand._
     input: Input =>
        Event(ParticipantAdded(input.lotteryName, input.participantName)) -> Output()
     }
    override def run = RunCommand {
     import RunCommand._
     input: Input =>
        val participantCount = domainRef.lottery.participantCount(input.lotteryName)
        val winnerIndex = scala.util.Random.nextInt(participantCount)
        val participantList = LotteryQuery().lotteryName.is(input.lotteryName).findOne.get.participantList
        val winner = participantList(winnerIndex)

        Event(LotteryRan(input.lotteryName, winner)) -> Output(winner)
     }
  • Edit LotteryDomain.kdomain and add the handler after domain Lottery{ }
handler[LotteryStream] LotteryStream All
  • $compile
  • move the code from LotteryAggregateCode to LotteryStreamAllHandlerCode
override def handle(event: LotteryCreated, context: EventContext): Unit =
     LotteryWriter.save(LotteryRow(lotteryName = event.lotteryName, amount = event.amount, open = true));

     override def handle(event: ParticipantAdded, context: EventContext): Unit = {
     val q = LotteryQuery().lotteryName.is(event.lotteryName);
     val u = LotteryUpdate().participantList.addToSet(event.participantName);
     LotteryWriter().updateOne(q, u)
     }

     override def handle(event: LotteryRan, context: EventContext): Unit = {
     val q = LotteryQuery().lotteryName.is(event.lotteryName);
     val u = LotteryUpdate().open.set(false).winner.set(event.winner);
     LotteryWriter().updateOne(q, u)
     }

More Commands!

  • Edit CreateCommandTestSuite.scala
test("test1", Tag("p1")) {
             LotteryWriter.drop();
             grab[CreateCommandFixture]
             .given()
             .when(Input("WBLoto", 200))
             .expectEvents(Event(LotteryCreated("WBLoto", 200)))
     }
    test("test2", Tag("n1")) {
             LotteryWriter.drop();
             grab[CreateCommandFixture]
             .given(LotteryCreated("WBLoto", 100))
             .when(Input("WBLoto", 200))
             .expectRequireFail("lotterMustNotExist");
     }
  • $test-only com.metastay.lotterydomain.aggregate.lottery.CreateCommandTestSuite
  • Edit AddParticipantCommandTestSuite.scala
test("test1", Tag("p1")) {
             grab[AddParticipantCommandFixture]
             .given(LotteryCreated("TestLottery", 1000))
             .when(Input(lotteryName = "TestLottery",participantName = "XYZ"))
             .expectEvents(Event(ParticipantAdded(lotteryName = "TestLottery",participantName = "XYZ")))
    }
    test("test2", Tag("n1")) {
             grab[AddParticipantCommandFixture]
             .given(LotteryCreated("TestLottery", 1000), ParticipantAdded(lotteryName = "TestLottery",participantName = "XYZ") )
             .when(Input(lotteryName = "TestLottery",participantName = "XYZ"))
             .expectRequireFail
    }
  • $test-only com.metastay.lotterydomain.aggregate.lottery.AddParticipantCommandTestSuite

Projection

  • Edit Lottery.ksmile - Add dependency of LotteryQuery in the Play module as well.
mongo-module LotteryReadMongo at com.metastay.lotteryreadmongo
    query-module LotteryQuery(LotteryReadMongo, LotteryStream) at com.metastay.lotteryquery
  • $smile,refresh eclipse
  • edit LotteryReadMongo.kmongo
collection LotteryRead {
     property lotteryName:String
     property amount:Int
     property participantList:String*
     property winner:String?
     property open:Boolean
    }
  • edit LotterQuery.kqyuery
projector [LotteryStream] Lottery All
  • $compile
  • Edit LotteryAllProjectorCode
override def project(event: LotteryCreated, context: EventContext): Unit = {
        LotteryReadWriter().save(LotteryReadRow(lotteryName = event.lotteryName, amount = event.amount, open = true))
}

override def project(event: ParticipantAdded, context: EventContext): Unit = {
        val q = LotteryReadQuery().lotteryName.is(event.lotteryName);
        val u = LotteryReadUpdate().participantList.addToSet(event.participantName)
        LotteryReadWriter().updateOne(q, u)
}
override def project(event: LotteryRan, context: EventContext): Unit = {
        val q = LotteryReadQuery().lotteryName.is(event.lotteryName)
        val u = LotteryReadUpdate().open.set(false).winner.set(event.winner)
        LotteryReadWriter().updateOne(q, u)
}
  • Edit LotteryWebReaderCode
override def lotteryList: Request[LotteryListView.Input] => List[Lottery] = {
      import LotteryListView._
      request: Request[Input] =>
        val input = request.body;
        LotteryReadQuery().find.map(
          row =>
            Lottery(
              lotteryName = row.lotteryName,
              amount = row.amount,
              participantList =  row.participantList,
              status = if(row.open)  "OPEN" else "CLOSED",
              winner = row.winner
            )
        )
    }

Review Write DB

  • we dont really need amount and winner in write db, they dont get involve in any of the decision making in domain.
  • edit LotteryMongo.kmongo in eclipse
collection Lottery {
        property lotteryName:String
        //property amount:Int
        property participantList:String*
        //property winner:String?
        property open:Boolean
    }

Send Email

  • Sending email to winner needs to capture email addresses for the participants, needs to change database to capture the email address
  • Capturing email of a participant and sending email to the winner
  • Edit LotteryReadMongo.kmongo
collection LotteryRead {
             property lotteryName:String
             property amount:Int
             reference participantList:Participant*
             property winner:String?
             property open:Boolean
            }
    collection Participant {
             property name:String
             property email:String
    }
  • participant’s email needs to be added to the event
  • Edit LotteryStream.kstream
event ParticipantAdded(lotteryName: String, participantName: String,email:String)
  • Edit LotteryDomain.kdomain,replace addParticipant command with the following,
command addParticipant {
        input(lotteryName:String, participantName:String,participantEmail: String)
        pre {
            require lotteryMustExists "Lottery must exist" => lottery.lotteryMustExist(input.lotteryName)
            require newParticpant => !lottery.participantExist(input.lotteryName, input.participantName) failing "participant ${input.participantName} is    already add to this lottery"
        }
        event(added:ParticipantAdded)
    }
  • $compile, and fix the compiler errors
  • Edit LotteryAllProjectorCode
override def project(event: ParticipantAdded, context: EventContext): Unit = {
        val q = LotteryReadQuery().lotteryName.is(event.lotteryName);
        Participant p = ParticipantWriter.save(ParticipantRow(event.participantName,event.participantEmail))
        val u = LotteryReadUpdate().participantList.addToSet(p)
        LotteryReadWriter.updateOne(q, u)

    }