14

Project: Accounting Library

For this project, we will be building the foundation for a double-entry accounting system called almighty-kaikei. Double-entry accounting is a system that is designed to prevent or detect accounting errors by tracking two sides of a ledger: the debit and credit sides.

There are five basic types of accounts in an double-entry accounting system: Asset, Expense, Income, Liability, and Equity. All of the accounts are recorded in what's called a Chart of Accounts.

The types are divided into two groups: left hand side (LHS) and right hand side (RHS) accounts. Assets and Expenses are LHS account, the rest are RHS accounts.

The balance of an account's ledger is calculated based on all of its debits and credits. For example, for a Sales Revenue account:

Date Description Debit Credit Balance
2025-11-4 Opening Balance     0
2025-11-5 Cash Sale   100 100
2025-11-5 Cash Sale   50 150
2025-11-6 Return 20   130
2025-11-7 Cash Sale   200 330
--------- --------------- ----- ------ -------
Total   20 350 330

On the other hand, the Cost of Goods Sold (COGS) account for these transactions will look like this:

Date Description Debit Credit Balance
2025-11-4 Opening Balance     0
2025-11-5 Cash Sale 60   60
2025-11-5 Cash Sale 20   80
2025-11-6 Return   10 70
2025-11-7 Cash Sale 150   220
--------- --------------- ----- ------ -------
Total   230 10 220

Notice that the value of the balance for the Sales (income/revenue) account increases with credits, but for the COGS (an asset account) the balance increases with debits.

For LHS accounts, credits increase the balance, debits decrease the balance. For RHS accounts, debits increase the balance, credits decrease the balance.

Thus,

  • For LHS accounts, (= balance (- debits credits)).
  • For RHS accounts, (= balance (- credits debits)).

The balance of a transaction is calculated from the debits and credits on two or more accounts in the transaction. A transaction consists of a least one debit and one credit to two separate accounts.

Let's say we are a business that wants to record a cash sale of physical merchandise. A sale will affect that following accounts:

  1. Inventory (asset)
  2. Cash (asset)
  3. Sales Revenue (income)
  4. Cost of Goods Sold (COGS, expense)

Typically, transactions are visualized as a table with a debit and credit column:

Account Debit Credit
Cash 100  
Sales Revenue   100
COGS 60  
Inventory   60
------------- ----- ------
Totals 160 160

So in the table above, cash is debited (increasing in value), sales revenue (an income account) is credited (increasing in value). You now have recorded an increase in your cash-on-hand and your sales.

The Cash of Goods Sold account is debited (increasing in value), and your inventory is credited (decreasing in value). You have paid down the expense you had when purchasing an inventory good and the size or value of your inventory has decreased. By subtracting COGS from Sales Revenue, you can also know how much money you made from the transaction.

And the debit and credit totals match on both sides.

Ledgers are ways of viewing the data your system records. For example, a general ledger will show raw data: what transactions occurred. A sales ledger will show sales. An account ledger will show the transactions for an account and its current balance. An accounts payable aging report will show how much money you owe, to whom, and how overdue you might be on a certain account. All of these different ledgers provide insight into your accounting: cash flow, sales/returns, etc.

Requirements

Given the above, we need to provide the following functionality in our library:

  1. Account Creation
  2. Balanced Transaction Creation
  3. Account Balance Retrieval
  4. Chart of Accounts Balance Retrieval

The library should also provide the ability to easily create ledgers from the data it provides.

Testing & Examples

This library's functionality is going to be trickier than the almighty-money library's was, so we're going to need to create tests. For that, we'll be using lisp-unit2. We will do some basic unit tests, and we'll make some example ledgers to demonstrate the library's use in an accounting system.

Dependencies

We will be using the almighty-money library we created along with some other dependencies.

Since we are now adding dependencies to our project, I will introduce you to vend, a library that replaces the defacto-standard quicklisp library for dependency management. Let me explain why.

Vendor Your Dependencies

The guiding principle of the Almighty Lisp developer is to, above all else, prioritize simplicity. Simplicity is the key to writing code that you and others can maintain without fear or frustration.

When it comes to dependencies, simplicity looks like this:

  1. Generally avoid adding dependencies to a project unless necessary. If you can write a simple thing yourself that gets the job done, you probably should.
  2. When adding dependencies to your project, adopt them as your code. You are now responsible for their bugs, features, efficiency, etc.
  3. When possible, remove dependencies. Usually this means slowly extracting functionality from a dependency and rewriting it customized for your needs.

Some people feel that the trend to make dependencies easier to add to a project via tools like npm or uv are detrimental to codebase quality and are a security liability. If adding and maintaining dependencies with such tools is easy, you will be tempted to add and keep large amounts of third-party code in your codebase. Adding a dependency is and should feel like a serious, risky decision.

While I can see the benefits of that mindset, the Almighty Lisp philosophy is one that is designed to maximize the effectiveness of the individual programmer. If you have a team of developers, you can expect to be able to work together to write the exact code that you need, both your library code and application code. Individual programmers need to prioritize writing application code over library code, which means individuals need to rely more heavily on dependencies.

If your brain is so big you can crank out the whole web stack by yourself while producing high-quality code, you can ignore the above. For the rest of us, we need a simple way of introducing dependencies into our projects that provide us the means to easily transition dependencies into code fit exactly to our purpose, free from the potential of introduced security vulnerabilities.

vend is a library that does precisely that by making it simple to vendor our dependencies. Vend works by looking at our defsystem definitions, looks at what other systems our system :depends-on, and cloning the requisite systems from github into a directory in our project folder–defaulting to vendored.

If for some reason you don't want to use vend, you can feel free to use quicklisp instead.

Installing vend

Installing vend is simple, but it does have one unusual requirement: it requires ECL, a different implementation of Common Lisp (we installed SBCL at the very beginning, before even installing Emacs). So first, we need to install ECL.

If you're on Linux, you can try looking for a package for your distribution.

On MacOS, ECL is available on Homebrew.

Otherwise, you can follow the instructions below to compile ECL from the source. These come directly from the manual.

Download ECL

Download the latest release of ECL from their server.

Extract the source code and enter its directory

$ tar -xf ecl-xx.x.x.tgz
$ cd ecl-xx.x.x

Run the configuration file, build the program and install it

$ ./configure --prefix=/usr/local
$ make                          # -jX if you have X cores
$ make install

Make sure the program is installed and ready to run

$ /usr/local/bin/ecl

ECL (Embeddable Common-Lisp) 16.0.0
Copyright (C) 1984 Taiichi Yuasa and Masami Hagiya
Copyright (C) 1993 Giuseppe Attardi
Copyright (C) 2000 Juan J. Garcia-Ripoll
Copyright (C) 2015 Daniel Kochmanski
ECL is free software, and you are welcome to redistribute it
under certain conditions; see file 'Copyright' for details.
Type :h for Help.
Top level in: #<process TOP-LEVEL>.
>

Clone vend git repo

Now that you have ECL installed, you can install vend.

Clone the vend git repository from Github.

Run make

After cloning the repo, navigate into the vend directory and run the following commands:

make
make install

After that, enter vend in the command line (you may need to restart the terminal) and you should see this:

vend - Vendor your Common Lisp dependencies

Commands:
  check  [focus] - Check your dependencies for issues
  get            - Download all project dependencies into 'vendored/'
  graph  [focus] - Visualise a graph of transitive project dependencies
  init   [name]  - Create a minimal project skeleton
  repl   [args]  - Start a Lisp session with only your vendored ASDF systems
  search [term]  - Search known systems
  test   [args]  - Run all detected test systems

Flags:
  --help    - Display this help message
  --version - Display the current version of vend

Emacs configuration

You need to configure Emacs to open the REPL via vend. To open your config.el file, type C-h d c or SPC f P (doom/open-private-config) and open config.el from the minibuffer. In your config.el file, add:

(setq sly-default-lisp 'sbcl
      sly-lisp-implementations '((sbcl  ("vend" "repl" "sbcl")  :coding-system
 utf-8-unix)
                                 (ecl   ("vend" "repl" "ecl")   :coding-system
 utf-8-unix)
                                 (abcl  ("vend" "repl" "abcl")  :coding-system
 utf-8-unix)
                                 (clasp ("vend" "repl" "clasp") :coding-system
 utf-8-unix)))
Can't find vend?

I had this error while testing on a fresh install of Ubuntu:

File local-variables error: (doom-hook-error lisp-mode-local-vars-hook sly-editing-mode (file-missing Searching for program No such file or directory vend))

That means that Emacs can't find the vend executable. To resolve the error, I installed the package exec-path-from-shell.

To install it, type SPC f P and open config.el. Add this to your config.el file:

(use-package! exec-path-from-shell
  :when (or (memq window-system '(mac ns x))
            (daemonp))
  :config
  (exec-path-from-shell-initialize))

And add this to your packages.el file (in the same folder as the config.el):

(package! exec-path-from-shell)

Run doom sync in the terminal and restart doom with SPC q r (doom/restart-and-restore).

Strangely, after I did this and then undid the changes, vend continued to work. We all love software that just works, don't we folks?

Project Setup

Our project is called almighty-kaikei, so find-file (C-x C-f or SPC f f) almighty-kaikei/almighty-kaikei.asd and save it. We're going to need tests later, so just make a t directory, too.

Now that we have vend installed, we can get the rest of our dependencies. First, we create a directory called almighty-kaikei and define our system in almighty-kaikei.asd.

(defsystem "almighty-kaikei"
  :author "Micah Killian"
  :version "0.0.1"
  :description "Almighty Double-Entry Accounting Program"
  :depends-on (#:almighty-money #:local-time #:mito #:sxql #:dbi)
  :components ((:file "src/main")))

Make sure to save the file.

Now it's time to run vend. In the terminal, run this command:

% vend get

You will get an error saying that almighty-money isn't a known system. Not surprising–it's on our computer!

To get around this, first we make the vendored directory ourselves and make another directory inside it called almighty-money.

% mkdir vendored
% cd vendored
% mkdir almighty-money

Now we can make a symbolic link from where we saved almighty-money and the directory we just made.

% ln -s ~/path/to/the/original/almighty-money almighty-money

If you did everything right, you can run vend get in the almighty-kaikei directory. It will find the almighty-money.asd file and won't attempt to search for or download it. Then it will download the rest of the dependencies as expected.

Now that we have our system setup, let's create the main.lisp file in the project root and define the package.

(defpackage #:almighty-kaikei
  (:use #:cl)
  (:nicknames #:almighty-kaikei/main #:ak)
  (:local-nicknames (#:lt #:local-time)
                    (#:m #:mito)
                    (#:s #:sxql)
                    (#:d #:dbi)
                    (#:am #:almighty-money)))

Now's a good time to learn a bit about our dependencies.

Introducing Our Dependencies

local-time is the defacto standard library for working with time, similar to Python's datetime library.

mito is a SQL object-relational mapper. It's not such an opinionated ORM–we can rely on simple SQL queries for the most part when using it. It does the work of creating database tables and serializing between Lisp and SQL data.

sxql is a domain specific language for writing SQL in Lisp. I happen to really enjoy doing everything in Lisp (in a later project we will write HTML with the hsx library, another DSL). Putting everything I'm doing right in one spot, rather than spreading it across several files, just feels right to me.

If you're not such an enthusiast of this style, you can check out the cl-yesql library. It is a library similar to Clojure's Yesql library. It allows you to use SQL from Lisp, rather than write it as with sxql.

dbi is a library for interfacing with databases. It provides functionality like SQL transactions, preparing and executing queries, etc. We need this to connect to our database and one query.

Why SQL?

You might be wondering why we're using SQL instead of straight Lisp. The answer is that we want to be able to use this library as is in a production environment where we expect to be using a SQL database.

Accounts

The first thing we need to do is provide a way of designating and knowing whether an account is a left-hand-side account or a right-hand-side account.

Recall that there are five basic categories of account: asset, expense, income, liability, and equity. Recall also that debits and credits influence the value of LHS and RHS differently and use a different formula for calculating their balance. So when we make accounts, we are going to assign it a certain category and we'll need to know which side it is in order to calculate its balance.

(defparameter *left-hand-side* '(("asset" -1) ("expense" -1)))
(defparameter *right-hand-side* '(("income" 1) ("liability" 1) ("equity" 1)))
(defparameter *account-types* (append *left-hand-side* *right-hand-side*))

(defun valid-account-category-p (account-category)
    "A predicate that takes an account category string. Returns a list of the
category and its side, or nil."
  (assoc account-category *account-types* :test #'equal))

Just a reminder: the :test #'equal parameter in the call to assoc is necessary because strings are arrays of characters that are instantiated separately. Since the default for :test is #'eql, member usually tests if two things are in identical places in memory. equal, on the other hand, will compare the structure of its inputs.

(eql "hello" "hello") ;; two separately instatiated arrays
                                        ; => NIL
(let ((str "hello"))
  (eql str str)) ;; same variable, same place in memory
                                        ; => T
(equal "hello" "hello")
                                        ; => T

Next, we define the account table with Mito:

(m:deftable account ()
  ((name :col-type :text)
   (code :col-type :text)
   (category :col-type :text)
   (currency :col-type :text))
  (:table-name "almighty_account")
  (:documentation "A mito macro that defines a class and SQL table for
 double-entry accounting accounts."))

(defmethod print-object ((this account) stream)
  (print-unreadable-object (this stream :type t :identity t)
    (with-slots (name code) this
      (format stream "~a ~a" name code))))

(defun side (account)
  "A function for getting the multiplier used for determining whether a
debit/credit increases or decreases the value of an account."
  (second (assoc (account-category account) *account-types* :test #'equal)))

Mito's deftable macro expand into a defclass with a special :meta-class. The slots of the class will all have accessors prefixed with the name of the table. In this case, the accessor for the name slot is account-name. This is similar to how structures work.

The :col-type slot option is for setting the SQL column type. In our case, we are going to use SQLite, which only really has text and integer types. If we were using PostgreSQL, we'd need to be more specific with our :col-types.

By default Mito will set the SQL table name to the name of the Lisp class (account here), but we can manually set it with the :table-name class option. Here we are namespacing it with the almighty_ prefix.

We also define a print-object method for the account class to make it easier to understand when we print account objects to the REPL.

side will be important for when we enter and retrieve data from the database: it will determine whether the amount entered or returned is negative or positive.

We can manually construct account objects with make-instance, but we'll make a custom constructor for convenience.

(defun make-account (name code category &optional (currency
 am:*default-currency*))
  "A constructor for ACCOUNT objects. CURRENCY defaults to
ALMIGHTY-MONEY:*DEFAULT-CURRENCY*."
  (unless (valid-account-category-p category)
    (error "~a is not a valid account category." category))
  (make-instance 'account :name name :code code :category category :currency
 currency))

That also allows us to validate the category.

Because we need to store and retrieve accounts to and from SQL tables, we need some functions for doing that for accounts.

(defun save-account (account)
  "A function for saving ACCOUNT rows to a database. Returns an error if the
account already exists."
  (let* ((code (account-code account))
         (account-exists (m:select-dao 'account (s:where (:= :code (account-code
 account))))))
    (when account-exists
      (error "An account with code ~a already exists." code))
    (m:insert-dao account)))

(defun get-or-create-account (account)
  "A function that takes an ACCOUNT object and searches your database for an
ACCOUNT with the same CODE. If one exists, it's returned. Eitherwise, it's
created and returned."
  (let ((account-exists (m:select-dao 'account (s:where (:= :code (account-code
 account))))))
    (if account-exists
        (first account-exists)
        (m:insert-dao account))))

(defun get-account (account-code)
  "A function that takes an ACCOUNT CODE and returns an ACCOUNT object. Returns
 an error if an ACCOUNT with that CODE doesn't exist."
  (let ((account (first (m:select-dao 'account
                          (s:where (:= :code account-code))))))
    (unless account
      (error "An account with code ~a doesn't exist." account-code))
    account))

(m:define-accessor account-transactions (this account)
  (m:select-dao 'transaction
    (s:where (:= :account_id (object-id this)))))

save-account is self-explanatory: We don't want duplicate accounts, so we prevent that with save-account by first doing a lookup for the account-code of the account we're trying to create.

get-or-create-account is useful for times where you want to ensure an account is saved in the database without causing an error if it already exists.

get-account makes it easy to get an account from a simple string containing an account code.

define-accessor is a mito macro useful for reducing database hits for things like one-to-many and many-to-many relationship queries. In this case, we have a one-to-many query: getting all the transactions for an account object we pass to it.

For all of these, we used mito and sxql to query our database. Mito provides select-dao, a macro creating a list of Lisp objects containing the queried data. The first argument is a symbol–the name of a table defined with mito:deftable. It acts as the "SELECT * FROM [table-name]" part of a SQL query. You then provide the rest of the query using sxql.

You'll notice that we refer to the transaction table in account-transactions. We'll define that next.

Transactions

When a transaction is made, such as a sale, a return, a payment, etc. we need to create different parts (what we'll call Legs)–debits and credits–that we need a way to know which parts go together. Transactions timestamps and ids are doing to be how we refer to groups of Legs.

Transactions are simple to implement.

(m:deftable transaction ()
  ((date :col-type :timestamp
         :inflate #'lt:universal-to-timestamp
         :deflate #'lt:timestamp-to-universal))
  (:table-name "almighty_transaction"))

(defmethod print-object ((this transaction) stream)
  (print-unreadable-object (this stream :type t :identity t)
    (with-slots (date) this
      (format stream "~a" date))))

(defun make-transaction (&optional (date (lt:now)))
  (make-instance 'transaction :date date))

Mito provides the ability to set how a slot "inflates" and "deflates"–serializes into Lisp or SQL values. We are using local-time to convert between universal times and local-time timestamps. Universal times are a simple integer–the number of seconds after 2000-3-1. While it's not strictly necessary to serialize to universal times before saving to the database, it will make future queries easier.

make-transaction sets the default value of the date slot to local-time:now.

Legs

The real fun begins with legs. Legs are like entries in the accounting books: This account was debited this amount at this time. A Transaction has to have at least two Legs–a debit and a credit–to be balanced. The whole purposes of a double-entry accounting system is to record at least two sides of a transaction to reduce errors in money tracking. If a transaction doesn't balance, that's a sign that there is an error in money tracking.

So first, let's setup our leg table.

(m:deftable leg ()
  ((account :col-type account)
   (side :col-type :integer)
   ;; Use LEG-ENTRY to access ENTRY-AMOUNT and ENTRY-CURRENCY as an
   ;; ALMIGHTY-MONEY:MONEY object.
   (entry-amount :col-type :integer)    ; ALMIGHTY-MONEY:MONEY object AMOUNT.
   (entry-currency :col-type :text) ; ALMIGHTY-MONEY:MONEY object CURRENCY-CODE.
   (transaction :col-type (or transaction :null)))
  (:table-name "almighty_leg"))

(defmethod print-object ((this leg) stream)
  (print-unreadable-object (this stream :type t :identity t)
    (let ((account (when (slot-boundp this 'account) (slot-value this 'account)))
          (transaction (when (slot-boundp this 'transaction) (slot-value this
 'transaction)))) 
      (format stream "~a ~[DEBIT~;CREDIT~] ~a" account side (leg-entry this)))))

Mito allows us to assign :col-type to other tables, creating a relationship between different tables. The SQL table will actually have account_id and transaction_id columns corresponding to the ids of the other table rows.

side records whether a leg is a debit or a credit. Debits will be represented with 0, and credits with 1. This makes the format string in print-object easier: we use tilde brackets ~[...;...~] to do conditional printing. If the value of slot is 0, then the first string (zero-indexed) will be printed. If it's 1, then the string after the ~; will be printed. You can do this with an arbitrary number of integer values.

(format nil "~[HELLO~;GOODBYE~;KONNICHIWA~;SAYONARA~]" 0)
                                        ; => "HELLO"
(format nil "~[HELLO~;GOODBYE~;KONNICHIWA~;SAYONARA~]" 3)
                                        ; => "SAYONARA"

entry-amount and entry-currency will record the amount of a certain currency of the leg. Ideally, we would be able to define some kind of custom :col-type for an almighty-money:money object and name it entry, but mito doesn't provide the ability to create custom column types on the Lisp side, so we will simulate it by creating an "accessor" for entries:

(defgeneric leg-entry (leg)
  (:documentation "A function for getting the ENTRY-AMOUNT and ENTRY-CURRENCY of
 a LEG object and returning a MONEY object.")
  (:method ((this leg))
    (am:make-money (leg-entry-amount this) (leg-entry-currency this))))

Since all class slot accessors are generic functions, I am implementing leg-entry as a generic, too.

Now we need a constructor.

(defun make-leg (account side entry transaction)
  "A constructor for making LEG objects. ACCOUNT can either be an ACCOUNT object
 or an ACCOUNT-CODE. ENTRY must receive an ALMIGHTY-MONEY:MONEY object.

Usage example:
    (make-leg \"CODE000\" (usd 5000))"
  (let ((account (etypecase account
                   (account account)
                   (string (get-account account)))))
    (unless (am:moneyp entry)
      (error 'type-error :expected 'am:money :datum entry))
    (make-instance 'leg :account account
                        :side side
                        :entry-amount (am:amount entry)
                        :entry-currency (am:currency-code entry)
                        :transaction transaction)))

We use etypecase to convert from one a string account-code to an account object if necessary. We want almighty-money:money objects passed as the entry, so we signal a type-error condition if we don't get a money object in the entry position.

Usually we don't want to use make-leg directly. Instead, we'll provide some helper functions for making debits and credits.

(defparameter *debit* -1)
(defparameter *credit* 1)

(defun debit (account amount)
  "A function for constructing a LEG object with a side of *DEBIT*. The
 TRANSACTION slot will be modified later before saving the row to the database.

Usage examples:
    (debit \"CASH000\" (usd 5000))
    (debit \"COGS000\" 10000)
"
  (let ((account (etypecase account
                   (account account)
                   (string (get-account account))))
        (amount (etypecase amount
                  (integer (am:make-money amount))
                  (am:money amount))))
    (make-leg account *debit* (am:money* amount (side account) *debit*) nil)))

(defun credit (account amount)
  "A function for constructing a LEG object with a side of *CREDIT*. The
 TRANSACTION slot will be modified later before saving the row to the database.

Usage example:
    (credit \"REV000\" (usd 5000))
    (credit \"SLS000\" 10000)
"
  (let ((account (etypecase account
                   (account account)
                   (string (get-account account))))
        (amount (etypecase amount
                  (integer (am:make-money amount))
                  (am:money amount))))
    (make-leg account *credit* (am:money* amount (side account) *credit*) nil)))

The amount can be either a almighty-money:money object, or a simple integer. If it's an integer, then we convert it to a money object using the almighty-money:*default-currency*. Unless you're dealing with multiple currencies, you can simply set the *default-currency* for your application and save yourself some typing.

Remember that almighty-money only supports USD and JPY at the moment. If you want to use some other currency in almighty-kaikei, you need to add support to almighty-money.

The important bit is the fact that we take the amount and multiply it by the side of the account and by *debit* or *credit*. An asset account that is credited should save a negative amount into the database–crediting a left-hand-side account should decrease its value. On the other hand, crediting a right-hand-side account should increase its value.

It might be clarifying to look at the math this way:

(let ((lhs -1)
      (rhs 1)
      (debit -1)
      (credit 1)
      (amount 7))
  (list (* amount lhs debit)
        (* amount lhs credit)
        (* amount rhs debit)
        (* amount rhs credit)))
                                        ; => (7 -7 -7 7)

We're getting close to being able to make a transaction. Before we get there, we need to be able to tell debit legs from credit legs so we can see if the two sides of the transaction are balanced.

(defun debit-p (leg)
  (= (leg-side leg) *debit*))

(defun credit-p (leg)
  (= (leg-side leg) *credit*))

(defun sides-balanced-p (legs)
  (let ((debits (sum-all-if #'debit-p legs))
        (credits (sum-all-if #'credit-p legs)))
    (= (abs (am:amount debits)) (abs (am:amount credits)))))

Note that we are looking at the absolute value of the debit and credit amounts. As mentioned before, crediting a left-hand-side account will result in a negative value being passed to make-leg, the same is if you debited a right-hand-side account like an income account. The effect is that we always are going to have a positive and a negative value in our transactions. Both should have the same absolute value, though.

Finally, we make the transact! function.

(define-condition unbalanced-transaction (error)
  ((debit-amount :initarg :debit-amount :reader debit-amount)
   (credit-amount :initarg :credit-amount :reader credit-amount)
   (message :initarg :message :reader message
            :initform "Left and right entries of the transaction do not balance."))
  (:report (lambda (condition stream)
             (with-slots (debit-amount credit-amount message) condition
               (format stream "~a LEFT: ~a | RIGHT: ~a" message debit-amount
 credit-amount)))))

(defun record-transaction (transaction legs)
    "A function for saving the data rows of a transaction to a SQL database:
 Legs for the transaction, and the Transaction itself."
  (let ((transaction (m:insert-dao transaction)))
    (loop :for leg :in legs
          :do (m:insert-dao leg))
    transaction))

(defun transact! (&rest legs)
  "A function for making a double-entry accounting transaction having at least
 one debit and one credit leg.

If there is a LOCAL-TIME:TIMESTAMP in LEGS, the first one is used for the
transaction. If no LOCAL-TIME:TIMESTAMP is in LEGS, uses LOCAL-TIME:NOW for the
transaction.

Returns an UNBALANCED-TRANSACTION error if debits /= credits.

Usage Examples:

    (transact! (debit \"CASH000\" 5000)
               (credit \"REV000\" 5000))
    (transact! (local-time:parse-timestring \"2025-12-25\")
               (debit \"CASH000\" 5000)
               (credit \"REV000\" 5000))
"
  (let ((date (or (find-if (lambda (x) (typep x 'lt:timestamp)) legs) (lt:now)))
        (legs (remove-if-not (lambda (x) (typep x 'leg)) legs)))
    (unless (> (length legs) 1)
      (error "transactions must have at least two legs."))
    (unless (and (find-if #'debit-p legs)
                 (find-if #'credit-p legs))
      (error "transactions must have at least one debit and one credit leg."))
    (let ((transaction (make-transaction date))
          (debits (sum-all-if #'debit-p legs))
          (credits (sum-all-if #'credit-p legs)))
      (unless (sides-balanced-p legs)
        (error 'unbalanced-transaction
               :debit-amount (am:amount debits)
               :credit-amount (am:amount credits)))
      (loop :for leg :in legs
            :do (setf (leg-transaction leg) transaction))
      (record-transaction transaction legs))))

If we look at transact! first, it can take an arbitrary number of legs. Since legs is a &rest parameter, legs will be accessible in a list. First, we check to see if any of the arguments are a local-time:timestamp. If we find one, we will use that for the transaction.

After that, we're going to ensure first that we have at least two legs. We also ensure that there is at least one debit and one credit in there. If the sides are all balanced, we add a transaction to the transaction slot of all the legs and save the transaction to the database.

If the sides are unbalanced, we signal an unbalanced-transaction error. Creating and signaling a condition will make it easy to take specific actions for that kind of error (in a web application, we'd like to send the error to the user in the browser, for example).

Playing around

Before we continue, how about we try playing with the code. In order to actually create any data, first we need to connect to our database and create tables. For right now, let's just keep things simple by establishing a long-lasting connection at toplevel.

(mito:connect-toplevel :sqlite3 :database-name "t/test.db")

If you forgot to make a t directory for tests during project setup, you should do that before running the above code.

Later, you can disconnect using disconnect-toplevel

(mito:disconnect-toplevel)

After establishing a connection, we need to create the tables. Mito provides a useful function for this purpose:

(m:ensure-table-exists 'account)
(m:ensure-table-exists 'transaction)
(m:ensure-table-exists 'leg)

ensure-table-exists will look to see if a table exists. If it doesn't, it creates it. Otherwise, it returns nil.

Once you have your tables, you need some accounts to mess with.

(save-account (make-account "Cash" "CSH000" "asset"))
(save-account (make-account "Sales Revenue" "REV000" "income"))

We'll just make these for now.

Let's make a sale: customer pays cash for something.

(transact! (debit "CSH000" (usd 5000))
           (credit "REV000" (usd 5000)))
                                        ; => #<TRANSACTION
                                        ; 2025-11-26T04:41:56.367255Z
                                        ; {700805E313}>

We can look at the contents of the database using Emacs' built-in sqlite-mode for looking at SQLite databases. Type M-x sqlite-mode-open-file and open t/test.db. You should see the names of the tables we made and the number of rows in each table.

Table Name Number of Rows
almightyaccount 2
almightyleg 2
almightytransaction 1

If you switch from Evil's Normal-mode to Emacs-mode, a mode that acts exactly like if you weren't using Evil at all, then you can even press Enter on a table name and see the rows and their data.

Account Balance

So we've confirmed that we're saving data to the database, but now we need to know something about the accounts: What are their current balances? For that, we need write another function.

(defun account-legs (account &optional (as-of (universal-now)))
  "A function for getting all the legs of an account as of a certain time,
defaulting to now.

Usage Examples:
    (account-legs \"CSH000\")
    (account-legs sales-account-object transaction-object)
    (account-legs cogs-account-object (local-time:parse-timestring
    \"2025-11-27\"))
"
  (let* ((account (etypecase account
                    (account account)
                    (string (get-account account))))
         (as-of (etypecase as-of
                  (lt:timestamp (lt:timestamp-to-universal as-of))
                  (transaction (lt:timestamp-to-universal (transaction-date
 as-of)))
                  (string (lt:timestamp-to-universal (lt:parse-timestring as-of)))
                  (integer as-of))))
    (m:select-by-sql 'leg
                     (s:select :*
                       (s:from (:as :almighty_leg :l))
                       (s:inner-join (:as :almighty_transaction :t) :on (:=
 :l.transaction_id :t.id))
                       (s:where (:and (:= :l.account_id (m:object-id account))
                                      (:<= :t.date as-of)))))))

(defun account-balance (account &optional (as-of (universal-now)))
  "A function for getting the balance of an account at a certain point in time,
defaulting to now.

Usage Example:
    (account-balance \"CSH000\")
    (account-balance sales-account-object)
"
  (let* ((account (if (stringp account)
                      (get-account account)
                      account))
         (legs (account-legs account as-of)))
    (if legs
        (am:money* (reduce #'am:money+ (mapcar #'leg-entry legs)) (side account))
        (am:make-money 0 (account-currency account)))))

account-legs gets all the legs for an account, filtered by the date of the transaction. account-balance sums up the debit and credit legs of an account then subtracts debits from credits or credits from debits depending on the type of account.

Long-term problems

Everything is chugging along quite nicely, but there's a problem. If you're familiar with database query optimization, you probably already wrote a nasty post about me on X when we made the account-balance function.

"You absolute clown, you're getting all of the leg rows for a certain account, then passing them to Lisp to do the summation of their debits and credits? Delete this book."

This is a reasonable position to have. With very few leg rows, this isn't really such a big deal. However, as the number of leg rows grows, the slower the operation will become. We can confirm that by making some mock data and then running account-balance on it.

;; Create lots of mock data, saving to disk. If you already had some test data
;; of your own, you can delete the .db file before running this.
;; 
;; This will take some time. Emacs will freeze while this process runs.
(with-coa
  (let ((cash (ak:get-account "CSH000"))
        (sale (ak:get-account "SLS000"))
        (date (lt:now))
        (times 100000)
        (amount 500))
    (dotimes (i times)
      (ak:transact! (ak:debit cash (am:usd amount))
                    (ak:credit sale (am:usd amount))
                    date))))
;; Run ACCOUNT-BALANCE on mock data
(with-coa (ak:account-balance "CSH000"))

If you run account-balance now, there will be a noticable lag between executing and receiving the return value.

Let's do some basic profiling:

(time (with-coa (ak:account-balance "CSH000")))

time is a function for basic, dirty profiling. It will show how long it takes to complete a task along with some other information. If you run it in the REPL, you should get a result that looks something like this:

Evaluation took:
  2.944 seconds of real time
  2.950970 seconds of total run time (2.826497 user, 0.124473 system)
  [ Real times consist of 0.256 seconds GC time, and 2.688 seconds non-GC time. ]
  [ Run times consist of 0.254 seconds GC time, and 2.697 seconds non-GC time. ]
  100.24% CPU
  7 forms interpreted
  1,680,876,464 bytes consed

Well, what happens if we add a check for the account balance of the sales account?

(time (with-coa
        (ak:account-balance-slow "CSH000")
        (ak:account-balance-slow "SLS000")))

The result:

Evaluation took:
  5.879 seconds of real time
  5.890071 seconds of total run time (5.630371 user, 0.259700 system)
  [ Real times consist of 0.511 seconds GC time, and 5.368 seconds non-GC time. ]
  [ Run times consist of 0.509 seconds GC time, and 5.382 seconds non-GC time. ]
  100.19% CPU
  7 forms interpreted
  3,361,540,272 bytes consed

As suspected, account-balance is going to be a problem in the long-term. We need to optimize.

Optimization

We could try getting back less data in our account-legs function just as an experiment.

(defun account-legs (account &optional (as-of (universal-now)))
  "A function for getting all the legs of an account as of a certain time,
defaulting to now.

Usage Examples:
(account-legs \"CSH000\")
(account-legs sales-account-object transaction-object)
(account-legs cogs-account-object (local-time:parse-timestring \"2025-11-27\"))"

  (let* ((account (etypecase account
                    (account account)
                    (string (get-account account))))
         (as-of (etypecase as-of
                  (lt:timestamp (lt:timestamp-to-universal as-of))
                  (transaction (lt:timestamp-to-universal (transaction-date as-of)))
                  (string (lt:timestamp-to-universal (lt:parse-timestring as-of)))
                  (integer as-of))))
    (m:select-by-sql 'leg
                     ;; Only returning amount, currency, and side
                     (s:select (:entry_amount :entry_currency :side)
                       (s:from (:as :almighty_leg :l))
                       (s:inner-join (:as :almighty_transaction :t)
                                     :on (:= :l.transaction_id :t.id))
                       (s:where (:and
                                 (:= :l.account_id (m:object-id account))
                                 (:<= :t.date as-of))))))

If we run our little test again (running account-balance twice) we get:

Evaluation took:
  1.296 seconds of real time
  1.300803 seconds of total run time (1.235469 user, 0.065334 system)
  [ Real times consist of 0.112 seconds GC time, and 1.184 seconds non-GC time. ]
  [ Run times consist of 0.112 seconds GC time, and 1.189 seconds non-GC time. ]
  100.39% CPU
  7 forms interpreted
  782,634,128 bytes consed

This confirms that we are definitely being bogged down by all this object construction and deconstruction (notice the dramatic decrease in bytes consed), but focusing what data we get from the database isn't remotely enough to fix our problem.

This is a common problem when using ORMs: if we try to do serious calculations by first serializing all of the data into our backend, we will inevitably see performance drops.

What we really want is for our database to do all the work of marshaling the data and summing up the amount column of all of the leg rows, and just receive a single value–the balance! That way we aren't building up and breaking down a bunch of Lisp objects just to do a little bit of math.

Making a Query With cl-dbi

This is where cl-dbi will be necessary. It will allow us to execute a query manually.

dbi works by first preparing a query for a certain database connection.

(dbi:prepare database-connection query)

The query is a simple string. You can yield one from a SQL statement prepared with sxql.

(sxql:select :*
  (sxql:from :almighty_account)
  (sxql:where (:= :almighty_account.id 5)))
                                        ; => #<SXQL-STATEMENT: SELECT * FROM
                                        ; almighty_account WHERE
                                        ; (almighty_account.id = 5)>


(sxql:yield
 (sxql:select :*
   (sxql:from :almighty_account)
   (sxql:where (:= :almighty_account.id 5))))
                                        ; => "SELECT * FROM almighty_account
                                        ; WHERE (almighty_account.id = ?)", (5)

Notice that we receive two values from yield. This is to make it easy to use dbi to prepare the statement–sanitizing user input to prevent SQL inject attacks.

(with-chart-of-accounts
  (multiple-value-bind (query binds)
      (sxql:yield
       (sxql:select :*
         (sxql:from :almighty_account)
         (sxql:where (:= :almighty_account.id 5))))
    (dbi:prepare mito:*connection* query)))
                                        ; => #<DBD.SQLITE3:DBD-SQLITE3-QUERY
                                        ; {70078DE2C3}>

We pass the prepared query and binds to execute to get a result.

(with-chart-of-accounts
  (multiple-value-bind (query binds)
      (sxql:yield
       (sxql:select :*
         (sxql:from :almighty_account)
         (sxql:where (:= :almighty_account.id 5))))
    (dbi:execute (dbi:prepare mito:*connection* query)
                 binds)))
                                        ; => #<DBD.SQLITE3:DBD-SQLITE3-QUERY
                                        ; {70081DDED3}>

We have two options: we can take one row of the result at a time and do something with it, or we can receive the whole thing all at once and then do something with all of the data.

(with-chart-of-accounts
  (multiple-value-bind (query binds)
      (sxql:yield
       (sxql:select :*
         (sxql:from :almighty_account)
         (sxql:where (:= :almighty_account.id 5))))
    (dbi:fetch (dbi:execute (dbi:prepare mito:*connection* query)
                            binds))))
                                        ; => (:|id| 5 :|name| "General Expenses"
                                        ; :|code| "GEN000" :|category| "expense"
                                        ; :|currency| "USD" :|created_at|
                                        ; "2025-11-30 03:59:02.662347Z"
                                        ; :|updated_at| "2025-11-30
                                        ; 03:59:02.662347Z")

(with-chart-of-accounts
  (multiple-value-bind (query binds)
      (sxql:yield
       (sxql:select :*
         (sxql:from :almighty_account)
         (sxql:where (:= :almighty_account.id 5))))
    (dbi:fetch-all (dbi:execute (dbi:prepare mito:*connection* query)
                                binds))))
                                        ; => ((:|id| 5 :|name| "General
                                        ;  Expenses" :|code| "GEN000"
                                        ;  :|category| "expense" :|currency|
                                        ;  "USD" :|created_at| "2025-11-30
                                        ;  03:59:02.662347Z" :|updated_at|
                                        ;  "2025-11-30 03:59:02.662347Z"))
                                        ;  notice the extra parentheses

And that's the basics of using dbi. multiple-value-bind is the go-to macro for working with forms that return multiple values like sxql:yield.

Rename to account-balance

Now that you know basically how we're going to use dbi to get our data, we can get started on the task at hand.

First, rename the account-balance function to account-balance-slow. We'll use it to test later.

+ (defun account-balance-slow (account &optional (as-of (universal-now))) +
....)

Don't forget to export it with sly-export-symbol-at-point (C-c x with the cursor on function name).

Writing the query

Now we need to get to the SQL query. What we want to do is get all of the legs for a certain account (as we do with account-legs) and then sum up the entry_amount columns of all those legs. In the end, we need an amount and a currency so we can properly make a money object later, so we're not going to (select :*), we're going to return a sum.

(with-coa
  (let ((account (ak:get-account "CSH000")))
    (multiple-value-bind (query binds)
        (s:yield
         (s:select ((:as (:sum :leg.entry_amount) :amount))
           (s:from (:as :almighty_account :account))
           (s:inner-join (:as :almighty_leg :leg) :on (:= :leg.account_id
 :account.id))
           (s:where (:= :account.id (m:object-id account)))))
      (d:fetch-all (d:execute (d:prepare m:*connection* query)
                              binds)))))
                                        ; => ((:|amount| 100000000))

That looks good, but let's just be sure with a smaller test.

(with-chart-of-accounts                 ; Using in-memory database
  ;; Use cash and revenue accounts.
  (let ((cash (ak:get-account "CSH000"))
        (revenue (ak:get-account "RMR000")))
    ;; Make transactions.
    (ak:transact! (ak:debit cash 5000)
                  (ak:credit revenue 5000))
    (ak:transact! (ak:debit cash 5000)
                  (ak:credit revenue 5000))
    (ak:transact! (ak:debit cash 5000)
                  (ak:credit revenue 5000))
    ;; Reverse the transactions.
    (ak:transact! (ak:credit cash 5000)
                  (ak:debit revenue 5000))
    (ak:transact! (ak:credit cash 5000)
                  (ak:debit revenue 5000))
    (ak:transact! (ak:credit cash 5000)
                  (ak:debit revenue 5000))
    (multiple-value-bind (query binds)
        (s:yield
         (s:select ((:as (:sum :leg.entry_amount) :amount))
           (s:from (:as :almighty_account :account))
           (s:inner-join (:as :almighty_leg :leg) :on (:= :leg.account_id
 :account.id))
           ;; Check cash balance.
           (s:where (:= :account.id (m:object-id cash)))))
      (d:fetch-all (d:execute (d:prepare m:*connection* query)
                              binds)))))
                                        ;  => ((:|amount| 0))

At this point, we just need to add a way to filter for the as-of transaction date, add the entry_currency to the return values, and turn this into a proper function.

(defun sql-account-balance (account as-of)
  (multiple-value-bind (query binds)
      (s:yield (s:select ((:as (:coalesce (:sum :leg.entry_amount) 0) :amount)
                          (:as :leg.entry_currency :currency))
                 (s:from (:as :almighty_account :account))
                 (s:inner-join (:as :almighty_leg :leg) :on (:= :leg.account_id
 :account.id))
                 (s:inner-join (:as :almighty_transaction :tran) :on (:=
 :leg.transaction_id :tran.id))
                 (s:where (:and (:= :account.id (m:object-id account))
                                (:<= :tran.date as-of)))
                 (s:group-by :leg.entry_currency)))
    (let ((result (d:fetch (d:execute (d:prepare m:*connection* query)
                                      binds))))
      (am:make-money (second result) (fourth result)))))

Then let's make a new account-balance function:

(defun account-balance (account &optional (as-of (universal-now)))
  "A function for getting the balance of an account at a certain point in time,
defaulting to now.

Usage Example:
    (account-balance \"CSH000\")
    (account-balance sales-account-object)
"
  (let ((account (if (stringp account)
                     (get-account account)
                     account))
        (as-of (etypecase as-of
                 (lt:timestamp (lt:timestamp-to-universal as-of))
                 (transaction (lt:timestamp-to-universal (transaction-date as-of)))
                 (string (lt:timestamp-to-universal (lt:parse-timestring as-of)))
                 (integer as-of))))
    (sql-account-balance account as-of)))

Now we can test it out against the slower version.

(time (with-coa (ak:account-balance-slow "CSH000")))

Results in:

Evaluation took:
  0.675 seconds of real time
  0.677828 seconds of total run time (0.638939 user, 0.038889 system)
  [ Real times consist of 0.060 seconds GC time, and 0.615 seconds non-GC time. ]
  [ Run times consist of 0.060 seconds GC time, and 0.618 seconds non-GC time. ]
  100.44% CPU
  7 forms interpreted
  391,349,920 bytes consed

Now for the optimized version:

(time (with-coa (ak:account-balance "CSH000")))

Results in:

Evaluation took:
  0.051 seconds of real time
  0.044906 seconds of total run time (0.033658 user, 0.011248 system)
  88.24% CPU
  7 forms interpreted
  262,080 bytes consed

Much better.

Example Usage

Now that we have the basic functionality of our library finished, it's time to kick the tires a bit and see what it can do. First, we'll make it a REPL-based application, but later we'll upgrade it into a CLI script in the chapter on DEPLOYING.

Setup

In the project root directory, make a new directory named example.

We need a system for this example, so let's set that up.

;; almighty-kaikei/almighty-kaikei-examples.asd
(defsystem "almighty-kaikei-examples"
  :author "Micah Killian"
  :version "0.0.1"
  :description "Examples of how to use the Almighty-Kaikei library."
  :depends-on (#:almighty-money #:almighty-kaikei #:local-time #:mito #:sxql
 #:dbi)
  :components ((:module "examples"
                :serial t
                :components ((:file "db")
                             (:file "views")))))

Before we work on our queries, it'll help to know the motivation for getting the data by looking at our views.

Views

First, let's get our package set up in examples/views.lisp.

(defpackage #:examples/views
  (:use #:cl)
  (:local-nicknames (#:db #:examples/db)
                    (#:lt #:local-time)
                    (#:ak #:almighty-kaikei)
                    (#:am #:almighty-money)))
(in-package #:examples/views)

In here, we're going to create some simple reports. The first one is a general ledger. The most basic kind of accounting report is the general ledger. It simply contains all of the transactions made, detailing the two sides and accounts affected.

;; * ░ VIEWS ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

(defun format-general-ledger (from to)
  (let ((transaction-count (db:transaction-count-for-period from to))
        (legs (db:legs-for-period from to)))
    (format t "~&Showing ~a transactions.~%" transaction-count)
    (format t "~a~30t~a~60t~a~%" "Transaction #" "Debits" "Credits")
    (loop :for leg :in legs
          :for i = 0
          :if (and (ak:debit-p leg) (= 0 i))
            :do (format t "~a~30t~30a~%" (ak:leg-transaction-id leg) (list (am:format-money
 nil (ak:leg-entry leg)) (ak:account-name (ak:leg-account leg))))
          :else
            :do (format t "~a~60t~30a~%" (ak:leg-transaction-id leg) (list (am:format-money
 nil (ak:leg-entry leg)) (ak:account-name (ak:leg-account leg))))
          :finally (format t "~%"))))

This function will

Queries

Let's make examples/db.lisp and define the package:

(defpackage #:examples/db
  (:use #:cl)
  (:local-nicknames (#:lt #:local-time)
                    (#:m #:mito)
                    (#:s #:sxql)
                    (#:ak #:almighty-kaikei)
                    (#:am #:almighty-money)))

(in-package #:examples/db)

This package will be in charge of helping us work with the database: establishing a connection, select queries, and other "business logic" queries.

(defmacro with-universals (bindings &body body)
  `(let ,(loop :for (var val) :in bindings
               :collect (list var
                              `(lt:timestamp-to-universal
                                (if (stringp ,val)
                                    (lt:parse-timestring ,val)
                                    ,val))))
     ,@body))

(defmacro with-db-connection (&optional (db-name "sqlite.db") &body body)
  `(let ((mito:*connection* (dbi:connect-cached :sqlite3 :database-name ,db-name)))
     (unwind-protect (progn ,@body)
       (dbi:disconnect mito:*connection*))))

(defun db-path ()
  (let ((db-path (asdf:system-relative-pathname :almighty-kaikei-examples #p"
data/sqlite.db")))
    (ensure-directories-exist db-path)
    db-path))

(defun initialize-application-database ()
  (with-db-connection (db-path)
    (let (
          ;; Asset Accounts
          (cash-account                 (ak:make-account "Cash"                "
CSH000" "asset"))
          (inventory-account            (ak:make-account "Inventory"           "
INV000" "asset"))
          (accounts-receivable-account  (ak:make-account "Accounts Receivable" "
REC000" "asset"))
          ;; Expense Accounts
          (cost-of-goods-sold-account   (ak:make-account "Cost of Goods Sold"  "
CGS000" "expense"))
          (general-expenses-account     (ak:make-account "General Expenses"    "
GEN000" "expense"))
          ;; Income Account
          (sales-revenue-account        (ak:make-account "Sales Revenue"       "
SLS000" "income"))
          ;; Liability Account
          (tax-account                  (ak:make-account "Tax Payable"         "
TAX000" "liability"))
          ;; Equity Accounts
          (room-revenue-account         (ak:make-account "Room Revenue"        "
RMR000" "equity")))
      (m:ensure-table-exists 'ak:account)
      (m:ensure-table-exists 'ak:transaction)
      (m:ensure-table-exists 'ak:leg)

      ;; Asset
      (ak:get-or-save-account cash-account)
      (ak:get-or-save-account inventory-account)
      (ak:get-or-save-account accounts-receivable-account)
      ;; Expense
      (ak:get-or-save-account cost-of-goods-sold-account)
      (ak:get-or-save-account general-expenses-account)
      ;; Income
      (ak:get-or-save-account sales-revenue-account)
      ;; Liablity
      (ak:get-or-save-account tax-account)
      ;; Equity
      (ak:get-or-save-account room-revenue-account))))

To begin with, we have with-universals. This converts local-time:timestamp objects into Common Lisp universal times. According to the HyperSpec (https://cl-community-spec.github.io/pages/Universal-Time.html),

Universal time is an absolute time represented as a single non-negative integer—the number of seconds since midnight, January 1, 1900 GMT (ignoring leap seconds).

Earlier, we wrote the transaction deftable in order to automatically serialize timestamps to universal times as well as the reverse. For our queries, if we ever want to filter rows by timestamps, we might be tempted to use local-time:timestamp objects directly, but they don't work. We could try converting timestamps into a format that SQLite understands that still looks like a timestamp, but for our purposes, a simple integer will do.

with-db-connection is a simple macro for running some code with a database connection. mito:*connection* needs to be set to a dbi connection, which we set to sqlite3. unwind-protect is a macro that will always run the form of its second argument even if the first argument form has some error. It's useful for any necessary cleanup after a form does some work, perhaps saving some data to a file, but then fails in the middle. In our case, we use it to close the connection.

db-path ensures that we have a place to put our database file (if the data folder isn't already created, for example).

initialize-application-database is for ensuring the almighty-kaikei tables are available and our Chart of Accounts is created. When we first start our application, we need to run this function.

General ledger

The most basic kind of accounting report is the General Ledger. It simply contains all of the transactions made, detailing the two sides and accounts affected.

(defun sales (from to)
  (with-universals ((from from)
                    (to to))
    (let* ((transaction-count (second (first (m:retrieve-by-sql
                                              (s:select ((:count :*))
                                                (s:from :almighty_transaction)
                                                (s:inner-join :almighty_leg :on (:=
 :almighty_leg.transaction_id :almighty_transaction.id))
                                                (s:where (:and
                                                          (:=
 :almighty_leg.account_id (m:object-id (ak:get-account "RMR000")))
                                                          (:>=
 :almighty_transaction.date from)
                                                          (:<=
 :almighty_transaction.date to))))))))
           (sales-account (ak:get-account "RMR000"))
           (legs (m:select-dao 'ak:leg
                   (m:joins 'ak:transaction)
                   (s:where (:and
                             (:= :almighty_leg.account-id (m:object-id
 sales-account))
                             (:>= :almighty_transaction.date from)
                             (:<= :almighty_transaction.date to)))
                   (s:order-by :almighty_transaction.id))))
      (format t "~&Showing ~a transactions.~%" transaction-count)

      (format t "~a~15t~a~45t~a~%" "Transaction #" "Debits" "Credits")
      (loop :for leg :in legs
            :for i = 0
            :if (and (ak:debit-p leg) (= 0 i))
              :do (format t "~a~15t~a~%" (ak:leg-transaction-id leg) (list (am:format-money
 nil (ak:leg-entry leg)) (ak:account-name (ak:leg-account leg))))
            :else
              :do (format t "~a~45t~a~%" (ak:leg-transaction-id leg) (list (am:format-money
 nil (ak:leg-entry leg)) (ak:account-name (ak:leg-account leg))))
            :finally (format t "~%")))))

general-ledger uses a with-universals macro for taking two local-time "timestrings" and converting them to universal times. When making queries like this, universal times are easier to use than trying to work with local-time timestamps–that's why we set up transaction dates to inflate/deflate (serialize/deserialize) between timestamps and universal times earlier.

Here's with-universals:

(defmacro with-universals (bindings &body body)
  `(let ,(loop :for (var val) :in bindings
               :collect (list var
                             `(lt:timestamp-to-universal
                               (if (stringp ,val)
                                   (lt:parse-timestring ,val)
                                   ,val))))
     ,@body))

It takes bindings, a list of lists (as in a let form) with a variable and the value to bind it to. It takes a string and converts it to a universal time. A simple but useful for reports like general-ledger that want to show data for a certain time range.

The transaction-count variable is bound to the result of a simple SQL query that gets the number of transactions for a time range.

legs is similar, with one small difference: it uses mito:joins–the equivalent of typing in inner-join :table_name :on .... Since we're using select-dao, we can use the joins macro to save a little bit of typing.

After that, we just go through the list of leg objects and format them. The TILDE t format directive here is useful for making tabular output. ~nt is padded to the nth space. ~15t means "If you aren't at column 15 yet, move there before continuing to format."

Output of general-ledger should look like this:

Showing 300 transactions.
Transaction #                 Debits                        Credits
1                             ($50.00 Cash)                 
1                                                           ($50.00 Room Revenue)         
2                             ($50.00 Cash)                 
2                                                           ($50.00 Room Revenue)         
3                             ($50.00 Cash)                 
3                                                           ($50.00 Room Revenue)         
4                             ($50.00 Cash)                 
4                                                           ($50.00 Room Revenue)

We're implementing these reports using format, but we could just as easily use HTML to format them for the web. For simplicity we're keeping the query definition and the "front end" together in one function.

Trial balance

A trial balance shows the net total balance of all accounts.

Tests

At this point, we need to start thinking about testing a little more seriously. We want to be able to test not only our ability to get account balances, but to do so at specific times. We need to be able to set up an environment for our tests that won't affect the other tests. What we want is essentially a lexical database environment.

So now we need to get systematic with testing, and for that we'll use lisp-unit2

Getting Started with lisp-unit2

To get started, open a buffer in t/main.lisp and add this:

(defpackage #:almighty-kaikei/test/main
  (:use #:cl)
  (:local-nicknames (#:lu #:lisp-unit2)
                    (#:lt #:local-time)
                    (#:ak #:almighty-kaikei)
                    (#:am #:almighty-money)))
(in-package #:almighty-kaikei/test/main)

Making a test

Now let's make a simple test to learn how to use lisp-unit2.

(lu:define-test testing-testing-123 ()
  (lu:assert-eql 123 123))

This test will pass if (eql 123 123) or fail otherwise. To run the test, we can use lu:run-tests.

(lu:run-tests)

It should return something like this:

#<LU:TEST-RESULTS-DB Tests:(1) Passed:(1) Failed:(0) Errors:(0) Warnings:(0) {700C0D01D3}>

Tags

You can group sets of tests using "tags". You can specify one or more tags.

(lu:define-test test-my-test (:tags '(math)))
(lu:define-test test-my-test2 (:tags '(accounting reporting)))

To run tests with a certain tag or tags:

(lu:run-tests :tags '(accounting))

Contexts

In addition to tags, you can specify contexts. Contexts are lexical environments for tests. You can specify them on a per-test basis or for all tests in the call to lu:run-tests.

(lu:define-test test-my-context-text (:contexts (list #'with-my-context
 #'with-another-context)))
(lu:run-tests :run-contexts (list #'lu:with-failure-debugging-context))

lu:with-failure-debugging-context is a built-in context that will open the debugger if an assertion fails, giving you an opportunity to look at the data.

Contexts are functions that return a funcall to a body function.

(defun with-my-context (body-fn)
  (let ((...))
    (do-thing)
    (do-another-thing)
    (funcall body-fn)))

Every test that uses with-my-custom-context will include this lexical environment.

Making testing contexts

In order to run our tests, we need to be able to do several things for every test:

  1. Connect and disconnect to a database.
  2. Create and destroy our database tables.
  3. Create a standard set of accounts (called a Chart of Accounts).

First, let's create the function and macro for connecting to the database:

(defmacro with-db-connection (&optional (db-name "sqlite.db") &body body)
  `(let ((mito:*connection* (dbi:connect-cached :sqlite3 :database-name ,db-name)))
     (unwind-protect (progn ,@body)
       (dbi:disconnect mito:*connection*))))

(defun test-db-path ()
  (let ((db-path (asdf:system-relative-pathname :almighty-kaikei #p"t/test.db")))
    (ensure-directories-exist db-path)
    db-path))

Next, we need to make a "chart of accounts" lexical database environment–"fixtures". To reiterate, there are five different categories of accounts, and a chart of accounts may have several of each category. In our context, we will make the SQL tables and then add the accounts to the database.

(defun with-chart-of-accounts-context (body-fn)
  (with-db-connection #p":memory:"
    (m:ensure-table-exists 'ak:account)
    (m:ensure-table-exists 'ak:transaction)
    (m:ensure-table-exists 'ak:leg)
    (let (
          ;; Asset Accounts
          (cash-account                 (ak:make-account "Cash"                "
CSH000" "asset"))
          (inventory-account            (ak:make-account "Inventory"           "
INV000" "asset"))
          (accounts-receivable-account  (ak:make-account "Accounts Receivable" "
REC000" "asset"))
          ;; Expense Accounts
          (cost-of-goods-sold-account   (ak:make-account "Cost of Goods Sold"  "
CGS000" "expense"))
          (general-expenses-account     (ak:make-account "General Expenses"    "
GEN000" "expense"))
          ;; Income Account
          (sales-revenue-account        (ak:make-account "Sales Revenue"       "
SLS000" "equity"))
          ;; Liability Account
          (liability-account            (ak:make-account "Liabilities"         "
LIA000" "liability"))
          ;; Equity Accounts
          (room-revenue-account         (ak:make-account "Room Revenue"        "
RMR000" "equity")))
      ;; Asset
      (ak:get-or-save-account cash-account)
      (ak:get-or-save-account inventory-account)
      (ak:get-or-save-account accounts-receivable-account)
      ;; Expense
      (ak:get-or-save-account cost-of-goods-sold-account)
      (ak:get-or-save-account general-expenses-account)
      ;; Income
      (ak:get-or-save-account sales-revenue-account)
      ;; Liablity
      (ak:get-or-save-account liability-account)
      ;; Equity
      (ak:get-or-save-account room-revenue-account)
      (funcall body-fn))))

This is the context that we'll use for our lisp-unit2 tests. It uses SQLite's in-memory database, making it temporary for the duration of each connection.

We also want a way to mess around outside of the tests, so let's make a macro that lets us do that:

(defmacro with-coa (&body body)
  `(with-db-connection (test-db-path)
     (let (
           ;; Asset Accounts
           (cash-account                 (ak:make-account "Cash"                "
CSH000" "asset"))
           (inventory-account            (ak:make-account "Inventory"           "
INV000" "asset"))
           (accounts-receivable-account  (ak:make-account "Accounts Receivable" "
REC000" "asset"))
           ;; Expense Accounts
           (cost-of-goods-sold-account   (ak:make-account "Cost of Goods Sold"  "
CGS000" "expense"))
           (general-expenses-account     (ak:make-account "General Expenses"    "
GEN000" "expense"))
           ;; Income Account
           (sales-revenue-account        (ak:make-account "Sales Revenue"       "
SLS000" "income"))
           ;; Liability Account
           (liability-account            (ak:make-account "Liabilities"         "
LIA000" "liability"))
           ;; Equity Accounts
           (room-revenue-account         (ak:make-account "Room Revenue"        "
RMR000" "equity")))
       (m:ensure-table-exists 'ak:account)
       (m:ensure-table-exists 'ak:transaction)
       (m:ensure-table-exists 'ak:leg)

       ;; Asset
       (ak:get-or-save-account cash-account)
       (ak:get-or-save-account inventory-account)
       (ak:get-or-save-account accounts-receivable-account)
       ;; Expense
       (ak:get-or-save-account cost-of-goods-sold-account)
       (ak:get-or-save-account general-expenses-account)
       ;; Income
       (ak:get-or-save-account sales-revenue-account)
       ;; Liablity
       (ak:get-or-save-account liability-account)
       ;; Equity
       (ak:get-or-save-account room-revenue-account)
       ,@body)))

Using with-coa will cause a .db file to be created that we can inspect and run queries on.

Making tests

So now it's time to make some tests. We'll start with some warm-up tests just to see if we can do basic stuff.

;; * ░ Accounting ░░░░░░░░░░░░░░░░░░░░ For our accounting scenarios, we will
;; imagine the accounting for a hotel. The hotel sells stays, has a restaurant,
;; and rooms have snack fridges. They take cash and credit.

(lu:define-test tests-get-account (:tags '(accounting) :contexts (list
 #'with-chart-of-accounts-context))
  (lu:assert-true (ak:get-account "CSH000")))

(lu:define-test test-account-code (:tags '(accounting) :contexts (list
 #'with-chart-of-accounts-context))
  (lu:assert-equal "CSH000" (ak:account-code (ak:get-account "CSH000"))))

(lu:define-test test-make-account (:tags '(accounting) :contexts (list
 #'with-chart-of-accounts-context))
  (lu:assert-true (typep (ak:make-account "Expenses" "EXP" "expense")
 'ak:account)))

(lu:define-test test-get-or-create-account (:tags '(accounting) :contexts (list
 #'with-chart-of-accounts-context))
  (let ((new-account (ak:get-or-save-account (ak:make-account "More Assets" "
ASS001" "asset"))))
    (lu:assert-no-error 'error (ak:account-code (ak:get-account "ASS001")))))

These tests check to see if everything related to getting or making accounts is working. We can run these with (lu:run-tests). If everything passes, great. If you have a failing test, run (lu:run-tests :run-contexts (list #'lu:with-failure-debugging-context)).

Assuming you have everything settled with the above tests, let's start getting into the meat of things. Remember that the purpose of these tests is to confirm both that the system is working and that we understand how the accounting works.

;; * ░ Transactions ░░░░░░░░░░░░░░░░░░░░ * ░ Card Sale of Service
;; *░░░░░░░░░░░░░░░░░░░░ ;; This scenario involves tracking: ;; 1. A card
;; *payment going to our Receivable account. ;; 2. Card payment provider
;; *expense. ;; 3. The revenue after paying that expense.

(lu:define-test test-transact!-card-sale-of-service (:tags '(accounting
 transactions)
                                                     :contexts (list
 #'with-chart-of-accounts-context))

  (ak:transact! (ak:debit "REC000" 4500)
                (ak:debit "GEN000" 500)
                (ak:credit "RMR000" 5000))
  (lu:assert-equality #'am:money= (ak:account-balance (ak:get-account "REC000")) (am:usd
 4500))
  (lu:assert-equality #'am:money= (ak:account-balance (ak:get-account "GEN000")) (am:usd
 500))
  (lu:assert-equality #'am:money= (ak:account-balance (ak:get-account "RMR000")) (am:usd
 5000)))

(lu:define-test test-transact!-unbalanced-sides (:tags '(accounting transactions)
                                                 :contexts (list
 #'with-chart-of-accounts-context))
  (lu:assert-error 'ak:unbalanced-transaction (ak:transact! (ak:debit "CSH000"
 5000)
                                                            (ak:credit "RMR000"
 4000))))

(lu:define-test test-transact!-not-enough-legs (:tags '(accounting transactions)
                                                :contexts (list
 #'with-chart-of-accounts-context))
  (lu:assert-error 'simple-error (ak:transact! (ak:debit "CSH000" 5000))))

Recall that we're considering the example of a hotel's finances. In the card-cale-of-service scenario, we receive a credit card payment for a room. We credit our Room Revenue income account 5000 cents ($50)–increasing its value. The card payment service charges us 500 cents ($5) that is debited to our general expenses account–adding to its value. Finally we credit our Receivable account the remainder of the money, 4500 cents ($45), which is what we expect to actually have after paying our expenses.

Since the test begins with no transactions in the database, all three accounts should have account balances equal the amounts they've been increased by.

The second test just checks that our unbalanced-transaction condition works properly. The third checks to make sure we receive an error if there aren't at least two legs in the transaction.

We'll make a couple more tests just to confirm that our understanding of the principles of double-entry accounting are solid.

;; ** ░ Cash Sale of Goods ░░░░░░░░░░░░░░░░░░░░ This scenario involves tracking:
;; 1. The cash paid. 2. The revenue earned. 3. The cost of the good sold. 4. The
;; reduction in inventory of the good. 5. The total profit (revenue - cogs)

(lu:define-test test-cash-sale-of-goods (:tags '(accounting transactions)
                                         :contexts (list
 #'with-chart-of-accounts-context))
  (let ((cash-account "CSH000")
        (sales-account "SLS000")
        (cogs-account "CGS000")
        (inventory-account "INV000"))
    (ak:transact! (ak:debit cash-account 5000) (ak:credit sales-account 5000)
                  (ak:debit inventory-account 3000) (ak:credit cogs-account 3000))
    (lu:assert-equality #'am:money= (ak:account-balance cash-account) (am:usd
 5000))
    (lu:assert-equality #'am:money= (ak:account-balance sales-account) (am:usd
 5000))
    (lu:assert-equality #'am:money= (ak:account-balance cogs-account) (am:usd -3000)) ; Crediting
                                                                                      ; expense
                                                                                      ; account
                                                                                      ; decreases
                                                                                      ; its
                                                                                      ; value.
    (lu:assert-equality #'am:money= (ak:account-balance inventory-account) (am:usd
 3000))
    (lu:assert-equality #'am:money=
                        (am:money+ (ak:account-balance sales-account) (ak:account-balance
 cogs-account))
                        (am:usd 2000))))

;; ** ░ Credit Card Sale of Hotel Stay ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
;; This scenario involves tracking: 1. Accounts Receivable (Asset) for credit
;; card payment. 2. The room revenue (Equity) earned. 3. Credit card processing
;; fees (Expense)

(lu:define-test test-debit-card-sale-of-hotel-stay (:tags '(accounting
 transactions)
                                                    :contexts (list
 #'with-chart-of-accounts-context))
  (let ((receivable-asset-account "REC000")
        (general-expense-account "GEN000")
        (room-revenue-account "RMR000")
        (cash-asset-account "CSH000"))
    ;; Sale is made--debit Receivable Account for card payment, debit general
    ;; expenses for card fees. Credit Room Revenue for sale.
    (ak:transact! (ak:credit room-revenue-account (am:usd 5000))
                  (ak:debit general-expense-account (am:usd 500))
                  (ak:debit receivable-asset-account (am:usd 4500)))
    (lu:assert-equality #'am:money=
                        (ak:account-balance receivable-asset-account)
                        (am:usd 4500))
    (lu:assert-equality #'am:money=
                        (ak:account-balance general-expense-account)
                        (am:usd 500))
    (lu:assert-equality #'am:money=
                        (ak:account-balance room-revenue-account)
                        (am:usd 5000))
    ;; Bank fulfills payment; clear receivable and add to cash
    (ak:transact! (ak:debit cash-asset-account (am:usd 4500))
                  (ak:credit receivable-asset-account (am:usd 4500)))
    (lu:assert-equality #'am:money=
                        (ak:account-balance receivable-asset-account)
                        (am:usd 0))
    (lu:assert-equality #'am:money=
                        (ak:account-balance cash-asset-account)
                        (am:usd 4500))))

Call (lu:run-tests) again. Everything pass? Good. Let's test one more important feature: the as-of parameter of account-balance.

(lu:define-test test-account-legs-as-of
    (:tags '(query) :contexts (list #'with-chart-of-accounts-context))
  (let ((cash (ak:get-account "CSH000"))
        (sale (ak:get-account "SLS000"))
        (date (lt:now)))
    (let ((1st (ak:transact! (ak:debit cash (am:usd 500))
                             (ak:credit sale (am:usd 500))
                             date))
          (2nd (ak:transact! (ak:debit cash (am:usd 500))
                             (ak:credit sale (am:usd 500))
                             (lt:timestamp+ date 10 :sec))))
      (lu:assert-number-equal 1 (length (ak:account-legs cash (lt:timestamp+ (ak:transaction-date
 2nd) -1 :sec)))))))

Here we make two transactions, one ten seconds after the other. We set the as-of parameter to one second before the second transaction. We expect for the cash account that we should only have one leg–the one for the first transaction. Call (lu:run-tests) and check if this passes. It does? Good.