F#: Bank Account Kata (Edge cases)

In the previous Bank Account kata, I attempted to implement happy path logic using a function-driven approach to later discover my types that would complement them.

Note that my initial implementation did not cover edge case scenarios.

For example:
Can one withdraw an amount that exceeds their balance?

Can one deposit absolutely nothing into their account?

My types were updated to the following:

type Account =        
    | Checking of decimal
    | Savings  of decimal
    | Business of decimal

type Commands =
    | Withdraw of Account * decimal
    | Deposit  of Account * decimal
    | Transfer of Account * Account * decimal

type Response =
    | Withdrawal       of State * State
    | WithdrawalFailed of Account * decimal

    | Deposited        of State * State             
    | DepositFailed    of Account * decimal

    | Transferred      of TransferSummary   * decimal
    | TransferFailed   of Account * Account * decimal

and State =
    | BeforeDeposit of Account * decimal
    | AfterDeposit  of Account

    | BeforeWithdrawal of Account * decimal
    | AfterWithdrawal  of Account

and TransferSummary = { 
    FromBalanceBefore: Account ; ToBalanceBefore: Account
    FromBalanceAfter:  Account ; ToBalanceAfter:  Account }

My functions were updated to the following:

(*Functions*)
let balanceOf = function
                | Checking v
                | Savings  v
                | Business v -> v

let update account amount operator = account |>  function
           | Checking v -> Checking (v + (operator * amount))
           | Savings  v -> Savings  (v + (operator * amount))
           | Business v -> Business (v + (operator * amount))

let debit       account amount =  update account amount (- 1m)
let credit      account amount =  update account amount (+ 1m)
let canWithdraw account amount = balanceOf account >= amount

let handleWithdraw =  function
                      | Withdraw (account , amount) -> 
                        if canWithdraw account amount then
                             Some (Withdrawal(BeforeWithdrawal (account ,     amount),
                                              AfterWithdrawal  (debit account amount)))
                        else Some (WithdrawalFailed (account, amount) )
                      | _ -> None

let handleDeposit =   function
                      | Deposit   (account , amount) ->
                        if amount > 0m then 
                             Some (Deposited((BeforeDeposit (account , amount),
                                              AfterDeposit  (credit account amount) )))
                        else Some (DepositFailed (account , amount))
                      | _ -> None

let handleTransfer =  function
                      | Transfer (fromAccount, toAccount, amount) ->
                        if canWithdraw fromAccount amount then
                            Some (Transferred ({ FromBalanceBefore=fromAccount
                                                 ToBalanceBefore=  toAccount

                                                 FromBalanceAfter= debit  fromAccount amount 
                                                 ToBalanceAfter=   credit toAccount   amount } ,

                                               amount))
                            else Some (TransferFailed (fromAccount, toAccount, amount))
                      | _ -> None

Here are some client calls for the updated code:

(*Client*)
let deposit =   handleDeposit  (Deposit  (Checking 100m , 10m))
let withdrawl = handleWithdraw (Withdraw (Savings  10m , 10m))
let transfer =  handleTransfer (Transfer (Checking 100m , Savings 150m , 10m))

Here’s the output from the client calls:

val deposit : Response option =
  Some
    (Deposited
       (BeforeDeposit (Checking 100M,10M),AfterDeposit (Checking 110M)))
val withdrawl : Response option =
  Some
    (Withdrawal
       (BeforeWithdrawal (Savings 10M,10M),AfterWithdrawal (Savings 0M)))
val transfer : Response option =
  Some (Transferred ({FromBalanceBefore = Checking 100M;
                      ToBalanceBefore = Savings 150M;
                      FromBalanceAfter = Checking 90M;
                      ToBalanceAfter = Savings 160M;},10M))

The entire domain is below:

module BankAccountImpl2

type Account =        
    | Checking of decimal
    | Savings  of decimal
    | Business of decimal

type Commands =
    | Withdraw of Account * decimal
    | Deposit  of Account * decimal
    | Transfer of Account * Account * decimal

type Response =
    | Withdrawal       of State * State
    | WithdrawalFailed of Account * decimal

    | Deposited        of State * State             
    | DepositFailed    of Account * decimal

    | Transferred      of TransferSummary   * decimal
    | TransferFailed   of Account * Account * decimal

and State =
    | BeforeDeposit of Account * decimal
    | AfterDeposit  of Account

    | BeforeWithdrawal of Account * decimal
    | AfterWithdrawal  of Account

and TransferSummary = { 
    FromBalanceBefore: Account ; ToBalanceBefore: Account
    FromBalanceAfter:  Account ; ToBalanceAfter:  Account }

(*Functions*)
let balanceOf = function
                | Checking v
                | Savings  v
                | Business v -> v

let update account amount operator = account |>  function
           | Checking v -> Checking (v + (operator * amount))
           | Savings  v -> Savings  (v + (operator * amount))
           | Business v -> Business (v + (operator * amount))

let debit       account amount =  update account amount (- 1m)
let credit      account amount =  update account amount (+ 1m)
let canWithdraw account amount = balanceOf account >= amount

let handleWithdraw =  function
                      | Withdraw (account , amount) -> 
                        if canWithdraw account amount then
                             Some (Withdrawal(BeforeWithdrawal (account ,     amount),
                                              AfterWithdrawal  (debit account amount)))
                        else Some (WithdrawalFailed (account, amount) )
                      | _ -> None

let handleDeposit =   function
                      | Deposit   (account , amount) ->
                        if amount > 0m then 
                             Some (Deposited((BeforeDeposit (account , amount),
                                              AfterDeposit  (credit account amount) )))
                        else Some (DepositFailed (account , amount))
                      | _ -> None

let handleTransfer =  function
                      | Transfer (fromAccount, toAccount, amount) ->
                        if canWithdraw fromAccount amount then
                            Some (Transferred ({ FromBalanceBefore=fromAccount
                                                 ToBalanceBefore=  toAccount

                                                 FromBalanceAfter= debit  fromAccount amount 
                                                 ToBalanceAfter=   credit toAccount   amount } ,

                                               amount))
                            else Some (TransferFailed (fromAccount, toAccount, amount))
                      | _ -> None

(*Client*)
let deposit =   handleDeposit  (Deposit  (Checking 100m , 10m))
let withdrawl = handleWithdraw (Withdraw (Savings  10m , 10m))
let transfer =  handleTransfer (Transfer (Checking 100m , Savings 150m , 10m))
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: