Recipes · 1 of 6

Banking — transfers, withdrawals, statements

Six procedures that together model a small bank: open account, deposit, withdraw, transfer, freeze, monthly statement.

Schema

accounts:     {_id, account_id, name, balance, frozen, created_at}
ledger:       {account_id, type, amount, ts}
transfers:    {from, to, amount, ts}

open_account

proc open_account(account_id, name, initial) {
    if count("accounts", {account_id: account_id}) > 0 {
        abort "account_id taken"
    }
    if initial < 0 { abort "initial deposit cannot be negative" }
    let id = insert("accounts", {
        account_id: account_id, name: name, balance: initial, frozen: false
    })
    insert("ledger", {account_id: account_id, type: "open", amount: initial})
    return {ok: true, id: id}
}

deposit

proc deposit(account_id, amount) {
    if amount <= 0 { abort "amount must be positive" }
    let acc = find_one("accounts", {account_id: account_id})
    if acc == null   { abort "account not found" }
    if acc.frozen     { abort "account frozen" }
    update("accounts", {account_id: account_id}, {$inc: {balance: amount}})
    insert("ledger", {account_id: account_id, type: "deposit", amount: amount})
    return {ok: true, balance: acc.balance + amount}
}

withdraw

proc withdraw(account_id, amount) {
    if amount <= 0 { abort "amount must be positive" }
    let acc = find_one("accounts", {account_id: account_id})
    if acc == null             { abort "account not found" }
    if acc.frozen               { abort "account frozen" }
    if acc.balance < amount     { abort "insufficient funds" }
    update("accounts", {account_id: account_id}, {$inc: {balance: -amount}})
    insert("ledger", {account_id: account_id, type: "withdraw", amount: -amount})
    return {ok: true, balance: acc.balance - amount}
}

transfer

proc transfer(from, to, amount) {
    if from == to    { abort "self-transfer" }
    if amount <= 0 { abort "invalid amount" }

    let s = find_one("accounts", {account_id: from})
    let r = find_one("accounts", {account_id: to})
    if s == null || r == null          { abort "account not found" }
    if s.frozen || r.frozen           { abort "account frozen" }
    if s.balance < amount             { abort "insufficient funds" }

    update("accounts", {account_id: from}, {$inc: {balance: -amount, sent_count: 1}})
    update("accounts", {account_id: to},   {$inc: {balance:  amount, recv_count: 1}})
    insert("transfers", {from: from, to: to, amount: amount})
    return {ok: true}
}

freeze / unfreeze

proc freeze(account_id, reason, actor) {
    update("accounts", {account_id: account_id}, {$set: {frozen: true}})
    insert("audit_log", {action: "freeze", target: account_id, reason: reason, actor: actor})
    return {ok: true}
}

proc unfreeze(account_id, actor) {
    update("accounts", {account_id: account_id}, {$set: {frozen: false}})
    insert("audit_log", {action: "unfreeze", target: account_id, actor: actor})
    return {ok: true}
}

monthly_statement

proc monthly_statement(account_id, year, month) {
    let acc = find_one("accounts", {account_id: account_id})
    if acc == null { abort "account not found" }

    let entries = find("ledger", {account_id: account_id, year: year, month: month})
    let totals = aggregate("ledger", [
        {$match: {account_id: account_id, year: year, month: month}},
        {$group: {_id: "$type", total: {$sum: "$amount"}, n: {$sum: 1}}}
    ])
    return {
        account: acc.name, balance: acc.balance,
        period: {year: year, month: month},
        entries: entries, totals: totals
    }
}
Report Issue