Skip to main content
Glama
GuoHaoyu1688

commission-cashflow-ledger

by GuoHaoyu1688

Commission Cashflow Ledger

An offline ledger for commission-based work. Track contracts and customer collections, calculate traceable commissions, and forecast personal cash flow without treating accrued commissions as money already received.

Python 3.11+ · SQLite · CLI · MCP stdio · local Web dashboard · MIT

Local dashboard with fictional data

Install

git clone https://github.com/GuoHaoyu1688/commission-cashflow-ledger.git
cd commission-cashflow-ledger
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e .

Dependencies are downloaded during installation. All application operations then run locally, with no API keys, cloud services, bank connections, telemetry, or runtime network calls. The dashboard binds to loopback only.

Related MCP server: financial-agent

Three-command quick start

After installation, run these from the repository root. All included contracts and customers are fictional; the fixed reporting date makes the example reproducible.

ledger --config examples/config.yaml import examples/ledger.json --json
ledger --config examples/config.yaml commission calc --month 2026-09 --json
ledger --config examples/config.yaml report --month 2026-09 --as-of 2026-09-30 --pdf --json

The import inserts 13 records and safely skips them on a repeat run. Reports appear in data/demo/reports/2026-09.md, a four-page PDF, and three PNG charts. Expected September figures:

Measure

CNY

Customer collections

650,000.00

Gross commission accrual

20,500.00

Flat withholding estimate (10%)

2,050.00

Net commission accrual

18,450.00

Actual commission payout

1,000.00

Other actual personal income

5,000.00

Actual personal expenses

2,000.00

Actual personal net cash

4,000.00

The arithmetic is 50,000 × 3% + 500,000 × 3% + 100,000 × 4% = 20,500. Net accrual is 20,500 − 2,050 = 18,450. Personal net cash is 1,000 + 5,000 − 2,000 = 4,000. The tests assert these figures independently.

Start the dashboard:

ledger --config examples/config.yaml serve --port 8767

Open http://127.0.0.1:8767. Browse or search deals, inspect installment and receipt timelines, record a receipt, compare forecast scenarios, and preview monthly reports. Use As of 2026-09-30, First month 2026-09, and Opening balance 1000 to reproduce the sample forecast. Responsive desktop and phone layouts use local HTML/CSS/JS with no build step or CDN. The JSON API schema is available at /openapi.json; no external documentation assets load.

Commission rules

The first invocation creates data/ledger.db and data/rules.yaml. Rules are read afresh on every calculation. The engine supports receipt or contract basis, progressive tiers, per-deal overrides, a lifetime floor/cap, configurable estimated withholding, and a whole-month payout delay.

version: 1
default:
  basis: receipts
  rate: '0.03'
  withholding_rate: '0'
  payout_delay_months: 0
  floor: '0'
  cap: null
overrides:
  D-2026-002:
    tiers:
      - up_to: '500000'
        rate: '0.03'
      - up_to: null
        rate: '0.04'
  D-2026-003:
    basis: contract
    rate: '0.02'
    cap: '5000'

Rates are fractions (0.03 means 3%). Quote money and rates in YAML. The final tier must be unlimited (up_to: null). Override values inherit defaults; a per-deal rate replaces inherited tiers. A deal's override_rate takes precedence over YAML tiers/rates, retaining the basis, floor, cap, and withholding settings.

Calculation conventions are explicit:

  • Tiers, floor, and cap apply to each deal's lifetime cumulative basis, in its own currency. Tiers are progressive, never retroactive flat rates. A 700,000 collection earns 500,000 × 3% + 200,000 × 4% = 23,000.

  • Gross cumulative commission rounds to cents using ROUND_HALF_UP, then applies floor/cap. Estimated cumulative withholding also rounds half up. Each receipt's accrual is the difference between consecutive cumulative totals, so split receipts reconcile exactly.

  • Two 100,000 receipts at 3% with a 5,000 lifetime cap earn 3,000 then 2,000. A 500 lifetime floor with collections of 5,000, 5,000, and 20,000 earns 500, 0, and 400. A zero cumulative basis earns zero, including after a full reversal.

  • At 3%, two receipts of 0.50 earn 0.02 then 0.01, totaling 0.03. The floor applies as soon as the basis becomes positive; it is not a guaranteed payment.

  • Contract-basis commission accrues once on the signature date for signed/active/settled deals. Receipt-basis commission follows actual collections. Negotiating/cancelled contracts do not create contract-basis accruals or future collection forecasts. Existing receipt-basis accruals remain visible after cancellation.

  • Receipts cannot be future-dated or exceed the contract's lifetime net amount at any chronological point. Overpayments are rejected; no silent clipping occurs. A reversal creates a linked negative receipt, never edits history, cannot predate or exceed the unreversed original, and requires --yes. Reversals recalculate negative commission deltas; they never reverse an actual personal payout automatically.

Every saved accrual carries a receipt/contract reference, effective rate, currency, cumulative basis, normalized rule, and SHA-256 rule snapshot. SQLite preserves the YAML snapshot and calculation differences. commission calc --month recalculates all history atomically, then filters the returned month. Read-only --dry-run previews changes. Editing rules restates history intentionally; old snapshots and before/after amounts remain in commission history. Back up before changes.

See the Python Decimal reference for exact decimal arithmetic and rounding semantics. The commission rules themselves are configurable product assumptions, not statutory formulas.

Personal cash and forecast assumptions

Customer receipts, accrued commissions, and personal cash are three separate records. Use receipt add for a customer collection. Use cash add --kind income --category commission --deal CODE for an actual personal commission payout. Other actual cash income and expenses use cash add with a category. Recurring records are schedules, not evidence of payment.

Forecasts default to six months. Optimistic/base/conservative scenarios delay remaining planned collections by 0/1/3 calendar months. Customer receipts are allocated FIFO to the earliest installments; a reversal reopens those installment balances. There is no invoice-specific receipt allocation in v0.1.0.

Unpaid net accrual is expected at the later of the as-of date or the last accrual date plus the configured payout delay. Future collection-based commission follows the delayed collection date plus payout delay. Payouts exceeding current accrual generate a warning and offset subsequent planned commissions; no automatic cash clawback occurs. Unplanned contract balances are reported as data gaps and excluded from collection-based forecasts.

Recurring schedules support monthly, quarterly, and once. Dates retain their original day, clamped at month end (January 31 → February 28 → March 31). Link recorded cash to --recurring-id in the occurrence month to reduce that month's estimate. Unpaid current-month schedules roll forward to the as-of date; past months show recorded cash only. Cross-month recurring settlements require a manual schedule adjustment. Schedules do not auto-create cash records.

Opening balance means the balance immediately before the first displayed month, not today's balance. Historical months use recorded cash; the current/future months add estimates. As-of filtering uses transaction dates, not a reconstruction of when the information was first known. Forecasts are planning assumptions, not guaranteed payment dates or investment/tax advice.

CLI reference

All commands are noninteractive. Global flags may appear before or after the command: --data-dir DIR, --config YAML, --json, --quiet, --progress, --version. Small queries return one JSON object. --progress logs to stderr; the first chart render may take longer while Matplotlib initializes its font cache.

Command

Main arguments

init

Initialize the database and rules

deal add

--code --client-code --signed-date --amount [--currency CNY] [--status signed] [--tag TAG] [--override-rate 0.04]

deal list / deal show CODE

Show deals / installment allocation and receipt timeline; show accepts --as-of

deal update CODE

--status active, --override-rate 0.04, or --clear-override

plan add

--deal CODE --due-date DATE and either --amount or --proportion 0.5

receipt add

--deal CODE --date DATE --amount MONEY [--note TEXT] [--idempotency-key KEY]

receipt reverse ID

--date DATE [--amount MONEY] --yes; omit amount for remaining full reversal

commission calc

[--month YYYY-MM] [--dry-run]

commission history

Stored before/after recalculation differences

recurring add / recurring list

--name --kind income|expense --amount --freq monthly|quarterly|once --start [--end] [--currency]

cash add / cash list

--date --kind income|expense --amount [--currency] [--category] [--note] [--deal] [--recurring-id]

cashflow

[--months 6] [--scenario base] [--start YYYY-MM] [--as-of DATE] [--opening-balance MONEY]

alerts

[--months 6] [--scenario base] [--as-of DATE]

report

--month YYYY-MM [--pdf] [--out-dir PATH] [--as-of DATE]

import FILE

JSON/CSV; optional --table when records omit record_type

export

--format json|csv --out PATH [--table TABLE] [--utf8]

backup

[--out PATH]; default dated ZIP in data/backups/

serve

[--host 127.0.0.1] [--port 8767]; loopback only

mcp

Official SDK stdio server

Example actual payout:

ledger cash add --date 2026-09-25 --kind income --amount 1000 --currency CNY --category commission --deal D-2026-001 --json

Success: {"ok":true,"data":...,"warnings":[]}. Failure: {"ok":false,"error":{"code":"INVALID_ARGUMENT","message":"..."}}. Exit codes: 0 success, 1 I/O/database failure, 2 invalid arguments/conflicting data, 3 not found. No stdout logging accompanies JSON. MCP stdout contains protocol messages only.

Configuration, storage, and interchange

Precedence: CLI data directory → LEDGER_DATA_DIR → YAML setting → ./data. --config overrides LEDGER_CONFIG. Relative paths inside a YAML config resolve beside that config; CLI/environment paths resolve from the working directory. Example config:

data_dir: ./data
rules_path: ./rules.yaml
timezone: Asia/Shanghai
base_currency: CNY
opening_balance: '1000.00'
fx_as_of: '2026-09-30'
fx_rates:
  CNY: '1'
  USD: '7.00'  # One USD expressed in CNY; supplied manually.

Amounts are integer cents in SQLite and JSON output; input --amount accepts major-unit decimal strings with at most two fractional digits. All supported currencies use two decimal places; zero- and three-decimal currency conventions are outside this version. Mixed-currency commissions remain grouped by currency; reports/forecasts convert using the explicit manual table. Missing FX or a missing fx_as_of fails closed. No current FX rate is implied.

The primary import/export tables are deals, payment_plans, receipts, recurring, and cash_entries. JSON is a list of objects; CSV uses the same fields as columns, with tags encoded as a JSON array. Every row requires record_type unless --table supplies it. Supply either integer amount_cents or a major-unit string amount. Foreign keys accept the deal UUID or its code. Examples:

[
  {"record_type":"deals","id":"fictional-deal","code":"D-2026-003","client_code":"CLIENT-C","signed_date":"2026-09-01","amount":"1000.00","currency":"CNY","status":"signed","tags":[]},
  {"record_type":"receipts","id":"fictional-receipt","deal_id":"D-2026-003","date":"2026-09-02","amount":"500.00","note":"Fictional collection"}
]

Import is transactional, validates every record, deduplicates identical IDs, and rejects conflicting IDs. Missing IDs are stable content hashes. Same receipt key or import is retry-safe; ordinary add commands create new records. CSV export defaults to UTF-8 BOM; --utf8 omits it. Treat CSV identifiers as text when opening in spreadsheet software. Derived commission snapshots and calculation history are preserved in backup, not the primary-record interchange export.

A backup ZIP contains a consistent SQLite online backup, the current rules YAML, and settings JSON. For a full restore, stop ledger processes, extract into a fresh directory, convert settings.json to YAML or use its values in a config, and point data_dir and rules_path to that directory. Do not overwrite a live database. Reports use deterministic month filenames and overwrite prior reports for that month; back up before changing rules or replacing outputs.

MCP

{
  "mcpServers": {
    "commission-ledger": {
      "command": "/workspace/apps/commission-cashflow-ledger/.venv/bin/ledger",
      "args": ["--data-dir", "/workspace/data/commission-cashflow-ledger", "mcp"]
    }
  }
}

Tools: add_receipt(deal_code, date, amount, note="", idempotency_key=null), get_commission_summary(period=null), get_cashflow_forecast(months=6, scenario="base", start=null, as_of=null, opening_balance=null), and list_alerts(months=6, scenario="base", as_of=null). The resource ledger://rules exposes the current YAML. Monetary input is a decimal string; outputs use integer cents. Tool schemas are generated by the official SDK. Summary/forecast/alerts are read-only; add_receipt writes to SQLite and supports retry keys. Errors are MCP tool errors. See AGENTS.md for headless workflows.

Tests, FAQ, and roadmap

python -m pip install -e '.[dev]'
ruff check .
ruff format --check .
pytest -q --cov=commission_cashflow_ledger --cov-report=term-missing

CI runs Python 3.11 and 3.12 on Ubuntu. Tests cover manual arithmetic, rule edits and snapshots, receipt reversals and concurrency, forecasts, import/export and backup, CLI errors, REST, every MCP tool, and a real stdio handshake.

Why is my forecast higher than actual cash? It includes unpaid accrued commissions and future planned income. Inspect the actual and projected columns separately.

Why did recalculation change a past month? The current YAML intentionally restates commission history. Inspect commission history and the preserved rule snapshots. Contract cancellation can remove contract-basis accruals; it does not erase actual cash.

How is withholding calculated? It is only your configured flat rate against cumulative gross commission. There are no jurisdictional tax brackets, allowances, social contributions, or payroll integrations. This is an estimate and is not tax, legal, or financial advice.

Can I expose the dashboard remotely? The supported server is local-only, without authentication. Keep it on the same trusted machine; no multi-user access control is provided.

What is next? Explicit receipt-to-invoice allocation, more flexible payroll timing, cross-month recurring settlement matching, optional encrypted backups, and a reviewed migration path for future schema versions.

MIT license. See LICENSE and CHANGELOG.md.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables interaction with local Beancount accounting ledgers through structured tools for viewing accounts, balances, and transactions, as well as inserting/removing transactions and answering natural-language questions via BeanQuery. Provides deterministic, validated, and auditable financial data operations with offline-first functionality.
    1
    -
  • A
    license
    B
    quality
    C
    maintenance
    Enables AI agents to manage personal finances with deterministic, grounded tools for cash-flow projection, drift detection, reconciliation, and Todoist integration. All figures trace back to a local SQLite database.
    55
    MIT
  • A
    license
    B
    quality
    A
    maintenance
    Enables local-first personal finance management through deterministic tools for importing, categorizing, and analyzing bank transactions.
    36
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Talk to your GnuCash books: a one-call financial dashboard (net worth, runway, budgets, what's overdue), and whole bank statements entered and reconciled in a single call from CSV, OFX, PDF, or even a screenshot or photo of the statement. Plus transactions, invoices, reports and multi-currency; local, SQLite/PostgreSQL/MySQL.
    87
    77
    MIT