Skip to content

Instantly share code, notes, and snippets.

@yordis
Forked from akhansari/EsBankAccount.ts
Created November 14, 2023 19:21
Show Gist options
  • Save yordis/9bac5d4db2f2962dd9f8122894a3fa53 to your computer and use it in GitHub Desktop.
Save yordis/9bac5d4db2f2962dd9f8122894a3fa53 to your computer and use it in GitHub Desktop.
TypeScript prototype of the Decider pattern. (F# version: https://github.com/akhansari/EsBankAccount)
import * as assert from "assert";
/** Decider Pattern **/
type Transaction = {
amount: number
date: Date
}
type Deposited = Transaction & {
kind: "deposited"
}
type Withdrawn = Transaction & {
kind: "withdrawn"
}
type Closed = {
kind: "closed"
closedOn: Date
}
export type Event_ =
| Deposited
| Withdrawn
| Closed
type State = {
Balance: number
IsClosed: boolean
}
export module State {
export const initial: Readonly<State> = {
Balance: 0,
IsClosed: false,
}
}
export const evolve = (state: Readonly<State>, event: Readonly<Event_>): Readonly<State> => {
switch (event.kind) {
case "deposited":
return { ...state, Balance: state.Balance + event.amount }
case "withdrawn":
return { ...state, Balance: state.Balance - event.amount }
case "closed":
return { ...state, IsClosed: true }
}
}
export type Command =
| { kind: "deposit", amount: number, date: Date }
| { kind: "withdraw", amount: number, date: Date }
| { kind: "close", date: Date }
export module Command {
export const deposit = (amount: number, date: Date): Command =>
({ kind: "deposit", amount: amount, date: date })
export const withdraw = (amount: number, date: Date): Command =>
({ kind: "withdraw", amount: amount, date: date })
export const close = (date: Date): Command =>
({ kind: "close", date: date })
}
export const decide = (command: Readonly<Command>) => (state: Readonly<State>): ReadonlyArray<Event_> => {
switch (command.kind) {
case "deposit":
return [ { kind: "deposited", amount: command.amount, date: command.date } ]
case "withdraw":
return [ { kind: "withdrawn", amount: command.amount, date: command.date } ]
case "close":
const events: Event_[] = []
if (state.Balance > 0) {
events.push({ kind: "withdrawn", amount: state.Balance, date: command.date })
}
events.push({ kind: "closed", closedOn: command.date })
return events
}
}
/** Tests **/
function deciderSpec<S, C, E>(
initialState: Readonly<S>,
evolve: (s: Readonly<S>, e: Readonly<E>) => Readonly<S>,
decide: (c: Readonly<C>) => (s: Readonly<S>) => ReadonlyArray<E>) {
const spec: { State: Readonly<S>, Outcome: ReadonlyArray<E> } = {
State: initialState,
Outcome: [],
}
return {
given(events: ReadonlyArray<E>) {
spec.State = events.reduce(evolve, spec.State)
return this
},
when(command: Readonly<C>) {
const events = decide(command)(spec.State)
spec.State = events.reduce(evolve, spec.State)
spec.Outcome = events
return this
},
then(expectedEvents: ReadonlyArray<E>) {
assert.deepStrictEqual(spec.Outcome, expectedEvents)
return this
},
}
}
const test = (message: string, fn: Function): void => {
try {
fn()
console.log(" ✅ " + message)
} catch (e) {
console.error(" ❌ " + message)
console.error(e)
}
}
const anyDate = new Date(2000, 1, 1)
test("make a deposit", () => {
deciderSpec(State.initial, evolve, decide)
.when(Command.deposit(5, anyDate))
.then([ { kind: "deposited", amount: 5, date: anyDate } ])
})
test("make a withdrawal", () => {
deciderSpec(State.initial, evolve, decide)
.when(Command.withdraw(5, anyDate))
.then([ { kind: "withdrawn", amount: 5, date: anyDate } ])
})
test("close the account and withdraw the remaining amount", () => {
deciderSpec(State.initial, evolve, decide)
.given([
{ kind: "deposited", amount: 50, date: anyDate },
])
.when(Command.deposit(50, anyDate))
.when(Command.close(anyDate))
.then([
{ kind: "withdrawn", amount: 100, date: anyDate },
{ kind: "closed", closedOn: anyDate },
])
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment