building a powerful double entry accounting system
TRANSCRIPT
BUILDING A POWERFUL DOUBLE-ENTRY ACCOUNTING SYSTEMLucas Cavalcanti
DOUBLE-ENTRY ACCOUNTING ANCIENT, UBIQUITOUS TECHNOLOGY
USER BALANCES
Future bills
Open bill
Due bill
Available balance
OPERATIONS
Purchases
Chargebacks
Payments
BALANCE SHEET
DEBIT CREDIT
General Ledger
USEFUL ABSTRACTIONS
Income statement
Cash flow statement
Balance sheet
LIABILITYASSET
EQUITY
Debits Credits
PROPERTY: on each movement
Image © http://chestofbooks.com/business/reference/Home-Cyclopedia-Of-Business/Bill-Book.html
Credits (CR): - $97 on liability payables (we will pay the merchant) - $3 Credit on P&L interchange (our profit)
- $100 on off-balance asset current-limit
EXAMPLE: A purchase of $100
Debits (DR): - $100 on asset settled (we will receive it from the customer)
- $100 on off-balance liability current-limit-cp
EXAMPLE: A payment of $100
Debits (DR): - $100 on asset cash (we received it from the customer)
- $100 on off-balance asset current-limit
Credits (CR): - $100 on asset settled (we paid the purchase)
- $100 on off balance liability current-limit-cp
DOUBLE ENTRY ACCOUNTING
EVENTS TRIGGERING MOVEMENTS • purchases • payments • bills
IMMUTABLE • append-only • entry log • can fix past by compensating
INVARIANTS • movements sums to zero • a book-account balance is sum of credits
and debits
1 MOVEMENT => N ENTRIES
(s/defschema Entry {:entry/id s/Uuid :entry/amount PositiveAmount :entry/debit-account BookAccount :entry/credit-account BookAccount :entry/post-date LocalDate :entry/movement Movement})
So by design,
(ns double-entry.models.entry (:require [schema.core :as s]))
1 BUSINESS EVENT => 1 MOVEMENT + META e.g new-purchase, new-payment, new-bill
(s/defschema Movement {:movement/id s/Uuid :movement/flow-id String :movement/topic Topic :movement/owner-account Account :movement/produced-at LocalDateTime :movement/consumed-at LocalDateTime :movement/user String})
(s/defschema Meta {:meta/id s/Uuid :meta/movement Movement :meta/entity (s/either Payment Purchase Bill ...)})
DECLARATIVE RULES FOR MOVEMENTS
(ns common-schemata.wire)
(s/defschema Purchase {:purchase {:id s/Uuid :merchant String :amount t-money/PositiveAmount :interchange t-money/PositiveAmount :time LocalDateTime …}})
(s/defschema Payment {:payment {:id s/Uuid :amount t-money/PositiveAmount :post-date LocalDate …}})
DECLARATIVE RULES FOR MOVEMENTS
(def new-purchase [{:entry/debit-account :book-account.asset/settled-brazil :entry/credit-account :book-account.liability/payable-brazil :entry/amount (comp :amount :purchase) :entry/post-date (comp time->date :time :purchase)}
{:entry/debit-account :book-account.liability/payable-brazil :entry/credit-account :book-account.profit-and-loss/interchange-brazil :entry/amount (comp :interchange :purchase) :entry/post-date (comp time->date :time :purchase)} {:entry/debit-account :book-account.liability/current-limit-counterparty :entry/credit-account :book-account.asset/current-limit :entry/amount (comp :amount :purchase) :entry/post-date (comp time->date :time :purchase)}])
DECLARATIVE RULES FOR MOVEMENTS
(def new-payment [{:entry/debit-account :book-account.asset/transitory-bank :entry/credit-account :book-account.asset/settled-brazil :entry/amount (comp :amount :payment) :entry/post-date (comp :post-date :payment)} {:entry/debit-account :book-account.asset/current-limit :entry/credit-account :book-account.liability/current-limit-counterparty :entry/amount (comp :amount :payment) :entry/post-date (comp :post-date :purchase)}])
[{:entry/id (uuid) :entry/amount 100.0M :entry/debit-account :book-account.asset/settled-brazil :entry/credit-account :book-account.liability/payable-brazil :entry/post-date #nu/date "2016-12-01" :entry/movement new-purchase} {:entry/id (uuid) :entry/amount 3.0M :entry/debit-account :book-account.liability/payable-brazil :entry/credit-account :book-account.profit-and-loss/interchange-brazil :entry/post-date #nu/date "2016-12-01" :entry/movement new-purchase}
{:entry/id (uuid) :entry/amount 100.0M :entry/debit-account :book-account.liability/current-limit-counterparty :entry/credit-account :book-account.asset/current-limit :entry/post-date #nu/date "2016-12-01" :entry/movement new-purchase}]
{:purchase {:id (uuid) :amount 100.0M :interchange 3.0M :time #nu/time "2016-12-01T13:37:42Z"}}
Rulebook
[{:entry/id (uuid) :entry/amount 100.0M :entry/debit-account :book-account.asset/transitory-bank :entry/credit-account :book-account.asset/settled-brazil :entry/post-date #nu/date "2016-12-01" :entry/movement new-payment}
{:entry/id (uuid) :entry/amount 100.0M :entry/debit-account :book-account.asset/current-limit :entry/credit-account :book-account.liability/current-limit-counterparty :entry/post-date #nu/date "2016-12-01" :entry/movement new-payment}]
{:payment {:id (uuid) :amount 100.0M :post-date #nu/date "2016-12-01"}}
Rulebook
Cumulative cache (balance sheet)
Event (log)
2 LOGS AND A CACHE
ACTUAL TIME audit trail / Datomic log “when did we know”day 0 day 30 day 90
SYSTEM OF RECORD TIME official version of events uses business-relevant “post dates” can correct after the fact
day 90
day 0day 0
day 30
SANITY CHECKS / BUSINESS INVARIANTS
• Balances must be always positive or always negative
• Cannot have a “late” balance there is a “prepaid” balance
• A purchase should “move” exactly the purchase amount on assets and on current limit
(def balances-property (prop/for-all [account (g/generator Account) events (gen/vector (gen/one-of [(g/generator Purchase) (g/generator Payment) ...]))] (->> (empty-db) (save-all! account events) :db-after (balances-are-positive!)))
(fact (tc/quick-check 50 balances-property) => (th/embeds {:result true}))
GENERATIVE TESTING(ns double-entry.controllers.rulebook-test (:require [midje.sweet :refer :all] [clojure.test.check.properties :as prop] [clojure.test.check :as tc] [schema-generators.generators :as g] [clojure.test.check.generators :as gen]))
EVENT STREAM FOR A SINGLE CUSTOMER
1. Business events generate idempotent Kafka messages
2. For each event, apply functions to convert the event data into a movement with 1+ entries • Movements balance by design • Movements associate provenance metadata
3. Pre-check guarantees invariants against db value
4. Eagerly cache resulting balances
debit-account credit-account amount
a/max-limit a/current-limit
l/max-limit-cpl/current-limit-cp
initial-limit
15000.0015000.00
CARD ISSUED
debit-account credit-account amount
new-purchase / settle-brazil
100.00100.00
100.00100.00
CARD ISSUED FIRST PURCHASE
l/current-limit-cpa/unsettled
l/unsettled-cpa/settled-brazil
a/current-limitl/unsettled-cp
a/unsettledl/payable-brazil
debit-account credit-account amount
closed-bill
15.00100.00
CARD ISSUED FIRST PURCHASE
a/minimum-paymenta/closed
l/minimum-payment-cpa/settled-brazil
FIRST BILL CLOSES
debit-account credit-account amount
new-payment
10.0010.0010.00
CARD ISSUED FIRST PURCHASE
a/current-limitl/minimum-payment-cpa/transitory-bank
l/current-limit-cpa/minimum-paymenta/late
FIRST BILL CLOSES PARTIAL PAYMENT
debit-account credit-account amount
new-adjustment
7.007.00
CARD ISSUED FIRST PURCHASE
l/current-limit-cpa/settled-financed
a/current-limite/revolving-interest
FIRST BILL CLOSES PARTIAL PAYMENT INTEREST ASSESSED
debit-account credit-account amount
closed-bill
7.005.0014.557.00
90.005.007.0090.007.007.007.002.557.00
CARD ISSUED FIRST PURCHASE
a/latel/minimum-payment-cpa/minimum-paymenta/closed
a/current-limitl/minimum-payment-cpa/transitory-banka/transitory-bankl/prepaida/current-limitl/minimum-payment-cpl/minimum-payment-cpa/closed
a/closeda/minimum-paymentl/minimum-payment-cpa/settled-financed
l/current-limit-cpa/minimum-paymentl/prepaida/latea/closedl/current-limit-cpa/minimum-paymenta/minimum-paymenta/late
FIRST BILL CLOSES PARTIAL PAYMENT INTEREST ASSESSED PAYMENT IN FULL
ZOOMING OUT
2015-01 2015-04 2015-07 2015-10 2015-01 2016-03
DETECTING OPERATIONAL MISTAKES
USE CASES
MANAGEMENT ACCOUNTING • delinquency tables by cohort and aging
• receivables (domestic, foreign, financed)
• revenue per customer (interchange, interest, fx spread)
REPORTING • covenants
• regulatory
FINANCIAL ACCOUNTING • consolidate to ERP
Declarative rules are extensible for additional financial products (e.g., already extending to rewards, debt financing)
Financial analysis applies at a micro level (negative balances, weird ratios, operational problems)
Business-specific invariants provide safety (declare mutually exclusive and impossible states,alert unexpected situations)
Generative testing finds real bugs
Service is shardable by customer account (no interactions between accounts)
WHAT DO WE LIKE?
33
IF YOU ARE ENTRUSTED WITH A CUSTOMER’S FINANCIAL RELATIONSHIP, CONSIDER BUILDING A DOUBLE-ENTRY SYSTEM FOR YOUR DOMAIN
Thank you!
Lucas Cavalcanti @lucascs
IF YOU CHOOSE TO DO SO, USE ALL THE POWER FUNCTIONAL PROGRAMMING GIVES YOU