Skip to main content
Version: Previous

Day four

Day three recap
Here are the main takeaways from Day three.
  • We introduced Views and how you can use them.
  • We extended our application adding Position table.
  • We started writing some automated tests for our application.
  • We added calculated data (derived fields) into our main View.
  • Consolidators perform data aggregation and calculations, and here we created one to real-time feed the Positions table.
  • We added a grid in the UI to display the Positions using Grid Pro, with more flexibility to customize the HTML and CSS.
  • This day covers:

    State Management​

    State Machines enable you to control workflow by defining the transitions from state to state. This example enables you to build a very simple state machine so that you can add new trades. You will create a new field called TRADE_STATUS, which can have three possible states: NEW, ALLOCATED, CANCELLED.

    • NEW can go to ALLOCATED or CANCELLED.
    • ALLOCATED and CANCELLED can’t go anywhere else.
    • NEW is the only state you can use to insert new records.

    Once we have added add a new field to the data model, we will edit the Event Handler file to add controlled transitions from one state to another.

    1. Data model

    Make sure you have added the TRADE_STATUS field to the TRADE table in the alpha-tables-dictionary.kts file.

    tables {
    table (name = "TRADE", id = 2000) {
    ...
    TRADE_STATUS

    primaryKey {
    TRADE_ID
    }

    }
    ...
    }

    If the TRADE_STATUS is missing, run generatefields to generate the fields, AND​ generatedao to create the DAOs.

    2. Create a new class for the State Machine

    Add a main folder in the Event Handler module alpha-eventhandler and create a state machine class called TradeStateMachine inside alpha-eventhandler/src/main/kotlin/global/genesis.

    Add a state machine definition and assign a field in the onCommit block,

    package global.genesis

    import com.google.inject.Inject
    import global.genesis.commons.annotation.Module
    import global.genesis.db.rx.entity.multi.AsyncEntityDb
    import global.genesis.db.rx.entity.multi.AsyncMultiEntityReadWriteGenericSupport
    import global.genesis.db.statemachine.StateMachine
    import global.genesis.db.statemachine.Transition
    import global.genesis.gen.dao.Trade
    import global.genesis.gen.dao.enums.TradeStatus

    @Module
    class TradeStateMachine @Inject constructor(
    db: AsyncEntityDb
    ) {
    private val internalState: StateMachine<Trade, TradeStatus, TradeEffect> = db.stateMachineBuilder {
    readState { tradeStatus }

    state(TradeStatus.NEW) {
    isMutable = true

    initialState(TradeEffect.New) {
    onValidate { trade ->
    if (trade.isTradeIdInitialised && trade.tradeId.isNotEmpty()) {
    verify {
    db hasNoEntry Trade.ById(trade.tradeId)
    }
    }
    }
    }

    onCommit { trade ->
    if (trade.enteredBy == "TestUser") {
    trade.price = 10.0
    }
    }

    transition(TradeStatus.ALLOCATED, TradeEffect.Allocated)
    transition(TradeStatus.CANCELLED, TradeEffect.Cancelled)
    }

    state(TradeStatus.ALLOCATED) {
    isMutable = false

    transition(TradeStatus.NEW, TradeEffect.Cancelled)
    transition(TradeStatus.CANCELLED, TradeEffect.Cancelled)
    }

    state(TradeStatus.CANCELLED) {
    isMutable = false
    }
    }

    suspend fun insert(trade: Trade): Transition<Trade, TradeStatus, TradeEffect> = internalState.create(trade)

    suspend fun modify(tradeId: String, modify: suspend (Trade) -> Unit): Transition<Trade, TradeStatus, TradeEffect>? =
    internalState.update(Trade.ById(tradeId)) { trade, _ -> modify(trade) }

    suspend fun modify(trade: Trade): Transition<Trade, TradeStatus, TradeEffect>? = internalState.update(trade)
    }

    sealed class TradeEffect {
    object New : TradeEffect()
    object Allocated : TradeEffect()
    object Cancelled : TradeEffect()
    }

    3. Add the module as a dependency in build.gradle.kts

    Add the module as a dependency in the build.gradle.kts inside alpha-script-config module.

    ...
    api(project(":alpha-eventhandler"))
    ...

    4. Add an integrated State Machine to the Event Handler

    Let's edit the Event Handler to add an integrated State Machine. First, in the alpha-eventhandler.kts file, add the imports below and declare a variable to be visible to all events by injecting the class TradeStateMachine, which we have just created.

    import java.io.File
    import java.time.LocalDate
    import global.genesis.TradeStateMachine
    import global.genesis.gen.dao.Trade
    import global.genesis.alpha.message.event.TradeAllocated
    import global.genesis.alpha.message.event.TradeCancelled
    import global.genesis.commons.standards.GenesisPaths
    import global.genesis.gen.view.repository.TradeViewAsyncRepository
    import global.genesis.jackson.core.GenesisJacksonMapper
    ...
    eventHandler {
    val stateMachine = inject<TradeStateMachine>()

    eventHandler<Trade>(name = "TRADE_INSERT", transactional = true) {
    ...
    }
    ...
    }

    Then, integrate the State Machine in the TRADE_INSERT event onCommit.

    eventHandler<Trade>(name = "TRADE_INSERT", transactional = true) {
    onCommit { event ->
    val trade = event.details
    trade.enteredBy = event.userName
    stateMachine.insert(trade)
    ack()
    }
    }

    Create two data classes that will be used in the cancel and allocated eventHandler codeblocks. These classes should be in alpha-messages/src/main/kotlin/global/genesis/alpha/message/event.

    • TradeAllocated
    • TradeCancelled

    Both classes have a single field: tradeId.

    TradeAllocated:

    package global.genesis.alpha.message.event

    data class TradeAllocated(val tradeId: String)

    TradeCancelled:

    package global.genesis.alpha.message.event

    data class TradeCancelled(val tradeId: String)

    Create a new eventHandler codeblock called TRADE_CANCELLED to handle cancellations. Then integrate the State Machine in it.

    eventHandler<TradeCancelled>(name = "TRADE_CANCELLED", transactional = true) {
    onCommit { event ->
    val message = event.details
    stateMachine.modify(message.tradeId) { trade ->
    trade.tradeStatus = TradeStatus.CANCELLED
    }
    ack()
    }
    }

    Create a new eventHandler codeblock called TRADE_ALLOCATED to handle completion. Integrate the State Machine in it.

    eventHandler<TradeAllocated>(name = "TRADE_ALLOCATED", transactional = true) {
    onCommit { event ->
    val message = event.details
    stateMachine.modify(message.tradeId) { trade ->
    trade.tradeStatus = TradeStatus.ALLOCATED
    }
    ack()
    }
    }

    Modify or add the TRADE_MODIFY eventHandler codeblock to use the State Machine.

    eventHandler<Trade>(name = "TRADE_MODIFY", transactional = true) {
    onCommit { event ->
    val trade = event.details
    stateMachine.modify(trade)
    ack()
    }
    }

    Remove the TRADE_DELETE eventHandler codeblock if you included it before. You only want to manage the state of the trade. If a trade is incorrect and needs to be deleted, similar functionality can be achieved by cancelling the trade.

    To test it, you can try to modify a TRADE and see the states changing accordingly.

    Exercise 4.1: State Machines

    ESTIMATED TIME

    40 mins

    Modify the class TradeStateMachine to keep the trade.price. Remove the current rule when TradeStatus.NEW, and set the field trade.enteredBy to empty when TradeStatus.CANCELLED.

    UI CHANGES

    Open the home.ts file and add the TRADE_STATUS field in the const COLUMNS.

    ...
    //grid columns that will be showed
    const COLUMNS = [
    {
    ...defaultColumnConfig,
    field: 'TRADE_ID',
    headerName: 'Id',
    },
    {
    ...defaultColumnConfig,
    field: 'QUANTITY',
    headerName: 'Quantity',
    },
    {
    ...defaultColumnConfig,
    field: 'PRICE',
    headerName: 'Price',
    },
    {
    ...defaultColumnConfig,
    field: 'SYMBOL',
    headerName: 'Symbol',
    },
    {
    ...defaultColumnConfig,
    field: 'DIRECTION',
    headerName: 'Direction',
    },
    {
    ...defaultColumnConfig,
    field: 'TRADE_STATUS',
    headerName: 'Status',
    },
    ];

    @customElement({
    name,
    template,
    styles,
    })
    export class Home extends FASTElement {
    ...
    constructor() {
    super();
    }
    }

    And add the deleteEvent to the home.template.ts file.

    ...
    export const HomeTemplate = html<Home>`
    <div class="split-layout">
    <div class="top-layout">
    <entity-management
    resourceName="ALL_TRADES"
    title = "Trades"
    entityLabel="Trades"
    createEvent = "EVENT_TRADE_INSERT"
    deleteEvent = "EVENT_TRADE_CANCELLED"
    :columns=${x => x.columns}
    :createFormUiSchema=${() => tradeFormCreateSchema}
    :updateFormUiSchema=${() => tradeFormUpdateSchema}
    ></entity-management>
    ...
    </div>
    </div>
    `;

    Remember to run build and deploy after the changes, and test it directly in the UI.

    Adding logic to the Event Handler

    We are going to change the code in the Event Handler so that:

    • it checks if the counterparty exists in the database (by checking COUNTERPARTY_ID field)
    • it checks if the instrument exists in the database (by checking INSTRUMENT_ID field)
    • it is able to modify records with the same verification on counterparty and instrument

    Add the validation code

    Go to the alpha-eventhandler.kts file for the Event Handler.

    Add the verification by inserting a verify inside the onValidate block, before the onCommit block in TRADE_INSERT. We can see this below, with separate lines checking that the Counterparty ID and the Instrument ID exist in the database. The new block ends by sending an ack().

    eventHandler<Trade>(name = "TRADE_INSERT", transactional = true) {
    onValidate { event ->
    val message = event.details
    verify {
    entityDb hasEntry Counterparty.ById(message.counterpartyId.toString())
    entityDb hasEntry Instrument.byId(message.instrumentId.toString())
    }
    ack()
    }
    onCommit { event ->
    val trade = event.details
    trade.enteredBy = event.userName
    stateMachine.insert(trade)
    ack()
    }
    }
    verify function

    The verify block you see above is part of the validation helper provided by the Platform to make it easier to verify conditions against the database. Outside the verify block, you can write any Kotlin code to validate whatever you want to.

    Exercise 4.2: adding onValidate to Event Handlers

    ESTIMATED TIME

    20 mins

    Add the same verification onValidate as in TRADE_INSERT to the TRADE_MODIFY eventHandler codeblock.

    Implement and test the back end with Console or Postman. To do that, see the Day 2 example. Basically, you should create a POST request using the URL http://localhost:9064/EVENT_TRADE_MODIFY, as well as setting the header accordingly (header with SOURCE_REF and SESSION_AUTH_TOKEN).

    Auditing​

    We want to be able to track the changes made to the various trades on the TRADE table, such that we could see the times and modifications made in the history of the trade. So, we are going to add basic auditing to the TRADE table in order to keep a record of the changing states of the trades.

    This can be useful for historical purposes, if you need to be able to produce an accurate course of events at a later date.

    Adding basic auditing

    Adding audit to the table dictionary

    For basic auditing, the first step is to change the relevant table dictionary. In this instance, we will make changes to the alpha-tables-dictionary.kts, in order to add the parameter audit = details() to the table definition. It should resemble the following:

    table (name = "TRADE", id = 2000, audit = details(id = 2100, sequence = "TR")) {
    sequence(TRADE_ID, "TR")
    COUNTERPARTY_ID
    INSTRUMENT_ID not null
    QUANTITY
    PRICE not null
    SYMBOL
    DIRECTION
    TRADE_DATE
    ENTERED_BY
    TRADE_STATUS

    primaryKey {
    TRADE_ID
    }
    }

    The id parameter indicates the id of the newly created audit table, and must be different from any other table id.

    As we are using GPAL Event Handlers, this is sufficient to enable auditing on this table. A new table is created with the name of the original table, and the _AUDIT suffix added. In this instance, that would be the TRADE_AUDIT table.

    Updating the State Machine to use auditing

    Next you need to extend the insert, and modify methods in the TradeStateMachine.kt file. Specifically, each method must have a second option so that the method signature uses the AsyncMultiEntityReadWriteGenericSupport parameter and the internalState.withTransaction(transaction) { } code block. For example:

        suspend fun insert(
    transaction: AsyncMultiEntityReadWriteGenericSupport,
    trade: Trade,
    ): Transition<Trade, TradeStatus, TradeEffect> =
    internalState.withTransaction(transaction) {
    create(trade)
    }

    suspend fun modify(
    transaction: AsyncMultiEntityReadWriteGenericSupport,
    tradeId: String, modify: suspend (Trade) -> Unit
    ): Transition<Trade, TradeStatus, TradeEffect>? =
    internalState.withTransaction(transaction) {
    update(Trade.ById(tradeId)) {
    trade, _ -> modify(trade)
    }
    }

    suspend fun modify(
    transaction: AsyncMultiEntityReadWriteGenericSupport,
    trade: Trade
    ): Transition<Trade, TradeStatus, TradeEffect>? =
    internalState.withTransaction(transaction) {
    update(trade)
    }

    Update the Event Handlers to use auditing

    Now you must update the alpha-eventhandler.kts in order to pass the entityDb object into the updated methods of the State Machine, as the syncMultiEntityReadWriteGenericSupport parameter. This should resemble the example below:

        eventHandler<Trade>(name = "TRADE_INSERT", transactional = true) {
    onValidate { event ->
    val message = event.details
    verify {
    entityDb hasEntry Counterparty.ById(message.counterpartyId)
    entityDb hasEntry Instrument.ById(message.instrumentId)
    }
    ack()
    }
    onCommit { event ->
    val trade = event.details
    stateMachine.insert(entityDb, trade)
    ack()
    }
    }
    eventHandler<Trade>(name = "TRADE_MODIFY", transactional = true) {
    onCommit { event ->
    val trade = event.details
    stateMachine.modify(entityDb, trade)
    ack()
    }
    }
    eventHandler<TradeCancelled>(name = "TRADE_CANCELLED", transactional = true) {
    onCommit { event ->
    val message = event.details
    stateMachine.modify(entityDb, message.tradeId) { trade ->
    trade.tradeStatus = TradeStatus.CANCELLED
    }
    ack()
    }
    }
    eventHandler<TradeAllocated>(name = "TRADE_ALLOCATED", transactional = true) {
    onCommit { event ->
    val message = event.details
    stateMachine.modify(entityDb, message.tradeId) { trade ->
    trade.tradeStatus = TradeStatus.ALLOCATED
    }
    ack()
    }
    }

    Run the generatedao, build and deploy.

    Exercise 4.3: testing an audited table

    ESTIMATED TIME

    20 mins

    Try to insert or modify a TRADE and see the auditing happening accordingly. You can use DbMon or Genesis Console to check the data in table TRADE_AUDIT.

    END OF DAY 4

    This is the end of day 4 of our training. To help your training journey, check out how your application should look at the end of day 4 here.