Building a powerful double entry accounting system

Preview:

Citation preview

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

Recommended