Last active
April 8, 2024 12:40
-
-
Save akhansari/b43a9a60ba1ca3f2a8c8705aa0db3efb to your computer and use it in GitHub Desktop.
TypeScript prototype of the Decider pattern. (F# version: https://github.com/akhansari/EsBankAccount)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
@cjjohansen It's a complicated question, because this bank account example is oversimplified.
I'm not an expert of banking system, but for sure we are in a fully asynchronous system.
Normally we do a request and once the remote system acknowledge the transfer then we can make a withdrawal.
It makes sens to have a MoneyTransferRequest stream to persist different steps and especially all legal events.
So there shouldn't be any change in the BankAccount stream.