Files
Distribution_GL/Docs/03_Progress_Tracker.md
T

172 KiB
Raw Blame History

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 accountCodes → 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 DbSets + 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.