{ "info": { "name": "General Ledger Service", "description": "Full request catalog for every endpoint in the General Ledger Service API (v1) - success and failure variations. Uses collection variables {{baseUrl}} and {{apiKey}}; every request (except /health, /swagger, /dev-tools/generate-api-key) sends X-Api-Key automatically via the collection-level auth/header.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "variable": [ { "key": "baseUrl", "value": "https://localhost:7024/api/v1" }, { "key": "apiKey", "value": "dev-only-api-key-please-change-me" } ], "auth": { "type": "apikey", "apikey": [ { "key": "key", "value": "X-Api-Key" }, { "key": "value", "value": "{{apiKey}}" }, { "key": "in", "value": "header" } ] }, "item": [ { "name": "Accounts", "item": [ { "name": "Create Account - success (root)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"Test Doc Account Root\",\n \"accountTypeId\": 1,\n \"isControlAccount\": false,\n \"isPostable\": true,\n \"currencyCode\": \"LKR\"\n}" }, "url": { "raw": "{{baseUrl}}/accounts", "host": [ "{{baseUrl}}" ], "path": [ "accounts" ] }, "description": "201 Created. parentAccountId omitted -> root account. currencyCode omitted would default to LKR (see next request). accountCode is system-generated (retrofit 2026-07-20, CLAUDE.md Rule 8.1) - e.g. 1000 for the first root Asset account; substitute the actual returned value ( below) into the requests that follow." } }, { "name": "Create Account - success (child, currencyCode omitted -> defaults LKR)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"Test Doc Account Child\",\n \"parentAccountId\": 1,\n \"accountTypeId\": 1,\n \"isControlAccount\": false,\n \"isPostable\": true\n}" }, "url": { "raw": "{{baseUrl}}/accounts", "host": [ "{{baseUrl}}" ], "path": [ "accounts" ] }, "description": "201 Created. Depth-guard (COA_MAX_DEPTH/COA_ENFORCE_MAX_DEPTH) runs because parentAccountId is present. accountCode is system-generated as {parentCode}-01 ( below)." } }, { "name": "Create Account - 400 invalid accountTypeId", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"bad type\",\n \"accountTypeId\": 9,\n \"isControlAccount\": false,\n \"isPostable\": true,\n \"currencyCode\": \"LKR\"\n}" }, "url": { "raw": "{{baseUrl}}/accounts", "host": [ "{{baseUrl}}" ], "path": [ "accounts" ] }, "description": "400. Message: \"Invalid accountTypeId '9'. Must be one of: 1 (Asset), 2 (Liability), 3 (Equity), 4 (Income), 5 (Expense).\" New validation added by the 2026-07-20 code-generation retrofit (accountTypeId now has to map to a known LKAS block to generate a root code)." } }, { "name": "Create Account - 404 parentAccountId not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"orphan\",\n \"parentAccountId\": 999999,\n \"accountTypeId\": 1,\n \"isControlAccount\": false,\n \"isPostable\": true,\n \"currencyCode\": \"LKR\"\n}" }, "url": { "raw": "{{baseUrl}}/accounts", "host": [ "{{baseUrl}}" ], "path": [ "accounts" ] }, "description": "404. Message: \"Parent account not found\"." } }, { "name": "Create Account - 400 max COA depth exceeded", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"too deep\",\n \"parentAccountId\": ,\n \"accountTypeId\": 1,\n \"isControlAccount\": false,\n \"isPostable\": true,\n \"currencyCode\": \"LKR\"\n}" }, "url": { "raw": "{{baseUrl}}/accounts", "host": [ "{{baseUrl}}" ], "path": [ "accounts" ] }, "description": "400. Message: \"Maximum Chart of Accounts depth exceeded\". Only fires when resulting depth > system_config COA_MAX_DEPTH AND COA_ENFORCE_MAX_DEPTH=TRUE. Not exercised with a live request here (would require building a chain COA_MAX_DEPTH levels deep first) - included for completeness; see DepthGuard.EnsureWithinMaxDepthAsync in Services/DepthGuard.cs." } }, { "name": "Get Account - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/accounts/", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "" ] }, "description": "200. Returns { account, ancestorBreadcrumb, descendantTree: null }. = the accountCode returned by \"Create Account - success (root)\" above (system-generated, e.g. 1000)." } }, { "name": "Get Account - success (?expand=tree)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/accounts/?expand=tree", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "" ], "query": [ { "key": "expand", "value": "tree" } ] }, "description": "200. Same shape, descendantTree populated with the recursive subtree." } }, { "name": "Get Account - 404 not found", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/accounts/NOTFOUND-XYZ", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "NOTFOUND-XYZ" ] }, "description": "404. Message: \"Account not found\"." } }, { "name": "List Accounts - success (no filters, no paging - full list)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/accounts", "host": [ "{{baseUrl}}" ], "path": [ "accounts" ] }, "description": "200. page/pageSize omitted -> both null in response, items = every matching row (no defaulting to page=1/pageSize=20)." } }, { "name": "List Accounts - success (filters + paging)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/accounts?accountTypeId=1&isPostable=true&isActive=true&page=1&pageSize=2", "host": [ "{{baseUrl}}" ], "path": [ "accounts" ], "query": [ { "key": "accountTypeId", "value": "1" }, { "key": "isPostable", "value": "true" }, { "key": "isActive", "value": "true" }, { "key": "page", "value": "1" }, { "key": "pageSize", "value": "2" } ] }, "description": "200. All four query params optional and independent; page/pageSize only slice when BOTH are supplied." } }, { "name": "Update Account - success (rename)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"Renamed Child\"\n}" }, "url": { "raw": "{{baseUrl}}/accounts/", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "" ] }, "description": "200. Only supplied fields change. parentAccountId not in payload -> audit category ADMINISTRATIVE." } }, { "name": "Update Account - success (re-parent, LEDGER audit)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"parentAccountId\": 9\n}" }, "url": { "raw": "{{baseUrl}}/accounts/", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "" ] }, "description": "200. parentAccountId present and changed -> cycle check + depth check run, audit category LEDGER." } }, { "name": "Update Account - success (clear parent, explicit null)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"parentAccountId\": null\n}" }, "url": { "raw": "{{baseUrl}}/accounts/", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "" ] }, "description": "200. Distinguishing 'field omitted' from 'field explicitly null' matters here - omitting parentAccountId entirely leaves it unchanged; sending null clears it. Handled via ParentAccountIdProvided in AccountService (payload.ContainsKey check)." } }, { "name": "Update Account - 400 self-parent", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"parentAccountId\": 10\n}" }, "url": { "raw": "{{baseUrl}}/accounts/", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "" ] }, "description": "400 (send the child account's own id as its new parentAccountId). Message: \"An account cannot be its own parent\"." } }, { "name": "Update Account - 400 cycle (new parent is a descendant)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"parentAccountId\": 10\n}" }, "url": { "raw": "{{baseUrl}}/accounts/", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "" ] }, "description": "400. Message: \"Cannot re-parent: the new parent is a descendant of this account\". (the root account is currently an ancestor of the child account; trying to make the child its parent would create a cycle.)" } }, { "name": "Update Account - success (accountCode in body is silently ignored - immutable)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"Still Renamed\",\n \"accountCode\": \"9999-HACK\"\n}" }, "url": { "raw": "{{baseUrl}}/accounts/", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "" ] }, "description": "200. accountName updates normally; accountCode is not part of UpdateAccountRequest at all (retrofit 2026-07-20, CLAUDE.md Rule 8.1) so the accountCode key in the body is simply never read - response's accountCode is unchanged from before this call, not '9999-HACK'." } }, { "name": "Update Account - 404 not found", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"x\"\n}" }, "url": { "raw": "{{baseUrl}}/accounts/NOTFOUND-XYZ", "host": [ "{{baseUrl}}" ], "path": [ "accounts", "NOTFOUND-XYZ" ] }, "description": "404. Message: \"Account not found\"." } } ] }, { "name": "Fiscal Calendar", "item": [ { "name": "Create Fiscal Year - success (autoGeneratePeriods=false)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"yearName\": \"FY2035-Doc\",\n \"startDate\": \"2035-01-01\",\n \"endDate\": \"2035-12-31\",\n \"autoGeneratePeriods\": false\n}" }, "url": { "raw": "{{baseUrl}}/fiscal-years", "host": [ "{{baseUrl}}" ], "path": [ "fiscal-years" ] }, "description": "201. data.generatedPeriods is [] when autoGeneratePeriods is false/omitted." } }, { "name": "Create Fiscal Year - success (autoGeneratePeriods=true, periodCount omitted -> 12)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"yearName\": \"FY2036-Doc\",\n \"startDate\": \"2036-01-01\",\n \"endDate\": \"2036-12-31\",\n \"autoGeneratePeriods\": true\n}" }, "url": { "raw": "{{baseUrl}}/fiscal-years", "host": [ "{{baseUrl}}" ], "path": [ "fiscal-years" ] }, "description": "201. data.generatedPeriods has 12 monthly periods, internally calling the same BuildPeriodsAsync logic as the standalone periods endpoint." } }, { "name": "Create Fiscal Year - 400 endDate <= startDate", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"yearName\": \"bad\",\n \"startDate\": \"2030-01-01\",\n \"endDate\": \"2030-01-01\"\n}" }, "url": { "raw": "{{baseUrl}}/fiscal-years", "host": [ "{{baseUrl}}" ], "path": [ "fiscal-years" ] }, "description": "400. Message: \"End date must be after start date\"." } }, { "name": "Create Fiscal Year - 400 overlapping range", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"yearName\": \"overlap\",\n \"startDate\": \"2035-06-01\",\n \"endDate\": \"2035-08-01\"\n}" }, "url": { "raw": "{{baseUrl}}/fiscal-years", "host": [ "{{baseUrl}}" ], "path": [ "fiscal-years" ] }, "description": "400. Message: \"Fiscal year date range overlaps with an existing fiscal year\"." } }, { "name": "Generate Periods - success", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodCount\": 12\n}" }, "url": { "raw": "{{baseUrl}}/fiscal-years/5/periods", "host": [ "{{baseUrl}}" ], "path": [ "fiscal-years", "5", "periods" ] }, "description": "201. Extends/adds periods to an existing fiscal year that has none yet (or fewer than a full year)." } }, { "name": "Generate Periods - 404 fiscal year not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodCount\": 12\n}" }, "url": { "raw": "{{baseUrl}}/fiscal-years/999999/periods", "host": [ "{{baseUrl}}" ], "path": [ "fiscal-years", "999999", "periods" ] }, "description": "404. Message: \"Fiscal year not found\"." } }, { "name": "Generate Periods - 400 exceeds fiscal year date range", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodCount\": 1\n}" }, "url": { "raw": "{{baseUrl}}/fiscal-years/5/periods", "host": [ "{{baseUrl}}" ], "path": [ "fiscal-years", "5", "periods" ] }, "description": "400. Message: \"Requested period count exceeds the fiscal year's date range\". Fires when generating past a fiscal year already fully covered by prior period generation." } }, { "name": "Lock Period - success LOCK", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"LOCK\",\n \"reason\": \"Doc test lock\"\n}" }, "url": { "raw": "{{baseUrl}}/periods/37/lock", "host": [ "{{baseUrl}}" ], "path": [ "periods", "37", "lock" ] }, "description": "200. action one of LOCK|UNLOCK|SOFT_CLOSE|CLOSE (case-insensitive). Writes both a period_lock_log row and an audit_log row (category LEDGER)." } }, { "name": "Lock Period - success UNLOCK (reopen)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"UNLOCK\",\n \"reason\": \"reopen\"\n}" }, "url": { "raw": "{{baseUrl}}/periods/37/lock", "host": [ "{{baseUrl}}" ], "path": [ "periods", "37", "lock" ] }, "description": "200. UNLOCK is the only action accepted on a CLOSED period (see the 400 case below)." } }, { "name": "Lock Period - success CLOSE", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"CLOSE\",\n \"reason\": \"year end\"\n}" }, "url": { "raw": "{{baseUrl}}/periods/37/lock", "host": [ "{{baseUrl}}" ], "path": [ "periods", "37", "lock" ] }, "description": "200." } }, { "name": "Lock Period - 400 action other than UNLOCK on a CLOSED period", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"LOCK\",\n \"reason\": \"x\"\n}" }, "url": { "raw": "{{baseUrl}}/periods/37/lock", "host": [ "{{baseUrl}}" ], "path": [ "periods", "37", "lock" ] }, "description": "400 (run right after the CLOSE request above). Message: \"Period is closed; only Unlock is allowed to reopen it\"." } }, { "name": "Lock Period - 400 invalid action", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"BOGUS\",\n \"reason\": \"x\"\n}" }, "url": { "raw": "{{baseUrl}}/periods/37/lock", "host": [ "{{baseUrl}}" ], "path": [ "periods", "37", "lock" ] }, "description": "400. Message: \"Invalid action 'BOGUS'. Must be one of: Lock, Unlock, SoftClose, Close.\"." } }, { "name": "Lock Period - 404 not found", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"LOCK\",\n \"reason\": \"x\"\n}" }, "url": { "raw": "{{baseUrl}}/periods/999999/lock", "host": [ "{{baseUrl}}" ], "path": [ "periods", "999999", "lock" ] }, "description": "404. Message: \"Period not found\"." } }, { "name": "Get Period By Date - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/fiscal-years/periods/by-date?date=2028-01-15", "host": [ "{{baseUrl}}" ], "path": [ "fiscal-years", "periods", "by-date" ], "query": [ { "key": "date", "value": "2028-01-15" } ] }, "description": "200. Returns the AccountingPeriod covering the given date (periodId, fiscalYearId, periodNumber, startDate, endDate, status, lockGl). Added Phase 38 for callers (e.g. ERPCore GRN module) that need a periodId for POST /journal-entries." } }, { "name": "Get Period By Date - 404 not found", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/fiscal-years/periods/by-date?date=1999-01-01", "host": [ "{{baseUrl}}" ], "path": [ "fiscal-years", "periods", "by-date" ], "query": [ { "key": "date", "value": "1999-01-01" } ] }, "description": "404. Message: \"Period not found\". No fiscal year/period covers this date." } } ] }, { "name": "Currency", "item": [ { "name": "List Currencies - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/currencies", "host": [ "{{baseUrl}}" ], "path": [ "currencies" ] }, "description": "200. Flat list, all 142 seeded ISO 4217 currencies, no filters/paging. Exactly one row has isBaseCurrency:true (LKR)." } } ] }, { "name": "Cost Centers", "item": [ { "name": "Create Cost Center - success (root)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"name\": \"Doc Test Dept\",\n \"isActive\": true\n}" }, "url": { "raw": "{{baseUrl}}/cost-centers", "host": [ "{{baseUrl}}" ], "path": [ "cost-centers" ] }, "description": "201. code is system-generated (retrofit 2026-07-20, CLAUDE.md Rule 8.1) - e.g. CC-001 for the first root cost center; substitute the returned value ( below) into the requests that follow." } }, { "name": "Create Cost Center - success (child)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"name\": \"Doc Test Sub-Dept\",\n \"parentCostCenterId\": 1,\n \"isActive\": true\n}" }, "url": { "raw": "{{baseUrl}}/cost-centers", "host": [ "{{baseUrl}}" ], "path": [ "cost-centers" ] }, "description": "201. Depth-guard runs against COST_CENTER_MAX_DEPTH (unconditional - no separate enforce flag for cost centers, unlike accounts). code is system-generated as {parentCode}-01." } }, { "name": "Create Cost Center - 404 parent not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"name\": \"orphan\",\n \"parentCostCenterId\": 999999,\n \"isActive\": true\n}" }, "url": { "raw": "{{baseUrl}}/cost-centers", "host": [ "{{baseUrl}}" ], "path": [ "cost-centers" ] }, "description": "404. Message: \"Parent cost center not found\"." } }, { "name": "Get Cost Center - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cost-centers/", "host": [ "{{baseUrl}}" ], "path": [ "cost-centers", "" ] }, "description": "200. data: { costCenter, ancestorBreadcrumb, descendantTree: null }. = the code returned by \"Create Cost Center - success (root)\" above." } }, { "name": "Get Cost Center - success (?expand=tree)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cost-centers/?expand=tree", "host": [ "{{baseUrl}}" ], "path": [ "cost-centers", "" ], "query": [ { "key": "expand", "value": "tree" } ] }, "description": "200. descendantTree populated." } }, { "name": "Get Cost Center - 404 not found", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cost-centers/NOTFOUND-XYZ", "host": [ "{{baseUrl}}" ], "path": [ "cost-centers", "NOTFOUND-XYZ" ] }, "description": "404. Message: \"Cost center not found\"." } } ] }, { "name": "Journal Entries (core double-entry action)", "item": [ { "name": "Create - success (plain, balanced lines - created AND posted in one call)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 49,\n \"entryDate\": \"2035-01-06\",\n \"journalType\": \"MANUAL\",\n \"reference\": \"DOC-TEST-2\",\n \"narration\": \"Inline lines\",\n \"preparedBy\": \"Doc Tester\",\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 100, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 100 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "201. CLAUDE.md Rule 14 (retrofit 2026-07-22): there is no client-facing DRAFT state anymore - this one call validates, inserts, balance-checks, period-lock-checks, and posts, all in one transaction. Response has isPosted:true, entryStatus:2 (Posted) immediately. lines use accountCode/costCenterCode (Rule 8.2), not accountId/costCenterId. journalNo is server-generated (JV-{year}-{6-digit sequence}), never client-supplied." } }, { "name": "Create - 400 lines missing/empty", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 49,\n \"entryDate\": \"2035-01-05\",\n \"journalType\": \"MANUAL\",\n \"reference\": \"DOC-TEST-1\"\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "400. Message: \"lines is required\". A plain create (no correction) with no lines is no longer a valid DRAFT - lines are mandatory now that create-and-post is one call." } }, { "name": "Create - 400 lines don't balance (checked immediately, not deferred to a separate post)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 49,\n \"entryDate\": \"2035-01-07\",\n \"journalType\": \"MANUAL\",\n \"reference\": \"DOC-TEST-3\",\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 100, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 75 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "400. Message: \"Journal entry does not balance\". Checked inside the same transaction as the insert, so the whole call rolls back - nothing is left half-inserted (unlike the old create-then-post flow, where an unbalanced draft could sit around indefinitely)." } }, { "name": "Create - 404 period not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 999999,\n \"entryDate\": \"2035-01-01\",\n \"journalType\": \"MANUAL\",\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 10, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 10 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "404. Message: \"Period not found\"." } }, { "name": "Create - 400 period is locked/closed", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 37,\n \"entryDate\": \"2031-01-01\",\n \"journalType\": \"MANUAL\",\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 10, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 10 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "400 (periodId 37 is Locked or Closed). Message: \"Period is locked\". Checked AFTER the balance check, inside the same transaction - an unbalanced entry in a locked period returns the balance error first." } }, { "name": "Create - 404 an accountCode in lines doesn't exist", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 49,\n \"entryDate\": \"2035-01-07\",\n \"journalType\": \"MANUAL\",\n \"lines\": [\n { \"accountCode\": \"NOTFOUND-XYZ\", \"debitAmount\": 10, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 10 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "404. Message: \"Account NOTFOUND-XYZ not found\"." } }, { "name": "Create - 400 an accountCode in lines isn't postable", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 49,\n \"entryDate\": \"2035-01-07\",\n \"journalType\": \"MANUAL\",\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 10, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 10 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "400. Message: \"Account is not postable\" (a control/header account with isPostable:false)." } }, { "name": "Create - success (correction, FullReversal against a posted entry - auto-flips the original)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 49,\n \"entryDate\": \"2035-01-08\",\n \"journalType\": \"CORRECTION\",\n \"reference\": \"REV-JE2\",\n \"correction\": {\n \"originalJournalNo\": \"JV-2035-000001\",\n \"correctionType\": \"FullReversal\",\n \"reason\": \"Wrong account used\",\n \"correctedBy\": \"Doc Tester\"\n }\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "201. correction.originalJournalNo (retrofit 2026-07-22, replaces originalJournalId) must reference an already-Posted entry. Lines are auto-generated by mirroring the original's lines with debit/credit swapped - request.lines is ignored for a correction. Created AND posted in this one call (Rule 14); in the SAME transaction, the ORIGINAL entry flips to entryStatus:3 (Reversed), isReversed:true - verified by re-fetching the original afterward. Two audit_log rows written: one Reverse for this entry's create+post, one Reverse against the original." } }, { "name": "Create - success (correction, PartialAdjustment - lines now required inline, no separate Set Lines call)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 49,\n \"entryDate\": \"2035-01-09\",\n \"journalType\": \"CORRECTION\",\n \"reference\": \"ADJ-JE1\",\n \"correction\": {\n \"originalJournalNo\": \"JV-2035-000002\",\n \"correctionType\": \"PartialAdjustment\",\n \"reason\": \"Add missing accrual\",\n \"correctedBy\": \"Doc Tester\"\n },\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 50, \"creditAmount\": 0, \"memo\": \"accrual\" },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 50 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "201. PUT .../lines is removed entirely (Rule 14) - a PartialAdjustment now takes its lines inline in the same body as a plain create, and is created AND posted immediately. The ORIGINAL entry's status is untouched by this call (only FullReversal flips the original)." } }, { "name": "Create - 400 correction against a non-posted (or already-reversed) original", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 49,\n \"entryDate\": \"2035-01-10\",\n \"journalType\": \"CORRECTION\",\n \"correction\": {\n \"originalJournalNo\": \"JV-2035-000001\",\n \"correctionType\": \"FullReversal\",\n \"reason\": \"x\",\n \"correctedBy\": \"y\"\n }\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "400 (run after JV-2035-000001 has already been reversed by the request above, so its status is now Reversed, not Posted). Message: \"Original journal entry must be posted before it can be corrected\" - same message whether the original was never posted or has already been reversed." } }, { "name": "Create - 400 invalid correctionType", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"periodId\": 49,\n \"entryDate\": \"2035-01-10\",\n \"journalType\": \"CORRECTION\",\n \"correction\": {\n \"originalJournalNo\": \"JV-2035-000002\",\n \"correctionType\": \"BOGUS\",\n \"reason\": \"x\",\n \"correctedBy\": \"y\"\n }\n}" }, "url": { "raw": "{{baseUrl}}/journal-entries", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries" ] }, "description": "400. Message: \"Invalid correctionType 'BOGUS'. Must be one of: FullReversal, PartialAdjustment.\"." } }, { "name": "PUT .../lines (REMOVED, CLAUDE.md Rule 14) - 404, route no longer exists", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "[\n { \"accountCode\": \"\", \"debitAmount\": 50, \"creditAmount\": 0 }\n]" }, "url": { "raw": "{{baseUrl}}/journal-entries/JV-2035-000001/lines", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries", "JV-2035-000001", "lines" ] }, "description": "404 (no matching route). Kept in this collection deliberately, as a regression check - confirms the retrofit actually removed the route rather than leaving it silently reachable. lines are now supplied inline in POST /journal-entries." } }, { "name": "PUT .../post (REMOVED, CLAUDE.md Rule 14) - 404, route no longer exists", "request": { "method": "PUT", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/journal-entries/JV-2035-000001/post", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries", "JV-2035-000001", "post" ] }, "description": "404 (no matching route). Kept as a regression check, same reasoning as the removed .../lines route above - POST /journal-entries now creates AND posts in one call." } }, { "name": "Get by journalNo - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/journal-entries/JV-2035-000002", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries", "JV-2035-000002" ] }, "description": "200. Looked up by journalNo, not the numeric journalId (CLAUDE.md Rule 8). Includes lines[]." } }, { "name": "Get by journalNo - 404 not found", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/journal-entries/JV-NOTFOUND", "host": [ "{{baseUrl}}" ], "path": [ "journal-entries", "JV-NOTFOUND" ] }, "description": "404. Message: \"Journal entry not found\"." } } ] }, { "name": "Recurring Entries", "item": [ { "name": "Create Template - success", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"templateName\": \"Doc Monthly Rent\",\n \"recurrencePattern\": \"Monthly\",\n \"nextRunDate\": \"2026-07-01\",\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 30, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 30 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/recurring-templates", "host": [ "{{baseUrl}}" ], "path": [ "recurring-templates" ] }, "description": "201. recurrencePattern one of Monthly|Quarterly|Annual. lines use accountCode (retrofit 2026-07-22, CLAUDE.md Rule 8.2 - replaces accountId; stored internally as the resolved numeric AccountId). Lines must balance at creation time (unlike journal entries, which balance-check at create time too now, but this template-level check is independent - a template is never itself posted)." } }, { "name": "Create Template - 400 lines don't balance", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"templateName\": \"bad\",\n \"recurrencePattern\": \"Monthly\",\n \"nextRunDate\": \"2026-08-01\",\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 30, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 20 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/recurring-templates", "host": [ "{{baseUrl}}" ], "path": [ "recurring-templates" ] }, "description": "400. Message: \"Recurring template lines do not balance\"." } }, { "name": "Create Template - 400 invalid recurrencePattern", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"templateName\": \"bad\",\n \"recurrencePattern\": \"Weekly\",\n \"nextRunDate\": \"2026-08-01\",\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 30, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 30 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/recurring-templates", "host": [ "{{baseUrl}}" ], "path": [ "recurring-templates" ] }, "description": "400. Message: \"Invalid recurrencePattern 'Weekly'. Must be one of: Monthly, Quarterly, Annual.\"." } }, { "name": "Create Template - 400 non-postable account in a line", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"templateName\": \"bad\",\n \"recurrencePattern\": \"Monthly\",\n \"nextRunDate\": \"2026-08-01\",\n \"lines\": [\n { \"accountCode\": \"\", \"debitAmount\": 30, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 30 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/recurring-templates", "host": [ "{{baseUrl}}" ], "path": [ "recurring-templates" ] }, "description": "400. Message: \"Account is not postable\"." } }, { "name": "Create Template - 404 account not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"templateName\": \"bad\",\n \"recurrencePattern\": \"Monthly\",\n \"nextRunDate\": \"2026-08-01\",\n \"lines\": [\n { \"accountCode\": \"NOTFOUND-XYZ\", \"debitAmount\": 30, \"creditAmount\": 0 },\n { \"accountCode\": \"\", \"debitAmount\": 0, \"creditAmount\": 30 }\n ]\n}" }, "url": { "raw": "{{baseUrl}}/recurring-templates", "host": [ "{{baseUrl}}" ], "path": [ "recurring-templates" ] }, "description": "404. Message: \"Account NOTFOUND-XYZ not found\"." } }, { "name": "Generate - success (due, nextRunDate <= today - now generates AND posts in one call)", "request": { "method": "POST", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/recurring-templates/3/generate", "host": [ "{{baseUrl}}" ], "path": [ "recurring-templates", "3", "generate" ] }, "description": "201 (retrofit 2026-07-22). Resolves 'today's' period via GetPeriodByDateAsync, resolves each line's stored numeric AccountId back to its AccountCode, delegates to IJournalEntryService.CreateAsync (journalType RECURLING) - which now creates AND posts in the same call (Rule 14), so the generated entry comes back already Posted, not a DRAFT needing a separate PUT .../post (removed). Then advances the template's nextRunDate by its recurrencePattern (+1 month/+3 months/+1 year) so a repeat call the same day doesn't duplicate." } }, { "name": "Generate - 400 not yet due", "request": { "method": "POST", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/recurring-templates/3/generate", "host": [ "{{baseUrl}}" ], "path": [ "recurring-templates", "3", "generate" ] }, "description": "400 (call a second time right after the success case - nextRunDate has already advanced past today). Message: \"Recurring template is not yet due to run\"." } }, { "name": "Generate - 404 template not found", "request": { "method": "POST", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/recurring-templates/999999/generate", "host": [ "{{baseUrl}}" ], "path": [ "recurring-templates", "999999", "generate" ] }, "description": "404. Message: \"Recurring template not found\"." } } ] }, { "name": "Tax", "item": [ { "name": "Create Tax Code - success", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"taxTypeCode\": \"VAT\",\n \"name\": \"VAT 18% Doc\",\n \"ratePercent\": 18,\n \"calculationBasis\": \"Exclusive\",\n \"appliesTo\": \"Sales\",\n \"effectiveFrom\": \"2026-01-01\"\n}" }, "url": { "raw": "{{baseUrl}}/tax-codes", "host": [ "{{baseUrl}}" ], "path": [ "tax-codes" ] }, "description": "201. taxTypeCode (retrofit 2026-07-22, CLAUDE.md Rule 8.2 - replaces taxTypeId). calculationBasis: Exclusive|Inclusive. appliesTo: Sales|Purchase|Both. outputTaxAccountCode/inputTaxAccountCode/effectiveTo all optional. code is system-generated as {TaxType.Code}-{seq:D3}, e.g. VAT-001 (retrofit 2026-07-20, CLAUDE.md Rule 8.1) - no longer accepted in the request body, and a duplicate-code scenario is now structurally unreachable (see 03_Progress_Tracker.md)." } }, { "name": "Create Tax Code - success (with output/input tax account codes)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"taxTypeCode\": \"WHT\",\n \"name\": \"WHT 5% Doc\",\n \"ratePercent\": 5,\n \"calculationBasis\": \"Exclusive\",\n \"appliesTo\": \"Purchase\",\n \"outputTaxAccountCode\": \"\",\n \"inputTaxAccountCode\": \"\",\n \"effectiveFrom\": \"2026-01-01\"\n}" }, "url": { "raw": "{{baseUrl}}/tax-codes", "host": [ "{{baseUrl}}" ], "path": [ "tax-codes" ] }, "description": "201. outputTaxAccountCode/inputTaxAccountCode (retrofit 2026-07-22, replace outputTaxAccountId/inputTaxAccountId) resolved via IAccountRepository.GetByCodeAsync." } }, { "name": "Create Tax Code - 400 effectiveFrom missing", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"taxTypeCode\": \"VAT\",\n \"name\": \"x\",\n \"ratePercent\": 18,\n \"calculationBasis\": \"Exclusive\",\n \"appliesTo\": \"Sales\"\n}" }, "url": { "raw": "{{baseUrl}}/tax-codes", "host": [ "{{baseUrl}}" ], "path": [ "tax-codes" ] }, "description": "400. Message: \"effectiveFrom is required\"." } }, { "name": "Create Tax Code - 404 taxTypeCode not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"taxTypeCode\": \"NOTFOUND\",\n \"name\": \"x\",\n \"ratePercent\": 18,\n \"calculationBasis\": \"Exclusive\",\n \"appliesTo\": \"Sales\",\n \"effectiveFrom\": \"2026-01-01\"\n}" }, "url": { "raw": "{{baseUrl}}/tax-codes", "host": [ "{{baseUrl}}" ], "path": [ "tax-codes" ] }, "description": "404. Message: \"Tax type not found\"." } }, { "name": "Create Tax Code - 404 outputTaxAccountCode/inputTaxAccountCode not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"taxTypeCode\": \"VAT\",\n \"name\": \"x\",\n \"ratePercent\": 18,\n \"calculationBasis\": \"Exclusive\",\n \"appliesTo\": \"Sales\",\n \"outputTaxAccountCode\": \"NOTFOUND-XYZ\",\n \"effectiveFrom\": \"2026-01-01\"\n}" }, "url": { "raw": "{{baseUrl}}/tax-codes", "host": [ "{{baseUrl}}" ], "path": [ "tax-codes" ] }, "description": "404. Message: \"Output tax account not found\" (input side reports \"Input tax account not found\")." } }, { "name": "Create Tax Code - 400 invalid calculationBasis/appliesTo", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"taxTypeCode\": \"VAT\",\n \"name\": \"x\",\n \"ratePercent\": 18,\n \"calculationBasis\": \"Bogus\",\n \"appliesTo\": \"Sales\",\n \"effectiveFrom\": \"2026-01-01\"\n}" }, "url": { "raw": "{{baseUrl}}/tax-codes", "host": [ "{{baseUrl}}" ], "path": [ "tax-codes" ] }, "description": "400. Message: \"Invalid calculationBasis 'Bogus'. Must be one of: Exclusive, Inclusive.\" (appliesTo validated the same way, checked second)." } }, { "name": "Calculate Tax - success (Exclusive: 1000 @ 18% = 180)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/tax-codes/calculate?taxCode=VAT-001&amount=1000&txnDate=2026-07-19", "host": [ "{{baseUrl}}" ], "path": [ "tax-codes", "calculate" ], "query": [ { "key": "taxCode", "value": "VAT-001" }, { "key": "amount", "value": "1000" }, { "key": "txnDate", "value": "2026-07-19" } ] }, "description": "200. taxCode (retrofit 2026-07-22, CLAUDE.md Rule 8.2 - replaces taxCodeId). Exclusive: taxAmount = amount * rate/100. Inclusive: taxAmount = amount - (amount / (1 + rate/100)) - i.e. amount is treated as already including tax. No persistence - pure computation." } }, { "name": "Calculate Tax - 404 not found / not effective for date", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/tax-codes/calculate?taxCode=VAT-001&amount=1000&txnDate=2020-01-01", "host": [ "{{baseUrl}}" ], "path": [ "tax-codes", "calculate" ], "query": [ { "key": "taxCode", "value": "VAT-001" }, { "key": "amount", "value": "1000" }, { "key": "txnDate", "value": "2020-01-01" } ] }, "description": "404. Message: \"Tax code not found or not effective for the given date\" - same message whether taxCode doesn't exist at all, or exists but txnDate falls outside [effectiveFrom, effectiveTo]." } }, { "name": "List Tax Types - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/tax-types", "host": [ "{{baseUrl}}" ], "path": [ "tax-types" ] }, "description": "200. Flat list, all 5 seeded rows (VAT, WHT, NBT, SSCL, INCOME_TAX)." } }, { "name": "List Tax Codes - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/tax-codes", "host": [ "{{baseUrl}}" ], "path": [ "tax-codes" ] }, "description": "200. Flat list, every tax code ever created, no filters/paging." } } ] }, { "name": "Fixed Assets", "item": [ { "name": "Create Asset - success (StraightLine)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"assetName\": \"Doc Test Laptop\",\n \"assetAccountCode\": \"\",\n \"accumDeprAccountCode\": \"\",\n \"deprExpenseAccountCode\": \"\",\n \"gainLossAccountCode\": \"\",\n \"acquisitionDate\": \"2035-01-01\",\n \"acquisitionCost\": 12000,\n \"residualValue\": 0,\n \"usefulLifeMonths\": 12,\n \"depreciationMethod\": \"StraightLine\"\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets" ] }, "description": "201. All four account fields take a code now (retrofit 2026-07-22, CLAUDE.md Rule 8.2 - replace assetAccountId/accumDeprAccountId/deprExpenseAccountId/gainLossAccountId). StraightLine: depreciationRatePercent not needed. Formula per period: (acquisitionCost - residualValue) / usefulLifeMonths. assetCode is system-generated (retrofit 2026-07-20, CLAUDE.md Rule 8.1) - e.g. FA-000001; substitute the returned value ( below) into the transaction requests that follow." } }, { "name": "Create Asset - success (ReducingBalance)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"assetName\": \"Doc Test Server\",\n \"assetAccountCode\": \"\",\n \"accumDeprAccountCode\": \"\",\n \"deprExpenseAccountCode\": \"\",\n \"gainLossAccountCode\": \"\",\n \"acquisitionDate\": \"2035-01-01\",\n \"acquisitionCost\": 10000,\n \"residualValue\": 1000,\n \"usefulLifeMonths\": 24,\n \"depreciationMethod\": \"ReducingBalance\",\n \"depreciationRatePercent\": 20\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets" ] }, "description": "201. ReducingBalance requires depreciationRatePercent. Formula per period: (acquisitionCost - priorAccumulatedDepreciation) * (rate/100), clamped so accumulated depreciation never exceeds acquisitionCost - residualValue. assetCode is system-generated; substitute the returned value ( below) into the transaction requests that follow." } }, { "name": "Create Asset - 400 ReducingBalance missing rate", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"assetName\": \"bad\",\n \"assetAccountCode\": \"\",\n \"accumDeprAccountCode\": \"\",\n \"deprExpenseAccountCode\": \"\",\n \"gainLossAccountCode\": \"\",\n \"acquisitionDate\": \"2035-01-01\",\n \"acquisitionCost\": 5000,\n \"residualValue\": 0,\n \"usefulLifeMonths\": 12,\n \"depreciationMethod\": \"ReducingBalance\"\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets" ] }, "description": "400. Message: \"depreciationRatePercent is required when depreciationMethod is ReducingBalance\"." } }, { "name": "Create Asset - 400 residualValue > acquisitionCost", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"assetName\": \"bad\",\n \"assetAccountCode\": \"\",\n \"accumDeprAccountCode\": \"\",\n \"deprExpenseAccountCode\": \"\",\n \"gainLossAccountCode\": \"\",\n \"acquisitionDate\": \"2035-01-01\",\n \"acquisitionCost\": 1000,\n \"residualValue\": 5000,\n \"usefulLifeMonths\": 12,\n \"depreciationMethod\": \"StraightLine\"\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets" ] }, "description": "400. Message: \"residualValue cannot exceed acquisitionCost\"." } }, { "name": "Create Asset - 400 an account is not postable", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"assetName\": \"bad\",\n \"assetAccountCode\": \"\",\n \"accumDeprAccountCode\": \"\",\n \"deprExpenseAccountCode\": \"\",\n \"gainLossAccountCode\": \"\",\n \"acquisitionDate\": \"2035-01-01\",\n \"acquisitionCost\": 1000,\n \"residualValue\": 0,\n \"usefulLifeMonths\": 12,\n \"depreciationMethod\": \"StraightLine\"\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets" ] }, "description": "400. Message: \"Asset account () is not postable\" - all four account fields (asset/accumDepr/deprExpense/gainLoss) are checked the same way, whichever fails first is named." } }, { "name": "Transaction - Depreciate success (StraightLine)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"transactionType\": \"Depreciate\",\n \"periodId\": 49\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets//transactions", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets", "", "transactions" ] }, "description": "201. (12000-0)/12 = 1000. Builds+creates+posts a real journal entry internally (Debit Depreciation Expense, Credit Accum. Depreciation) via IJournalEntryService, posted as of the period's endDate. Returns the resulting DepreciationSchedule row, which embeds the journal." } }, { "name": "Transaction - Depreciate success (ReducingBalance)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"transactionType\": \"Depreciate\",\n \"periodId\": 49\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets//transactions", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets", "", "transactions" ] }, "description": "201. (10000-0)*20% = 2000 (first period; prior accumulated depreciation is 0)." } }, { "name": "Transaction - Depreciate 400 period is locked", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"transactionType\": \"Depreciate\",\n \"periodId\": 37\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets//transactions", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets", "", "transactions" ] }, "description": "400. Message: \"Period is locked\"." } }, { "name": "Transaction - 404 asset not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"transactionType\": \"Depreciate\",\n \"periodId\": 49\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets/NOTFOUND-XYZ/transactions", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets", "NOTFOUND-XYZ", "transactions" ] }, "description": "404. Message: \"Fixed asset not found\"." } }, { "name": "Transaction - 400 invalid transactionType", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"transactionType\": \"Bogus\",\n \"periodId\": 49\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets//transactions", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets", "", "transactions" ] }, "description": "400. Message: \"Invalid transactionType 'Bogus'. Must be one of: Depreciate, Dispose.\"." } }, { "name": "Transaction - Dispose success (loss scenario)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"transactionType\": \"Dispose\",\n \"disposalDate\": \"2035-01-20\",\n \"proceeds\": 9000,\n \"proceedsAccountCode\": \"\"\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets//transactions", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets", "", "transactions" ] }, "description": "201. proceedsAccountCode (retrofit 2026-07-22, CLAUDE.md Rule 8.2 - replaces proceedsAccountId). Real verified loss scenario: NBV = 12000 acquisition - 1000 depreciated = 11000; proceeds 9000 -> gainLoss = 9000-11000 = -2000 (a loss). Journal (4 lines, balanced at 12000 both sides): Debit proceedsAccount 9000, Debit AccumDepr 1000, Credit Asset 12000, Debit GainLoss 2000. Zero-value lines are omitted entirely (e.g. no proceeds line if proceeds=0) - journal_entry_line's own one-sided check constraint rejects a zero-amount line. Internally the flow now creates AND posts its journal entry in one call (CLAUDE.md Rule 14) - no separate .PostAsync afterward." } }, { "name": "Transaction - Dispose 400 already disposed", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"transactionType\": \"Dispose\",\n \"disposalDate\": \"2035-01-20\",\n \"proceeds\": 9000,\n \"proceedsAccountCode\": \"\"\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets//transactions", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets", "", "transactions" ] }, "description": "400. Message: \"Asset has already been disposed\"." } }, { "name": "Transaction - Depreciate 400 asset already disposed", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"transactionType\": \"Depreciate\",\n \"periodId\": 49\n}" }, "url": { "raw": "{{baseUrl}}/fixed-assets//transactions", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets", "", "transactions" ] }, "description": "400. Message: \"Asset has been disposed\" (both transaction types are rejected once an asset is Disposed)." } }, { "name": "List Fixed Assets - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/fixed-assets", "host": [ "{{baseUrl}}" ], "path": [ "fixed-assets" ] }, "description": "200. Flat list, no filters/paging." } } ] }, { "name": "Bank", "item": [ { "name": "Create Bank Account - success", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"Doc Test Account\",\n \"bankName\": \"Test Bank\",\n \"accountNumber\": \"999888777\",\n \"glAccountCode\": \"\"\n}" }, "url": { "raw": "{{baseUrl}}/bank-accounts", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts" ] }, "description": "201. glAccountCode (retrofit 2026-07-22, CLAUDE.md Rule 8.2 - replaces glAccountId). currencyCode omitted -> defaults \"LKR\" (same precedent as Accounts)." } }, { "name": "Create Bank Account - 404 GL account not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"bad\",\n \"glAccountCode\": \"NOTFOUND-XYZ\"\n}" }, "url": { "raw": "{{baseUrl}}/bank-accounts", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts" ] }, "description": "404. Message: \"GL account not found\"." } }, { "name": "Create Bank Account - 400 GL account not postable", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"bad\",\n \"glAccountCode\": \"\"\n}" }, "url": { "raw": "{{baseUrl}}/bank-accounts", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts" ] }, "description": "400. Message: \"GL account () is not postable\"." } }, { "name": "Create Cash Account - success (cashAccountTypeName matches a seeded type)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"Doc Test Petty Cash\",\n \"cashAccountTypeName\": \"Petty Cash\",\n \"glAccountCode\": \"\"\n}" }, "url": { "raw": "{{baseUrl}}/cash-accounts", "host": [ "{{baseUrl}}" ], "path": [ "cash-accounts" ] }, "description": "201. accountNumber omitted -> auto-generated sequential CASH-000001-style reference (CLAUDE.md Rule 8.1). glAccountCode required (CLAUDE.md Rule 8.2)." } }, { "name": "Create Cash Account - success (cashAccountTypeName is new - creates the type on the fly)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"Doc Test Vault Reserve\",\n \"cashAccountTypeName\": \"Vault Reserve\",\n \"glAccountCode\": \"\"\n}" }, "url": { "raw": "{{baseUrl}}/cash-accounts", "host": [ "{{baseUrl}}" ], "path": [ "cash-accounts" ] }, "description": "201. A cashAccountTypeName with no case-insensitive match against cash_account_type adds a new row on the fly - visible afterward via GET /cash-account-types." } }, { "name": "Create Cash Account - 400 cashAccountTypeName missing", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"accountName\": \"bad\",\n \"glAccountCode\": \"\"\n}" }, "url": { "raw": "{{baseUrl}}/cash-accounts", "host": [ "{{baseUrl}}" ], "path": [ "cash-accounts" ] }, "description": "400. Message: \"cashAccountTypeName is required\"." } }, { "name": "List Cash Account Types - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cash-account-types", "host": [ "{{baseUrl}}" ], "path": [ "cash-account-types" ] }, "description": "200. Seeded: Petty Cash, Till Cash, Safe Cash, Cash in Transit - grows over time via Create Cash Account above." } }, { "name": "Import Statement Lines - success (mixed valid + malformed rows)", "request": { "method": "POST", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "formdata", "formdata": [ { "key": "file", "type": "file", "src": "doc_test_statement.xlsx" } ] }, "url": { "raw": "{{baseUrl}}/bank-accounts/2/statement-lines/import", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts", "2", "statement-lines", "import" ] }, "description": "201 (not 200 - it's a creation). multipart/form-data, field name 'file', .xlsx only. Column layout, header row required: TxnDate | Description | Amount. Verified live: a 2-row file (1 valid date+amount, 1 with a non-numeric Amount) -> data.imported has 1 row, data.failedRows has 1 entry { rowNumber, reason }. Valid rows are saved even when other rows in the same file fail - not all-or-nothing." } }, { "name": "Import Statement Lines - 400 not a valid .xlsx", "request": { "method": "POST", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "formdata", "formdata": [ { "key": "file", "type": "file", "src": "corrupt.txt" } ] }, "url": { "raw": "{{baseUrl}}/bank-accounts/2/statement-lines/import", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts", "2", "statement-lines", "import" ] }, "description": "400. Message: \"Uploaded file is not a valid .xlsx spreadsheet\" - a structurally corrupt/wrong-format file fails outright (unlike a bad row inside a valid .xlsx, which is partially salvaged)." } }, { "name": "Import Statement Lines - 404 bank account not found", "request": { "method": "POST", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "formdata", "formdata": [ { "key": "file", "type": "file", "src": "doc_test_statement.xlsx" } ] }, "url": { "raw": "{{baseUrl}}/bank-accounts/999999/statement-lines/import", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts", "999999", "statement-lines", "import" ] }, "description": "404. Message: \"Bank account not found\"." } }, { "name": "Reconcile - success (amounts match)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"matchedJournalNo\": \"JV-2035-000001\"\n}" }, "url": { "raw": "{{baseUrl}}/bank-statement-lines/3/reconcile", "host": [ "{{baseUrl}}" ], "path": [ "bank-statement-lines", "3", "reconcile" ] }, "description": "200. matchedJournalNo (retrofit 2026-07-22, CLAUDE.md Rule 8.2 - replaces matchedJournalId; internal storage stays the numeric journalId). \"Amounts must match\" resolved as: matched journal entry must be Posted; sum its lines posted to the bank account's own glAccountId; compare net (debit-credit) movement to the statement line's Amount. Verified live: a 500-amount statement line reconciled against a journal entry whose net movement on the bank's GL account is exactly 500." } }, { "name": "Reconcile - 400 already reconciled", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"matchedJournalNo\": \"JV-2035-000001\"\n}" }, "url": { "raw": "{{baseUrl}}/bank-statement-lines/3/reconcile", "host": [ "{{baseUrl}}" ], "path": [ "bank-statement-lines", "3", "reconcile" ] }, "description": "400 (call a second time on the same line). Message: \"Statement line is already reconciled\"." } }, { "name": "Reconcile - 400 journal entry not posted", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"matchedJournalNo\": \"JV-NONPOSTED\"\n}" }, "url": { "raw": "{{baseUrl}}/bank-statement-lines/2/reconcile", "host": [ "{{baseUrl}}" ], "path": [ "bank-statement-lines", "2", "reconcile" ] }, "description": "400. Message: \"Journal entry must be posted before it can be used for reconciliation\". Rare in practice now that create-and-post is one call (Rule 14) - would need a journal entry that failed post-time balance/lock checks, which never happens since those checks are now inside the same atomic create call." } }, { "name": "Reconcile - 400 amount mismatch", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"matchedJournalNo\": \"JV-2035-000002\"\n}" }, "url": { "raw": "{{baseUrl}}/bank-statement-lines/4/reconcile", "host": [ "{{baseUrl}}" ], "path": [ "bank-statement-lines", "4", "reconcile" ] }, "description": "400. Message: \"Statement line amount (500.00) does not match the journal entry's movement on the bank's GL account (300.00)\" (verified live with a 500 statement line vs a 300-net-movement journal entry)." } }, { "name": "Reconcile - 404 statement line not found", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"matchedJournalNo\": \"JV-2035-000001\"\n}" }, "url": { "raw": "{{baseUrl}}/bank-statement-lines/999999/reconcile", "host": [ "{{baseUrl}}" ], "path": [ "bank-statement-lines", "999999", "reconcile" ] }, "description": "404. Message: \"Bank statement line not found\"." } }, { "name": "Reconcile - 404 journal entry not found", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"matchedJournalNo\": \"JV-NOTFOUND\"\n}" }, "url": { "raw": "{{baseUrl}}/bank-statement-lines/2/reconcile", "host": [ "{{baseUrl}}" ], "path": [ "bank-statement-lines", "2", "reconcile" ] }, "description": "404. Message: \"Journal entry not found\" (checked before the posted-status check, so a nonexistent journalNo reports 404 not 400)." } }, { "name": "List Cash and Bank Accounts - success (unified, default Both)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/bank-accounts", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts" ] }, "description": "200 (retrofit 2026-07-22). Combines bank_account + cash_account into one flat, filterable list - { accountType, accountId, accountName, bankName, cashAccountTypeName, accountNumber, glAccountId, currencyCode, createdAt } per row (accountType is response-only, not a stored column)." } }, { "name": "List Cash and Bank Accounts - success (accountType=Bank)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/bank-accounts?accountType=Bank", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts" ], "query": [ { "key": "accountType", "value": "Bank" } ] }, "description": "200. Bank accounts only." } }, { "name": "List Cash and Bank Accounts - success (accountType=Cash)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/bank-accounts?accountType=Cash", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts" ], "query": [ { "key": "accountType", "value": "Cash" } ] }, "description": "200. Cash accounts only." } }, { "name": "List Cash and Bank Accounts - 400 invalid accountType", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/bank-accounts?accountType=Bogus", "host": [ "{{baseUrl}}" ], "path": [ "bank-accounts" ], "query": [ { "key": "accountType", "value": "Bogus" } ] }, "description": "400. Message: \"Invalid accountType 'Bogus'. Must be one of: Cash, Bank, Both.\"." } } ] }, { "name": "Budget", "item": [ { "name": "Create Budget - success", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"fiscalYearId\": 5,\n \"name\": \"Doc Test Budget 2035\"\n}" }, "url": { "raw": "{{baseUrl}}/budgets", "host": [ "{{baseUrl}}" ], "path": [ "budgets" ] }, "description": "201. lines: [] on creation - always added afterward via the lines endpoint." } }, { "name": "Create Budget - 404 fiscal year not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"fiscalYearId\": 999999,\n \"name\": \"bad\"\n}" }, "url": { "raw": "{{baseUrl}}/budgets", "host": [ "{{baseUrl}}" ], "path": [ "budgets" ] }, "description": "404. Message: \"Fiscal year not found\"." } }, { "name": "Add Lines - success (raw array body, not wrapped in an object)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "[\n { \"accountCode\": \"\", \"periodId\": 49, \"budgetedAmount\": 1000 },\n { \"accountCode\": \"\", \"periodId\": 49, \"costCenterCode\": \"\", \"budgetedAmount\": 500 }\n]" }, "url": { "raw": "{{baseUrl}}/budgets/2/lines", "host": [ "{{baseUrl}}" ], "path": [ "budgets", "2", "lines" ] }, "description": "201. accountCode/costCenterCode (retrofit 2026-07-22, CLAUDE.md Rule 8.2 - replace accountId/costCenterId; periodId stays numeric, AccountingPeriod has no unique code column). costCenterCode optional per line. No postable check on the account - budgeting against a summary/control account is allowed here, unlike journal entry lines." } }, { "name": "Add Lines - success (APPENDS, does not replace existing lines)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "[\n { \"accountCode\": \"\", \"periodId\": 49, \"budgetedAmount\": 250 }\n]" }, "url": { "raw": "{{baseUrl}}/budgets/2/lines", "host": [ "{{baseUrl}}" ], "path": [ "budgets", "2", "lines" ] }, "description": "201. Deliberately different from Journal Entries' now-removed 'Set Lines' (which replaced wholesale) - budget lines accumulate incrementally over time. Calling this twice results in 3 total lines, not 1." } }, { "name": "Add Lines - 404 account not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "[\n { \"accountCode\": \"NOTFOUND-XYZ\", \"periodId\": 49, \"budgetedAmount\": 100 }\n]" }, "url": { "raw": "{{baseUrl}}/budgets/2/lines", "host": [ "{{baseUrl}}" ], "path": [ "budgets", "2", "lines" ] }, "description": "404. Message: \"Account NOTFOUND-XYZ not found\"." } }, { "name": "Add Lines - 404 period not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "[\n { \"accountCode\": \"\", \"periodId\": 999999, \"budgetedAmount\": 100 }\n]" }, "url": { "raw": "{{baseUrl}}/budgets/2/lines", "host": [ "{{baseUrl}}" ], "path": [ "budgets", "2", "lines" ] }, "description": "404. Message: \"Period 999999 not found\"." } }, { "name": "Add Lines - 404 cost center not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "[\n { \"accountCode\": \"\", \"periodId\": 49, \"costCenterCode\": \"NOTFOUND-XYZ\", \"budgetedAmount\": 100 }\n]" }, "url": { "raw": "{{baseUrl}}/budgets/2/lines", "host": [ "{{baseUrl}}" ], "path": [ "budgets", "2", "lines" ] }, "description": "404. Message: \"Cost center NOTFOUND-XYZ not found\"." } }, { "name": "Add Lines - 404 budget not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "[\n { \"accountCode\": \"\", \"periodId\": 49, \"budgetedAmount\": 100 }\n]" }, "url": { "raw": "{{baseUrl}}/budgets/999999/lines", "host": [ "{{baseUrl}}" ], "path": [ "budgets", "999999", "lines" ] }, "description": "404. Message: \"Budget not found\"." } }, { "name": "List Budgets - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/budgets", "host": [ "{{baseUrl}}" ], "path": [ "budgets" ] }, "description": "200. Flat list. Note: data[].lines is always [] here - GetAllAsync doesn't eager-load lines, unlike the AddLines response which does return them. Use the BudgetVsActual report (GET /reports) to see a budget's lines with actuals." } } ] }, { "name": "Cheque Management", "item": [ { "name": "Create Cheque Book - success (auto-generates all leaves)", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"branchId\": 1,\n \"bankAccountId\": 1,\n \"chequeBookNo\": \"CB-2028-001\",\n \"startChequeNo\": \"000101\",\n \"endChequeNo\": \"000110\",\n \"totalLeaves\": 10,\n \"receivedDate\": \"2028-01-15\",\n \"description\": \"Doc test book\",\n \"createdBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-books", "host": [ "{{baseUrl}}" ], "path": [ "cheque-books" ] }, "description": "201. New module (03_Progress_Tracker.md Phase 37, added 2026-07-30). bankAccountId stays numeric - BankAccount has no unique code column (CLAUDE.md Rule 8.2's carve-out). branchId required, no companyId on this table, per explicit direction - plain unvalidated long, no Company/Branch table exists in this system. Auto-generates 10 ChequePage rows (000101-000110), all status Unused, in the same transaction - data.pages has all 10." } }, { "name": "Create Cheque Book - 404 bank account not found", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"branchId\": 1,\n \"bankAccountId\": 999999,\n \"chequeBookNo\": \"CB-2028-BAD1\",\n \"startChequeNo\": \"000201\",\n \"endChequeNo\": \"000210\",\n \"totalLeaves\": 10,\n \"receivedDate\": \"2028-01-15\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-books", "host": [ "{{baseUrl}}" ], "path": [ "cheque-books" ] }, "description": "404. Message: \"Bank account not found\"." } }, { "name": "Create Cheque Book - 400 chequeBookNo already exists", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"branchId\": 1,\n \"bankAccountId\": 1,\n \"chequeBookNo\": \"CB-2028-001\",\n \"startChequeNo\": \"000301\",\n \"endChequeNo\": \"000310\",\n \"totalLeaves\": 10,\n \"receivedDate\": \"2028-01-15\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-books", "host": [ "{{baseUrl}}" ], "path": [ "cheque-books" ] }, "description": "400 (run after the success case above, same chequeBookNo). Message: \"Cheque book number 'CB-2028-001' already exists\"." } }, { "name": "Create Cheque Book - 400 totalLeaves doesn't match the numeric range", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"branchId\": 1,\n \"bankAccountId\": 1,\n \"chequeBookNo\": \"CB-2028-BAD2\",\n \"startChequeNo\": \"000201\",\n \"endChequeNo\": \"000210\",\n \"totalLeaves\": 5,\n \"receivedDate\": \"2028-01-15\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-books", "host": [ "{{baseUrl}}" ], "path": [ "cheque-books" ] }, "description": "400. Message: \"totalLeaves (5) does not match the range startChequeNo-endChequeNo (10 cheques)\"." } }, { "name": "Create Cheque Book - 400 startChequeNo/endChequeNo not numeric", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"branchId\": 1,\n \"bankAccountId\": 1,\n \"chequeBookNo\": \"CB-2028-BAD3\",\n \"startChequeNo\": \"ABC\",\n \"endChequeNo\": \"XYZ\",\n \"totalLeaves\": 10,\n \"receivedDate\": \"2028-01-15\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-books", "host": [ "{{baseUrl}}" ], "path": [ "cheque-books" ] }, "description": "400. Message: \"startChequeNo and endChequeNo must be numeric\"." } }, { "name": "Get Cheque Book - success (no expand, pages: [])", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cheque-books/CB-2028-001", "host": [ "{{baseUrl}}" ], "path": [ "cheque-books", "CB-2028-001" ] }, "description": "200. Looked up by chequeBookNo (CLAUDE.md Rule 8), not the numeric chequeBookId. expand omitted -> data.pages: []." } }, { "name": "Get Cheque Book - success (?expand=pages)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cheque-books/CB-2028-001?expand=pages", "host": [ "{{baseUrl}}" ], "path": [ "cheque-books", "CB-2028-001" ], "query": [ { "key": "expand", "value": "pages" } ] }, "description": "200. data.pages has the 10 generated ChequePage rows." } }, { "name": "Get Cheque Book - 404 not found", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cheque-books/NOTFOUND-XYZ", "host": [ "{{baseUrl}}" ], "path": [ "cheque-books", "NOTFOUND-XYZ" ] }, "description": "404. Message: \"Cheque book not found\"." } }, { "name": "List Cheque Books - success (filters + paging)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cheque-books?bankAccountId=1&branchId=1&status=Active&page=1&pageSize=10", "host": [ "{{baseUrl}}" ], "path": [ "cheque-books" ], "query": [ { "key": "bankAccountId", "value": "1" }, { "key": "branchId", "value": "1" }, { "key": "status", "value": "Active" }, { "key": "page", "value": "1" }, { "key": "pageSize", "value": "10" } ] }, "description": "200. All filters optional and independent. Ordered by receivedDate descending, then chequeBookNo." } }, { "name": "Issue Cheque - success", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"payeeType\": \"Supplier\",\n \"payeeName\": \"ACME Supplies\",\n \"issueDate\": \"2028-01-16\",\n \"amount\": 15000.50,\n \"referenceNo\": \"INV-9001\",\n \"purpose\": \"Payment for goods\",\n \"isCrossCheque\": true,\n \"isAccountPayee\": true,\n \"printedBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000101/issue", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000101", "issue" ] }, "description": "200. payeeType (Supplier|Customer|Employee|Other) sent as a STRING, parsed server-side via Enum.TryParse - this project has no global JsonStringEnumConverter (same pattern as recurrencePattern). payeeId omitted -> null (loose reference, no Supplier/Customer/Employee table exists here). currencyCode omitted -> defaults LKR. Only a page currently Unused can be issued. issueStatus -> Issued (2), printedAt set to now." } }, { "name": "Issue Cheque - 400 page must be Unused", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"payeeType\": \"Supplier\",\n \"payeeName\": \"ACME Supplies\",\n \"issueDate\": \"2028-01-16\",\n \"amount\": 100\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000101/issue", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000101", "issue" ] }, "description": "400 (run a second time on the same, now-Issued cheque). Message: \"Cheque page must be Unused to be issued (currently Issued)\"." } }, { "name": "Issue Cheque - 400 invalid payeeType", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"payeeType\": \"Bogus\",\n \"payeeName\": \"x\",\n \"issueDate\": \"2028-01-16\",\n \"amount\": 100\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000102/issue", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000102", "issue" ] }, "description": "400. Message: \"Invalid payeeType 'Bogus'. Must be one of: Supplier, Customer, Employee, Other.\"." } }, { "name": "Issue Cheque - 400 amount not > 0", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"payeeType\": \"Customer\",\n \"payeeName\": \"x\",\n \"issueDate\": \"2028-01-16\",\n \"amount\": 0\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000102/issue", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000102", "issue" ] }, "description": "400. Message: \"amount must be greater than 0\"." } }, { "name": "Issue Cheque - 404 chequeNo not found", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"payeeType\": \"Customer\",\n \"payeeName\": \"x\",\n \"issueDate\": \"2028-01-16\",\n \"amount\": 100\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/NOTFOUND/issue", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "NOTFOUND", "issue" ] }, "description": "404. Message: \"Cheque page not found\"." } }, { "name": "Issue Cheque - success (second page, Customer payee, for the status-transition requests below)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"payeeType\": \"Customer\",\n \"payeeName\": \"Some Customer Refund\",\n \"issueDate\": \"2028-01-16\",\n \"amount\": 500\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000102/issue", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000102", "issue" ] }, "description": "200. Issues 000102 so the Bounce example below has an Issued page to act on." } }, { "name": "Cheque Page Status - Clear success (requires clearedDate)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Clear\",\n \"clearedDate\": \"2028-01-20\",\n \"performedBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000101/status", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000101", "status" ] }, "description": "200. Action-based, same pattern as PUT /periods/{id}/lock. Only valid from Issued. issueStatus -> Cleared (3), clearedByBank set true." } }, { "name": "Cheque Page Status - Bounce success", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Bounce\",\n \"performedBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000102/status", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000102", "status" ] }, "description": "200. Only valid from Issued. issueStatus -> Bounced (4)." } }, { "name": "Cheque Page Status - Cancel success (requires cancelReason, from Unused)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Cancel\",\n \"cancelReason\": \"Printing error\",\n \"performedBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000103/status", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000103", "status" ] }, "description": "200. Valid from Unused or Issued. issueStatus -> Cancelled (5)." } }, { "name": "Cheque Page Status - Void success (from Unused only)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Void\",\n \"performedBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000104/status", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000104", "status" ] }, "description": "200. Valid from Unused only. issueStatus -> Void (6)." } }, { "name": "Cheque Page Status - 400 Clear on a still-Unused page", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Clear\",\n \"clearedDate\": \"2028-01-20\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000105/status", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000105", "status" ] }, "description": "400. Message: \"Cheque must be Issued to be cleared (currently Unused)\"." } }, { "name": "Cheque Page Status - 400 invalid action", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Bogus\"\n}" }, "url": { "raw": "{{baseUrl}}/cheque-pages/000106/status", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000106", "status" ] }, "description": "400. Message: \"Invalid action 'Bogus'. Must be one of: Clear, Bounce, Cancel, Void.\"." } }, { "name": "Get Cheque Page - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cheque-pages/000101", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "000101" ] }, "description": "200. Looked up by chequeNo. Includes the parent chequeBook." } }, { "name": "Get Cheque Page - 404 not found", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cheque-pages/NOTFOUND", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages", "NOTFOUND" ] }, "description": "404. Message: \"Cheque page not found\"." } }, { "name": "List Cheque Pages - success (filters + paging)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/cheque-pages?chequeBookNo=CB-2028-001&issueStatus=Cleared&page=1&pageSize=10", "host": [ "{{baseUrl}}" ], "path": [ "cheque-pages" ], "query": [ { "key": "chequeBookNo", "value": "CB-2028-001" }, { "key": "issueStatus", "value": "Cleared" }, { "key": "page", "value": "1" }, { "key": "pageSize", "value": "10" } ] }, "description": "200. All filters optional and independent (chequeBookNo, issueStatus, payeeType, payeeId). Ordered by chequeNo." } }, { "name": "Create Received Cheque - success", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"companyId\": 1,\n \"branchId\": 1,\n \"receivedFromType\": \"Customer\",\n \"receivedFromName\": \"Big Customer Ltd\",\n \"drawerBankName\": \"HNB\",\n \"drawerBankBranch\": \"Colombo 03\",\n \"accountHolderName\": \"Big Customer Ltd\",\n \"chequeNo\": \"777001\",\n \"chequeDate\": \"2028-01-18\",\n \"amount\": 25000,\n \"receivedDate\": \"2028-01-19\",\n \"referenceType\": \"Invoice\",\n \"referenceId\": 501,\n \"createdBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques" ] }, "description": "201. No chequeBookId link, by design - this is a cheque received from the other side, not from this company's own cheque book (per the client's spec). companyId required, branchId nullable (asymmetric with ChequeBook, per explicit direction). drawerBankName/drawerBankBranch are plain free text, not a FK - no bank-master table exists in this service, same precedent as BankAccount.BankName. receivedFromType (Customer|Supplier|Other) sent as a string, parsed server-side. Status always defaults to Received (1)." } }, { "name": "Create Received Cheque - 400 chequeNo missing", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"companyId\": 1,\n \"receivedFromType\": \"Customer\",\n \"receivedFromName\": \"x\",\n \"chequeDate\": \"2028-01-18\",\n \"amount\": 100,\n \"receivedDate\": \"2028-01-19\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques" ] }, "description": "400. Message: \"chequeNo is required\"." } }, { "name": "Create Received Cheque - 400 invalid receivedFromType", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"companyId\": 1,\n \"receivedFromType\": \"Bogus\",\n \"receivedFromName\": \"x\",\n \"chequeNo\": \"777002\",\n \"chequeDate\": \"2028-01-18\",\n \"amount\": 100,\n \"receivedDate\": \"2028-01-19\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques" ] }, "description": "400. Message: \"Invalid receivedFromType 'Bogus'. Must be one of: Customer, Supplier, Other.\"." } }, { "name": "Get Received Cheque - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/received-cheques/1", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques", "1" ] }, "description": "200. Looked up by the numeric id - ChequeNo isn't guaranteed globally unique across different drawers/banks here, unlike our own ChequeBook/ChequePage." } }, { "name": "Get Received Cheque - 404 not found", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/received-cheques/999999", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques", "999999" ] }, "description": "404. Message: \"Received cheque not found\"." } }, { "name": "Received Cheque Status - Deposit success (requires depositBankAccountId + depositDate)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Deposit\",\n \"depositBankAccountId\": 1,\n \"depositDate\": \"2028-01-20\",\n \"performedBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques/1/status", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques", "1", "status" ] }, "description": "200. Only valid from Received. depositBankAccountId validated against a real bank account (numeric - BankAccount has no unique code column, same carve-out as ChequeBook.bankAccountId). status -> Deposited (2)." } }, { "name": "Received Cheque Status - Clear success (from Deposited)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Clear\",\n \"performedBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques/1/status", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques", "1", "status" ] }, "description": "200. Only valid from Deposited. status -> Cleared (3)." } }, { "name": "Received Cheque Status - Return success (from Deposited)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Return\",\n \"performedBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques/2/status", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques", "2", "status" ] }, "description": "200 (run against a different received cheque, still Deposited). Only valid from Deposited. status -> Returned (4)." } }, { "name": "Received Cheque Status - Cancel success (from Received)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Cancel\",\n \"performedBy\": \"Doc Tester\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques/3/status", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques", "3", "status" ] }, "description": "200 (run against a still-Received cheque). Only valid from Received. status -> Cancelled (5)." } }, { "name": "Received Cheque Status - 400 wrong current status", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Deposit\",\n \"depositBankAccountId\": 1,\n \"depositDate\": \"2028-01-20\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques/1/status", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques", "1", "status" ] }, "description": "400 (run a second time, after the Deposit + Clear requests above already moved this record past Received). Message: \"Received cheque must be in Received status to be deposited (currently Cleared)\"." } }, { "name": "Received Cheque Status - 404 depositBankAccountId not found", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Deposit\",\n \"depositBankAccountId\": 999999,\n \"depositDate\": \"2028-01-20\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques/2/status", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques", "2", "status" ] }, "description": "404. Message: \"Deposit bank account not found\"." } }, { "name": "Received Cheque Status - 400 invalid action", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"action\": \"Bogus\"\n}" }, "url": { "raw": "{{baseUrl}}/received-cheques/2/status", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques", "2", "status" ] }, "description": "400. Message: \"Invalid action 'Bogus'. Must be one of: Deposit, Clear, Return, Cancel.\"." } }, { "name": "List Received Cheques - success (filters + paging)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/received-cheques?companyId=1&status=Cleared&page=1&pageSize=10", "host": [ "{{baseUrl}}" ], "path": [ "received-cheques" ], "query": [ { "key": "companyId", "value": "1" }, { "key": "status", "value": "Cleared" }, { "key": "page", "value": "1" }, { "key": "pageSize", "value": "10" } ] }, "description": "200. All filters optional and independent (companyId, branchId, status, receivedFromType). Ordered by receivedDate descending." } } ] }, { "name": "Reporting", "item": [ { "name": "TrialBalance - Json", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=TrialBalance&outputFormat=Json&asOfDate=2035-12-31", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "TrialBalance" }, { "key": "outputFormat", "value": "Json" }, { "key": "asOfDate", "value": "2035-12-31" } ] }, "description": "200. Requires asOfDate. Rows: { depth, indentedCode, accountName, debit, credit } - only postable accounts with a non-zero posted balance as of the date." } }, { "name": "BalanceSheet - Json", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=BalanceSheet&outputFormat=Json&asOfDate=2035-12-31", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "BalanceSheet" }, { "key": "outputFormat", "value": "Json" }, { "key": "asOfDate", "value": "2035-12-31" } ] }, "description": "200. Requires asOfDate. Asset/Liability/Equity account types only (account_type_id IN (1,2,3)). Rows: { depth, lineItem, accountType, balance } - balance is a rolled-up subtree total, not just the leaf's own postings." } }, { "name": "GeneralLedger - Json (single account + descendants)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=GeneralLedger&outputFormat=Json&accountCode=&periodStart=2035-01-01&periodEnd=2035-12-31", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "GeneralLedger" }, { "key": "outputFormat", "value": "Json" }, { "key": "accountCode", "value": "" }, { "key": "periodStart", "value": "2035-01-01" }, { "key": "periodEnd", "value": "2035-12-31" } ] }, "description": "200. accountCode (retrofit 2026-07-22, CLAUDE.md Rule 8.2 - replaces accountId, closing the gap this report used to be the one exception to) + periodStart + periodEnd. accountCode is now optional (retrofit 2026-07-20 - two modes): supplied here -> includes that account's own postings AND all descendant accounts' postings (recursive), with a running balance column." } }, { "name": "GeneralLedger - Json (accountCode omitted - every postable account)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=GeneralLedger&outputFormat=Json&periodStart=2035-01-01&periodEnd=2035-12-31", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "GeneralLedger" }, { "key": "outputFormat", "value": "Json" }, { "key": "periodStart", "value": "2035-01-01" }, { "key": "periodEnd", "value": "2035-12-31" } ] }, "description": "200. accountCode omitted -> a true General Ledger across every postable account (no descendant CTE, just filters is_postable = TRUE), not just one account + its descendants." } }, { "name": "GeneralLedger - 404 accountCode supplied but not found", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=GeneralLedger&outputFormat=Json&accountCode=NOTFOUND-XYZ&periodStart=2035-01-01&periodEnd=2035-12-31", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "GeneralLedger" }, { "key": "outputFormat", "value": "Json" }, { "key": "accountCode", "value": "NOTFOUND-XYZ" }, { "key": "periodStart", "value": "2035-01-01" }, { "key": "periodEnd", "value": "2035-12-31" } ] }, "description": "404. Message: \"Account not found\" - only checked when accountCode is present at all." } }, { "name": "ProfitAndLoss - Json", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=ProfitAndLoss&outputFormat=Json&periodStart=2035-01-01&periodEnd=2035-12-31", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "ProfitAndLoss" }, { "key": "outputFormat", "value": "Json" }, { "key": "periodStart", "value": "2035-01-01" }, { "key": "periodEnd", "value": "2035-12-31" } ] }, "description": "200. Requires periodStart + periodEnd. Income/Expense account types only (4,5). Rows: { depth, lineItem, accountType, amount }." } }, { "name": "CashFlow - Json (documented simplification, not full LKAS 7)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=CashFlow&outputFormat=Json&periodStart=2035-01-01&periodEnd=2035-12-31", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "CashFlow" }, { "key": "outputFormat", "value": "Json" }, { "key": "periodStart", "value": "2035-01-01" }, { "key": "periodEnd", "value": "2035-12-31" } ] }, "description": "200. Requires periodStart + periodEnd. Returns a single object, not an array: { periodStart, periodEnd, netProfitForPeriod, netCashFromInvestingActivities, netCashFromFinancingActivities, netIncreaseDecreaseInCash }. Simplification: Operating = P&L's net profit (no non-cash/working-capital adjustments - the schema has no account-level tag for those); Investing/Financing come from the cash_flow_category rollup; no opening/closing cash balance line (no reliable way to identify 'cash' accounts from the current schema). Not full LKAS 7 compliance - see 03_Progress_Tracker.md." } }, { "name": "BudgetVsActual - Json", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=BudgetVsActual&outputFormat=Json&budgetId=2", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "BudgetVsActual" }, { "key": "outputFormat", "value": "Json" }, { "key": "budgetId", "value": "2" } ] }, "description": "200. Requires budgetId. Rows: { budgetLineId, accountCode, accountName, periodId, budgetedAmount, actualAmount, variance }. This is the endpoint to use for a budget's lines with real figures - GET /budgets itself never returns lines (see Budget module notes)." } }, { "name": "TrialBalance - Pdf", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=TrialBalance&outputFormat=Pdf&asOfDate=2035-12-31", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "TrialBalance" }, { "key": "outputFormat", "value": "Pdf" }, { "key": "asOfDate", "value": "2035-12-31" } ] }, "description": "200 (not a raw binary response - stays inside the normal ApiResponse envelope, per CLAUDE.md Rule 7). data: { fileName, contentType: \"application/pdf\", contentBase64 }. Client must base64-decode contentBase64 to get real PDF bytes (verified live: decodes to a file starting with %PDF-1.4). Every reportType supports outputFormat=Pdf via the same shared reflection-based table renderer, not just TrialBalance." } }, { "name": "400 - missing/empty reportType", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?outputFormat=Json", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "outputFormat", "value": "Json" } ] }, "description": "400. Message: \"Invalid reportType ''. Must be one of: GeneralLedger, TrialBalance, BalanceSheet, ProfitAndLoss, CashFlow, BudgetVsActual.\"." } }, { "name": "400 - invalid reportType", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=Bogus&outputFormat=Json", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "Bogus" }, { "key": "outputFormat", "value": "Json" } ] }, "description": "400. Same message shape as the missing case above." } }, { "name": "400 - invalid outputFormat", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=TrialBalance&outputFormat=Bogus&asOfDate=2035-12-31", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "TrialBalance" }, { "key": "outputFormat", "value": "Bogus" }, { "key": "asOfDate", "value": "2035-12-31" } ] }, "description": "400. Message: \"Invalid outputFormat 'Bogus'. Must be one of: Json, Pdf.\"." } }, { "name": "400 - missing required parameter for reportType (TrialBalance without asOfDate)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=TrialBalance&outputFormat=Json", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "TrialBalance" }, { "key": "outputFormat", "value": "Json" } ] }, "description": "400. Message names the specific missing parameter: \"asOfDate is required for the TrialBalance report\". Each reportType has its own required-parameter set, validated in the Service before dispatch. (Not accountCode/GeneralLedger - accountCode is optional there since the 2026-07-20 retrofit, two modes: single-account+descendants when supplied, every postable account when omitted.)" } }, { "name": "404 - BudgetVsActual, budgetId not found", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/reports?reportType=BudgetVsActual&outputFormat=Json&budgetId=999999", "host": [ "{{baseUrl}}" ], "path": [ "reports" ], "query": [ { "key": "reportType", "value": "BudgetVsActual" }, { "key": "outputFormat", "value": "Json" }, { "key": "budgetId", "value": "999999" } ] }, "description": "404. Message: \"Budget not found\" - the only reportType that does an existence check before running its query (the others simply return an empty array if nothing matches)." } } ] }, { "name": "System Config", "item": [ { "name": "Update Config - success (auto-locks)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"configValue\": \"5\"\n}" }, "url": { "raw": "{{baseUrl}}/system-config/COST_CENTER_MAX_DEPTH", "host": [ "{{baseUrl}}" ], "path": [ "system-config", "COST_CENTER_MAX_DEPTH" ] }, "description": "200. Every successful write sets isLocked:true in the same call - each key is editable exactly once via this endpoint, ever. Audit category is per-key (a static lookup dictionary; all 3 current keys map to Ledger, any future unmapped key defaults to Administrative)." } }, { "name": "Update Config - 400 already locked", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"configValue\": \"6\"\n}" }, "url": { "raw": "{{baseUrl}}/system-config/COST_CENTER_MAX_DEPTH", "host": [ "{{baseUrl}}" ], "path": [ "system-config", "COST_CENTER_MAX_DEPTH" ] }, "description": "400 (call a second time on the same key). Message: \"System config key 'COST_CENTER_MAX_DEPTH' cannot be edited\". Rule: reject if !IsEditable || IsLocked." } }, { "name": "Update Config - 404 key not found", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" }, { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "body": { "mode": "raw", "raw": "{\n \"configValue\": \"x\"\n}" }, "url": { "raw": "{{baseUrl}}/system-config/NOTFOUND_KEY", "host": [ "{{baseUrl}}" ], "path": [ "system-config", "NOTFOUND_KEY" ] }, "description": "404. Message: \"System config key not found\"." } }, { "name": "List Config - success", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/system-config", "host": [ "{{baseUrl}}" ], "path": [ "system-config" ] }, "description": "200. All seeded keys: COA_MAX_DEPTH, COST_CENTER_MAX_DEPTH, COA_ENFORCE_MAX_DEPTH." } } ] }, { "name": "Audit Log", "item": [ { "name": "List - success (no filters)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/audit-log", "host": [ "{{baseUrl}}" ], "path": [ "audit-log" ] }, "description": "200. category defaults to both LEDGER+ADMINISTRATIVE when omitted. No page/pageSize -> full result set (same optional-paging pattern as Accounts)." } }, { "name": "List - success (category=LEDGER)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/audit-log?category=LEDGER", "host": [ "{{baseUrl}}" ], "path": [ "audit-log" ], "query": [ { "key": "category", "value": "LEDGER" } ] }, "description": "200. category is a repeatable query param: ?category=LEDGER&category=ADMINISTRATIVE selects both explicitly (same result as omitting it)." } }, { "name": "List - success (entityName + action filters)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/audit-log?entityName=Account&action=Create", "host": [ "{{baseUrl}}" ], "path": [ "audit-log" ], "query": [ { "key": "entityName", "value": "Account" }, { "key": "action", "value": "Create" } ] }, "description": "200. entityName/performedBy are exact-match string filters." } }, { "name": "List - success (invalid category -> empty result, not an error)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/audit-log?category=BOGUS", "host": [ "{{baseUrl}}" ], "path": [ "audit-log" ], "query": [ { "key": "category", "value": "BOGUS" } ] }, "description": "200. data: { items: [], totalCount: 0, page: null, pageSize: null }. A typo'd filter value is a read-side no-match, not a validation error - deliberate, documented design choice." } }, { "name": "List - success (invalid action -> empty result)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/audit-log?action=BOGUS", "host": [ "{{baseUrl}}" ], "path": [ "audit-log" ], "query": [ { "key": "action", "value": "BOGUS" } ] }, "description": "200. Same treatment as an invalid category - empty result, 200, not 400." } }, { "name": "List - success (date range on occurredAt)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/audit-log?from=2026-07-19&to=2026-07-19", "host": [ "{{baseUrl}}" ], "path": [ "audit-log" ], "query": [ { "key": "from", "value": "2026-07-19" }, { "key": "to", "value": "2026-07-19" } ] }, "description": "200. from/to filter occurredAt (when the audit row was written - real wall-clock time), NOT any business/entryDate field on the underlying record. A journal entry dated far in the future/past still writes its audit_log row with occurredAt = right now." } }, { "name": "List - success (page+pageSize)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/audit-log?page=1&pageSize=3", "host": [ "{{baseUrl}}" ], "path": [ "audit-log" ], "query": [ { "key": "page", "value": "1" }, { "key": "pageSize", "value": "3" } ] }, "description": "200. Ordered by occurredAt descending (newest first)." } } ] }, { "name": "Dev Tools", "item": [ { "name": "Generate API Key - success (Development only)", "request": { "method": "POST", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/dev-tools/generate-api-key", "host": [ "{{baseUrl}}" ], "path": [ "dev-tools", "generate-api-key" ] }, "description": "200. No X-Api-Key needed - this path is excluded from ApiKeyMiddleware (it's the bootstrap tool that produces the very first key). Guarded separately: throws 403 outside the Development environment, verified live (Hosting environment: Production -> 403). Does not persist the key anywhere - copy data.apiKey into appsettings.*.json and the core backend's config by hand." } } ] }, { "name": "Status & Health (unauthenticated / excluded from ApiKeyMiddleware)", "item": [ { "name": "GET /status", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "{{baseUrl}}/status", "host": [ "{{baseUrl}}" ], "path": [ "status" ] }, "description": "200. { service, status, timestampUtc }. NOT an ApiResponse - this is the one endpoint that predates that convention and still returns Ok(new {...}) directly from the Controller. Still requires X-Api-Key (not excluded from ApiKeyMiddleware)." } }, { "name": "GET /health (no auth)", "request": { "method": "GET", "header": [ { "key": "X-Api-Key", "value": "{{apiKey}}" } ], "url": { "raw": "http://localhost:5299/health", "host": [ "http://localhost:5299" ], "path": [ "health" ] }, "description": "200, plain text \"Healthy\". Excluded from ApiKeyMiddleware and lives outside the /api/v1 prefix entirely (ASP.NET Core's built-in health check middleware, not an action on GeneralLedgerController)." } } ] } ] }