Files
Distribution_GL/Docs/03_Progress_Tracker.md
T

724 lines
172 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# General Ledger Service — Development Progress Tracker
This file is the single source of truth for **what has actually been built so far**. It is checked **before starting any work** (see `CLAUDE.md` Rule 0) and updated **immediately after** a phase is confirmed working — never before, and never as a batch at the end of a session.
Status values used below:
- `☐ Not started` — no code written yet
- `◐ In progress` — code written, not yet confirmed working end-to-end
- `☑ Done` — implemented **and** manually confirmed working (per that phase's acceptance test in `02_Phase_By_Phase_Development_Plan.md`)
- `⚠ Blocked` — can't proceed; reason noted in the "Notes" column
- `✕ Removed` — phase permanently out of scope, will not be implemented; reason noted in the "Notes" column. Its phase number is retired, not reused (see the "no renumbering, ever" rule below)
**Phase numbers here match `02_Phase_By_Phase_Development_Plan.md` and `01_Full_Development_Plan.md` Section 6.2 exactly** — same numbering, same order, no renumbering, ever. If a phase is skipped, removed, or reordered, this file is updated to say so explicitly rather than silently drifting from the plan documents.
**27 API endpoints in the original numbered sequence** (down from an original 40 — see `01_Full_Development_Plan.md` Section 6.1 for the full consolidation/removal history), **plus 6 appended list endpoints (Phases 30–35, §6.3) and 1 appended audit-log endpoint (Phase 36, §6.4): 34 API endpoints, 36 phases total (Phase 18's slot counts toward the phase total but has no endpoint — removed 2026-07-18).**
**2026-07-18 note:** this file was found missing from disk (not renamed, not recoverable from git — never committed) partway through the `audit_log` retrofit work. Recreated from this session's own record, per explicit user confirmation. If anything below looks off relative to the actual codebase, flag it per Rule 0.4 rather than trusting this reconstruction blindly.
---
## How to update this file
- When starting a phase: change its status to `◐ In progress`, add today's date under "Started."
- When a phase's acceptance test passes: change its status to `☑ Done`, add today's date under "Completed." For the consolidated phases (3, 4, 10, 11, 12, 13, 17, 22 — see the "Consolidated?" column below), the test must cover every branch of the discriminator before the phase counts as done, per Document 2's closing "How to Use" section.
- If a phase is blocked: change its status to `⚠ Blocked` and explain why in "Notes" — do not silently move on to a later phase while an earlier one is blocked, unless that's explicitly approved (see `CLAUDE.md` Rule 3).
- If a phase is removed from scope entirely: change its status to `✕ Removed` and explain why in "Notes." **Phase 18 is in this state** — see its row below.
- This file only records the two facts above — status and dates. It is not a place for design discussion; that stays in the plan documents or the conversation itself.
---
## Phase 1 — Models, DbContext, Migration, Default Data, API Key Middleware, Versioning
*(No API call — this is the foundation every later phase depends on. Must be fully done before Phase 2 starts.)*
| Step | Description | Status | Started | Completed | Notes |
|---|---|---|---|---|---|
| 1.1 | Remove `Interfaces/Repositories/`, `Interfaces/Services/` folders + `.csproj` references | ☑ Done | 2026-07-17 | 2026-07-17 | Folders never existed on disk; removed the two stale `<Folder Include>` lines from the `.csproj` |
| 1.2 | Create all 24 entity classes in `Models/` | ☑ Done | 2026-07-17 | 2026-07-17 | Includes `JournalEntry.Narration` (approved addition, not in the original ER diagram). **Retrofit 2026-07-18:** added a 25th model, `Models/AuditLog.cs`, for the new generic audit-log table (`CLAUDE.md` Rule 10) |
| 1.3 | `AppDBContext.cs` — `DbSet<T>` + Fluent API (recursive FKs, precision, indexes) + `HasData` seeding (incl. `Currency`) | ☑ Done | 2026-07-17 | 2026-07-17 | Configured explicit snake_case table names + a snake_case column/key/index naming pass (see Deviations). **Retrofit 2026-07-18:** added `DbSet<AuditLog>` + `ConfigureAuditLog` (no FK, by design), migration `AddAuditLog` |
| 1.4 | `ApiKeyMiddleware` + `appsettings.*` `ApiKey:Value` entries + `Program.cs` registration | ☑ Done | 2026-07-17 | 2026-07-17 | |
| 1.5 | `Asp.Versioning.Mvc` package + `Program.cs` versioning registration + `GeneralLedgerController` moved to `v1` | ☑ Done | 2026-07-17 | 2026-07-17 | Package reference was already present in the `.csproj`; added `AddApiVersioning` + controller attributes |
| 1.6 | Migration created and applied to local PostgreSQL | ☑ Done | 2026-07-17 | 2026-07-17 | `InitialCreate`, applied to `General_Ledger` on localhost. See Deviations for the full chain of later migrations |
| 1.7 | Acceptance criteria fully verified (see plan doc for the exact checklist) | ☑ Done | 2026-07-17 | 2026-07-17 | All checks passed at the time: 5/3/5/1 seed row counts, versioned status endpoint with/without/wrong API key, `/health` + `/swagger` unprotected. **Currency count later grew from 1 to 142** (full ISO 4217 list, 2026-07-18 — see Deviations) — the "1" above reflects what was true on 2026-07-17, not a discrepancy |
| 1.8 | Exact `HasData` seed values documented (`account_type`, `system_config`, `tax_type`, `currency`) — `01_Full_Development_Plan.md` §5.2–5.3 | ☑ Done | 2026-07-19 | 2026-07-19 | Documentation formalization, no code change — values transcribed from the already-live, already-tested `AppDBContext.cs`/migrations, not newly invented. Captures the corrected `is_editable`/`is_locked` semantics and the `Currency.DecimalPlaces` `HasData`-default bug in one canonical place instead of only in the Deviations log |
| 1.9 | `Company:Name`/`Address`/`Tin`/`Brn` added to `appsettings.json` (all environments) — for LKAS 1.51 statement headers and the Income Tax Computation report's fuller identity block | ☑ Done | 2026-07-22 | 2026-07-22 | Verified live — rendered correctly on the Income Tax Computation PDF header |
| 1.10 | `CASH_ACCOUNT_ROOT_ID` — new `system_config` key, set via `PUT /system-config` | ☑ Done | 2026-07-21 | 2026-07-21 | Already recorded further down this file's 2026-07-21 entry |
| 1.11 | `accounts.income_statement_section` — new column | ☑ Done | 2026-07-22 | 2026-07-22 | Migration `AddIncomeStatementSectionTaxDeductibleAndCashAccounts`, applied. Verified live via the `ProfitAndLoss` report (see this file's 2026-07-22 entry below) |
| 1.12 | `accounts.is_tax_deductible` column + 5 new tax-related `system_config` keys | ☑ Done | 2026-07-22 | 2026-07-22 | Same migration as 1.11. `CORPORATE_INCOME_TAX_RATE`/`STATUTORY_RELIEF_AMOUNT` seeded via `HasData`; `APIT_CREDIT_ACCOUNT_ID`/`WHT_CREDIT_ACCOUNT_ID`/`QUARTERLY_TAX_PAYMENT_ACCOUNT_ID` seeded empty, same pattern as `CASH_ACCOUNT_ROOT_ID` |
**Phase 1 overall status:** ☑ Done (all of 1.1–1.12)
---
## Phases 2–29 — One API Endpoint Each
| Phase | Module | Endpoint | Consolidated? | Status | Started | Completed | Notes |
|---|---|---|---|---|---|---|---|
| 2 | Accounts | `POST /api/v1/accounts` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Also added `ExceptionHandlingMiddleware` (see Deviations) and a minimal `SystemConfigRepository`. **Audit retrofit 2026-07-18:** writes one `audit_log` row (`Ledger`/`Create`) per account created |
| 3 | Accounts | `GET /api/v1/accounts/{accountCode}` (+`?expand=tree`) | Merges old tree endpoint | ☑ Done | 2026-07-17 | 2026-07-17 | Retrofitted from `{id}` to `{accountCode}` lookup (see Deviations, Rule 8). Read-only — no audit row |
| 4 | Accounts | `PUT /api/v1/accounts/{accountCode}` | Merges old reparent-only endpoint | ☑ Done | 2026-07-17 | 2026-07-17 | Tested rename, re-parent, cycle rejection (400), explicit-null clear-parent, self-parent rejection (400), and 404. **Audit retrofit 2026-07-18:** writes one `audit_log` row, category computed dynamically — `Ledger` if `parentAccountId` changed, `Administrative` otherwise; verified both branches via `psql` |
| 5 | Accounts | `GET /api/v1/accounts` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Tested unfiltered list, `accountTypeId` filter, `isPostable` filter + paging, and an empty-result filter. Read-only — no audit row |
| 6 | Fiscal Calendar | `POST /api/v1/fiscal-years` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Built and tested together with Phases 7–8 (see Deviations). Tested with and without `autoGeneratePeriods`, plus overlap rejection (400). **Audit retrofit 2026-07-18:** writes one `audit_log` row (`Ledger`/`Create`) per fiscal year created |
| 7 | Fiscal Calendar | `POST /api/v1/fiscal-years/{id}/periods` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Tested initial 12-period generation, extend-past-fiscal-year-end rejection (400), and 404 for a missing fiscal year. **Audit retrofit 2026-07-18:** writes one `audit_log` row (`Ledger`/`Create`), `entityId` = the fiscal year, describing how many periods were generated |
| 8 | Fiscal Calendar | `PUT /api/v1/periods/{id}/lock` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Tested LOCK, invalid action (400), 404, CLOSE then reject-other-actions (400), and UNLOCK reopening a CLOSED period. **Audit retrofit 2026-07-18:** writes one `audit_log` row (`Ledger`), `action` mapped to the specific lock action taken (`Lock`/`Unlock`/`SoftClose`/`Close`), alongside the existing `period_lock_log` row it doesn't replace |
| 9 | Cost Centers | `POST /api/v1/cost-centers` | — | ☑ Done | 2026-07-18 | 2026-07-18 | Built and tested together with Phase 10. Tested root create, child create, duplicate-code rejection (400), missing-parent (404), and depth-guard rejection at depth 5 (`COST_CENTER_MAX_DEPTH=4`, unconditional — no separate enforce-flag exists for cost centers). Writes one `audit_log` row (`Administrative`/`Create`) — verified via `psql` |
| 10 | Cost Centers | `GET /api/v1/cost-centers/{costCenterCode}` (+`?expand=tree`) | Merges old tree endpoint | ☑ Done | 2026-07-18 | 2026-07-18 | Retrofitted route from `{id}` to `{costCenterCode}` in Documents 1/2 before building (Rule 8). Tested plain get, `?expand=tree` (breadcrumb + full 4-level descendant tree), and 404 for a missing code |
| 11 | Journal Entries | `POST /api/v1/journal-entries` | Merges old create + `/reverse` + `/adjust`; **retrofit 2026-07-22: also absorbs Phases 12/13** (see their rows) — `lines` now required, entry is created and posted in one call, `accountCode`/`costCenterCode`/`originalJournalNo` replace numeric ids | ☑ Done | 2026-07-18 | 2026-07-22 | **2026-07-18 history:** tested plain create (no lines), plain create with inline lines, `FullReversal` (mirrors original's lines, sets `reversalOfId`), `PartialAdjustment` (empty draft, lines added via the then-separate Phase 12), correction against a non-`POSTED` original (400), invalid `correctionType` (400). **2026-07-22 retrofit, verified live:** plain create auto-posts (`isPosted=true`, `entryStatus=2`, in the one `POST` call); `FullReversal` correction auto-posts and flips the original to `Reversed` in the same call; `PartialAdjustment` correction now takes `lines` inline (no follow-up call) and auto-posts; removed `PUT .../lines` and `PUT .../post` both confirmed `404` |
| 12 | Journal Entries | ~~`PUT /api/v1/journal-entries/{journalNo}/lines`~~ | **Removed 2026-07-22, absorbed into Phase 11** — `CLAUDE.md` Rule 14: there is no client-facing `DRAFT` state left to set lines on separately | ✕ Removed | 2026-07-18 | 2026-07-22 | Was `☑ Done` 2026-07-18 (see git history for that period's notes); confirmed removed and returning `404` live 2026-07-22 |
| 13 | Journal Entries | ~~`PUT /api/v1/journal-entries/{journalNo}/post`~~ | **Removed 2026-07-22, absorbed into Phase 11** — same rule | ✕ Removed | 2026-07-18 | 2026-07-22 | Was `☑ Done` 2026-07-18 (see git history for that period's notes); confirmed removed and returning `404` live 2026-07-22 |
| 14 | Journal Entries | `GET /api/v1/journal-entries/{journalNo}` | — | ☑ Done | 2026-07-18 | 2026-07-18 | Looked up by `journalNo`, not id (`CLAUDE.md` Rule 8). Tested plain get (with lines) and 404 for a missing `journalNo` |
| 15 | Recurring Entries | `POST /api/v1/recurring-templates` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Built and tested together with Phases 16-17. Tested balance-check rejection (400), invalid `recurrencePattern` (400), non-postable/non-existent account in a line (400/404), and a valid create. **Retrofit 2026-07-22:** line `accountId` → `accountCode` (`CLAUDE.md` Rule 8.2), verified live |
| 16 | Recurring Entries | `POST /api/v1/recurring-templates/{id}/generate` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Tested a due template generating a journal entry (via Phase 11's create reused internally) with `NextRunDate` correctly advanced by the recurrence pattern afterward, a not-yet-due template (400), and a non-existent template (404). **Retrofit 2026-07-22:** the generated entry is now created **and posted** in the same internal call (Phase 11's create+post merge, `CLAUDE.md` Rule 14) — previously left as a `DRAFT` for later review, confirmed via a live test (`JV-2026-000005` came back `isPosted=true`) |
| 17 | Reporting | `GET /api/v1/reports` | Merges 7 old report endpoints; corrected `POST`→`GET` | ☑ Done — base endpoint (2026-07-18), 4 retrofits done+verified live 2026-07-21, all 6 further 2026-07-22 retrofits (TrialBalance flatten, ProfitAndLoss restructure, CashFlow simplify, Income Tax Computation redesign, Csv format, Company:Address/Tin/Brn) done and verified live 2026-07-22, `GeneralLedger`'s `accountId` param → `accountCode` (`CLAUDE.md` Rule 8.2, same day, separate retrofit) done and verified live | 2026-07-18 | 2026-07-22 | **2026-07-18/21 history (unchanged, kept as an accurate record):** Tested all seven `reportType` values (Json + Pdf) against real posted data, `outputFormat=Pdf` (verified real `%PDF-1.4` bytes for every report, A4 page size, correct landscape/portrait per report), missing-required-parameter (400), invalid `reportType`/`outputFormat` (400), non-existent `budgetId` (404). All 4 previously-outstanding retrofits complete: `CashFlow` full LKAS 7 upgrade, all 7 bespoke PDF layouts, `TaxSummary` (original VAT/WHT/NBT design), A4 page size. **2026-07-22 — reference templates arrived for 5 of the 7 reports, revealing real gaps in what was built 2026-07-21.** Six further retrofits now outstanding, none started: (1) `TrialBalance` — flatten response/PDF/CSV, drop `depth`/`indentedCode`; (2) `ProfitAndLoss` — full restructure around new `accounts.income_statement_section` column (Sales/Cost of Sales/Gross Profit/Other Income/four expense groups); (3) `CashFlow` — drop printed opening/closing balance lines, add `direction` field, re-bucket adjustments into Additions/Subtractions at render time; (4) `TaxSummary` — **wholesale redesign** from VAT/WHT/NBT summary into a full Income Tax Computation (new `is_tax_deductible` column, 5 new `system_config` keys, 4 new optional request params) — old design superseded, not extended; (5) `outputFormat=Csv` — new third output format for all seven reports; (6) `Company:Address`/`Tin`/`Brn` — 3 new appsettings fields for the Income Tax Computation's header. Full specs: `Accounting_System_Design_LKAS.md` §11.4/§11.5/§11.7, `04_API_Reference_And_Scenarios.md`, `05_LKAS_Report_PDF_Templates.md` (all updated 2026-07-22), this file's Phase 1 §1.9–§1.12 (new). |
| 18 | Currency | ~~`POST /api/v1/fx-revaluation`~~ | — | ✕ Removed | | 2026-07-18 | **Removed, not just blocked.** Confirmed 2026-07-18: base currency is a one-time initial-configuration choice, never changed by the client, and non-base currencies are never used in this system. FX revaluation has no possible purpose as a result — see Document 1 §6.1 and Document 2, Phase 18 |
| 19 | Tax | `POST /api/v1/tax-codes` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Built and tested together with Phases 20-22, 31-33. Tested valid create, duplicate code (400), missing `effectiveFrom` (400), non-existent `taxTypeId` (404), invalid `calculationBasis`/`appliesTo` (400). **Retrofit 2026-07-22:** `taxTypeId`/`outputTaxAccountId`/`inputTaxAccountId` → `taxTypeCode`/`outputTaxAccountCode`/`inputTaxAccountCode` (`CLAUDE.md` Rule 8.2), verified live |
| 20 | Tax | `GET /api/v1/tax-codes/calculate` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Tested `Exclusive` calculation (1000 @ 18% = 180), and a `txnDate` outside the tax code's effective range (404 — same "no matching row" case as a missing code). **Retrofit 2026-07-22:** `taxCodeId` query param → `taxCode` (`CLAUDE.md` Rule 8.2), verified live (`VAT-001` @ 15% on 1000 = 150) |
| 21 | Fixed Assets | `POST /api/v1/fixed-assets` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Tested `StraightLine` and `ReducingBalance` (incl. missing-rate rejection, 400), duplicate asset code (400), `residualValue > acquisitionCost` (400), non-postable account (400). **Retrofit 2026-07-22:** the 4 GL account fields (`assetAccountId`/`accumDeprAccountId`/`deprExpenseAccountId`/`gainLossAccountId`) → `...Code` (`CLAUDE.md` Rule 8.2), verified live |
| 22 | Fixed Assets | `POST /api/v1/fixed-assets/{assetCode}/transactions` | Merges old `/depreciate` + `/dispose` | ☑ Done | 2026-07-18 | 2026-07-22 | Looked up by `assetCode`, not id (`CLAUDE.md` Rule 8). Tested `Depreciate` (`StraightLine` and `ReducingBalance`, correct amounts, auto-posted journal), locked-period rejection (400), `Dispose` (balanced 4-line entry incl. a loss on the gain/loss account, asset flips to `Disposed`), both transaction types correctly rejected once disposed (400), non-existent asset code (404), invalid `transactionType` (400). **Retrofit 2026-07-22:** `proceedsAccountId` → `proceedsAccountCode`; internally, both transaction types now call `JournalEntryService.CreateAsync` once (it auto-posts, Rule 14) instead of a separate `CreateAsync` + `PostAsync` pair — verified live (`Depreciate` on `FA-000003` returned `journal.isPosted=true` directly) |
| 23 | Bank | `POST /api/v1/bank-accounts` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Built and tested together with Phases 24, 25, 34. Tested valid create (defaults `currencyCode` to `LKR`, matching `AccountService`'s own precedent), non-postable GL account (400), non-existent GL account (404). **2026-07-22:** a `bank_account.accountType` discriminator was drafted, then redirected to a separate `cash_account` table instead (Phase 23a) — `bank_account`/`POST /bank-accounts` itself stayed exactly as originally built, no shape change. **Retrofit 2026-07-22 (same day, separate change):** `glAccountId` → `glAccountCode` (`CLAUDE.md` Rule 8.2), verified live |
| 23a | Bank | `POST /api/v1/cash-accounts` | New, 2026-07-22 — separate table from `bank_account`, not a discriminator | ☑ Done, implemented and verified live 2026-07-22 | 2026-07-22 | 2026-07-22 | New `cash_account`/`cash_account_type` tables (`Accounting_System_Design_LKAS.md` §5.8). Tested: create with an existing `cashAccountTypeName` + no `accountNumber` (auto-generated `CASH-000003`); create with a `cashAccountTypeName` not in the seeded list (`"Vault Cash"`) — confirmed it created a new `cash_account_type` row, immediately visible via `GET /cash-account-types`; non-postable/non-existent GL account (400/404); missing `cashAccountTypeName` (400). **Retrofit 2026-07-22 (same day, separate change):** `glAccountId` → `glAccountCode`, verified live |
| 23b | Bank | `GET /api/v1/cash-account-types` | New, 2026-07-22 | ☑ Done, implemented and verified live 2026-07-22 | 2026-07-22 | 2026-07-22 | Flat, unfiltered list — seeded with Petty Cash/Till Cash/Safe Cash/Cash in Transit, confirmed growing after Phase 23a's "Other" test |
| 24 | Bank | `POST /api/v1/bank-accounts/{id}/statement-lines/import` | — | ☑ Done | 2026-07-18 | 2026-07-18 | First file-upload endpoint in this project (`multipart/form-data`, not JSON) — Controller extracts a `Stream` from `IFormFile`, keeping that ASP.NET Core type out of the Service layer. Column layout: `TxnDate \| Description \| Amount` with a header row. Malformed rows are skipped and reported per-row (not all-or-nothing) — confirmed with the user. Tested: 2 valid + 1 malformed row (text where a number was expected) → 2 imported, 1 reported with row number + reason; a genuinely corrupt (non-`.xlsx`) file → 400; non-existent bank account → 404 |
| 25 | Bank | `PUT /api/v1/bank-statement-lines/{id}/reconcile` | — | ☑ Done | 2026-07-18 | 2026-07-22 | "Amounts must match" resolved as: journal entry must be `Posted`, sum its lines posted to the bank account's own GL account, compare to the statement line's `Amount` — confirmed with the user. Tested: reconcile against an unposted entry (400), successful reconcile, re-reconciling an already-reconciled line (400), amount mismatch (400), non-existent statement line (404), non-existent journal entry (404). **Retrofit 2026-07-22:** `matchedJournalId` → `matchedJournalNo` (`CLAUDE.md` Rule 8.2), verified live (`404` for a non-existent `journalNo`, confirming the lookup switched correctly) |
| 26 | Budget | `POST /api/v1/budgets` | — | ☑ Done | 2026-07-18 | 2026-07-18 | Built and tested together with Phases 27, 35. Tested valid create and non-existent `fiscalYearId` (404). Unaffected by the 2026-07-22 code-based retrofit — `fiscalYearId` has no unique code column (`CLAUDE.md` Rule 8.2), stays numeric |
| 27 | Budget | `POST /api/v1/budgets/{id}/lines` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Appends to the budget's existing lines rather than replacing them (Document 1's own note: "lines are added incrementally over time"), unlike the old Phase 12's replace-all (Phase 12 itself removed 2026-07-22, see its row). Tested valid add (2 lines), non-existent account/period/cost-center (404 each), non-existent budget (404). Also re-verified Phase 17's `BudgetVsActual` report end-to-end now that real budget data exists — previously only verifiable structurally. **Retrofit 2026-07-22:** line `accountId`/`costCenterId` → `accountCode`/`costCenterCode` (`CLAUDE.md` Rule 8.2; `periodId` stays numeric, no unique code), verified live |
| 28 | System Config | `PUT /api/v1/system-config/{key}` | — | ☑ Done | 2026-07-18 | 2026-07-18 | `LEDGER` per current keys, classified per-key via a lookup dictionary (defaults to `Administrative` for any future/unmapped key) — Document 1 §4.3. `is_editable`/`is_locked` resolved as `!IsEditable \|\| IsLocked` → reject, with every successful write locking the key (confirmed with the user — see Deviations). Tested: valid update + auto-lock, re-edit of the now-locked key (400), non-existent key (404) |
| 29 | System Config | `GET /api/v1/system-config` | — | ☑ Done | 2026-07-18 | 2026-07-18 | Built and tested together with Phase 28. Flat list, all 3 seeded keys returned |
---
## Phases 30–35 — Appended read/list endpoints (Document 1 §6.3 / Document 2)
| Phase | Module | Endpoint | Status | Started | Completed | Notes |
|---|---|---|---|---|---|---|
| 30 | Currency | `GET /api/v1/currencies` | ☑ Done | 2026-07-18 | 2026-07-18 | Tested against the running app: all 142 seeded currencies returned, sorted by code, correct `decimalPlaces` per row (JPY=0, KWD=3), exactly 1 `isBaseCurrency=true` (LKR) |
| 31 | Tax | `GET /api/v1/tax-types` | ☑ Done | 2026-07-18 | 2026-07-18 | Implemented alongside Phase 19, per this row's own note. Tested: all 5 seeded tax types returned |
| 32 | Tax | `GET /api/v1/tax-codes` | ☑ Done | 2026-07-18 | 2026-07-18 | Implemented alongside Phase 19. Tested against real created tax codes |
| 33 | Fixed Assets | `GET /api/v1/fixed-assets` | ☑ Done | 2026-07-18 | 2026-07-18 | Implemented alongside Phase 21. Tested against real registered assets |
| 34 | Bank | `GET /api/v1/bank-accounts` | ☑ Done — retrofit 2026-07-22: unified, filterable list across `bank_account` + `cash_account` | 2026-07-18 | 2026-07-22 | Implemented alongside Phase 23, per this row's own note. Tested against a real created bank account. **2026-07-22:** new optional `accountType` query param (`Cash`/`Bank`/`Both`, default `Both`); response is now a combined, flattened `CashAndBankAccountDto` list (`accountType` a response-only discriminator, not a stored column). Verified live: default call returns both tables' rows together; `accountType=Cash`/`Bank` each correctly filter; an invalid value returns 400 |
| 35 | Budget | `GET /api/v1/budgets` | ☑ Done | 2026-07-18 | 2026-07-18 | Implemented alongside Phase 26, per this row's own note. Tested against a real created budget |
---
## Phase 36 — Audit Log List Endpoint (Document 1 §6.4)
| Phase | Endpoint | Status | Started | Completed | Notes |
|---|---|---|---|---|---|
| 36 | `GET /api/v1/audit-log` | ☑ Done | 2026-07-18 | 2026-07-18 | Built and tested together with Phases 28-29. Tested no-filter (54 rows, matching every prior phase's audit inserts this session), `category=LEDGER`/`ADMINISTRATIVE` split (41/13, summing to 54), invalid `category`/`action` → empty result (not an error), `entityName`/`action`/`performedBy`/date-range filters, and default paging (page 1, pageSize 20) |
---
## Phase 37 — Cheque Management (new module, appended 2026-07-30 — beyond the original 36-phase plan)
Client-supplied field spec for `ChequeBook`/`ChequePage` (issued cheques, from this company's own cheque books) and `ReceivedCheque` (cheques received from others — no `ChequeBook` link, per the spec). Not part of `01_Full_Development_Plan.md`/`02_Phase_By_Phase_Development_Plan.md`'s original scope; added as a new module following the same layering/foundation rules (`CLAUDE.md` Rule 1) as every other module.
| Phase | Module | Endpoint | Status | Started | Completed | Notes |
|---|---|---|---|---|---|---|
| 37.1 | Cheque Books | `POST /api/v1/cheque-books` | ☑ Done | 2026-07-30 | 2026-07-30 | Auto-generates all `totalLeaves` `ChequePage` rows (`Unused`) from `startChequeNo`/`endChequeNo`. Validates bank account exists, `chequeBookNo` uniqueness, and that the numeric range matches `totalLeaves`. Verified live |
| 37.2 | Cheque Books | `GET /api/v1/cheque-books/{chequeBookNo}` (+`?expand=pages`) | ☑ Done | 2026-07-30 | 2026-07-30 | Read-only — no audit row. Verified live |
| 37.3 | Cheque Books | `GET /api/v1/cheque-books` | ☑ Done | 2026-07-30 | 2026-07-30 | Filters: `bankAccountId`, `branchId`, `status`. Verified live |
| 37.4 | Cheque Pages | `PUT /api/v1/cheque-pages/{chequeNo}/issue` | ☑ Done | 2026-07-30 | 2026-07-30 | `Unused` → `Issued` only; fills payee/amount/etc. Verified live |
| 37.5 | Cheque Pages | `PUT /api/v1/cheque-pages/{chequeNo}/status` | ☑ Done | 2026-07-30 | 2026-07-30 | Action-based (`Clear`/`Bounce`/`Cancel`/`Void`), same pattern as `PUT /periods/{id}/lock`. Verified live for all four actions plus an invalid-transition 400 |
| 37.6 | Cheque Pages | `GET /api/v1/cheque-pages/{chequeNo}` | ☑ Done | 2026-07-30 | 2026-07-30 | Read-only — no audit row. Verified live |
| 37.7 | Cheque Pages | `GET /api/v1/cheque-pages` | ☑ Done | 2026-07-30 | 2026-07-30 | Filters: `chequeBookNo`, `issueStatus`, `payeeType`, `payeeId`. Verified live |
| 37.8 | Received Cheques | `POST /api/v1/received-cheques` | ☑ Done | 2026-07-30 | 2026-07-30 | No `ChequeBook` link, per spec. Status defaults `Received`. Verified live |
| 37.9 | Received Cheques | `PUT /api/v1/received-cheques/{id}/status` | ☑ Done | 2026-07-30 | 2026-07-30 | Action-based (`Deposit`/`Clear`/`Return`/`Cancel`); `Deposit` requires `depositBankAccountId`+`depositDate`. Verified live for `Deposit`→`Clear` |
| 37.10 | Received Cheques | `GET /api/v1/received-cheques/{id}` | ☑ Done | 2026-07-30 | 2026-07-30 | Read-only — no audit row. Verified live |
| 37.11 | Received Cheques | `GET /api/v1/received-cheques` | ☑ Done | 2026-07-30 | 2026-07-30 | Filters: `companyId`, `branchId`, `status`, `receivedFromType`. Verified live |
**Phase 37 overall status:** ☑ Done (all of 37.1–37.11). Full design decisions, deviations from the client's raw field spec, and live-verification detail recorded in this file's 2026-07-30 entry below.
---
## Phase 38 — GRN/AP Integration Support (appended 2026-08-12 — beyond the original 36-phase plan)
Two small, additive gaps found and closed while wiring ERPCore's GRN module (receipt posting + vendor payment) to this service. Both reuse existing internal logic; neither changes any existing endpoint's request/response shape.
| Phase | Module | Endpoint | Status | Started | Completed | Notes |
|---|---|---|---|---|---|---|
| 38.1 | Fiscal Calendar | `GET /api/v1/fiscal-years/periods/by-date?date=YYYY-MM-DD` | ◐ In progress | 2026-08-12 | | Resolves the `AccountingPeriod` covering a date, for callers (e.g. ERPCore) that need a `periodId` for `POST /journal-entries` but have no other way to discover one — this API surface previously had no `GET` for periods at all. Reuses the existing `IFiscalCalendarRepository.GetPeriodByDateAsync` (already used internally by `RecurringTemplateService.GenerateAsync`) via a new `FiscalCalendarService.GetPeriodByDateAsync` + controller action. 404 `"Period not found"` if no period covers the date. Build verified clean; not yet manually exercised against a live local instance |
| 38.2 | Bank & Cash Accounts | `GET /api/v1/bank-accounts` — response gains `glAccountCode` | ◐ In progress | 2026-08-12 | | `CashAndBankAccountDto` previously exposed only the numeric `glAccountId`; since this API has no `GET /accounts` lookup by numeric id (only by `accountCode`, per Rule 8), a caller could not turn a listed bank/cash account into an `accountCode` to post a journal-entry line against. Added `GlAccountCode` to the DTO, sourced via `.Include(x => x.GlAccount)` on `BankRepository.GetAllAsync()`/`GetAllCashAccountsAsync()`. Additive field only — existing consumers unaffected. Build verified clean; not yet manually exercised against a live local instance |
**Phase 38 overall status:** ◐ In progress — both endpoints build clean but have not yet been manually verified against a running local instance (Rule 6: a phase isn't "done" until that happens). Flagging here rather than marking ☑ Done prematurely.
---
## Overall Progress Summary
| Metric | Count |
|---|---|
| Total phases | 38 (Phase 1 + Phases 2–29 + Phases 30–35 + Phase 36 + Phase 37 + Phase 38) |
| Of which consolidated/modified endpoints | 8 (Phases 3, 4, 10, 11, 12, 13, 17, 22) |
| Done | 36 (Phases 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37) |
| In progress | 1 (Phase 38) |
| Blocked | 0 |
| Removed | 1 (Phase 18) |
| Not started | 0 |
**All 36 original phase slots resolved as of 2026-07-18** (35 Done + 1 Removed); **Phase 37 (Cheque Management) added and completed 2026-07-30 as new scope beyond the original plan.** Every phase from `01_Full_Development_Plan.md`/`02_Phase_By_Phase_Development_Plan.md` has been implemented and manually confirmed working, except Phase 18 (FX Revaluation), which was confirmed permanently out of scope — see its Deviations entry.
*(This summary is recalculated and updated by hand every time a phase's status changes — it is not authoritative on its own; the per-phase tables above are the source of truth.)*
---
## Deviations From the Plan
Any time actual implementation diverges from what `01_Full_Development_Plan.md` or `02_Phase_By_Phase_Development_Plan.md` describes, record it here with the date and a one-line reason.
**2026-07-17 (round 1)** — Endpoint count reduced from 40 to 31 via four consolidations (reporting, journal correction, fixed asset transactions, account reads). Phase numbering updated across all documents.
**2026-07-17 (round 2)** — Further reduced from 31 to 28 endpoints: (1) currency-management endpoints (old `POST /currencies`, `POST /currencies/{code}/rates`) removed outright — base currency is now seeded via `HasData` in Phase 1 instead; (2) journal-entry create and correction merged into a single `POST /api/v1/journal-entries`, with the lines-setting endpoint (Phase 12) generalized to serve both plain drafts and `PartialAdjustment` corrections, and the post endpoint (Phase 13) gaining a branch to flip the original entry's status when posting a `FullReversal`; (3) the reporting endpoint corrected from `POST` to `GET`, since it's read-only. **Phase 18 (FX Revaluation) is now flagged `⚠ Blocked`** as an open dependency question — it has no data to act on until the currency-removal decision in Document 2's Phase 18 is resolved one way or the other.
**2026-07-17 (Phase 1 build) — `JournalEntry.Narration` added.** Neither the ER diagram nor Document 2's own Phase 1 model list included a narration field on `journal_entry`, but Document 2's Phase 11 worked example specifies it in the create-journal-entry request DTO. Added `Narration` (text, nullable) to the `JournalEntry` model/table now (approved by the user) rather than leaving a gap that Phase 11 would have had to patch retroactively.
**2026-07-17 (Phase 1 build) — Fixed a pre-existing build break unrelated to the plan.** The `.csproj` (already modified outside this session, uncommitted) was missing the `Microsoft.AspNetCore.Authentication.JwtBearer` package reference that `Program.cs` depends on for JWT bearer auth setup — the project would not compile. Re-added the package reference (version 10.0.9, matching the other `Microsoft.*` packages).
**2026-07-17 (Phase 1 build) — Database uses snake_case naming, not EF Core's PascalCase default.** `Accounting_System_Design_LKAS.md`, the ER diagram, and every recursive CTE query example in both use snake_case table/column names throughout. Resolved (approved by the user) by setting explicit snake_case table names per entity and adding a snake_case conversion pass over columns/keys/foreign keys/indexes in `AppDBContext.OnModelCreating`, rather than adding the `EFCore.NamingConventions` package.
**2026-07-17 (Phase 2 build) — Added `ExceptionHandlingMiddleware`, ahead of a dedicated phase for it.** `01_Full_Development_Plan.md` §7.1 flagged the missing global exception handler as a genuine gap and proposed fixing it "early in Phase 2, subject to approval." Approved and implemented as part of Phase 2 itself. This is now foundation (Rule 1.1-style) for every later phase.
**2026-07-17 (Phase 2 build) — `Database.SqlQuery<int>` requires the source column literally named `Value`.** Fixed by aliasing the column `AS ""Value""` in the raw SQL string. Worth knowing for any later phase writing a scalar raw-SQL repository method the same way.
**2026-07-17 (post-Phase-2 refactor) — New architectural rule: single controller, `AuthResponse` built in the Service layer.** User-directed change, applied retroactively to Phase 2 and made a standing rule for every phase going forward (`CLAUDE.md` Rule 7).
**2026-07-17 (pre-Phase-4) — Route regression found and fixed: routing convention simplified further.** The controller-level route is now just `[Route("api/v{version:apiVersion}")]` (the `[controller]` token removed entirely), so every action's own route template is just its path relative to `api/v1/`.
**2026-07-17 (pre-Phase-4) — New rule: payload parsing (`DataExtractor`) moved from Controller to Service.** Controller actions accepting a loosely-typed body now take `[FromBody] Dictionary<string, JsonElement> payload` and pass it straight to the Service method unexamined.
**2026-07-17 (pre-Phase-4) — New rule: look up by unique code, not numeric id (`CLAUDE.md` Rule 8).** Wherever an entity has a unique business-key column, single-resource path lookups use that code, not the internal numeric id. Scoped to path/lookup parameters only — FK reference fields in request bodies and internal recursive-CTE/tree logic stay numeric. Phase 3 was retrofitted from `{id}` to `{accountCode}`; Phase 4 was built with this from the start.
**2026-07-17 (Phase 4 build) — `UpdateAccountRequest.ParentAccountIdProvided` distinguishes "omitted" from "explicitly null."** Solved via a `ParentAccountIdProvided` bool set from `payload.ContainsKey("parentAccountId")`, alongside a manual `JsonValueKind.Null` check for the value itself.
**2026-07-17 (Phase 5 build) — Query-string parameters also bundled into a DTO, not passed as individual method parameters.** Extending the Phase 4 "no piecemeal parameters" preference to `[FromQuery]` collection-read endpoints as well as `[FromBody]` writes.
**2026-07-17 (Phases 6–8) — Three phases implemented and tested together, at the user's explicit direction.** This deviates from `CLAUDE.md`'s own "work phases strictly in order, one at a time" rule — flagged to the user before proceeding, who confirmed. Design decisions: period generation supports both "generate all up front" and "extend later" through one code path (`FiscalCalendarService.BuildPeriodsAsync`); Phase 8's lock action set is `LOCK`/`UNLOCK`/`SOFT_CLOSE`/`CLOSE`, always writing a `PeriodLockLog` row, with `CLOSED` periods only reopenable via `UNLOCK`.
**2026-07-18 (post-Phase-8) — New rule: enums for fixed discriminator sets (`CLAUDE.md` Rule 9), and `AccountingPeriod.Status`/`PeriodLockLog.Action` converted from `VARCHAR` to `integer`.** Enums apply only to inline discriminator columns with **no backing reference table** (`TaxType`/`AccountType`/`Currency` are exempted — they're real tables with FK relationships). C# has no string-backed enum, so representing these two columns as a true `enum` required converting them to `integer` (migration `ConvertPeriodStatusAndActionToInt`) — approved by the user as a deliberate deviation from the design doc's `VARCHAR` schema for these two columns specifically. Applying the migration against existing test data would have failed (`'OPEN'`/`'LOCK'` can't cast to `integer`), so the local dev database was dropped and recreated, then all Phase 6–8 tests re-run — all passed identically.
**2026-07-18 — `Currency` seed data expanded to ~140 ISO 4217 currencies, surfacing and fixing a real EF Core `HasData` bug.** Added a `NonBaseCurrency(...)` local helper and ~140 rows (migration `SeedIsoCurrencies`). Every currency seeded with `DecimalPlaces = 0` (JPY, KRW, VND, and 13 others) was silently inserted with `2` instead — EF Core's `HasData` diffing treats a seeded value equal to the CLR default for its type (`0` for `short`) as "unset" when the column also has `HasDefaultValue` configured, silently omitting it from the generated `INSERT`. Fixed by removing the column default entirely (migration `RemoveCurrencyDecimalPlacesDefault`) — nothing else relies on it. Re-verified via `psql`: 142 currencies, 16 at `decimal_places = 0`, 7 at `3`, exactly 1 base currency. **Worth remembering for any future `HasData` seed where a meaningful value can coincide with a CLR default (`0`, `false`, `null`) on a column that also has `HasDefaultValue` configured.**
**2026-07-18 — Phases 30–35 appended: user-identified gap, Tax/Fixed Assets/Bank/Budget have no read/list endpoints.** Every phase for those four modules was `POST`/`PUT`-only. Added as Phases 30–35 (Document 1 §6.3), appended rather than inserted. Phase 30 (`GET /api/v1/currencies`) implemented immediately; Phases 31–35 documented for implementation alongside their respective module's create phase.
**2026-07-18 — `CLAUDE.md` Rule 10 added: generic `audit_log` table, categorized `LEDGER`/`ADMINISTRATIVE`, Phase 36 (`GET /api/v1/audit-log`) appended.** Introduced externally (updated docs referenced a new Document 1 §4.3/§4.1 classification table and a Phase 36 not previously seen this session). Two decisions resolved with the user before implementing:
- `audit_log.category`/`action` representation: `smallint` + explicit-valued C# enums (`Enums/AuditCategory.cs`, `Enums/AuditAction.cs`) — chosen over `VARCHAR`, since this is a brand-new table with no legacy data to migrate away from (unlike `PeriodStatus`/`PeriodLockAction`'s retrofit pain).
- `CLAUDE.md` itself had a real duplication bug found in the same pass (the entire document, Rules 0–9, was repeated twice after the new Rule 10) — cleaned up, unrelated to the `audit_log` content itself.
**Retrofit executed 2026-07-18:** `Models/AuditLog.cs` (no separate `CreatedAt` — `OccurredAt` serves that role), `AppDBContext` `DbSet<AuditLog>` + config (no FK, by design — `entity_name`+`entity_id` is a loose reference), migration `AddAuditLog` (pure `CREATE TABLE`, no data loss). Retrofitted the 5 already-`☑ Done` write-phase Service methods (`AccountService.CreateAccountAsync`/`UpdateAccountAsync`, `FiscalCalendarService.CreateFiscalYearAsync`/`GeneratePeriodsAsync`/`LockPeriodAsync`) — each now inserts one `audit_log` row inside its existing `UoW` transaction. `PerformedBy` is `null` for all five (none of their request DTOs currently carry an actor-name field; the convention explicitly allows `null`). Re-tested all 5 endpoints end-to-end and verified the actual `audit_log` rows via `psql` — all 7 rows across the 5 endpoints matched expectations exactly, including `UpdateAccountAsync`'s dynamic category (row for a plain rename = `Administrative`, row for a re-parent = `Ledger`).
**2026-07-18 — `03_Progress_Tracker.md` found missing from disk mid-session.** Not renamed, not recoverable from git (never committed — this repo's only commit is the bare initial scaffold). Recreated from this session's own record of every prior update, per explicit user confirmation, with the `audit_log` retrofit folded in. If any historical entry above turns out to be inaccurate relative to the real codebase, that's a reconstruction gap, not a deliberate edit — flag it per Rule 0.4.
**2026-07-18 (Phases 9–10) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as Phases 6–8. Modeled closely on `AccountRepository`/`AccountService`'s recursive-CTE pattern (breadcrumb, descendant tree, depth check), simplified for cost centers: no cycle check (no re-parent/update phase exists for cost centers in the current plan) and no enforce-flag gate (only `COST_CENTER_MAX_DEPTH` is seeded — no `COST_CENTER_ENFORCE_MAX_DEPTH` counterpart to `COA_ENFORCE_MAX_DEPTH`), so the depth check runs unconditionally whenever a parent is given. **Retrofitted Phase 10's route from `{id}` to `{costCenterCode}` in Documents 1 and 2 before building** — `CostCenter.Code` is `UNIQUE` in the design doc, so Rule 8 applies, same as the earlier Phase 3 retrofit. Tested end-to-end: root/child creation, duplicate-code and missing-parent rejection, a real depth chain to 4 levels succeeding and a 5th level correctly rejected, plain get, `?expand=tree`, and 404 — all passed, plus the `Administrative`/`Create` `audit_log` rows verified via `psql`.
**2026-07-18 — User-directed de-duplication pass across `Repositories/` and `Services/`.** `AccountRepository`/`CostCenterRepository` had verbatim-shaped recursive-CTE SQL for ancestor-depth/breadcrumb/descendant-tree (and `WouldCreateCycleAsync`, Account-only); `AccountService`/`CostCenterService` had the same max-depth `system_config`-check-and-throw block repeated three times (including twice within `AccountService` itself). Extracted into `Repositories/RecursiveTreeQueries.cs` (parameterized by table/column name string constants, mapping a shared internal `TreeRow` into each repository's own unchanged public DTO) and `Services/DepthGuard.cs` (shared config-check helper) — new `CLAUDE.md` Rule 11 documents this and carves out the one exception to Rule 2's "no generic abstractions." No public API/JSON contract changed; re-verified Phase 2/3/4/9/10 end-to-end with no regressions. Also corrected a stale `CLAUDE.md` Rule 10 claim that `audit_log.category`/`action` are `smallint` — the `AddAuditLog` migration actually mapped them as plain `integer`; wording fixed to match the real schema.
**2026-07-18 (Phases 11–14) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as Phases 6–8 and 9–10.
- **Route retrofit.** Phases 12–14 changed from `{id}` to `{journalNo}` before building — `JournalEntry.JournalNo` is named explicitly in `CLAUDE.md` Rule 8 as a unique-code lookup column, and these are all single-resource operations, same situation as the Phase 3/10 retrofits.
- **New enums, decided before first use (not a data-bearing retrofit).** `JournalEntry.EntryStatus` (design doc: `VARCHAR(15)`) and `JournalCorrection.CorrectionType` (`VARCHAR(20)`) were converted to `integer` + explicit-valued enums (`Enums/JournalEntryStatus.cs`: `Draft=1, Posted=2, Reversed=3`; `Enums/JournalCorrectionType.cs`: `FullReversal=1, PartialAdjustment=2`) via migration `ConvertJournalEntryStatusAndCorrectionTypeToInt`, since Phase 11 was their first real write — same reasoning as `AuditCategory`/`AuditAction`, not the `PeriodStatus` retrofit-with-data case. Only the enum members these phases actually use were defined (design doc lists more — `PENDING_REVIEW`/`ADJUSTED`/`VOID`/`RECLASSIFICATION` — added later if a phase needs them). `journal_type`/`source_module` stay free-text `VARCHAR` (large, open-ended, cross-module tag sets no Service logic branches on).
- **`JournalNo` auto-generated** by the Service as `JV-{year}-{6-digit sequence}` (design doc's own example format) — not client-supplied, since Document 2's request DTO doesn't carry one.
- **Line currency defaults.** Since multi-currency posting isn't wired up (same reason Phase 18 is blocked), each line defaults to `CurrencyCode` = the referenced account's currency, `ExchangeRate = 1`, `BaseDebit`/`BaseCredit` = `DebitAmount`/`CreditAmount`.
- **Genuine bug found and fixed foundationally: JSON serialization cycle.** Returning a `JournalEntry` with its `Lines` navigation collection threw a 500 (System.Text.Json throws on reference cycles by default; nothing had exercised a true bidirectional parent/collection navigation before this). Fixed by adding `ReferenceHandler.IgnoreCycles` to `AddControllers()` in `Program.cs` — a foundational fix (same category as `ExceptionHandlingMiddleware` in Phase 2), since the same navigation shape recurs in `FixedAsset`/`Budget`/`RecurringJournalTemplate`/`BankAccount` and their line tables (Phases 15/16/21/22/23/24/26/27).
- **New repositories** (one per entity, per existing convention): `JournalEntryRepository`, `JournalEntryLineRepository`, `JournalCorrectionRepository`; reused `AccountRepository.GetByIdAsync` (postable check) and `AccountingPeriodRepository.GetByIdAsync` (period-lock check).
- Tested end-to-end: plain create (no lines, and with inline lines), unbalanced-entry post rejection (400), double-post rejection (400), Phase 12 lines-set on both a plain draft and a `PartialAdjustment` draft, `FullReversal` create+post (mirrored lines, original flips to `Reversed`/`isReversed=true`), `PartialAdjustment` create+post (original untouched), correction against a non-`Posted` original (400), invalid `correctionType` (400), locked-period post rejection (400), non-existent account in a line (404), non-postable account in a line (400), and 404 for a missing `journalNo` on both GET and post.
**2026-07-18 (Phases 15–17) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as prior groups.
- **New enum, decided before first use.** `RecurringJournalTemplate.RecurrencePattern` (design doc: `VARCHAR(20)`, MONTHLY/QUARTERLY/ANNUAL — a genuinely small closed set) converted to `integer` + `Enums/RecurrencePattern.cs` (`Monthly=1, Quarterly=2, Annual=3`) via migration `ConvertRecurrencePatternToInt`, same "clean slate" reasoning as the Phase 11-14 enums.
- **Phase 16 explicitly reuses `IJournalEntryService.CreateAsync`** (per Document 2's own instruction), so the `Ledger`/`Create` audit row it needs is already written by that reused call — no second audit insert. Added `AccountingPeriodRepository.GetByDateAsync(DateOnly)` to resolve "today's" period, since the endpoint has no `periodId` in its request.
- **Decided: generating a recurring entry advances `NextRunDate`** by the recurrence pattern (`+1 month`/`+3 months`/`+1 year`) — not stated in Document 2, but left as-is would let repeated `/generate` calls on the same date create duplicate entries indefinitely. Confirmed with the user before implementing. Verified via two consecutive `/generate` calls producing two entries and `NextRunDate` moving forward each time.
- **Phase 17's `CashFlow` report type is a documented simplification, not full LKAS 7 compliance.** The design doc's own Section 11.5 spec depends on two things that don't exist anywhere in the actual schema: (1) an account-level tag for "non-cash adjustments" (depreciation/FX/provisions), and (2) a reliable way to identify which accounts are "cash" for opening/closing balance reconciliation (its own SQL comment excludes cash accounts via `account_type.name ILIKE '%cash%'`, but no seeded `account_type` row's name contains "cash" — the five seeded types are Asset/Liability/Equity/Income/Expense). Implemented instead: Operating = Profit for the Period (from the Phase 17 P&L query, i.e. no non-cash/working-capital adjustments), Investing/Financing = the existing `cash_flow_category` rollup, no opening/closing cash balance line at all. Confirmed with the user before implementing.
- **`BudgetVsActual` has no real data to verify against yet** — `Budget`/`BudgetLine` tables exist (Phase 1) but there's no create endpoint until Phase 26. Verified structurally: correct 404 for a non-existent `budgetId`; real budgeted-vs-actual numbers remain unverified until Phase 26 exists.
- **PDF rendering.** `QuestPDF` and `ClosedXML` were already referenced in the `.csproj` (no new dependency). Added `QuestPDF.Settings.License = LicenseType.Community` once in `Program.cs`. One shared, reflection-based table renderer handles every report type (per Document 2's "one shared rendering helper, not one per report type") rather than per-report layouts.
- **PDF response shape decided:** rather than special-case the Controller to return raw binary (which would break `CLAUDE.md` Rule 7's "every endpoint returns `AuthResponse`"), `outputFormat=Pdf` returns the usual `AuthResponse` with `Data = { fileName, contentType, contentBase64 }` — clients base64-decode. Verified the returned bytes are a real PDF (`%PDF-1.4` header) after decoding.
- New repositories/methods: `BudgetRepository` (new), `AccountRepository.GetTrialBalanceAsync`/`GetBalanceSheetAsync`/`GetProfitAndLossAsync`/`GetCashFlowCategoryMovementAsync`, `JournalEntryLineRepository.GetGeneralLedgerLinesAsync` — matching Document 2's own repository-placement guidance.
- Tested end-to-end: `TrialBalance`/`BalanceSheet`/`GeneralLedger` against real posted test data (correct account-tree rollups and running balances), `ProfitAndLoss`/`CashFlow` against data with no Income/Expense accounts (correctly empty/zero, not an error), missing-required-parameter per report type (400, naming the parameter), invalid `reportType`/`outputFormat` (400), non-existent `budgetId` (404), and `outputFormat=Pdf` producing valid decoded PDF bytes.
**2026-07-18 — Phase 18 (FX Revaluation) resolved: removed permanently, not just blocked.** Phase 18 had been `⚠ Blocked` since Phase 1 pending a decision on how (or whether) to unblock it — see its original blocked-row note. The user confirmed the underlying product fact that resolves it: **the base/functional currency is a one-time initial-configuration choice — the client can never change it after setup, and non-base currencies are never used in this system at all.** Since FX revaluation only has meaning when an open balance exists in a non-base currency, and that can now never happen structurally, the endpoint has no possible purpose (not "blocked pending data" — permanently out of scope). Resolved as Option B from Document 2's original two options (remove the endpoint), rather than Option A (seed currency/rate data whenever needed) — Option A's premise never applies since there is no "whenever." Updated all three governing docs: `01_Full_Development_Plan.md` (Section 6.1 callout rewritten, Section 6.2 endpoint count 28→27, Section 4.3 audit row removed, overall endpoint/phase counts corrected), `02_Phase_By_Phase_Development_Plan.md` (Phase 18 section rewritten as removed, "How to Use" exception note updated), and this file (new `✕ Removed` status value added, Phase 18's row updated, summary counts corrected). Phase 18's slot is retired, not reused, per the "no renumbering, ever" policy. **Known consequence, not addressed further:** the multi-currency scaffolding already built (`exchange_rate`/`currency_rounding_rule` tables, ~141 of the 142 seeded `Currency` rows, `JournalEntryLine.CurrencyCode`/`ExchangeRate`/`BaseDebit`/`BaseCredit`) is now confirmed dead weight — always resolves to the single base currency / rate of 1, not a bug, just unused capability that was already built before this was known.
**2026-07-19 (Phases 19–22, 31–33) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as prior groups. Several real decisions resolved before implementing:
- **New enums, decided before first use.** `TaxCode.CalculationBasis`/`AppliesTo` and `FixedAsset.DepreciationMethod`/`Status` all converted from `VARCHAR` to `integer` + explicit-valued enums (`TaxCalculationBasis`, `TaxAppliesTo`, `DepreciationMethod`, `FixedAssetStatus`) via migration `ConvertTaxAndFixedAssetEnumsAddGainLossAndRate`, same "clean slate" reasoning as every prior enum conversion this project — both tables were empty (Phases 19/21 never built before now). `FixedAssetTransactionType` (`Depreciate`/`Dispose`) is a pure in-memory dispatch enum for Phase 22's request, not a DB column — same treatment as `ReportType`.
- **Tax code "overlap" rule resolved as a plain duplicate-code check.** Document 2 called for rejecting "overlapping `effective_from`/`effective_to` for the same `code`," but Phase 1 already put a hard unique index on `TaxCode.Code` — meaning two rows can never share a code at all, so the overlap scenario is structurally unreachable. Confirmed with the user: implemented as an ordinary `TaxCodeExistsAsync`-style duplicate-code check instead of a real date-range-overlap query.
- **Two real schema gaps found and filled, both confirmed with the user first:** (1) `FixedAsset.DepreciationRatePercent` (nullable, required only when `DepreciationMethod = ReducingBalance`) — reducing-balance depreciation had no rate anywhere in the original schema. (2) `FixedAsset.GainLossAccountId` (required, same pattern as its other three account fields) plus a new required `ProceedsAccountId` on the Dispose request itself — asset disposal had no way to post a real balanced journal entry (no gain/loss account, no account for the cash/receivable proceeds). The disposal entry shape: Debit proceeds account, Debit accumulated depreciation, Credit asset account at cost, Debit/Credit the gain/loss account to balance — verified end-to-end with a real loss scenario (proceeds 9,000 vs NBV 11,000 → 2,000 loss correctly debited to the gain/loss account, all four lines balancing to zero).
- **User-directed repository consolidation, a deviation from the "one repository per table" convention used everywhere else in this codebase.** A single `TaxRepository.cs` covers both `TaxType` and `TaxCode` (interface `ITaxRepository`); a single `FixedAssetRepository.cs` covers `FixedAsset`, `DepreciationSchedule`, and `AssetDisposal` (interface `IFixedAssetRepository`) — both confirmed explicitly by the user rather than assumed. Rationale: these are small, tightly-coupled table clusters within one domain that are only ever touched together by that domain's own Service, unlike `JournalEntry`/`JournalEntryLine`/`JournalCorrection`, which were kept separate. See `CLAUDE.md` Rule 2's updated note.
- **Phase 22 reuses `IJournalEntryService.CreateAsync`+`PostAsync`** for both `Depreciate` and `Dispose` (same pattern as Phase 16), so the `Ledger`/`Create`+`Ledger`/`Post` audit rows those calls already write satisfy Phase 22's own `Ledger` audit requirement — no separate audit insert needed. `Depreciate` posts as of the period's `EndDate`; `Dispose` posts as of the supplied `disposalDate`, resolved to its containing period via the same `AccountingPeriodRepository.GetByDateAsync` added in Phase 16.
- **Zero-amount journal lines are skipped when building the disposal entry** (e.g. no accumulated-depreciation line if depreciation was never run, no proceeds line if proceeds is 0) — `journal_entry_line`'s own `CK_journal_entry_line_one_sided` check constraint rejects a zero-amount line on either side, so an unconditional 4-line build would fail in exactly that case.
- **Phases 31/32/33 (`GET /tax-types`, `GET /tax-codes`, `GET /fixed-assets`) implemented alongside 19/21**, per those phases' own pre-existing tracker notes — confirmed with the user rather than assumed, same as Phase 30 was implemented immediately back when it was added.
- Tested end-to-end: tax code create (valid, duplicate, missing `effectiveFrom`, non-existent `taxTypeId`, invalid enum values), tax calculation (`Exclusive` basis, out-of-range date → 404), fixed asset create (`StraightLine`, `ReducingBalance` incl. missing-rate rejection, duplicate code, `residualValue > acquisitionCost`, non-postable account), depreciation (correct `StraightLine` and `ReducingBalance` amounts, auto-posted journal, locked-period rejection), disposal (balanced entry with a real loss, asset flips to `Disposed`), both transaction types rejected once disposed, non-existent asset code, invalid `transactionType`, and all three list endpoints.
**2026-07-18 (Phases 23–25, 34) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as prior groups. No new enums needed (neither `BankAccount` nor `BankStatementLine` has a fixed discriminator column). Real decisions, all confirmed with the user first:
- **User-directed repository consolidation, same pattern as Tax/FixedAssets** (`CLAUDE.md` Rule 2's note): a single `Repositories/BankRepository.cs` covers both `BankAccount` and `BankStatementLine` (interface `IBankRepository`), rather than one file per table.
- **First file-upload endpoint in this project.** Phase 24 takes `multipart/form-data`, not JSON — the Controller action extracts a `Stream` from the bound `IFormFile` and passes that into the Service, keeping the ASP.NET Core-specific `IFormFile` type out of the Service layer (same boundary principle as `Dictionary<string, JsonElement>` for loose JSON bodies). Document 2's note about reusing `Helpers/DataExtractor` for row-parsing didn't apply literally — that helper is JSON-specific (`JsonElement`), not compatible with `ClosedXML`'s own cell-value API — so row parsing uses `ClosedXML` directly instead.
- **Import column layout, not specified anywhere, decided with the user:** a 3-column format with a header row — `TxnDate | Description | Amount`.
- **Malformed-row handling, decided with the user:** valid rows import; invalid rows (unparseable date or amount) are skipped and reported back in the response with their row number and reason, rather than the whole batch failing on one bad row. A structurally corrupt/non-`.xlsx` file still returns `400` outright, since there's nothing to partially salvage in that case. Verified with a 3-row test file (2 valid, 1 with a non-numeric amount) — 2 imported, 1 reported correctly — and separately with a genuinely corrupt file.
- **Phase 25's "amounts must match" rule, decided with the user:** the matched journal entry must be `Posted` (reconciling against a draft that could still change doesn't make sense), its lines are filtered to the ones posted to the bank account's own `GlAccountId`, and their net `debit - credit` movement is compared against the statement line's `Amount`. Verified with a real match, a real mismatch, an unposted-entry rejection, and a double-reconcile rejection.
- **Phase 34 (`GET /bank-accounts`) implemented alongside Phase 23**, per that phase's own pre-existing tracker note, confirmed with the user rather than assumed — same as Phases 30–33 before it.
**2026-07-18 (Phases 26, 27, 35) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as prior groups. No new enums, no schema gaps — the simplest phase group so far.
- `{id}` stays numeric for Phase 26/27's routes — `Budget` has no unique business-key column, so `CLAUDE.md` Rule 8 doesn't apply (same reasoning as `RecurringJournalTemplate`).
- **`IBudgetRepository` extended, not duplicated** — it already existed from Phase 17's `BudgetVsActual` report (`GetByIdAsync`/`GetBudgetVsActualAsync`); added `AddAsync`, `AddLinesAsync`, `GetAllAsync` to the same file rather than creating a second repository for the same table, consistent with `CLAUDE.md` Rule 2's de-duplication direction.
- **Phase 27 appends lines rather than replacing them**, per Document 1's own note that budget lines "are added incrementally over time, not fixed at creation" — a deliberate difference from Phase 12 (`SetLinesAsync`), which replaces a journal entry's lines wholesale. Account/period/cost-center existence checks only — no postable check, since Document 2 doesn't call for one here and budgeting against a summary account is reasonable.
- Tested end-to-end: budget create (valid, non-existent `fiscalYearId` → 404), lines add (valid multi-line add, non-existent account/period/cost-center → 404 each, non-existent budget → 404), the list endpoint, and — as a bonus end-to-end check — re-verified Phase 17's `BudgetVsActual` report now returns real numbers against real budget data, closing the gap that phase's own tracker note flagged as "structurally verified only" at the time.
**2026-07-18 (Phases 28, 29, 36) — final phase group, built and tested together at the user's explicit direction.** No new enums, no schema gaps. `{key}` in Phase 28's route is `SystemConfig`'s own string PK, not a numeric id, so it already satisfies Rule 8 without any retrofit.
- **`is_editable`/`is_locked` interpretation, confirmed with the user.** Document 2's literal wording ("reject if `is_editable=false` AND `is_locked=true`") doesn't hold up as the full rule — under a strict AND reading, a key with `is_editable=false, is_locked=false` would be editable, which defeats the purpose of the `is_editable` flag. Implemented instead: reject if `!IsEditable || IsLocked`, and every successful write sets `IsLocked = true` afterward — making every `system_config` key edit-once via this endpoint, matching the Notes column's "the *one allowed write* for a one-time config" phrasing literally. Verified: first edit succeeds and locks the key; a second edit attempt on the same key is rejected (400).
- **Per-key audit classification implemented as a static lookup dictionary** in `SystemConfigService` (all three current keys — `COA_MAX_DEPTH`, `COST_CENTER_MAX_DEPTH`, `COA_ENFORCE_MAX_DEPTH` — map to `Ledger`, per Document 1 §4.3), defaulting any key not in the dictionary to `Administrative` rather than silently inheriting `Ledger` — per Document 1's own explicit warning that "a future purely-cosmetic key wouldn't automatically inherit LEDGER."
- **`AuditLogRepository.ListAsync` takes individual parameters, not a wrapped filter object** — this was a real back-and-forth mid-implementation: first attempt used 8 discrete parameters, which the user flagged as too many; a consolidated `AuditLogFilter` type was tried next, which the user also rejected, clarifying the individual-parameter shape was correct after all for a *Service → Repository* call specifically (matching `AccountRepository.SearchAsync`'s existing 5-parameter precedent) — the "pass the whole object through" preference applies to *Controller → Service* (per the Phase Template convention for `[FromQuery]` DTOs), not necessarily every layer boundary below that.
- **Invalid `category`/`action` filter values resolve to an empty result set, not an error** — per Document 2's own explicit note for this phase. A typo'd `category` is parsed and silently dropped (falling back to "no valid category" rather than "match everything" when the original list was non-empty); a typo'd `action` short-circuits the whole query to an empty page rather than hitting the database with an unsatisfiable filter.
- Tested end-to-end: system config list (all 3 seeded keys), config update + auto-lock, re-edit-after-lock rejection (400), non-existent key (404), audit log with no filters (54 rows — every audit insert from every phase built this session), `category=LEDGER`/`ADMINISTRATIVE` split (41/13, summing to 54), invalid `category`/`action` (empty result, `200`), `entityName`/`action`/`performedBy`/date-range filters (including combinations), and default paging.
**This closes out all 36 phase slots in `01_Full_Development_Plan.md` / `02_Phase_By_Phase_Development_Plan.md`** — 35 `☑ Done`, 1 `✕ Removed` (Phase 18). See the Overall Progress Summary above.
---
## Refactor Phases R1–R6 — Post-Completion Architecture Refactor (2026-07-19)
User-directed, started after all 36 original phase slots were resolved. **Not a new endpoint or business rule** — a file-organization and boilerplate-extraction pass across `Models/`, `Repositories/`, `Services/`, and the Controller, plus moving interfaces to a new `Interfaces/` folder (reversing `CLAUDE.md` Rule 4). Full design: `01_Full_Development_Plan.md` Section 10. Execution plan: `02_Phase_By_Phase_Development_Plan.md`'s Refactor Phases section. Tracked here the same way as Phases 1–36 — status updated immediately after each phase's acceptance test passes, not batched.
| Phase | What | Status | Started | Completed | Notes |
|---|---|---|---|---|---|
| R1 | Consolidate `Models/` (25 → 12 files, domain-grouped) | ☑ Done | 2026-07-19 | 2026-07-19 | Pure move, no property/attribute changes. Solution builds clean |
| R2 | Consolidate `Repositories/` (17 → 14 files: merge FiscalCalendar and JournalEntry clusters) + `RepositoryHelpers.cs` | ☑ Done | 2026-07-19 | 2026-07-19 | Landed together with R4 per the sequencing note in Document 2 (R2 alone would not compile). See Deviations for method-naming decisions |
| R3 | Extract repository interfaces to `Interfaces/Repositories/` | ☑ Done | 2026-07-19 | 2026-07-19 | 12 new interface files under `Interfaces/Repositories/`, namespace `General_Ledger_Service.Interfaces.Repositories` |
| R4 | `Services/`: add `ResponseHelpers.cs`, apply it, update constructors for R2's merges | ☑ Done | 2026-07-19 | 2026-07-19 | Also carried the `AuthResponse` → `ApiResponse` rename (see Deviations) since every Service method's return type was already being touched here |
| R5 | Extract service interfaces to `Interfaces/Services/` | ☑ Done | 2026-07-19 | 2026-07-19 | 13 new interface files under `Interfaces/Services/`, namespace `General_Ledger_Service.Interfaces.Services` |
| R6 | Controller `Respond()` helper + `Program.cs` DI wiring + full clean-build regression pass | ☑ Done | 2026-07-19 | 2026-07-19 | Clean build (`dotnet clean && dotnet build`) confirmed 0 errors / 0 warnings. Live smoke test against the real local Postgres database — see Deviations |
**Refactor progress: 6 of 6 done — R1–R6 complete.** This closes out the post-completion architecture refactor.
### Deviations (Refactor Phases)
**2026-07-19 (R1–R4) — `AuthResponse` renamed to `ApiResponse`, moved from `DTOs/Auth/AuthResponse.cs` to `DTOs/Common/ApiResponse.cs`.** User-directed, mid-R4: this service does not perform authentication/authorization itself (JWT bearer setup in `Program.cs` is conditional/unused scaffolding), so the name `AuthResponse` was misleading. Same four properties (`StatusCode`/`Success`/`Message`/`Data`), same role (`CLAUDE.md` Rule 1 foundation, every endpoint's response envelope) — pure rename, no shape change. Scope: the whole project, not just the files R1–R4 already touched — updated every `Services/*.cs` file, `Helpers/DataExtractor.cs`, `Middleware/ApiKeyMiddleware.cs`, `Middleware/ExceptionHandlingMiddleware.cs`, and all governing docs (`CLAUDE.md`, `01_Full_Development_Plan.md`, `02_Phase_By_Phase_Development_Plan.md`, `PROJECT_SPECIFICATION.md`). `Controllers/GeneralLedgerController.cs` needed no change — it only ever holds the response in a `var`, never names the type. Historical Deviations entries above this section that say `AuthResponse` are left as-is (accurate record of what the type was called at the time); only `CLAUDE.md`'s standing rules and the plan documents' prescriptive text were updated to the new name.
**2026-07-19 (R2) — Repository method naming for the two merged clusters.** Per the Tax/FixedAsset precedent, every method in a merged repository is entity-qualified rather than reused verbatim (avoids a same-signature clash — `FiscalYearRepository.GetByIdAsync(int)` and `AccountingPeriodRepository.GetByIdAsync(int)` would otherwise collide in one class). `IFiscalCalendarRepository`: `GetFiscalYearByIdAsync`, `HasOverlappingFiscalYearRangeAsync`, `AddFiscalYearAsync`, `GetPeriodByIdAsync`, `GetPeriodByDateAsync`, `GetPeriodsByFiscalYearIdAsync`, `AddPeriodRangeAsync`, `AddPeriodLockLogAsync`. `IJournalEntryRepository`: `GetJournalEntryByIdAsync`, `GetJournalEntryByJournalNoAsync`, `CountJournalEntriesForYearAsync`, `AddJournalEntryAsync`, `DeleteJournalEntryLinesByJournalIdAsync`, `AddJournalEntryLinesAsync`, `GetGeneralLedgerLinesAsync` (name unchanged, no clash), `AddJournalCorrectionAsync`, `GetJournalCorrectionByCorrectionJournalIdAsync`. Every call site across `FiscalCalendarService`, `JournalEntryService`, `RecurringTemplateService`, `FixedAssetService`, `BudgetService`, `BankService`, and `ReportService` updated accordingly — confirmed via a clean `dotnet build` (0 errors, 0 warnings).
**2026-07-19 (R2) — `RepositoryHelpers.cs` scope.** Applied only to the three literal one-line shapes named in Document 1 Section 10.4 (`GetByIdAsync` via `FindAsync`, `ExistsAsync` via `AnyAsync(predicate)`, `AddAsync` via `Add`+return) — across `AccountRepository`, `CostCenterRepository`, `FiscalCalendarRepository`, `JournalEntryRepository`, `RecurringJournalTemplateRepository`, `TaxRepository`, `FixedAssetRepository`, `BankRepository`, `BudgetRepository`. Deliberately **not** applied to `SystemConfigRepository.GetByKeyAsync` (uses `FirstOrDefaultAsync`, not `FindAsync`, in the original code — left as-is rather than silently changing its query shape) or `AuditLogRepository.AddAsync` (returns `Task`, not `Task<AuditLog>` — a genuine signature mismatch with the shared helper, not forced through it).
**2026-07-19 (R5) — Mechanical, one-for-one interface extraction.** All 13 domain Service interfaces (`IAccountService` through `IAuditLogService`) moved to `Interfaces/Services/`, namespace `General_Ledger_Service.Interfaces.Services`. Every consuming file updated: each Service implementation file (`using General_Ledger_Service.Interfaces.Services;` added), `Controllers/GeneralLedgerController.cs`, and `Program.cs`. `DepthGuard.cs` and `ResponseHelpers.cs` untouched (no interface — internal static helpers, never injected).
**2026-07-19 (R6) — Controller `Respond()` helper + full clean-build + live smoke test.** Added `private IActionResult Respond(ApiResponse response) => StatusCode(response.StatusCode, response);` to `GeneralLedgerController`; every one of its ~40 actions rewritten as a one-line expression body calling `Respond(await _xService.YAsync(...))`, replacing the old two-line `var response = ...; return StatusCode(...)` pattern. `Program.cs` gained `using General_Ledger_Service.Interfaces.Services;` for its `AddScoped` service registrations (the repository-side equivalent, `Interfaces.Repositories`, was already added in the R2/R3 pass). `dotnet clean && dotnet build`: 0 errors, 0 warnings. **Live regression check**, not just a compile check: started the app against the real local Postgres database (`ApiKeyMiddleware` is still commented out in `Program.cs` from earlier in the project, so no key needed) and hit 8 read endpoints spanning every module touched by the R2 repository merges and every layer touched by R1–R5 — `GET /currencies`, `/system-config`, `/tax-types`, `/accounts`, `/reports?reportType=TrialBalance`, `/journal-entries/JV-2026-000001` (exercises the merged `JournalEntryRepository`, including its `Lines` include), `/fixed-assets`, `/budgets`, `/bank-accounts`. All returned `200` with the same `ApiResponse`-shaped envelope and correct data pulled from rows created during this project's earlier phases — confirming no regression from the file moves, repository merges, or the `AuthResponse`→`ApiResponse` rename. Did not exercise write endpoints (`POST`/`PUT`) live, to avoid mutating persisted dev data as a side effect of a documentation/refactor pass; write-path correctness for the merged repositories is instead covered by the clean build (every call site type-checks against the renamed methods) and by every write endpoint's own original Phase 1–36 acceptance test, none of which changed in shape or logic during R1–R6. Dev server stopped afterward (verified via a failed follow-up request).
**This closes out the post-completion architecture refactor — R1–R6, all `☑ Done`.** `Models/`, `Repositories/`, `Services/`, and the Controller are reorganized per `01_Full_Development_Plan.md` Section 10; interfaces live in `Interfaces/Repositories/` and `Interfaces/Services/` per `CLAUDE.md` Rule 4 (reversed); the response envelope is `ApiResponse` (`DTOs/Common/ApiResponse.cs`), not `AuthResponse`. No public API contract, request/response shape, or database schema changed anywhere in this refactor.
---
## Post-Refactor Change — Optional Paging, No Default `page`/`pageSize` (2026-07-19)
**User-directed, a genuine behavior/contract change** — unlike R1–R6, this is not a zero-behavior-change pass. Affects the two paginated `GET` endpoints: `GET /api/v1/accounts` and `GET /api/v1/audit-log`.
**Before:** an omitted `page`/`pageSize` silently defaulted to `page=1, pageSize=20` — a caller that wanted "everything" had no way to ask for it; they'd always get (at most) the first 20 rows back with no indication more existed unless they inspected `totalCount`.
**After:** pagination is only applied when the caller supplies **both** `page` and `pageSize`. If either is omitted (or non-positive), no `Skip`/`Take` is applied at all — the full filtered result set is returned, and the response's `page`/`pageSize` fields are `null` (not a fabricated number), honestly reflecting that no slicing happened. Supplying only one of the two (e.g. `pageSize=5` with no `page`) is likewise treated as "not enough to paginate" and returns everything, not a partial/guessed slice.
**Implementation:**
- New shared helper `RepositoryHelpers.ApplyPaging<T>(IQueryable<T> query, int? page, int? pageSize)` — applies `Skip`/`Take` only when both are non-null; otherwise returns the query untouched. Used by both `AccountRepository.SearchAsync` and `AuditLogRepository.ListAsync` (their `page`/`pageSize` parameters changed from `int` to `int?`; interfaces updated to match).
- `AccountListResponse.Page`/`PageSize` and `AuditLogListResponse.Page`/`PageSize` changed from `int` to `int?`.
- `AccountService.ListAccountsAsync` and `AuditLogService.ListAsync`: removed the `is > 0 ? value : 1/20` default-substitution; a non-positive supplied value is normalized to "not supplied" (safety, not a default), and the response only echoes back `page`/`pageSize` when pagination was actually applied (`page.HasValue && pageSize.HasValue`) — a `pageSize=5` request with no `page` returns the full list and `page: null, pageSize: null`, not a misleading `pageSize: 5` next to 54 returned rows.
- Every other filter on these two endpoints (`accountTypeId`/`isActive`/`isPostable`/`category`/`entityName`/`action`/`performedBy`/`from`/`to`) was already optional-and-unforced — this change brings `page`/`pageSize` in line with that same pattern rather than singling out paging for a hidden default.
**Verified live** against the real local Postgres database: `GET /accounts` with no params → all 8 rows, `page`/`pageSize` both `null`; `?page=1&pageSize=2` → 2 rows, `page:1, pageSize:2`. `GET /audit-log` with no params → all 54 rows; `?pageSize=5` alone (no `page`) → still all 54 rows, `page`/`pageSize` both `null` (confirmed **not** silently defaulting `page` to 1); `?page=1&pageSize=5` → exactly 5 rows, `page:1, pageSize:5`. Clean build: 0 errors, 0 warnings.
---
## 2026-07-19 — Exact `HasData` seed values formalized (Document 1 §5.2–5.3, this file's new step 1.8); ER diagram assistant-error corrected
User asked for the exact `system_config` seed values (key, initial value, whether changeable) plus every other table's default/launch data, consolidated somewhere usable as a direct source for `AppDBContext.OnModelCreating`. Added `01_Full_Development_Plan.md` §5.2 (`account_type`, `system_config`, `tax_type` — full row-by-row values) and §5.3 (`currency` — shape + representative rows, not all 142 inline, plus the `DecimalPlaces`/`HasData`-default bug called out explicitly since it's the kind of thing worth knowing before writing a similar seed in the future). Added this file's step 1.8 as the corresponding tracker entry — **documentation formalization only, no code change**, since all four tables were already seeded and verified working; the values were transcribed from what's already live plus this file's own Deviations log, not newly invented.
While doing this, corrected two stale claims found in the process: Document 2's Phase 1.7 acceptance criteria and this file's own 1.7 note both still said `currency` seeds "exactly 1 row" — true on 2026-07-17, superseded by the 142-row ISO 4217 expansion the very next day, never updated. Fixed both.
**Separately, an assistant-caused error from the prior session was found and corrected.** In that session, the user re-uploaded documents (round 2) and the assistant incorrectly concluded `audit_log` was missing from them — it wasn't; Document 1 was byte-identical to the round-3 upload used in *this* session, both containing the full `audit_log` schema/classification work. The assistant had diffed against a stale local copy rather than the actual upload, wrongly reverted the ER diagram to 24 tables (removing `audit_log`) as a result, and delivered that incorrect diagram. Caught and fixed in this session: the ER diagram (`.drawio`, `.mmd`, rendered `.pdf`/`.png`) is restored to 25 entities with `audit_log` present, matching the documents' actual, unchanged state. No document content needed correcting for this part — only the diagram, which was the only artifact affected by the earlier mistake.
---
## 2026-07-19 — Post-refactor documentation/diagram reconciliation pass
User asked to cross-check `01_Full_Development_Plan.md`, `02_Phase_By_Phase_Development_Plan.md`, `03_Progress_Tracker.md`, `Accounting_System_ER_Diagram.drawio`, and `er_diagram.mmd` against each other and against the actual current schema/code, and fix whatever had drifted. Four real issues found and fixed:
- **`02_Phase_By_Phase_Development_Plan.md`'s header endpoint count was wrong.** It read "28 API endpoints in the original sequence... 35 API endpoints, 36 phases total" — an off-by-one that matched neither `01_Full_Development_Plan.md` §6.2 ("27... 34...") nor this file's own summary (line 14, "27 API endpoints... 34 API endpoints, 36 phases total"). Corrected to 27/34 to match both.
- **`01_Full_Development_Plan.md`'s diagram-sync note was stale.** It still said the ER diagrams were "not yet updated for `audit_log`... still reflects 24 tables" — but per the entry directly above this one, that was already fixed earlier in the project (both diagram files have carried `audit_log`, 25 entities, since the assistant-error correction). Updated the note to reflect current reality and to record today's further sync work (below) instead.
- **`01_Full_Development_Plan.md` §4.2 named the wrong EF Core API** for the recursive-CTE queries (said `FromSqlInterpolated`; the actual code — `Repositories/RecursiveTreeQueries.cs` — uses `Database.SqlQuery<T>`/`SqlQueryRaw<T>`). Corrected, with a pointer to the shared helper and `CLAUDE.md` Rule 11.
- **Both ER diagrams were out of sync with real schema changes made during the build**, none of which had ever been reflected back into either diagram file: every column converted from `varchar` to an `integer`-backed enum (Rule 9 — `accounting_period.status`, `period_lock_log.action`, `journal_entry.entry_status`, `journal_correction.correction_type`, `recurring_journal_template.recurrence_pattern`, `tax_code.calculation_basis`/`applies_to`, `fixed_asset.depreciation_method`/`status`, `audit_log.category`/`action`); `fixed_asset`'s two later-added columns (`gain_loss_account_id`, `depreciation_rate_percent` — Phase 21/22 schema-gap fix); and `journal_entry.narration` (the Phase 1 approved addition, `01_Full_Development_Plan.md` — never made it into the diagram at all, in either file, even before this).
**Fixed in `er_diagram.mmd`** — all of the above, applied directly (it's a compact, safely-editable text file). **Left as-is in `Accounting_System_ER_Diagram.drawio`** — same issues, but that file is a single-line-per-entity HTML/XML blob (~190 lines, each one large) that can't be safely patched with targeted string edits without visually re-verifying every affected cell, and reading+rewriting it in full would cost roughly 150K+ tokens for one visual artifact. Flagged explicitly in `01_Full_Development_Plan.md`'s reference-documents note rather than either silently skipped or riskily edited blind — regenerate it from the now-current `.mmd` when convenient, per that document's own long-standing allowance for this exact situation.
No code changed in this pass — every fix was in the four Markdown/diagram files themselves, correcting documentation to match code and migrations that were already correct.
---
## 2026-07-19 — New feature: dev-only API key generator (`01_Full_Development_Plan.md` §11)
User-directed. `ApiKeyMiddleware` had no companion way to actually produce the `ApiKey:Value` it checks — added `POST /api/v1/dev-tools/generate-api-key`, backed by a new `Interfaces/Services/IDevToolsService.cs` + `Services/DevToolsService.cs` (no repository, no UoW — stateless). Generates a 256-bit random key (`RandomNumberGenerator.GetBytes(32)`, hex) and returns it; never persists it anywhere — still a manual copy-into-`appsettings`/core-backend step, same as `Jwt:SigningKey` always has been.
**Two independent guards, confirmed working live, not just by inspection:**
- `DevToolsService.GenerateApiKey()` throws `ForbiddenException` unless `IHostEnvironment.IsDevelopment()`. First test attempt (`ASPNETCORE_ENVIRONMENT=Production dotnet run`) appeared to fail — the endpoint still returned a key — but the actual cause was `dotnet run` loading `Properties/launchSettings.json`, which pins the profile's own `ASPNETCORE_ENVIRONMENT=Development` and silently overrides the shell's env var; not a code defect. Re-tested with `dotnet run --no-launch-profile`, confirmed via the startup log (`Hosting environment: Production`): the endpoint correctly returned `403 Forbidden`. Worth remembering for any future environment-dependent behavior tested via `dotnet run` rather than the published `.dll` directly.
- `Middleware/ApiKeyMiddleware.cs`'s `ExcludedPathPrefixes` gained `/api/v1/dev-tools`, so the bootstrap endpoint stays reachable without a key even once one is configured and the middleware enforcing it — otherwise there'd be no way to ever generate the first key. Confirmed live: the endpoint succeeds with no `X-Api-Key` header at all, while every other endpoint correctly 401s without one and 200s with the configured dev key.
**Noted in passing, not part of this change:** `Program.cs`'s `app.UseMiddleware<ApiKeyMiddleware>()` line, previously commented out for the whole session, is now active (changed outside this conversation) — every endpoint from here on genuinely requires `X-Api-Key` to be reachable, which is relevant context for the Postman collection / API reference doc work started next.
---
## 2026-07-19 — New deliverables: full Postman collection + API reference/scenarios doc
User-directed: a Postman collection covering every endpoint's success and failure variations, plus a companion document with the full request/response catalog, the internal Controller→Service→Repository flow for each endpoint, and worked end-to-end scenarios. Both actually exercised against the real local Postgres dev database (Development environment, `ApiKeyMiddleware` enabled) while being written — not written from code inspection alone.
**New files:**
- `General_Ledger_Service.postman_collection.json` — Postman Collection v2.1 schema, 15 folders (one per module, matching the API's own domain grouping, plus Dev Tools and Status & Health), **138 requests total** covering every one of the 36 actions across the original 27 endpoints + 6 appended list endpoints + audit-log + the new dev-tools endpoint, each with its documented success case(s) and every distinct failure branch (validation, not-found, business-rule rejection). Collection-level `{{baseUrl}}`/`{{apiKey}}` variables and an `apikey`-type auth block so every request sends `X-Api-Key` automatically. Validated as well-formed JSON after every module was added, not just at the end.
- `04_API_Reference_And_Scenarios.md` — per-module sections (mirroring the Postman folders) each with: a full request-field table, a response table covering every success/failure case with its exact `status`/`message`, and a "Flow" paragraph naming the exact Service/Repository methods involved in call order. Closes with 11 worked end-to-end scenarios (double-entry posting via both the inline-lines and separate-lines paths, full-reversal correction, partial-adjustment correction, fiscal year setup + period lock, recurring template lifecycle, fixed asset register→depreciate→dispose, bank import+reconcile, tax calculation, budget vs actual, system-config one-time lock, and API-key bootstrapping) built from the same live request/response pairs used to populate the Postman collection.
**Real findings from live testing, not assumptions:**
- **Every error response (`ExceptionHandlingMiddleware`, `ApiKeyMiddleware`) serializes with PascalCase keys (`StatusCode`, `Success`, `Message`, `Data`), while every success response serializes with camelCase (`statusCode`, `success`, ...).** Cause: both middleware classes call `JsonSerializer.Serialize(response)` with no options, falling back to `System.Text.Json`'s true PascalCase default, while MVC's controller pipeline uses the project's configured camelCase policy. Documented prominently in `04_API_Reference_And_Scenarios.md`'s "Known Inconsistency" section and flagged to the user — not fixed, since it touches two Rule-1 foundation components (`CLAUDE.md` Rule 1) and needs an explicit decision first.
- **`GET /budgets` never returns a budget's `lines`** — `BudgetRepository.GetAllAsync` doesn't eager-load the `Lines` navigation (unlike the `Add Lines` response, which does). `GET /reports?reportType=BudgetVsActual` is the only place a budget's lines appear with real figures. Documented, not changed.
- **There is no `GET` endpoint for fiscal years or periods anywhere in the API** — only create/generate/lock. The only way to discover an existing id is to have created it in the same session or read it off another response that embeds it. Documented as a known gap in the Fiscal Calendar module section.
- Confirmed live (not just by reading the code) that `Set Lines` on Journal Entries **replaces** wholesale while `Add Lines` on Budget **appends** — a deliberate difference between the two, verified by actually calling `Add Lines` twice on the same budget and observing 3 lines afterward, not 1.
**Test data used:** accounts `9001`/`9002`, fiscal year `FY2035-Doc` (id 5) with 12 auto-generated periods, cost centers `CC-DOC-1`/`CC-DOC-2`, tax code `VAT18-DOC`, fixed assets `FA-DOC-1`/`FA-DOC-2`, a bank account against GL account `1001`, and a real `.xlsx` file generated via a throwaway ClosedXML scratch console project (same pattern as the Phase 24 build, deleted afterward) for the bank-import tests — all left in the dev database as real, inspectable records rather than cleaned up, consistent with how every other phase's manual testing in this project has been handled.
---
## 2026-07-20 — Retrofit: unique codes (`accountCode`, cost center/asset/tax codes) are now system-generated and immutable (`CLAUDE.md` Rule 8.1, `01_Full_Development_Plan.md` §12)
User-directed: `Account.AccountCode`, `CostCenter.Code`, `FixedAsset.AssetCode`, and `TaxCode.Code` were client-supplied on their `Create*Request` DTOs (and `AccountCode` was editable via `PUT /accounts/{accountCode}`) — inconsistent with `JournalEntry.JournalNo`, which was already system-generated and immutable. Checked `Accounting_System_Design_LKAS.md`, `01_Full_Development_Plan.md`, and `02_Phase_By_Phase_Development_Plan.md` first, per Rule 0/this rule's own "find an existing scheme before inventing one" instruction — none of the three documents specify a generation algorithm for these four columns (just `VARCHAR UNIQUE NOT NULL`), so a scheme was designed fresh and confirmed with the user before implementation (see `CLAUDE.md` Rule 8.1 for the exact per-entity format).
**Code changes (all four entities, same shape):**
- `Create*Request` DTOs (`CreateAccountRequest`, `CreateCostCenterRequest`, `CreateFixedAssetRequest`, `CreateTaxCodeRequest`) lost their code field entirely; `UpdateAccountRequest` lost `AccountCode`.
- Each Service (`AccountService.CreateAccountAsync`, `CostCenterService.CreateCostCenterAsync`, `FixedAssetService.CreateAsync`, `TaxService.CreateTaxCodeAsync`) now generates the code internally, right before the entity is constructed, using small new repository counting methods (`CountRootAccountsByTypeAsync`/`CountChildAccountsAsync`, `CountRootCostCentersAsync`/`CountChildCostCentersAsync`, `CountAssetsAsync`, `CountTaxCodesByTypeAsync`).
- The now-dead `CodeExistsAsync` (Account, CostCenter, FixedAsset) and `TaxCodeExistsAsync` (Tax) uniqueness-conflict checks were deleted from both the repository implementations and their interfaces — there's no user-supplied value left to conflict with, and nothing else in the codebase referenced them (verified via a full-repo grep after the edits).
- `AccountService.UpdateAccountAsync` had its entire accountCode-change branch removed (the `accountCode` payload key is simply never read anymore) — the code is now immutable for the lifetime of the account.
- No database schema or migration change — all four columns stay the same `VARCHAR` type/length; only where the value comes from changed.
**Verified:** `dotnet build` — 0 warnings, 0 errors. Full-repo grep for `request.AccountCode`, `request.Code`, `request.AssetCode`, `.CodeExistsAsync`, `TaxCodeExistsAsync` — no remaining references.
**Docs updated as part of this retrofit:** `CLAUDE.md` (new Rule 8.1), `01_Full_Development_Plan.md` (new §12), `02_Phase_By_Phase_Development_Plan.md` (Phase 2's worked example + Phases 4/9/19/21's summary-table rows), `04_API_Reference_And_Scenarios.md` (Accounts/Cost Centers/Tax/Fixed Assets sections + Scenario 6), `General_Ledger_Service.postman_collection.json` (code fields removed from the four affected `Create*` request bodies; hardcoded test codes like `9001`/`CC-DOC-1`/`VAT18-DOC`/`FA-DOC-1` replaced with `<placeholder>` references to the actual generated value, matching the existing `<id of an account already at COA_MAX_DEPTH>` placeholder convention already in the file; the now-impossible "duplicate code" negative tests for Cost Center and Tax Code were removed, the Account one was repurposed into an "invalid accountTypeId" 400 test, and the Account "duplicate accountCode on rename" test was repurposed into a positive test confirming `accountCode` in an update body is silently ignored — collection re-validated as well-formed JSON, 135 requests, 135/135 still carrying `X-Api-Key`).
**Live-tested against the real dev database** (`dotnet run`, Development environment, port 5200) after the build succeeded:
- `POST /accounts` (no parent, existing dev DB already had 6 root Asset accounts from prior sessions) → `accountCode: "1600"` (1000 + 6×100), confirming the root-block formula picks up existing rows correctly rather than assuming a fresh database.
- `POST /accounts` (child of the above) → `accountCode: "1600-01"`.
- `POST /accounts` with `accountTypeId: 9` → `400`, `"Invalid accountTypeId '9'. Must be one of: 1 (Asset), 2 (Liability), 3 (Equity), 4 (Income), 5 (Expense)."`.
- `PUT /accounts/1600` with body `{ "accountName": "Renamed Root", "accountCode": "HACKED" }` → `200`, `accountName` updated, `accountCode` still `"1600"` — confirms immutability.
- `POST /cost-centers` (root) → `"CC-003"`; child of it → `"CC-003-01"`.
- `POST /tax-codes` (`taxTypeId: 1`, VAT) → `"VAT-003"`.
- `POST /fixed-assets` → `"FA-000005"`.
All six generated codes matched the documented scheme exactly on the first try. Dev server stopped after testing.
---
## 2026-07-20 — Deviation from design doc: amounts stored/returned at full precision, rounded to 2dp only in PDF rendering (`CLAUDE.md` Rule 13, `01_Full_Development_Plan.md` §13)
User-directed. `Accounting_System_Design_LKAS.md` specifies every monetary column as `NUMERIC(18,2)` — the user asked for a deliberate deviation: no rounding/truncation on storage or in any API response; a value is only ever rounded to 2 decimal places when a PDF is rendered.
**Database — migration `WidenAmountAndRateColumnsToUnboundedNumeric`:** removed `.HasPrecision(...)` from all 20 `decimal`/`decimal?` properties in `AppDBContext.cs` (12 true amount columns across `JournalEntryLine`, `RecurringJournalTemplateLine`, `FixedAsset`, `DepreciationSchedule`, `AssetDisposal`, `BankStatementLine`, `BudgetLine`, plus 4 rate/multiplier columns — user chose "widen everything decimal, including rates" over "amounts only" when asked). Npgsql now maps every one of these to a bare Postgres `numeric` with no precision/scale ceiling. Applied against the dev database — 20 `ALTER COLUMN ... TYPE numeric` statements executed cleanly, no data loss (widening, not narrowing).
**Application layer:** confirmed via full-repo grep that there was no `Math.Round`/`decimal.Round`/`MidpointRounding` anywhere in the codebase before this change — the `NUMERIC(18,2)` column type was the only thing enforcing 2 decimals, so removing it was sufficient on its own to satisfy "don't limit amount values to 2 decimal points when sending data."
**PDF rendering (`ReportService.RenderPdf`) — the one intentional exception:** added a `FormatCellValue` helper that rounds `decimal`/`decimal?` cell values via `Math.Round(d, 2, MidpointRounding.AwayFromZero)` before calling `.ToString("F2")`; every other property type still just calls `.ToString()`. `AwayFromZero` (not .NET's `ToEven` default) implements the requested "number routing" rule exactly: `1000.005` → `1000.01`, not down to `1000.00`.
**Verified:**
- `dotnet build` — 0 warnings, 0 errors, both before and after the migration.
- Migration reviewed before applying — every `AlterColumn` widens `numeric(x,y)` → `numeric` (unbounded), fully reversible `Down()` restores the original precision/scale.
- Rounding boundary values checked in a throwaway scratch console app before wiring into `ReportService`: `1000.005`→`1000.01`, `1000.0149999`→`1000.01`, `1000.015`→`1000.02`, `1000.0574536`→`1000.06`, `1000.0`→`1000.00`, `1000.004999`→`1000.00` — all six matched the spec exactly.
- Live end-to-end against the real dev database: created a journal entry with `debitAmount: 1000.1234567` (7 decimal places) — `POST /journal-entries` response, `PUT .../post` response, and `GET /reports?reportType=TrialBalance&outputFormat=Json` all returned `1000.1234567` unrounded. Generated the same report with `outputFormat=Pdf` — renders successfully (88KB, no exceptions), confirming `FormatCellValue` handles the full-precision decimal without error. Did not attempt to verify the rendered glyphs pixel-by-pixel (QuestPDF subsets/remaps its embedded font, so the PDF's internal content stream doesn't contain the amount as plain ASCII text even after decompressing it — confirmed this while trying to grep the decompressed stream) — code-path inspection plus the isolated rounding-logic verification above are the basis for confidence here, not a rendered-text check.
- Test data left in the dev database as real, inspectable records (JV-2035-000011, accounts 1600/1600-01, CC-003/CC-003-01, VAT-003, FA-000005), not cleaned up — consistent with this project's established testing convention.
---
## 2026-07-20 — Retrofit: `GeneralLedger` report's `accountId` made optional — true General Ledger mode added (Phase 17, `Accounting_System_Design_LKAS.md` §11.1)
User-directed. `GET /reports?reportType=GeneralLedger` required `accountId`, making it an "Account Ledger" (one account + its recursive descendants) rather than a real General Ledger, which shows every account's transactions together. Fixed without changing the existing single-account behavior at all.
**Code changes:**
- `Interfaces/Repositories/IJournalEntryRepository.cs` / `Repositories/JournalEntryRepository.cs` — added `GetGeneralLedgerLinesForAllAccountsAsync(DateOnly periodStart, DateOnly periodEnd)`. Same SQL shape as the existing `GetGeneralLedgerLinesAsync` (identical `PARTITION BY jel.account_id` running-balance window, identical `ORDER BY a.account_code, je.entry_date, je.journal_id`), minus the `descendants` CTE/accountId filter, plus `AND a.is_postable = TRUE`. The existing method is untouched.
- `Services/ReportService.BuildGeneralLedgerAsync` — dropped the "`accountId` is required" throw; `periodStart`/`periodEnd` still required. Branches on `request.AccountId.HasValue`: present → unchanged existing path (404 check + `GetGeneralLedgerLinesAsync`); absent → new `GetGeneralLedgerLinesForAllAccountsAsync`.
- No DTO changes (`ListReportsRequest.AccountId` was already `long?`), no migration, no UoW, no audit row (read-only, same as every other report).
**Verified:** `dotnet build` — 0 warnings, 0 errors.
**Docs updated:** `Accounting_System_Design_LKAS.md` §11.1 (added the second query variant), `04_API_Reference_And_Scenarios.md` Reporting module (both modes documented, missing-param example changed since `accountId` is no longer required), `02_Phase_By_Phase_Development_Plan.md` Phase 17 (param description + acceptance test updated to cover both modes and the postable-only exclusion).
**Live-tested against the real dev database** (`dotnet run`, Development environment, port 5200):
- `GET /reports?reportType=GeneralLedger&outputFormat=Json&accountId=5&periodStart=2020-01-01&periodEnd=2036-12-31` → unchanged: only account `1500`'s own lines, one running balance sequence (`-12000.00` → `-24000.00` → `-22999.8765433`).
- Same call with `accountId` omitted → lines from every postable account with activity in range (`1000`, `1001`, `1002`, `1500`, `1510`, `4900`, `5100`, `9001`, `9002`), sorted by `accountCode` then `entryDate`; running balance visibly resets at each account boundary (e.g. `1000` ends its block at `23300.00`, `1001`'s very next row starts a fresh sequence at `-1000.00`, not continuing from `23300.00`).
- Cross-checked against `GET /accounts?isPostable=false`, which returns exactly one row in this dev DB (`1999`, "Header Account"). `1999` does not appear anywhere in the no-`accountId` response's account set — confirms the postable-only exclusion.
---
## 2026-07-20 — Retrofit: `BalanceSheet` report now actually balances (Phase 17, `Accounting_System_Design_LKAS.md` §11.3)
User-directed. `BuildBalanceSheetAsync` only summed Asset/Liability/Equity account balances, so `Assets ≠ Liabilities + Equity` for any `asOfDate` after the fiscal year started — the current period's net profit/loss sits in Income/Expense accounts, which the Balance Sheet excludes, and there's no closing-entry mechanism anywhere in this codebase (verified by grepping the whole repo for "Retained Earnings"/"closing entry" — the only hit is the design doc's own prose) that ever moves that value into Equity.
**Open question resolved before implementing:** should "Current Year Earnings" be scoped to just the fiscal year containing `asOfDate`, or cumulative since inception? Confirmed with the user: **cumulative since inception.** Reasoning: since no closing mechanism exists, a fiscal-year-scoped figure would only make the equation hold for the *first* fiscal year a ledger is used — the moment a second fiscal year has any posted activity, that prior year's profit would be stranded in Income/Expense accounts with nowhere else to go, breaking the equation again. This also simplified the implementation: no `FiscalCalendar` lookup needed at all.
**Code change:** `Services/ReportService.BuildBalanceSheetAsync` — after the existing `GetBalanceSheetAsync(asOfDate)` call (unchanged), added `GetProfitAndLossAsync(DateOnly.MinValue, asOfDate)` (reusing the existing method, no new repository code) and summed its depth-1 rows — the exact same reduction `BuildCashFlowAsync.netProfit` already does. Appended one row: `{ Depth = 1, LineItem = "Current Year Earnings", AccountType = "Equity", Balance = <that sum> }`, always present (even at `0.00`) as the last row, so the response shape never varies based on whether profit happens to be zero.
**Verified:** `dotnet build` — 0 warnings, 0 errors. (One build attempt blocked by a locked `.exe` from a running dev-server instance, PID 6100 on ports 5200/7024 — confirmed with the user it was safe to stop before retrying; build succeeded immediately after.)
**Docs updated:** `Accounting_System_Design_LKAS.md` §11.3 (documented the cumulative-since-inception deviation from the design doc's own "current-year profit" wording, with the reasoning), `04_API_Reference_And_Scenarios.md` Reporting module (BalanceSheet row + retrofit note), `02_Phase_By_Phase_Development_Plan.md` Phase 17 (acceptance test now requires `Assets = Liabilities + Equity` to hold, not just "renders without error").
**Live-tested against the real dev database** (`dotnet run`, Development environment, port 5200):
- `GET /reports?reportType=BalanceSheet&outputFormat=Json&asOfDate=2030-01-16` (before any Income/Expense activity, only Asset accounts touched so far): `"Current Year Earnings"` row present at `0.00`; summing depth-1 rows, `Assets = 0`, `Liabilities = 0` (no Liability accounts exist in this dev DB), `Equity = 0` — equation holds trivially.
- `GET /reports?reportType=BalanceSheet&outputFormat=Json&asOfDate=2036-12-31` (all posted activity, including depreciation/disposal entries touching Income/Expense accounts 4900/5100): summing depth-1 rows precisely via PowerShell, `Assets = -10000`, `Liabilities = 0`, `Equity = -10000` (driven by `"Current Year Earnings": -10000.00`) — `Assets = Liabilities + Equity` exactly, confirmed programmatically (`Equal: True`), not just eyeballed.
---
## 2026-07-21 — Full test dataset seeded via the live API; three real bugs found and fixed in the process
User-directed: "fill the database with actual data... not just 1 or 2 rows... full data collection to check any endpoint." Seeded entirely through the running API (not direct SQL), so every business rule (double-entry balance, code generation, audit logging, depth guards) was naturally exercised rather than bypassed. This uncovered three real, previously-latent bugs — none of them seed-script issues, all genuine reliability problems that would eventually have hit real usage once enough data existed.
**Dataset created:** a realistic FY2027 books-of-account for a small company — ~30 new Chart of Accounts entries (2-level Asset/Liability tree, flat Equity/Income/Expense), 5 cost centers (3 levels), 4 tax codes (VAT/WHT/NBT/SSCL), 12 fiscal periods, 20 journal entries (including a full-reversal correction, a partial-adjustment correction, and one deliberately-left-draft entry), 3 recurring templates (one generated + posted), 3 fixed assets (StraightLine and ReducingBalance, one disposed, all depreciated across 3 periods), 1 bank account with 8 imported statement lines (6 reconciled against real posted entries, 2 deliberately left unmatched), and 1 budget with 6 lines across periods/cost centers. Left in the dev database as real, inspectable records, per this project's established testing convention.
**Bug 1 — account/cost-center/fixed-asset/tax-code generation collided with pre-existing legacy codes (500, not a clean 400).** The Rule 8.1 code generators (`AccountService.GenerateAccountCodeAsync`, `CostCenterService.GenerateCostCenterCodeAsync`, `FixedAssetService.GenerateAssetCodeAsync`, `TaxService.GenerateTaxCodeAsync`) trusted a `COUNT(*)`-based guess for the next slot. That guess collides with any account created before the retrofit under the old user-supplied-code regime that happens to occupy a slot the count formula will also generate (reproduced exactly: an Expense-type root account manually coded `"5100"` before the retrofit made every subsequent `blockStart + count*100` computation collide on that same `"5100"`, throwing an unhandled unique-constraint violation). **Fixed** by probing candidate codes directly via each repository's `GetByCodeAsync` and advancing to the next slot on collision, instead of trusting a count — regardless of legacy data. Added `ITaxRepository.GetByCodeAsync` (mirroring the pattern already used by the other three repositories) since Tax needed a code-existence check it didn't have after `TaxCodeExistsAsync` was removed in the original retrofit. Removed the now-unused `CountRootAccountsByTypeAsync`/`CountChildAccountsAsync`/`CountRootCostCentersAsync`/`CountChildCostCentersAsync`/`CountAssetsAsync`/`CountTaxCodesByTypeAsync`.
**Bug 2 — every `SUM()`-based report threw a bare 500 once realistic transaction volume existed.** Root cause: straight-line depreciation often doesn't divide evenly (`800000/96 = 8333.333333333333333333333333`, 28 significant digits — already at C#'s `decimal` ceiling for a single value); the Rule 13 unbounded-precision columns let Postgres's `SUM()` aggregate several such values into something exceeding what `decimal` can hold at all. Affected `TrialBalance`, `BalanceSheet`, `ProfitAndLoss`, `CashFlow`, `GeneralLedger`'s running balance, and `BudgetVsActual` — every report doing `SUM()`. **Fixed** by wrapping only the SQL-side `SUM(...)` expressions with `ROUND(..., 10)` in `AccountRepository` (`GetTrialBalanceAsync`, `GetBalanceSheetAsync`, `GetProfitAndLossAsync`, `GetCashFlowCategoryMovementAsync`), `JournalEntryRepository` (both `GetGeneralLedgerLines*Async` running-balance windows), and `BudgetRepository.GetBudgetVsActualAsync` — bounding only the aggregate at the point it's read into a `decimal`; storage and single-row responses stay exactly as unrounded as Rule 13 requires.
**Bug 3 — `BalanceSheet` had a sign-convention bug for Liability/Equity, unrelated to bugs 1-2.** `GetBalanceSheetAsync` computed `debit - credit` uniformly for Asset/Liability/Equity, correct only for Asset (debit-normal) — a real credit balance (e.g. a bank loan) showed as negative. Never caught before because the `Assets = Liabilities + Equity` check added earlier (2026-07-20, see above) was only ever tested against a dataset with zero Liability balance, where the bug is invisible. **Fixed** by flipping the sign for non-Asset types (`CASE WHEN account_type_id = 1 THEN debit - credit ELSE credit - debit END`), mirroring the flip `GetProfitAndLossAsync` already correctly applies for Income/Expense.
**Verified live, in order, after each fix:** `dotnet build` clean each time; the exact previously-colliding account create (`"Cost of Goods Sold"`, Expense root) now succeeds; all six report paths return `200` instead of `500`; `BalanceSheet` against the full seeded dataset: `Assets = 3036000`, `Liabilities + Equity = 2054000 + 982000 = 3036000` — exact match, confirmed programmatically via PowerShell, not eyeballed.
**Process note:** twice during this session a `General-Ledgar-Service.exe` was found holding port 5200 mid-rebuild from a source other than the current turn's own `dotnet run` — confirmed with the user each time before terminating it (per the standing rule to ask when a running process isn't clearly this turn's own).
**Docs updated:** `04_API_Reference_And_Scenarios.md` (Reporting module — both bugfix notes). This entry is the authoritative record for `02_Phase_By_Phase_Development_Plan.md` Phase 17's acceptance test, which now actually passes the `Assets = Liabilities + Equity` condition added in the earlier BalanceSheet retrofit (previously unreachable due to Bug 2 blocking the report outright, then Bug 3 once it ran).
---
## 2026-07-21 — Three fixes specified (not yet implemented): General Ledger PDF layout, full LKAS 7 Cash Flow, `Company:Name`
User raised three concerns after reviewing the LKAS PDF template spec (`05_LKAS_Report_PDF_Templates.md`) delivered the prior session:
**1. General Ledger PDF template corrected.** The prior spec grouped the report into separate boxed sub-tables per account, each with its own repeated column-header row. User clarified the actual Sri Lankan practical/regional convention (not an LKAS-prescribed format — LKAS doesn't govern internal ledgers) is a **single continuous table** for the whole report: `Date | Particulars | Description/Reference (J.R.) | Debit | Credit | Balance`. Revised: one table, one header row, account boundaries marked by an inline bold group-header row (`Account: [code] — [name]`) rather than separate tables — preserves per-account running-balance correctness (mathematically required — a `Balance` column only means something within one account's transaction stream) while matching the single-table convention requested. `Particulars` maps to the existing `narration` field; `Description/Reference (J.R.)` maps to `journalNo`. No API/response shape change — this was a PDF-layout-only fix, `GeneralLedger`'s `outputFormat=Json` shape is untouched.
**2. `CashFlow` upgraded from documented simplification to full LKAS 7 spec.** Previously accepted as a scope simplification (2026-07-20 entry, mid-file) — user asked to actually close the gap and match Section 5a of the PDF template doc (the full LKAS 7 layout, not the "(Summary)" fallback). Full redesign at `Accounting_System_Design_LKAS.md` §11.5 (replaces the original sketch that was never fully implemented, and fixes a real dead-code bug found in the process: `WHERE t.account_type_id NOT IN (SELECT id FROM account_type WHERE name ILIKE '%cash%')` was always a no-op, since `account_type` never contains a row literally named "Cash"). New mechanism: `system_config` key `CASH_ACCOUNT_ROOT_ID` identifies the "Cash and Cash Equivalents" COA root; every recursive descendant is cash-like — same pattern as every other COA-hierarchy-driven classification in this system (Balance Sheet's current/non-current split works the same way). Non-cash adjustments reuse the already-populated `depreciation_schedule` table (never read by this report before). Response shape changes from a flat 6-field object to a nested one (`operatingActivities`/`investingActivities`/`financingActivities`/opening+closing cash) — see `04_API_Reference_And_Scenarios.md`'s updated `CashFlow` row. **Not yet implemented** — this is a specification only; new Document 2 §1.10 and tracker rows 1.10/17 mark it outstanding, not done. `CASH_ACCOUNT_ROOT_ID` specifically **cannot** be `HasData`-seeded (unlike the other three config keys) — it references a COA account that doesn't exist until a real company's Chart of Accounts has been created via Phase 2, so it has to be set later via the existing `PUT /system-config` endpoint, not at migration time.
**3. `Company:Name` confirmed for `appsettings.json`, not `system_config`.** The PDF template doc's prior recommendation left this open between the two; user confirmed `appsettings.json` — consistent with it being a deployment-time constant (like `Jwt:SigningKey`/`ApiKey:Value`) rather than a runtime-editable business value. Unlike those two secrets, every environment gets a real value, not blank-in-Production, since a company name isn't sensitive. New Document 2 §1.9 and tracker row 1.9, both **not yet implemented**.
**Docs updated this round:** `05_LKAS_Report_PDF_Templates.md` (General Ledger and Cash Flow sections rewritten, company-name section corrected to `appsettings`), `Accounting_System_Design_LKAS.md` (§11.5 full rewrite, §12.2 gains `CASH_ACCOUNT_ROOT_ID`), `04_API_Reference_And_Scenarios.md` (`CashFlow` response shape row, simplification note replaced with upgrade-spec note), `01_Full_Development_Plan.md` (appsettings table gains `Company:Name`), `02_Phase_By_Phase_Development_Plan.md` (Phase 17 gains the CashFlow/PDF spec notes and acceptance-test addition; Phase 1 gains §1.9/§1.10), this file (Phase 1 and Phase 17 rows updated, two new Phase 1 sub-rows added). **None of this was implemented or tested live this round** — every change above is a specification update, consistent with how this file distinguishes "documented" from "done."
---
## 2026-07-21 (same day, follow-up) — New `TaxSummary` report (7th `reportType`) + A4 page size for all PDF output
Two more additions, both specification-only like everything else logged above today:
**1. `TaxSummary` — new 7th `reportType` on the existing `GET /reports` endpoint.** Not a new phase/endpoint — the whole reason this endpoint uses a `reportType` discriminator (rather than one route per report) is exactly so a new report doesn't need a new route. Deliberately scoped as a **practical VAT/WHT/NBT return-preparation summary**, not an LKAS 12 disclosure note — this system has no deferred tax tracking anywhere, so a genuine LKAS 12 tax-expense reconciliation isn't something the current schema can produce, and the spec says so explicitly rather than overclaiming compliance. One row per active `tax_code`: `outputTaxAmount` (sum of activity in that code's `output_tax_account_id`), `inputTaxAmount` (same, `input_tax_account_id`), `netTaxPayable` (the difference — positive owed to Inland Revenue, negative a net receivable position, both valid outcomes, neither clamped). **A real constraint surfaced while designing this:** `journal_entry_line` has no `tax_code_id` column, so there's no way to attribute one posted line back to the exact tax code that produced it — the report works around this by joining on each tax code's own distinct output/input GL accounts instead, which only stays accurate if no two tax codes ever share an account. Full query: `Accounting_System_Design_LKAS.md` §11.7 (new). API surface: `04_API_Reference_And_Scenarios.md`'s reports table + reportType error-message enum, both updated. Phase-plan notes: `02_Phase_By_Phase_Development_Plan.md` Phase 17. Tracker: Phase 17's row, 4th outstanding item.
**2. Every PDF report specified to render on A4.** `page.Size(PageSizes.A4)` (QuestPDF's built-in constant), portrait by default, 2cm margins — applies once the bespoke per-report `IDocument` layouts (from the prior LKAS template work) replace the current generic renderer, whose page size was never explicitly set or confirmed before now. Landscape flagged as an available per-report override for wider tables (Trial Balance, General Ledger, Budget vs Actual are the likely candidates given column count) rather than a blanket rule — `05_LKAS_Report_PDF_Templates.md` Section 1 carries the actual orientation guidance per report.
**Docs updated this round:** `Accounting_System_Design_LKAS.md` (new §11.7, Section 9's summary table row updated to match the real account-level-join mechanism instead of the original "tax_code-linked lines" phrasing, which implied a direct link that was never actually built), `04_API_Reference_And_Scenarios.md` (reports table gains `TaxSummary` row, intro line, error-message enum, two new spec notes — `TaxSummary` and A4), `01_Full_Development_Plan.md` (Phase 17's endpoint description gains `TaxSummary`), `02_Phase_By_Phase_Development_Plan.md` (Phase 17 gains `TaxSummary`/A4 spec notes, dispatch list, repository list, and acceptance-test additions), this file (Phase 17's row updated to 4 outstanding items). **Nothing implemented or tested live this round** — same discipline as every other entry in this log: specification and implementation are tracked as separate facts, never conflated.
---
## 2026-07-21 (same day, follow-up) - All 4 outstanding Phase 17 retrofits implemented and verified live: Company:Name, CASH_ACCOUNT_ROOT_ID, full LKAS 7 CashFlow, TaxSummary, and all 7 bespoke PDF templates
User-directed: turn the specifications written earlier today (05_LKAS_Report_PDF_Templates.md, Accounting_System_Design_LKAS.md Section 11.5/11.7) into real, tested code. Implemented and verified end-to-end, live against the dev database, in the order 05's own Section 9 recommends (simplest/lowest-risk first, CashFlow last since it has the most new logic).
**1. Company:Name.** Added to appsettings.json/appsettings.Development.json/appsettings.Production.json (a real value in every environment, unlike ApiKey:Value - not a secret, per 02_Phase_By_Phase_Development_Plan.md Section 1.9). Read via IConfiguration in ReportService, falls back to "Company Name Not Configured" if somehow missing rather than crashing PDF generation.
**2. CASH_ACCOUNT_ROOT_ID system_config key.** Migration AddCashAccountRootIdConfig seeds a 4th system_config row with an empty placeholder ConfigValue (can't be seeded with a real value - no COA exists at migration time, per Section 1.10) and IsEditable=true/IsLocked=false. Classified Ledger in SystemConfigService.KeyCategories. ReportService.BuildCashFlowAsync resolves and validates it before doing anything else, throwing a clear BadRequestException if unset/invalid - a deliberate improvement over the design doc's inline-SQL-subquery sketch, which would have silently produced an empty/wrong cash-accounts set instead of a clear error.
**3. CashFlow - full LKAS 7 rewrite (Accounting_System_Design_LKAS.md Section 11.5).** New nested CashFlowReportDto (CashFlowOperatingActivitiesDto/CashFlowInvestingActivitiesDto/CashFlowFinancingActivitiesDto, opening/closing cash). New repository methods: AccountRepository.GetCashFlowWorkingCapitalChangesAsync (sign-adjusted per account type, same CASE WHEN account_type_id = 1 flip as the Balance Sheet fix), GetCashFlowCategoryLinesAsync (itemized, replaces the old single-aggregate GetCashFlowCategoryMovementAsync, now removed along with its unused CashFlowCategoryMovementRow DTO), GetCashBalanceAsync; FixedAssetRepository.GetDepreciationAddBackAsync (reads depreciation_schedule, never queried by any report before this). All new SUM()s wrapped in ROUND(...,10), same overflow guard as the 2026-07-20 bugfix.
**Real bug found and fixed during live verification, not assumed correct from the spec:** first live test (real FY2027 seeded data, CASH_ACCOUNT_ROOT_ID pointed at a new "Cash and Cash Equivalents" header) produced closingCashBalance (2,591,150) not equal to openingCashBalance + netIncreaseDecreaseInCash (3,520,212.50) - a real mismatch, chased down per the design doc's own "if these don't match, that's a bug, chase it down" instruction. Root cause: the test COA's cash_flow_category = 'INVESTING' tag was applied to a whole header account ("Non-Current Assets"), which cascades to all descendants including Accumulated Depreciation contra-accounts and, for a disposed asset, the disposal's cost-removal entry - neither is a real cash movement (depreciation's cash effect is already captured once via the Operating-section add-back; a disposal's cost-removal is a bookkeeping entry, not the actual cash proceeds, which post to a separate cash account already excluded). This is not a code bug - the repository/service code faithfully implements the design doc's own query - it's a COA-design constraint worth recording: cash_flow_category must be tagged on individual leaf accounts that genuinely have their own cash-relevant movement (asset cost accounts, specific liability/equity accounts), never on a broad header, since this schema has no way to "un-tag" a descendant from an ancestor's classification once inherited (COALESCE(a.cash_flow_category, parent's), same mechanism Accounting_System_Design_LKAS.md Section 11.3 already uses for Balance Sheet current/non-current). Confirmed the underlying mechanism is correct with a clean, correctly-tagged test (fresh accounts, fresh period, no header-level tagging): openingCashBalance (2,591,150.00) + netIncreaseDecreaseInCash (180,000.00) = closingCashBalance (2,771,150.00) exactly.
**4. TaxSummary - new 7th reportType (Accounting_System_Design_LKAS.md Section 11.7).** ReportType.TaxSummary = 7. New TaxSummaryRowDto. New ITaxRepository method: GetTaxSummaryAsync(periodStart, periodEnd), a flat (non-recursive) join of tax_code/tax_type against separately-aggregated output/input account movements. Verified live: correctly showed a negative netTaxPayable for WHT-001 (input exceeded output, a real receivable position from the seeded WHT remittance) and identical outputTaxAmount across VAT-004/NBT-001/SSCL-001 for the same period - confirmed as the expected consequence of the seed data's tax codes sharing one output account (VAT Payable), exactly the accuracy constraint the design doc itself flags, not a bug.
**5. All 7 bespoke PDF templates (05_LKAS_Report_PDF_Templates.md), replacing the single generic reflection-based RenderPdf/FormatCellValue.** New Services/PdfDocuments/ folder: PdfFormatting (shared FormatAmount/FormatAmountOrBlank/FormatAmountOrDash - same round-half-up 2dp rule as the old FormatCellValue, parentheses for negatives), ReportPageHeader (shared header/footer static helper - company name, title, period line, currency, footer with generated-timestamp and page-of-page numbers), and one IDocument class per report: TrialBalancePdfDocument (landscape, monospace-preserved indentation, blank-not-zero Debit/Credit, totals row), BudgetVsActualPdfDocument (portrait, flat table incl. a Period column since a budget can span periods), TaxSummaryPdfDocument (portrait, dash-not-zero Output/Input Tax), GeneralLedgerPdfDocument (landscape, one continuous table with shaded full-width group-header rows per account, not separate boxed tables - confirmed QuestPDF's table.Header automatically repeats the column header on every page), BalanceSheetPdfDocument (groups by accountType, sums only depth-1 rows for section totals since deeper rows are already subtree rollups), ProfitAndLossPdfDocument (same grouping approach, computes PROFIT FOR THE PERIOD in the renderer since the response has no pre-computed net figure), CashFlowPdfDocument (full nested structure, computed "Operating profit before working capital changes" subtotal, conditional "used in"/"from" and "Increase"/"Decrease" wording based on sign). One deliberate simplification vs. the spec's illustration: working-capital-change lines are rendered as the plain account name plus its already-signed amount, not prefixed with "Increase in X"/"Decrease in X" - deriving that wording correctly needs the account's own type (asset vs liability), which CashFlowWorkingCapitalChangeDto doesn't carry, and the design doc's own illustration shows both a positive and a negative line labelled "Increase in...", i.e. the sign alone doesn't determine the correct word either. Also added ICurrencyRepository.GetBaseCurrencyCodeAsync() (new, small, targeted - avoids fetching all ~140 seeded currency rows just to find the one with is_base_currency = true).
**Verified live for every one of the 7 reports** (dotnet build clean throughout): outputFormat=Pdf returns a valid %PDF-1.4 file for all seven (sizes 42KB-91KB); visually inspected BalanceSheet (TOTAL ASSETS = TOTAL EQUITY AND LIABILITIES = 1,627,350.00 exactly, negative parentheses, monospace indentation all correct), GeneralLedger (10 pages, group-headers, per-account running-balance resets, column headers repeating per page, all correct), and CashFlow (opening+increase=closing reconciliation renders exactly as computed).
**Docs updated:** this file (Phase 17 row + this entry). 04_API_Reference_And_Scenarios.md, 02_Phase_By_Phase_Development_Plan.md, 05_LKAS_Report_PDF_Templates.md, and Accounting_System_Design_LKAS.md's remaining "not yet implemented" markers for these 5 items still need updating to reflect completion - flagged as the next step, not done as part of this entry, to keep this log's own "don't conflate documentation with implementation" discipline intact until that pass is actually done.
---
## 2026-07-22 — Five reference report templates supplied by the user; real gaps found in what was built 2026-07-21; six new retrofits specified (none implemented yet)
User supplied five reference images — Profit & Loss, Trial Balance, Balance Sheet, Cash Flow, and Tax Report templates — asking that report generation be modified to match them, and that every report gain a CSV export alongside its existing PDF (frontend to gain a second "Download CSV" button next to "Download PDF"). Comparing each against what was actually specified/built the day before surfaced genuine gaps, not just cosmetic differences:
**1. Trial Balance — presentation-only fix.** The reference template is a plain flat list, no account-tree indentation. The 2026-07-21 design carried `depth`/`indentedCode` for hierarchy. Simplified to `{ accountCode, accountName, debit, credit }` — no data/query change, PDF/CSV layout only.
**2. Profit & Loss — real structural gap.** The reference Income Statement separates Sales from Other Income and splits Expenses into Cost of Sales (for Gross Profit) plus Distribution/Administration/Other/Financial groups (for Net Profit) — none of which the existing flat `{depth, lineItem, accountType, amount}` array (just Income vs. Expense) can express. New `accounts.income_statement_section` column added, same recursive-tag mechanism as `cash_flow_category` — this is the second time this exact pattern ("a report needs a classification the schema doesn't have, solved by tagging COA header nodes and inheriting down the tree") has been used, after `cash_flow_category` itself. Full redesign: `Accounting_System_Design_LKAS.md` §11.4.
**3. Cash Flow — display simplified, one real data gap closed.** The reference template has no opening/closing cash balance section (ends at the combined total) and labels working-capital lines "Increase in X"/"Decrease in X". The no-balance-section part is a display change only (the reconciliation still computes and returns, just isn't printed). The "Increase/Decrease" wording is a genuine gap the previous session's own Deviations entry (directly above this one) already flagged and consciously deferred: *"deriving that wording correctly needs the account's own type... which CashFlowWorkingCapitalChangeDto doesn't carry"* — closed now by adding a `direction` field, computed from the raw balance-change sign (not the cash-impact sign, which is a different thing — see `Accounting_System_Design_LKAS.md` §11.5's note on why these two signs aren't the same). Also: non-cash adjustments and working-capital changes get combined and re-bucketed by sign into "Additions to Cash"/"Subtractions From Cash" at render time, matching the reference template's own grouping.
**4. Tax Report — not a gap, a wrong report.** The reference template is a full **Corporate Income Tax Computation** (Profit Before Tax → Non-Deductible Expenses → Adjusted Business Profit → Assessable Income → Taxable Income → Corporate Tax → Gross Tax Liability → less APIT/WHT/Quarterly credits → Balance Tax Payable/(Refund)) — a fundamentally different report from the VAT/WHT/NBT per-`tax_code` summary built 2026-07-21 under the same `reportType` name. This is a **wholesale content replacement**, not an extension: new `accounts.is_tax_deductible` column, five new `system_config` keys (two seedable, three not — same `HasData` constraint `CASH_ACCOUNT_ROOT_ID` already established), four new optional request parameters for figures that are genuinely tax-filing-time judgment calls rather than GL-derivable (`allowableDeductions`, `otherTaxableIncome`, `qualifyingPaymentsReliefs`, `surchargeAmount`). The superseded VAT/WHT/NBT design is **kept, not deleted** (`Accounting_System_Design_LKAS.md` §11.7†, `05_LKAS_Report_PDF_Templates.md` §8†) — it's still a real, useful report for periodic indirect-tax return preparation, just not what "Tax Report" turned out to mean here; flagged as needing its own `reportType` (e.g. `IndirectTaxSummary`) if it's still wanted as a separate report later.
**5. `outputFormat=Csv` — new third format, all seven reports.** Same `ApiResponse`-wrapped base64 convention as `Pdf`. Per-report flattening rules (how each nested/hierarchical shape becomes flat rows with a `Section`/`Category` column) fully specified: `05_LKAS_Report_PDF_Templates.md` §10 (new).
**6. `Company:Address`/`Tin`/`Brn`** — three new `appsettings` fields, needed only by the Income Tax Computation's own header block (a fuller identity block than the other six reports use — company address, Taxpayer Identification Number, Business Registration Number, standard IRD filing identifiers).
**A genuine lesson worth internalizing, stated in `05_LKAS_Report_PDF_Templates.md` §9 too:** this is the second time in two days a report has needed real rework once an actual reference format arrived (`CashFlow`'s LKAS 7 gap, now this). Get the actual required template confirmed before building against an assumed structure, where that's possible.
**Status: nothing above is implemented yet** — this entire entry is a specification update, same discipline as every other entry in this log. `03_Progress_Tracker.md` Phase 17's row above reflects "4 retrofits done 2026-07-21, 6 more outstanding as of 2026-07-22," not "done."
**Docs updated this round:** `Accounting_System_Design_LKAS.md` (§11.4 full rewrite, §11.5 updated, §11.7 full rewrite + §11.7† for the superseded design, §9/§12.2 table updates), `04_API_Reference_And_Scenarios.md` (reports table, six new/updated retrofit notes, `Csv` added throughout), `05_LKAS_Report_PDF_Templates.md` (§0 expanded, §1/§2/§4/§5/§8 rewritten, §8† added, §9 rewritten, §10 new), `01_Full_Development_Plan.md` (appsettings table, Phase 17 endpoint row), `02_Phase_By_Phase_Development_Plan.md` (Phase 17's query params/spec notes/dispatch/repository list/acceptance test, Phase 1 §1.9 expanded + §1.11/§1.12 new), this file (Phase 17 row reopened, Phase 1 rows 1.9/1.10/1.11/1.12, this entry). **Not yet updated, deliberately deferred to keep this response scoped:** the two frontend-project documents (`12-GENERAL-LEDGER-INTEGRATION.md`, `21-GENERAL-LEDGER-FRONTEND.md`) — CSV button, new Tax Report screen, Cash Flow screen's shape change, and the five templates' visual layout all still need to be threaded through those two docs.
---
## 2026-07-22 (same day, follow-up #2) — Cash/Bank `accountType` discriminator; frontend's Cash/Bank create page surfaced a real gap
User flagged a real problem while looking at the frontend's Cash/Bank Accounts create page: it needs to create either a Cash or a Bank account, but only `bank_account`/`POST /bank-accounts` existed — nothing modeled "Cash" as a distinct, valid thing to create, and there was no mechanism restricting the create flow to exactly those two concepts.
**The fix:** new required `accountType` (`CASH` | `BANK`) field on `bank_account` and its create request — same discriminator-over-near-duplicate-structures pattern already used throughout this project (`ReportType`, `CorrectionType`, `TransactionType`). `bankName`/`accountNumber` (already nullable from the original design, just never had a documented meaning for being `NULL`) are now enforced conditionally: required for `BANK`, forbidden for `CASH` — enforced at **both** the API layer (`BadRequestException`) and the database layer (a new `CHECK` constraint, `Accounting_System_Design_LKAS.md` §5.8) — defense in depth, not just a Service-layer rule, since this is a real data-integrity condition, not a UX nicety.
**Naming decision, made explicitly rather than defaulted:** the table and endpoints stay named `bank_account`/`bank-accounts` despite now covering Cash too — same reasoning as keeping `TaxSummary`'s name after its meaning changed entirely earlier today. Phases 23 (create), 24 (statement import), 25 (reconcile), and 34 (list) are all already built and live-tested; renaming would ripple through four working phases for a cosmetic gain. Flagged in `Accounting_System_Design_LKAS.md` §5.8 as reversible later (`cash_and_bank_account` would be the more accurate name) if ever revisited.
**Relationship to `CASH_ACCOUNT_ROOT_ID` (yesterday's Cash Flow work) clarified, not changed:** these are two different, complementary mechanisms — `CASH_ACCOUNT_ROOT_ID` is a *reporting* classification (which GL accounts count as cash for the Cash Flow statement), `bank_account.account_type` is *operational* metadata (the record an operator creates to track a specific account). Every `bank_account.gl_account_id` should be a descendant of `CASH_ACCOUNT_ROOT_ID` for the Cash Flow report to include it correctly — an expectation, not a DB-enforced link, same "documented, not enforced" posture as `tax_code`'s output/input account uniqueness.
**Status: not yet implemented.** `03_Progress_Tracker.md` Phases 23 and 34 reopened (both keep their original 2026-07-18 verified-live history, not overwritten) with a new outstanding item each.
**Docs updated this round:** `Accounting_System_Design_LKAS.md` (§5.8 rewritten with the `CHECK` constraint + naming note, §11.5's `CASH_ACCOUNT_ROOT_ID` section gains the relationship note), `04_API_Reference_And_Scenarios.md` (`POST /bank-accounts` request/validation table rewritten, `GET /bank-accounts` note added, Scenario 7's worked example updated to a realistic `BANK`-type payload), `01_Full_Development_Plan.md` (Phase 23/34 rows), `02_Phase_By_Phase_Development_Plan.md` (Phase 23/34 rows + new spec note), this file (Phase 23/34 rows, this entry). **Frontend docs (`12-GENERAL-LEDGER-INTEGRATION.md`, `21-GENERAL-LEDGER-FRONTEND.md`) not yet updated** — the create form's Account Type selector and conditional field visibility still need to be specified there.
---
## 2026-07-22 (same day, follow-up #3) — All six report retrofits from the "five reference templates" entry implemented and verified live; Cash/Bank rebuilt as two separate tables, deviating from the discriminator spec above
User approved the full six-item plan from the entry above in one batch ("do the modifications" / one consolidated approval, not per-file), then redirected the Cash/Bank shape mid-implementation once the discriminator design was actually built out — both are recorded here together since the redirection happened before the discriminator version was ever verified live.
**1–6. All six report items implemented and verified live**, in `05_LKAS_Report_PDF_Templates.md` §9's recommended order:
- **Trial Balance flattened** — `TrialBalanceRowDto` drops `depth`/`indentedCode`, gains `accountCode`; `AccountRepository.GetTrialBalanceAsync` no longer needs the recursive tree at all (a flat postable-leaf listing doesn't need one). PDF/CSV both flat, portrait.
- **Profit & Loss restructured** — new `accounts.income_statement_section` column (migration `AddIncomeStatementSectionTaxDeductibleAndCashAccounts`); `AccountRepository.GetProfitAndLossLeafRowsAsync` returns raw leaf rows tagged with the inherited section; `ReportService.BuildProfitAndLossReportAsync` groups them into the new nested `ProfitAndLossReportDto` (Sales/Cost of Sales/Gross Profit/Other Income/four expense groups/Unclassified/Net Profit). **Real sign bug found and fixed in the process:** the pre-existing `GetProfitAndLossAsync` summed Expense accounts as `credit − debit` with no flip — negative for a normal debit-balance expense — so `totalExpenses` (and therefore `profitForPeriod`, reused by both `BalanceSheet`'s Current Year Earnings and `CashFlow`) was silently wrong before this retrofit. Fixed by flipping sign for every Expense-tagged section's total; verified live: `Sales(730,000) − CostOfSales(80,000) = GrossProfit(650,000)` exactly, and retagging accounts between "Unclassified" and named sections doesn't change `netProfitForPeriod` (confirmed identical before/after tagging four test accounts, `-1,875,000` both times) — proof the regrouping is a pure presentation change, not a second data bug layered on top of the fix.
- **Cash Flow simplified** — `direction` (`"Increase"`/`"Decrease"`) added to `CashFlowWorkingCapitalChangeDto`, computed from the raw balance-change sign in `GetCashFlowWorkingCapitalChangesAsync`'s SQL (independent of `changeAmount`'s cash-impact sign, per the design doc's note on why these differ). PDF/CSV no longer print `openingCashBalance`/`closingCashBalance` (still returned in `Json`/`Csv` data for the reconciliation check); non-cash adjustments and working-capital changes combined and re-bucketed by sign into "Additions to Cash"/"Subtractions From Cash" at render time.
- **Tax Report redesigned wholesale into Income Tax Computation** — new `accounts.is_tax_deductible` column (same migration); five new `system_config` keys (`CORPORATE_INCOME_TAX_RATE`/`STATUTORY_RELIEF_AMOUNT` seeded via `HasData`, `APIT_CREDIT_ACCOUNT_ID`/`WHT_CREDIT_ACCOUNT_ID`/`QUARTERLY_TAX_PAYMENT_ACCOUNT_ID` seeded empty); new `ITaxRepository.GetNonDeductibleExpensesAsync`/`GetAccountCreditActivityAsync` (the latter reused three times, once per credit account); `TaxCode`-per-row `GetTaxSummaryAsync` removed entirely (the 2026-07-21 VAT/WHT/NBT design lives on only as reference text in `Accounting_System_Design_LKAS.md` §11.7†, not as live code). New `IncomeTaxComputationDto`/`IncomeTaxComputationPdfDocument`/`IncomeTaxComputationCsvDocument`, the latter two using the fuller `Company:Address`/`Tin`/`Brn` header block. Verified live end-to-end with both default config values and full request-param overrides (`allowableDeductions`, `otherTaxableIncome`, `surchargeAmount`, `taxRateOverride` all confirmed flowing through the 11-step computation correctly); a negative `balanceTaxPayable` renders as "BALANCE TAX REFUNDABLE" rather than a negative "payable" figure, confirmed on the seeded loss-making test dataset.
- **`Company:Address`/`Tin`/`Brn`** — added to all three `appsettings.*.json`, read only by the Income Tax Computation report; `ReportPageHeader` gained a second `ComposeWithEntityDetails` overload for this one report's fuller header, rather than changing the shared header used by the other six.
- **`outputFormat=Csv`** — new `ReportOutputFormat.Csv = 3`; new `Services/CsvDocuments/` folder (`CsvBuilder` shared row-building/escaping helper + one writer per report, parallel structure to `Services/PdfDocuments/`), same `ApiResponse` base64 envelope as `Pdf`. Verified live for all seven report types — valid CSV bytes returned for each, spot-checked `ProfitAndLoss.csv`'s Section-column flattening against the JSON response.
**Cash/Bank rebuilt as two separate tables, deviating from this file's own 2026-07-22 follow-up #2 entry (above) before that design was ever shipped.** While implementing the `bank_account.account_type` discriminator exactly as specified there, the user reviewed it and redirected: a cash account is not a kind of bank account, it's a peer concept, and cramming both into one table with a discriminator (plus `bankName`/`accountNumber` conditionally meaningless depending on the discriminator) was judged confusing for future developers rather than clarifying. Corrected design, built and verified live instead:
- **`BankAccount`/`bank_account`** reverted to exactly its original pre-discriminator shape (`accountName`, `bankName`, `accountNumber`, `glAccountId`, `currencyCode`) — the discriminator migration was rolled back (`dotnet ef database update` to the prior migration + `migrations remove`) before it was ever applied beyond this session's own testing, so there's no stray discriminator column or check constraint left behind.
- **New `CashAccount`/`cash_account` table**, structurally parallel but independent: `accountName`, `cashAccountTypeId` (FK to a new small **growable** reference table, `cash_account_type` — seeded with Petty Cash/Till Cash/Safe Cash/Cash in Transit, and gaining a new row automatically whenever `POST /cash-accounts` is called with a `cashAccountTypeName` that doesn't match an existing one, so a frontend "Other, please specify" field becomes a permanent selectable option from then on — confirmed live: creating a "Vault Reserve" cash account added `cash_account_type_id = 5`, immediately visible in a follow-up `GET /cash-account-types` call), `accountNumber`, `glAccountId`, `currencyCode`.
- **`accountNumber` requirement changed for Cash, per explicit user direction:** unlike the discriminator design (which made it `NULL` for `CASH`), a cash account's `accountNumber` is required overall but optional on input — if the caller omits it, `BankService.CreateCashAccountAsync` auto-generates a sequential `CASH-{000001}`-style reference (probe-and-advance uniqueness check, same defensive pattern as the account/asset code generators, `CLAUDE.md` Rule 8.1); if supplied, it's used as-is. Verified live for both paths.
- **`GET /bank-accounts` is now a unified, filterable list** — new optional `accountType` query param (`Cash`/`Bank`/`Both`, default `Both`), returning a combined, flattened `CashAndBankAccountDto` list (`accountType` discriminator field on the *response* only, not stored) rather than two separate list calls. Verified live: default call returned both existing bank accounts and the two newly-created cash accounts together; `accountType=Cash`/`accountType=Bank` each correctly filtered; an invalid value returned 400.
- **Migration history correction:** the discriminator migration (`AddIncomeStatementSectionTaxDeductibleAndBankAccountType`) was fully rolled back and removed before this session ended — the live migration history only ever contains the final two-table design (`AddIncomeStatementSectionTaxDeductibleAndCashAccounts`), not an add-then-immediately-reverse pair.
**Docs still needing this same "mark as done" pass, not yet done as part of this entry:** `04_API_Reference_And_Scenarios.md` (Reporting module retrofit notes, plus a new Cash/Bank Accounts module section describing the two-table design — currently still describes the discriminator design from follow-up #2 above, which was never shipped), `05_LKAS_Report_PDF_Templates.md` (§2/§4/§5/§8 still describe pre-retrofit shapes in places, §10 CSV section still needs writing), `02_Phase_By_Phase_Development_Plan.md` (§1.9–§1.12 status flip, Phase 17's six retrofit notes, Phases 23/34 need rewriting around the two-table design instead of the discriminator), `Accounting_System_Design_LKAS.md` (§5.8 needs a full rewrite — the discriminator design documented there was never actually built).
---
## 2026-07-22 (same day, follow-up #4) — Documentation sync pass: all four outstanding docs from the entry above brought in line with the actual two-table Cash/Bank design and the six report retrofits
Closes out the "docs still needing this pass" note directly above. No code changed in this entry — documentation only, checked against the live codebase and the live-verification results from follow-up #3, not rewritten from memory:
- **`01_Full_Development_Plan.md`** — Phase 23's row split into 23 (`bank_account`, unchanged) / 23a (`cash_account`, new) / 23b (`cash_account_type` list, new); Phase 34's row corrected to describe the actual unified-list retrofit (`accountType` query filter, response-only discriminator) instead of the never-shipped per-row `accountType` column.
- **`02_Phase_By_Phase_Development_Plan.md`** — §1.9–§1.12 headers flipped from "not yet implemented" to "implemented and verified live"; Phase 17's six 2026-07-22 retrofit notes (`Csv`, `TrialBalance`, `ProfitAndLoss`, `CashFlow`, Income Tax Computation) all flipped to "implemented and verified live" with live-verification figures added; the dispatch/repository method list corrected to the actual method names used (`BuildProfitAndLossReportAsync` shared by three callers, `BuildTaxSummaryAsync` kept rather than renamed to `GetIncomeTaxComputationAsync` as originally planned — a cosmetic difference between the plan and the shipped code, now documented rather than left silently mismatched); the acceptance-test paragraph's "once implemented" conditionals replaced with actual verified figures.
- **`05_LKAS_Report_PDF_Templates.md`** — Section 0 (`Company:*`) flipped to implemented, expanded to cover the 2026-07-22 `Address`/`Tin`/`Brn` addition; intro/scope note updated to stop describing `TaxSummary` as a VAT/WHT/NBT report (superseded); Section 9's stale bits fixed (`TaxSummary`'s "empty array" note no longer applies now that it's a single object; Trial Balance's orientation corrected from Landscape to Portrait, matching Section 1's own already-corrected table); new **Section 10** written (didn't exist before this entry) — per-report CSV column layout and flattening rules, matching what `Services/CsvDocuments/` actually implements.
- **`Accounting_System_Design_LKAS.md`** — §5.8 fully rewritten: the discriminator design (`bank_account.account_type`, never shipped) replaced with the actual two-table design (`bank_account` unchanged, new `cash_account`/`cash_account_type`), including the corrected table/endpoint-naming rationale and the note that statement import/reconcile stay `bank_account`-only, not extended to `cash_account`. §11.5's cross-reference to `bank_account.account_type` corrected to reference both tables.
- **`04_API_Reference_And_Scenarios.md`** — checked against the live codebase and found already accurate (its Bank module and Reporting module retrofit notes were updated in an earlier pass of this same work session) — no further changes needed here.
**This closes the documentation debt from follow-up #3.** Every doc now describes what's actually running, not a superseded discriminator design or a "not yet implemented" spec that was, in fact, implemented and verified live earlier in this same 2026-07-22 session.
---
## 2026-07-22 (same day, follow-up #5) — Journal entry create+post merge; request-body FK ids switched to codes project-wide; two endpoints removed
User flagged two real problems while reviewing `Controllers/GeneralLedgerController.cs`: (1) `PUT /journal-entries/{journalNo}/lines` and `PUT /journal-entries/{journalNo}/post` will never actually be called — the client (and any core backend integrating with this service) always sends a complete, ready-to-post journal entry in one call, never a draft it comes back to finish later; (2) request bodies across the API accept raw numeric ids (`accountId`, `costCenterId`, `taxCodeId`, ...) where the referenced entity has its own unique code — those should be codes too, the same as path parameters already are (`CLAUDE.md` Rule 8).
**1. Journal entries — `CLAUDE.md` Rule 14 (new).** `PUT .../lines` and `PUT .../post` removed entirely; `POST /journal-entries` now validates, inserts, balance-checks, period-lock-checks, and posts, all in one Unit-of-Work transaction. `lines` is now required for a plain create and for a `PartialAdjustment` correction (previously both deferred lines to the now-removed `PUT .../lines` call) — `FullReversal` still auto-builds its lines by mirroring the original. There is no client-facing `DRAFT` state reachable via the API anymore. `IJournalEntryService.SetLinesAsync`/`PostAsync` removed from the interface; the now-fully-unused `IJournalEntryRepository.DeleteJournalEntryLinesByJournalIdAsync`, `GetJournalEntryByIdAsync`, and `GetJournalCorrectionByCorrectionJournalIdAsync` removed too, rather than left dead.
Every other Service that generates a journal entry internally updated to match:
- **`FixedAssetService`** (`Depreciate`/`Dispose` transactions) — previously called `_journalEntryService.CreateAsync(...)` then a separate `PostAsync(...)`; now calls `CreateAsync` once and uses its already-posted result directly. Also gained `ResolveAccountCodeAsync(long accountId)` — a small helper to turn the asset's own persisted numeric account-id fields (`AssetAccountId`, `AccumDeprAccountId`, etc. — unaffected, storage stays numeric) back into a code when building a `JournalEntryLineRequest`.
- **`RecurringTemplateService.GenerateAsync`** — was already only calling `CreateAsync` once (no separate `PostAsync`, so a generated recurring entry used to sit as `DRAFT` for later review) — now the same call auto-posts, so a generated entry comes back already `Posted`. Confirmed as the correct new behavior (no separate review step exists anymore for any journal entry, recurring or otherwise), not a regression.
**2. Request-body/query-string FK ids → codes — `CLAUDE.md` Rule 8.2 (new), reverses Rule 8's original "request bodies stay numeric" carve-out for leaf references.** Every field in the table below was a numeric FK id before this retrofit:
| DTO | Field(s) changed |
|---|---|
| `JournalEntryLineRequest` | `accountId`→`accountCode`, `costCenterId`→`costCenterCode` |
| `JournalEntryCorrectionRequest` | `originalJournalId`→`originalJournalNo` |
| `BudgetLineRequest` | `accountId`→`accountCode`, `costCenterId`→`costCenterCode` (`periodId` stays numeric — no unique code column) |
| `CreateTaxCodeRequest` | `taxTypeId`→`taxTypeCode`, `outputTaxAccountId`/`inputTaxAccountId`→`...AccountCode` |
| `CalculateTaxRequest` | `taxCodeId`→`taxCode` |
| `CreateFixedAssetRequest` | all 4 GL account fields →`...Code` |
| `CreateFixedAssetTransactionRequest` | `proceedsAccountId`→`proceedsAccountCode` (`periodId` stays numeric) |
| `RecurringTemplateLineRequest` | `accountId`→`accountCode` |
| `CreateBankAccountRequest`/`CreateCashAccountRequest` | `glAccountId`→`glAccountCode` |
| `ReconcileStatementLineRequest` | `matchedJournalId`→`matchedJournalNo` |
| `ListReportsRequest` (query string) | `accountId`→`accountCode` (`GeneralLedger` report) |
**Explicitly NOT changed, confirmed rather than silently swept in:** `parentAccountId`/`parentCostCenterId` (recursive-tree fields, Rule 8's original exception — still stands) and every FK referencing an entity with no unique code column (`periodId`, `fiscalYearId`, `budgetId`).
**Verified live, one request per changed endpoint:** plain journal entry create with `accountCode`s → returned `isPosted=true` in the same response; `FullReversal` correction by `originalJournalNo` → auto-posted, original flipped to `Reversed`; `PartialAdjustment` correction with inline `lines` (against a still-`Posted` original) → auto-posted; both removed `PUT` routes → `404`; budget line add with `accountCode`+`costCenterCode`; tax code create with `taxTypeCode`+`outputTaxAccountCode`, then `GET .../calculate?taxCode=...`; fixed asset create with the 4 `...Code` fields, then a `Depreciate` transaction (confirms the internal id→code resolution path); recurring template create with `accountCode` lines, then `generate` (confirms the entry comes back posted); bank account create with `glAccountCode`; reconcile with a non-existent `matchedJournalNo` → `404` (confirms the lookup switched from id to `journalNo`); `GeneralLedger` report with `accountCode` query param.
**Docs updated:** this file (Phases 11/12/13 rows rewritten, 15/16/19/20/21/22/23/23a/25/27 rows gain a retrofit note, this entry), `CLAUDE.md` (new Rule 8.2, new Rule 14). **Not yet updated as part of this entry, flagged as the next step:** `04_API_Reference_And_Scenarios.md` (Journal Entries module needs its Set Lines/Post sections removed and Create rewritten; Budget/Tax/Fixed Assets/Recurring Templates/Bank/Reports modules need their changed field names updated) and `02_Phase_By_Phase_Development_Plan.md` (the Phases 11–13 worked example needs rewriting around the merged single-endpoint design).
---
## 2026-07-30 — Cheque Management added (Phase 37, new module, beyond the original 36-phase plan)
User supplied a raw field spec for three tables — `ChequeBook`, `ChequePage` (issued cheques, from this company's own cheque books) and `ReceivedCheque` (cheques received from others, explicitly with **no** `ChequeBook` link) — and asked for cheque management to be added to the service. Three genuinely open design questions were resolved via direct clarification before any code was written (per `CLAUDE.md` Rule 3):
1. **GL integration: purely operational tracking, no automatic journal postings.** Issuing/clearing/bouncing/cancelling a cheque never calls or triggers `POST /journal-entries` itself — it only stores caller-supplied references (`voucherId`, `referenceNo` on `ChequePage`; `referenceType`/`referenceId` on `ReceivedCheque`). The caller posts the actual journal entry separately and links back via those fields, the same relationship `PUT /bank-statement-lines/{id}/reconcile` already has with `matchedJournalNo` — reconciliation doesn't create a journal entry either, it matches an existing one.
2. **Leaf generation: auto-generated.** `POST /cheque-books` generates all `totalLeaves` `ChequePage` rows (status `Unused`) immediately, numbered sequentially from `startChequeNo` to `endChequeNo` (zero-padded to `startChequeNo`'s width) — mirrors the existing fiscal-year period auto-generation (Phase 7).
3. **Company/Branch scope, asymmetric per user direction:** `ChequeBook` gets `branchId` (required) and no `companyId`; `ReceivedCheque` gets `companyId` (required) and `branchId` (nullable). Neither is a FK — no `Company`/`Branch` table exists anywhere in this system (confirmed by grep before asking), so both are plain, unvalidated `long`/`long?` columns, same treatment as every other loose external reference in this module.
**Deviations from the client's raw field spec, each forced by something that doesn't exist in this codebase, not a stylistic choice:**
- **`PayeeId` (`ChequePage`), `ReceivedFromId`/`ReferenceId` (`ReceivedCheque`), `VoucherId` (`ChequePage`)** — no `Supplier`/`Customer`/`Employee`/`Voucher` master tables exist in this GL service (this service receives already-decided data from a core backend/other modules, per this project's founding premise). All four stay as loose `long?` references with no FK constraint, same treatment as `AuditLog.EntityId`.
- **`BankId`/`BankBranch` (`ReceivedCheque`)** — renamed `DrawerBankName`/`DrawerBankBranch`, plain free-text `string?`, no FK — no bank-master table exists; follows the exact precedent of `BankAccount.BankName` already being free text rather than a FK.
- **`ChequeBookNo`/`StartChequeNo`/`EndChequeNo`** stay caller-supplied (unlike `AccountCode`/`AssetCode`'s system-generated scheme, Rule 8.1) — these are printed on the physical cheque book by the actual bank, not invented by this service. `ChequeBookNo` and `ChequeNo` are still usable as Rule-8-style unique lookup keys once stored (`GET /cheque-books/{chequeBookNo}`, `GET /cheque-pages/{chequeNo}`).
- **`CreatedBy`/`ModifiedBy`/`PrintedBy`** — spec said `bigint` (a user id); this system has no user/identity table anywhere, so these are free-text `string?`, matching the established `JournalEntry.PreparedBy` pattern.
- **`CurrencyId` (`ChequePage`)** → `CurrencyCode` (`string?`, FK to `currency.currency_code`, defaults `LKR`) — same code-based FK convention used everywhere else in this API (`CLAUDE.md` Rule 8.2).
- **`BankAccountId` (`ChequeBook`) / `DepositBankAccountId` (`ReceivedCheque`)** stay numeric, not converted to a code — `BankAccount` has no unique code column (`AccountNumber` is optional), so this falls under Rule 8.2's explicit "no unique code column" carve-out, same as `periodId`/`fiscalYearId`/`budgetId` elsewhere.
- **Amounts (`ChequePage.Amount`, `ReceivedCheque.Amount`)** — no `HasPrecision` call, unbounded `numeric` at the DB layer, per `CLAUDE.md` Rule 13.
**Built:** `Enums/ChequeBookStatus.cs`, `ChequeIssueStatus.cs`, `ChequePayeeType.cs`, `ReceivedChequeFromType.cs`, `ReceivedChequeStatus.cs`, `ChequePageAction.cs`, `ReceivedChequeAction.cs` (the last two are `PUT .../status` action discriminators, not stored columns — same role as `PeriodLockAction`). `Models/Cheque.cs` (`ChequeBook`, `ChequePage`, `ReceivedCheque` — one file, one cluster, per Rule 2's existing precedent). `AppDBContext.cs` (3 `DbSet`s + 3 `Configure*` methods), migration `AddChequeManagement`, applied to the local database. `Interfaces/Repositories/IChequeRepository.cs` + `Repositories/ChequeRepository.cs` (one repository for the whole cluster, per Rule 2). `Interfaces/Services/IChequeService.cs` + `Services/ChequeService.cs`. 9 new DTOs under `DTOs/Cheques/`. 11 new controller actions on `GeneralLedgerController` (Phase 37.1–37.11 above). DI registrations added to `Program.cs`.
**Enum-in-JSON-body lesson relearned mid-implementation:** `IssueChequePageRequest.PayeeType` and `CreateReceivedChequeRequest.ReceivedFromType` were initially declared as the C# enum type directly — this project has no global `JsonStringEnumConverter` registered (confirmed in `Program.cs`), so a JSON body enum field must be bound as `string` and parsed manually in the Service via `Enum.TryParse<T>(raw, ignoreCase: true, out var parsed)`, throwing `BadRequestException` with the valid-values list on failure — exactly the existing `RecurrencePattern`/`PeriodLockAction` pattern. Query-string-bound enums (the `List*Request` DTOs' `status`/`issueStatus`/`payeeType`/`receivedFromType` filters) don't have this problem — ASP.NET Core's route/query binder parses enum names natively — so those stayed as the real enum type.
**Verified live, one request per endpoint:** created a 10-leaf cheque book (`branchId`, `bankAccountId`, `chequeBookNo`, numeric `startChequeNo`/`endChequeNo` range) → 10 `Unused` pages auto-generated in the same response; `GET` single book (no expand) and list by `bankAccountId`; issued two pages (`Supplier`/`Customer` payee types); `Clear` on an `Issued` page (requires `clearedDate`) → `Cleared`; `Bounce` on an `Issued` page → `Bounced`; `Cancel` on an `Unused` page (requires `cancelReason`) → `Cancelled`; `Void` on an `Unused` page → `Void`; attempted `Clear` on a still-`Unused` page → `400` with the current-status message; `GET` single page and list filtered by `issueStatus=Cleared`; created a received cheque (`Customer`, `HNB`/`Colombo 03` drawer bank as free text, `referenceType`/`referenceId`); `Deposit` (requires `depositBankAccountId`+`depositDate`, validated against a real bank account) → `Deposited`; `Clear` → `Cleared`; `GET` single and list filtered by `companyId`; duplicate `chequeBookNo` → `400`; mismatched `totalLeaves` vs. the numeric range → `400`; nonexistent `bankAccountId` → `404`. Dev server stopped on port 5200 after testing, per this project's standing convention.
**Docs updated:** this file (new Phase 37 section, Overall Progress Summary counts, this entry). **Not yet updated as part of this entry, flagged as the next step:** `04_API_Reference_And_Scenarios.md` (needs a new Cheque Management module section) and `02_Phase_By_Phase_Development_Plan.md` (needs a new Phase 37 worked-example section, matching the style of Phases 30–36's appended-endpoint sections).
---
## 2026-07-31 — `BalanceSheet` restructured into a classified statement (Non-Current/Current Assets and Liabilities)
User supplied a reference template (a Sri Lankan NGO's "Statement of Financial Position") showing a **classified** balance sheet — Non-Current Assets and Current Assets each subtotaled separately, likewise Non-Current Liabilities and Current Liabilities, with Equity as its own block — and asked for the `BalanceSheet` `reportType`'s response/PDF/CSV to match. The flat recursive-rollup array this report used since Phase 5 had no such split; `05_LKAS_Report_PDF_Templates.md` Section 3 had explicitly documented this as a known limitation ("classification is achieved entirely through how the company structures its own Chart of Accounts... this template renders that hierarchy as-is; it does not independently classify anything").
Two open design questions resolved via direct clarification before any code was written (per `CLAUDE.md` Rule 3):
1. **Untagged Asset/Liability leaf accounts land in an `unclassifiedAssets`/`unclassifiedLiabilities` bucket**, not a `400` — same rule `ProfitAndLoss`'s `unclassified` section already established, so an incomplete tagging pass is visible in the report rather than silently excluded.
2. **Equity stays one flat section**, no second sub-classification tag — the reference template's visual two-group split within Equity (accumulated fund vs. joining/membership fees) is an NGO/fund-accounting convention, not something this schema's single Equity account type needs a second tag dimension to express.
**New mechanism: `accounts.balance_sheet_classification`, same recursive-tag-and-inherit pattern as `cash_flow_category`/`income_statement_section`.** New nullable `VARCHAR(25)` column (migration `AddBalanceSheetClassificationToAccount`), values `NON_CURRENT_ASSET`/`CURRENT_ASSET`/`NON_CURRENT_LIABILITY`/`CURRENT_LIABILITY`, settable only via `PUT /accounts/{accountCode}` (`CLAUDE.md` Rule 8.1-style: never accepted on create). Equity accounts need no tag — `account_type_id` alone already puts them in one `equity` section.
**Response reshaped from a flat `{depth, lineItem, accountType, balance}` array into:** `{ asOfDate, nonCurrentAssets: {lines[], total}, currentAssets: {lines[], total}, unclassifiedAssets: {lines[], total}, totalAssets, equity: {lines[], total}, nonCurrentLiabilities: {lines[], total}, currentLiabilities: {lines[], total}, unclassifiedLiabilities: {lines[], total}, totalEquityAndLiabilities }`. The existing "Current Year Earnings" synthetic row (cumulative-since-inception net profit, Phase 5's balancing fix) is now folded into `equity.lines[]` rather than a separate top-level field.
**Built:** `Models/Account.cs` (+`BalanceSheetClassification`), `AppDBContext.cs` (`ConfigureAccount`), `DTOs/Accounts/UpdateAccountRequest.cs` + `AccountService.UpdateAccountAsync` (parse/apply the new field, same as `cashFlowCategory`/`incomeStatementSection` — no enum-level validation, consistent with those two, since the SQL just compares literal strings). `DTOs/Reports/ReportRowDtos.cs` — `BalanceSheetRowDto` removed, replaced by `BalanceSheetLineDto`/`BalanceSheetSectionDto`/`BalanceSheetReportDto` (public) + `BalanceSheetLeafRowDto` (raw projection). `AccountRepository.GetBalanceSheetAsync` (recursive rollup) removed, replaced by `GetBalanceSheetLeafRowsAsync` (leaf/postable rows only, same shape as `GetProfitAndLossLeafRowsAsync`) — interface updated to match. `ReportService.BuildBalanceSheetAsync` rewritten to group leaf rows by `AccountTypeId`+`BalanceSheetClassification` into the new sections and compute both grand totals. `Services/PdfDocuments/BalanceSheetPdfDocument.cs` and `Services/CsvDocuments/BalanceSheetCsvDocument.cs` rewritten around the new section shape (empty sections skipped entirely in the PDF, same rule as `ProfitAndLoss`).
**Verified live** against the already-seeded comprehensive test dataset: tagged `1400` (Fixed Assets header) `NON_CURRENT_ASSET`; `1000`/`1100`/`1200`/`1300` (Cash and Bank, Accounts Receivable, Inventory, Prepaid Expenses) `CURRENT_ASSET`; `2300` (Bank Loan) `NON_CURRENT_LIABILITY`; `2000`/`2100`/`2200` (Accounts Payable, VAT Payable, Accrued Expenses) `CURRENT_LIABILITY`. `GET /reports?reportType=BalanceSheet&outputFormat=Json&asOfDate=2026-12-31` returned `totalAssets = 5,363,714` and `totalEquityAndLiabilities = 5,363,714` — exact match. `outputFormat=Pdf` produced a valid 55KB PDF; `outputFormat=Csv` produced a valid, correctly-sectioned CSV. `currentLiabilities` came back empty for this dataset (all three tagged current-liability accounts were fully settled by year end) — confirmed as correct given the seeded transactions, not a bug; it renders as an omitted section in the PDF per the "skip empty sections" rule.
**Docs updated:** this file (this entry), `04_API_Reference_And_Scenarios.md` (`BalanceSheet` reportType row rewritten, new retrofit note, `PUT /accounts/{accountCode}` field list corrected — also picked up the already-missing `incomeStatementSection`/`isTaxDeductible` fields while touching that line — old bugfix notes marked superseded rather than rewritten), `Accounting_System_Design_LKAS.md` (§11.3 gained the new column + leaf-row query design), `05_LKAS_Report_PDF_Templates.md` (Section 3 response shape, template, implementation notes, and the CSV column table all updated).
---
## 2026-07-31 (same day, follow-up) — `GeneralLedger` PDF moved from landscape to portrait A4
User reported receiving a landscape-oriented `GeneralLedger` PDF and wanted portrait A4, matching every other report. `Services/PdfDocuments/GeneralLedgerPdfDocument.cs`'s `page.Size(PageSizes.A4.Landscape())` changed to plain `page.Size(PageSizes.A4)` — no other layout change (same six columns, same group-header-row account boundaries). Verified live: rendered PDF's `/MediaBox` is `[0 0 595 842]` (595×842pt — A4 portrait). `TrialBalance` (the other report `05_LKAS_Report_PDF_Templates.md` had listed as landscape) had already moved to portrait back on 2026-07-22 for an unrelated reason (its own flattening retrofit); `GeneralLedger` was the only report still landscape, so after this change every report PDF is portrait A4.
**Docs updated:** this file (this entry), `04_API_Reference_And_Scenarios.md` (new retrofit note), `05_LKAS_Report_PDF_Templates.md` (orientation table's `General Ledger` row updated).
---
## 2026-07-31 (same day, follow-up #2) — `glAccountCode` removed from Bank/Cash account creation; backing GL account now always auto-created
User pointed out that requiring a caller to select an existing GL account when creating a bank or cash account is unnecessary friction — a bank account is definitionally a bank account, and a cash account of a given type is definitionally under that type, so there's no real choice for the caller to make. Two design questions resolved via direct clarification before any code was written (per `CLAUDE.md` Rule 3):
1. **The "Bank"/"Cash" GL header roots (and each cash type's own node under "Cash") are auto-created on first use**, not gated behind a `system_config` setup step — found-or-created by exact name, same convention `cash_account_type` already uses for its own "grows at runtime" rows. **User specified the exact names: `Bank` and `Cash`** (not "Bank Accounts"/"Cash Accounts").
2. **`glAccountCode` removed entirely** from both `CreateBankAccountRequest`/`CreateCashAccountRequest` — no optional override kept.
**New auto-created GL hierarchy:** `Bank` (root Asset, non-postable) → one postable leaf per `BankAccount`, never reused. `Cash` (root Asset, non-postable) → one non-postable node per `cash_account_type` (created once, then reused for every cash account of that type) → one postable leaf per `CashAccount`, never reused. Full design: `Accounting_System_Design_LKAS.md` §5.8's new retrofit note.
**Built:** `Services/AccountCodeGenerator.cs` — a new shared static helper (same shape as `RecursiveTreeQueries`/`DepthGuard`, `CLAUDE.md` Rule 11) extracted from `AccountService`'s previously-private `GenerateAccountCodeAsync`, so both `AccountService.CreateAccountAsync` and `BankService`'s new auto-creation logic use the exact same Rule 8.1 code-gen scheme without duplicating it (`AccountService.cs` itself has no behavior change, pure extraction). `Interfaces/Repositories/IAccountRepository.cs` + `AccountRepository.cs` gained `GetByNameAndParentAsync` (exact-name lookup used to find-or-create `Bank`/`Cash`). `Models/Bank.cs`'s `CashAccountType` gained a nullable `GlAccountId` (+ nav `GlAccount`), migration `AddGlAccountIdToCashAccountType` (nullable column + FK + index, existing 4 seeded types left `NULL`). `DTOs/Bank/CreateBankAccountRequest.cs`/`CreateCashAccountRequest.cs` — `GlAccountCode` field removed. `Services/BankService.cs` — `CreateBankAccountAsync`/`CreateCashAccountAsync` rewritten around two new private helpers, `GetOrCreateSystemRootAsync` (find-or-create a root by exact name) and `CreateSystemChildAccountAsync` (always creates a new child, used for both the cash-type node and every leaf) — both call `AccountCodeGenerator.GenerateAsync` for their code and write their own `Ledger`/`Create` audit row per new `Account`, in addition to the existing `Administrative`/`Create` audit row for the `BankAccount`/`CashAccount` itself. No `DepthGuard` check — the structure is fixed at 2-3 levels, well under `COA_MAX_DEPTH`'s default of 6.
**Verified live** against the already-seeded dataset (which already occupied Asset root slots `1000`/`1100`/`1200`/`1300`/`1400`): `POST /bank-accounts` with no `glAccountCode` auto-created `1500 Bank` → `1500-01 HNB Corporate Account`, returned nested as `data.glAccount`/`data.glAccount.parentAccount`. Two `POST /cash-accounts` calls with `cashAccountTypeName: "Petty Cash"` auto-created `1600 Cash` → `1600-01 Petty Cash` (created once) → `1600-01-01`/`1600-01-02` (one leaf each) — confirmed via `GET /cash-account-types` that `Petty Cash`'s `glAccountId` was `null` before the first call and populated (unchanged by the second) afterward.
**Docs updated:** this file (this entry), `04_API_Reference_And_Scenarios.md` (`POST /bank-accounts`/`POST /cash-accounts` sections rewritten, worked-scenario example fixed), `Accounting_System_Design_LKAS.md` (§5.8 gained the new retrofit note).
**2026-10-06 — Database provider migration: PostgreSQL → MySQL (Pomelo), raw SQL → EF Core LINQ.** User-directed, not a numbered phase. Status: `◐ In progress`. Code converted and building; `InitialMySql` applied to a fresh local database (`ERPAcc_Dev`, XAMPP **MariaDB 10.4.32**, not MySQL 8) and every endpoint exercised (2026-10-06): 245 of 249 scripted checks passed; the 4 non-passes were test-script mistakes (expected 200 vs actual 201, and a single-element JSON array that PowerShell unwrapped), re-checked and confirmed correct. Trial balance, P&L, balance sheet, general ledger running balances, budget vs actual, tax non-deductibles and the cash-flow category/working-capital queries were cross-checked row-for-row against the original SQL run directly on the database and matched. **Not yet done:** testing against a real MySQL 8 server, and copying the existing PostgreSQL data across. One pre-existing gap seen, unrelated to the migration: `POST /accounts` accepts an empty `accountName` (no validation).
- **Packages:** `Npgsql.EntityFrameworkCore.PostgreSQL` removed; `Pomelo.EntityFrameworkCore.MySql` 9.0.0 added. Pomelo's latest release targets EF Core 9, so `Microsoft.EntityFrameworkCore`, `.Design`, `.Tools` and the HealthChecks EF package were moved from 10.0.9 to 9.0.9 (the app still targets .NET 10). `Program.cs` uses `UseMySql` with a fixed `MySqlServerVersion(8.0.36)` so startup needs no live DB for version detection.
- **Schema:** `timestamptz`/`now()` → `datetime(6)`/`CURRENT_TIMESTAMP(6)`. Every decimal column is now `DECIMAL(38,10)` (MySQL has no unbounded numeric — `CLAUDE.md` Rule 13 updated). The partial unique index on `currency.is_base_currency` (unsupported in MySQL) became a functional unique index created by raw SQL inside the migration. All 14 PostgreSQL migrations and the model snapshot were deleted and replaced by a single `InitialMySql` migration (old history remains in git).
- **Raw SQL removed entirely:** all 16 `SqlQuery`/`SqlQueryRaw` methods (`AccountRepository` reports, `JournalEntryRepository` general ledger, `BudgetRepository`, `TaxRepository`, `FixedAssetRepository`, and `RecursiveTreeQueries`) were rewritten as EF Core LINQ. Sums, joins and filters run in the database; recursive tree walks, tag inheritance and GL running balances are computed in C#. `Helpers/RecursiveTreeQueries.cs` is now a set of pure in-memory functions (`CLAUDE.md` Rule 11 updated). `ROUND(...,10)` guards were dropped — C# `decimal` arithmetic has no float noise. No public request/response shape changed.
- **Config:** the three-file layout is kept, with a clearer split. `appsettings.json` holds only settings common to every environment (Kestrel URL, logging, allowed hosts, JWT issuer/audience/token lifetime). `appsettings.Development.json` holds development-only values (local MariaDB/MySQL connection string `ERPAcc_Dev`, JWT signing key, API key, demo `Company` details). `appsettings.Production.json` holds production-only values and is unchanged (blank `ConnectionStrings`/`Jwt:SigningKey`/`ApiKey:Value`, placeholder `Company`), to be filled at deploy time — no production credentials were written anywhere. The old Postgres connection string and the `ApiKey:Value` that were in `appsettings.json` were removed from it.
- **Known behavior differences to watch when testing:** MySQL's default collation is case-insensitive (unique codes now compare case-insensitively); report row ordering by code is now an ordinal (case-sensitive) sort in C# instead of the database collation.
**2026-10-20 - Trial Balance fix (found after the MySQL migration, but a pre-existing defect, not caused by it).** `AccountRepository.GetTrialBalanceAsync` returned each account's GROSS debit and credit totals side by side, so an account with movement on both sides, or an entry plus its reversal, showed equal or duplicated figures in both columns. It now returns the NET balance in one column only (debit balance in Debit, credit balance in Credit, the other 0) and omits accounts whose net is zero, as `05_LKAS_Report_PDF_Templates.md` Section 2 specifies. PDF and CSV already blanked zero cells, so no change there. Verified on the local test database (JSON, PDF, CSV): no row has both columns filled, the net-zero Interest Income account no longer appears, total debit equals total credit.