Adding more logic

In our simple example a customer can always deposit money, but making a cash withdrawal is another thing. We should ensure that the customer has the requested funds available before releasing them, lest the account overdraw.

When discussing events, we noted that that the process of applying events cannot produce an error since it is a past event. Instead, errors should be produced before the event is generated, during the processing of the command.

Account withdrawal - happy path

First, let's add a test for a happy path withdrawal, again with a previous deposit using the given initial method:

#[test]
fn test_withdraw_money() {
    let previous = BankAccountEvent::CustomerDepositedMoney(CustomerDepositedMoney { amount: 200.0, balance: 200.0 });
    let expected = BankAccountEvent::CustomerWithdrewCash(CustomerWithdrewCash { amount: 100.0, balance: 100.0 });

    AccountTestFramework::default()
        .given(vec![previous])
        .when(WithdrawMoney{ amount: 100.0 })
        .then_expect_events(vec![expected]);
}

Since we have not added any withdrawal logic yet this should fail. Let's correct this with some naive logic to produce the event:

impl Command<BankAccount, BankAccountEvent> for WithdrawMoney {
    fn handle(self, account: &BankAccount) -> Result<Vec<BankAccountEvent>, AggregateError> {
        let balance = account.balance - self.amount;
        let event_payload = CustomerWithdrewCash{
            amount: self.amount,
            balance
        };
        Ok(vec![BankAccountEvent::CustomerWithdrewCash(event_payload)])
    }
}

Verify funds are available

Now we have success with our happy path test, but then there is nothing to stop a customer from withdrawing more than is deposited. Let's add a test case using the then_expect_error expect case:

#[test]
fn test_withdraw_money_funds_not_available() {
    AccountTestFramework::default()
        .given_no_previous_events()
        .when(WithdrawMoney{ amount: 200.0 })
        .then_expect_error("funds not available")
}

We should see our new test fail since our naive logic cannot handle this yet. Now we will need to update our command logic to return an error when this situation arises:

impl Command<BankAccount, BankAccountEvent> for WithdrawMoney {
    fn handle(self, account: &BankAccount) -> Result<Vec<BankAccountEvent>, AggregateError> {
        let balance = account.balance - self.amount;
        if balance < 0_f64 {
            return Err(AggregateError::new("funds not available"))
        }
        let event_payload = CustomerWithdrewCash{
            amount: self.amount,
            balance
        };
        Ok(vec![BankAccountEvent::CustomerWithdrewCash(event_payload)])
    }
}

And we should now see our test pass.

Note that handling a command is always an atomic process, either all produced events become a part of the factual history of this aggregate instance, or an error is returned.