Documentation
¶
Index ¶
- func RevenueCrossCheck(ctx context.Context, reportsRepo data.ReportsQuerier, ...) error
- type ArchetypeKind
- type AssetSpec
- type BalanceSheetResult
- type BranchSpec
- type Client
- func (c *Client) DeleteForm(path string, values url.Values) (*http.Response, error)
- func (c *Client) Get(path string) (*http.Response, error)
- func (c *Client) Login(email, password string) error
- func (c *Client) PostForm(path string, values url.Values) (*http.Response, error)
- func (c *Client) PutForm(path string, values url.Values) (*http.Response, error)
- func (c *Client) Register(name, email, password string) error
- type DemoStats
- type DiscountSpec
- type ExpenseProfile
- type Harness
- func (h *Harness) AccountingRepo() data.AccountingRepoer
- func (h *Harness) BranchesByCompany(ctx context.Context, companyID uuid.UUID) ([]model.Branch, error)
- func (h *Harness) Close() error
- func (h *Harness) CompanyByOwnerEmail(ctx context.Context, ownerEmail string) (*model.Company, error)
- func (h *Harness) CompanyByOwnerEmailAndName(ctx context.Context, ownerEmail, name string) (*model.Company, error)
- func (h *Harness) NewClient() (*Client, error)
- func (h *Harness) OpenOrderBySession(ctx context.Context, sessionID uuid.UUID) (*model.Order, error)
- func (h *Harness) OrderItemsByOrder(ctx context.Context, orderID uuid.UUID) ([]model.OrderItem, error)
- func (h *Harness) ProductBySKU(ctx context.Context, companyID uuid.UUID, sku string) (*model.Product, error)
- func (h *Harness) ReportsRepo() data.ReportsQuerier
- func (h *Harness) SelectableDiscounts(ctx context.Context, companyID uuid.UUID, at time.Time) ([]model.Discount, error)
- func (h *Harness) StationsByCompany(ctx context.Context, companyID uuid.UUID) ([]model.POSStation, error)
- type HarnessOption
- type Persona
- func BankruptBusiness() Persona
- func BoomingBusiness() Persona
- func FnBCafe() Persona
- func GrowingSmallShopWithStaff() Persona
- func KopiSelasar() Persona
- func MultiChain() Persona
- func SoloRetailShop() Persona
- func SteadyGrowthBusiness() Persona
- func ThreeYearVeteran() Persona
- func TokoMakmurJaya() Persona
- type ProductSpec
- type RecipeComponentSpec
- type Result
- type Staff
- type SyncEnqueuer
- type TrajectoryFn
- type TrialBalanceResult
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RevenueCrossCheck ¶
func RevenueCrossCheck( ctx context.Context, reportsRepo data.ReportsQuerier, balances []model.AccountBalance, companyID uuid.UUID, from, to string, ) error
RevenueCrossCheck compares ledger-recorded net revenue against operationally-recorded net revenue computed independently from the raw orders/order_returns tables via data.ReportsQuerier.SalesTotals. These two code paths share no logic, so agreement is a real correctness signal, not a tautology.
"Net" here means post-discount, post-return, matching model.SalesSummary.NetRevenue exactly: accounting.Service.postSale (accounting/service.go) always credits 4000 Sales Revenue at gross and separately debits 4100 Sales Discounts for any discount — so the ledger's net revenue is (4000 credit total) - (4000 debit total, from OrderReturned's direct reversal) - (4100 debit total). Comparing against SalesTotals.TotalRevenue (which excludes returns) would be wrong once a persona uses returns; NetRevenue is the correct counterpart.
Types ¶
type ArchetypeKind ¶
type ArchetypeKind int
ArchetypeKind distinguishes retail (Simple products only) from F&B (Recipe products + Ingredient stock, exercising the recipe-cost/COGS path). Round 1 only implements Retail personas.
const ( ArchetypeRetail ArchetypeKind = iota ArchetypeFnB )
type AssetSpec ¶
type AssetSpec struct {
Name string
Category string
Cost int64 // subunits
Salvage int64 // subunits
UsefulLifeMonths int64
}
AssetSpec describes one fixed asset a persona registers at the start of its history.
type BalanceSheetResult ¶
BalanceSheetResult sums lifetime account balances by model.AccountType. Written fresh here rather than importing view/accounting/helpers.go's RollUp, which is presentation layer and shouldn't be a harness dependency.
func BalanceSheet ¶
func BalanceSheet(balances []model.AccountBalance) BalanceSheetResult
BalanceSheet classifies balances (as returned by TrialBalance) by account type. Asset accounts are debit-normal (balance = debit - credit); liability and equity accounts are credit-normal (balance = credit - debit).
func (BalanceSheetResult) Balanced ¶
func (r BalanceSheetResult) Balanced() bool
Balanced reports the fundamental accounting equation: Assets == Liabilities + Equity.
type BranchSpec ¶
type BranchSpec struct {
Name string
Address string
Stations []string
// Weight controls this branch's share of daily order traffic relative
// to the persona's other branches (largest-remainder split, ported from
// internal/seeddata/history.go's distributeOrdersAcrossStations). Zero
// defaults to 1 — a single-branch persona can leave this unset.
Weight float64
}
BranchSpec describes one branch and the stations inside it.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client drives the web app the way a real browser would: it persists cookies across requests (auth + CSRF), follows redirects, and attaches the CSRF token and _method override transparently so callers never touch either. Each Client is one logged-in actor — use Harness.NewClient for a second staff member acting concurrently in the same simulated business.
func (*Client) DeleteForm ¶
DeleteForm is PutForm's DELETE counterpart.
func (*Client) Get ¶
Get issues a GET request, following redirects. There's no per-call cancellation to propagate here: the transport is in-process (inprocessTransport.RoundTrip calls echo.ServeHTTP directly, no real socket), and every real unit of work underneath it (SQLite queries) is already context-aware at the repo layer.
func (*Client) Login ¶
Login submits POST /login. See Register's doc comment for why the auth cookie, not the response status/body, is the success signal.
func (*Client) PostForm ¶
PostForm submits values as application/x-www-form-urlencoded, attaching the CSRF token automatically. Redirects are followed by the underlying http.Client (converting POST to GET on 301/302/303, per net/http's default policy), so the returned response is the final page in the chain.
func (*Client) PutForm ¶
PutForm simulates a PUT via the app's _method override (core/app.go's MethodOverrideWithConfig) — real HTML forms can't submit PUT/DELETE, so every "edit" route in this app is actually POST with _method=PUT.
func (*Client) Register ¶
Register submits POST /register. Returns an error if the app didn't set the auth cookie (webhandler.AuthCookieName) — the reliable signal of success, independent of which page the post-register redirect chain lands on (varies: /setup/companies/new for a fresh owner, / otherwise).
type DemoStats ¶
DemoStats summarizes what SeedDemo generated.
func SeedDemo ¶
func SeedDemo( ctx context.Context, h *Harness, out io.Writer, seed int64, scale float64, ) (DemoStats, error)
SeedDemo populates h with the two public demo businesses documented in README.md — Toko Makmur Jaya (retail) and Kopi Selasar (coffee shop) — both owned by the same person, matching the real demo exactly. seed controls RNG reproducibility (offset by 1 for the second business, so a single --seed flag still determines the whole run); scale multiplies both businesses' daily order volume on top of their own baseline (see TokoMakmurJaya/KopiSelasar's doc comments for why that baseline was lowered from the old seeder's literal 100/day).
After both businesses are generated, SeedDemo verifies their books balance (TrialBalance, BalanceSheet, RevenueCrossCheck) before returning successfully — the seeded demo image is a real correctness check on the accounting engine, not just a data-population step.
type DiscountSpec ¶
DiscountSpec describes one discount a persona creates via POST /discounts, matching webhandler/discount_handler.go's createDiscount validation: Type must be "fixed" or "percentage". Value is subunits for "fixed" (converted via moneyForm) or a plain percentage integer (1-100) for "percentage".
type ExpenseProfile ¶
type ExpenseProfile struct {
RentPerBranch int64 // subunits, posted on day-of-month 1
UtilitiesPerBranch int64 // subunits, posted on day-of-month 5
PayrollBase int64 // subunits, company-wide, posted on day-of-month 25
MiscChance float64
MiscAmount int64 // subunits
}
ExpenseProfile describes a persona's recurring costs, ported from internal/seeddata/history.go's writeRecurringExpenses shape (fixed recurring entries on specific days-of-month, plus a Bernoulli-triggered irregular one).
type Harness ¶
type Harness struct {
Echo *echo.Echo
Clock *clock.Fake
DB *sql.DB
Client *Client // the owner's client — call Register/Login on it first
// contains filtered or unexported fields
}
Harness wires a full, real okpos app (real Echo router, real middleware — CSRF, auth-cookie parsing, role checks — real SQLite, real accounting engine) against an in-memory database, with two swaps that make it suitable for fast, deterministic simulation instead of a live server: a clock.Fake in place of the system clock, and a SyncEnqueuer in place of the async job queue (see enqueuer.go's doc comment for why).
func NewHarness ¶
NewHarness builds a fresh app pinned to t0. Advance h.Clock to move the simulated "now" forward. By default it uses a throwaway in-memory database and a discarding logger — the right shape for tests and for RunPersona's own use; pass WithDBPath/WithLogger for a real seeding run (see cmd/seed.go) that needs to write to and log against a real database.
func (*Harness) AccountingRepo ¶
func (h *Harness) AccountingRepo() data.AccountingRepoer
AccountingRepo exposes the accounting repo for verification checks.
func (*Harness) BranchesByCompany ¶
func (h *Harness) BranchesByCompany( ctx context.Context, companyID uuid.UUID, ) ([]model.Branch, error)
BranchesByCompany returns every branch for companyID.
func (*Harness) CompanyByOwnerEmail ¶
func (h *Harness) CompanyByOwnerEmail( ctx context.Context, ownerEmail string, ) (*model.Company, error)
CompanyByOwnerEmail finds the (single, in Round 1) company owned by the user with the given email — used right after Register+create-company to discover the handle/ID needed for every subsequent request.
func (*Harness) CompanyByOwnerEmailAndName ¶
func (h *Harness) CompanyByOwnerEmailAndName( ctx context.Context, ownerEmail, name string, ) (*model.Company, error)
CompanyByOwnerEmailAndName finds one of an owner's companies by exact name — needed once an owner has more than one company (see Persona's SharedOwner field), where CompanyByOwnerEmail's "just take the first one" is ambiguous. data.CompanyRepoer.ListByOwner is itself documented as existing because "a user may own multiple companies (pro)".
func (*Harness) NewClient ¶
NewClient returns a second Client (its own cookie jar) against the same running app — used for staff actors, who must log in separately to get correctly company-scoped JWT claims (see webhandler/middleware.go's CompanyMiddleware: an owner is checked via company.OwnerID == claims.UserID, but staff are checked via claims.CompanyID == company.ID, populated at login time — the owner's own client can't act as staff).
func (*Harness) OpenOrderBySession ¶
func (h *Harness) OpenOrderBySession( ctx context.Context, sessionID uuid.UUID, ) (*model.Order, error)
OpenOrderBySession returns the still-open order for sessionID — used to read back a real, server-computed total after applying a discount, since percentage-discount rounding is the server's business, not something a caller should recompute independently.
func (*Harness) OrderItemsByOrder ¶
func (h *Harness) OrderItemsByOrder( ctx context.Context, orderID uuid.UUID, ) ([]model.OrderItem, error)
OrderItemsByOrder returns every line item on orderID.
func (*Harness) ProductBySKU ¶
func (h *Harness) ProductBySKU( ctx context.Context, companyID uuid.UUID, sku string, ) (*model.Product, error)
ProductBySKU looks up a product the runner itself created, by the deterministic SKU it assigned — the same lookup production's barcode-scan flow uses (data.ProductRepoer.GetBySKU).
func (*Harness) ReportsRepo ¶
func (h *Harness) ReportsRepo() data.ReportsQuerier
ReportsRepo exposes the reports repo for verification cross-checks.
func (*Harness) SelectableDiscounts ¶
func (h *Harness) SelectableDiscounts( ctx context.Context, companyID uuid.UUID, at time.Time, ) ([]model.Discount, error)
SelectableDiscounts returns companyID's active, uncoded, currently-valid discounts as of at — the same query the real POS discount dropdown uses (data.DiscountRepoer.ListSelectable) — used by the runner to resolve a just-created DiscountSpec's name to its server-assigned ID.
func (*Harness) StationsByCompany ¶
func (h *Harness) StationsByCompany( ctx context.Context, companyID uuid.UUID, ) ([]model.POSStation, error)
StationsByCompany returns every POS station for companyID.
type HarnessOption ¶
type HarnessOption func(*harnessSettings)
HarnessOption configures NewHarness for callers beyond the default in-memory test setup — currently just production/CLI seeding (cmd/seed.go), which needs a real file-backed database and a real logger instead of the fast, throwaway defaults every e2e test wants.
func WithDBPath ¶
func WithDBPath(path string) HarnessOption
WithDBPath points the harness at a real file-backed SQLite database (via repo.NewSQLiteDB, which configures WAL + busy_timeout + foreign_keys for a file path) instead of the default unique shared-cache in-memory database. Leave unset for tests.
func WithLogger ¶
func WithLogger(l *slog.Logger) HarnessOption
WithLogger routes the harness's internal logging (and, via slog.SetDefault, webhandler's package-level error logging — see the slog.SetDefault call below) through l instead of discarding it. Leave unset for tests.
type Persona ¶
type Persona struct {
Name string // used as the company name; handle is auto-slugified
Archetype ArchetypeKind
Currency string
OwnerName string
OwnerEmail string
OwnerPassword string
// StartOffset/Duration place the persona's history on the timeline
// ending at the Harness's current clock value ("now"): history starts
// StartOffset before now and runs for Duration.
StartOffset time.Duration
Duration time.Duration
Branches []BranchSpec
Staff []Staff // beyond the owner; nil is valid (solo owner)
Products []ProductSpec
AvgOrdersPerDayStart float64
Trajectory TrajectoryFn
ProcurementCycleDays int
ExpenseProfile ExpenseProfile
Discounts []DiscountSpec // created via POST /discounts before the day loop
DiscountRate float64 // fraction of orders that get a random discount from Discounts
EnableAssetTracking bool
EnableAdvancedAccounting bool
FixedAssets []AssetSpec
// Client to reuse instead of registering a fresh one — lets a second
// persona create a second company under the same real-world owner (e.g.
// one person running two businesses), matching data.CompanyRepoer's
// documented "a user may own multiple companies" support. Nil (default)
// preserves today's behavior: RunPersona registers a brand-new owner.
SharedOwner *Client
// Scale multiplies AvgOrdersPerDayStart, mirroring
// internal/seeddata.Config.Scale — the volume knob that keeps a
// multi-month/year persona's e2e run fast.
Scale float64
// RNGSeed seeds the persona's own deterministic RNG (order timing, item
// selection, noise) — same seed always produces the same simulated
// history.
RNGSeed int64
}
Persona declaratively describes a fictional business: its structure (branches, staff, catalog), its financial behavior (expenses, procurement, discounts), and the shape of its history over time (Trajectory). RunPersona drives every field below entirely through real HTTP requests against a Harness.
func BankruptBusiness ¶
func BankruptBusiness() Persona
BankruptBusiness proves the ledger stays internally consistent even when the business it describes is failing: revenue plateaus for the first 40% of the year then collapses toward near-zero (CliffTrajectory), while rent and utilities stay FIXED — expenses don't decline just because revenue did, which is exactly what drives a real business into the ground. The accounting invariants (trial balance, balance sheet) don't require a healthy business, only a correctly-recorded one; this persona is the proof that a collapsing cash position (even a negative one, since nothing stops the ledger's Cash account from going negative — there's no "insufficient funds" check on recording an expense) still balances.
func BoomingBusiness ¶
func BoomingBusiness() Persona
BoomingBusiness is the opposite of BankruptBusiness: explosive growth over a short window (9 months) rather than a slow climb — two branches from day one (rapid expansion, not one branch growing in place) and a team hired up front to handle the scale-up, with volume growing more than 10x from start to end.
func FnBCafe ¶
func FnBCafe() Persona
FnBCafe proves the recipe/ingredient path end-to-end: a solo-owner coffee shop where every drink sold deducts ingredient stock (not its own stock — recipe products have none) and posts COGS computed from the recipe's components, not a flat per-product cost. Single branch/station and no staff, deliberately, so this persona isolates the recipe dimension from the multi-branch/multi-staff one (see MultiChain).
func GrowingSmallShopWithStaff ¶
func GrowingSmallShopWithStaff() Persona
GrowingSmallShopWithStaff proves three mechanisms SoloRetailShop can't: multi-actor login/claims (a cashier rings up orders under their own session, not the owner's), depreciation idempotency (one fixed asset, run-depreciation invoked monthly across a 6-month history), and discount creation/application (Persona.DiscountRate/Discounts, exercised end to end for the first time anywhere in this suite — see runner.go's createDiscounts/simulateOrder). Real upward Trajectory — first persona to actually tell a growth story.
func KopiSelasar ¶
func KopiSelasar() Persona
KopiSelasar ports the "Kopi Selasar" demo coffee-shop business from internal/seeddata/setup.go+catalog.go+assets.go verbatim — including all 8 recipes (the docs site says "six," but the code (ground truth) has always defined 8; this port follows the code). SharedOwner is left unset here — SeedDemo sets it to the already-registered Toko Makmur Jaya owner before running this persona, since both demo businesses belong to the same real-world owner.
func MultiChain ¶
func MultiChain() Persona
MultiChain proves branch-weighted traffic distribution and multi-staff station rotation: three branches with different traffic shares, several staff across all three non-owner roles (including one "inventory" role staffer, who must be excluded from POS station rotation entirely — see runner.go's assignStationActors). Retail archetype, kept separate from FnBCafe so this persona isolates the multi-branch/multi-staff dimension.
func SoloRetailShop ¶
func SoloRetailShop() Persona
SoloRetailShop is Round 1's minimum viable proof persona: a ~2.5-month-old single-branch, single-station retail shop with no staff at all — the owner does everything. Retail archetype (no recipes), gentle flat-ish trajectory, no fixed assets — keeps the first proof free of the multi-actor and depreciation dimensions covered by GrowingSmallShopWithStaff.
func SteadyGrowthBusiness ¶
func SteadyGrowthBusiness() Persona
SteadyGrowthBusiness grows consistently month over month for a year and a half — LinearGrowthTrajectory more than doubles daily volume from start to end, smoothly rather than via a dramatic event, and a cashier is hired to help handle the growing load.
func ThreeYearVeteran ¶
func ThreeYearVeteran() Persona
ThreeYearVeteran is a mature, long-running business: three years of history, a brief early ramp then a long flat plateau (an established customer base, not a startup), one staff member, and one fixed asset with a long useful life — long enough that run-depreciation has to catch up many periods across the full three-year span, not just one or two.
func TokoMakmurJaya ¶
func TokoMakmurJaya() Persona
TokoMakmurJaya ports the "Toko Makmur Jaya" demo retail business from internal/seeddata/setup.go+catalog.go+assets.go verbatim (names, logins, catalog, discounts, fixed asset) — everything documented in README.md and the docs site. AvgOrdersPerDayStart is deliberately lower than the old seeder's literal 100/day: at that volume, this engine's real HTTP+ accounting round-trips per order would take 20-30+ minutes for 2 years of history, vs. the old raw-SQL seeder's few seconds. ~18/day keeps --scale 1.0 (demo.Dockerfile's default) in the low single-digit minutes while still producing a substantial multi-year history.
type ProductSpec ¶
type ProductSpec struct {
Name string
SKU string
Type model.ProductType
BasePrice int64 // subunits; must be 0 for Ingredient
Unit string
InitialQuantity float64
InitialUnitCost int64 // subunits
Weight float64 // selection weight among sellable products
Category string // optional; sent as the "categories" form field
RecipeComponents []RecipeComponentSpec
}
ProductSpec describes one product in a persona's catalog. SKU is persona-assigned and deterministic so the runner can add it to an order via POST /pos/session/:id/scan (sku=...), exactly like a real barcode scanner, without any ID lookup.
type RecipeComponentSpec ¶
RecipeComponentSpec references an ingredient by the SKU another ProductSpec in the same Persona was given.
type Result ¶
type Result struct {
Sessions int
Orders int
OrderItems int
Payments int
Expenses int
ProcurementRuns int
DepreciationRuns int
}
Result summarizes what RunPersona did — used both as a coarse sanity check ("Orders > 0") and, later, as the input to per-persona narrative assertions.
func RunPersona ¶
RunPersona drives h through p's entire lifecycle — registration, company setup, branches/stations/staff/catalog, then a day-by-day simulated history — entirely via real HTTP requests against h.Echo. Every journal entry that results comes from accounting.Service.Handle, triggered by the real event-emission path (see enqueuer.go), not from any duplicated posting logic in this package. p is taken by value (not *Persona) deliberately: every persona builder (SoloRetailShop, TokoMakmurJaya, etc.) returns a Persona by value, and RunPersona is called exactly once per simulated business, not in a hot loop, so the copy is a one-time cost, not a perf concern.
type Staff ¶
Staff is one non-owner actor in a persona (beyond the owner, who always exists). Role must be "manager", "cashier", or "inventory" — the only roles POST /settings/users accepts (owner accounts are never company-scoped).
type SyncEnqueuer ¶
type SyncEnqueuer struct {
// contains filtered or unexported fields
}
SyncEnqueuer is a data.Enqueuer that processes accounting jobs synchronously, in the same call, instead of production's SQLite-backed queue drained by a background goroutine polling every 500ms (repo.NewGoqiteEnqueuer + worker.NewRunner). A simulation needs to see ledger state change the instant an HTTP request returns — sleeping past production's poll interval would be slow and still nondeterministic.
This swaps only the transport: core/dispatcher.go's event-name-to-job mapping is untouched, and worker.JobAccounting jobs are handled by calling accounting.Service.Handle directly — the exact same production code path, just invoked inline instead of via goqite+JSON. The payload still gets a JSON marshal/unmarshal round trip before Handle sees it (see normalizeJobPayload) — skipping that round trip was tried first and is NOT safe: domain services put typed Go values (uuid.UUID, int64) straight into event.Payload, and accounting.Service's payload-parsing helpers expect the JSON-native shapes (string, float64) those values become after a real marshal/unmarshal — e.g. a raw uuid.UUID in the map fails a map[string]any type assertion to string and gets silently treated as "invalid company_id". Round-tripping through JSON costs microseconds and guarantees this enqueuer matches production byte-for-byte.
func NewSyncEnqueuer ¶
func NewSyncEnqueuer(svc *accounting.Service, log *slog.Logger) *SyncEnqueuer
NewSyncEnqueuer returns a SyncEnqueuer that posts accounting jobs via svc.
func (*SyncEnqueuer) Enqueue ¶
func (e *SyncEnqueuer) Enqueue(ctx context.Context, job data.BackgroundJob) error
Enqueue implements data.Enqueuer.
type TrajectoryFn ¶
TrajectoryFn returns a multiplier (typically centered around 1.0) for a given zero-based day index into the persona's timeline. It generalizes internal/seeddata/history.go's hardcoded `ramp := min(1, i/60.0)` (monotonic ramp-up only) into a pluggable per-persona curve — this is the seam Round 2's growth/decline/plateau/boom/bankruptcy personas plug into without any engine changes. The runner multiplies this into the existing weekday/noise shaping, so a TrajectoryFn only needs to describe the overall trend.
func CliffTrajectory ¶
func CliffTrajectory(plateauMultiplier, cliffFraction, floorMultiplier float64) TrajectoryFn
CliffTrajectory holds steady at plateauMultiplier until cliffFraction of the timeline has elapsed, then declines linearly to floorMultiplier by the end — models a business that looks healthy and then collapses, rather than one that was always declining (LinearGrowthTrajectory with a high start and low end gives a steady decline instead; use that for a "slow bleed" persona and this one for a "sudden bankruptcy" persona).
func FlatTrajectory ¶
func FlatTrajectory(rampDays int) TrajectoryFn
FlatTrajectory ramps gently up over rampDays then holds steady — the default for a persona that isn't trying to tell a growth/decline story.
func LinearGrowthTrajectory ¶
func LinearGrowthTrajectory(startMultiplier, endMultiplier float64) TrajectoryFn
LinearGrowthTrajectory ramps from startMultiplier to endMultiplier evenly across the whole timeline. Passing a startMultiplier greater than endMultiplier produces a steady decline rather than growth.
type TrialBalanceResult ¶
type TrialBalanceResult struct {
Balances []model.AccountBalance
TotalDebit int64
TotalCredit int64
}
TrialBalanceResult holds the global "books balance" check: no such check exists anywhere else in the codebase today — AccountingRepoer.BalanceByAccount (repo/accounting_repo.go) returns per-account rows only.
func TrialBalance ¶
func TrialBalance( ctx context.Context, accountingRepo data.AccountingRepoer, companyID uuid.UUID, ) (*TrialBalanceResult, error)
TrialBalance sums every account's lifetime debit/credit totals for companyID.
func (*TrialBalanceResult) Balanced ¶
func (r *TrialBalanceResult) Balanced() bool
Balanced reports whether every debit is matched by a credit, company-wide.