From 4aaa0a0ef099801500e5b984887a724ea17e568c Mon Sep 17 00:00:00 2001 From: ZProger Date: Wed, 2 Sep 2026 02:26:27 +0300 Subject: [PATCH] docs: sync README and DOCS with health version, category delete rules, split transactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /api/health now returns a version field, default categories can be deleted once unused, and split transactions (dividing one purchase across a parent category's subcategories) were fully undocumented — added a dedicated section in DOCS.md and a mention in README's feature list. Also fixed the "update to newer code" command, which pulled a Docker image tag that doesn't exist for locally-built services instead of the actual git repo. --- DOCS.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++------- README.md | 4 ++-- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/DOCS.md b/DOCS.md index 2011d24..3c57447 100644 --- a/DOCS.md +++ b/DOCS.md @@ -116,8 +116,9 @@ string. ### `GET /api/health` -No auth required. Returns `{"status": "ok"}`. Use this to check the backend is up before hitting -anything else. +No auth required. Returns `{"status": "ok", "version": "1.1.0"}` — `version` is the running app's +version, the same value shown in the UI under Settings. Use this to check the backend is up before +hitting anything else. ## Accounts @@ -181,7 +182,7 @@ though they can be renamed, recolored, and reordered. | `GET` | `/categories` | List categories, sorted by `sort_order`. `?kind=income` or `?kind=expense` to filter. | | `POST` | `/categories` | Create a category (or subcategory, via `parent_id`). | | `PATCH` | `/categories/{id}` | Update a category (partial). | -| `DELETE` | `/categories/{id}` | Delete a category. Fails with `400` if it's a default category. | +| `DELETE` | `/categories/{id}` | Delete a category. For a default (`is_default: true`) category, fails with `400` if any transaction or transaction split still points at it — a custom category has no such guard and can always be deleted. | **Create body:** @@ -293,7 +294,7 @@ Field rules, enforced server-side: `kind` if given (an `income` transaction can't use an `expense` category). `transfer_account_id` must be omitted. - `transfer`: `transfer_account_id` is **required** and must differ from `account_id`. - `category_id` must be omitted — transfers aren't categorized. + `category_id` must be omitted — transfers aren't categorized. `splits` aren't valid either. - `amount`: required, `> 0` (transfers/expenses aren't signed negative — direction comes from `type`), up to 14 digits, 2 decimal places. - `description`: required, 1–255 chars. Auto-capitalized server-side (first letter uppercased). @@ -301,13 +302,61 @@ Field rules, enforced server-side: - `notes`: optional, free text. - `date`: required. - `tag_ids`: optional list of existing tag IDs. Unknown IDs return `400`. +- `splits`: optional list of `{"category_id": ..., "amount": ..., "note": ...}` — divides the + transaction across 2+ categories instead of one. See **Splitting a transaction** below. **Update** (`TransactionUpdate`) accepts the same fields, all optional, plus the same -type/category/transfer consistency rules applied to the *effective* (merged) values. `tag_ids`: omit -to leave tags untouched; send (even `[]`) to replace the full tag set. +type/category/transfer/split consistency rules applied to the *effective* (merged) values. `tag_ids`: +omit to leave tags untouched; send (even `[]`) to replace the full tag set. `splits`: omit to leave +splits untouched; send (even `[]`) to replace the full split set — send `[]` together with a +`category_id` to turn a split transaction back into a normal single-category one. **Response** (`TransactionRead`): the input fields plus `id`, and the nested `account` -(`AccountRead`), `category` (`CategoryRead` or `null`), `tags` (`TagRead[]`). +(`AccountRead`), `category` (`CategoryRead` or `null`), `tags` (`TagRead[]`), `splits` +(`TransactionSplitRead[]`, empty for a normal non-split transaction). + +### Splitting a transaction across categories + +A single purchase that covers more than one subcategory of the same parent (a supermarket receipt +that's part "Sweets", part "Alcohol", both under "Groceries") can be recorded as one transaction +divided across those categories instead of several separate transactions: + +```json +{ + "account_id": 1, + "type": "expense", + "amount": 84.20, + "description": "Grocery run", + "date": "2026-08-29", + "splits": [ + { "category_id": 12, "amount": 60.00, "note": "Sweets" }, + { "category_id": 13, "amount": 24.20, "note": "Alcohol" } + ] +} +``` + +Rules, enforced on both create and update (against the row as it would look *after* the change): + +- `category_id` on the transaction itself **must be omitted** when `splits` is given — the split + entries carry the categories instead. +- Not valid on a `transfer` transaction. +- At least 2 entries — a single split is just `category_id` with extra steps and is rejected. +- Each split's `category_id` is required and must be a category of the transaction's own kind + (`income`/`expense`, same check a plain `category_id` gets); `amount` is required (`> 0`, up to 14 + digits/2 decimals); `note` is optional, up to 200 chars. +- **All splits must share the same top-level category** — either the parent itself (an + unspecified-subcategory line) or one of its direct subcategories. Splitting across two unrelated + top-level categories (e.g. part "Groceries", part "Transport") is rejected with `400`, since the + feature exists to divide one purchase's total *within* one category tree, not to record what's + really two separate transactions as one. +- The split amounts **must sum exactly** to the transaction's own `amount`, or the request is + rejected with `400`. + +Aggregation endpoints are split-aware: `/dashboard/summary`'s `spending_by_category` and +`/reports/category-ranking` count a split transaction's amount under every category it actually +touches (rolled up to the shared parent), not just once under a single category — a receipt split +$60/$24.20 between Sweets and Alcohol above contributes exactly those amounts to each subcategory's +total under Groceries. ### Bulk create (`POST /transactions/bulk`) diff --git a/README.md b/README.md index 6913b49..2422fdf 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Aurum treats your financial life as a **system**, not a spreadsheet. ## 🧩 Core Features ### 💰 Cash Flow & Transactions -Log income, expenses, and transfers across as many accounts as you want, organized into categories (with one level of subcategories) plus free-form tags, and searchable. Already have history elsewhere? Import a bank's CSV export in a guided 3-step wizard instead of typing every line by hand. A dedicated Cash Flow view charts income vs. expense month by month. +Log income, expenses, and transfers across as many accounts as you want, organized into categories (with one level of subcategories) plus free-form tags, and searchable. One purchase spanning multiple subcategories of the same parent (a grocery receipt part "Sweets", part "Alcohol") can be recorded as a single split transaction instead of several — every report and chart still counts each share under its own category. Already have history elsewhere? Import a bank's CSV export in a guided 3-step wizard instead of typing every line by hand. A dedicated Cash Flow view charts income vs. expense month by month. ### 📈 Net Worth Engine A live net worth timeline (30 days to all-time) aggregating cash and every manually tracked asset — investments, crypto, real estate, vehicles, precious metals — into one number, with a full breakdown by asset class, by how each asset behaves (income / neutral / drain), and by risk level. @@ -157,7 +157,7 @@ docker compose logs web docker compose down # stop everything, keep your data docker compose up -d # start it again later docker compose down -v # stop AND permanently delete your data — be sure -docker compose pull && docker compose up -d --build # update to newer code +git pull && docker compose up -d --build # update to newer code ``` Your data lives in a Docker named volume (`aurum_pgdata`), not in the repo folder — it survives `docker compose down`, image rebuilds, and `git pull`. It's only gone if you explicitly run `docker compose down -v` or delete the volume yourself. For anything short of that, use the in-app **Settings → Backup & Restore** to export a JSON snapshot of everything before making risky changes.