Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

cashflow source pure

Package cashflow derives a DIRECT-METHOD statement of cash flows from a double-entry book: how cash moved over one wi...

Overview

Package cashflow derives a DIRECT-METHOD statement of cash flows from a double-entry book: how cash moved over one window, split into operating, investing and financing activity, with an explicit refusal where the split is not determinate.

It is the third primary financial statement. p/statements derives the other two from a chart at an instant, and p/periods derives one window's movement; all three of those packages carry a note saying they do not do this one, and all three say the INDIRECT method needs a cash-vs-accrual classification journal.Posting does not carry. That is true. The direct method needs something different and smaller: a declaration of which chart codes hold cash. A declaration is something a caller can supply, which is what makes this package possible without changing what a posting is.

The sign convention, and why cash must be an asset

A debit to a cash account is an INFLOW and a credit is an OUTFLOW. That follows from cash being debit-normal, so Policy requires every declared cash code to be a journal.Asset and refuses anything else (ErrCashAccountNotAsset). Declaring a credit-normal account as cash would invert every figure in the report and nothing downstream could tell. The cost of the rule is real and is stated rather than hidden: a bank overdraft carried as a liability cannot be declared as cash, and a caller holding one must carry it as a contra-asset or leave it out.

Attribution is per ENTRY, never per leg

This is the package's central correctness decision, and the natural design is wrong in a way that produces numbers which balance. Take one ordinary part-paid purchase:

Example
1debit  Inventory          100
2credit Cash                60
3credit AccountsPayable     40

Classifying each non-cash leg and attributing its own amount reports 100 of inventory-related outflow against the 60 of cash that actually moved. It invents a cash flow of 40 that did not happen, and the statement still adds up. So the unit of attribution is the entry:

  • An entry with NO cash legs is skipped. It is accrual-only and moved no cash. This is the direct method's whole point.
  • An entry whose net cash movement is ZERO is named in [Report.NetZero] and not classified. A transfer between two declared cash accounts is the common case; there is nothing to attribute, so determinacy is not demanded of it.
  • Otherwise the entry's net cash movement is computed from its CASH legs alone — so the figure is what actually moved — and that whole figure is attributed to one category.

The determinacy rule, stated as a refusal

An entry touching cash is determinate iff it has at least one non-cash leg and all of its non-cash legs classify to the SAME category, in which case the entry's whole net cash delta goes there. Otherwise the entry is named in [Report.Unresolved] with the reason, and its cash delta is carried in [Report.Unattributed].

The "at least one" clause is not pedantry. An entry with nonzero net cash and no non-cash leg at all cannot come out of a balanced book, and it gets its own ReasonUnbalanced rather than being reported as a conflict with nothing to conflict — see that constant.

Under that rule the purchase above is determinate when Inventory and AccountsPayable both classify operating: 60 of operating outflow, which is right. An entry that settles a loan and buys equipment at once — one leg financing, one investing — is NOT determinate, and no apportionment is invented for it. Splitting it proportionally would be a guess wearing a number's clothes.

Why classification is per CODE and not per journal.Kind

A mapping from journal.Kind to a category would need no caller declaration at all, and it is the first design anyone reaches for. It is wrong on the most common accounts in any book: receivables and inventory are assets whose movement is OPERATING, not investing, and a kind-based mapping puts the entire working-capital cycle in the wrong section. So there is no kind-based default and no default bucket of any sort. A non-cash account with no declared category makes its entry unresolved (ReasonUndeclared) — silence is not assent.

What this package does not do

  • **It never writes, and never sees a Book.** Derive returns figures; the caller owns persistence and idempotence.
  • **It cannot verify the policy.** Which codes hold cash and what each other code's movement means are caller declarations. A chart that declares a receivable as cash produces a plausible, wrong statement. The declaration is the trust boundary.
  • **It cannot verify the opening balance.** A window-filtered entry slice carries period movement, not a cumulative position, so [Report.Opening] is echoed back exactly as supplied and [Report.Closing] is only as good as it. This is p/periods' limitation inherited, not a new one.
  • **No reversal handling.** A reversing entry that touches cash moved cash, and is classified like any other entry. That differs from p/periods, where an unresolved reversal is a census item, and the difference is deliberate: a cash-flow statement reports movement, and a reversal's movement is movement.
  • **No paging and no merge.** MaxEntries is the hard bound. Merging reports across pages would make the unresolved set an artifact of the paging rather than of the data.
  • **No audit.** Every figure is derived from what the caller supplied. A chart from one book and entries from another produce a plausible report and nothing here detects it.
  • **Reconciling against p/periods proves nothing about the split.** Comparing [Report.NetChange] with the cash accounts' movement in a periods report is an arithmetic self-check that both packages added the same columns the same way. A statement that files every flow under the wrong category reconciles perfectly.

p/journal: /p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal p/periods: /p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/periods p/statements: /p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/statements

Constants 5

const MaxAccounts

1const MaxAccounts = periods.MaxAccounts
source

MaxAccounts bounds the chart Derive will accept. It is periods' bound, which is statements' bound, which is journal's own account cap — one number, declared once, so a chart that clears one of these packages clears all three.

const MaxEntries

1const MaxEntries = periods.MaxEntries
source

MaxEntries bounds the entry slice Derive will walk. It is periods' bound for the same reason: a caller feeding the same window to both packages must not have one of them refuse what the other accepted.

const NumCategories

1const NumCategories = 3
source

NumCategories is how many categories a statement of cash flows has, and so the length [Report.Lines] always has. It is a constant of the standard, not a configuration.

const CategoryOperating, CategoryInvesting, CategoryFinancing

1const (
2	CategoryOperating Category = 1 // trading: the working-capital cycle
3	CategoryInvesting Category = 2 // long-lived assets and investments
4	CategoryFinancing Category = 3 // capital and borrowings
5)
source

The three activity categories of a statement of cash flows.

const ReasonConflict, ReasonUndeclared, ReasonUnbalanced

 1const (
 2	// ReasonConflict is an entry whose non-cash legs classify to more than
 3	// one category. [Unresolved.Categories] names them, and names at least
 4	// two.
 5	ReasonConflict Reason = 1
 6
 7	// ReasonUndeclared is an entry with a non-cash leg whose account has no
 8	// policy rule. [Unresolved.Codes] names them. An undeclared account is
 9	// named rather than defaulted, because a default bucket turns a missing
10	// declaration into a figure nobody questions.
11	//
12	// It takes precedence over [ReasonConflict] when an entry has both an
13	// undeclared code and two declared categories, because the undeclared
14	// code is the actionable half: declaring it may resolve the entry, and
15	// reporting a conflict while concealing a missing declaration would send
16	// the reader to re-examine a policy that is not yet complete.
17	ReasonUndeclared Reason = 2
18
19	// ReasonUnbalanced is an entry all of whose legs are declared cash
20	// accounts and whose cash legs do not net to zero.
21	//
22	// [journal.Book] cannot produce one: a balanced entry whose every leg is
23	// cash has equal debits and credits over cash and therefore nets to zero,
24	// which makes it a [Report.NetZero] item instead. So this reason is
25	// reachable only from a hand-built entry slice, the same class
26	// [p/periods] documents for its own hand-built Report.
27	//
28	// It exists rather than being folded into [ReasonConflict] because a
29	// conflict carrying an EMPTY [Unresolved.Categories] is indistinguishable
30	// from a bug in this package, and a census item a reader cannot tell from
31	// a defect is not a census item. Found by writing the test for the
32	// conflict case, not by reading the code.
33	ReasonUnbalanced Reason = 3
34)
source

The three reasons an entry is unresolved.

Variables 1

var ErrInvalidWindow, ErrTooManyAccounts, ErrChartNotSorted, ErrInvalidKind, ErrTooManyEntries, ErrEntriesNotSorted, ErrTooManyPostings, ErrInvalidPosting, ErrUnknownAccount, ErrNoCashAccounts, ErrCashNotSorted, ErrUnknownCashAccount, ErrCashAccountNotAsset, ErrRulesNotSorted, ErrInvalidCategory, ErrUnknownRuleAccount, ErrCashAccountClassified, ErrOverflow

 1var (
 2	// ErrInvalidWindow is a window failing [periods.Window.Valid].
 3	ErrInvalidWindow = errors.New("cashflow: invalid window")
 4
 5	// ErrTooManyAccounts is a chart longer than [MaxAccounts].
 6	ErrTooManyAccounts = errors.New("cashflow: chart length exceeds MaxAccounts")
 7
 8	// ErrChartNotSorted is a chart whose codes are not strictly ascending.
 9	// Membership here is a binary search, so the ordering is load-bearing
10	// rather than cosmetic.
11	ErrChartNotSorted = errors.New("cashflow: chart codes not strictly ascending")
12
13	// ErrInvalidKind is a chart row whose Kind is outside journal's five.
14	ErrInvalidKind = errors.New("cashflow: invalid account kind")
15
16	// ErrTooManyEntries is an entry slice longer than [MaxEntries].
17	ErrTooManyEntries = errors.New("cashflow: entry count exceeds MaxEntries")
18
19	// ErrEntriesNotSorted is an entry slice not strictly ascending by ID.
20	ErrEntriesNotSorted = errors.New("cashflow: entry IDs not strictly ascending")
21
22	// ErrTooManyPostings is an entry with more than
23	// [journal.MaxPostingsPerEntry] postings. journal cannot produce one, and
24	// re-checking is what makes [MaxEntries]'s cost bound a proven ceiling
25	// rather than a hope.
26	ErrTooManyPostings = errors.New("cashflow: entry exceeds journal.MaxPostingsPerEntry")
27
28	// ErrInvalidPosting is an in-window posting with an invalid Side or a
29	// non-positive Amount. journal carries direction in Side and amounts are
30	// always positive, so a signed amount here is a posting from somewhere
31	// else.
32	ErrInvalidPosting = errors.New("cashflow: invalid posting")
33
34	// ErrUnknownAccount is an in-window posting naming a code absent from the
35	// chart. It is scoped to the window because no figure in the report
36	// depends on an entry outside it.
37	ErrUnknownAccount = errors.New("cashflow: posting names an account absent from the chart")
38
39	// ErrNoCashAccounts is an empty [Policy.Cash]. A statement of cash flows
40	// over a book with no declared cash is all zeroes and looks exactly like
41	// a quiet period, so the empty declaration is refused rather than
42	// reported.
43	ErrNoCashAccounts = errors.New("cashflow: policy declares no cash accounts")
44
45	// ErrCashNotSorted is a [Policy.Cash] not strictly ascending.
46	ErrCashNotSorted = errors.New("cashflow: policy cash codes not strictly ascending")
47
48	// ErrUnknownCashAccount is a declared cash code absent from the chart.
49	ErrUnknownCashAccount = errors.New("cashflow: policy declares a cash account absent from the chart")
50
51	// ErrCashAccountNotAsset is a declared cash code whose chart Kind is not
52	// [journal.Asset]. See the package doc on the sign convention: this is
53	// what keeps a debit to cash meaning an inflow.
54	ErrCashAccountNotAsset = errors.New("cashflow: policy declares a non-asset account as cash")
55
56	// ErrRulesNotSorted is a [Policy.Rules] not strictly ascending by Code.
57	ErrRulesNotSorted = errors.New("cashflow: policy rule codes not strictly ascending")
58
59	// ErrInvalidCategory is a category outside the declared three — in a
60	// policy rule, or passed to [Report.Line].
61	ErrInvalidCategory = errors.New("cashflow: unknown category")
62
63	// ErrUnknownRuleAccount is a policy rule naming a code absent from the
64	// chart.
65	ErrUnknownRuleAccount = errors.New("cashflow: policy rule names an account absent from the chart")
66
67	// ErrCashAccountClassified is a code declared both as cash and as a
68	// classified account. The two roles are exclusive: a cash account is what
69	// MOVES, and a classified account is what the movement is FOR. Accepting
70	// both would make the same code answer two different questions in one
71	// entry.
72	ErrCashAccountClassified = errors.New("cashflow: code is declared as cash and also classified")
73
74	// ErrOverflow is an int64 overflow in any accumulation, including the
75	// entry-level cash total and the closing balance.
76	ErrOverflow = errors.New("cashflow: int64 overflow")
77)
source

Functions 1

func Derive

1func Derive(chart []journal.Account, entries []journal.Entry, w periods.Window, p Policy, opening int64) (Report, error)
source

Derive builds the statement for one window.

Preconditions, all refused rather than assumed: w must be valid; chart must be strictly ascending by Code, no longer than MaxAccounts, with every Kind one of journal's five; entries must be strictly ascending by ID and no longer than MaxEntries, with at most journal.MaxPostingsPerEntry postings each; and p must satisfy the rules on Policy.

Entries outside w are skipped, not refused — a caller passing the whole book is the expected case — and their postings are therefore not content-checked, because no figure in the report depends on them. ErrInvalidPosting and ErrUnknownAccount are scoped to the window for that reason; the postings-per-entry cap is not, since it is what makes MaxEntries's cost ceiling a bound rather than a hope.

Types 7

type Category

ident
1type Category uint8
source

Category is which activity a cash flow belongs to. The zero value is deliberately invalid, so a forgotten field is an error rather than operating activity — which is the bucket a defaulting implementation would pick and the one that would hide the most.

The constants carry a Category prefix for the same reason [statements.Section]'s do: a bare Operating would read as a field name.

Methods on Category

func String

method on Category
1func (c Category) String() string
source

String returns the category's name, or "invalid" for the zero value and any unrecognised code.

func Valid

method on Category
1func (c Category) Valid() bool
source

Valid reports whether c is one of the three categories.

type Classification

struct
1type Classification struct {
2	Code     string
3	Category Category
4}
source

Classification is one account code's activity category, declared by the caller. See the package doc on why this is per code rather than per journal.Kind.

type Line

struct
 1type Line struct {
 2	Category Category
 3
 4	// Inflows is the gross cash received, non-negative.
 5	Inflows int64
 6
 7	// Outflows is the gross cash paid, non-negative.
 8	Outflows int64
 9
10	// Entries is how many in-window entries were attributed here.
11	Entries int
12}
source

Line is one category's gross flows over the window.

Inflows and Outflows are both non-negative AS PRODUCED BY Derive, and gross rather than netted, because a category with 1,000 in and 1,000 out is not the same business as one with nothing happening and a net of zero is all a netted figure can say about either.

Methods on Line

func Net

method on Line
1func (l Line) Net() int64
source

Net returns Inflows minus Outflows.

The subtraction is unchecked, and that is safe for any Line Derive produces: both columns are non-negative, so the difference is in range for every pair of int64 values they can hold. A hand-built Line with a negative column can wrap, and that is the only way it can.

The wrapped value does not stay local, which is the part worth stating: Report.Reconciles sums Net() over the lines, and a wrapped Net() can make it agree over a report whose columns say something else entirely. So Reconciles refuses a negative column outright rather than summing it — see the guard there. Net() itself stays unchecked because it has no way to report a refusal and a Line is a figure, not a claim.

This note previously compared the blast radius to periods.Activity.Movement and said that one was wider because a negative column there also inverts a sign convention. That comparative was wrong, and an audit of this package found it: both wrap a figure that then passes every downstream guard, and the real asymmetry is in where the wrapped value GOES, not in sign inversion. The note is kept, corrected, because the equivalent note in periods was also once wrong about its own blast radius and also had to be found by an audit — twice is a pattern, and the pattern is that a comparative claim about another package's hazard is the sentence nothing checks.

type Policy

struct
 1type Policy struct {
 2	// Cash is the codes that hold cash, strictly ascending. Must be
 3	// non-empty, and every code must be a [journal.Asset] in the chart.
 4	Cash []string
 5
 6	// Rules is the activity category for non-cash codes, strictly ascending
 7	// by Code. A code absent from here is NOT defaulted; an entry touching it
 8	// is unresolved with [ReasonUndeclared].
 9	Rules []Classification
10}
source

Policy is the caller's whole declaration: which chart codes hold cash, and what each other code's movement is for.

Both slices must be strictly ascending by code, because membership is a binary search in Derive's inner loop. The two sets must be disjoint (ErrCashAccountClassified). Neither is verified against anything but the chart — this package cannot know whether a code called "petty cash" holds cash, and says so in its doc rather than pretending the name is evidence.

type Reason

ident
1type Reason uint8
source

Reason is why Derive would not classify an entry. The zero value is deliberately invalid.

Methods on Reason

func String

method on Reason
1func (r Reason) String() string
source

String returns the reason's name, or "invalid".

func Valid

method on Reason
1func (r Reason) Valid() bool
source

Valid reports whether r is one of the three reasons.

type Report

struct
 1type Report struct {
 2	// Window is the window the figures are for, echoed back so a stored
 3	// report cannot be read without it.
 4	Window periods.Window
 5
 6	// Lines is the three categories, ascending by Category, always present
 7	// and always of length [NumCategories] — a category with no activity is a
 8	// zero line, not an absent one, because a statement of cash flows with a
 9	// missing section is not a statement of cash flows. Use [Report.Line] to
10	// reach one by category rather than indexing by arithmetic.
11	Lines []Line
12
13	// Opening is the caller-supplied opening cash position, echoed back
14	// unchanged. This package cannot verify it; see the package doc. A
15	// negative value is accepted — an overdrawn position is a real one — so
16	// the sign is not constrained here.
17	Opening int64
18
19	// NetChange is the window's total net cash movement, attributed and
20	// unattributed together. It is the book's figure, not the statement's, so
21	// it is true even when the split is not complete.
22	NetChange int64
23
24	// Unattributed is the net cash movement of the entries in Unresolved.
25	// Zero exactly when every entry that moved cash was classified.
26	Unattributed int64
27
28	// Closing is Opening plus NetChange.
29	Closing int64
30
31	// Entries is how many entries fell in the window, including the ones that
32	// moved no cash. Zero is a legitimate report of a quiet period, not an
33	// error.
34	Entries int
35
36	// CashEntries is how many in-window entries touched a declared cash
37	// account at all, including the ones in NetZero and Unresolved.
38	CashEntries int
39
40	// NetZero is the IDs of in-window entries that touched cash but moved
41	// none on net — a transfer between two declared cash accounts, possibly
42	// alongside non-cash legs. Classification is deliberately not attempted:
43	// there is nothing to attribute, so demanding determinacy of them would
44	// manufacture unresolved entries out of non-events.
45	NetZero []uint64
46
47	// Unresolved is the entries whose category was not determinate.
48	Unresolved []Unresolved
49}
source

Report is one window's statement of cash flows. Every slice is freshly allocated by Derive on every call and is never nil, so the caller owns it and an empty result is an empty slice rather than a nil to be distinguished from one.

The invariant that makes the report readable when the split is incomplete:

Example
1sum(Lines[i].Net()) + Unattributed == NetChange
2Opening + NetChange             == Closing

Both hold on every Report Derive returns, including one with unresolved entries. Report.Reconciles asserts the first.

Methods on Report

func Complete

method on Report
1func (r Report) Complete() bool
source

Complete reports whether every in-window entry that moved cash was attributed to a category.

A complete report is not a correct one: completeness is a property of the POLICY's coverage, and a policy that classifies every account wrongly is complete. See the package doc.

func Line

method on Report
1func (r Report) Line(c Category) (Line, error)
source

Line returns the line for c. An invalid category is ErrInvalidCategory; there is no default line.

This is the accessor to use. [Report.Lines] is ordered by Category and a consumer could index it as Lines[c-1], but that arithmetic is a second copy of the ordering invariant and the kind of thing that survives a reordering silently.

func Reconciles

method on Report
1func (r Report) Reconciles() bool
source

Reconciles reports whether the attributed lines plus the unattributed residual equal NetChange.

It is offered as API rather than asserted internally because a Report can be built by hand, and a consumer that stores one should be able to check it on the way back out. On a Report from Derive it is always true.

False means one of three things and does not distinguish them: the lines do not sum to NetChange, the sum overflows, or a line carries a negative column (which Line.Net would wrap). All three are "do not trust this report", which is the whole question a bool can answer.

type Unresolved

struct
 1type Unresolved struct {
 2	// EntryID is the entry's journal ID.
 3	EntryID uint64
 4
 5	// Reason is why it could not be classified.
 6	Reason Reason
 7
 8	// Net is the entry's net cash movement: positive for an inflow. Reported
 9	// so a reader can see the size of what is unattributed rather than only
10	// that something is.
11	Net int64
12
13	// Categories is the conflicting set, ascending and of length at least
14	// two, when Reason is [ReasonConflict]; empty otherwise. Freshly
15	// allocated and never nil.
16	//
17	// Ascending rather than in posting order, so that two records of the same
18	// conflict compare equal whichever leg was posted first.
19	Categories []Category
20
21	// Codes is the undeclared account codes, in posting order and
22	// deduplicated, when Reason is [ReasonUndeclared]; empty otherwise.
23	// Freshly allocated and never nil.
24	//
25	// Posting order rather than ascending, because these are a worklist for
26	// whoever extends the policy and the order a reader meets them in the
27	// entry is the more useful one. The two slices differ on this
28	// deliberately: a conflict set is compared, a code list is worked
29	// through.
30	Codes []string
31}
source

Unresolved is one in-window entry Derive would not classify. It is a census item, not an error: the entry's cash movement is real and is carried in [Report.Unattributed], so [Report.NetChange] and [Report.Closing] stay true even when the split is incomplete.

Whether an entry here SHOULD have been classifiable is an accounting judgement made outside this package. What is reported is that this policy could not decide it, and exactly what was missing or in conflict.

Dependencies

Source Files 2