Distribution_GL Initialization
This commit is contained in:
@@ -0,0 +1,632 @@
|
||||
# General Ledger Service — Full Development Plan
|
||||
|
||||
**Project:** General-Ledgar-Service (ASP.NET Core Web API, .NET 10)
|
||||
**Part of:** ERPCore multi-service ERP suite
|
||||
**Reference documents:**
|
||||
- `PROJECT_SPECIFICATION.md` — current scaffolded state of this repo (source of truth for what already exists and must not be redesigned)
|
||||
- `Accounting_System_Design_LKAS.md` — the accounting domain design (LKAS-compliant, recursive Chart of Accounts, core double-entry ledger)
|
||||
- `Accounting_System_ER_Diagram.drawio` / `er_diagram.mmd` — the current (trimmed, core-ledger-only) entity relationship diagram, 25 tables including `audit_log` (Section 4.1/4.3). **Sync status as of 2026-07-19:** `er_diagram.mmd` is up to date — `audit_log` is present, and every column later 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`) now shows its real stored type, and `fixed_asset` carries its two later-added columns (`gain_loss_account_id`, `depreciation_rate_percent`). **`Accounting_System_ER_Diagram.drawio` was left as-is** — it already had `audit_log`, but still shows the pre-conversion `varchar` types and is missing `fixed_asset`'s two added columns; it's a single-line-per-entity HTML/XML blob (~190 lines, each one huge) that isn't safe to patch precisely without visually re-verifying every cell, so it's flagged here rather than edited blind — regenerate it from the now-current `.mmd` when convenient.
|
||||
- `02_Phase_By_Phase_Development_Plan.md` — the execution plan this document feeds into
|
||||
- `CLAUDE.md` — working rules for Claude Code during implementation
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose of This Document
|
||||
|
||||
This is the **full-scope plan** — everything the General Ledger Service needs to become, end to end. It exists to give a complete picture of the domain, the architecture, the API surface, and the conventions, so that the phase-by-phase execution plan (Document 2) can slice this scope into small, independently testable, one-API-call-per-phase units without losing sight of the whole.
|
||||
|
||||
This document does not get executed phase-by-phase itself — Document 2 does. This document is the map; Document 2 is the route.
|
||||
|
||||
---
|
||||
|
||||
## 2. Current State (from `PROJECT_SPECIFICATION.md`)
|
||||
|
||||
The repository is a **bootstrap/scaffold only**. What already exists and is considered **final / do-not-redesign**:
|
||||
|
||||
| Component | File(s) | Status |
|
||||
|---|---|---|
|
||||
| Composition root | `Program.cs` | Done — controllers, Swagger, EF Core + Pomelo (MySQL), health checks, conditional JWT bearer auth, authorization, HTTPS redirection |
|
||||
| Unit of Work | `Infra/UoW/IUnitOfWork.cs`, `EFUnitOfWork.cs` | Done — `BeginAsync`, `CommitAsync`, `RollbackAsync`, `SaveChangesAsync` |
|
||||
| Custom exceptions | `Exceptions/CustomExceptions.cs` | Done — `NotFoundException`, `BadRequestException`, `UnauthorizedException`, `ForbiddenException` |
|
||||
| Auth response envelope | `DTOs/Common/ApiResponse.cs` | Done — `StatusCode`, `Success`, `Message`, `Data` |
|
||||
| Helpers | `Helpers/DataExtractor.cs`, `DeserializePayload.cs`, `EncryptionHelper.cs` | Done |
|
||||
| DbContext | `AppDBContext.cs` | Empty shell — no `DbSet<T>` yet |
|
||||
| Controllers | `Controllers/GeneralLedgerController.cs` | Placeholder — only a `GET /status` liveness probe |
|
||||
| `Interfaces/Repositories/`, `Interfaces/Services/` | — | **To be removed** (see Section 3.2 and `CLAUDE.md` rule 4) |
|
||||
| `Models/`, `Repositories/`, `Services/` | — | Empty, to be populated phase by phase |
|
||||
| `appsettings.json` (all environments) | — | **New in this plan:** gains an `ApiKey:Value` entry (same blank-in-Production, filled-via-secret-store pattern already used for `Jwt:SigningKey`) to support `ApiKeyMiddleware` (Section 3.3). **New 2026-07-21:** also gains a `Company:Name` entry — the reporting entity's name required on every LKAS-presented statement (LKAS 1.51), read by the shared PDF header component (`05_LKAS_Report_PDF_Templates.md` Section 1). This is a deployment-time constant, not a runtime-editable value, so it belongs in `appsettings` rather than `system_config` — every environment's file gets a real value (not blank-in-Production like the two secrets above; a company name isn't a secret, but it's still environment-specific if this service is ever deployed for more than one client). **Expanded 2026-07-22:** `Company:Address`, `Company:Tin`, `Company:Brn` added alongside `Company:Name` — needed only by the Income Tax Computation report's header (`05_LKAS_Report_PDF_Templates.md` §0/§8), a fuller identity block than the other six reports use |
|
||||
| `Middleware/` | — | **New folder, new in this plan:** houses `ApiKeyMiddleware` (Section 3.3), built in Phase 1 |
|
||||
| Controller versioning | `Controllers/GeneralLedgerController.cs` and every controller going forward | **New in this plan:** routes move to `api/v{version}/[controller]` via `Asp.Versioning.Mvc` (Section 3.4), starting at `v1.0` |
|
||||
|
||||
**Nothing in the "Done" row above is to be modified as part of this plan** unless a phase explicitly identifies a genuine defect in one of those files (in which case, per `CLAUDE.md` rule 3, approval is asked first).
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture & Layering Convention
|
||||
|
||||
```
|
||||
HTTP Request
|
||||
│
|
||||
▼
|
||||
Controller — thin; parses request, calls one Service method, returns the ApiResponse the Service
|
||||
│ already built via StatusCode(response.StatusCode, response). Single controller for
|
||||
│ the whole service — see Section 3.2a
|
||||
▼
|
||||
Service — ALL business logic lives here; owns the Unit of Work; calls one or more Repositories;
|
||||
│ throws CustomExceptions for domain-rule violations; uses Helpers where payloads are
|
||||
│ loosely typed; builds and returns the success-path ApiResponse itself
|
||||
▼
|
||||
Repository — thin data-access only; no business rules; talks to AppDBContext; no UoW calls itself
|
||||
│
|
||||
▼
|
||||
AppDBContext (EF Core / MySQL)
|
||||
```
|
||||
|
||||
### 3.1 Why Unit of Work lives in the Service layer, not the Repository layer
|
||||
|
||||
A single service method often needs to touch **more than one repository** in one atomic operation — for example, posting a journal entry touches `JournalEntryRepository` (update status) and `JournalEntryLineRepository` (validate balance) in one transaction, or generating a depreciation entry touches both `FixedAssetRepository` and a repository that creates the `journal_entry`/`journal_entry_line` rows. If each repository managed its own transaction, a multi-repository operation could never be atomic. The Service layer is the only layer that knows the full scope of "what must succeed or fail together," so it is the only layer that calls `BeginAsync` / `CommitAsync` / `RollbackAsync`.
|
||||
|
||||
Repositories only ever call `SaveChangesAsync` indirectly through the Service's Unit of Work — they do not open their own transactions or commit independently.
|
||||
|
||||
### 3.2 No dedicated `Interfaces/` folder
|
||||
|
||||
Per `CLAUDE.md` rule 4, the `Interfaces/Repositories/` and `Interfaces/Services/` folders are removed. Instead, each Repository and each Service **declares its own interface in the same file**, directly above the implementing class:
|
||||
|
||||
```csharp
|
||||
public interface IAccountRepository
|
||||
{
|
||||
Task<Account?> GetByIdAsync(long accountId);
|
||||
Task<Account> AddAsync(Account account);
|
||||
}
|
||||
|
||||
public class AccountRepository : IAccountRepository
|
||||
{
|
||||
private readonly AppDBContext _context;
|
||||
public AccountRepository(AppDBContext context) => _context = context;
|
||||
|
||||
public async Task<Account?> GetByIdAsync(long accountId) =>
|
||||
await _context.Accounts.FindAsync(accountId);
|
||||
|
||||
public async Task<Account> AddAsync(Account account)
|
||||
{
|
||||
await _context.Accounts.AddAsync(account);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This keeps every layer's contract next to its only implementation — there is exactly one implementation per interface in this codebase, so a separate interface file adds indirection without adding value (this is also why rule 2, "keep it simple," and rule 4 go together).
|
||||
|
||||
### 3.2a Single controller; `ApiResponse` built in the Service layer; payload parsing lives in the Service too
|
||||
|
||||
Every endpoint across every phase/module is an action on the one `Controllers/GeneralLedgerController.cs` — not a separate controller per module. The controller-level route carries only the version prefix (`[Route("api/v{version:apiVersion}")]`, no `[controller]` token), so each action's own route template is its full path relative to `api/v1/` with no prefix repetition, e.g. `[HttpPost("accounts")]` resolves to `api/v1/accounts`.
|
||||
|
||||
The Controller action never constructs `new ApiResponse { ... }` — the Service method's success path returns an already-built `ApiResponse`, and the Controller just relays it via `StatusCode(response.StatusCode, response)`. Failure paths are untouched by this: a Service still throws a `CustomException`, and `ExceptionHandlingMiddleware` builds the `ApiResponse` for those.
|
||||
|
||||
The Controller also does **no payload parsing**. For any endpoint accepting a loosely-typed JSON body, the action's only parameter is `[FromBody] Dictionary<string, JsonElement> payload`, passed straight through to the Service method unexamined; `DataExtractor` calls happen inside the Service, which builds whatever internal request shape it needs before running its business logic. The Controller's job everywhere is exactly: bind the route/body, call one Service method, relay its `ApiResponse`.
|
||||
|
||||
Full rationale and worked examples: `CLAUDE.md` Rule 7 (controller/response) and Rule 1's `Helpers` row (payload parsing in the Service).
|
||||
|
||||
### 3.3 API Key Middleware — service-to-service authentication
|
||||
|
||||
Every request into this service is expected to come from the shared ERPCore backend, not directly from an end-user client. To guarantee that, a **custom middleware** validates a shared secret on every incoming request, before it reaches any controller:
|
||||
|
||||
```
|
||||
HTTP Request
|
||||
│
|
||||
▼
|
||||
UseHttpsRedirection
|
||||
│
|
||||
▼
|
||||
ApiKeyMiddleware ← NEW: rejects the request here if the key is missing/wrong
|
||||
│
|
||||
▼
|
||||
UseAuthentication (JWT bearer, existing)
|
||||
│
|
||||
▼
|
||||
UseAuthorization (existing)
|
||||
│
|
||||
▼
|
||||
MapControllers
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- The core backend sends a header on every call — e.g. `X-Api-Key: <shared-secret>`.
|
||||
- `ApiKeyMiddleware` reads the expected value from configuration (`ApiKey:Value` in `appsettings.json`, following the exact same pattern already used for `Jwt:SigningKey` — blank in `appsettings.Production.json`, filled via environment variable/secret store at deploy time).
|
||||
- If the header is missing or doesn't match, the middleware **short-circuits the pipeline** and writes a `401` response directly — it runs before MVC, so it builds the same `ApiResponse` JSON shape by hand (`Success = false`, `Message = "Invalid or missing API key"`) rather than throwing a `CustomException` (those are only caught inside the MVC/Service pipeline, which this request never reaches).
|
||||
- Two paths are excluded from the check: `/health` (infrastructure monitoring shouldn't need a secret) and `/swagger` (dev-only UI).
|
||||
- This check applies uniformly, **once, in the middleware pipeline** — individual controllers and endpoints do not each implement their own check. "Checked in every API call" is satisfied by the middleware sitting in front of all of them, not by per-endpoint code.
|
||||
|
||||
This is a new piece of shared foundation, built once in Phase 1, and — like the four components in Section 3.5 below — **not to be modified per-phase** once it exists; changes to it go through the same ask-first rule as everything else (`CLAUDE.md` Rule 3).
|
||||
|
||||
### 3.4 API Versioning
|
||||
|
||||
All controllers are versioned using `Asp.Versioning.Mvc` (package reference below). Routes take the shape `api/v{version}/[controller]`, e.g. `api/v1/accounts`. Every controller in this plan starts at `v1.0`; a `v2` only gets introduced later if a breaking change to an existing endpoint is ever needed — new, non-breaking endpoints are simply added to the existing controller under `v1`.
|
||||
|
||||
```csharp
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[ApiController]
|
||||
public class AccountsController : ControllerBase { ... }
|
||||
```
|
||||
|
||||
`Program.cs` registers versioning services (`AddApiVersioning`) alongside the existing MVC/Swagger/EF Core/health-check/JWT registration described in Section 2.
|
||||
|
||||
### 3.5 Where `CustomException`, `Helpers`, `UnitOfWork`, and `ApiResponse` are used
|
||||
|
||||
| Component | Layer | When |
|
||||
|---|---|---|
|
||||
| `NotFoundException` | Service | A requested entity (account, journal entry, fiscal year, etc.) doesn't exist |
|
||||
| `BadRequestException` | Service | A domain rule is violated — unbalanced journal, posting to a non-postable account, posting into a locked period, exceeding configured COA depth, re-parenting that would create a cycle, editing a `is_locked = TRUE` system_config row |
|
||||
| `UnauthorizedException` / `ForbiddenException` | Service | Reserved for when identity/permission checks are added later (this service currently only validates a JWT's signature/issuer/audience — no role checks exist yet, consistent with the "no authentication/role layer" decision in the accounting design; these exceptions exist for future use and for any endpoint that needs to confirm a caller identity claim is present via `GetUserIdFromContext`) |
|
||||
| `DataExtractor` | Service (payload parsing) | Any endpoint accepting a loosely-typed JSON body instead of a strongly-typed DTO — e.g. a generic "post adjustment" endpoint where the line items are dynamic. The Controller accepts `[FromBody] Dictionary<string, JsonElement>` and passes it straight to the Service unexamined (Section 3.2a) |
|
||||
| `DeserializePayload` | Service | Converting an incoming DTO into the `Dictionary<string, JsonElement>` shape `DataExtractor` expects, where applicable |
|
||||
| `IUnitOfWork` / `EFUnitOfWork` | Service only | Every Service method that writes data wraps its repository calls in `BeginAsync` → (repository calls) → `CommitAsync`, with `RollbackAsync` on failure |
|
||||
| `ApiResponse` | Service (constructed) / Controller (relayed) | The **standard response envelope for every endpoint in this service** — not just auth-related calls. `StatusCode`/`Success`/`Message`/`Data` wraps every response for consistency across the ERP suite. Built in the Service on the success path (Section 3.2a); the Controller only relays it |
|
||||
| `ApiKeyMiddleware` | Pipeline (before MVC) | Validates the `X-Api-Key` header on every request against configuration; rejects with a hand-built `ApiResponse`-shaped `401` before the request ever reaches a controller. Applies globally — not per-endpoint code |
|
||||
|
||||
`EncryptionHelper` is available but not expected to be used by General Ledger endpoints unless a future phase needs to encrypt a stored value (e.g. a bank account number). Its hardcoded key is a known, flagged issue (Section 2 of `PROJECT_SPECIFICATION.md`) that is explicitly **out of scope** to fix as part of this plan — noted here so it isn't silently "fixed" mid-phase without approval (`CLAUDE.md` rule 3).
|
||||
|
||||
---
|
||||
|
||||
## 4. Domain Model Scope
|
||||
|
||||
The domain is the **pure double-entry ledger core** defined in `Accounting_System_Design_LKAS.md` and reflected in the current ER diagram — 25 tables (the original 24 plus `audit_log`, Section 4.3 — both diagram files now include it, see the reference-documents note above), no AR/AP party subledger, no inventory, no multi-branch (all removed by design decision in that document's revision history).
|
||||
|
||||
### 4.1 Entity groups
|
||||
|
||||
| Group | Tables |
|
||||
|---|---|
|
||||
| Core Ledger | `account_type`, `accounts`, `fiscal_year`, `accounting_period`, `period_lock_log`, `journal_entry`, `journal_entry_line`, `journal_correction`, `cost_center` |
|
||||
| Fixed Assets | `fixed_asset`, `depreciation_schedule`, `asset_disposal` |
|
||||
| Bank & Cash | `bank_account`, `bank_statement_line` |
|
||||
| Tax Configuration | `tax_type`, `tax_code` |
|
||||
| Currency Configuration | `currency`, `exchange_rate`, `currency_rounding_rule` |
|
||||
| System Configuration | `system_config` |
|
||||
| Budgeting | `budget`, `budget_line` |
|
||||
| Manual Entry / Recurring | `recurring_journal_template`, `recurring_journal_template_line` |
|
||||
| Audit & Change Tracking | `audit_log` (generic, category-tagged — Section 4.3), plus the two purpose-specific tables already listed above under Core Ledger (`journal_correction`, `period_lock_log`), which `audit_log` does not replace or duplicate |
|
||||
|
||||
Every table now also carries `created_at` (added in a later revision of the LKAS document), and `system_config` carries `is_editable` / `is_locked` for one-time-vs-always-editable settings. These are reflected in the Phase 1 model list in Document 2. **Exception: `audit_log` has no separate `created_at` — its own `occurred_at` column already serves that role, and a duplicate timestamp column on a table that only ever gets inserted, never updated, would be redundant.**
|
||||
|
||||
### 4.2 Two structurally recursive entities
|
||||
|
||||
`accounts` (self-referencing `parent_account_id`) and `cost_center` (self-referencing `parent_cost_center_id`) are the two trees the whole reporting engine is built around. Both need:
|
||||
- A cycle-prevention check before any re-parent operation
|
||||
- A configurable max-depth check driven by `system_config` (`COA_MAX_DEPTH`, `COST_CENTER_MAX_DEPTH`, `COA_ENFORCE_MAX_DEPTH`)
|
||||
- Tree traversal and rollups in plain EF Core (no raw SQL, as of the 2026-10-06 MySQL migration — LINQ cannot express recursive CTEs, so repositories load the entity's rows as a `TreeRow` projection and walk them in memory). Implemented as a single shared helper, `Helpers/RecursiveTreeQueries.cs`, rather than duplicated per entity — see `CLAUDE.md` Rule 11
|
||||
|
||||
### 4.3 Audit Log — schema and classification (`LEDGER` vs `ADMINISTRATIVE`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE audit_log (
|
||||
audit_log_id BIGSERIAL PRIMARY KEY,
|
||||
category VARCHAR(20) NOT NULL, -- 'LEDGER' | 'ADMINISTRATIVE' — representation flagged in CLAUDE.md Rule 10, not decided here
|
||||
entity_name VARCHAR(50) NOT NULL, -- 'Account', 'JournalEntry', 'TaxCode', ...
|
||||
entity_id VARCHAR(50) NOT NULL, -- affected row's PK, stored as text since PK types vary across tables
|
||||
action VARCHAR(20) NOT NULL, -- 'CREATE', 'UPDATE', 'POST', 'REVERSE', 'ADJUST', 'LOCK', ...
|
||||
description TEXT, -- human-readable summary, e.g. "Account 5401 created"
|
||||
performed_by VARCHAR(100), -- free text, same pattern as JournalEntry.PreparedBy — no user table, no FK
|
||||
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
```
|
||||
|
||||
No FK to any other table — `entity_name` + `entity_id` is a loose reference by design, the same way a generic log naturally spans every table without needing 25 separate nullable FK columns.
|
||||
|
||||
**Classification rule:** a write is `LEDGER` if it touches recorded balances, the double-entry ledger itself, or the structural integrity of something reports roll up through (the COA tree, tax rates, the fiscal calendar). Everything else is `ADMINISTRATIVE`. `GET` endpoints never write an audit row — nothing to log.
|
||||
|
||||
| Phase | Endpoint | Category | Why |
|
||||
|---|---|---|---|
|
||||
| 2 | `POST /accounts` | `LEDGER` | Creates a COA node every rollup depends on |
|
||||
| 3 | `GET /accounts/{accountCode}` | *(none — read)* | |
|
||||
| 4 | `PUT /accounts/{accountCode}` | `LEDGER` **if** `parentAccountId` changed, `ADMINISTRATIVE` otherwise | Field-dependent — decided in the Service method itself, not fixed per-endpoint. Re-parenting moves the account in the tree every rollup depends on; renaming/deactivating doesn't |
|
||||
| 5 | `GET /accounts` | *(none — read)* | |
|
||||
| 6 | `POST /fiscal-years` | `LEDGER` | Every journal entry references a period by FK |
|
||||
| 7 | `POST /fiscal-years/{id}/periods` | `LEDGER` | Same reason as #6 |
|
||||
| 8 | `PUT /periods/{id}/lock` | `LEDGER` | Already has its own richer `period_lock_log` row — the `audit_log` entry is a secondary, lightweight cross-reference for the unified feed, not a replacement |
|
||||
| 9 | `POST /cost-centers` | `ADMINISTRATIVE` | A reporting dimension; doesn't affect the Balance Sheet/P&L/Trial Balance, which are account-based only |
|
||||
| 10 | `GET /cost-centers/{costCenterCode}` | *(none — read)* | |
|
||||
| 11 | `POST /journal-entries` | `LEDGER` | The core of the ledger — plain entries and corrections alike |
|
||||
| 12 | `PUT /journal-entries/{id}/lines` | `LEDGER` | Still editing a ledger entry's content, even pre-post |
|
||||
| 13 | `PUT /journal-entries/{id}/post` | `LEDGER` | The moment an entry becomes real |
|
||||
| 14 | `GET /journal-entries/{id}` | *(none — read)* | |
|
||||
| 15 | `POST /recurring-templates` | `ADMINISTRATIVE` | A saved recipe — posts nothing itself |
|
||||
| 16 | `POST /recurring-templates/{id}/generate` | `LEDGER` | Produces a real draft entry (delegates to #11's classification) |
|
||||
| 17 | `GET /reports` | *(none — read)* | |
|
||||
| 18 | *(removed 2026-07-18)* | *(n/a)* | No longer part of the API surface — see Section 6.1's FX Revaluation removal callout |
|
||||
| 19 | `POST /tax-codes` | `LEDGER` | Rate applies to every future transaction's tax calculation |
|
||||
| 20 | `GET /tax-codes/calculate` | *(none — read)* | |
|
||||
| 21 | `POST /fixed-assets` | `LEDGER` | Asset register underlies future depreciation/disposal postings |
|
||||
| 22 | `POST /fixed-assets/{assetCode}/transactions` | `LEDGER` | Posts a journal entry (both `Depreciate` and `Dispose`) |
|
||||
| 23 | `POST /bank-accounts` | `ADMINISTRATIVE` | Master data / cross-reference, not a posting |
|
||||
| 24 | `POST /bank-accounts/{id}/statement-lines/import` | `ADMINISTRATIVE` | Same reasoning as #23 — the import itself doesn't post anything |
|
||||
| 25 | `PUT /bank-statement-lines/{id}/reconcile` | `ADMINISTRATIVE` | Cross-references an existing entry, doesn't alter it |
|
||||
| 26 | `POST /budgets` | `ADMINISTRATIVE` | Planning data, not recorded financial history |
|
||||
| 27 | `POST /budgets/{id}/lines` | `ADMINISTRATIVE` | Same reasoning as #26 |
|
||||
| 28 | `PUT /system-config/{key}` | `LEDGER` for both current keys (`COA_MAX_DEPTH`, `COST_CENTER_MAX_DEPTH`, `COA_ENFORCE_MAX_DEPTH` all gate ledger structure) | Classified per config key, not fixed for the endpoint — a future purely-cosmetic key wouldn't automatically inherit `LEDGER` |
|
||||
| 29 | `GET /system-config` | *(none — read)* | |
|
||||
| 30–35 | All appended list endpoints (Section 6.3) | *(none — read)* | Every Phase 30–35 endpoint is a `GET` |
|
||||
|
||||
---
|
||||
|
||||
## 5. Default / Seed Data Strategy
|
||||
|
||||
Per the design document, the following tables **must** have default data present before the system is usable, because report queries and validation logic reference fixed values:
|
||||
|
||||
| Table | Why it needs default data | Seed content |
|
||||
|---|---|---|
|
||||
| `account_type` | Balance Sheet / P&L rollup queries filter by `account_type_id IN (1,2,3)` (Asset/Liability/Equity) and `IN (4,5)` (Income/Expense) — these IDs are structural, not business data | 5 fixed rows: `1=Asset(DR)`, `2=Liability(CR)`, `3=Equity(CR)`, `4=Income(CR)`, `5=Expense(DR)` |
|
||||
| `system_config` | The recursive-depth guard and other engine-level settings must exist before the first account is ever created | `COA_MAX_DEPTH=6` (editable), `COST_CENTER_MAX_DEPTH=4` (editable), `COA_ENFORCE_MAX_DEPTH=TRUE` (editable) |
|
||||
| `currency` | Exactly one base/functional currency must exist before any account or journal line can be created | `LKR`, `is_base_currency = TRUE`, seeded alongside the full ISO 4217 currency list (142 currencies total) — now seeded via `HasData` directly, since the runtime setup endpoint was removed (Section 6.1). **Confirmed 2026-07-18: this is a one-time initial-configuration choice — the client can never change the base currency afterward, and non-base currencies are never used at all (see the FX Revaluation removal callout in Section 6.1). The other 141 rows are seeded but functionally dead weight as a result — see Section 5.3** |
|
||||
| `tax_type` | Reference categories that `tax_code` rows are classified under | `VAT`, `WHT`, `NBT`, `SSCL`, `INCOME_TAX` |
|
||||
|
||||
**Not seeded by default** (business/company-specific, entered during setup, not hardcoded): `accounts` (the actual Chart of Accounts), `tax_code` (actual rates), `fiscal_year`/`accounting_period`, `cost_center`, `budget`.
|
||||
|
||||
### 5.1 Seeding mechanism
|
||||
|
||||
Seed data is applied via **EF Core `HasData` in `OnModelCreating`** for `account_type`, `system_config`, `tax_type`, and (as of the endpoint reduction in Section 6.1) `currency` — all four are now static, deterministic, and safe to bake into a migration, since there's no longer a runtime API call responsible for any of them.
|
||||
|
||||
This keeps every table in this section consistent: reference/foundational data that the engine depends on structurally lives in `HasData`, while genuinely business-specific data (the actual Chart of Accounts, tax rates, fiscal calendar, cost centers, budgets) is entered through the API once the system is running.
|
||||
|
||||
### 5.2 Exact seed values — copy-paste source for `AppDBContext.OnModelCreating`
|
||||
|
||||
This is the authoritative, exact specification for every `HasData` call needed before the application can be used — the same four tables from the summary table above, spelled out row by row rather than categorically. Implemented and verified live (Document 2, Phase 1 / `03_Progress_Tracker.md`); this section exists so the values have one canonical source instead of being re-derived from scattered notes each time they're needed.
|
||||
|
||||
**`account_type` — 5 rows, fixed IDs (referenced by report queries, Section 4.2):**
|
||||
|
||||
| `id` | `name` | `normal_balance` |
|
||||
|---|---|---|
|
||||
| 1 | Asset | DR |
|
||||
| 2 | Liability | CR |
|
||||
| 3 | Equity | CR |
|
||||
| 4 | Income | CR |
|
||||
| 5 | Expense | DR |
|
||||
|
||||
**`system_config` — 3 rows.** `is_editable` / `is_locked` follow the **corrected semantics** (Deviations log, 2026-07-18 — not the original design doc wording): the API rejects a write when `!IsEditable || IsLocked`, and **every successful write sets `IsLocked = TRUE` afterward** — so `IsEditable = TRUE` does not mean "always changeable," it means "eligible to be changed exactly once via the API." A key seeded with `IsEditable = FALSE` would never be writable via the API at all (fixed for the life of the system, seed-only). All three current keys start `IsEditable = TRUE`, `IsLocked = FALSE` — each can be set once (e.g. during initial deployment/configuration) and is then permanently locked:
|
||||
|
||||
| `config_key` | `config_value` | `description` | `is_editable` | `is_locked` |
|
||||
|---|---|---|---|---|
|
||||
| `COA_MAX_DEPTH` | `6` | Maximum allowed levels in the recursive Chart of Accounts tree | `TRUE` | `FALSE` |
|
||||
| `COST_CENTER_MAX_DEPTH` | `4` | Maximum allowed levels in the recursive Cost Center tree | `TRUE` | `FALSE` |
|
||||
| `COA_ENFORCE_MAX_DEPTH` | `TRUE` | If `FALSE`, COA depth is unlimited (`COA_MAX_DEPTH` ignored) | `TRUE` | `FALSE` |
|
||||
|
||||
**`tax_type` — 5 rows:**
|
||||
|
||||
| `id` | `code` | `name` |
|
||||
|---|---|---|
|
||||
| 1 | `VAT` | Value Added Tax |
|
||||
| 2 | `WHT` | Withholding Tax |
|
||||
| 3 | `NBT` | Nation Building Tax |
|
||||
| 4 | `SSCL` | Social Security Contribution Levy |
|
||||
| 5 | `INCOME_TAX` | Income Tax |
|
||||
|
||||
### 5.3 Currency seed data — full ISO 4217 list, not enumerated inline
|
||||
|
||||
142 rows, seeded via migration `SeedIsoCurrencies` (Deviations log, 2026-07-18) — too long to usefully list row-by-row in this document. The shape every row follows:
|
||||
|
||||
| `currency_code` | `currency_name` | `symbol` | `decimal_places` | `is_base_currency` | `is_active` |
|
||||
|---|---|---|---|---|---|
|
||||
| `LKR` | Sri Lankan Rupee | `Rs` | 2 | `TRUE` | `TRUE` |
|
||||
| `USD` | US Dollar | `$` | 2 | `FALSE` | `TRUE` |
|
||||
| `JPY` | Japanese Yen | `¥` | 0 | `FALSE` | `TRUE` |
|
||||
| `KWD` | Kuwaiti Dinar | `KD` | 3 | `FALSE` | `TRUE` |
|
||||
| *(...138 more, one per active ISO 4217 currency)* | | | | | |
|
||||
|
||||
- Exactly one row (`LKR`) has `is_base_currency = TRUE` — enforced by the partial unique index from the design document, not just seed-time discipline.
|
||||
- `decimal_places` varies by currency: 16 currencies seed at `0` (JPY, KRW, VND, and 13 others), 7 at `3` (KWD and similar), the rest at the default `2`.
|
||||
- **A real `HasData` bug was found and fixed while seeding this table**, worth knowing before writing any future `HasData` seed with a similar shape: EF Core's change-tracking diff treats a seeded value equal to the CLR default for its type (`0` for `short`) as "unset" when the column also has `HasDefaultValue` configured in the model, and silently drops it from the generated `INSERT` — every `decimal_places = 0` row was seeding as `2` instead until the column's default value convention was removed entirely (migration `RemoveCurrencyDecimalPlacesDefault`). If a future `HasData` seed needs a column where a meaningful value can coincide with `0`/`false`/`null`, don't also configure `HasDefaultValue` on that column.
|
||||
- Confirmed dead weight, not a bug: since Phase 18 (FX Revaluation) was permanently removed (Section 6.1) and the base currency can never change, the 141 non-base currency rows and the `exchange_rate`/`currency_rounding_rule` tables are fully seeded/functional but structurally unreachable — every journal line always resolves to the single base currency at a rate of `1`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Full API Surface
|
||||
|
||||
### 6.1 Endpoint consolidation strategy
|
||||
|
||||
The domain has ~40 distinct write/read operations, but that doesn't mean 40 endpoints. Several groups of operations are **the same shape** — one aggregate, one write, differing only in a small discriminator value — and are collapsed into a single endpoint that takes an enum/discriminator in the request body, rather than one endpoint per variant. Other groups look similar on the surface but actually differ in resource, HTTP semantics, or failure mode, and are kept separate. The rule applied throughout:
|
||||
|
||||
> **Merge when** two or more operations write to the same aggregate, share the same validation shape, and differ only by "which kind" — expressed as one enum field. **Keep separate when** operations act on different resources, mix reads with writes, or have meaningfully different request/response shapes — merging those would blur REST semantics and make each phase's acceptance test ambiguous (Section 7.2).
|
||||
|
||||
**Five consolidations applied, plus two endpoints removed and one method correction:**
|
||||
|
||||
| Change | Old endpoints | New endpoint | Mechanism |
|
||||
|---|---|---|---|
|
||||
| Reporting | `GET .../reports/general-ledger`, `.../trial-balance`, `.../balance-sheet`, `.../profit-and-loss`, `.../cash-flow`, `.../trial-balance/pdf`, `.../reports/budget-vs-actual` (7 endpoints) | `GET /api/v1/reports` | `ReportType` and `OutputFormat` as **query parameters** (not a request body — see the method correction below), selecting which report logic runs and whether the response is JSON or PDF |
|
||||
| Journal entry create + correction | `POST .../journal-entries`, `POST .../journal-entries/{id}/reverse`, `POST .../journal-entries/{id}/adjust` (3 endpoints, after an earlier round already merged reverse+adjust into 2) | `POST /api/v1/journal-entries` | A single creation endpoint now handles **both** a plain new entry and a correction of an existing one. An optional `correction` object in the request body (`{ originalJournalId, correctionType }`) is the discriminator — its *absence* means "just create a normal draft"; its presence means "create this new entry as a correction of that one." Both cases are fundamentally the same operation: insert a new `journal_entry` row. See the worked example in Document 2, Phase 11 |
|
||||
| Journal entry lines (generalized) | `PUT .../journal-entries/{id}/lines` (manual drafts only) | `PUT /api/v1/journal-entries/{id}/lines` (same route, broader purpose) | No longer documented as "manual entries only" — it sets/replaces the lines of **any** entry still in `DRAFT` status, whether that draft originated as a plain manual entry or as a `PartialAdjustment` correction (Document 2, Phase 12). This isn't a new endpoint; it's the existing one doing the job it was already structurally capable of doing, now used consistently for both cases instead of only one |
|
||||
| Fixed asset transactions | `POST .../fixed-assets/{id}/depreciate`, `POST .../fixed-assets/{id}/dispose` (2 endpoints) | `POST /api/v1/fixed-assets/{assetCode}/transactions` | `TransactionType` enum (`Depreciate`, `Dispose`); looked up by `assetCode`, not id (`CLAUDE.md` Rule 8) |
|
||||
| Account read | `GET .../accounts/{id}`, `GET .../accounts/{id}/tree` (2 endpoints) | `GET /api/v1/accounts/{accountCode}` | Optional `?expand=tree` query parameter. Looked up by `accountCode`, not the numeric id — see `CLAUDE.md` Rule 8 |
|
||||
| **Removed entirely** | `POST /api/v1/currencies`, `POST /api/v1/currencies/{code}/rates` | *(none)* | Multi-currency management is out of scope for the API surface — see the callout below |
|
||||
| **Removed entirely (2026-07-18)** | `POST /api/v1/fx-revaluation` (was row 18) | *(none)* | Confirmed out of scope — see the callout below |
|
||||
| **Method corrected** | `POST /api/v1/reports` (an earlier round's choice) | `GET /api/v1/reports` | Reporting is read-only with no side effects — `GET` is the correct HTTP verb; there was never a strong reason it needed to be `POST`. See the callout below |
|
||||
|
||||
**Net effect:** 40 original endpoints → **27 endpoints** (7 saved from reporting, 1 from merging journal-entry create+correction into one route, 1 from fixed asset transactions, 1 from account read, 2 removed outright for currency, 1 removed outright for FX revaluation = **13 fewer than the original 40**, a 32.5% reduction).
|
||||
|
||||
**Callout — why the currency endpoints were removed:** `POST /api/v1/currencies` and `POST /api/v1/currencies/{code}/rates` are no longer part of the API surface. This is a **scope decision, not a technical merge** — the schema (`currency`, `exchange_rate`, `currency_rounding_rule` tables) is unaffected, and multi-currency data can still be seeded directly in the database if it's ever needed, the same way `account_type`/`tax_type` are seeded via `HasData` rather than an API call.
|
||||
|
||||
**Callout — why `POST /api/v1/fx-revaluation` (Phase 18) was removed, resolving its earlier `⚠ Blocked` status:** this was originally kept in the plan as an open decision, flagged because removing the currency-management endpoints left it with no API-driven way to populate the non-base-currency balances and exchange rates it would revalue. **Confirmed 2026-07-18: the base/functional currency is a one-time initial-configuration choice that can never be changed by the client afterward, 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, the endpoint has no possible purpose — not "blocked pending data," but permanently out of scope. Removed outright, same treatment as the currency-management endpoints above, rather than left "blocked" indefinitely. Per the "no renumbering, ever" policy, **Phase 18's slot is not reused** — Document 2's Phase 18 section and `03_Progress_Tracker.md` record it as removed, not silently dropped. This also leaves a chunk of already-built scaffolding as known, harmless dead weight: the `exchange_rate`/`currency_rounding_rule` tables, ~141 of the 142 seeded `Currency` rows, and `JournalEntryLine`'s `CurrencyCode`/`ExchangeRate`/`BaseDebit`/`BaseCredit` columns — none of it is wrong, it just always resolves to the single base currency / rate of 1 and always will.
|
||||
|
||||
**Callout — why reporting is `GET`, not `POST`:** report parameters (`reportType`, `outputFormat`, a date or two, an optional id) are simple scalars that fit a query string perfectly well — `GET /api/v1/reports?reportType=TrialBalance&asOfDate=2026-07-31&outputFormat=Json`. A report has no side effects, is safely retryable, and is naturally cacheable — all textbook reasons to use `GET` over `POST`. The earlier choice of `POST` didn't have a strong justification behind it and has been corrected.
|
||||
|
||||
**Considered and deliberately *not* merged**, with the reasoning:
|
||||
|
||||
- **Create fiscal year vs. generate periods** (`POST /fiscal-years` vs `POST /fiscal-years/{id}/periods`) — left separate. A fiscal year can gain an optional `autoGeneratePeriods` flag on creation to cover the common case in one call, but the periods-generation endpoint stays independently callable, because periods sometimes need to be (re)generated or extended for a fiscal year that already exists.
|
||||
- **Journal entry lines vs. post** (`PUT .../lines` vs `PUT .../post`) — left separate; editing lines is a repeatable step, posting is a one-way state transition with its own heavy validation (balance check, period-lock check) — collapsing these would make the balance check ambiguous about when it runs.
|
||||
- **Recurring template generate** (`POST /recurring-templates/{id}/generate`) — kept as its own endpoint for on-demand/manual triggering and testing, in addition to (not instead of) a scheduled job that could call the same Service method internally.
|
||||
- **Bank statement import vs. reconcile** — left separate; import is a bulk file write with per-row failure handling, reconcile is a single-row interactive state change. Different payload shapes, different failure modes.
|
||||
- **Tax code create vs. calculate** — left separate; one persists a new rate row, the other is a stateless computation against existing rows.
|
||||
- **Budget create vs. add lines** — left separate; line items are added incrementally over time, not fixed at creation.
|
||||
- **System config update vs. list** — left separate; standard single-resource-write vs. collection-read pair.
|
||||
|
||||
### 6.2 Full API Surface — 27 endpoints, each justified
|
||||
|
||||
| # | Module | Method | Endpoint | Purpose | Why this is its own endpoint |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | *(Phase 1 — no API, models + DB + seed data only)* | | | | — |
|
||||
| 2 | Accounts | `POST` | `/api/v1/accounts` | Create an account (validates parent depth, cycle-safe by construction on create) | Distinct write with its own validation (depth guard, duplicate code) — the entry point to the recursive COA |
|
||||
| 3 | Accounts | `GET` | `/api/v1/accounts/{accountCode}` | Get one account, ancestor breadcrumb, and (via `?expand=tree`) its recursive descendant tree | Merged with the old tree-only endpoint — one read, two optional shapes of the same underlying resource. Looked up by `accountCode`, not id (`CLAUDE.md` Rule 8) |
|
||||
| 4 | Accounts | `PUT` | `/api/v1/accounts/{accountCode}` | General account update, including re-parenting (cycle + depth check triggers only when `parentAccountId` is part of the payload and has changed) | One write endpoint for "anything about this account changed," rather than a narrow reparent-only endpoint plus a separate future endpoint for renaming/deactivating. Looked up by `accountCode`, not id; `parentAccountId` in the body stays numeric (`CLAUDE.md` Rule 8) |
|
||||
| 5 | Accounts | `GET` | `/api/v1/accounts` | List/search accounts (filter by type, active, postable) | Collection read is a different shape (pagination, filters) from a single-resource read |
|
||||
| 6 | Fiscal Calendar | `POST` | `/api/v1/fiscal-years` | Create a fiscal year (optional `autoGeneratePeriods` flag covers the common case in one call) | Distinct aggregate root — a real operational record with its own lock/close lifecycle, not expressible as a `system_config` key (Section 4.2 — periods are referenced by FK from every journal entry and individually lockable) |
|
||||
| 7 | Fiscal Calendar | `POST` | `/api/v1/fiscal-years/{id}/periods` | Generate/add accounting periods for a fiscal year | Kept independently callable for regenerating or extending periods later — see Section 6.1 |
|
||||
| 8 | Fiscal Calendar | `PUT` | `/api/v1/periods/{id}/lock` | Lock/soft-close/unlock a period (writes `period_lock_log`) | A state-transition action with its own audit trail, not a field update |
|
||||
| 9 | Cost Centers | `POST` | `/api/v1/cost-centers` | Create a cost center (recursive, same depth-guard pattern as accounts) | Separate aggregate/dimension from the COA — tags *who* spent, independent of *which account* was hit; own recursive tree, own depth guard against `COST_CENTER_MAX_DEPTH` |
|
||||
| 10 | Cost Centers | `GET` | `/api/v1/cost-centers/{costCenterCode}` | Get one cost center, with `?expand=tree` for its recursive descendant tree | Same read-merge pattern as #3. Looked up by `costCenterCode`, not id — `CostCenter.Code` is `UNIQUE` in the design doc (`CLAUDE.md` Rule 8) |
|
||||
| 11 | Journal Entries | `POST` | `/api/v1/journal-entries` | Create a new journal entry — a plain manual `DRAFT` (optionally with inline `lines`), **or** a correction of an existing posted entry (optional `correction: { originalJournalId, correctionType }`) | Merged — see Section 6.1. One endpoint for "a new journal_entry row needs to be created," regardless of why |
|
||||
| 12 | Journal Entries | `PUT` | `/api/v1/journal-entries/{id}/lines` | Add/replace lines on any entry still in `DRAFT` status — plain manual entries **and** `PartialAdjustment` corrections alike | Generalized in purpose (not in route) — see Section 6.1. Kept separate from create/post since editing lines is a repeatable step before the one-way post transition |
|
||||
| 13 | Journal Entries | `PUT` | `/api/v1/journal-entries/{id}/post` | Post a draft entry (balance check, period-open check, postable-account check). If the entry being posted is a `FullReversal` correction, this also flips the original entry to `REVERSED` in the same transaction | The one moment an entry becomes real (`is_posted = TRUE`) — a distinct, heavily-validated state transition |
|
||||
| 14 | Journal Entries | `GET` | `/api/v1/journal-entries/{id}` | Get one entry with its lines | Standard single-resource read, needed by every other journal-entry operation's "load it first" step |
|
||||
| 15 | Recurring Entries | `POST` | `/api/v1/recurring-templates` | Create a recurring journal template | Distinct aggregate (its own table, its own line items) — a reusable recipe, not a transaction |
|
||||
| 16 | Recurring Entries | `POST` | `/api/v1/recurring-templates/{id}/generate` | Generate the next draft entry from a template (internally calls #11's create-draft logic) | Kept as an on-demand action for manual/immediate triggering and testing — see Section 6.1 |
|
||||
| 17 | Reporting | `GET` | `/api/v1/reports` | Unified reporting endpoint — `reportType` and `outputFormat` query parameters (`GeneralLedger`, `TrialBalance`, `BalanceSheet`, `ProfitAndLoss`, `CashFlow`, `BudgetVsActual`, `TaxSummary` × `Json`, `Pdf`, `Csv`) | Merged from 7 endpoints, corrected from `POST` to `GET` — see Section 6.1. `TaxSummary` added 2026-07-21 as a 7th `reportType` on this same endpoint (not a new phase/endpoint — the whole point of the reportType discriminator is that a new report doesn't need a new route), then **wholesale redesigned 2026-07-22** from a VAT/WHT/NBT summary into a full Income Tax Computation (`04_API_Reference_And_Scenarios.md`). `Csv` added 2026-07-22 as a third `outputFormat`, same reasoning — no new route needed for a new format either |
|
||||
| 18 | Currency | *(removed)* | *(removed)* | **Removed 2026-07-18** — base currency is a one-time initial-configuration choice, never changed, non-base currencies never used; FX revaluation has no possible purpose in this system (Section 6.1 callout) | Phase slot not reused — "no renumbering, ever" |
|
||||
| 19 | Tax | `POST` | `/api/v1/tax-codes` | Create a tax code (rate, basis, accounts, effective dates) | Distinct, low-frequency setup write |
|
||||
| 20 | Tax | `GET` | `/api/v1/tax-codes/calculate` | Calculate tax for a given amount/date/tax code (no posting) | Pure read/computation, not a write |
|
||||
| 21 | Fixed Assets | `POST` | `/api/v1/fixed-assets` | Register a fixed asset | Distinct aggregate root for the asset register |
|
||||
| 22 | Fixed Assets | `POST` | `/api/v1/fixed-assets/{assetCode}/transactions` | Unified depreciation/disposal (`TransactionType`: `Depreciate` \| `Dispose`) | Merged — see Section 6.1. Looked up by `assetCode`, not id (`CLAUDE.md` Rule 8) |
|
||||
| 23 | Bank | `POST` | `/api/v1/bank-accounts` | Create a Bank account (unchanged original shape) | Distinct, low-frequency setup write |
|
||||
| 23a | Bank | `POST` | `/api/v1/cash-accounts` | Create a Cash account — two separate tables, not a discriminator, retrofit 2026-07-22 (see `Accounting_System_Design_LKAS.md` §5.8): a cash account is a peer concept to a bank account, not a kind of one | Same table/endpoint-naming precedent as `TaxSummary` keeping its name after a content redesign — `bank_account`/`bank-accounts` predate this split and stay as-is, cash gets its own new table/route instead of retrofitting a discriminator onto an already-shipped one |
|
||||
| 23b | Bank | `GET` | `/api/v1/cash-account-types` | List cash account types (new, 2026-07-22) | Growable reference list (seeded Petty Cash/Till Cash/Safe Cash/Cash in Transit, gains a row automatically when `POST /cash-accounts` is called with an unrecognized `cashAccountTypeName`) needed to populate a frontend dropdown |
|
||||
| 24 | Bank | `POST` | `/api/v1/bank-accounts/{id}/statement-lines/import` | Import a bank statement via `.xlsx` (ClosedXML) | Bulk file-based write with per-row failure handling |
|
||||
| 25 | Bank | `PUT` | `/api/v1/bank-statement-lines/{id}/reconcile` | Match a statement line to a journal entry | Single-row interactive state change |
|
||||
| 26 | Budget | `POST` | `/api/v1/budgets` | Create a budget for a fiscal year | Distinct aggregate root |
|
||||
| 27 | Budget | `POST` | `/api/v1/budgets/{id}/lines` | Add budget lines | Kept separate — lines are added incrementally over time, not fixed at creation |
|
||||
| 28 | System Config | `PUT` | `/api/v1/system-config/{key}` | Update a config value (enforces `is_editable`/`is_locked`) | Single-key write with its own one-time-lock enforcement |
|
||||
| 29 | System Config | `GET` | `/api/v1/system-config` | List all config keys/values | Standard collection read, distinct from the single-key write above |
|
||||
|
||||
Each row above (2 through 29) becomes exactly one phase in Document 2, in this order, following Phase 1 (models/DB/seed), **except Phase 18 (removed 2026-07-18 — see the FX Revaluation callout above; its slot is not reused).** **27 API endpoints in the original numbered sequence — down from the original 40 — plus 6 appended list endpoints (Phases 30–35, Section 6.3) and 1 appended audit-log endpoint (Phase 36, Section 6.4): 34 API endpoints, 36 phases total (Phase 18's slot counts toward the phase total but has no endpoint).**
|
||||
|
||||
### 6.3 Phases 30–35 — appended read/list endpoints (user-identified gap, 2026-07-18)
|
||||
|
||||
Reviewing the surface above turned up a real gap: **Tax, Fixed Assets, Bank, and Budget have write-only API surfaces** — every phase for those four modules is a `POST`/`PUT`, with no way to list or read back what's actually in those tables (the same problem `Currency` had, since its own create endpoints were removed entirely per Section 6.1). These six endpoints are appended as new phases rather than inserted into the existing numbering, per the phase-numbering policy in `03_Progress_Tracker.md` ("no renumbering, ever").
|
||||
|
||||
| # | Module | Method | Endpoint | Purpose | Why this is its own endpoint |
|
||||
|---|---|---|---|---|---|
|
||||
| 30 | Currency | `GET` | `/api/v1/currencies` | List all currencies (seeded via `HasData`, ~140 rows) | `Currency` has zero API read visibility today — the only way to see what's seeded is direct DB access |
|
||||
| 31 | Tax | `GET` | `/api/v1/tax-types` | List all tax types (seeded via `HasData`: VAT, WHT, NBT, SSCL, INCOME_TAX) | Same gap as `Currency` — pure reference data, no read endpoint |
|
||||
| 32 | Tax | `GET` | `/api/v1/tax-codes` | List tax codes created via Phase 19 | Phase 19 is create-only; no way to list what's been created |
|
||||
| 33 | Fixed Assets | `GET` | `/api/v1/fixed-assets` | List fixed assets created via Phase 21 | Phase 21 is create-only; no way to list the asset register |
|
||||
| 34 | Bank | `GET` | `/api/v1/bank-accounts` | Unified, filterable list of Cash and Bank accounts together (`?accountType=Cash\|Bank\|Both`, default `Both`), retrofit 2026-07-22 — combines Phase 23's `bank_account` rows and Phase 23a's `cash_account` rows into one flattened `CashAndBankAccountDto` list, `accountType` a response-only discriminator (not a stored column) | Phases 23/23a are create-only; no way to list configured accounts of either kind without two separate calls |
|
||||
| 35 | Budget | `GET` | `/api/v1/budgets` | List budgets created via Phase 26 | Phase 26 is create-only; no way to list existing budgets |
|
||||
|
||||
Phase 30 is implemented immediately (Currency already has seed data with nothing depending on it). Phases 31–35 are implemented alongside (immediately after) their respective module's create phase, since list-reading a table that doesn't exist yet has nothing to test against.
|
||||
|
||||
### 6.4 Phase 36 — audit log list endpoint (appended, user request, filterable by category)
|
||||
|
||||
Same "append, never renumber" policy as Section 6.3.
|
||||
|
||||
| # | Module | Method | Endpoint | Purpose | Why this is its own endpoint |
|
||||
|---|---|---|---|---|---|
|
||||
| 36 | Audit | `GET` | `/api/v1/audit-log` | List `audit_log` rows, filterable by `category` (`LEDGER`, `ADMINISTRATIVE`, both if omitted, or repeated to select more than one), `entityName`, `action`, `performedBy`, and an `occurredAt` date range; paginated | The one read surface for everything Section 4.3 writes — a standard collection read, same shape as every other list endpoint in Phases 30–35, just against a table with more filter dimensions |
|
||||
|
||||
`category` accepts zero, one, or multiple values via a repeated query parameter — `?category=LEDGER`, `?category=LEDGER&category=ADMINISTRATIVE`, or the parameter omitted entirely, which returns both. This is plain ASP.NET Core query-string array binding (`List<string>? Category` on the request DTO), not a custom parser — consistent with `CLAUDE.md`'s "keep it simple" rule.
|
||||
|
||||
Requires the Phase 1 retrofit (`audit_log` model + migration) to exist first, and in practice returns nothing meaningful until at least one write-phase's own audit-insert retrofit (Section 4.3) is also in place — but as a `GET`, it can be implemented and smoke-tested (empty result set) independently of that sequencing.
|
||||
|
||||
---
|
||||
|
||||
## 7. Non-Functional Considerations
|
||||
|
||||
### 7.1 Global exception handling — a genuine gap, flagged for a decision
|
||||
|
||||
`PROJECT_SPECIFICATION.md` explicitly notes the four `CustomExceptions` aren't yet caught by any global handler, so today they'd surface as unhandled 500s. This plan proposes (subject to approval per `CLAUDE.md` rule 3) adding **one small piece of exception-handling middleware early in Phase 2** that:
|
||||
- Catches `NotFoundException` → 404, `BadRequestException` → 400, `UnauthorizedException` → 401, `ForbiddenException` → 403
|
||||
- Wraps the error in the same `ApiResponse` envelope (`Success = false`, `Message` = exception message) used for every successful response, so callers get one consistent response shape regardless of outcome
|
||||
|
||||
This is flagged rather than silently added because it touches shared composition-root code (`Program.cs`), which falls under "ask before editing" per `CLAUDE.md` rule 3.
|
||||
|
||||
### 7.2 Testing approach per phase
|
||||
|
||||
Each phase (from Phase 2 onward) is scoped to exactly one API call end-to-end, specifically so it can be manually verified via the existing `General_Ledger_Service.http` scratch file (updated with a real request for that phase's endpoint) or Swagger UI before moving to the next phase. No phase should be considered complete until its endpoint has been exercised at least once against a real local PostgreSQL database.
|
||||
|
||||
### 7.3 Logging
|
||||
|
||||
Serilog is already wired (`Serilog.AspNetCore`, `Serilog.Sinks.File`). Each Service method that performs a write (journal posting, period locking, reversal/adjustment) should log at `Information` level on success and the Service should let thrown `CustomExceptions` be logged by the (future) global handler rather than logging-and-rethrowing in multiple places.
|
||||
|
||||
### 7.4 Testing with the API Key
|
||||
|
||||
Since `ApiKeyMiddleware` sits in front of every controller once Phase 1 is complete, **every manual test from Phase 2 onward must include the `X-Api-Key` header** with the configured local development value, or every request will fail at the pipeline stage with a `401` before it ever reaches the endpoint being tested. The `General_Ledger_Service.http` scratch file should define this header once as a variable and reuse it across every request.
|
||||
|
||||
### 7.5 PDF / Excel output
|
||||
|
||||
`QuestPDF` (rendering, selected via `outputFormat=Pdf` on the unified `/api/v1/reports` endpoint) and `ClosedXML` (bank statement import) are already referenced in the `.csproj`. These are used starting at Phase 17 (Reporting, which handles both JSON and PDF output from the start) and Phase 24 (Excel import) — the core ledger and reporting logic is still proven correct with plain JSON responses first, since `outputFormat=Json` is the default and PDF is only exercised once the JSON path already works within that same phase.
|
||||
|
||||
---
|
||||
|
||||
## 8. Explicitly Out of Scope
|
||||
|
||||
Carried over from the accounting design document's own scope decisions — **not part of this service's build-out**:
|
||||
- Authentication/role-based access control beyond the existing JWT bearer validation (no user/role tables)
|
||||
- AR/AP party subledgers (`customer`, `supplier`) — removed from the design
|
||||
- Inventory (`inventory_item`, `stock_movement`) — removed from the design
|
||||
- Multi-branch (`branch`, `inter_branch_account_map`) — removed from the design
|
||||
|
||||
If any of these are reintroduced later, they get their own new phases appended to Document 2 — they are not retrofitted into existing phases.
|
||||
|
||||
---
|
||||
|
||||
## 9. Tech Stack (fixed, from `.csproj` — not to be changed without approval)
|
||||
|
||||
.NET 10 / ASP.NET Core Web API · EF Core 9 + Pomelo (MySQL) · Serilog · Swashbuckle (Swagger) · QuestPDF · ClosedXML · JWT Bearer auth against `ERPCore` issuer · `Asp.Versioning.Mvc` 10.0.0 (controller/route versioning, `api/v1/...`) · custom `ApiKeyMiddleware` (service-to-service authentication, shared secret via `ApiKey:Value` config)
|
||||
|
||||
---
|
||||
|
||||
## 10. Post-Completion Architecture Refactor (2026-07-19)
|
||||
|
||||
With all 36 phase slots resolved (Section 6, `03_Progress_Tracker.md`), the user directed a project-wide consolidation pass — **not a new feature, no new endpoint, no schema change** — across `Models/`, `Repositories/`, `Services/`, and `Controllers/GeneralLedgerController.cs`. Two things drove it, stated directly by the user: (1) the number of files per layer had grown roughly 1:1 with the number of tables, most of them trivial (10–40 lines), and (2) the same handful of CRUD/response-building shapes (`GetByIdAsync`, `ExistsAsync`, `AddAsync`, building an `ApiResponse`) were being hand-written over and over rather than shared, the same category of problem Rule 11 (`CLAUDE.md`) already solved once for recursive-tree logic.
|
||||
|
||||
This section is the design; `02_Phase_By_Phase_Development_Plan.md`'s Refactor Phases (R1–R6) is the ordered execution plan; `03_Progress_Tracker.md` tracks completion the same way it does for Phases 1–36.
|
||||
|
||||
**Ground rule for the whole refactor: zero behavior change.** No public API contract, no request/response DTO shape, no database schema, no business rule changes. Every endpoint must return byte-for-byte the same response after the refactor that it did before. This is why the refactor is scoped to file organization + boilerplate extraction only, not touched interleaved with any new work.
|
||||
|
||||
### 10.1 Models/ — 25 files → 12 files, grouped by domain
|
||||
|
||||
One file per domain module (mirroring the Repository clusters in 10.2), not one class per file. Pure move — no property, attribute, or navigation changes.
|
||||
|
||||
| New file | Classes moved in |
|
||||
|---|---|
|
||||
| `Models/Account.cs` | `Account`, `AccountType` |
|
||||
| `Models/CostCenter.cs` | `CostCenter` |
|
||||
| `Models/Currency.cs` | `Currency`, `CurrencyRoundingRule`, `ExchangeRate` |
|
||||
| `Models/FiscalCalendar.cs` | `FiscalYear`, `AccountingPeriod`, `PeriodLockLog` |
|
||||
| `Models/JournalEntry.cs` | `JournalEntry`, `JournalEntryLine`, `JournalCorrection` |
|
||||
| `Models/RecurringTemplate.cs` | `RecurringJournalTemplate`, `RecurringJournalTemplateLine` |
|
||||
| `Models/Tax.cs` | `TaxType`, `TaxCode` |
|
||||
| `Models/FixedAsset.cs` | `FixedAsset`, `DepreciationSchedule`, `AssetDisposal` |
|
||||
| `Models/Bank.cs` | `BankAccount`, `BankStatementLine` |
|
||||
| `Models/Budget.cs` | `Budget`, `BudgetLine` |
|
||||
| `Models/SystemConfig.cs` | `SystemConfig` |
|
||||
| `Models/AuditLog.cs` | `AuditLog` |
|
||||
|
||||
### 10.2 Repositories/ — 17 files → 14 files
|
||||
|
||||
Tax/FixedAsset/Bank/Budget are already-consolidated clusters (Rule 2) and are untouched here. Two new merges, both reversing/extending prior per-table decisions — see `CLAUDE.md` Rule 2's updated note for the full reasoning:
|
||||
|
||||
| New/changed file | Interface | Tables covered | Change |
|
||||
|---|---|---|---|
|
||||
| `AccountRepository.cs` | `IAccountRepository` | `accounts` | Unchanged — too large/central (237 lines, 4 embedded report queries) to fold into anything |
|
||||
| `CostCenterRepository.cs` | `ICostCenterRepository` | `cost_center` | Unchanged — standalone recursive entity |
|
||||
| `CurrencyRepository.cs` | `ICurrencyRepository` | `currency` | Unchanged |
|
||||
| `FiscalCalendarRepository.cs` | `IFiscalCalendarRepository` | `fiscal_year`, `accounting_period`, `period_lock_log` | **New merge** — was 3 files (`FiscalYearRepository`, `AccountingPeriodRepository`, `PeriodLockLogRepository`); mirrors `FiscalCalendarService`, which already spans exactly these three tables |
|
||||
| `JournalEntryRepository.cs` | `IJournalEntryRepository` | `journal_entry`, `journal_entry_line`, `journal_correction` | **New merge, reverses the prior explicit decision to keep these separate** — was 3 files. `JournalEntryService` is the sole consumer of all three tables, the same "only ever touched together" test already used for Tax/FixedAsset/Bank/Budget |
|
||||
| `RecurringJournalTemplateRepository.cs` | `IRecurringJournalTemplateRepository` | `recurring_journal_template` (+ line, via EF cascade on the parent's collection nav — no separate line repository ever existed) | Unchanged |
|
||||
| `TaxRepository.cs` | `ITaxRepository` | `tax_type`, `tax_code` | Unchanged (already consolidated) |
|
||||
| `FixedAssetRepository.cs` | `IFixedAssetRepository` | `fixed_asset`, `depreciation_schedule`, `asset_disposal` | Unchanged (already consolidated) |
|
||||
| `BankRepository.cs` | `IBankRepository` | `bank_account`, `bank_statement_line` | Unchanged (already consolidated) |
|
||||
| `BudgetRepository.cs` | `IBudgetRepository` | `budget`, `budget_line` | Unchanged (already consolidated) |
|
||||
| `SystemConfigRepository.cs` | `ISystemConfigRepository` | `system_config` | Unchanged |
|
||||
| `AuditLogRepository.cs` | `IAuditLogRepository` | `audit_log` | Unchanged |
|
||||
| `RecursiveTreeQueries.cs` | *(none — shared helper, Rule 11)* | — | Unchanged |
|
||||
| `RepositoryHelpers.cs` | *(none — shared helper)* | — | **New file** — see 10.4 |
|
||||
|
||||
### 10.3 Services/ — already at the right granularity
|
||||
|
||||
14 files, one per domain module (the same modules as 10.2, plus `DepthGuard.cs`) — no file consolidation needed here; Services were already grouped by domain rather than by table (`FiscalCalendarService` already spans 3 tables, `JournalEntryService` already spans 3 tables). The only addition is `Services/ResponseHelpers.cs` (10.4) and the constructor updates flowing from 10.2's two repository merges (`IFiscalCalendarRepository` replaces 3 injected repositories in `FiscalCalendarService`; `IJournalEntryRepository` replaces 3 injected repositories in `JournalEntryService`, `RecurringTemplateService`, `FixedAssetService`, and `BankService` — wherever those services currently depend on `IJournalEntryRepository`/`IJournalEntryLineRepository`/`IJournalCorrectionRepository` individually).
|
||||
|
||||
### 10.4 Two new shared helpers (Rule 11 pattern, extended)
|
||||
|
||||
- **`Repositories/RepositoryHelpers.cs`** — static helper, no base class/inheritance, for the boilerplate duplicated ~20+ times across repositories: `GetByIdAsync<T>(DbSet<T> set, params object[] keys)`, `ExistsAsync<T>(DbSet<T> set, Expression<Func<T,bool>> predicate)`, `AddAsync<T>(DbSet<T> set, T entity)`. Each repository keeps its own public interface and method names — it just delegates the one-line body to the shared helper internally, exactly like `RecursiveTreeQueries.cs` already does for tree logic.
|
||||
- **`Services/ResponseHelpers.cs`** — static helper with `ApiResponse`-building methods (e.g. `ResponseHelpers.Ok(string message, object? data)`) to remove the repeated `new ApiResponse { StatusCode = 200, Success = true, Message = ..., Data = ... }` block duplicated in nearly every Service method's success path. Does not modify `ApiResponse` itself (Rule 1 foundation — still off-limits without asking); this is a separate file that constructs one.
|
||||
|
||||
### 10.5 `Interfaces/` folder (Rule 4, reversed)
|
||||
|
||||
```text
|
||||
Interfaces/
|
||||
Repositories/
|
||||
IAccountRepository.cs
|
||||
ICostCenterRepository.cs
|
||||
ICurrencyRepository.cs
|
||||
IFiscalCalendarRepository.cs
|
||||
IJournalEntryRepository.cs
|
||||
IRecurringJournalTemplateRepository.cs
|
||||
ITaxRepository.cs
|
||||
IFixedAssetRepository.cs
|
||||
IBankRepository.cs
|
||||
IBudgetRepository.cs
|
||||
ISystemConfigRepository.cs
|
||||
IAuditLogRepository.cs
|
||||
Services/
|
||||
IAccountService.cs
|
||||
ICostCenterService.cs
|
||||
ICurrencyService.cs
|
||||
IFiscalCalendarService.cs
|
||||
IJournalEntryService.cs
|
||||
IRecurringTemplateService.cs
|
||||
IReportService.cs
|
||||
ITaxService.cs
|
||||
IFixedAssetService.cs
|
||||
IBankService.cs
|
||||
IBudgetService.cs
|
||||
ISystemConfigService.cs
|
||||
IAuditLogService.cs
|
||||
```
|
||||
|
||||
One interface file per repository/service **implementation file** (12 + 13 = 25 interface files), not per table — a merged file's interface covers every method for its whole cluster. Namespace matches the folder path (`General_Ledger_Service.Interfaces.Repositories` / `.Interfaces.Services`), not the implementation's own namespace — every consumer (the implementing class, any Service depending on a Repository, `Program.cs`, the Controller) needs the new `using`. `RecursiveTreeQueries.cs` and `RepositoryHelpers.cs`/`ResponseHelpers.cs`/`DepthGuard.cs` have no interface — they're internal static helpers, never injected via DI.
|
||||
|
||||
### 10.6 Controller
|
||||
|
||||
One private helper added to `GeneralLedgerController`:
|
||||
|
||||
```csharp
|
||||
private IActionResult Respond(DTOs.Common.ApiResponse response) => StatusCode(response.StatusCode, response);
|
||||
```
|
||||
|
||||
Replacing every action's `var response = await _xService.YAsync(...); return StatusCode(response.StatusCode, response);` with `return Respond(await _xService.YAsync(...));`. No route, no method signature, no behavior changes.
|
||||
|
||||
### 10.7 Net file-count effect
|
||||
|
||||
| Layer | Before | After |
|
||||
| --- | --- | --- |
|
||||
| `Models/` | 25 | 12 |
|
||||
| `Repositories/` (incl. shared helpers) | 17 | 14 |
|
||||
| `Services/` (incl. shared helpers) | 14 | 15 |
|
||||
| `Interfaces/Repositories/` + `Interfaces/Services/` | 0 | 25 (new) |
|
||||
| `Controllers/` | 1 | 1 |
|
||||
|
||||
Total source files across these layers goes from 57 to 67 — more files overall once `Interfaces/` is counted, but far fewer files carrying actual logic, and every remaining file groups a coherent domain rather than a single table.
|
||||
|
||||
---
|
||||
|
||||
## 11. Dev-Only API Key Generator (2026-07-19)
|
||||
|
||||
`ApiKeyMiddleware` (Section 3.3) checks every request against a pre-configured `ApiKey:Value` — but nothing in the original plan ever generated that value; it had to be invented by hand and typed into `appsettings.*.json` and the core backend's config separately. This adds one small endpoint to do that generation safely.
|
||||
|
||||
**Endpoint:** `POST /api/v1/dev-tools/generate-api-key`
|
||||
|
||||
**What it does:** generates a cryptographically random 256-bit key (`RandomNumberGenerator.GetBytes(32)`, hex-encoded) and returns it in the response `Data`. It does **not** write the key anywhere — appsettings, the core backend's config, and (eventually) `appsettings.Production.json`/the deploy-time secret store are all still updated by hand, the same "generate once, paste into config, done" flow already established for `Jwt:SigningKey`.
|
||||
|
||||
**Guarded two independent ways, not one:**
|
||||
|
||||
- **`DevToolsService.GenerateApiKey()` checks `IHostEnvironment.IsDevelopment()` and throws `ForbiddenException` (403) otherwise** — this is the real gate. It runs regardless of `ASPNETCORE_ENVIRONMENT`, is not a route-registration trick, and was verified live: `Hosting environment: Production` → `403 Forbidden`; `Hosting environment: Development` → the key.
|
||||
- **`ApiKeyMiddleware`'s `ExcludedPathPrefixes` gained `/api/v1/dev-tools`** — a separate, narrower concern: once a key exists and the middleware is enforcing it (as of this change, it's enabled in `Program.cs` — no longer commented out), this bootstrap endpoint has to stay reachable *without* a key, or there would be no way to ever generate the first one. This exclusion only removes the API-key requirement; it does not bypass the Development-only check above.
|
||||
|
||||
**Architecture, following the existing convention exactly:** thin Controller action (`GeneralLedgerController.GenerateApiKey`, no async work needed) → `IDevToolsService`/`DevToolsService` (new, `Interfaces/Services/IDevToolsService.cs` + `Services/DevToolsService.cs`) → returns `ApiResponse` via `ResponseHelpers.Ok`. No Repository, no Unit of Work — nothing is persisted or read from the database.
|
||||
|
||||
**Not audited** (`CLAUDE.md` Rule 10) — it doesn't touch any table; there's nothing for `audit_log` to record.
|
||||
|
||||
## 12. Unique Code Auto-Generation Retrofit (2026-07-20)
|
||||
|
||||
`Account.AccountCode`, `CostCenter.Code`, `FixedAsset.AssetCode`, and `TaxCode.Code` were originally plain fields on their `Create*Request` DTOs — client-supplied, and in `Account`'s case editable afterward via `PUT /accounts/{accountCode}`. That's inconsistent with `JournalEntry.JournalNo`, which was already system-generated and immutable from the start (`JournalEntryService.GenerateJournalNoAsync`), and it's the wrong shape for a business-key column in general: a unique code should be assigned once, by the system, and never touched again. Full rule and generation scheme now live in `CLAUDE.md` Rule 8.1 — this section is the pointer plus the reasoning for why each scheme was chosen.
|
||||
|
||||
**Why no scheme existed to just "apply from the docs":** `Accounting_System_Design_LKAS.md` declares all four columns as `VARCHAR UNIQUE NOT NULL` with no generation algorithm specified anywhere — they were designed to be populated by *something*, but never said what. The closest real precedent in the actual codebase was `JournalNo`'s `JV-{year}-{seq:D6}`.
|
||||
|
||||
**Schemes chosen** (see `CLAUDE.md` Rule 8.1 for full detail):
|
||||
|
||||
- `Account.AccountCode` — LKAS-standard type-block root numbering (Asset=1xxx, Liability=2xxx, Equity=3xxx, Income=4xxx, Expense=5xxx) + a recursive child-suffix scheme (`{parentCode}-{seq:D2}`) that scales to arbitrary depth, consistent with Section 4.2's requirement that the COA tree support unlimited depth without a schema change.
|
||||
- `CostCenter.Code` — same root/child shape as Account, minus the type blocks (cost centers have no type classification): `CC-{seq:D3}` roots, `{parentCode}-{seq:D2}` children.
|
||||
- `FixedAsset.AssetCode` — flat global sequence, `FA-{seq:D6}` (no recursion, no natural grouping to key off).
|
||||
- `TaxCode.Code` — prefixed by its parent `TaxType.Code` (fixed seed data: `VAT`, `WHT`, `NBT`, `SSCL`, `INCOME_TAX`), sequential per type: `{TaxTypeCode}-{seq:D3}`.
|
||||
|
||||
**What changed in code:** the four `Create*Request` DTOs lost their code field entirely; each Service generates the code internally before insert (new small `CountXxxAsync`-style repository methods replace the old `CodeExistsAsync`/`TaxCodeExistsAsync` uniqueness-conflict checks, which no longer have any user input to conflict with); `AccountService.UpdateAccountAsync` lost its accountCode-mutation branch entirely. No database schema change — same `VARCHAR` columns, just populated by the Service instead of the client.
|
||||
|
||||
## 13. Full-Precision Amounts, PDF-Only Rounding (2026-07-20)
|
||||
|
||||
`Accounting_System_Design_LKAS.md` specifies every monetary column as `NUMERIC(18,2)` (rate/FX columns as `NUMERIC(18,6)`). The user directed a deliberate deviation: the database no longer rounds or truncates any decimal value on storage, and no API response rounds one either — full precision flows all the way through from request body to stored row to JSON response. The only place a value is ever rounded to 2 decimal places is when rendering a PDF report, since that's the one output where a long decimal tail genuinely shouldn't be shown to a reader.
|
||||
|
||||
**Database:** every `decimal`/`decimal?` property across `AppDBContext.cs` — 16 columns total, both real monetary amounts and the rate/multiplier fields (`ExchangeRate`, `TaxCode.RatePercent`, `FixedAsset.DepreciationRatePercent`, `ExchangeRate.RateToBase`) — had its `.HasPrecision(...)` call removed entirely, so Npgsql maps the column to a bare Postgres `numeric` (unbounded precision/scale). Migration: `WidenAmountAndRateColumnsToUnboundedNumeric`.
|
||||
|
||||
**Application layer:** confirmed there was no `Math.Round`/`decimal.Round` call anywhere in the codebase before this change, so nothing needed removing there — the DB column precision was the only thing enforcing 2 decimal places.
|
||||
|
||||
**PDF rendering — the sole exception:** `ReportService.RenderPdf`'s cell formatter rounds `decimal`/`decimal?` values only, via `Math.Round(d, 2, MidpointRounding.AwayFromZero)`. `AwayFromZero` (not .NET's `ToEven` default) is required for the requested "number routing" rule — any value in `[x.xx5, x.xx5+0.01)` rounds up to the next cent (e.g. `1000.005` → `1000.01`, `1000.0574536` → `1000.06`), which is exactly round-half-up, not banker's rounding. This only produces correct results because `decimal` represents base-10 fractions like `1000.005` exactly — the same call on a `double` would be unreliable.
|
||||
|
||||
See `CLAUDE.md` Rule 13 for the full column list and the verification notes.
|
||||
@@ -0,0 +1,512 @@
|
||||
# General Ledger Service — Phase-by-Phase Development Plan
|
||||
### (Execution plan for use with Claude Code — see `CLAUDE.md` for working rules)
|
||||
|
||||
This document breaks `01_Full_Development_Plan.md` into small, sequential, independently testable phases. **Phase 1 is the only phase without an API call** (models, database, default data, API key middleware, versioning). Every phase from Phase 2 onward implements **exactly one API endpoint, end to end** (Controller → Service → Repository), so each phase can be finished, tested, and confirmed working before the next one starts.
|
||||
|
||||
**Phase numbers in this document match `01_Full_Development_Plan.md` Section 6.2 exactly, row for row.** 27 API endpoints in the original sequence (down from an original 40 — see Section 6.1 for the full consolidation history: reporting, journal-entry create+correction, fixed asset transactions, and account reads collapsed into single endpoints; the two currency-management endpoints and the FX Revaluation endpoint removed outright; reporting corrected from `POST` to `GET`), plus 6 appended list endpoints (Phases 30–35, Document 1 §6.3) and 1 appended audit-log endpoint (Phase 36, Document 1 §6.4) — **34 API endpoints, 36 phases total** (Phase 18's slot counts toward the phase total but has no endpoint — matches `01_Full_Development_Plan.md` §6.2 and `03_Progress_Tracker.md`'s counts exactly; this line previously read "28/35 endpoints," an off-by-one that didn't match either of those two documents — corrected 2026-07-19).
|
||||
|
||||
General rule for every phase from Phase 2 onward: only the Service methods and Repository methods actually needed for *that one endpoint* are written — not the full CRUD surface of the entity "while we're in there." If a later phase needs another method on the same repository, it gets added in that later phase.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Models, DbContext, Migration, Default Data, API Key Middleware, Versioning
|
||||
|
||||
**No API call in this phase.** This is the foundation every later phase depends on.
|
||||
|
||||
### 1.1 Remove the unused scaffolding
|
||||
- Delete `Interfaces/Repositories/` and `Interfaces/Services/` folders and their references in the `.csproj` (per `CLAUDE.md` rule 4).
|
||||
|
||||
### 1.2 Create entity classes in `Models/`
|
||||
|
||||
One class per table, matching `Accounting_System_Design_LKAS.md` / the ER diagram exactly (24 tables, all with `created_at`):
|
||||
|
||||
| Model | Key fields (beyond PK/created_at) |
|
||||
|---|---|
|
||||
| `AccountType` | `Id (smallint)`, `Name`, `NormalBalance` |
|
||||
| `Account` | `AccountCode`, `AccountName`, `ParentAccountId` (self FK, nullable), `AccountTypeId`, `IsControlAccount`, `IsPostable`, `CurrencyCode`, `CashFlowCategory`, `IsActive` |
|
||||
| `FiscalYear` | `YearName`, `StartDate`, `EndDate`, `IsClosed` |
|
||||
| `AccountingPeriod` | `FiscalYearId`, `PeriodNumber`, `StartDate`, `EndDate`, `Status`, `LockGl`, `LockReason` |
|
||||
| `PeriodLockLog` | `PeriodId`, `Action`, `ModuleLocked`, `Reason` |
|
||||
| `JournalEntry` | `JournalNo`, `PeriodId`, `EntryDate`, `JournalType`, `SourceModule`, `Reference`, `IsPosted`, `IsReversed`, `ReversalOfId` (self FK, nullable), `EntryStatus`, `PreparedBy` |
|
||||
| `JournalEntryLine` | `JournalId`, `AccountId`, `DebitAmount`, `CreditAmount`, `CurrencyCode`, `ExchangeRate`, `BaseDebit`, `BaseCredit`, `CostCenterId` (nullable), `Memo` |
|
||||
| `JournalCorrection` | `OriginalJournalId`, `CorrectionJournalId`, `CorrectionType`, `Reason`, `CorrectedBy` |
|
||||
| `CostCenter` | `Code`, `Name`, `ParentCostCenterId` (self FK, nullable), `IsActive` |
|
||||
| `FixedAsset` | `AssetCode`, `AssetName`, `AssetAccountId`, `AccumDeprAccountId`, `DeprExpenseAccountId`, `AcquisitionDate`, `AcquisitionCost`, `ResidualValue`, `UsefulLifeMonths`, `DepreciationMethod`, `Status` |
|
||||
| `DepreciationSchedule` | `AssetId`, `PeriodId`, `DepreciationAmount`, `AccumulatedDepreciation`, `NetBookValue`, `JournalId` (nullable) |
|
||||
| `AssetDisposal` | `AssetId`, `DisposalDate`, `Proceeds`, `NetBookValueAtDisposal`, `GainLoss`, `JournalId` (nullable) |
|
||||
| `BankAccount` | `AccountName`, `BankName`, `AccountNumber`, `GlAccountId`, `CurrencyCode` |
|
||||
| `BankStatementLine` | `BankAccountId`, `TxnDate`, `Description`, `Amount`, `IsReconciled`, `MatchedJournalId` (nullable) |
|
||||
| `TaxType` | `Code`, `Name` |
|
||||
| `TaxCode` | `TaxTypeId`, `Code`, `Name`, `RatePercent`, `CalculationBasis`, `AppliesTo`, `OutputTaxAccountId` (nullable), `InputTaxAccountId` (nullable), `EffectiveFrom`, `EffectiveTo` (nullable), `IsActive` |
|
||||
| `Currency` | `CurrencyCode (PK, char(3))`, `CurrencyName`, `Symbol`, `DecimalPlaces`, `IsBaseCurrency`, `IsActive` |
|
||||
| `ExchangeRate` | `CurrencyCode`, `RateDate`, `RateToBase`, `RateSource` |
|
||||
| `CurrencyRoundingRule` | `CurrencyCode (PK/FK)`, `RoundingMethod`, `RoundingAccountId` (nullable) |
|
||||
| `SystemConfig` | `ConfigKey (PK)`, `ConfigValue`, `Description`, `IsEditable`, `IsLocked` |
|
||||
| `Budget` | `FiscalYearId`, `Name` |
|
||||
| `BudgetLine` | `BudgetId`, `AccountId`, `PeriodId`, `CostCenterId` (nullable), `BudgetedAmount` |
|
||||
| `RecurringJournalTemplate` | `TemplateName`, `RecurrencePattern`, `NextRunDate`, `EndDate` (nullable), `IsActive` |
|
||||
| `RecurringJournalTemplateLine` | `TemplateId`, `AccountId`, `DebitAmount`, `CreditAmount` |
|
||||
| `AuditLog` | `Category`, `EntityName`, `EntityId`, `Action`, `Description` (nullable), `PerformedBy` (nullable), `OccurredAt` (no separate `CreatedAt` — see Document 1 Section 4.1). **Retrofit** — added after Phase 1 was already `☑ Done`; requires its own follow-up migration, not a rebuild of `InitialCreate` |
|
||||
|
||||
### 1.3 `AppDBContext.cs` — add `DbSet<T>` + Fluent API
|
||||
|
||||
- One `DbSet<T>` per model above.
|
||||
- `OnModelCreating`: configure the two self-referencing relationships (`Account.ParentAccountId`, `CostCenter.ParentCostCenterId`) with `DeleteBehavior.Restrict`, decimal precision (`NUMERIC(18,2)` / `NUMERIC(18,6)` for rates), and unique indexes (`AccountCode`, `JournalNo`, currency/tax codes, etc.) exactly as specified in the design document's DDL.
|
||||
- Seed static reference data via `HasData`: `AccountType` (5 rows), `SystemConfig` (3 rows — `COA_MAX_DEPTH`, `COST_CENTER_MAX_DEPTH`, `COA_ENFORCE_MAX_DEPTH`, all `IsEditable=true`/`IsLocked=false` — see `01_Full_Development_Plan.md` Section 5.2 for exact values and the corrected `IsEditable`/`IsLocked` semantics), `TaxType` (`VAT`, `WHT`, `NBT`, `SSCL`, `INCOME_TAX`), and **`Currency`** (full ISO 4217 list, 142 rows, `LKR` the sole `is_base_currency = true` row — see Section 5.3). Currency is seeded here now, not through an API call — see `01_Full_Development_Plan.md` Section 5.1, a consequence of removing the currency-management endpoints (Section 6.1).
|
||||
|
||||
### 1.4 API Key Middleware (`ApiKeyMiddleware`)
|
||||
|
||||
Every request into this service must be validated against a shared secret sent by the core backend, before it reaches any controller (see `01_Full_Development_Plan.md` Section 3.3 for full rationale):
|
||||
|
||||
- New `Middleware/ApiKeyMiddleware.cs` — reads the `X-Api-Key` header, compares it to `ApiKey:Value` from configuration; if missing/mismatched, writes a hand-built `401` response in the same shape as `ApiResponse` and short-circuits (does not call `next()`).
|
||||
- Excludes `/health` and `/swagger` paths from the check.
|
||||
- `appsettings.json` / `appsettings.Development.json` / `appsettings.Production.json` each gain an `ApiKey:Value` entry (Production stays blank, filled at deploy time — same pattern as `Jwt:SigningKey`).
|
||||
- `Program.cs` registers the middleware: `app.UseMiddleware<ApiKeyMiddleware>()`, placed after `UseHttpsRedirection` and before `UseAuthentication`.
|
||||
- Update `General_Ledger_Service.http` to define the local dev API key once as a variable, reused by every request in the file from this point forward.
|
||||
|
||||
### 1.5 API Versioning
|
||||
|
||||
- Add the `Asp.Versioning.Mvc` (10.0.0) package reference to `General-Ledgar-Service.csproj`.
|
||||
- `Program.cs`: register versioning services — `AddApiVersioning` (default version `1.0`, version reader from the URL segment).
|
||||
- Update `GeneralLedgerController` (the one existing controller) to the new versioned route convention:
|
||||
```csharp
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[ApiController]
|
||||
public class GeneralLedgerController : ControllerBase { ... }
|
||||
```
|
||||
Its existing `GET /status` endpoint moves from `/api/GeneralLedger/status` to `/api/v1/GeneralLedger/status`.
|
||||
- Every controller created from Phase 2 onward follows this same convention from the start — see the updated Full API Surface table in `01_Full_Development_Plan.md` Section 6.2, which already reflects the `v1` prefix.
|
||||
|
||||
### 1.6 Migration
|
||||
- `dotnet ef migrations add InitialCreate`
|
||||
- `dotnet ef database update` against the local PostgreSQL instance from `appsettings.Development.json`
|
||||
|
||||
### 1.7 Acceptance criteria for Phase 1
|
||||
- Solution builds with the `Interfaces/` folders removed and no dangling references.
|
||||
- Migration applies cleanly to a fresh database.
|
||||
- `SELECT * FROM account_type` returns exactly 5 rows; `SELECT * FROM system_config` returns exactly 3 rows; `SELECT * FROM tax_type` returns exactly 5 rows; `SELECT * FROM currency` returns 142 rows, exactly 1 with `is_base_currency = TRUE` (`LKR`) — corrected from an earlier "exactly 1 row" draft of this criterion; the full ISO 4217 list was seeded, not just the base currency (Section 1.8, `01_Full_Development_Plan.md` Section 5.3).
|
||||
- `GET /api/v1/GeneralLedger/status` succeeds **with** a valid `X-Api-Key` header and returns the same payload as before.
|
||||
- The same request **without** the `X-Api-Key` header (or with a wrong value) returns `401` with the `ApiResponse`-shaped error body, and never reaches the controller.
|
||||
- `GET /health` and `/swagger` still work **without** an API key.
|
||||
|
||||
### 1.8 Exact `HasData` seed values (for `AppDBContext.OnModelCreating`)
|
||||
|
||||
The row-by-row values for all four seeded tables — `account_type`, `system_config`, `tax_type`, `currency` — are specified once, in full, at `01_Full_Development_Plan.md` Sections 5.2–5.3, rather than duplicated here. That's the canonical source to copy from when writing (or checking) the actual `HasData` calls.
|
||||
|
||||
Two things worth restating here rather than only there, because they change how the `HasData` calls must be written, not just what values they contain:
|
||||
|
||||
- **`system_config`'s `is_editable`/`is_locked` semantics were corrected after the original design doc was written** (Deviations log, 2026-07-18) — `IsEditable = TRUE` means "editable exactly once via the API," not "always editable." All three seeded keys start `IsEditable = TRUE, IsLocked = FALSE`.
|
||||
- **The `Currency.DecimalPlaces` `HasData` bug** (Section 5.3) — don't configure `HasDefaultValue` on a `HasData`-seeded column where a real, meaningful value can equal that type's CLR default (`0`, `false`, `null`); EF Core's diffing silently drops it from the generated `INSERT` when both are present. This already bit `DecimalPlaces = 0` once (migration `RemoveCurrencyDecimalPlacesDefault` fixed it) and is worth checking for on any future seeded table with a similar shape.
|
||||
|
||||
**This step is a documentation formalization, not new code** — the underlying `HasData` seeding was already implemented and verified as part of Phase 1.2/1.3 (and the `currency` expansion + bugfix migrations noted in the Deviations log). If a discrepancy turns up between what's documented here and what's actually in `AppDBContext.cs`, flag it per `CLAUDE.md` Rule 0.4 rather than trusting this document over the live code.
|
||||
|
||||
### 1.9 `Company:*` in `appsettings.json` — implemented and verified live
|
||||
|
||||
Add a `Company` section to `appsettings.json` / `appsettings.Development.json` / `appsettings.Production.json`. Originally just `Company:Name` (read by the shared PDF header component, `05_LKAS_Report_PDF_Templates.md` §1, to satisfy LKAS 1.51's requirement that every statement display the reporting entity's name) — **expanded 2026-07-22** to also include `Company:Address`, `Company:Tin`, `Company:Brn`, needed only by the Income Tax Computation report's own header (`05_LKAS_Report_PDF_Templates.md` §0/§8 — a fuller identity block than the other six reports use). All four are real values in every environment's file — unlike `Jwt:SigningKey`/`ApiKey:Value`, none of this is a secret, so there's no blank-in-Production convention here; whoever deploys this fills it in once per environment. See `01_Full_Development_Plan.md`'s Current State table for the equivalent entry.
|
||||
|
||||
### 1.10 `CASH_ACCOUNT_ROOT_ID` — new `system_config` key, implemented and verified live
|
||||
|
||||
Add a fourth key to Phase 1.8's `system_config` `HasData` seed — but **not with a hardcoded value**, unlike the other three. `CASH_ACCOUNT_ROOT_ID` has to reference a real `accounts.account_id` for whichever account the company designates as its "Cash and Cash Equivalents" header (`Accounting_System_Design_LKAS.md` Section 11.5's new mechanism, needed for the Phase 17 Cash Flow retrofit) — that account doesn't exist until the company's own Chart of Accounts is created (Phase 2, an API call, not a seed). This key can't be part of `HasData` at all as a result; it has to be set via the existing `PUT /api/v1/system-config/{key}` endpoint (Phase 28) once the company's COA has a suitable header account, following the same one-time `is_editable`/`is_locked` write rule as everything else in `system_config`. **Confirm before implementing:** should Phase 1's acceptance criteria (1.7) gain a note that `CASH_ACCOUNT_ROOT_ID` is expected to remain unset until COA setup, so its absence during initial Phase 1 testing isn't mistaken for a missed seed?
|
||||
|
||||
### 1.11 `accounts.income_statement_section` — new column, implemented and verified live
|
||||
|
||||
Same recursive-tag pattern as `cash_flow_category` (already on `accounts` since Phase 1.2) — `VARCHAR(25)`, nullable, values `SALES`/`OTHER_INCOME`/`COST_OF_SALES`/`DISTRIBUTION_EXPENSE`/`ADMINISTRATION_EXPENSE`/`OTHER_EXPENSE`/`FINANCIAL_EXPENSE`/`NULL`, inherited down the COA tree exactly like `cash_flow_category`. Needed for the Phase 17 `ProfitAndLoss` retrofit (`Accounting_System_Design_LKAS.md` §11.4). No default seed value — like the tag itself, this is set per-company during COA setup (Phase 2/4), not part of `HasData`. New migration, additive column on an existing table — no data migration needed for existing rows (they'll simply be `NULL`/"Unclassified" until tagged).
|
||||
|
||||
### 1.12 `accounts.is_tax_deductible` and five new tax-related `system_config` keys — implemented and verified live
|
||||
|
||||
For the Phase 17 `TaxSummary` redesign (Income Tax Computation, `Accounting_System_Design_LKAS.md` §11.7):
|
||||
- **`accounts.is_tax_deductible`** — new column, `BOOLEAN NOT NULL DEFAULT TRUE`. Meaningful only on Expense accounts; drives "Non-Deductible Expenses." Existing rows default to `TRUE` (deductible) — the common case — so no retroactive tagging pass is required before this column can be added; a company only needs to flip specific accounts to `FALSE` if/when it has non-deductible expense categories to model.
|
||||
- **Five new `system_config` keys**, added to the Phase 1.8 `HasData` seed **where they can be** — `CORPORATE_INCOME_TAX_RATE` (`'30'`) and `STATUTORY_RELIEF_AMOUNT` (`'0'`) are plain scalar defaults and can be seeded normally. `APIT_CREDIT_ACCOUNT_ID`, `WHT_CREDIT_ACCOUNT_ID`, `QUARTERLY_TAX_PAYMENT_ACCOUNT_ID` **cannot** be `HasData`-seeded — same constraint as `CASH_ACCOUNT_ROOT_ID` (§1.10): each references a real `accounts.account_id` that doesn't exist until the company's COA is built, so all three are set later via `PUT /api/v1/system-config/{key}`, not at migration time.
|
||||
|
||||
---
|
||||
|
||||
## Phase Template (applies to every phase from here on)
|
||||
|
||||
Each phase below is documented with:
|
||||
- **Endpoint** — method + route, added as an action on the single `Controllers/GeneralLedgerController.cs` (`CLAUDE.md` Rule 7) — not a new controller class. Single-resource lookups (`GET`/`PUT`/etc. on one record) use that entity's unique code, not its numeric id, wherever it has one (`CLAUDE.md` Rule 8)
|
||||
- **Request / Response** — DTO shape; response always wrapped in `ApiResponse`, built by the Service method itself on the success path, not the Controller (`CLAUDE.md` Rule 7). For a loosely-typed body, the Controller action takes `[FromBody] Dictionary<string, JsonElement>` and passes it straight to the Service, which does the `DataExtractor` parsing itself (`CLAUDE.md` Rule 1's `Helpers` row). For query-string parameters (filters, paging), the Controller binds them into a single `[FromQuery] XyzRequest` DTO — not individual method parameters — and passes that DTO straight to the Service, which applies any defaulting itself (e.g. `page`/`pageSize`)
|
||||
- **Service method(s)** — only what's needed for this endpoint
|
||||
- **Repository method(s)** — only what's needed for this endpoint
|
||||
- **CustomExceptions used**
|
||||
- **UoW usage** — where `BeginAsync`/`CommitAsync`/`RollbackAsync` wrap the operation
|
||||
- **Audit** — whether this phase writes an `audit_log` row, and with which `category` (`CLAUDE.md` Rule 10) — `—` for every `GET`
|
||||
- **Acceptance test** — how to manually confirm the phase works
|
||||
|
||||
Three phases are written out in full below as worked templates (**Phase 2**, **Phases 11–13** covering the journal entry create/lines/post flow including the create+correction merge, and **Phase 17** covering the unified reporting endpoint — the biggest consolidation). **All remaining phases in the Full API Surface table (Document 1, Section 6.2) follow this identical structural template.**
|
||||
|
||||
---
|
||||
|
||||
## Audit Logging Convention (applies to every write phase — new, retrofit required for Phases 2, 4, 6, 7, 8)
|
||||
|
||||
Every write-phase Service method inserts one `audit_log` row as **one more repository call inside its existing UoW block** — not a separate transaction, not a different mechanism:
|
||||
|
||||
```csharp
|
||||
await _unitOfWork.BeginAsync();
|
||||
try
|
||||
{
|
||||
// ...existing repository calls for this phase's actual write...
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = "LEDGER", // or "ADMINISTRATIVE" — per Document 1 Section 4.3's table
|
||||
EntityName = "Account",
|
||||
EntityId = account.AccountId.ToString(),
|
||||
Action = "CREATE",
|
||||
Description = $"Account {account.AccountCode} created",
|
||||
PerformedBy = request.PreparedBy // free text, may be null — same pattern as JournalEntry.PreparedBy
|
||||
});
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
catch { await _unitOfWork.RollbackAsync(); throw; }
|
||||
```
|
||||
|
||||
For the two field-dependent phases (4 and 28), the `category` value is computed in the Service method before this insert, not hardcoded — see Document 1 Section 4.3's notes on those two rows.
|
||||
|
||||
**Retrofit scope.** Phases 1 (add the `audit_log` model + migration), 2, 4, 6, 7, and 8 are already `☑ Done` (`03_Progress_Tracker.md`). Adding this convention means each of those Services gains the insert above — a real, approved-first change to already-working code (`CLAUDE.md` Rule 3), not something to bundle silently into whichever phase happens to be worked on next. Phases 3, 5, and 30 are `GET`s and need no retrofit. Every phase from 9 onward (not yet started) is built with the audit insert from the start — no separate retrofit step for those.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Create Account
|
||||
|
||||
**Endpoint:** `POST /api/v1/accounts`
|
||||
|
||||
**Request DTO** (`CreateAccountRequest`) — **retrofit 2026-07-20:** `accountCode` removed, see `CLAUDE.md` Rule 8.1:
|
||||
```json
|
||||
{
|
||||
"accountName": "Rent Expense",
|
||||
"parentAccountId": 120,
|
||||
"accountTypeId": 5,
|
||||
"isControlAccount": false,
|
||||
"isPostable": true,
|
||||
"currencyCode": "LKR"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `ApiResponse` with `Data` = the created account (including generated `AccountId` and the system-generated `AccountCode`).
|
||||
|
||||
**Service — `AccountService.CreateAccountAsync`:**
|
||||
1. If `ParentAccountId` provided: call repository to load it; if not found → `NotFoundException("Parent account not found")`.
|
||||
2. Recursive depth check: walk up from the parent (via a repository method that runs the `WITH RECURSIVE ancestor_depth` query from the design doc) to compute the resulting depth; compare against `system_config.COA_MAX_DEPTH` (only if `COA_ENFORCE_MAX_DEPTH = TRUE`); if exceeded → `BadRequestException("Maximum Chart of Accounts depth exceeded")`.
|
||||
3. **(Retrofit 2026-07-20)** Generate `AccountCode` — root accounts get the next multiple of 100 in their account type's LKAS block (1000/2000/3000/4000/5000 for Asset/Liability/Equity/Income/Expense; capped at 10 roots/type); child accounts get `{parentCode}-{seq:D2}`. See `CLAUDE.md` Rule 8.1 for the full scheme. Replaces the old "duplicate `AccountCode` check" step — there's no user input left to collide with.
|
||||
4. `_unitOfWork.BeginAsync()` → `_accountRepository.AddAsync(account)` → `_unitOfWork.SaveChangesAsync()` → `_unitOfWork.CommitAsync()`; on any exception, `_unitOfWork.RollbackAsync()` and rethrow.
|
||||
|
||||
**Repository — `AccountRepository`:**
|
||||
- `Task<Account?> GetByIdAsync(long id)`
|
||||
- `Task<int> CountRootAccountsByTypeAsync(short accountTypeId)` / `Task<int> CountChildAccountsAsync(long parentAccountId)` — **retrofit 2026-07-20**, replace the removed `CodeExistsAsync`
|
||||
- `Task<int> GetAncestorDepthAsync(long parentAccountId)` — raw SQL recursive CTE
|
||||
- `Task<Account> AddAsync(Account account)`
|
||||
|
||||
**CustomExceptions used:** `NotFoundException`, `BadRequestException`
|
||||
**Helpers used:** none (strongly-typed DTO, no loose JSON parsing needed here)
|
||||
**UoW usage:** wraps the single insert (still routed through UoW for consistency even though it's one repository call, per the "UoW lives in Service" rule)
|
||||
**Audit:** one `audit_log` row, `category = "LEDGER"`, `action = "CREATE"`, in the same UoW block as the account insert (retrofit — see the Audit Logging Convention section above)
|
||||
|
||||
**Acceptance test:** `POST` a root-level account (no parent) succeeds and returns a generated `1x00`-style code; `POST` a child account gets `{parentCode}-01`; `POST` a child account at depth 7 when `COA_MAX_DEPTH=6` returns 400 with the expected message via `ApiResponse`. (As with every phase from here on, all requests include the `X-Api-Key` header — see Phase 1.4 — or they never reach the controller.)
|
||||
|
||||
---
|
||||
|
||||
## Phases 11–13 — Journal Entry Flow: Create (+ Correction), Lines, Post
|
||||
|
||||
**Retrofit 2026-07-22 (`CLAUDE.md` Rule 14) — Phases 12 and 13 are removed, absorbed into Phase 11.** These three phases were originally the worked example of the journal-entry create+correction merge (Document 1, Section 6.1): Phase 11 handled create (plain or correction), Phase 12 set lines on a still-`DRAFT` entry, Phase 13 posted it. The client (and any core backend integrating with this service) always sends a complete, ready-to-post entry in one call — there is no workflow that needs an entry to sit in `DRAFT` waiting for a follow-up call. Both `PUT` endpoints are gone; `POST /journal-entries` (below) now does everything Phases 11–13 used to do across three calls, in one. This is the same "merge to match how the endpoint is actually used" reasoning Document 1 Section 6.1 already applied to fold reverse/adjust into create — just applied one step further, to fold lines+post in too.
|
||||
|
||||
### Phase 11 — `POST /api/v1/journal-entries` (create — and post, in one call — plain or a correction)
|
||||
|
||||
**Request DTO:**
|
||||
```json
|
||||
{
|
||||
"periodId": 45,
|
||||
"entryDate": "2026-07-20",
|
||||
"journalType": "MANUAL",
|
||||
"reference": "INV-1001",
|
||||
"narration": "Rent accrual - July",
|
||||
"preparedBy": "A. Perera",
|
||||
"lines": [
|
||||
{ "accountCode": "5300", "debitAmount": 25000, "creditAmount": 0 },
|
||||
{ "accountCode": "2101", "debitAmount": 0, "creditAmount": 25000 }
|
||||
],
|
||||
"correction": null
|
||||
}
|
||||
```
|
||||
Two independent fields drive the branching (**retrofit 2026-07-22**: `accountId`/`costCenterId`/`originalJournalId` replaced with `accountCode`/`costCenterCode`/`originalJournalNo`, `CLAUDE.md` Rule 8.2):
|
||||
- `lines` (array of `{ accountCode, debitAmount, creditAmount, costCenterCode?, memo? }`) — **required** for a plain create and for a `PartialAdjustment` correction (previously optional/deferred). Ignored for a `FullReversal` correction, whose lines are auto-built by mirroring the original.
|
||||
- `correction` (optional, `{ originalJournalNo, correctionType, reason, correctedBy }`) — if present, this call creates a **correction** entry instead of a plain one. `correctionType` is `FullReversal` | `PartialAdjustment`.
|
||||
|
||||
**Service — `JournalEntryService.CreateAsync`:**
|
||||
1. Validate `PeriodId` exists (`NotFoundException` if not) — keep the loaded `AccountingPeriod` for step 6's lock check.
|
||||
2. **If `correction` is null** (plain create) or `correctionType = PartialAdjustment`: `lines` is required (`BadRequestException("lines is required")` if missing/empty); build each line via `BuildLineAsync` — resolve `accountCode`→`Account` (`GetByCodeAsync`, 404 if missing, 400 if not postable), resolve `costCenterCode`→`CostCenter` if supplied (`GetByCodeAsync`, 404 if missing).
|
||||
3. **If `correctionType = FullReversal`:** load the original entry by `originalJournalNo` (`GetJournalEntryByJournalNoAsync`); `NotFoundException` if missing; `BadRequestException` if it isn't `POSTED`; build the new entry's lines automatically by mirroring the original with debit/credit swapped.
|
||||
4. Either branch: insert the `JournalEntry` header + lines; if a correction, insert one `journal_correction` row linking `original_journal_id` → the new entry's id.
|
||||
5. Insert the `Create`/`Reverse`/`Adjust` audit row (matching the branch).
|
||||
6. **Immediately, same transaction:** balance check (`SUM(debit) == SUM(credit)`, `BadRequestException("Journal entry does not balance")` if not); period-lock check (`BadRequestException("Period is locked")` if `Status` is `LOCKED`/`CLOSED` or `LockGl = true`); set `IsPosted = true`, `EntryStatus = "POSTED"`; if `correctionType = FullReversal`, also flip the *original* entry's `EntryStatus = "REVERSED"`/`IsReversed = true` in the same transaction.
|
||||
7. Insert the `Post` audit row, plus a second `Reverse` audit row against the original's `entity_id` only when step 6's `FullReversal` branch fires.
|
||||
8. UoW wraps the entire method (steps 1–7) as one atomic write — a balance or lock failure at step 6 rolls back the header/line/correction inserts from steps 4–5 too, so nothing is left half-created.
|
||||
|
||||
**Repository additions:** `JournalEntryRepository.AddJournalEntryAsync`, `AddJournalEntryLinesAsync`, `AddJournalCorrectionAsync`, `GetJournalEntryByJournalNoAsync` (used both for the correction's original lookup and by the `GET` endpoint below); reused `AccountRepository.GetByCodeAsync`, `CostCenterRepository.GetByCodeAsync`, `FiscalCalendarRepository.GetPeriodByIdAsync`.
|
||||
**CustomExceptions used:** `NotFoundException`, `BadRequestException`
|
||||
**Audit:** two `audit_log` rows per call in the common case (`Create`/`Reverse`/`Adjust` for the write, `Post` for posting), three for a `FullReversal` (the extra `Reverse` against the original) — all in the same UoW block
|
||||
**Test:** plain create with balanced `lines` returns `isPosted:true`/`entryStatus:2` directly; missing `lines` returns 400; unbalanced `lines` returns 400 and nothing is persisted; `correction` with `FullReversal` returns an already-posted entry whose original has flipped to `REVERSED` in the same response cycle; `correction` with `PartialAdjustment` takes `lines` inline and returns already-posted, original untouched; either correction type against a non-`POSTED` (or already-`REVERSED`) original returns 400.
|
||||
|
||||
### `GET /api/v1/journal-entries/{journalNo}` — Get
|
||||
|
||||
Unchanged in shape from the original Phase 14 (`GET`, read-only) — kept here since it's the only other journal-entry endpoint left. 200 with `lines[]`, or 404 `"Journal entry not found"`. Looked up by `journalNo` (`CLAUDE.md` Rule 8), not the numeric `journalId`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 17 — Unified Reporting Endpoint (worked example of the largest consolidation)
|
||||
|
||||
**Endpoint:** `GET /api/v1/reports`
|
||||
|
||||
Corrected to `GET` (from an earlier round's `POST`) — see Document 1 Section 6.1: reporting is read-only with no side effects, and its parameters are simple scalars that fit a query string.
|
||||
|
||||
**Query parameters:**
|
||||
```
|
||||
GET /api/v1/reports?reportType=TrialBalance&outputFormat=Json&asOfDate=2026-07-31
|
||||
```
|
||||
- `reportType` (required): `GeneralLedger` | `TrialBalance` | `BalanceSheet` | `ProfitAndLoss` | `CashFlow` | `BudgetVsActual` | `TaxSummary`
|
||||
- `outputFormat` (required): `Json` | `Pdf` | `Csv` (`Csv` added 2026-07-22)
|
||||
- The remaining parameters are **optional and validated per report type** in the Service layer — e.g. `GeneralLedger` requires `periodStart`/`periodEnd` (`accountId` is optional — see the retrofit note below); `TrialBalance`/`BalanceSheet` require `asOfDate`; `ProfitAndLoss`/`CashFlow` require `periodStart`/`periodEnd`; `BudgetVsActual` requires `budgetId`; `TaxSummary` requires `periodStart`/`periodEnd` plus five new optional parameters (see the 2026-07-22 note below).
|
||||
|
||||
**Retrofit 2026-07-20 — `GeneralLedger` accountId made optional.** Originally `accountId` was required, which made this report an "Account Ledger" (one account + its recursive descendants, one running balance) rather than a true General Ledger. Now: `accountId` supplied → unchanged existing behavior; `accountId` omitted → every `is_postable = true` account with at least one posted line in the period, each with its own running balance (resets per account), sorted by `accountCode` then `entryDate` — non-postable accounts never appear in this mode. New repository method `JournalEntryRepository.GetGeneralLedgerLinesForAllAccountsAsync(periodStart, periodEnd)`, same SQL shape as the existing `GetGeneralLedgerLinesAsync` minus the descendants CTE/accountId filter, plus `AND a.is_postable = TRUE`. Full detail: `Accounting_System_Design_LKAS.md` §11.1, `04_API_Reference_And_Scenarios.md` Reporting module.
|
||||
|
||||
**Spec 2026-07-21 — `CashFlow` upgraded to full LKAS 7.** The response shape changes from a flat 6-field object to a nested one: `operatingActivities` (profit + non-cash add-backs from `depreciation_schedule` + working-capital changes from `cash_flow_category = 'OPERATING'` accounts), `investingActivities`/`financingActivities` (now itemized `lines[]`, not a single aggregate each), plus `openingCashBalance`/`closingCashBalance`. Requires one new `system_config` key, `CASH_ACCOUNT_ROOT_ID` (§1.10 below), to identify which accounts are cash-like — full query design in `Accounting_System_Design_LKAS.md` §11.5. New repository methods: `AccountRepository.GetCashFlowWorkingCapitalChangesAsync`, `GetCashFlowCategoryLinesAsync`, `GetCashBalanceAsync(asOfDate)`, plus a read of `depreciation_schedule` for the add-back.
|
||||
|
||||
**Spec 2026-07-21 — `TaxSummary`, a new 7th `reportType`, original design.** `periodStart`/`periodEnd` required. A practical VAT/WHT/NBT return-preparation summary, one row per active `tax_code`, per `Accounting_System_Design_LKAS.md` §11.7†'s query — **superseded 2026-07-22, see below.**
|
||||
|
||||
**Spec 2026-07-21 — every PDF report renders on A4.** `page.Size(PageSizes.A4)`, portrait by default, standard 2cm margins. Landscape for wider tables per `05_LKAS_Report_PDF_Templates.md` Section 1 (updated 2026-07-22 — Trial Balance moved back to portrait once its layout flattened; General Ledger stays landscape).
|
||||
|
||||
**All four of the above were implemented and verified live 2026-07-21** (`03_Progress_Tracker.md`). **Four are reworked again as of 2026-07-22**, once actual reference templates became available — the notes immediately below describe the current target; the four notes above are kept as the historical record of what was actually built and tested first, not deleted:
|
||||
|
||||
**Spec 2026-07-22 — `outputFormat=Csv` added, a third format alongside `Json`/`Pdf`. Implemented and verified live.** Works for every `reportType`. Same `ApiResponse`-wrapped base64 convention as `Pdf`: `data: { fileName, contentType: "text/csv", contentBase64 }`. Per-report column layout and flattening rules: `05_LKAS_Report_PDF_Templates.md` §10. New `Services/CsvDocuments/` folder (`CsvBuilder` shared helper + one writer class per report, parallel structure to `Services/PdfDocuments/`) — verified live, valid CSV bytes for all seven report types.
|
||||
|
||||
**Spec 2026-07-22 — `TrialBalance` flattened. Implemented and verified live.** Response drops `depth`/`indentedCode`; becomes `{ accountCode, accountName, debit, credit }`. Underlying query/filtering unchanged — presentation-only; `AccountRepository.GetTrialBalanceAsync` no longer needs the recursive tree at all. `Accounting_System_Design_LKAS.md` §11.2 unaffected; `04_API_Reference_And_Scenarios.md` and `05_LKAS_Report_PDF_Templates.md` §2 updated.
|
||||
|
||||
**Spec 2026-07-22 — `ProfitAndLoss` restructured around the new `accounts.income_statement_section` column. Implemented and verified live.** Response is a nested object with `sales`/`costOfSales`/`grossProfit`/`otherIncome`/`distributionExpenses`/`administrationExpenses`/`otherExpenses`/`financialExpenses`/`unclassified`/`netProfitForPeriod` — replacing the flat `{depth, lineItem, accountType, amount}` array entirely. New migration for the column (same recursive-tag pattern as `cash_flow_category`); `AccountRepository.GetProfitAndLossLeafRowsAsync` returns raw leaf rows tagged with the inherited section, `ReportService.BuildProfitAndLossReportAsync` groups them into the response and computes `grossProfit`/`netProfitForPeriod`. **Real sign bug found and fixed in the process:** the pre-existing P&L query summed Expense accounts as `credit − debit` with no flip — negative for a normal debit-balance expense — silently corrupting `profitForPeriod` (reused by `BalanceSheet`'s Current Year Earnings and `CashFlow`) before this retrofit. Verified live: `Sales(730,000) − CostOfSales(80,000) = GrossProfit(650,000)` exactly.
|
||||
|
||||
**Spec 2026-07-22 — `CashFlow` display simplified, `direction` added to `workingCapitalChanges[]`. Implemented and verified live.** No opening/closing cash balance section printed in `Pdf`/`Csv` output (still computed and returned in `Json`/`Csv` data for the validation cross-check, per `Accounting_System_Design_LKAS.md` §11.5); `nonCashAdjustments`/`workingCapitalChanges` combined and re-bucketed by sign into "Additions to Cash"/"Subtractions From Cash" at render time. `direction` (`"Increase"`/`"Decrease"`) added to each `workingCapitalChanges` entry, computed alongside the existing sign-flipped `changeAmount` in `GetCashFlowWorkingCapitalChangesAsync`'s SQL.
|
||||
|
||||
**Spec 2026-07-22 — `TaxSummary` redesigned wholesale from VAT/WHT/NBT summary into Income Tax Computation. Implemented and verified live.** A user-supplied reference template made clear the originally-built report (one row per `tax_code`) wasn't what "Tax Report" needed to be — the actual requirement is a single Profit-Before-Tax-to-Balance-Tax-Payable computation. This replaces `TaxSummary`'s content entirely, under the same `reportType` name (the superseded design is kept in `Accounting_System_Design_LKAS.md` §11.7† for reference — it's still a real, useful report, just not this one). Built: `accounts.is_tax_deductible` column (boolean, default `TRUE`); five new `system_config` keys (`CORPORATE_INCOME_TAX_RATE`, `STATUTORY_RELIEF_AMOUNT`, `APIT_CREDIT_ACCOUNT_ID`, `WHT_CREDIT_ACCOUNT_ID`, `QUARTERLY_TAX_PAYMENT_ACCOUNT_ID` — the latter three following the same can't-be-`HasData`-seeded, set-later-via-`PUT`-`system-config` constraint as `CASH_ACCOUNT_ROOT_ID`); four new optional request parameters (`allowableDeductions`, `otherTaxableIncome`, `qualifyingPaymentsReliefs`, `surchargeAmount`) plus optional `taxRateOverride`. New repository methods `ITaxRepository.GetNonDeductibleExpensesAsync`/`GetAccountCreditActivityAsync` (the latter reused three times, once per credit account), replacing `GetTaxSummaryAsync` (removed, not kept alongside). **Actual method naming, differing slightly from this section's original plan:** the dispatch/builder method kept its original name, `ReportService.BuildTaxSummaryAsync` (not renamed to `GetIncomeTaxComputationAsync`/`BuildIncomeTaxComputationAsync`) — a cosmetic naming choice with no functional difference, documented here so this doc matches the actual code rather than a planned name that was never applied. Full design: `Accounting_System_Design_LKAS.md` §11.7. Verified live with both default config values and full parameter overrides.
|
||||
|
||||
**Response:** `ApiResponse` with `Data` = the report payload (a report-specific JSON shape) when `outputFormat=Json`, or a PDF byte response when `outputFormat=Pdf` (exact binary-response mechanics decided during this phase).
|
||||
|
||||
**Service — `ReportService.GenerateAsync`:**
|
||||
1. Validate that the parameters required by the given `reportType` are present (`BadRequestException` naming the missing parameter if not).
|
||||
2. `switch` on `reportType`, dispatching to one internal method per report — `BuildGeneralLedgerAsync`, `BuildTrialBalanceAsync`, `BuildBalanceSheetAsync`, `BuildProfitAndLossAsync` (thin wrapper over the shared `BuildProfitAndLossReportAsync`, also reused by `BuildBalanceSheetAsync`'s Current Year Earnings and `BuildCashFlowAsync`'s `profitForPeriod`), `BuildCashFlowAsync`, `BuildBudgetVsActualAsync`, `BuildTaxSummaryAsync` (kept its original name — see the Income Tax Computation retrofit note above) — each running the corresponding recursive-CTE query from `Accounting_System_Design_LKAS.md` Section 11/12.
|
||||
3. If `outputFormat=Pdf`, pass the JSON result through a `QuestPDF` rendering step; if `outputFormat=Csv`, through the parallel `Services/CsvDocuments/` writer. **Implemented and verified live** — the original single shared reflection-based table renderer was replaced with seven bespoke `IDocument` layouts (`Services/PdfDocuments/`) sharing one header/footer static helper (`ReportPageHeader`), plus seven parallel CSV writers (`Services/CsvDocuments/`, `CsvBuilder` shared helper).
|
||||
4. No UoW — every branch is read-only.
|
||||
|
||||
**Repository — one method per report, added to whichever repository owns that data** (e.g. `AccountRepository.GetTrialBalanceAsync`/`GetProfitAndLossLeafRowsAsync`, `JournalEntryRepository.GetGeneralLedgerLinesAsync`, `BudgetRepository.GetBudgetVsActualAsync`, `ITaxRepository.GetNonDeductibleExpensesAsync`/`GetAccountCreditActivityAsync`).
|
||||
|
||||
**CustomExceptions used:** `NotFoundException` (e.g. `accountId` or `budgetId` doesn't exist), `BadRequestException` (missing required parameter for the given `reportType`)
|
||||
**Helpers used:** none
|
||||
**UoW usage:** none — read-only
|
||||
**Audit:** none — `GET`, nothing to log
|
||||
|
||||
**Acceptance test:** each of the seven `reportType` values returns the expected shape with `outputFormat=Json`; `TrialBalance` with `outputFormat=Pdf` returns a valid PDF **sized A4**; `outputFormat=Csv` returns a valid CSV for every report (header row present, correct per-report column set per `05_LKAS_Report_PDF_Templates.md` §10); an unsupported/missing `reportType` or `outputFormat` returns 400; `GeneralLedger` without `periodStart`/`periodEnd` returns 400 naming the missing parameter. **Retrofit 2026-07-20 (GeneralLedger):** `GeneralLedger` with `accountId` supplied still returns exactly that account's subtree with one running balance (unchanged); `GeneralLedger` with `accountId` omitted returns every postable account's lines together, each with its own running balance that resets when the account changes, sorted by `accountCode` then `entryDate`; and no row in the no-`accountId` response ever carries the `accountCode` of a non-postable header/summary account. **Retrofit 2026-07-20 (BalanceSheet):** summing the response's `balance` values where `accountType` is `"Asset"` must equal the sum where `accountType` is `"Liability"` plus `"Equity"` (including the synthetic `"Current Year Earnings"` row) — required pass condition, not just "renders without error." **Spec 2026-07-22 (ProfitAndLoss), verified live:** `grossProfit` equals `sales.total − costOfSales.total` and `netProfitForPeriod` equals `grossProfit + otherIncome.total − (distributionExpenses.total + administrationExpenses.total + otherExpenses.total + financialExpenses.total) + unclassified.total`, both confirmed computed independently against a real seeded dataset (`650,000` and `-1,875,000` respectively). **Spec 2026-07-22 (CashFlow), verified live:** `closingCashBalance` equals `openingCashBalance + netIncreaseDecreaseInCash` computed independently on a cleanly-tagged test dataset (unchanged pass condition from the 2026-07-21 spec); every `workingCapitalChanges[]` entry's `direction` matches the sign of its own raw balance movement (not `changeAmount`'s sign, which can differ — see `Accounting_System_Design_LKAS.md` §11.5), confirmed live (`"Trade Receivables"`/`direction: "Increase"`/`changeAmount: -106,200` — an asset increase that consumes cash, opposite-signed from a liability increase also labelled `"Increase"`). **Spec 2026-07-22 (TaxSummary/Income Tax Computation), verified live:** `balanceTaxPayable` equals `grossTaxLiability − apitCredit − whtCredit − quarterlyTaxPayments` computed independently; a request with all four optional adjustment parameters omitted succeeds, defaulting each to `0` (or `qualifyingPaymentsReliefs`/the tax rate to their `system_config` defaults) rather than erroring as if they were required — confirmed with both the all-defaults path and a full-override request (`allowableDeductions`/`otherTaxableIncome`/`taxRateOverride` all confirmed flowing through the computation correctly).
|
||||
|
||||
---
|
||||
|
||||
## Phase 18 — FX Revaluation — ✕ Removed (2026-07-18), not implemented
|
||||
|
||||
**Endpoint:** ~~`POST /api/v1/fx-revaluation`~~ — removed from the API surface, this phase's slot is not reused (Document 1 Section 6.2, "no renumbering, ever").
|
||||
|
||||
This phase was originally kept in the plan but flagged as an open dependency question, because removing the currency-management endpoints (Document 1, Section 6.1) left it with no API-driven way to add a non-base currency or an exchange rate — both of which it needs in order to have anything to revalue. Two resolution options were on the table:
|
||||
|
||||
- Option A: seed currency/rate data directly into the database outside the API whenever multi-currency is actually needed, and implement this endpoint as originally planned.
|
||||
- Option B: remove the endpoint entirely, since it has no practical way to be exercised without A.
|
||||
|
||||
**Resolved 2026-07-18 — Option B, definitively rather than provisionally.** The user confirmed that the base/functional currency is a one-time initial-configuration choice (set once, then permanent — the client can never change it afterward), 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, Option A's premise (seed currency/rate data "whenever multi-currency is actually needed") never applies — there is no "whenever." The endpoint has no possible purpose, not a temporarily-missing one. Removed outright; do not implement.
|
||||
|
||||
---
|
||||
|
||||
## Remaining Phases (3–10, 14–16, 19–29)
|
||||
|
||||
Every remaining row from the Full API Surface table (Document 1, Section 6.2) is its own phase, using the identical pattern demonstrated above.
|
||||
|
||||
| Phase | Endpoint | Key business rules enforced in Service | New Exceptions beyond the standard set | Audit | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| 3 | `GET /api/v1/accounts/{accountCode}` | none (read-only); `?expand=tree` includes the recursive descendant tree in the same response | `NotFoundException` if code missing | — | Two response shapes from one handler. Looked up by `accountCode`, not id (`CLAUDE.md` Rule 8) |
|
||||
| 4 | `PUT /api/v1/accounts/{accountCode}` | general field update; if `parentAccountId` is present **and** different from the current value, runs the same cycle check + depth check as Phase 2's create | `BadRequestException`, `NotFoundException` | `LEDGER` if `parentAccountId` changed, else `ADMINISTRATIVE` (Document 1 §4.3) — **retrofit** | Replaces the old reparent-only endpoint. Looked up by `accountCode`, not id; `parentAccountId` in the body stays numeric (`CLAUDE.md` Rule 8). `accountCode` cannot be changed via this endpoint — codes are immutable once generated (`CLAUDE.md` Rule 8.1 — **retrofit 2026-07-20**) |
|
||||
| 5 | `GET /api/v1/accounts` | none | — | — | Filter/paging params |
|
||||
| 6 | `POST /api/v1/fiscal-years` | overlapping date range check; optional `autoGeneratePeriods` flag internally calls the same logic as Phase 7 | `BadRequestException` | `LEDGER` — **retrofit** | Cannot be replaced by `system_config` — see Document 1 Section 6.2 row 6's justification |
|
||||
| 7 | `POST /api/v1/fiscal-years/{id}/periods` | generates N periods spanning the fiscal year, no gaps/overlaps | `BadRequestException` | `LEDGER` — **retrofit** | Also called internally by Phase 6 when `autoGeneratePeriods = true` |
|
||||
| 8 | `PUT /api/v1/periods/{id}/lock` | writes `period_lock_log` row with reason; enforces `CLOSED` periods can't be reopened without explicit `UNLOCK` action | `BadRequestException` | `LEDGER` (secondary to the existing `period_lock_log` row — Document 1 §4.3) — **retrofit** | |
|
||||
| 9 | `POST /api/v1/cost-centers` | same depth-guard pattern as Phase 2, against `COST_CENTER_MAX_DEPTH` | `BadRequestException`, `NotFoundException` | `ADMINISTRATIVE` | `Code` is system-generated (`CC-{seq:D3}` root / `{parentCode}-{seq:D2}` child), not accepted in the request body (`CLAUDE.md` Rule 8.1 — **retrofit 2026-07-20**) |
|
||||
| 10 | `GET /api/v1/cost-centers/{costCenterCode}` | none; `?expand=tree` includes the recursive descendant tree | `NotFoundException` | — | Same read-merge pattern as Phase 3. Looked up by `costCenterCode`, not id (`CLAUDE.md` Rule 8) |
|
||||
| 14 | `GET /api/v1/journal-entries/{journalNo}` | none | `NotFoundException` | — | Looked up by `journalNo`, not id (`CLAUDE.md` Rule 8) |
|
||||
| 15 | `POST /api/v1/recurring-templates` | balance check on template lines at creation time | `BadRequestException` | `ADMINISTRATIVE` | |
|
||||
| 16 | `POST /api/v1/recurring-templates/{id}/generate` | only fires if `NextRunDate <= today`; creates a `DRAFT` journal entry (reuses Phase 11's plain-create service branch internally) | `BadRequestException` | `LEDGER` | |
|
||||
| 19 | `POST /api/v1/tax-codes` | `effective_from` required; overlapping `effective_from`/`effective_to` for the same `code` rejected | `BadRequestException` | `LEDGER` | `Code` is system-generated (`{TaxType.Code}-{seq:D3}`), not accepted in the request body (`CLAUDE.md` Rule 8.1 — **retrofit 2026-07-20**) |
|
||||
| 20 | `GET /api/v1/tax-codes/calculate` | none (pure calculation, no persistence) | `NotFoundException` if tax code missing | — | No UoW |
|
||||
| 21 | `POST /api/v1/fixed-assets` | asset/accum-depreciation/depreciation-expense accounts must exist and be postable | `BadRequestException`, `NotFoundException` | `LEDGER` | `AssetCode` is system-generated (`FA-{seq:D6}`), not accepted in the request body (`CLAUDE.md` Rule 8.1 — **retrofit 2026-07-20**) |
|
||||
| 22 | `POST /api/v1/fixed-assets/{assetCode}/transactions` | `transactionType: Depreciate` — period must be open, posts Debit Depr. Expense / Credit Accum. Depreciation; `transactionType: Dispose` — computes gain/loss vs net book value, posts the disposal entry | `BadRequestException` | `LEDGER` | Multi-repository UoW, branches by `transactionType` the same way Phase 11 branches by `correction`. Looked up by `assetCode`, not id (`CLAUDE.md` Rule 8) |
|
||||
| 23 | `POST /api/v1/bank-accounts` | GL account must exist and be postable; `accountType` must be `CASH` or `BANK`; `bankName`/`accountNumber` required when `BANK`, forbidden when `CASH` (retrofit 2026-07-22 — see below) | `BadRequestException`, `NotFoundException` | `ADMINISTRATIVE` | |
|
||||
| 24 | `POST /api/v1/bank-accounts/{id}/statement-lines/import` | parses uploaded `.xlsx` via `ClosedXML` | `BadRequestException` on malformed file | `ADMINISTRATIVE` | Uses `Helpers/DataExtractor` if the row-parsing needs loose typing per cell |
|
||||
| 25 | `PUT /api/v1/bank-statement-lines/{id}/reconcile` | statement line and journal entry amounts must match | `BadRequestException`, `NotFoundException` | `ADMINISTRATIVE` | |
|
||||
| 26 | `POST /api/v1/budgets` | fiscal year must exist | `NotFoundException` | `ADMINISTRATIVE` | |
|
||||
| 27 | `POST /api/v1/budgets/{id}/lines` | account/period/cost-center must exist | `BadRequestException`, `NotFoundException` | `ADMINISTRATIVE` | |
|
||||
| 28 | `PUT /api/v1/system-config/{key}` | enforces `is_editable`/`is_locked`: if `is_editable=false` and `is_locked=true`, reject | `BadRequestException`, `NotFoundException` | `LEDGER` (per current keys; classified per-key — Document 1 §4.3) | On the one allowed write for a one-time config, flips `is_locked` to `true` in the same transaction |
|
||||
| 29 | `GET /api/v1/system-config` | none | — | — | |
|
||||
|
||||
**Spec 2026-07-22 — Phases 23/34 retrofit: `accountType` discriminator for Cash vs. Bank (not yet implemented).** A frontend Cash/Bank management screen surfaced a real gap: `bank_account` had nullable `bankName`/`accountNumber` from the start, but nothing distinguished "an actual bank account" from "this represents physical cash" — and there was no way to *restrict* the create flow to exactly those two concepts (as opposed to, say, letting someone create a row that's neither). New required request field `accountType` (`CASH` | `BANK`), enforced both at the API layer (`BadRequestException` on the wrong field combination — see `04_API_Reference_And_Scenarios.md`'s updated `POST /bank-accounts` cases) and at the database layer (`CHECK` constraint, `Accounting_System_Design_LKAS.md` §5.8 — defense in depth, not just a Service-layer rule). `BankService.CreateBankAccountAsync` gains the `accountType` validation as its first step, before the existing GL-account-exists/postable checks. **Table and endpoint names stay `bank_account`/`bank-accounts`** — same "don't rename an already-built, tested thing for a purely cosmetic gain" reasoning already applied to `TaxSummary` (Section 6.1). `03_Progress_Tracker.md` Phases 23/34 reopened for this retrofit.
|
||||
|
||||
## Phases 30–35 — Appended read/list endpoints (Document 1, Section 6.3)
|
||||
|
||||
User-identified gap (2026-07-18): Tax, Fixed Assets, Bank, and Budget are write-only in the plan above — no phase lists or reads back what those endpoints create. Appended rather than inserted, per the "no renumbering" phase-numbering policy.
|
||||
|
||||
| Phase | Endpoint | Key business rules enforced in Service | New Exceptions beyond the standard set | Audit | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| 30 | `GET /api/v1/currencies` | none (read-only) | — | — | No UoW. Implemented immediately — `Currency` already has ~140 seeded rows and no create-flow to wait for |
|
||||
| 31 | `GET /api/v1/tax-types` | none (read-only) | — | — | No UoW. Same shape as Phase 30 — pure `HasData` reference table |
|
||||
| 32 | `GET /api/v1/tax-codes` | none (read-only) | — | — | No UoW. Implement alongside Phase 19 |
|
||||
| 33 | `GET /api/v1/fixed-assets` | none (read-only) | — | — | No UoW. Implement alongside Phase 21 |
|
||||
| 34 | `GET /api/v1/bank-accounts` | none (read-only) — response gains `accountType` per row, retrofit 2026-07-22 | — | — | No UoW. Implement alongside Phase 23 |
|
||||
| 35 | `GET /api/v1/budgets` | none (read-only) | — | — | No UoW. Implement alongside Phase 26 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 36 — Audit Log List Endpoint (appended, filterable by category)
|
||||
|
||||
**Endpoint:** `GET /api/v1/audit-log`
|
||||
|
||||
**Query parameters** (bound into a single `[FromQuery] ListAuditLogRequest`, per the Phase Template's query-DTO convention):
|
||||
```
|
||||
GET /api/v1/audit-log?category=LEDGER&category=ADMINISTRATIVE&entityName=Account&action=CREATE&performedBy=A.Perera&from=2026-07-01&to=2026-07-31&page=1&pageSize=50
|
||||
```
|
||||
- `category` (optional, repeatable — `List<string>?`): `LEDGER`, `ADMINISTRATIVE`, both explicitly, or omitted entirely for both. Plain ASP.NET Core array query-string binding — no custom parsing.
|
||||
- `entityName`, `action`, `performedBy` (all optional, exact match)
|
||||
- `from`, `to` (optional, filters `occurred_at`)
|
||||
- `page`, `pageSize` (paging, same defaulting pattern as Phase 5's `ListAccountsRequest`)
|
||||
|
||||
**Response:** `ApiResponse` with `Data` = a paged list of `audit_log` rows.
|
||||
|
||||
**Service — `AuditLogService.ListAsync(ListAuditLogRequest request)`:**
|
||||
1. Default `category` to both values if the list is null/empty; otherwise use exactly what was supplied.
|
||||
2. Build the filter set (category list, optional entityName/action/performedBy/date-range) and pass to the repository.
|
||||
3. Apply `page`/`pageSize` defaulting (same as Phase 5).
|
||||
4. No UoW — read-only.
|
||||
|
||||
**Repository — `AuditLogRepository.ListAsync(AuditLogFilter filter)`:** one method, dynamic `WHERE` built from whichever filter fields are non-null — same shape as `AccountRepository`'s Phase 5 list method, just more filter dimensions.
|
||||
|
||||
**CustomExceptions used:** none expected (an invalid `category`/`action` string is simply ignored by the filter rather than rejected, since this is a read — a typo'd filter value just returns an empty result set, not an error)
|
||||
**Helpers used:** none
|
||||
**UoW usage:** none — read-only
|
||||
**Audit:** none — `GET`, nothing to log
|
||||
|
||||
**Acceptance test:** no filters returns everything (both categories, paginated); `?category=LEDGER` returns only `LEDGER` rows; `?category=LEDGER&category=ADMINISTRATIVE` returns the same as no filter; `?entityName=Account&action=CREATE` narrows correctly; a date range excludes rows outside it; an empty result set (e.g. a `performedBy` that matches nothing) returns `200` with an empty list, not an error. This phase's own test data depends on at least one write-phase's audit retrofit already being in place — verify against Phase 2's retrofitted `POST /accounts` first.
|
||||
|
||||
---
|
||||
|
||||
## Phase 37 — Cheque Management (new module, appended 2026-07-30, beyond the original plan)
|
||||
|
||||
User-supplied field spec (not from Document 1/Document 2 originally) for three tables: `ChequeBook`, `ChequePage` (cheques issued from this company's own cheque books), and `ReceivedCheque` (cheques received from others — deliberately no `ChequeBook` link, per the spec). Appended per the "no renumbering" policy, same as Phases 30–36.
|
||||
|
||||
**Design decisions resolved via direct clarification before implementation (`CLAUDE.md` Rule 3):**
|
||||
1. **No automatic GL postings.** Every write here is operational tracking only — `ChequePage`/`ReceivedCheque` store caller-supplied references (`voucherId`/`referenceNo`, `referenceType`/`referenceId`); the caller posts the real journal entry separately via `POST /journal-entries`. Same relationship as `PUT /bank-statement-lines/{id}/reconcile` and `matchedJournalNo`.
|
||||
2. **Cheque pages auto-generate.** `POST /cheque-books` builds all `totalLeaves` `ChequePage` rows (`Unused`) immediately from the numeric `startChequeNo`–`endChequeNo` range — same idea as Phase 7's period auto-generation.
|
||||
3. **Company/Branch, asymmetric per user direction:** `ChequeBook` gets `branchId` (required), no `companyId`; `ReceivedCheque` gets `companyId` (required), `branchId` (nullable). Neither is a FK — no `Company`/`Branch` table exists anywhere in this system.
|
||||
|
||||
**Deviations from the client's raw field spec** (each forced by something absent from this codebase, not stylistic): `PayeeId`/`ReceivedFromId`/`ReferenceId`/`VoucherId` are loose `long?` references with no FK (no Supplier/Customer/Employee/Voucher tables exist here); `BankId`/`BankBranch` on `ReceivedCheque` became free-text `DrawerBankName`/`DrawerBankBranch` (no bank-master table — same precedent as `BankAccount.BankName`); `ChequeBookNo`/`StartChequeNo`/`EndChequeNo` stay caller-supplied (printed by the actual bank, unlike `AccountCode`'s system-generated scheme); `CreatedBy`/`ModifiedBy`/`PrintedBy` are free-text `string?` (no user table); `CurrencyId` → `CurrencyCode` (Rule 8.2 convention); `BankAccountId`/`DepositBankAccountId` stay numeric (`BankAccount` has no unique code column, Rule 8.2's carve-out); `Amount` columns have no `HasPrecision` (Rule 13, unbounded).
|
||||
|
||||
| Phase | Endpoint | Key business rules enforced in Service | New Exceptions beyond the standard set | Audit | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| 37.1 | `POST /api/v1/cheque-books` | Bank account must exist; `chequeBookNo` unique; `startChequeNo`/`endChequeNo` numeric and consistent with `totalLeaves`; auto-generates all leaves | `BadRequestException` (range mismatch, non-numeric, duplicate book no) | `Administrative`/`Create` | `BuildChequeNoRange` preserves `startChequeNo`'s digit width (leading zeros) |
|
||||
| 37.2 | `GET /api/v1/cheque-books/{chequeBookNo}` | none (read-only) | — | — | `?expand=pages` includes the generated leaves |
|
||||
| 37.3 | `GET /api/v1/cheque-books` | none (read-only) | — | — | Filters: `bankAccountId`, `branchId`, `status` |
|
||||
| 37.4 | `PUT /api/v1/cheque-pages/{chequeNo}/issue` | Page must be `Unused`; `payeeName` required; `amount > 0`; `payeeType` parsed from string (no global `JsonStringEnumConverter` in this project — same pattern as `recurrencePattern`) | `BadRequestException` (wrong status, invalid `payeeType`, non-positive amount) | `Administrative`/`Update` | `Unused` → `Issued` only |
|
||||
| 37.5 | `PUT /api/v1/cheque-pages/{chequeNo}/status` | Action-based (`Clear`/`Bounce`/`Cancel`/`Void`), each with its own valid-from-status guard, same pattern as `PUT /periods/{id}/lock` | `BadRequestException` (invalid action, invalid transition) | `Administrative`/`Update` | `Clear` requires `clearedDate`; `Cancel` requires `cancelReason` |
|
||||
| 37.6 | `GET /api/v1/cheque-pages/{chequeNo}` | none (read-only) | — | — | Includes parent `chequeBook` |
|
||||
| 37.7 | `GET /api/v1/cheque-pages` | none (read-only) | — | — | Filters: `chequeBookNo`, `issueStatus`, `payeeType`, `payeeId` |
|
||||
| 37.8 | `POST /api/v1/received-cheques` | `chequeNo`/`receivedFromName` required; `amount > 0`; `receivedFromType` parsed from string | `BadRequestException` | `Administrative`/`Create` | No `ChequeBook` link, per spec; status defaults `Received` |
|
||||
| 37.9 | `PUT /api/v1/received-cheques/{id}/status` | Action-based (`Deposit`/`Clear`/`Return`/`Cancel`); `Deposit` validates `depositBankAccountId` against a real bank account | `BadRequestException`/`NotFoundException` | `Administrative`/`Update` | `Deposit` requires `depositBankAccountId` + `depositDate` |
|
||||
| 37.10 | `GET /api/v1/received-cheques/{id}` | none (read-only) | — | — | |
|
||||
| 37.11 | `GET /api/v1/received-cheques` | none (read-only) | — | — | Filters: `companyId`, `branchId`, `status`, `receivedFromType` |
|
||||
|
||||
**CustomExceptions used:** `NotFoundException` (bank account, cheque book/page, received cheque not found), `BadRequestException` (every validation/status-guard case above).
|
||||
**Helpers used:** `DataExtractor` for the two action-based `Dictionary<string, JsonElement>` payloads (37.5, 37.9), same convention as `PUT /periods/{id}/lock`.
|
||||
**UoW usage:** every write phase (37.1, 37.4, 37.5, 37.8, 37.9) wraps its work in `BeginAsync`/`SaveChangesAsync`/`CommitAsync` with `RollbackAsync` on failure; all reads (37.2, 37.3, 37.6, 37.7, 37.10, 37.11) skip UoW entirely.
|
||||
**Audit:** every write logs one `Administrative` row (cheque management never touches recorded ledger balances directly, so nothing here qualifies as `Ledger`).
|
||||
|
||||
**Acceptance test, verified live 2026-07-30:** created a 10-leaf cheque book → 10 `Unused` pages generated; issued two pages with different `payeeType`s; `Clear`ed one `Issued` page, `Bounce`d another, `Cancel`led an `Unused` page, `Void`ed another `Unused` page; confirmed `Clear` on a still-`Unused` page returns 400 with the current-status message; created a received cheque, `Deposit`ed it against a real bank account, then `Clear`ed it; confirmed duplicate `chequeBookNo`, mismatched `totalLeaves`, and a nonexistent `bankAccountId` all fail with the documented error. Full detail: `03_Progress_Tracker.md`'s 2026-07-30 entry; full request/response reference: `04_API_Reference_And_Scenarios.md`'s "Module: Cheque Management".
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Document With Claude Code
|
||||
|
||||
1. Work phases **strictly in order**. Do not start a phase until the previous one has been manually confirmed working (Section 7.2 of the Full Development Plan). **Exception: Phase 18 (FX Revaluation) is removed (2026-07-18, resolved — see its section above) — skip it permanently and continue with Phase 19 onward; it will not come back.**
|
||||
2. At the start of each phase, Claude Code should restate: which endpoint (including its `v1` route), which Service/Repository methods will be added, and which of `CustomException` / `Helpers` / `UnitOfWork` / `ApiResponse` will be touched — then wait for approval before writing code (`CLAUDE.md` rule 3).
|
||||
3. Every manual test, from Phase 1's acceptance check onward, includes the `X-Api-Key` header (Phase 1.4) — a request without it never reaches the controller being tested, regardless of which phase is being verified.
|
||||
4. For the consolidated endpoints (Phases 3, 4, 10, 11, 12, 13, 17, 22), the acceptance test must exercise **every branch of the discriminator**, not just one — e.g. Phase 11 isn't done until a plain create, a `FullReversal` correction, and a `PartialAdjustment` correction have all been tested; Phase 13 isn't done until posting a plain entry, a reversal, and an adjustment have all been tested.
|
||||
5. If a phase's implementation reveals that an earlier phase needs a change, that change is proposed and approved as part of the *current* phase — earlier phases are not silently revisited.
|
||||
6. The `audit_log` retrofit (models + Phases 2, 4, 6, 7, 8 — see "Audit Logging Convention" above) is its own explicitly-approved unit of work, not something to fold silently into whichever phase happens to be next. Once approved, do the model/migration retrofit first, then the five write-phase retrofits, then proceed to Phase 9 as normal — Phase 36 itself can be built and smoke-tested any time after the model/migration retrofit, independent of that sequencing.
|
||||
|
||||
---
|
||||
|
||||
## Refactor Phases R1–R6 — Post-Completion Architecture Refactor (2026-07-19)
|
||||
|
||||
**Design lives in `01_Full_Development_Plan.md` Section 10** — this section is the ordered execution plan only. Same working method as Phases 1–36: restate the phase, wait for approval (`CLAUDE.md` Rule 3), make the change, confirm it, update `03_Progress_Tracker.md`, move to the next one — **strictly in order**, since R2 depends on R1's files existing, R3 depends on R2's repository files being final, R4 depends on R3's interfaces existing, R5 depends on R4's services being final, and R6 wires up everything R1–R5 produced.
|
||||
|
||||
**Ground rule, repeated from `01_Full_Development_Plan.md` Section 10: zero behavior change.** No phase in this section adds, removes, or alters an endpoint, a request/response shape, a business rule, or a database table/column. Every phase's acceptance test is "the solution builds" plus "every endpoint already covered by `General_Ledger_Service.http` returns the same result it did before this phase" — not a new business-rule test, because there is no new business rule.
|
||||
|
||||
### R1 — Consolidate `Models/` (25 files → 12 files)
|
||||
|
||||
**What moves:** every class listed in `01_Full_Development_Plan.md` Section 10.1's table, into its new domain-grouped file. No property, attribute, `[Table]`/Fluent-API-relevant naming, or navigation-property change — this is a pure move-and-recompile.
|
||||
|
||||
**Files touched:** all 25 existing `Models/*.cs` files deleted and replaced by the 12 new ones. `AppDBContext.cs` is **not** touched in this phase — its `DbSet<T>` declarations and `OnModelCreating` reference class names, not file names/paths, so C# doesn't care which file a class lives in as long as the namespace (`General_Ledger_Service.Models`) is unchanged. Every `using General_Ledger_Service.Models;` elsewhere in the project keeps working unmodified.
|
||||
|
||||
**Acceptance test:** `dotnet build` succeeds with zero errors/warnings introduced; `git diff --stat` shows only `Models/` files touched (no `Repositories/`, `Services/`, `AppDBContext.cs`, or `Controllers/` changes in this phase).
|
||||
|
||||
### R2 — Consolidate `Repositories/` + introduce `RepositoryHelpers.cs`
|
||||
|
||||
**What happens**, per `01_Full_Development_Plan.md` Section 10.2/10.4:
|
||||
|
||||
1. Merge `FiscalYearRepository.cs` + `AccountingPeriodRepository.cs` + `PeriodLockLogRepository.cs` → `FiscalCalendarRepository.cs`, one `IFiscalCalendarRepository` interface (still declared in the same file at this stage — R3 moves it out) covering every method the three old interfaces had.
|
||||
2. Merge `JournalEntryRepository.cs` + `JournalEntryLineRepository.cs` + `JournalCorrectionRepository.cs` → `JournalEntryRepository.cs`, one `IJournalEntryRepository` interface covering every method the three old interfaces had.
|
||||
3. Add `RepositoryHelpers.cs` (static, no interface) with `GetByIdAsync<T>`, `ExistsAsync<T>`, `AddAsync<T>` — then update every repository's own trivial one-line methods (across every file in `Repositories/`, not just the two merged ones) to call the shared helper internally instead of repeating `_context.Set.FindAsync(...)`/`AnyAsync(...)`/`AddAsync(...)` inline. Public interface signatures on every repository stay identical — this is an internal implementation change only.
|
||||
|
||||
**Files touched:** `Repositories/FiscalYearRepository.cs`, `AccountingPeriodRepository.cs`, `PeriodLockLogRepository.cs` deleted, replaced by `Repositories/FiscalCalendarRepository.cs`. `Repositories/JournalEntryRepository.cs`, `JournalEntryLineRepository.cs`, `JournalCorrectionRepository.cs` deleted, replaced by a new `Repositories/JournalEntryRepository.cs` (same filename as one of the three it replaces, different content). New `Repositories/RepositoryHelpers.cs`. Every other file in `Repositories/` gets its trivial one-liners routed through `RepositoryHelpers` but keeps its own file/interface.
|
||||
|
||||
**Not yet touched:** `Services/` (still references the old `IFiscalYearRepository`/`IAccountingPeriodRepository`/`IPeriodLockLogRepository`/`IJournalEntryLineRepository`/`IJournalCorrectionRepository` — this phase intentionally leaves the solution **not building** at its very end only if R2 and R4 aren't done together; in practice, do R2's repository-file changes and R4's consuming-service constructor updates as one atomic commit, or the intermediate state won't compile. Restate this explicitly before starting R2 and confirm the user wants R2+R4's service-constructor updates bundled, or wants a temporary compatibility shim — this is a real sequencing decision, not a foregone one).
|
||||
|
||||
**Acceptance test:** once the consuming Services are updated (see the note above), `dotnet build` succeeds; every endpoint touching Fiscal Calendar (Phases 6–8) or Journal Entries (Phases 11–14, 16, 22) still returns identical results via `General_Ledger_Service.http`.
|
||||
|
||||
### R3 — Extract repository interfaces to `Interfaces/Repositories/`
|
||||
|
||||
**What happens:** for each of the 12 repository files (10.2's table), cut its `public interface I...Repository { ... }` block out of the implementation file and into a new file `Interfaces/Repositories/I...Repository.cs`, under namespace `General_Ledger_Service.Interfaces.Repositories`. The implementation class adds `using General_Ledger_Service.Interfaces.Repositories;` and otherwise doesn't change.
|
||||
|
||||
**Files touched:** 12 new files under `Interfaces/Repositories/`; all 12 `Repositories/*.cs` implementation files gain a `using` and lose their interface block.
|
||||
|
||||
**Acceptance test:** `dotnet build` succeeds. `RecursiveTreeQueries.cs` and `RepositoryHelpers.cs` are untouched (no interface to extract).
|
||||
|
||||
### R4 — `Services/`: introduce `ResponseHelpers.cs`, apply it, update constructors for R2's merges
|
||||
|
||||
**What happens:**
|
||||
|
||||
1. Add `Services/ResponseHelpers.cs` (static, no interface) per `01_Full_Development_Plan.md` Section 10.4.
|
||||
2. Update every Service method's success-path `return new ApiResponse { ... }` to `return ResponseHelpers.Ok(...)` (or whichever helper method shape is settled on when this phase starts) — across all 13 domain Services. `DepthGuard.cs` is untouched (different kind of helper, no `ApiResponse` involved).
|
||||
3. Update constructors: `FiscalCalendarService` takes `IFiscalCalendarRepository` instead of three separate repository interfaces; `JournalEntryService`, `RecurringTemplateService`, `FixedAssetService`, and `BankService` take `IJournalEntryRepository` instead of whichever of `IJournalEntryRepository`/`IJournalEntryLineRepository`/`IJournalCorrectionRepository` they previously injected individually — call sites inside each method updated to call the merged repository's methods (method names unchanged from R2, so this is a mechanical injection-site update, not a logic rewrite).
|
||||
|
||||
**Files touched:** new `Services/ResponseHelpers.cs`; all 13 domain `Services/*.cs` files (response-building) plus the 5 named above (constructor/call-site updates for R2's merges).
|
||||
|
||||
**Acceptance test:** `dotnet build` succeeds; the full `General_Ledger_Service.http` regression pass — every endpoint from every module (Accounts, Fiscal Calendar, Cost Centers, Journal Entries, Recurring Entries, Reporting, Tax, Fixed Assets, Bank, Budget, System Config, Audit Log) — returns identical results to its last confirmed-working test.
|
||||
|
||||
### R5 — Extract service interfaces to `Interfaces/Services/`
|
||||
|
||||
Same mechanical move as R3, one layer up: for each of the 13 domain Service files, cut its `public interface I...Service { ... }` block into `Interfaces/Services/I...Service.cs` under `General_Ledger_Service.Interfaces.Services`.
|
||||
|
||||
**Files touched:** 13 new files under `Interfaces/Services/`; all 13 `Services/*.cs` implementation files gain a `using` and lose their interface block. `DepthGuard.cs`/`ResponseHelpers.cs` untouched (no interface).
|
||||
|
||||
**Acceptance test:** `dotnet build` succeeds.
|
||||
|
||||
### R6 — Controller + `Program.cs` wiring, full regression pass
|
||||
|
||||
**What happens:**
|
||||
|
||||
1. Add the `Respond(ApiResponse)` helper to `GeneralLedgerController` (10.6) and replace every action's two-line return with the one-line `Respond(...)` call.
|
||||
2. Add `using General_Ledger_Service.Interfaces.Repositories;` and `using General_Ledger_Service.Interfaces.Services;` to `Program.cs`; update every `AddScoped<I..., ...>` line for the R2 merges (`IFiscalYearRepository`/`IAccountingPeriodRepository`/`IPeriodLockLogRepository` → `IFiscalCalendarRepository`; `IJournalEntryRepository`/`IJournalEntryLineRepository`/`IJournalCorrectionRepository` → the merged `IJournalEntryRepository`) — every other `AddScoped` line keeps the same interface/implementation pair, just resolved from the new namespace.
|
||||
3. Full solution rebuild from clean (`dotnet clean && dotnet build`) to catch any stale `using` left pointing at a namespace/file that no longer exists.
|
||||
4. Full regression pass: every phase's acceptance test from `03_Progress_Tracker.md` Phases 1–36 re-run against a real local database via `General_Ledger_Service.http`/Swagger, confirming identical behavior end-to-end.
|
||||
|
||||
**Files touched:** `Controllers/GeneralLedgerController.cs`, `Program.cs`.
|
||||
|
||||
**Acceptance test:** clean build with zero errors/warnings; every one of the 34 API endpoints re-verified against its own `03_Progress_Tracker.md` acceptance-test notes with no regressions. This is the phase that closes out the whole refactor — `03_Progress_Tracker.md`'s refactor section gets its final "all R1–R6 done" summary here, the same way Phase 36 closed out the original 36.
|
||||
@@ -0,0 +1,723 @@
|
||||
# General Ledger Service — Development Progress Tracker
|
||||
|
||||
This file is the single source of truth for **what has actually been built so far**. It is checked **before starting any work** (see `CLAUDE.md` Rule 0) and updated **immediately after** a phase is confirmed working — never before, and never as a batch at the end of a session.
|
||||
|
||||
Status values used below:
|
||||
- `☐ Not started` — no code written yet
|
||||
- `◐ In progress` — code written, not yet confirmed working end-to-end
|
||||
- `☑ Done` — implemented **and** manually confirmed working (per that phase's acceptance test in `02_Phase_By_Phase_Development_Plan.md`)
|
||||
- `⚠ Blocked` — can't proceed; reason noted in the "Notes" column
|
||||
- `✕ Removed` — phase permanently out of scope, will not be implemented; reason noted in the "Notes" column. Its phase number is retired, not reused (see the "no renumbering, ever" rule below)
|
||||
|
||||
**Phase numbers here match `02_Phase_By_Phase_Development_Plan.md` and `01_Full_Development_Plan.md` Section 6.2 exactly** — same numbering, same order, no renumbering, ever. If a phase is skipped, removed, or reordered, this file is updated to say so explicitly rather than silently drifting from the plan documents.
|
||||
|
||||
**27 API endpoints in the original numbered sequence** (down from an original 40 — see `01_Full_Development_Plan.md` Section 6.1 for the full consolidation/removal history), **plus 6 appended list endpoints (Phases 30–35, §6.3) and 1 appended audit-log endpoint (Phase 36, §6.4): 34 API endpoints, 36 phases total (Phase 18's slot counts toward the phase total but has no endpoint — removed 2026-07-18).**
|
||||
|
||||
**2026-07-18 note:** this file was found missing from disk (not renamed, not recoverable from git — never committed) partway through the `audit_log` retrofit work. Recreated from this session's own record, per explicit user confirmation. If anything below looks off relative to the actual codebase, flag it per Rule 0.4 rather than trusting this reconstruction blindly.
|
||||
|
||||
---
|
||||
|
||||
## How to update this file
|
||||
|
||||
- When starting a phase: change its status to `◐ In progress`, add today's date under "Started."
|
||||
- When a phase's acceptance test passes: change its status to `☑ Done`, add today's date under "Completed." For the consolidated phases (3, 4, 10, 11, 12, 13, 17, 22 — see the "Consolidated?" column below), the test must cover every branch of the discriminator before the phase counts as done, per Document 2's closing "How to Use" section.
|
||||
- If a phase is blocked: change its status to `⚠ Blocked` and explain why in "Notes" — do not silently move on to a later phase while an earlier one is blocked, unless that's explicitly approved (see `CLAUDE.md` Rule 3).
|
||||
- If a phase is removed from scope entirely: change its status to `✕ Removed` and explain why in "Notes." **Phase 18 is in this state** — see its row below.
|
||||
- This file only records the two facts above — status and dates. It is not a place for design discussion; that stays in the plan documents or the conversation itself.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Models, DbContext, Migration, Default Data, API Key Middleware, Versioning
|
||||
|
||||
*(No API call — this is the foundation every later phase depends on. Must be fully done before Phase 2 starts.)*
|
||||
|
||||
| Step | Description | Status | Started | Completed | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| 1.1 | Remove `Interfaces/Repositories/`, `Interfaces/Services/` folders + `.csproj` references | ☑ Done | 2026-07-17 | 2026-07-17 | Folders never existed on disk; removed the two stale `<Folder Include>` lines from the `.csproj` |
|
||||
| 1.2 | Create all 24 entity classes in `Models/` | ☑ Done | 2026-07-17 | 2026-07-17 | Includes `JournalEntry.Narration` (approved addition, not in the original ER diagram). **Retrofit 2026-07-18:** added a 25th model, `Models/AuditLog.cs`, for the new generic audit-log table (`CLAUDE.md` Rule 10) |
|
||||
| 1.3 | `AppDBContext.cs` — `DbSet<T>` + Fluent API (recursive FKs, precision, indexes) + `HasData` seeding (incl. `Currency`) | ☑ Done | 2026-07-17 | 2026-07-17 | Configured explicit snake_case table names + a snake_case column/key/index naming pass (see Deviations). **Retrofit 2026-07-18:** added `DbSet<AuditLog>` + `ConfigureAuditLog` (no FK, by design), migration `AddAuditLog` |
|
||||
| 1.4 | `ApiKeyMiddleware` + `appsettings.*` `ApiKey:Value` entries + `Program.cs` registration | ☑ Done | 2026-07-17 | 2026-07-17 | |
|
||||
| 1.5 | `Asp.Versioning.Mvc` package + `Program.cs` versioning registration + `GeneralLedgerController` moved to `v1` | ☑ Done | 2026-07-17 | 2026-07-17 | Package reference was already present in the `.csproj`; added `AddApiVersioning` + controller attributes |
|
||||
| 1.6 | Migration created and applied to local PostgreSQL | ☑ Done | 2026-07-17 | 2026-07-17 | `InitialCreate`, applied to `General_Ledger` on localhost. See Deviations for the full chain of later migrations |
|
||||
| 1.7 | Acceptance criteria fully verified (see plan doc for the exact checklist) | ☑ Done | 2026-07-17 | 2026-07-17 | All checks passed at the time: 5/3/5/1 seed row counts, versioned status endpoint with/without/wrong API key, `/health` + `/swagger` unprotected. **Currency count later grew from 1 to 142** (full ISO 4217 list, 2026-07-18 — see Deviations) — the "1" above reflects what was true on 2026-07-17, not a discrepancy |
|
||||
| 1.8 | Exact `HasData` seed values documented (`account_type`, `system_config`, `tax_type`, `currency`) — `01_Full_Development_Plan.md` §5.2–5.3 | ☑ Done | 2026-07-19 | 2026-07-19 | Documentation formalization, no code change — values transcribed from the already-live, already-tested `AppDBContext.cs`/migrations, not newly invented. Captures the corrected `is_editable`/`is_locked` semantics and the `Currency.DecimalPlaces` `HasData`-default bug in one canonical place instead of only in the Deviations log |
|
||||
| 1.9 | `Company:Name`/`Address`/`Tin`/`Brn` added to `appsettings.json` (all environments) — for LKAS 1.51 statement headers and the Income Tax Computation report's fuller identity block | ☑ Done | 2026-07-22 | 2026-07-22 | Verified live — rendered correctly on the Income Tax Computation PDF header |
|
||||
| 1.10 | `CASH_ACCOUNT_ROOT_ID` — new `system_config` key, set via `PUT /system-config` | ☑ Done | 2026-07-21 | 2026-07-21 | Already recorded further down this file's 2026-07-21 entry |
|
||||
| 1.11 | `accounts.income_statement_section` — new column | ☑ Done | 2026-07-22 | 2026-07-22 | Migration `AddIncomeStatementSectionTaxDeductibleAndCashAccounts`, applied. Verified live via the `ProfitAndLoss` report (see this file's 2026-07-22 entry below) |
|
||||
| 1.12 | `accounts.is_tax_deductible` column + 5 new tax-related `system_config` keys | ☑ Done | 2026-07-22 | 2026-07-22 | Same migration as 1.11. `CORPORATE_INCOME_TAX_RATE`/`STATUTORY_RELIEF_AMOUNT` seeded via `HasData`; `APIT_CREDIT_ACCOUNT_ID`/`WHT_CREDIT_ACCOUNT_ID`/`QUARTERLY_TAX_PAYMENT_ACCOUNT_ID` seeded empty, same pattern as `CASH_ACCOUNT_ROOT_ID` |
|
||||
|
||||
**Phase 1 overall status:** ☑ Done (all of 1.1–1.12)
|
||||
|
||||
---
|
||||
|
||||
## Phases 2–29 — One API Endpoint Each
|
||||
|
||||
| Phase | Module | Endpoint | Consolidated? | Status | Started | Completed | Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 2 | Accounts | `POST /api/v1/accounts` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Also added `ExceptionHandlingMiddleware` (see Deviations) and a minimal `SystemConfigRepository`. **Audit retrofit 2026-07-18:** writes one `audit_log` row (`Ledger`/`Create`) per account created |
|
||||
| 3 | Accounts | `GET /api/v1/accounts/{accountCode}` (+`?expand=tree`) | Merges old tree endpoint | ☑ Done | 2026-07-17 | 2026-07-17 | Retrofitted from `{id}` to `{accountCode}` lookup (see Deviations, Rule 8). Read-only — no audit row |
|
||||
| 4 | Accounts | `PUT /api/v1/accounts/{accountCode}` | Merges old reparent-only endpoint | ☑ Done | 2026-07-17 | 2026-07-17 | Tested rename, re-parent, cycle rejection (400), explicit-null clear-parent, self-parent rejection (400), and 404. **Audit retrofit 2026-07-18:** writes one `audit_log` row, category computed dynamically — `Ledger` if `parentAccountId` changed, `Administrative` otherwise; verified both branches via `psql` |
|
||||
| 5 | Accounts | `GET /api/v1/accounts` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Tested unfiltered list, `accountTypeId` filter, `isPostable` filter + paging, and an empty-result filter. Read-only — no audit row |
|
||||
| 6 | Fiscal Calendar | `POST /api/v1/fiscal-years` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Built and tested together with Phases 7–8 (see Deviations). Tested with and without `autoGeneratePeriods`, plus overlap rejection (400). **Audit retrofit 2026-07-18:** writes one `audit_log` row (`Ledger`/`Create`) per fiscal year created |
|
||||
| 7 | Fiscal Calendar | `POST /api/v1/fiscal-years/{id}/periods` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Tested initial 12-period generation, extend-past-fiscal-year-end rejection (400), and 404 for a missing fiscal year. **Audit retrofit 2026-07-18:** writes one `audit_log` row (`Ledger`/`Create`), `entityId` = the fiscal year, describing how many periods were generated |
|
||||
| 8 | Fiscal Calendar | `PUT /api/v1/periods/{id}/lock` | — | ☑ Done | 2026-07-17 | 2026-07-17 | Tested LOCK, invalid action (400), 404, CLOSE then reject-other-actions (400), and UNLOCK reopening a CLOSED period. **Audit retrofit 2026-07-18:** writes one `audit_log` row (`Ledger`), `action` mapped to the specific lock action taken (`Lock`/`Unlock`/`SoftClose`/`Close`), alongside the existing `period_lock_log` row it doesn't replace |
|
||||
| 9 | Cost Centers | `POST /api/v1/cost-centers` | — | ☑ Done | 2026-07-18 | 2026-07-18 | Built and tested together with Phase 10. Tested root create, child create, duplicate-code rejection (400), missing-parent (404), and depth-guard rejection at depth 5 (`COST_CENTER_MAX_DEPTH=4`, unconditional — no separate enforce-flag exists for cost centers). Writes one `audit_log` row (`Administrative`/`Create`) — verified via `psql` |
|
||||
| 10 | Cost Centers | `GET /api/v1/cost-centers/{costCenterCode}` (+`?expand=tree`) | Merges old tree endpoint | ☑ Done | 2026-07-18 | 2026-07-18 | Retrofitted route from `{id}` to `{costCenterCode}` in Documents 1/2 before building (Rule 8). Tested plain get, `?expand=tree` (breadcrumb + full 4-level descendant tree), and 404 for a missing code |
|
||||
| 11 | Journal Entries | `POST /api/v1/journal-entries` | Merges old create + `/reverse` + `/adjust`; **retrofit 2026-07-22: also absorbs Phases 12/13** (see their rows) — `lines` now required, entry is created and posted in one call, `accountCode`/`costCenterCode`/`originalJournalNo` replace numeric ids | ☑ Done | 2026-07-18 | 2026-07-22 | **2026-07-18 history:** tested plain create (no lines), plain create with inline lines, `FullReversal` (mirrors original's lines, sets `reversalOfId`), `PartialAdjustment` (empty draft, lines added via the then-separate Phase 12), correction against a non-`POSTED` original (400), invalid `correctionType` (400). **2026-07-22 retrofit, verified live:** plain create auto-posts (`isPosted=true`, `entryStatus=2`, in the one `POST` call); `FullReversal` correction auto-posts and flips the original to `Reversed` in the same call; `PartialAdjustment` correction now takes `lines` inline (no follow-up call) and auto-posts; removed `PUT .../lines` and `PUT .../post` both confirmed `404` |
|
||||
| 12 | Journal Entries | ~~`PUT /api/v1/journal-entries/{journalNo}/lines`~~ | **Removed 2026-07-22, absorbed into Phase 11** — `CLAUDE.md` Rule 14: there is no client-facing `DRAFT` state left to set lines on separately | ✕ Removed | 2026-07-18 | 2026-07-22 | Was `☑ Done` 2026-07-18 (see git history for that period's notes); confirmed removed and returning `404` live 2026-07-22 |
|
||||
| 13 | Journal Entries | ~~`PUT /api/v1/journal-entries/{journalNo}/post`~~ | **Removed 2026-07-22, absorbed into Phase 11** — same rule | ✕ Removed | 2026-07-18 | 2026-07-22 | Was `☑ Done` 2026-07-18 (see git history for that period's notes); confirmed removed and returning `404` live 2026-07-22 |
|
||||
| 14 | Journal Entries | `GET /api/v1/journal-entries/{journalNo}` | — | ☑ Done | 2026-07-18 | 2026-07-18 | Looked up by `journalNo`, not id (`CLAUDE.md` Rule 8). Tested plain get (with lines) and 404 for a missing `journalNo` |
|
||||
| 15 | Recurring Entries | `POST /api/v1/recurring-templates` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Built and tested together with Phases 16-17. Tested balance-check rejection (400), invalid `recurrencePattern` (400), non-postable/non-existent account in a line (400/404), and a valid create. **Retrofit 2026-07-22:** line `accountId` → `accountCode` (`CLAUDE.md` Rule 8.2), verified live |
|
||||
| 16 | Recurring Entries | `POST /api/v1/recurring-templates/{id}/generate` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Tested a due template generating a journal entry (via Phase 11's create reused internally) with `NextRunDate` correctly advanced by the recurrence pattern afterward, a not-yet-due template (400), and a non-existent template (404). **Retrofit 2026-07-22:** the generated entry is now created **and posted** in the same internal call (Phase 11's create+post merge, `CLAUDE.md` Rule 14) — previously left as a `DRAFT` for later review, confirmed via a live test (`JV-2026-000005` came back `isPosted=true`) |
|
||||
| 17 | Reporting | `GET /api/v1/reports` | Merges 7 old report endpoints; corrected `POST`→`GET` | ☑ Done — base endpoint (2026-07-18), 4 retrofits done+verified live 2026-07-21, all 6 further 2026-07-22 retrofits (TrialBalance flatten, ProfitAndLoss restructure, CashFlow simplify, Income Tax Computation redesign, Csv format, Company:Address/Tin/Brn) done and verified live 2026-07-22, `GeneralLedger`'s `accountId` param → `accountCode` (`CLAUDE.md` Rule 8.2, same day, separate retrofit) done and verified live | 2026-07-18 | 2026-07-22 | **2026-07-18/21 history (unchanged, kept as an accurate record):** Tested all seven `reportType` values (Json + Pdf) against real posted data, `outputFormat=Pdf` (verified real `%PDF-1.4` bytes for every report, A4 page size, correct landscape/portrait per report), missing-required-parameter (400), invalid `reportType`/`outputFormat` (400), non-existent `budgetId` (404). All 4 previously-outstanding retrofits complete: `CashFlow` full LKAS 7 upgrade, all 7 bespoke PDF layouts, `TaxSummary` (original VAT/WHT/NBT design), A4 page size. **2026-07-22 — reference templates arrived for 5 of the 7 reports, revealing real gaps in what was built 2026-07-21.** Six further retrofits now outstanding, none started: (1) `TrialBalance` — flatten response/PDF/CSV, drop `depth`/`indentedCode`; (2) `ProfitAndLoss` — full restructure around new `accounts.income_statement_section` column (Sales/Cost of Sales/Gross Profit/Other Income/four expense groups); (3) `CashFlow` — drop printed opening/closing balance lines, add `direction` field, re-bucket adjustments into Additions/Subtractions at render time; (4) `TaxSummary` — **wholesale redesign** from VAT/WHT/NBT summary into a full Income Tax Computation (new `is_tax_deductible` column, 5 new `system_config` keys, 4 new optional request params) — old design superseded, not extended; (5) `outputFormat=Csv` — new third output format for all seven reports; (6) `Company:Address`/`Tin`/`Brn` — 3 new appsettings fields for the Income Tax Computation's header. Full specs: `Accounting_System_Design_LKAS.md` §11.4/§11.5/§11.7, `04_API_Reference_And_Scenarios.md`, `05_LKAS_Report_PDF_Templates.md` (all updated 2026-07-22), this file's Phase 1 §1.9–§1.12 (new). |
|
||||
| 18 | Currency | ~~`POST /api/v1/fx-revaluation`~~ | — | ✕ Removed | | 2026-07-18 | **Removed, not just blocked.** Confirmed 2026-07-18: base currency is a one-time initial-configuration choice, never changed by the client, and non-base currencies are never used in this system. FX revaluation has no possible purpose as a result — see Document 1 §6.1 and Document 2, Phase 18 |
|
||||
| 19 | Tax | `POST /api/v1/tax-codes` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Built and tested together with Phases 20-22, 31-33. Tested valid create, duplicate code (400), missing `effectiveFrom` (400), non-existent `taxTypeId` (404), invalid `calculationBasis`/`appliesTo` (400). **Retrofit 2026-07-22:** `taxTypeId`/`outputTaxAccountId`/`inputTaxAccountId` → `taxTypeCode`/`outputTaxAccountCode`/`inputTaxAccountCode` (`CLAUDE.md` Rule 8.2), verified live |
|
||||
| 20 | Tax | `GET /api/v1/tax-codes/calculate` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Tested `Exclusive` calculation (1000 @ 18% = 180), and a `txnDate` outside the tax code's effective range (404 — same "no matching row" case as a missing code). **Retrofit 2026-07-22:** `taxCodeId` query param → `taxCode` (`CLAUDE.md` Rule 8.2), verified live (`VAT-001` @ 15% on 1000 = 150) |
|
||||
| 21 | Fixed Assets | `POST /api/v1/fixed-assets` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Tested `StraightLine` and `ReducingBalance` (incl. missing-rate rejection, 400), duplicate asset code (400), `residualValue > acquisitionCost` (400), non-postable account (400). **Retrofit 2026-07-22:** the 4 GL account fields (`assetAccountId`/`accumDeprAccountId`/`deprExpenseAccountId`/`gainLossAccountId`) → `...Code` (`CLAUDE.md` Rule 8.2), verified live |
|
||||
| 22 | Fixed Assets | `POST /api/v1/fixed-assets/{assetCode}/transactions` | Merges old `/depreciate` + `/dispose` | ☑ Done | 2026-07-18 | 2026-07-22 | Looked up by `assetCode`, not id (`CLAUDE.md` Rule 8). Tested `Depreciate` (`StraightLine` and `ReducingBalance`, correct amounts, auto-posted journal), locked-period rejection (400), `Dispose` (balanced 4-line entry incl. a loss on the gain/loss account, asset flips to `Disposed`), both transaction types correctly rejected once disposed (400), non-existent asset code (404), invalid `transactionType` (400). **Retrofit 2026-07-22:** `proceedsAccountId` → `proceedsAccountCode`; internally, both transaction types now call `JournalEntryService.CreateAsync` once (it auto-posts, Rule 14) instead of a separate `CreateAsync` + `PostAsync` pair — verified live (`Depreciate` on `FA-000003` returned `journal.isPosted=true` directly) |
|
||||
| 23 | Bank | `POST /api/v1/bank-accounts` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Built and tested together with Phases 24, 25, 34. Tested valid create (defaults `currencyCode` to `LKR`, matching `AccountService`'s own precedent), non-postable GL account (400), non-existent GL account (404). **2026-07-22:** a `bank_account.accountType` discriminator was drafted, then redirected to a separate `cash_account` table instead (Phase 23a) — `bank_account`/`POST /bank-accounts` itself stayed exactly as originally built, no shape change. **Retrofit 2026-07-22 (same day, separate change):** `glAccountId` → `glAccountCode` (`CLAUDE.md` Rule 8.2), verified live |
|
||||
| 23a | Bank | `POST /api/v1/cash-accounts` | New, 2026-07-22 — separate table from `bank_account`, not a discriminator | ☑ Done, implemented and verified live 2026-07-22 | 2026-07-22 | 2026-07-22 | New `cash_account`/`cash_account_type` tables (`Accounting_System_Design_LKAS.md` §5.8). Tested: create with an existing `cashAccountTypeName` + no `accountNumber` (auto-generated `CASH-000003`); create with a `cashAccountTypeName` not in the seeded list (`"Vault Cash"`) — confirmed it created a new `cash_account_type` row, immediately visible via `GET /cash-account-types`; non-postable/non-existent GL account (400/404); missing `cashAccountTypeName` (400). **Retrofit 2026-07-22 (same day, separate change):** `glAccountId` → `glAccountCode`, verified live |
|
||||
| 23b | Bank | `GET /api/v1/cash-account-types` | New, 2026-07-22 | ☑ Done, implemented and verified live 2026-07-22 | 2026-07-22 | 2026-07-22 | Flat, unfiltered list — seeded with Petty Cash/Till Cash/Safe Cash/Cash in Transit, confirmed growing after Phase 23a's "Other" test |
|
||||
| 24 | Bank | `POST /api/v1/bank-accounts/{id}/statement-lines/import` | — | ☑ Done | 2026-07-18 | 2026-07-18 | First file-upload endpoint in this project (`multipart/form-data`, not JSON) — Controller extracts a `Stream` from `IFormFile`, keeping that ASP.NET Core type out of the Service layer. Column layout: `TxnDate \| Description \| Amount` with a header row. Malformed rows are skipped and reported per-row (not all-or-nothing) — confirmed with the user. Tested: 2 valid + 1 malformed row (text where a number was expected) → 2 imported, 1 reported with row number + reason; a genuinely corrupt (non-`.xlsx`) file → 400; non-existent bank account → 404 |
|
||||
| 25 | Bank | `PUT /api/v1/bank-statement-lines/{id}/reconcile` | — | ☑ Done | 2026-07-18 | 2026-07-22 | "Amounts must match" resolved as: journal entry must be `Posted`, sum its lines posted to the bank account's own GL account, compare to the statement line's `Amount` — confirmed with the user. Tested: reconcile against an unposted entry (400), successful reconcile, re-reconciling an already-reconciled line (400), amount mismatch (400), non-existent statement line (404), non-existent journal entry (404). **Retrofit 2026-07-22:** `matchedJournalId` → `matchedJournalNo` (`CLAUDE.md` Rule 8.2), verified live (`404` for a non-existent `journalNo`, confirming the lookup switched correctly) |
|
||||
| 26 | Budget | `POST /api/v1/budgets` | — | ☑ Done | 2026-07-18 | 2026-07-18 | Built and tested together with Phases 27, 35. Tested valid create and non-existent `fiscalYearId` (404). Unaffected by the 2026-07-22 code-based retrofit — `fiscalYearId` has no unique code column (`CLAUDE.md` Rule 8.2), stays numeric |
|
||||
| 27 | Budget | `POST /api/v1/budgets/{id}/lines` | — | ☑ Done | 2026-07-18 | 2026-07-22 | Appends to the budget's existing lines rather than replacing them (Document 1's own note: "lines are added incrementally over time"), unlike the old Phase 12's replace-all (Phase 12 itself removed 2026-07-22, see its row). Tested valid add (2 lines), non-existent account/period/cost-center (404 each), non-existent budget (404). Also re-verified Phase 17's `BudgetVsActual` report end-to-end now that real budget data exists — previously only verifiable structurally. **Retrofit 2026-07-22:** line `accountId`/`costCenterId` → `accountCode`/`costCenterCode` (`CLAUDE.md` Rule 8.2; `periodId` stays numeric, no unique code), verified live |
|
||||
| 28 | System Config | `PUT /api/v1/system-config/{key}` | — | ☑ Done | 2026-07-18 | 2026-07-18 | `LEDGER` per current keys, classified per-key via a lookup dictionary (defaults to `Administrative` for any future/unmapped key) — Document 1 §4.3. `is_editable`/`is_locked` resolved as `!IsEditable \|\| IsLocked` → reject, with every successful write locking the key (confirmed with the user — see Deviations). Tested: valid update + auto-lock, re-edit of the now-locked key (400), non-existent key (404) |
|
||||
| 29 | System Config | `GET /api/v1/system-config` | — | ☑ Done | 2026-07-18 | 2026-07-18 | Built and tested together with Phase 28. Flat list, all 3 seeded keys returned |
|
||||
|
||||
---
|
||||
|
||||
## Phases 30–35 — Appended read/list endpoints (Document 1 §6.3 / Document 2)
|
||||
|
||||
| Phase | Module | Endpoint | Status | Started | Completed | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 30 | Currency | `GET /api/v1/currencies` | ☑ Done | 2026-07-18 | 2026-07-18 | Tested against the running app: all 142 seeded currencies returned, sorted by code, correct `decimalPlaces` per row (JPY=0, KWD=3), exactly 1 `isBaseCurrency=true` (LKR) |
|
||||
| 31 | Tax | `GET /api/v1/tax-types` | ☑ Done | 2026-07-18 | 2026-07-18 | Implemented alongside Phase 19, per this row's own note. Tested: all 5 seeded tax types returned |
|
||||
| 32 | Tax | `GET /api/v1/tax-codes` | ☑ Done | 2026-07-18 | 2026-07-18 | Implemented alongside Phase 19. Tested against real created tax codes |
|
||||
| 33 | Fixed Assets | `GET /api/v1/fixed-assets` | ☑ Done | 2026-07-18 | 2026-07-18 | Implemented alongside Phase 21. Tested against real registered assets |
|
||||
| 34 | Bank | `GET /api/v1/bank-accounts` | ☑ Done — retrofit 2026-07-22: unified, filterable list across `bank_account` + `cash_account` | 2026-07-18 | 2026-07-22 | Implemented alongside Phase 23, per this row's own note. Tested against a real created bank account. **2026-07-22:** new optional `accountType` query param (`Cash`/`Bank`/`Both`, default `Both`); response is now a combined, flattened `CashAndBankAccountDto` list (`accountType` a response-only discriminator, not a stored column). Verified live: default call returns both tables' rows together; `accountType=Cash`/`Bank` each correctly filter; an invalid value returns 400 |
|
||||
| 35 | Budget | `GET /api/v1/budgets` | ☑ Done | 2026-07-18 | 2026-07-18 | Implemented alongside Phase 26, per this row's own note. Tested against a real created budget |
|
||||
|
||||
---
|
||||
|
||||
## Phase 36 — Audit Log List Endpoint (Document 1 §6.4)
|
||||
|
||||
| Phase | Endpoint | Status | Started | Completed | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| 36 | `GET /api/v1/audit-log` | ☑ Done | 2026-07-18 | 2026-07-18 | Built and tested together with Phases 28-29. Tested no-filter (54 rows, matching every prior phase's audit inserts this session), `category=LEDGER`/`ADMINISTRATIVE` split (41/13, summing to 54), invalid `category`/`action` → empty result (not an error), `entityName`/`action`/`performedBy`/date-range filters, and default paging (page 1, pageSize 20) |
|
||||
|
||||
---
|
||||
|
||||
## Phase 37 — Cheque Management (new module, appended 2026-07-30 — beyond the original 36-phase plan)
|
||||
|
||||
Client-supplied field spec for `ChequeBook`/`ChequePage` (issued cheques, from this company's own cheque books) and `ReceivedCheque` (cheques received from others — no `ChequeBook` link, per the spec). Not part of `01_Full_Development_Plan.md`/`02_Phase_By_Phase_Development_Plan.md`'s original scope; added as a new module following the same layering/foundation rules (`CLAUDE.md` Rule 1) as every other module.
|
||||
|
||||
| Phase | Module | Endpoint | Status | Started | Completed | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 37.1 | Cheque Books | `POST /api/v1/cheque-books` | ☑ Done | 2026-07-30 | 2026-07-30 | Auto-generates all `totalLeaves` `ChequePage` rows (`Unused`) from `startChequeNo`/`endChequeNo`. Validates bank account exists, `chequeBookNo` uniqueness, and that the numeric range matches `totalLeaves`. Verified live |
|
||||
| 37.2 | Cheque Books | `GET /api/v1/cheque-books/{chequeBookNo}` (+`?expand=pages`) | ☑ Done | 2026-07-30 | 2026-07-30 | Read-only — no audit row. Verified live |
|
||||
| 37.3 | Cheque Books | `GET /api/v1/cheque-books` | ☑ Done | 2026-07-30 | 2026-07-30 | Filters: `bankAccountId`, `branchId`, `status`. Verified live |
|
||||
| 37.4 | Cheque Pages | `PUT /api/v1/cheque-pages/{chequeNo}/issue` | ☑ Done | 2026-07-30 | 2026-07-30 | `Unused` → `Issued` only; fills payee/amount/etc. Verified live |
|
||||
| 37.5 | Cheque Pages | `PUT /api/v1/cheque-pages/{chequeNo}/status` | ☑ Done | 2026-07-30 | 2026-07-30 | Action-based (`Clear`/`Bounce`/`Cancel`/`Void`), same pattern as `PUT /periods/{id}/lock`. Verified live for all four actions plus an invalid-transition 400 |
|
||||
| 37.6 | Cheque Pages | `GET /api/v1/cheque-pages/{chequeNo}` | ☑ Done | 2026-07-30 | 2026-07-30 | Read-only — no audit row. Verified live |
|
||||
| 37.7 | Cheque Pages | `GET /api/v1/cheque-pages` | ☑ Done | 2026-07-30 | 2026-07-30 | Filters: `chequeBookNo`, `issueStatus`, `payeeType`, `payeeId`. Verified live |
|
||||
| 37.8 | Received Cheques | `POST /api/v1/received-cheques` | ☑ Done | 2026-07-30 | 2026-07-30 | No `ChequeBook` link, per spec. Status defaults `Received`. Verified live |
|
||||
| 37.9 | Received Cheques | `PUT /api/v1/received-cheques/{id}/status` | ☑ Done | 2026-07-30 | 2026-07-30 | Action-based (`Deposit`/`Clear`/`Return`/`Cancel`); `Deposit` requires `depositBankAccountId`+`depositDate`. Verified live for `Deposit`→`Clear` |
|
||||
| 37.10 | Received Cheques | `GET /api/v1/received-cheques/{id}` | ☑ Done | 2026-07-30 | 2026-07-30 | Read-only — no audit row. Verified live |
|
||||
| 37.11 | Received Cheques | `GET /api/v1/received-cheques` | ☑ Done | 2026-07-30 | 2026-07-30 | Filters: `companyId`, `branchId`, `status`, `receivedFromType`. Verified live |
|
||||
|
||||
**Phase 37 overall status:** ☑ Done (all of 37.1–37.11). Full design decisions, deviations from the client's raw field spec, and live-verification detail recorded in this file's 2026-07-30 entry below.
|
||||
|
||||
---
|
||||
|
||||
## Phase 38 — GRN/AP Integration Support (appended 2026-08-12 — beyond the original 36-phase plan)
|
||||
|
||||
Two small, additive gaps found and closed while wiring ERPCore's GRN module (receipt posting + vendor payment) to this service. Both reuse existing internal logic; neither changes any existing endpoint's request/response shape.
|
||||
|
||||
| Phase | Module | Endpoint | Status | Started | Completed | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 38.1 | Fiscal Calendar | `GET /api/v1/fiscal-years/periods/by-date?date=YYYY-MM-DD` | ◐ In progress | 2026-08-12 | | Resolves the `AccountingPeriod` covering a date, for callers (e.g. ERPCore) that need a `periodId` for `POST /journal-entries` but have no other way to discover one — this API surface previously had no `GET` for periods at all. Reuses the existing `IFiscalCalendarRepository.GetPeriodByDateAsync` (already used internally by `RecurringTemplateService.GenerateAsync`) via a new `FiscalCalendarService.GetPeriodByDateAsync` + controller action. 404 `"Period not found"` if no period covers the date. Build verified clean; not yet manually exercised against a live local instance |
|
||||
| 38.2 | Bank & Cash Accounts | `GET /api/v1/bank-accounts` — response gains `glAccountCode` | ◐ In progress | 2026-08-12 | | `CashAndBankAccountDto` previously exposed only the numeric `glAccountId`; since this API has no `GET /accounts` lookup by numeric id (only by `accountCode`, per Rule 8), a caller could not turn a listed bank/cash account into an `accountCode` to post a journal-entry line against. Added `GlAccountCode` to the DTO, sourced via `.Include(x => x.GlAccount)` on `BankRepository.GetAllAsync()`/`GetAllCashAccountsAsync()`. Additive field only — existing consumers unaffected. Build verified clean; not yet manually exercised against a live local instance |
|
||||
|
||||
**Phase 38 overall status:** ◐ In progress — both endpoints build clean but have not yet been manually verified against a running local instance (Rule 6: a phase isn't "done" until that happens). Flagging here rather than marking ☑ Done prematurely.
|
||||
|
||||
---
|
||||
|
||||
## Overall Progress Summary
|
||||
|
||||
| Metric | Count |
|
||||
|---|---|
|
||||
| Total phases | 38 (Phase 1 + Phases 2–29 + Phases 30–35 + Phase 36 + Phase 37 + Phase 38) |
|
||||
| Of which consolidated/modified endpoints | 8 (Phases 3, 4, 10, 11, 12, 13, 17, 22) |
|
||||
| Done | 36 (Phases 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37) |
|
||||
| In progress | 1 (Phase 38) |
|
||||
| Blocked | 0 |
|
||||
| Removed | 1 (Phase 18) |
|
||||
| Not started | 0 |
|
||||
|
||||
**All 36 original phase slots resolved as of 2026-07-18** (35 Done + 1 Removed); **Phase 37 (Cheque Management) added and completed 2026-07-30 as new scope beyond the original plan.** Every phase from `01_Full_Development_Plan.md`/`02_Phase_By_Phase_Development_Plan.md` has been implemented and manually confirmed working, except Phase 18 (FX Revaluation), which was confirmed permanently out of scope — see its Deviations entry.
|
||||
|
||||
*(This summary is recalculated and updated by hand every time a phase's status changes — it is not authoritative on its own; the per-phase tables above are the source of truth.)*
|
||||
|
||||
---
|
||||
|
||||
## Deviations From the Plan
|
||||
|
||||
Any time actual implementation diverges from what `01_Full_Development_Plan.md` or `02_Phase_By_Phase_Development_Plan.md` describes, record it here with the date and a one-line reason.
|
||||
|
||||
**2026-07-17 (round 1)** — Endpoint count reduced from 40 to 31 via four consolidations (reporting, journal correction, fixed asset transactions, account reads). Phase numbering updated across all documents.
|
||||
|
||||
**2026-07-17 (round 2)** — Further reduced from 31 to 28 endpoints: (1) currency-management endpoints (old `POST /currencies`, `POST /currencies/{code}/rates`) removed outright — base currency is now seeded via `HasData` in Phase 1 instead; (2) journal-entry create and correction merged into a single `POST /api/v1/journal-entries`, with the lines-setting endpoint (Phase 12) generalized to serve both plain drafts and `PartialAdjustment` corrections, and the post endpoint (Phase 13) gaining a branch to flip the original entry's status when posting a `FullReversal`; (3) the reporting endpoint corrected from `POST` to `GET`, since it's read-only. **Phase 18 (FX Revaluation) is now flagged `⚠ Blocked`** as an open dependency question — it has no data to act on until the currency-removal decision in Document 2's Phase 18 is resolved one way or the other.
|
||||
|
||||
**2026-07-17 (Phase 1 build) — `JournalEntry.Narration` added.** Neither the ER diagram nor Document 2's own Phase 1 model list included a narration field on `journal_entry`, but Document 2's Phase 11 worked example specifies it in the create-journal-entry request DTO. Added `Narration` (text, nullable) to the `JournalEntry` model/table now (approved by the user) rather than leaving a gap that Phase 11 would have had to patch retroactively.
|
||||
|
||||
**2026-07-17 (Phase 1 build) — Fixed a pre-existing build break unrelated to the plan.** The `.csproj` (already modified outside this session, uncommitted) was missing the `Microsoft.AspNetCore.Authentication.JwtBearer` package reference that `Program.cs` depends on for JWT bearer auth setup — the project would not compile. Re-added the package reference (version 10.0.9, matching the other `Microsoft.*` packages).
|
||||
|
||||
**2026-07-17 (Phase 1 build) — Database uses snake_case naming, not EF Core's PascalCase default.** `Accounting_System_Design_LKAS.md`, the ER diagram, and every recursive CTE query example in both use snake_case table/column names throughout. Resolved (approved by the user) by setting explicit snake_case table names per entity and adding a snake_case conversion pass over columns/keys/foreign keys/indexes in `AppDBContext.OnModelCreating`, rather than adding the `EFCore.NamingConventions` package.
|
||||
|
||||
**2026-07-17 (Phase 2 build) — Added `ExceptionHandlingMiddleware`, ahead of a dedicated phase for it.** `01_Full_Development_Plan.md` §7.1 flagged the missing global exception handler as a genuine gap and proposed fixing it "early in Phase 2, subject to approval." Approved and implemented as part of Phase 2 itself. This is now foundation (Rule 1.1-style) for every later phase.
|
||||
|
||||
**2026-07-17 (Phase 2 build) — `Database.SqlQuery<int>` requires the source column literally named `Value`.** Fixed by aliasing the column `AS ""Value""` in the raw SQL string. Worth knowing for any later phase writing a scalar raw-SQL repository method the same way.
|
||||
|
||||
**2026-07-17 (post-Phase-2 refactor) — New architectural rule: single controller, `AuthResponse` built in the Service layer.** User-directed change, applied retroactively to Phase 2 and made a standing rule for every phase going forward (`CLAUDE.md` Rule 7).
|
||||
|
||||
**2026-07-17 (pre-Phase-4) — Route regression found and fixed: routing convention simplified further.** The controller-level route is now just `[Route("api/v{version:apiVersion}")]` (the `[controller]` token removed entirely), so every action's own route template is just its path relative to `api/v1/`.
|
||||
|
||||
**2026-07-17 (pre-Phase-4) — New rule: payload parsing (`DataExtractor`) moved from Controller to Service.** Controller actions accepting a loosely-typed body now take `[FromBody] Dictionary<string, JsonElement> payload` and pass it straight to the Service method unexamined.
|
||||
|
||||
**2026-07-17 (pre-Phase-4) — New rule: look up by unique code, not numeric id (`CLAUDE.md` Rule 8).** Wherever an entity has a unique business-key column, single-resource path lookups use that code, not the internal numeric id. Scoped to path/lookup parameters only — FK reference fields in request bodies and internal recursive-CTE/tree logic stay numeric. Phase 3 was retrofitted from `{id}` to `{accountCode}`; Phase 4 was built with this from the start.
|
||||
|
||||
**2026-07-17 (Phase 4 build) — `UpdateAccountRequest.ParentAccountIdProvided` distinguishes "omitted" from "explicitly null."** Solved via a `ParentAccountIdProvided` bool set from `payload.ContainsKey("parentAccountId")`, alongside a manual `JsonValueKind.Null` check for the value itself.
|
||||
|
||||
**2026-07-17 (Phase 5 build) — Query-string parameters also bundled into a DTO, not passed as individual method parameters.** Extending the Phase 4 "no piecemeal parameters" preference to `[FromQuery]` collection-read endpoints as well as `[FromBody]` writes.
|
||||
|
||||
**2026-07-17 (Phases 6–8) — Three phases implemented and tested together, at the user's explicit direction.** This deviates from `CLAUDE.md`'s own "work phases strictly in order, one at a time" rule — flagged to the user before proceeding, who confirmed. Design decisions: period generation supports both "generate all up front" and "extend later" through one code path (`FiscalCalendarService.BuildPeriodsAsync`); Phase 8's lock action set is `LOCK`/`UNLOCK`/`SOFT_CLOSE`/`CLOSE`, always writing a `PeriodLockLog` row, with `CLOSED` periods only reopenable via `UNLOCK`.
|
||||
|
||||
**2026-07-18 (post-Phase-8) — New rule: enums for fixed discriminator sets (`CLAUDE.md` Rule 9), and `AccountingPeriod.Status`/`PeriodLockLog.Action` converted from `VARCHAR` to `integer`.** Enums apply only to inline discriminator columns with **no backing reference table** (`TaxType`/`AccountType`/`Currency` are exempted — they're real tables with FK relationships). C# has no string-backed enum, so representing these two columns as a true `enum` required converting them to `integer` (migration `ConvertPeriodStatusAndActionToInt`) — approved by the user as a deliberate deviation from the design doc's `VARCHAR` schema for these two columns specifically. Applying the migration against existing test data would have failed (`'OPEN'`/`'LOCK'` can't cast to `integer`), so the local dev database was dropped and recreated, then all Phase 6–8 tests re-run — all passed identically.
|
||||
|
||||
**2026-07-18 — `Currency` seed data expanded to ~140 ISO 4217 currencies, surfacing and fixing a real EF Core `HasData` bug.** Added a `NonBaseCurrency(...)` local helper and ~140 rows (migration `SeedIsoCurrencies`). Every currency seeded with `DecimalPlaces = 0` (JPY, KRW, VND, and 13 others) was silently inserted with `2` instead — EF Core's `HasData` diffing treats a seeded value equal to the CLR default for its type (`0` for `short`) as "unset" when the column also has `HasDefaultValue` configured, silently omitting it from the generated `INSERT`. Fixed by removing the column default entirely (migration `RemoveCurrencyDecimalPlacesDefault`) — nothing else relies on it. Re-verified via `psql`: 142 currencies, 16 at `decimal_places = 0`, 7 at `3`, exactly 1 base currency. **Worth remembering for any future `HasData` seed where a meaningful value can coincide with a CLR default (`0`, `false`, `null`) on a column that also has `HasDefaultValue` configured.**
|
||||
|
||||
**2026-07-18 — Phases 30–35 appended: user-identified gap, Tax/Fixed Assets/Bank/Budget have no read/list endpoints.** Every phase for those four modules was `POST`/`PUT`-only. Added as Phases 30–35 (Document 1 §6.3), appended rather than inserted. Phase 30 (`GET /api/v1/currencies`) implemented immediately; Phases 31–35 documented for implementation alongside their respective module's create phase.
|
||||
|
||||
**2026-07-18 — `CLAUDE.md` Rule 10 added: generic `audit_log` table, categorized `LEDGER`/`ADMINISTRATIVE`, Phase 36 (`GET /api/v1/audit-log`) appended.** Introduced externally (updated docs referenced a new Document 1 §4.3/§4.1 classification table and a Phase 36 not previously seen this session). Two decisions resolved with the user before implementing:
|
||||
- `audit_log.category`/`action` representation: `smallint` + explicit-valued C# enums (`Enums/AuditCategory.cs`, `Enums/AuditAction.cs`) — chosen over `VARCHAR`, since this is a brand-new table with no legacy data to migrate away from (unlike `PeriodStatus`/`PeriodLockAction`'s retrofit pain).
|
||||
- `CLAUDE.md` itself had a real duplication bug found in the same pass (the entire document, Rules 0–9, was repeated twice after the new Rule 10) — cleaned up, unrelated to the `audit_log` content itself.
|
||||
|
||||
**Retrofit executed 2026-07-18:** `Models/AuditLog.cs` (no separate `CreatedAt` — `OccurredAt` serves that role), `AppDBContext` `DbSet<AuditLog>` + config (no FK, by design — `entity_name`+`entity_id` is a loose reference), migration `AddAuditLog` (pure `CREATE TABLE`, no data loss). Retrofitted the 5 already-`☑ Done` write-phase Service methods (`AccountService.CreateAccountAsync`/`UpdateAccountAsync`, `FiscalCalendarService.CreateFiscalYearAsync`/`GeneratePeriodsAsync`/`LockPeriodAsync`) — each now inserts one `audit_log` row inside its existing `UoW` transaction. `PerformedBy` is `null` for all five (none of their request DTOs currently carry an actor-name field; the convention explicitly allows `null`). Re-tested all 5 endpoints end-to-end and verified the actual `audit_log` rows via `psql` — all 7 rows across the 5 endpoints matched expectations exactly, including `UpdateAccountAsync`'s dynamic category (row for a plain rename = `Administrative`, row for a re-parent = `Ledger`).
|
||||
|
||||
**2026-07-18 — `03_Progress_Tracker.md` found missing from disk mid-session.** Not renamed, not recoverable from git (never committed — this repo's only commit is the bare initial scaffold). Recreated from this session's own record of every prior update, per explicit user confirmation, with the `audit_log` retrofit folded in. If any historical entry above turns out to be inaccurate relative to the real codebase, that's a reconstruction gap, not a deliberate edit — flag it per Rule 0.4.
|
||||
|
||||
**2026-07-18 (Phases 9–10) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as Phases 6–8. Modeled closely on `AccountRepository`/`AccountService`'s recursive-CTE pattern (breadcrumb, descendant tree, depth check), simplified for cost centers: no cycle check (no re-parent/update phase exists for cost centers in the current plan) and no enforce-flag gate (only `COST_CENTER_MAX_DEPTH` is seeded — no `COST_CENTER_ENFORCE_MAX_DEPTH` counterpart to `COA_ENFORCE_MAX_DEPTH`), so the depth check runs unconditionally whenever a parent is given. **Retrofitted Phase 10's route from `{id}` to `{costCenterCode}` in Documents 1 and 2 before building** — `CostCenter.Code` is `UNIQUE` in the design doc, so Rule 8 applies, same as the earlier Phase 3 retrofit. Tested end-to-end: root/child creation, duplicate-code and missing-parent rejection, a real depth chain to 4 levels succeeding and a 5th level correctly rejected, plain get, `?expand=tree`, and 404 — all passed, plus the `Administrative`/`Create` `audit_log` rows verified via `psql`.
|
||||
|
||||
**2026-07-18 — User-directed de-duplication pass across `Repositories/` and `Services/`.** `AccountRepository`/`CostCenterRepository` had verbatim-shaped recursive-CTE SQL for ancestor-depth/breadcrumb/descendant-tree (and `WouldCreateCycleAsync`, Account-only); `AccountService`/`CostCenterService` had the same max-depth `system_config`-check-and-throw block repeated three times (including twice within `AccountService` itself). Extracted into `Repositories/RecursiveTreeQueries.cs` (parameterized by table/column name string constants, mapping a shared internal `TreeRow` into each repository's own unchanged public DTO) and `Services/DepthGuard.cs` (shared config-check helper) — new `CLAUDE.md` Rule 11 documents this and carves out the one exception to Rule 2's "no generic abstractions." No public API/JSON contract changed; re-verified Phase 2/3/4/9/10 end-to-end with no regressions. Also corrected a stale `CLAUDE.md` Rule 10 claim that `audit_log.category`/`action` are `smallint` — the `AddAuditLog` migration actually mapped them as plain `integer`; wording fixed to match the real schema.
|
||||
|
||||
**2026-07-18 (Phases 11–14) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as Phases 6–8 and 9–10.
|
||||
- **Route retrofit.** Phases 12–14 changed from `{id}` to `{journalNo}` before building — `JournalEntry.JournalNo` is named explicitly in `CLAUDE.md` Rule 8 as a unique-code lookup column, and these are all single-resource operations, same situation as the Phase 3/10 retrofits.
|
||||
- **New enums, decided before first use (not a data-bearing retrofit).** `JournalEntry.EntryStatus` (design doc: `VARCHAR(15)`) and `JournalCorrection.CorrectionType` (`VARCHAR(20)`) were converted to `integer` + explicit-valued enums (`Enums/JournalEntryStatus.cs`: `Draft=1, Posted=2, Reversed=3`; `Enums/JournalCorrectionType.cs`: `FullReversal=1, PartialAdjustment=2`) via migration `ConvertJournalEntryStatusAndCorrectionTypeToInt`, since Phase 11 was their first real write — same reasoning as `AuditCategory`/`AuditAction`, not the `PeriodStatus` retrofit-with-data case. Only the enum members these phases actually use were defined (design doc lists more — `PENDING_REVIEW`/`ADJUSTED`/`VOID`/`RECLASSIFICATION` — added later if a phase needs them). `journal_type`/`source_module` stay free-text `VARCHAR` (large, open-ended, cross-module tag sets no Service logic branches on).
|
||||
- **`JournalNo` auto-generated** by the Service as `JV-{year}-{6-digit sequence}` (design doc's own example format) — not client-supplied, since Document 2's request DTO doesn't carry one.
|
||||
- **Line currency defaults.** Since multi-currency posting isn't wired up (same reason Phase 18 is blocked), each line defaults to `CurrencyCode` = the referenced account's currency, `ExchangeRate = 1`, `BaseDebit`/`BaseCredit` = `DebitAmount`/`CreditAmount`.
|
||||
- **Genuine bug found and fixed foundationally: JSON serialization cycle.** Returning a `JournalEntry` with its `Lines` navigation collection threw a 500 (System.Text.Json throws on reference cycles by default; nothing had exercised a true bidirectional parent/collection navigation before this). Fixed by adding `ReferenceHandler.IgnoreCycles` to `AddControllers()` in `Program.cs` — a foundational fix (same category as `ExceptionHandlingMiddleware` in Phase 2), since the same navigation shape recurs in `FixedAsset`/`Budget`/`RecurringJournalTemplate`/`BankAccount` and their line tables (Phases 15/16/21/22/23/24/26/27).
|
||||
- **New repositories** (one per entity, per existing convention): `JournalEntryRepository`, `JournalEntryLineRepository`, `JournalCorrectionRepository`; reused `AccountRepository.GetByIdAsync` (postable check) and `AccountingPeriodRepository.GetByIdAsync` (period-lock check).
|
||||
- Tested end-to-end: plain create (no lines, and with inline lines), unbalanced-entry post rejection (400), double-post rejection (400), Phase 12 lines-set on both a plain draft and a `PartialAdjustment` draft, `FullReversal` create+post (mirrored lines, original flips to `Reversed`/`isReversed=true`), `PartialAdjustment` create+post (original untouched), correction against a non-`Posted` original (400), invalid `correctionType` (400), locked-period post rejection (400), non-existent account in a line (404), non-postable account in a line (400), and 404 for a missing `journalNo` on both GET and post.
|
||||
|
||||
**2026-07-18 (Phases 15–17) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as prior groups.
|
||||
- **New enum, decided before first use.** `RecurringJournalTemplate.RecurrencePattern` (design doc: `VARCHAR(20)`, MONTHLY/QUARTERLY/ANNUAL — a genuinely small closed set) converted to `integer` + `Enums/RecurrencePattern.cs` (`Monthly=1, Quarterly=2, Annual=3`) via migration `ConvertRecurrencePatternToInt`, same "clean slate" reasoning as the Phase 11-14 enums.
|
||||
- **Phase 16 explicitly reuses `IJournalEntryService.CreateAsync`** (per Document 2's own instruction), so the `Ledger`/`Create` audit row it needs is already written by that reused call — no second audit insert. Added `AccountingPeriodRepository.GetByDateAsync(DateOnly)` to resolve "today's" period, since the endpoint has no `periodId` in its request.
|
||||
- **Decided: generating a recurring entry advances `NextRunDate`** by the recurrence pattern (`+1 month`/`+3 months`/`+1 year`) — not stated in Document 2, but left as-is would let repeated `/generate` calls on the same date create duplicate entries indefinitely. Confirmed with the user before implementing. Verified via two consecutive `/generate` calls producing two entries and `NextRunDate` moving forward each time.
|
||||
- **Phase 17's `CashFlow` report type is a documented simplification, not full LKAS 7 compliance.** The design doc's own Section 11.5 spec depends on two things that don't exist anywhere in the actual schema: (1) an account-level tag for "non-cash adjustments" (depreciation/FX/provisions), and (2) a reliable way to identify which accounts are "cash" for opening/closing balance reconciliation (its own SQL comment excludes cash accounts via `account_type.name ILIKE '%cash%'`, but no seeded `account_type` row's name contains "cash" — the five seeded types are Asset/Liability/Equity/Income/Expense). Implemented instead: Operating = Profit for the Period (from the Phase 17 P&L query, i.e. no non-cash/working-capital adjustments), Investing/Financing = the existing `cash_flow_category` rollup, no opening/closing cash balance line at all. Confirmed with the user before implementing.
|
||||
- **`BudgetVsActual` has no real data to verify against yet** — `Budget`/`BudgetLine` tables exist (Phase 1) but there's no create endpoint until Phase 26. Verified structurally: correct 404 for a non-existent `budgetId`; real budgeted-vs-actual numbers remain unverified until Phase 26 exists.
|
||||
- **PDF rendering.** `QuestPDF` and `ClosedXML` were already referenced in the `.csproj` (no new dependency). Added `QuestPDF.Settings.License = LicenseType.Community` once in `Program.cs`. One shared, reflection-based table renderer handles every report type (per Document 2's "one shared rendering helper, not one per report type") rather than per-report layouts.
|
||||
- **PDF response shape decided:** rather than special-case the Controller to return raw binary (which would break `CLAUDE.md` Rule 7's "every endpoint returns `AuthResponse`"), `outputFormat=Pdf` returns the usual `AuthResponse` with `Data = { fileName, contentType, contentBase64 }` — clients base64-decode. Verified the returned bytes are a real PDF (`%PDF-1.4` header) after decoding.
|
||||
- New repositories/methods: `BudgetRepository` (new), `AccountRepository.GetTrialBalanceAsync`/`GetBalanceSheetAsync`/`GetProfitAndLossAsync`/`GetCashFlowCategoryMovementAsync`, `JournalEntryLineRepository.GetGeneralLedgerLinesAsync` — matching Document 2's own repository-placement guidance.
|
||||
- Tested end-to-end: `TrialBalance`/`BalanceSheet`/`GeneralLedger` against real posted test data (correct account-tree rollups and running balances), `ProfitAndLoss`/`CashFlow` against data with no Income/Expense accounts (correctly empty/zero, not an error), missing-required-parameter per report type (400, naming the parameter), invalid `reportType`/`outputFormat` (400), non-existent `budgetId` (404), and `outputFormat=Pdf` producing valid decoded PDF bytes.
|
||||
|
||||
**2026-07-18 — Phase 18 (FX Revaluation) resolved: removed permanently, not just blocked.** Phase 18 had been `⚠ Blocked` since Phase 1 pending a decision on how (or whether) to unblock it — see its original blocked-row note. The user confirmed the underlying product fact that resolves it: **the base/functional currency is a one-time initial-configuration choice — the client can never change it after setup, and non-base currencies are never used in this system at all.** Since FX revaluation only has meaning when an open balance exists in a non-base currency, and that can now never happen structurally, the endpoint has no possible purpose (not "blocked pending data" — permanently out of scope). Resolved as Option B from Document 2's original two options (remove the endpoint), rather than Option A (seed currency/rate data whenever needed) — Option A's premise never applies since there is no "whenever." Updated all three governing docs: `01_Full_Development_Plan.md` (Section 6.1 callout rewritten, Section 6.2 endpoint count 28→27, Section 4.3 audit row removed, overall endpoint/phase counts corrected), `02_Phase_By_Phase_Development_Plan.md` (Phase 18 section rewritten as removed, "How to Use" exception note updated), and this file (new `✕ Removed` status value added, Phase 18's row updated, summary counts corrected). Phase 18's slot is retired, not reused, per the "no renumbering, ever" policy. **Known consequence, not addressed further:** the multi-currency scaffolding already built (`exchange_rate`/`currency_rounding_rule` tables, ~141 of the 142 seeded `Currency` rows, `JournalEntryLine.CurrencyCode`/`ExchangeRate`/`BaseDebit`/`BaseCredit`) is now confirmed dead weight — always resolves to the single base currency / rate of 1, not a bug, just unused capability that was already built before this was known.
|
||||
|
||||
**2026-07-19 (Phases 19–22, 31–33) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as prior groups. Several real decisions resolved before implementing:
|
||||
- **New enums, decided before first use.** `TaxCode.CalculationBasis`/`AppliesTo` and `FixedAsset.DepreciationMethod`/`Status` all converted from `VARCHAR` to `integer` + explicit-valued enums (`TaxCalculationBasis`, `TaxAppliesTo`, `DepreciationMethod`, `FixedAssetStatus`) via migration `ConvertTaxAndFixedAssetEnumsAddGainLossAndRate`, same "clean slate" reasoning as every prior enum conversion this project — both tables were empty (Phases 19/21 never built before now). `FixedAssetTransactionType` (`Depreciate`/`Dispose`) is a pure in-memory dispatch enum for Phase 22's request, not a DB column — same treatment as `ReportType`.
|
||||
- **Tax code "overlap" rule resolved as a plain duplicate-code check.** Document 2 called for rejecting "overlapping `effective_from`/`effective_to` for the same `code`," but Phase 1 already put a hard unique index on `TaxCode.Code` — meaning two rows can never share a code at all, so the overlap scenario is structurally unreachable. Confirmed with the user: implemented as an ordinary `TaxCodeExistsAsync`-style duplicate-code check instead of a real date-range-overlap query.
|
||||
- **Two real schema gaps found and filled, both confirmed with the user first:** (1) `FixedAsset.DepreciationRatePercent` (nullable, required only when `DepreciationMethod = ReducingBalance`) — reducing-balance depreciation had no rate anywhere in the original schema. (2) `FixedAsset.GainLossAccountId` (required, same pattern as its other three account fields) plus a new required `ProceedsAccountId` on the Dispose request itself — asset disposal had no way to post a real balanced journal entry (no gain/loss account, no account for the cash/receivable proceeds). The disposal entry shape: Debit proceeds account, Debit accumulated depreciation, Credit asset account at cost, Debit/Credit the gain/loss account to balance — verified end-to-end with a real loss scenario (proceeds 9,000 vs NBV 11,000 → 2,000 loss correctly debited to the gain/loss account, all four lines balancing to zero).
|
||||
- **User-directed repository consolidation, a deviation from the "one repository per table" convention used everywhere else in this codebase.** A single `TaxRepository.cs` covers both `TaxType` and `TaxCode` (interface `ITaxRepository`); a single `FixedAssetRepository.cs` covers `FixedAsset`, `DepreciationSchedule`, and `AssetDisposal` (interface `IFixedAssetRepository`) — both confirmed explicitly by the user rather than assumed. Rationale: these are small, tightly-coupled table clusters within one domain that are only ever touched together by that domain's own Service, unlike `JournalEntry`/`JournalEntryLine`/`JournalCorrection`, which were kept separate. See `CLAUDE.md` Rule 2's updated note.
|
||||
- **Phase 22 reuses `IJournalEntryService.CreateAsync`+`PostAsync`** for both `Depreciate` and `Dispose` (same pattern as Phase 16), so the `Ledger`/`Create`+`Ledger`/`Post` audit rows those calls already write satisfy Phase 22's own `Ledger` audit requirement — no separate audit insert needed. `Depreciate` posts as of the period's `EndDate`; `Dispose` posts as of the supplied `disposalDate`, resolved to its containing period via the same `AccountingPeriodRepository.GetByDateAsync` added in Phase 16.
|
||||
- **Zero-amount journal lines are skipped when building the disposal entry** (e.g. no accumulated-depreciation line if depreciation was never run, no proceeds line if proceeds is 0) — `journal_entry_line`'s own `CK_journal_entry_line_one_sided` check constraint rejects a zero-amount line on either side, so an unconditional 4-line build would fail in exactly that case.
|
||||
- **Phases 31/32/33 (`GET /tax-types`, `GET /tax-codes`, `GET /fixed-assets`) implemented alongside 19/21**, per those phases' own pre-existing tracker notes — confirmed with the user rather than assumed, same as Phase 30 was implemented immediately back when it was added.
|
||||
- Tested end-to-end: tax code create (valid, duplicate, missing `effectiveFrom`, non-existent `taxTypeId`, invalid enum values), tax calculation (`Exclusive` basis, out-of-range date → 404), fixed asset create (`StraightLine`, `ReducingBalance` incl. missing-rate rejection, duplicate code, `residualValue > acquisitionCost`, non-postable account), depreciation (correct `StraightLine` and `ReducingBalance` amounts, auto-posted journal, locked-period rejection), disposal (balanced entry with a real loss, asset flips to `Disposed`), both transaction types rejected once disposed, non-existent asset code, invalid `transactionType`, and all three list endpoints.
|
||||
|
||||
**2026-07-18 (Phases 23–25, 34) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as prior groups. No new enums needed (neither `BankAccount` nor `BankStatementLine` has a fixed discriminator column). Real decisions, all confirmed with the user first:
|
||||
- **User-directed repository consolidation, same pattern as Tax/FixedAssets** (`CLAUDE.md` Rule 2's note): a single `Repositories/BankRepository.cs` covers both `BankAccount` and `BankStatementLine` (interface `IBankRepository`), rather than one file per table.
|
||||
- **First file-upload endpoint in this project.** Phase 24 takes `multipart/form-data`, not JSON — the Controller action extracts a `Stream` from the bound `IFormFile` and passes that into the Service, keeping the ASP.NET Core-specific `IFormFile` type out of the Service layer (same boundary principle as `Dictionary<string, JsonElement>` for loose JSON bodies). Document 2's note about reusing `Helpers/DataExtractor` for row-parsing didn't apply literally — that helper is JSON-specific (`JsonElement`), not compatible with `ClosedXML`'s own cell-value API — so row parsing uses `ClosedXML` directly instead.
|
||||
- **Import column layout, not specified anywhere, decided with the user:** a 3-column format with a header row — `TxnDate | Description | Amount`.
|
||||
- **Malformed-row handling, decided with the user:** valid rows import; invalid rows (unparseable date or amount) are skipped and reported back in the response with their row number and reason, rather than the whole batch failing on one bad row. A structurally corrupt/non-`.xlsx` file still returns `400` outright, since there's nothing to partially salvage in that case. Verified with a 3-row test file (2 valid, 1 with a non-numeric amount) — 2 imported, 1 reported correctly — and separately with a genuinely corrupt file.
|
||||
- **Phase 25's "amounts must match" rule, decided with the user:** the matched journal entry must be `Posted` (reconciling against a draft that could still change doesn't make sense), its lines are filtered to the ones posted to the bank account's own `GlAccountId`, and their net `debit - credit` movement is compared against the statement line's `Amount`. Verified with a real match, a real mismatch, an unposted-entry rejection, and a double-reconcile rejection.
|
||||
- **Phase 34 (`GET /bank-accounts`) implemented alongside Phase 23**, per that phase's own pre-existing tracker note, confirmed with the user rather than assumed — same as Phases 30–33 before it.
|
||||
|
||||
**2026-07-18 (Phases 26, 27, 35) — Built and tested together, at the user's explicit direction.** Same "combine phases" deviation as prior groups. No new enums, no schema gaps — the simplest phase group so far.
|
||||
- `{id}` stays numeric for Phase 26/27's routes — `Budget` has no unique business-key column, so `CLAUDE.md` Rule 8 doesn't apply (same reasoning as `RecurringJournalTemplate`).
|
||||
- **`IBudgetRepository` extended, not duplicated** — it already existed from Phase 17's `BudgetVsActual` report (`GetByIdAsync`/`GetBudgetVsActualAsync`); added `AddAsync`, `AddLinesAsync`, `GetAllAsync` to the same file rather than creating a second repository for the same table, consistent with `CLAUDE.md` Rule 2's de-duplication direction.
|
||||
- **Phase 27 appends lines rather than replacing them**, per Document 1's own note that budget lines "are added incrementally over time, not fixed at creation" — a deliberate difference from Phase 12 (`SetLinesAsync`), which replaces a journal entry's lines wholesale. Account/period/cost-center existence checks only — no postable check, since Document 2 doesn't call for one here and budgeting against a summary account is reasonable.
|
||||
- Tested end-to-end: budget create (valid, non-existent `fiscalYearId` → 404), lines add (valid multi-line add, non-existent account/period/cost-center → 404 each, non-existent budget → 404), the list endpoint, and — as a bonus end-to-end check — re-verified Phase 17's `BudgetVsActual` report now returns real numbers against real budget data, closing the gap that phase's own tracker note flagged as "structurally verified only" at the time.
|
||||
|
||||
**2026-07-18 (Phases 28, 29, 36) — final phase group, built and tested together at the user's explicit direction.** No new enums, no schema gaps. `{key}` in Phase 28's route is `SystemConfig`'s own string PK, not a numeric id, so it already satisfies Rule 8 without any retrofit.
|
||||
- **`is_editable`/`is_locked` interpretation, confirmed with the user.** Document 2's literal wording ("reject if `is_editable=false` AND `is_locked=true`") doesn't hold up as the full rule — under a strict AND reading, a key with `is_editable=false, is_locked=false` would be editable, which defeats the purpose of the `is_editable` flag. Implemented instead: reject if `!IsEditable || IsLocked`, and every successful write sets `IsLocked = true` afterward — making every `system_config` key edit-once via this endpoint, matching the Notes column's "the *one allowed write* for a one-time config" phrasing literally. Verified: first edit succeeds and locks the key; a second edit attempt on the same key is rejected (400).
|
||||
- **Per-key audit classification implemented as a static lookup dictionary** in `SystemConfigService` (all three current keys — `COA_MAX_DEPTH`, `COST_CENTER_MAX_DEPTH`, `COA_ENFORCE_MAX_DEPTH` — map to `Ledger`, per Document 1 §4.3), defaulting any key not in the dictionary to `Administrative` rather than silently inheriting `Ledger` — per Document 1's own explicit warning that "a future purely-cosmetic key wouldn't automatically inherit LEDGER."
|
||||
- **`AuditLogRepository.ListAsync` takes individual parameters, not a wrapped filter object** — this was a real back-and-forth mid-implementation: first attempt used 8 discrete parameters, which the user flagged as too many; a consolidated `AuditLogFilter` type was tried next, which the user also rejected, clarifying the individual-parameter shape was correct after all for a *Service → Repository* call specifically (matching `AccountRepository.SearchAsync`'s existing 5-parameter precedent) — the "pass the whole object through" preference applies to *Controller → Service* (per the Phase Template convention for `[FromQuery]` DTOs), not necessarily every layer boundary below that.
|
||||
- **Invalid `category`/`action` filter values resolve to an empty result set, not an error** — per Document 2's own explicit note for this phase. A typo'd `category` is parsed and silently dropped (falling back to "no valid category" rather than "match everything" when the original list was non-empty); a typo'd `action` short-circuits the whole query to an empty page rather than hitting the database with an unsatisfiable filter.
|
||||
- Tested end-to-end: system config list (all 3 seeded keys), config update + auto-lock, re-edit-after-lock rejection (400), non-existent key (404), audit log with no filters (54 rows — every audit insert from every phase built this session), `category=LEDGER`/`ADMINISTRATIVE` split (41/13, summing to 54), invalid `category`/`action` (empty result, `200`), `entityName`/`action`/`performedBy`/date-range filters (including combinations), and default paging.
|
||||
|
||||
**This closes out all 36 phase slots in `01_Full_Development_Plan.md` / `02_Phase_By_Phase_Development_Plan.md`** — 35 `☑ Done`, 1 `✕ Removed` (Phase 18). See the Overall Progress Summary above.
|
||||
|
||||
---
|
||||
|
||||
## Refactor Phases R1–R6 — Post-Completion Architecture Refactor (2026-07-19)
|
||||
|
||||
User-directed, started after all 36 original phase slots were resolved. **Not a new endpoint or business rule** — a file-organization and boilerplate-extraction pass across `Models/`, `Repositories/`, `Services/`, and the Controller, plus moving interfaces to a new `Interfaces/` folder (reversing `CLAUDE.md` Rule 4). Full design: `01_Full_Development_Plan.md` Section 10. Execution plan: `02_Phase_By_Phase_Development_Plan.md`'s Refactor Phases section. Tracked here the same way as Phases 1–36 — status updated immediately after each phase's acceptance test passes, not batched.
|
||||
|
||||
| Phase | What | Status | Started | Completed | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| R1 | Consolidate `Models/` (25 → 12 files, domain-grouped) | ☑ Done | 2026-07-19 | 2026-07-19 | Pure move, no property/attribute changes. Solution builds clean |
|
||||
| R2 | Consolidate `Repositories/` (17 → 14 files: merge FiscalCalendar and JournalEntry clusters) + `RepositoryHelpers.cs` | ☑ Done | 2026-07-19 | 2026-07-19 | Landed together with R4 per the sequencing note in Document 2 (R2 alone would not compile). See Deviations for method-naming decisions |
|
||||
| R3 | Extract repository interfaces to `Interfaces/Repositories/` | ☑ Done | 2026-07-19 | 2026-07-19 | 12 new interface files under `Interfaces/Repositories/`, namespace `General_Ledger_Service.Interfaces.Repositories` |
|
||||
| R4 | `Services/`: add `ResponseHelpers.cs`, apply it, update constructors for R2's merges | ☑ Done | 2026-07-19 | 2026-07-19 | Also carried the `AuthResponse` → `ApiResponse` rename (see Deviations) since every Service method's return type was already being touched here |
|
||||
| R5 | Extract service interfaces to `Interfaces/Services/` | ☑ Done | 2026-07-19 | 2026-07-19 | 13 new interface files under `Interfaces/Services/`, namespace `General_Ledger_Service.Interfaces.Services` |
|
||||
| R6 | Controller `Respond()` helper + `Program.cs` DI wiring + full clean-build regression pass | ☑ Done | 2026-07-19 | 2026-07-19 | Clean build (`dotnet clean && dotnet build`) confirmed 0 errors / 0 warnings. Live smoke test against the real local Postgres database — see Deviations |
|
||||
|
||||
**Refactor progress: 6 of 6 done — R1–R6 complete.** This closes out the post-completion architecture refactor.
|
||||
|
||||
### Deviations (Refactor Phases)
|
||||
|
||||
**2026-07-19 (R1–R4) — `AuthResponse` renamed to `ApiResponse`, moved from `DTOs/Auth/AuthResponse.cs` to `DTOs/Common/ApiResponse.cs`.** User-directed, mid-R4: this service does not perform authentication/authorization itself (JWT bearer setup in `Program.cs` is conditional/unused scaffolding), so the name `AuthResponse` was misleading. Same four properties (`StatusCode`/`Success`/`Message`/`Data`), same role (`CLAUDE.md` Rule 1 foundation, every endpoint's response envelope) — pure rename, no shape change. Scope: the whole project, not just the files R1–R4 already touched — updated every `Services/*.cs` file, `Helpers/DataExtractor.cs`, `Middleware/ApiKeyMiddleware.cs`, `Middleware/ExceptionHandlingMiddleware.cs`, and all governing docs (`CLAUDE.md`, `01_Full_Development_Plan.md`, `02_Phase_By_Phase_Development_Plan.md`, `PROJECT_SPECIFICATION.md`). `Controllers/GeneralLedgerController.cs` needed no change — it only ever holds the response in a `var`, never names the type. Historical Deviations entries above this section that say `AuthResponse` are left as-is (accurate record of what the type was called at the time); only `CLAUDE.md`'s standing rules and the plan documents' prescriptive text were updated to the new name.
|
||||
|
||||
**2026-07-19 (R2) — Repository method naming for the two merged clusters.** Per the Tax/FixedAsset precedent, every method in a merged repository is entity-qualified rather than reused verbatim (avoids a same-signature clash — `FiscalYearRepository.GetByIdAsync(int)` and `AccountingPeriodRepository.GetByIdAsync(int)` would otherwise collide in one class). `IFiscalCalendarRepository`: `GetFiscalYearByIdAsync`, `HasOverlappingFiscalYearRangeAsync`, `AddFiscalYearAsync`, `GetPeriodByIdAsync`, `GetPeriodByDateAsync`, `GetPeriodsByFiscalYearIdAsync`, `AddPeriodRangeAsync`, `AddPeriodLockLogAsync`. `IJournalEntryRepository`: `GetJournalEntryByIdAsync`, `GetJournalEntryByJournalNoAsync`, `CountJournalEntriesForYearAsync`, `AddJournalEntryAsync`, `DeleteJournalEntryLinesByJournalIdAsync`, `AddJournalEntryLinesAsync`, `GetGeneralLedgerLinesAsync` (name unchanged, no clash), `AddJournalCorrectionAsync`, `GetJournalCorrectionByCorrectionJournalIdAsync`. Every call site across `FiscalCalendarService`, `JournalEntryService`, `RecurringTemplateService`, `FixedAssetService`, `BudgetService`, `BankService`, and `ReportService` updated accordingly — confirmed via a clean `dotnet build` (0 errors, 0 warnings).
|
||||
|
||||
**2026-07-19 (R2) — `RepositoryHelpers.cs` scope.** Applied only to the three literal one-line shapes named in Document 1 Section 10.4 (`GetByIdAsync` via `FindAsync`, `ExistsAsync` via `AnyAsync(predicate)`, `AddAsync` via `Add`+return) — across `AccountRepository`, `CostCenterRepository`, `FiscalCalendarRepository`, `JournalEntryRepository`, `RecurringJournalTemplateRepository`, `TaxRepository`, `FixedAssetRepository`, `BankRepository`, `BudgetRepository`. Deliberately **not** applied to `SystemConfigRepository.GetByKeyAsync` (uses `FirstOrDefaultAsync`, not `FindAsync`, in the original code — left as-is rather than silently changing its query shape) or `AuditLogRepository.AddAsync` (returns `Task`, not `Task<AuditLog>` — a genuine signature mismatch with the shared helper, not forced through it).
|
||||
|
||||
**2026-07-19 (R5) — Mechanical, one-for-one interface extraction.** All 13 domain Service interfaces (`IAccountService` through `IAuditLogService`) moved to `Interfaces/Services/`, namespace `General_Ledger_Service.Interfaces.Services`. Every consuming file updated: each Service implementation file (`using General_Ledger_Service.Interfaces.Services;` added), `Controllers/GeneralLedgerController.cs`, and `Program.cs`. `DepthGuard.cs` and `ResponseHelpers.cs` untouched (no interface — internal static helpers, never injected).
|
||||
|
||||
**2026-07-19 (R6) — Controller `Respond()` helper + full clean-build + live smoke test.** Added `private IActionResult Respond(ApiResponse response) => StatusCode(response.StatusCode, response);` to `GeneralLedgerController`; every one of its ~40 actions rewritten as a one-line expression body calling `Respond(await _xService.YAsync(...))`, replacing the old two-line `var response = ...; return StatusCode(...)` pattern. `Program.cs` gained `using General_Ledger_Service.Interfaces.Services;` for its `AddScoped` service registrations (the repository-side equivalent, `Interfaces.Repositories`, was already added in the R2/R3 pass). `dotnet clean && dotnet build`: 0 errors, 0 warnings. **Live regression check**, not just a compile check: started the app against the real local Postgres database (`ApiKeyMiddleware` is still commented out in `Program.cs` from earlier in the project, so no key needed) and hit 8 read endpoints spanning every module touched by the R2 repository merges and every layer touched by R1–R5 — `GET /currencies`, `/system-config`, `/tax-types`, `/accounts`, `/reports?reportType=TrialBalance`, `/journal-entries/JV-2026-000001` (exercises the merged `JournalEntryRepository`, including its `Lines` include), `/fixed-assets`, `/budgets`, `/bank-accounts`. All returned `200` with the same `ApiResponse`-shaped envelope and correct data pulled from rows created during this project's earlier phases — confirming no regression from the file moves, repository merges, or the `AuthResponse`→`ApiResponse` rename. Did not exercise write endpoints (`POST`/`PUT`) live, to avoid mutating persisted dev data as a side effect of a documentation/refactor pass; write-path correctness for the merged repositories is instead covered by the clean build (every call site type-checks against the renamed methods) and by every write endpoint's own original Phase 1–36 acceptance test, none of which changed in shape or logic during R1–R6. Dev server stopped afterward (verified via a failed follow-up request).
|
||||
|
||||
**This closes out the post-completion architecture refactor — R1–R6, all `☑ Done`.** `Models/`, `Repositories/`, `Services/`, and the Controller are reorganized per `01_Full_Development_Plan.md` Section 10; interfaces live in `Interfaces/Repositories/` and `Interfaces/Services/` per `CLAUDE.md` Rule 4 (reversed); the response envelope is `ApiResponse` (`DTOs/Common/ApiResponse.cs`), not `AuthResponse`. No public API contract, request/response shape, or database schema changed anywhere in this refactor.
|
||||
|
||||
---
|
||||
|
||||
## Post-Refactor Change — Optional Paging, No Default `page`/`pageSize` (2026-07-19)
|
||||
|
||||
**User-directed, a genuine behavior/contract change** — unlike R1–R6, this is not a zero-behavior-change pass. Affects the two paginated `GET` endpoints: `GET /api/v1/accounts` and `GET /api/v1/audit-log`.
|
||||
|
||||
**Before:** an omitted `page`/`pageSize` silently defaulted to `page=1, pageSize=20` — a caller that wanted "everything" had no way to ask for it; they'd always get (at most) the first 20 rows back with no indication more existed unless they inspected `totalCount`.
|
||||
|
||||
**After:** pagination is only applied when the caller supplies **both** `page` and `pageSize`. If either is omitted (or non-positive), no `Skip`/`Take` is applied at all — the full filtered result set is returned, and the response's `page`/`pageSize` fields are `null` (not a fabricated number), honestly reflecting that no slicing happened. Supplying only one of the two (e.g. `pageSize=5` with no `page`) is likewise treated as "not enough to paginate" and returns everything, not a partial/guessed slice.
|
||||
|
||||
**Implementation:**
|
||||
- New shared helper `RepositoryHelpers.ApplyPaging<T>(IQueryable<T> query, int? page, int? pageSize)` — applies `Skip`/`Take` only when both are non-null; otherwise returns the query untouched. Used by both `AccountRepository.SearchAsync` and `AuditLogRepository.ListAsync` (their `page`/`pageSize` parameters changed from `int` to `int?`; interfaces updated to match).
|
||||
- `AccountListResponse.Page`/`PageSize` and `AuditLogListResponse.Page`/`PageSize` changed from `int` to `int?`.
|
||||
- `AccountService.ListAccountsAsync` and `AuditLogService.ListAsync`: removed the `is > 0 ? value : 1/20` default-substitution; a non-positive supplied value is normalized to "not supplied" (safety, not a default), and the response only echoes back `page`/`pageSize` when pagination was actually applied (`page.HasValue && pageSize.HasValue`) — a `pageSize=5` request with no `page` returns the full list and `page: null, pageSize: null`, not a misleading `pageSize: 5` next to 54 returned rows.
|
||||
- Every other filter on these two endpoints (`accountTypeId`/`isActive`/`isPostable`/`category`/`entityName`/`action`/`performedBy`/`from`/`to`) was already optional-and-unforced — this change brings `page`/`pageSize` in line with that same pattern rather than singling out paging for a hidden default.
|
||||
|
||||
**Verified live** against the real local Postgres database: `GET /accounts` with no params → all 8 rows, `page`/`pageSize` both `null`; `?page=1&pageSize=2` → 2 rows, `page:1, pageSize:2`. `GET /audit-log` with no params → all 54 rows; `?pageSize=5` alone (no `page`) → still all 54 rows, `page`/`pageSize` both `null` (confirmed **not** silently defaulting `page` to 1); `?page=1&pageSize=5` → exactly 5 rows, `page:1, pageSize:5`. Clean build: 0 errors, 0 warnings.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-19 — Exact `HasData` seed values formalized (Document 1 §5.2–5.3, this file's new step 1.8); ER diagram assistant-error corrected
|
||||
|
||||
User asked for the exact `system_config` seed values (key, initial value, whether changeable) plus every other table's default/launch data, consolidated somewhere usable as a direct source for `AppDBContext.OnModelCreating`. Added `01_Full_Development_Plan.md` §5.2 (`account_type`, `system_config`, `tax_type` — full row-by-row values) and §5.3 (`currency` — shape + representative rows, not all 142 inline, plus the `DecimalPlaces`/`HasData`-default bug called out explicitly since it's the kind of thing worth knowing before writing a similar seed in the future). Added this file's step 1.8 as the corresponding tracker entry — **documentation formalization only, no code change**, since all four tables were already seeded and verified working; the values were transcribed from what's already live plus this file's own Deviations log, not newly invented.
|
||||
|
||||
While doing this, corrected two stale claims found in the process: Document 2's Phase 1.7 acceptance criteria and this file's own 1.7 note both still said `currency` seeds "exactly 1 row" — true on 2026-07-17, superseded by the 142-row ISO 4217 expansion the very next day, never updated. Fixed both.
|
||||
|
||||
**Separately, an assistant-caused error from the prior session was found and corrected.** In that session, the user re-uploaded documents (round 2) and the assistant incorrectly concluded `audit_log` was missing from them — it wasn't; Document 1 was byte-identical to the round-3 upload used in *this* session, both containing the full `audit_log` schema/classification work. The assistant had diffed against a stale local copy rather than the actual upload, wrongly reverted the ER diagram to 24 tables (removing `audit_log`) as a result, and delivered that incorrect diagram. Caught and fixed in this session: the ER diagram (`.drawio`, `.mmd`, rendered `.pdf`/`.png`) is restored to 25 entities with `audit_log` present, matching the documents' actual, unchanged state. No document content needed correcting for this part — only the diagram, which was the only artifact affected by the earlier mistake.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-19 — Post-refactor documentation/diagram reconciliation pass
|
||||
|
||||
User asked to cross-check `01_Full_Development_Plan.md`, `02_Phase_By_Phase_Development_Plan.md`, `03_Progress_Tracker.md`, `Accounting_System_ER_Diagram.drawio`, and `er_diagram.mmd` against each other and against the actual current schema/code, and fix whatever had drifted. Four real issues found and fixed:
|
||||
|
||||
- **`02_Phase_By_Phase_Development_Plan.md`'s header endpoint count was wrong.** It read "28 API endpoints in the original sequence... 35 API endpoints, 36 phases total" — an off-by-one that matched neither `01_Full_Development_Plan.md` §6.2 ("27... 34...") nor this file's own summary (line 14, "27 API endpoints... 34 API endpoints, 36 phases total"). Corrected to 27/34 to match both.
|
||||
- **`01_Full_Development_Plan.md`'s diagram-sync note was stale.** It still said the ER diagrams were "not yet updated for `audit_log`... still reflects 24 tables" — but per the entry directly above this one, that was already fixed earlier in the project (both diagram files have carried `audit_log`, 25 entities, since the assistant-error correction). Updated the note to reflect current reality and to record today's further sync work (below) instead.
|
||||
- **`01_Full_Development_Plan.md` §4.2 named the wrong EF Core API** for the recursive-CTE queries (said `FromSqlInterpolated`; the actual code — `Repositories/RecursiveTreeQueries.cs` — uses `Database.SqlQuery<T>`/`SqlQueryRaw<T>`). Corrected, with a pointer to the shared helper and `CLAUDE.md` Rule 11.
|
||||
- **Both ER diagrams were out of sync with real schema changes made during the build**, none of which had ever been reflected back into either diagram file: every column converted from `varchar` to an `integer`-backed enum (Rule 9 — `accounting_period.status`, `period_lock_log.action`, `journal_entry.entry_status`, `journal_correction.correction_type`, `recurring_journal_template.recurrence_pattern`, `tax_code.calculation_basis`/`applies_to`, `fixed_asset.depreciation_method`/`status`, `audit_log.category`/`action`); `fixed_asset`'s two later-added columns (`gain_loss_account_id`, `depreciation_rate_percent` — Phase 21/22 schema-gap fix); and `journal_entry.narration` (the Phase 1 approved addition, `01_Full_Development_Plan.md` — never made it into the diagram at all, in either file, even before this).
|
||||
|
||||
**Fixed in `er_diagram.mmd`** — all of the above, applied directly (it's a compact, safely-editable text file). **Left as-is in `Accounting_System_ER_Diagram.drawio`** — same issues, but that file is a single-line-per-entity HTML/XML blob (~190 lines, each one large) that can't be safely patched with targeted string edits without visually re-verifying every affected cell, and reading+rewriting it in full would cost roughly 150K+ tokens for one visual artifact. Flagged explicitly in `01_Full_Development_Plan.md`'s reference-documents note rather than either silently skipped or riskily edited blind — regenerate it from the now-current `.mmd` when convenient, per that document's own long-standing allowance for this exact situation.
|
||||
|
||||
No code changed in this pass — every fix was in the four Markdown/diagram files themselves, correcting documentation to match code and migrations that were already correct.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-19 — New feature: dev-only API key generator (`01_Full_Development_Plan.md` §11)
|
||||
|
||||
User-directed. `ApiKeyMiddleware` had no companion way to actually produce the `ApiKey:Value` it checks — added `POST /api/v1/dev-tools/generate-api-key`, backed by a new `Interfaces/Services/IDevToolsService.cs` + `Services/DevToolsService.cs` (no repository, no UoW — stateless). Generates a 256-bit random key (`RandomNumberGenerator.GetBytes(32)`, hex) and returns it; never persists it anywhere — still a manual copy-into-`appsettings`/core-backend step, same as `Jwt:SigningKey` always has been.
|
||||
|
||||
**Two independent guards, confirmed working live, not just by inspection:**
|
||||
|
||||
- `DevToolsService.GenerateApiKey()` throws `ForbiddenException` unless `IHostEnvironment.IsDevelopment()`. First test attempt (`ASPNETCORE_ENVIRONMENT=Production dotnet run`) appeared to fail — the endpoint still returned a key — but the actual cause was `dotnet run` loading `Properties/launchSettings.json`, which pins the profile's own `ASPNETCORE_ENVIRONMENT=Development` and silently overrides the shell's env var; not a code defect. Re-tested with `dotnet run --no-launch-profile`, confirmed via the startup log (`Hosting environment: Production`): the endpoint correctly returned `403 Forbidden`. Worth remembering for any future environment-dependent behavior tested via `dotnet run` rather than the published `.dll` directly.
|
||||
- `Middleware/ApiKeyMiddleware.cs`'s `ExcludedPathPrefixes` gained `/api/v1/dev-tools`, so the bootstrap endpoint stays reachable without a key even once one is configured and the middleware enforcing it — otherwise there'd be no way to ever generate the first key. Confirmed live: the endpoint succeeds with no `X-Api-Key` header at all, while every other endpoint correctly 401s without one and 200s with the configured dev key.
|
||||
|
||||
**Noted in passing, not part of this change:** `Program.cs`'s `app.UseMiddleware<ApiKeyMiddleware>()` line, previously commented out for the whole session, is now active (changed outside this conversation) — every endpoint from here on genuinely requires `X-Api-Key` to be reachable, which is relevant context for the Postman collection / API reference doc work started next.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-19 — New deliverables: full Postman collection + API reference/scenarios doc
|
||||
|
||||
User-directed: a Postman collection covering every endpoint's success and failure variations, plus a companion document with the full request/response catalog, the internal Controller→Service→Repository flow for each endpoint, and worked end-to-end scenarios. Both actually exercised against the real local Postgres dev database (Development environment, `ApiKeyMiddleware` enabled) while being written — not written from code inspection alone.
|
||||
|
||||
**New files:**
|
||||
- `General_Ledger_Service.postman_collection.json` — Postman Collection v2.1 schema, 15 folders (one per module, matching the API's own domain grouping, plus Dev Tools and Status & Health), **138 requests total** covering every one of the 36 actions across the original 27 endpoints + 6 appended list endpoints + audit-log + the new dev-tools endpoint, each with its documented success case(s) and every distinct failure branch (validation, not-found, business-rule rejection). Collection-level `{{baseUrl}}`/`{{apiKey}}` variables and an `apikey`-type auth block so every request sends `X-Api-Key` automatically. Validated as well-formed JSON after every module was added, not just at the end.
|
||||
- `04_API_Reference_And_Scenarios.md` — per-module sections (mirroring the Postman folders) each with: a full request-field table, a response table covering every success/failure case with its exact `status`/`message`, and a "Flow" paragraph naming the exact Service/Repository methods involved in call order. Closes with 11 worked end-to-end scenarios (double-entry posting via both the inline-lines and separate-lines paths, full-reversal correction, partial-adjustment correction, fiscal year setup + period lock, recurring template lifecycle, fixed asset register→depreciate→dispose, bank import+reconcile, tax calculation, budget vs actual, system-config one-time lock, and API-key bootstrapping) built from the same live request/response pairs used to populate the Postman collection.
|
||||
|
||||
**Real findings from live testing, not assumptions:**
|
||||
- **Every error response (`ExceptionHandlingMiddleware`, `ApiKeyMiddleware`) serializes with PascalCase keys (`StatusCode`, `Success`, `Message`, `Data`), while every success response serializes with camelCase (`statusCode`, `success`, ...).** Cause: both middleware classes call `JsonSerializer.Serialize(response)` with no options, falling back to `System.Text.Json`'s true PascalCase default, while MVC's controller pipeline uses the project's configured camelCase policy. Documented prominently in `04_API_Reference_And_Scenarios.md`'s "Known Inconsistency" section and flagged to the user — not fixed, since it touches two Rule-1 foundation components (`CLAUDE.md` Rule 1) and needs an explicit decision first.
|
||||
- **`GET /budgets` never returns a budget's `lines`** — `BudgetRepository.GetAllAsync` doesn't eager-load the `Lines` navigation (unlike the `Add Lines` response, which does). `GET /reports?reportType=BudgetVsActual` is the only place a budget's lines appear with real figures. Documented, not changed.
|
||||
- **There is no `GET` endpoint for fiscal years or periods anywhere in the API** — only create/generate/lock. The only way to discover an existing id is to have created it in the same session or read it off another response that embeds it. Documented as a known gap in the Fiscal Calendar module section.
|
||||
- Confirmed live (not just by reading the code) that `Set Lines` on Journal Entries **replaces** wholesale while `Add Lines` on Budget **appends** — a deliberate difference between the two, verified by actually calling `Add Lines` twice on the same budget and observing 3 lines afterward, not 1.
|
||||
|
||||
**Test data used:** accounts `9001`/`9002`, fiscal year `FY2035-Doc` (id 5) with 12 auto-generated periods, cost centers `CC-DOC-1`/`CC-DOC-2`, tax code `VAT18-DOC`, fixed assets `FA-DOC-1`/`FA-DOC-2`, a bank account against GL account `1001`, and a real `.xlsx` file generated via a throwaway ClosedXML scratch console project (same pattern as the Phase 24 build, deleted afterward) for the bank-import tests — all left in the dev database as real, inspectable records rather than cleaned up, consistent with how every other phase's manual testing in this project has been handled.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-20 — Retrofit: unique codes (`accountCode`, cost center/asset/tax codes) are now system-generated and immutable (`CLAUDE.md` Rule 8.1, `01_Full_Development_Plan.md` §12)
|
||||
|
||||
User-directed: `Account.AccountCode`, `CostCenter.Code`, `FixedAsset.AssetCode`, and `TaxCode.Code` were client-supplied on their `Create*Request` DTOs (and `AccountCode` was editable via `PUT /accounts/{accountCode}`) — inconsistent with `JournalEntry.JournalNo`, which was already system-generated and immutable. Checked `Accounting_System_Design_LKAS.md`, `01_Full_Development_Plan.md`, and `02_Phase_By_Phase_Development_Plan.md` first, per Rule 0/this rule's own "find an existing scheme before inventing one" instruction — none of the three documents specify a generation algorithm for these four columns (just `VARCHAR UNIQUE NOT NULL`), so a scheme was designed fresh and confirmed with the user before implementation (see `CLAUDE.md` Rule 8.1 for the exact per-entity format).
|
||||
|
||||
**Code changes (all four entities, same shape):**
|
||||
|
||||
- `Create*Request` DTOs (`CreateAccountRequest`, `CreateCostCenterRequest`, `CreateFixedAssetRequest`, `CreateTaxCodeRequest`) lost their code field entirely; `UpdateAccountRequest` lost `AccountCode`.
|
||||
- Each Service (`AccountService.CreateAccountAsync`, `CostCenterService.CreateCostCenterAsync`, `FixedAssetService.CreateAsync`, `TaxService.CreateTaxCodeAsync`) now generates the code internally, right before the entity is constructed, using small new repository counting methods (`CountRootAccountsByTypeAsync`/`CountChildAccountsAsync`, `CountRootCostCentersAsync`/`CountChildCostCentersAsync`, `CountAssetsAsync`, `CountTaxCodesByTypeAsync`).
|
||||
- The now-dead `CodeExistsAsync` (Account, CostCenter, FixedAsset) and `TaxCodeExistsAsync` (Tax) uniqueness-conflict checks were deleted from both the repository implementations and their interfaces — there's no user-supplied value left to conflict with, and nothing else in the codebase referenced them (verified via a full-repo grep after the edits).
|
||||
- `AccountService.UpdateAccountAsync` had its entire accountCode-change branch removed (the `accountCode` payload key is simply never read anymore) — the code is now immutable for the lifetime of the account.
|
||||
- No database schema or migration change — all four columns stay the same `VARCHAR` type/length; only where the value comes from changed.
|
||||
|
||||
**Verified:** `dotnet build` — 0 warnings, 0 errors. Full-repo grep for `request.AccountCode`, `request.Code`, `request.AssetCode`, `.CodeExistsAsync`, `TaxCodeExistsAsync` — no remaining references.
|
||||
|
||||
**Docs updated as part of this retrofit:** `CLAUDE.md` (new Rule 8.1), `01_Full_Development_Plan.md` (new §12), `02_Phase_By_Phase_Development_Plan.md` (Phase 2's worked example + Phases 4/9/19/21's summary-table rows), `04_API_Reference_And_Scenarios.md` (Accounts/Cost Centers/Tax/Fixed Assets sections + Scenario 6), `General_Ledger_Service.postman_collection.json` (code fields removed from the four affected `Create*` request bodies; hardcoded test codes like `9001`/`CC-DOC-1`/`VAT18-DOC`/`FA-DOC-1` replaced with `<placeholder>` references to the actual generated value, matching the existing `<id of an account already at COA_MAX_DEPTH>` placeholder convention already in the file; the now-impossible "duplicate code" negative tests for Cost Center and Tax Code were removed, the Account one was repurposed into an "invalid accountTypeId" 400 test, and the Account "duplicate accountCode on rename" test was repurposed into a positive test confirming `accountCode` in an update body is silently ignored — collection re-validated as well-formed JSON, 135 requests, 135/135 still carrying `X-Api-Key`).
|
||||
|
||||
**Live-tested against the real dev database** (`dotnet run`, Development environment, port 5200) after the build succeeded:
|
||||
|
||||
- `POST /accounts` (no parent, existing dev DB already had 6 root Asset accounts from prior sessions) → `accountCode: "1600"` (1000 + 6×100), confirming the root-block formula picks up existing rows correctly rather than assuming a fresh database.
|
||||
- `POST /accounts` (child of the above) → `accountCode: "1600-01"`.
|
||||
- `POST /accounts` with `accountTypeId: 9` → `400`, `"Invalid accountTypeId '9'. Must be one of: 1 (Asset), 2 (Liability), 3 (Equity), 4 (Income), 5 (Expense)."`.
|
||||
- `PUT /accounts/1600` with body `{ "accountName": "Renamed Root", "accountCode": "HACKED" }` → `200`, `accountName` updated, `accountCode` still `"1600"` — confirms immutability.
|
||||
- `POST /cost-centers` (root) → `"CC-003"`; child of it → `"CC-003-01"`.
|
||||
- `POST /tax-codes` (`taxTypeId: 1`, VAT) → `"VAT-003"`.
|
||||
- `POST /fixed-assets` → `"FA-000005"`.
|
||||
|
||||
All six generated codes matched the documented scheme exactly on the first try. Dev server stopped after testing.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-20 — Deviation from design doc: amounts stored/returned at full precision, rounded to 2dp only in PDF rendering (`CLAUDE.md` Rule 13, `01_Full_Development_Plan.md` §13)
|
||||
|
||||
User-directed. `Accounting_System_Design_LKAS.md` specifies every monetary column as `NUMERIC(18,2)` — the user asked for a deliberate deviation: no rounding/truncation on storage or in any API response; a value is only ever rounded to 2 decimal places when a PDF is rendered.
|
||||
|
||||
**Database — migration `WidenAmountAndRateColumnsToUnboundedNumeric`:** removed `.HasPrecision(...)` from all 20 `decimal`/`decimal?` properties in `AppDBContext.cs` (12 true amount columns across `JournalEntryLine`, `RecurringJournalTemplateLine`, `FixedAsset`, `DepreciationSchedule`, `AssetDisposal`, `BankStatementLine`, `BudgetLine`, plus 4 rate/multiplier columns — user chose "widen everything decimal, including rates" over "amounts only" when asked). Npgsql now maps every one of these to a bare Postgres `numeric` with no precision/scale ceiling. Applied against the dev database — 20 `ALTER COLUMN ... TYPE numeric` statements executed cleanly, no data loss (widening, not narrowing).
|
||||
|
||||
**Application layer:** confirmed via full-repo grep that there was no `Math.Round`/`decimal.Round`/`MidpointRounding` anywhere in the codebase before this change — the `NUMERIC(18,2)` column type was the only thing enforcing 2 decimals, so removing it was sufficient on its own to satisfy "don't limit amount values to 2 decimal points when sending data."
|
||||
|
||||
**PDF rendering (`ReportService.RenderPdf`) — the one intentional exception:** added a `FormatCellValue` helper that rounds `decimal`/`decimal?` cell values via `Math.Round(d, 2, MidpointRounding.AwayFromZero)` before calling `.ToString("F2")`; every other property type still just calls `.ToString()`. `AwayFromZero` (not .NET's `ToEven` default) implements the requested "number routing" rule exactly: `1000.005` → `1000.01`, not down to `1000.00`.
|
||||
|
||||
**Verified:**
|
||||
|
||||
- `dotnet build` — 0 warnings, 0 errors, both before and after the migration.
|
||||
- Migration reviewed before applying — every `AlterColumn` widens `numeric(x,y)` → `numeric` (unbounded), fully reversible `Down()` restores the original precision/scale.
|
||||
- Rounding boundary values checked in a throwaway scratch console app before wiring into `ReportService`: `1000.005`→`1000.01`, `1000.0149999`→`1000.01`, `1000.015`→`1000.02`, `1000.0574536`→`1000.06`, `1000.0`→`1000.00`, `1000.004999`→`1000.00` — all six matched the spec exactly.
|
||||
- Live end-to-end against the real dev database: created a journal entry with `debitAmount: 1000.1234567` (7 decimal places) — `POST /journal-entries` response, `PUT .../post` response, and `GET /reports?reportType=TrialBalance&outputFormat=Json` all returned `1000.1234567` unrounded. Generated the same report with `outputFormat=Pdf` — renders successfully (88KB, no exceptions), confirming `FormatCellValue` handles the full-precision decimal without error. Did not attempt to verify the rendered glyphs pixel-by-pixel (QuestPDF subsets/remaps its embedded font, so the PDF's internal content stream doesn't contain the amount as plain ASCII text even after decompressing it — confirmed this while trying to grep the decompressed stream) — code-path inspection plus the isolated rounding-logic verification above are the basis for confidence here, not a rendered-text check.
|
||||
- Test data left in the dev database as real, inspectable records (JV-2035-000011, accounts 1600/1600-01, CC-003/CC-003-01, VAT-003, FA-000005), not cleaned up — consistent with this project's established testing convention.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-20 — Retrofit: `GeneralLedger` report's `accountId` made optional — true General Ledger mode added (Phase 17, `Accounting_System_Design_LKAS.md` §11.1)
|
||||
|
||||
User-directed. `GET /reports?reportType=GeneralLedger` required `accountId`, making it an "Account Ledger" (one account + its recursive descendants) rather than a real General Ledger, which shows every account's transactions together. Fixed without changing the existing single-account behavior at all.
|
||||
|
||||
**Code changes:**
|
||||
|
||||
- `Interfaces/Repositories/IJournalEntryRepository.cs` / `Repositories/JournalEntryRepository.cs` — added `GetGeneralLedgerLinesForAllAccountsAsync(DateOnly periodStart, DateOnly periodEnd)`. Same SQL shape as the existing `GetGeneralLedgerLinesAsync` (identical `PARTITION BY jel.account_id` running-balance window, identical `ORDER BY a.account_code, je.entry_date, je.journal_id`), minus the `descendants` CTE/accountId filter, plus `AND a.is_postable = TRUE`. The existing method is untouched.
|
||||
- `Services/ReportService.BuildGeneralLedgerAsync` — dropped the "`accountId` is required" throw; `periodStart`/`periodEnd` still required. Branches on `request.AccountId.HasValue`: present → unchanged existing path (404 check + `GetGeneralLedgerLinesAsync`); absent → new `GetGeneralLedgerLinesForAllAccountsAsync`.
|
||||
- No DTO changes (`ListReportsRequest.AccountId` was already `long?`), no migration, no UoW, no audit row (read-only, same as every other report).
|
||||
|
||||
**Verified:** `dotnet build` — 0 warnings, 0 errors.
|
||||
|
||||
**Docs updated:** `Accounting_System_Design_LKAS.md` §11.1 (added the second query variant), `04_API_Reference_And_Scenarios.md` Reporting module (both modes documented, missing-param example changed since `accountId` is no longer required), `02_Phase_By_Phase_Development_Plan.md` Phase 17 (param description + acceptance test updated to cover both modes and the postable-only exclusion).
|
||||
|
||||
**Live-tested against the real dev database** (`dotnet run`, Development environment, port 5200):
|
||||
|
||||
- `GET /reports?reportType=GeneralLedger&outputFormat=Json&accountId=5&periodStart=2020-01-01&periodEnd=2036-12-31` → unchanged: only account `1500`'s own lines, one running balance sequence (`-12000.00` → `-24000.00` → `-22999.8765433`).
|
||||
- Same call with `accountId` omitted → lines from every postable account with activity in range (`1000`, `1001`, `1002`, `1500`, `1510`, `4900`, `5100`, `9001`, `9002`), sorted by `accountCode` then `entryDate`; running balance visibly resets at each account boundary (e.g. `1000` ends its block at `23300.00`, `1001`'s very next row starts a fresh sequence at `-1000.00`, not continuing from `23300.00`).
|
||||
- Cross-checked against `GET /accounts?isPostable=false`, which returns exactly one row in this dev DB (`1999`, "Header Account"). `1999` does not appear anywhere in the no-`accountId` response's account set — confirms the postable-only exclusion.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-20 — Retrofit: `BalanceSheet` report now actually balances (Phase 17, `Accounting_System_Design_LKAS.md` §11.3)
|
||||
|
||||
User-directed. `BuildBalanceSheetAsync` only summed Asset/Liability/Equity account balances, so `Assets ≠ Liabilities + Equity` for any `asOfDate` after the fiscal year started — the current period's net profit/loss sits in Income/Expense accounts, which the Balance Sheet excludes, and there's no closing-entry mechanism anywhere in this codebase (verified by grepping the whole repo for "Retained Earnings"/"closing entry" — the only hit is the design doc's own prose) that ever moves that value into Equity.
|
||||
|
||||
**Open question resolved before implementing:** should "Current Year Earnings" be scoped to just the fiscal year containing `asOfDate`, or cumulative since inception? Confirmed with the user: **cumulative since inception.** Reasoning: since no closing mechanism exists, a fiscal-year-scoped figure would only make the equation hold for the *first* fiscal year a ledger is used — the moment a second fiscal year has any posted activity, that prior year's profit would be stranded in Income/Expense accounts with nowhere else to go, breaking the equation again. This also simplified the implementation: no `FiscalCalendar` lookup needed at all.
|
||||
|
||||
**Code change:** `Services/ReportService.BuildBalanceSheetAsync` — after the existing `GetBalanceSheetAsync(asOfDate)` call (unchanged), added `GetProfitAndLossAsync(DateOnly.MinValue, asOfDate)` (reusing the existing method, no new repository code) and summed its depth-1 rows — the exact same reduction `BuildCashFlowAsync.netProfit` already does. Appended one row: `{ Depth = 1, LineItem = "Current Year Earnings", AccountType = "Equity", Balance = <that sum> }`, always present (even at `0.00`) as the last row, so the response shape never varies based on whether profit happens to be zero.
|
||||
|
||||
**Verified:** `dotnet build` — 0 warnings, 0 errors. (One build attempt blocked by a locked `.exe` from a running dev-server instance, PID 6100 on ports 5200/7024 — confirmed with the user it was safe to stop before retrying; build succeeded immediately after.)
|
||||
|
||||
**Docs updated:** `Accounting_System_Design_LKAS.md` §11.3 (documented the cumulative-since-inception deviation from the design doc's own "current-year profit" wording, with the reasoning), `04_API_Reference_And_Scenarios.md` Reporting module (BalanceSheet row + retrofit note), `02_Phase_By_Phase_Development_Plan.md` Phase 17 (acceptance test now requires `Assets = Liabilities + Equity` to hold, not just "renders without error").
|
||||
|
||||
**Live-tested against the real dev database** (`dotnet run`, Development environment, port 5200):
|
||||
|
||||
- `GET /reports?reportType=BalanceSheet&outputFormat=Json&asOfDate=2030-01-16` (before any Income/Expense activity, only Asset accounts touched so far): `"Current Year Earnings"` row present at `0.00`; summing depth-1 rows, `Assets = 0`, `Liabilities = 0` (no Liability accounts exist in this dev DB), `Equity = 0` — equation holds trivially.
|
||||
- `GET /reports?reportType=BalanceSheet&outputFormat=Json&asOfDate=2036-12-31` (all posted activity, including depreciation/disposal entries touching Income/Expense accounts 4900/5100): summing depth-1 rows precisely via PowerShell, `Assets = -10000`, `Liabilities = 0`, `Equity = -10000` (driven by `"Current Year Earnings": -10000.00`) — `Assets = Liabilities + Equity` exactly, confirmed programmatically (`Equal: True`), not just eyeballed.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-21 — Full test dataset seeded via the live API; three real bugs found and fixed in the process
|
||||
|
||||
User-directed: "fill the database with actual data... not just 1 or 2 rows... full data collection to check any endpoint." Seeded entirely through the running API (not direct SQL), so every business rule (double-entry balance, code generation, audit logging, depth guards) was naturally exercised rather than bypassed. This uncovered three real, previously-latent bugs — none of them seed-script issues, all genuine reliability problems that would eventually have hit real usage once enough data existed.
|
||||
|
||||
**Dataset created:** a realistic FY2027 books-of-account for a small company — ~30 new Chart of Accounts entries (2-level Asset/Liability tree, flat Equity/Income/Expense), 5 cost centers (3 levels), 4 tax codes (VAT/WHT/NBT/SSCL), 12 fiscal periods, 20 journal entries (including a full-reversal correction, a partial-adjustment correction, and one deliberately-left-draft entry), 3 recurring templates (one generated + posted), 3 fixed assets (StraightLine and ReducingBalance, one disposed, all depreciated across 3 periods), 1 bank account with 8 imported statement lines (6 reconciled against real posted entries, 2 deliberately left unmatched), and 1 budget with 6 lines across periods/cost centers. Left in the dev database as real, inspectable records, per this project's established testing convention.
|
||||
|
||||
**Bug 1 — account/cost-center/fixed-asset/tax-code generation collided with pre-existing legacy codes (500, not a clean 400).** The Rule 8.1 code generators (`AccountService.GenerateAccountCodeAsync`, `CostCenterService.GenerateCostCenterCodeAsync`, `FixedAssetService.GenerateAssetCodeAsync`, `TaxService.GenerateTaxCodeAsync`) trusted a `COUNT(*)`-based guess for the next slot. That guess collides with any account created before the retrofit under the old user-supplied-code regime that happens to occupy a slot the count formula will also generate (reproduced exactly: an Expense-type root account manually coded `"5100"` before the retrofit made every subsequent `blockStart + count*100` computation collide on that same `"5100"`, throwing an unhandled unique-constraint violation). **Fixed** by probing candidate codes directly via each repository's `GetByCodeAsync` and advancing to the next slot on collision, instead of trusting a count — regardless of legacy data. Added `ITaxRepository.GetByCodeAsync` (mirroring the pattern already used by the other three repositories) since Tax needed a code-existence check it didn't have after `TaxCodeExistsAsync` was removed in the original retrofit. Removed the now-unused `CountRootAccountsByTypeAsync`/`CountChildAccountsAsync`/`CountRootCostCentersAsync`/`CountChildCostCentersAsync`/`CountAssetsAsync`/`CountTaxCodesByTypeAsync`.
|
||||
|
||||
**Bug 2 — every `SUM()`-based report threw a bare 500 once realistic transaction volume existed.** Root cause: straight-line depreciation often doesn't divide evenly (`800000/96 = 8333.333333333333333333333333`, 28 significant digits — already at C#'s `decimal` ceiling for a single value); the Rule 13 unbounded-precision columns let Postgres's `SUM()` aggregate several such values into something exceeding what `decimal` can hold at all. Affected `TrialBalance`, `BalanceSheet`, `ProfitAndLoss`, `CashFlow`, `GeneralLedger`'s running balance, and `BudgetVsActual` — every report doing `SUM()`. **Fixed** by wrapping only the SQL-side `SUM(...)` expressions with `ROUND(..., 10)` in `AccountRepository` (`GetTrialBalanceAsync`, `GetBalanceSheetAsync`, `GetProfitAndLossAsync`, `GetCashFlowCategoryMovementAsync`), `JournalEntryRepository` (both `GetGeneralLedgerLines*Async` running-balance windows), and `BudgetRepository.GetBudgetVsActualAsync` — bounding only the aggregate at the point it's read into a `decimal`; storage and single-row responses stay exactly as unrounded as Rule 13 requires.
|
||||
|
||||
**Bug 3 — `BalanceSheet` had a sign-convention bug for Liability/Equity, unrelated to bugs 1-2.** `GetBalanceSheetAsync` computed `debit - credit` uniformly for Asset/Liability/Equity, correct only for Asset (debit-normal) — a real credit balance (e.g. a bank loan) showed as negative. Never caught before because the `Assets = Liabilities + Equity` check added earlier (2026-07-20, see above) was only ever tested against a dataset with zero Liability balance, where the bug is invisible. **Fixed** by flipping the sign for non-Asset types (`CASE WHEN account_type_id = 1 THEN debit - credit ELSE credit - debit END`), mirroring the flip `GetProfitAndLossAsync` already correctly applies for Income/Expense.
|
||||
|
||||
**Verified live, in order, after each fix:** `dotnet build` clean each time; the exact previously-colliding account create (`"Cost of Goods Sold"`, Expense root) now succeeds; all six report paths return `200` instead of `500`; `BalanceSheet` against the full seeded dataset: `Assets = 3036000`, `Liabilities + Equity = 2054000 + 982000 = 3036000` — exact match, confirmed programmatically via PowerShell, not eyeballed.
|
||||
|
||||
**Process note:** twice during this session a `General-Ledgar-Service.exe` was found holding port 5200 mid-rebuild from a source other than the current turn's own `dotnet run` — confirmed with the user each time before terminating it (per the standing rule to ask when a running process isn't clearly this turn's own).
|
||||
|
||||
**Docs updated:** `04_API_Reference_And_Scenarios.md` (Reporting module — both bugfix notes). This entry is the authoritative record for `02_Phase_By_Phase_Development_Plan.md` Phase 17's acceptance test, which now actually passes the `Assets = Liabilities + Equity` condition added in the earlier BalanceSheet retrofit (previously unreachable due to Bug 2 blocking the report outright, then Bug 3 once it ran).
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-21 — Three fixes specified (not yet implemented): General Ledger PDF layout, full LKAS 7 Cash Flow, `Company:Name`
|
||||
|
||||
User raised three concerns after reviewing the LKAS PDF template spec (`05_LKAS_Report_PDF_Templates.md`) delivered the prior session:
|
||||
|
||||
**1. General Ledger PDF template corrected.** The prior spec grouped the report into separate boxed sub-tables per account, each with its own repeated column-header row. User clarified the actual Sri Lankan practical/regional convention (not an LKAS-prescribed format — LKAS doesn't govern internal ledgers) is a **single continuous table** for the whole report: `Date | Particulars | Description/Reference (J.R.) | Debit | Credit | Balance`. Revised: one table, one header row, account boundaries marked by an inline bold group-header row (`Account: [code] — [name]`) rather than separate tables — preserves per-account running-balance correctness (mathematically required — a `Balance` column only means something within one account's transaction stream) while matching the single-table convention requested. `Particulars` maps to the existing `narration` field; `Description/Reference (J.R.)` maps to `journalNo`. No API/response shape change — this was a PDF-layout-only fix, `GeneralLedger`'s `outputFormat=Json` shape is untouched.
|
||||
|
||||
**2. `CashFlow` upgraded from documented simplification to full LKAS 7 spec.** Previously accepted as a scope simplification (2026-07-20 entry, mid-file) — user asked to actually close the gap and match Section 5a of the PDF template doc (the full LKAS 7 layout, not the "(Summary)" fallback). Full redesign at `Accounting_System_Design_LKAS.md` §11.5 (replaces the original sketch that was never fully implemented, and fixes a real dead-code bug found in the process: `WHERE t.account_type_id NOT IN (SELECT id FROM account_type WHERE name ILIKE '%cash%')` was always a no-op, since `account_type` never contains a row literally named "Cash"). New mechanism: `system_config` key `CASH_ACCOUNT_ROOT_ID` identifies the "Cash and Cash Equivalents" COA root; every recursive descendant is cash-like — same pattern as every other COA-hierarchy-driven classification in this system (Balance Sheet's current/non-current split works the same way). Non-cash adjustments reuse the already-populated `depreciation_schedule` table (never read by this report before). Response shape changes from a flat 6-field object to a nested one (`operatingActivities`/`investingActivities`/`financingActivities`/opening+closing cash) — see `04_API_Reference_And_Scenarios.md`'s updated `CashFlow` row. **Not yet implemented** — this is a specification only; new Document 2 §1.10 and tracker rows 1.10/17 mark it outstanding, not done. `CASH_ACCOUNT_ROOT_ID` specifically **cannot** be `HasData`-seeded (unlike the other three config keys) — it references a COA account that doesn't exist until a real company's Chart of Accounts has been created via Phase 2, so it has to be set later via the existing `PUT /system-config` endpoint, not at migration time.
|
||||
|
||||
**3. `Company:Name` confirmed for `appsettings.json`, not `system_config`.** The PDF template doc's prior recommendation left this open between the two; user confirmed `appsettings.json` — consistent with it being a deployment-time constant (like `Jwt:SigningKey`/`ApiKey:Value`) rather than a runtime-editable business value. Unlike those two secrets, every environment gets a real value, not blank-in-Production, since a company name isn't sensitive. New Document 2 §1.9 and tracker row 1.9, both **not yet implemented**.
|
||||
|
||||
**Docs updated this round:** `05_LKAS_Report_PDF_Templates.md` (General Ledger and Cash Flow sections rewritten, company-name section corrected to `appsettings`), `Accounting_System_Design_LKAS.md` (§11.5 full rewrite, §12.2 gains `CASH_ACCOUNT_ROOT_ID`), `04_API_Reference_And_Scenarios.md` (`CashFlow` response shape row, simplification note replaced with upgrade-spec note), `01_Full_Development_Plan.md` (appsettings table gains `Company:Name`), `02_Phase_By_Phase_Development_Plan.md` (Phase 17 gains the CashFlow/PDF spec notes and acceptance-test addition; Phase 1 gains §1.9/§1.10), this file (Phase 1 and Phase 17 rows updated, two new Phase 1 sub-rows added). **None of this was implemented or tested live this round** — every change above is a specification update, consistent with how this file distinguishes "documented" from "done."
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-21 (same day, follow-up) — New `TaxSummary` report (7th `reportType`) + A4 page size for all PDF output
|
||||
|
||||
Two more additions, both specification-only like everything else logged above today:
|
||||
|
||||
**1. `TaxSummary` — new 7th `reportType` on the existing `GET /reports` endpoint.** Not a new phase/endpoint — the whole reason this endpoint uses a `reportType` discriminator (rather than one route per report) is exactly so a new report doesn't need a new route. Deliberately scoped as a **practical VAT/WHT/NBT return-preparation summary**, not an LKAS 12 disclosure note — this system has no deferred tax tracking anywhere, so a genuine LKAS 12 tax-expense reconciliation isn't something the current schema can produce, and the spec says so explicitly rather than overclaiming compliance. One row per active `tax_code`: `outputTaxAmount` (sum of activity in that code's `output_tax_account_id`), `inputTaxAmount` (same, `input_tax_account_id`), `netTaxPayable` (the difference — positive owed to Inland Revenue, negative a net receivable position, both valid outcomes, neither clamped). **A real constraint surfaced while designing this:** `journal_entry_line` has no `tax_code_id` column, so there's no way to attribute one posted line back to the exact tax code that produced it — the report works around this by joining on each tax code's own distinct output/input GL accounts instead, which only stays accurate if no two tax codes ever share an account. Full query: `Accounting_System_Design_LKAS.md` §11.7 (new). API surface: `04_API_Reference_And_Scenarios.md`'s reports table + reportType error-message enum, both updated. Phase-plan notes: `02_Phase_By_Phase_Development_Plan.md` Phase 17. Tracker: Phase 17's row, 4th outstanding item.
|
||||
|
||||
**2. Every PDF report specified to render on A4.** `page.Size(PageSizes.A4)` (QuestPDF's built-in constant), portrait by default, 2cm margins — applies once the bespoke per-report `IDocument` layouts (from the prior LKAS template work) replace the current generic renderer, whose page size was never explicitly set or confirmed before now. Landscape flagged as an available per-report override for wider tables (Trial Balance, General Ledger, Budget vs Actual are the likely candidates given column count) rather than a blanket rule — `05_LKAS_Report_PDF_Templates.md` Section 1 carries the actual orientation guidance per report.
|
||||
|
||||
**Docs updated this round:** `Accounting_System_Design_LKAS.md` (new §11.7, Section 9's summary table row updated to match the real account-level-join mechanism instead of the original "tax_code-linked lines" phrasing, which implied a direct link that was never actually built), `04_API_Reference_And_Scenarios.md` (reports table gains `TaxSummary` row, intro line, error-message enum, two new spec notes — `TaxSummary` and A4), `01_Full_Development_Plan.md` (Phase 17's endpoint description gains `TaxSummary`), `02_Phase_By_Phase_Development_Plan.md` (Phase 17 gains `TaxSummary`/A4 spec notes, dispatch list, repository list, and acceptance-test additions), this file (Phase 17's row updated to 4 outstanding items). **Nothing implemented or tested live this round** — same discipline as every other entry in this log: specification and implementation are tracked as separate facts, never conflated.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-21 (same day, follow-up) - All 4 outstanding Phase 17 retrofits implemented and verified live: Company:Name, CASH_ACCOUNT_ROOT_ID, full LKAS 7 CashFlow, TaxSummary, and all 7 bespoke PDF templates
|
||||
|
||||
User-directed: turn the specifications written earlier today (05_LKAS_Report_PDF_Templates.md, Accounting_System_Design_LKAS.md Section 11.5/11.7) into real, tested code. Implemented and verified end-to-end, live against the dev database, in the order 05's own Section 9 recommends (simplest/lowest-risk first, CashFlow last since it has the most new logic).
|
||||
|
||||
**1. Company:Name.** Added to appsettings.json/appsettings.Development.json/appsettings.Production.json (a real value in every environment, unlike ApiKey:Value - not a secret, per 02_Phase_By_Phase_Development_Plan.md Section 1.9). Read via IConfiguration in ReportService, falls back to "Company Name Not Configured" if somehow missing rather than crashing PDF generation.
|
||||
|
||||
**2. CASH_ACCOUNT_ROOT_ID system_config key.** Migration AddCashAccountRootIdConfig seeds a 4th system_config row with an empty placeholder ConfigValue (can't be seeded with a real value - no COA exists at migration time, per Section 1.10) and IsEditable=true/IsLocked=false. Classified Ledger in SystemConfigService.KeyCategories. ReportService.BuildCashFlowAsync resolves and validates it before doing anything else, throwing a clear BadRequestException if unset/invalid - a deliberate improvement over the design doc's inline-SQL-subquery sketch, which would have silently produced an empty/wrong cash-accounts set instead of a clear error.
|
||||
|
||||
**3. CashFlow - full LKAS 7 rewrite (Accounting_System_Design_LKAS.md Section 11.5).** New nested CashFlowReportDto (CashFlowOperatingActivitiesDto/CashFlowInvestingActivitiesDto/CashFlowFinancingActivitiesDto, opening/closing cash). New repository methods: AccountRepository.GetCashFlowWorkingCapitalChangesAsync (sign-adjusted per account type, same CASE WHEN account_type_id = 1 flip as the Balance Sheet fix), GetCashFlowCategoryLinesAsync (itemized, replaces the old single-aggregate GetCashFlowCategoryMovementAsync, now removed along with its unused CashFlowCategoryMovementRow DTO), GetCashBalanceAsync; FixedAssetRepository.GetDepreciationAddBackAsync (reads depreciation_schedule, never queried by any report before this). All new SUM()s wrapped in ROUND(...,10), same overflow guard as the 2026-07-20 bugfix.
|
||||
|
||||
**Real bug found and fixed during live verification, not assumed correct from the spec:** first live test (real FY2027 seeded data, CASH_ACCOUNT_ROOT_ID pointed at a new "Cash and Cash Equivalents" header) produced closingCashBalance (2,591,150) not equal to openingCashBalance + netIncreaseDecreaseInCash (3,520,212.50) - a real mismatch, chased down per the design doc's own "if these don't match, that's a bug, chase it down" instruction. Root cause: the test COA's cash_flow_category = 'INVESTING' tag was applied to a whole header account ("Non-Current Assets"), which cascades to all descendants including Accumulated Depreciation contra-accounts and, for a disposed asset, the disposal's cost-removal entry - neither is a real cash movement (depreciation's cash effect is already captured once via the Operating-section add-back; a disposal's cost-removal is a bookkeeping entry, not the actual cash proceeds, which post to a separate cash account already excluded). This is not a code bug - the repository/service code faithfully implements the design doc's own query - it's a COA-design constraint worth recording: cash_flow_category must be tagged on individual leaf accounts that genuinely have their own cash-relevant movement (asset cost accounts, specific liability/equity accounts), never on a broad header, since this schema has no way to "un-tag" a descendant from an ancestor's classification once inherited (COALESCE(a.cash_flow_category, parent's), same mechanism Accounting_System_Design_LKAS.md Section 11.3 already uses for Balance Sheet current/non-current). Confirmed the underlying mechanism is correct with a clean, correctly-tagged test (fresh accounts, fresh period, no header-level tagging): openingCashBalance (2,591,150.00) + netIncreaseDecreaseInCash (180,000.00) = closingCashBalance (2,771,150.00) exactly.
|
||||
|
||||
**4. TaxSummary - new 7th reportType (Accounting_System_Design_LKAS.md Section 11.7).** ReportType.TaxSummary = 7. New TaxSummaryRowDto. New ITaxRepository method: GetTaxSummaryAsync(periodStart, periodEnd), a flat (non-recursive) join of tax_code/tax_type against separately-aggregated output/input account movements. Verified live: correctly showed a negative netTaxPayable for WHT-001 (input exceeded output, a real receivable position from the seeded WHT remittance) and identical outputTaxAmount across VAT-004/NBT-001/SSCL-001 for the same period - confirmed as the expected consequence of the seed data's tax codes sharing one output account (VAT Payable), exactly the accuracy constraint the design doc itself flags, not a bug.
|
||||
|
||||
**5. All 7 bespoke PDF templates (05_LKAS_Report_PDF_Templates.md), replacing the single generic reflection-based RenderPdf/FormatCellValue.** New Services/PdfDocuments/ folder: PdfFormatting (shared FormatAmount/FormatAmountOrBlank/FormatAmountOrDash - same round-half-up 2dp rule as the old FormatCellValue, parentheses for negatives), ReportPageHeader (shared header/footer static helper - company name, title, period line, currency, footer with generated-timestamp and page-of-page numbers), and one IDocument class per report: TrialBalancePdfDocument (landscape, monospace-preserved indentation, blank-not-zero Debit/Credit, totals row), BudgetVsActualPdfDocument (portrait, flat table incl. a Period column since a budget can span periods), TaxSummaryPdfDocument (portrait, dash-not-zero Output/Input Tax), GeneralLedgerPdfDocument (landscape, one continuous table with shaded full-width group-header rows per account, not separate boxed tables - confirmed QuestPDF's table.Header automatically repeats the column header on every page), BalanceSheetPdfDocument (groups by accountType, sums only depth-1 rows for section totals since deeper rows are already subtree rollups), ProfitAndLossPdfDocument (same grouping approach, computes PROFIT FOR THE PERIOD in the renderer since the response has no pre-computed net figure), CashFlowPdfDocument (full nested structure, computed "Operating profit before working capital changes" subtotal, conditional "used in"/"from" and "Increase"/"Decrease" wording based on sign). One deliberate simplification vs. the spec's illustration: working-capital-change lines are rendered as the plain account name plus its already-signed amount, not prefixed with "Increase in X"/"Decrease in X" - deriving that wording correctly needs the account's own type (asset vs liability), which CashFlowWorkingCapitalChangeDto doesn't carry, and the design doc's own illustration shows both a positive and a negative line labelled "Increase in...", i.e. the sign alone doesn't determine the correct word either. Also added ICurrencyRepository.GetBaseCurrencyCodeAsync() (new, small, targeted - avoids fetching all ~140 seeded currency rows just to find the one with is_base_currency = true).
|
||||
|
||||
**Verified live for every one of the 7 reports** (dotnet build clean throughout): outputFormat=Pdf returns a valid %PDF-1.4 file for all seven (sizes 42KB-91KB); visually inspected BalanceSheet (TOTAL ASSETS = TOTAL EQUITY AND LIABILITIES = 1,627,350.00 exactly, negative parentheses, monospace indentation all correct), GeneralLedger (10 pages, group-headers, per-account running-balance resets, column headers repeating per page, all correct), and CashFlow (opening+increase=closing reconciliation renders exactly as computed).
|
||||
|
||||
**Docs updated:** this file (Phase 17 row + this entry). 04_API_Reference_And_Scenarios.md, 02_Phase_By_Phase_Development_Plan.md, 05_LKAS_Report_PDF_Templates.md, and Accounting_System_Design_LKAS.md's remaining "not yet implemented" markers for these 5 items still need updating to reflect completion - flagged as the next step, not done as part of this entry, to keep this log's own "don't conflate documentation with implementation" discipline intact until that pass is actually done.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-22 — Five reference report templates supplied by the user; real gaps found in what was built 2026-07-21; six new retrofits specified (none implemented yet)
|
||||
|
||||
User supplied five reference images — Profit & Loss, Trial Balance, Balance Sheet, Cash Flow, and Tax Report templates — asking that report generation be modified to match them, and that every report gain a CSV export alongside its existing PDF (frontend to gain a second "Download CSV" button next to "Download PDF"). Comparing each against what was actually specified/built the day before surfaced genuine gaps, not just cosmetic differences:
|
||||
|
||||
**1. Trial Balance — presentation-only fix.** The reference template is a plain flat list, no account-tree indentation. The 2026-07-21 design carried `depth`/`indentedCode` for hierarchy. Simplified to `{ accountCode, accountName, debit, credit }` — no data/query change, PDF/CSV layout only.
|
||||
|
||||
**2. Profit & Loss — real structural gap.** The reference Income Statement separates Sales from Other Income and splits Expenses into Cost of Sales (for Gross Profit) plus Distribution/Administration/Other/Financial groups (for Net Profit) — none of which the existing flat `{depth, lineItem, accountType, amount}` array (just Income vs. Expense) can express. New `accounts.income_statement_section` column added, same recursive-tag mechanism as `cash_flow_category` — this is the second time this exact pattern ("a report needs a classification the schema doesn't have, solved by tagging COA header nodes and inheriting down the tree") has been used, after `cash_flow_category` itself. Full redesign: `Accounting_System_Design_LKAS.md` §11.4.
|
||||
|
||||
**3. Cash Flow — display simplified, one real data gap closed.** The reference template has no opening/closing cash balance section (ends at the combined total) and labels working-capital lines "Increase in X"/"Decrease in X". The no-balance-section part is a display change only (the reconciliation still computes and returns, just isn't printed). The "Increase/Decrease" wording is a genuine gap the previous session's own Deviations entry (directly above this one) already flagged and consciously deferred: *"deriving that wording correctly needs the account's own type... which CashFlowWorkingCapitalChangeDto doesn't carry"* — closed now by adding a `direction` field, computed from the raw balance-change sign (not the cash-impact sign, which is a different thing — see `Accounting_System_Design_LKAS.md` §11.5's note on why these two signs aren't the same). Also: non-cash adjustments and working-capital changes get combined and re-bucketed by sign into "Additions to Cash"/"Subtractions From Cash" at render time, matching the reference template's own grouping.
|
||||
|
||||
**4. Tax Report — not a gap, a wrong report.** The reference template is a full **Corporate Income Tax Computation** (Profit Before Tax → Non-Deductible Expenses → Adjusted Business Profit → Assessable Income → Taxable Income → Corporate Tax → Gross Tax Liability → less APIT/WHT/Quarterly credits → Balance Tax Payable/(Refund)) — a fundamentally different report from the VAT/WHT/NBT per-`tax_code` summary built 2026-07-21 under the same `reportType` name. This is a **wholesale content replacement**, not an extension: new `accounts.is_tax_deductible` column, five new `system_config` keys (two seedable, three not — same `HasData` constraint `CASH_ACCOUNT_ROOT_ID` already established), four new optional request parameters for figures that are genuinely tax-filing-time judgment calls rather than GL-derivable (`allowableDeductions`, `otherTaxableIncome`, `qualifyingPaymentsReliefs`, `surchargeAmount`). The superseded VAT/WHT/NBT design is **kept, not deleted** (`Accounting_System_Design_LKAS.md` §11.7†, `05_LKAS_Report_PDF_Templates.md` §8†) — it's still a real, useful report for periodic indirect-tax return preparation, just not what "Tax Report" turned out to mean here; flagged as needing its own `reportType` (e.g. `IndirectTaxSummary`) if it's still wanted as a separate report later.
|
||||
|
||||
**5. `outputFormat=Csv` — new third format, all seven reports.** Same `ApiResponse`-wrapped base64 convention as `Pdf`. Per-report flattening rules (how each nested/hierarchical shape becomes flat rows with a `Section`/`Category` column) fully specified: `05_LKAS_Report_PDF_Templates.md` §10 (new).
|
||||
|
||||
**6. `Company:Address`/`Tin`/`Brn`** — three new `appsettings` fields, needed only by the Income Tax Computation's own header block (a fuller identity block than the other six reports use — company address, Taxpayer Identification Number, Business Registration Number, standard IRD filing identifiers).
|
||||
|
||||
**A genuine lesson worth internalizing, stated in `05_LKAS_Report_PDF_Templates.md` §9 too:** this is the second time in two days a report has needed real rework once an actual reference format arrived (`CashFlow`'s LKAS 7 gap, now this). Get the actual required template confirmed before building against an assumed structure, where that's possible.
|
||||
|
||||
**Status: nothing above is implemented yet** — this entire entry is a specification update, same discipline as every other entry in this log. `03_Progress_Tracker.md` Phase 17's row above reflects "4 retrofits done 2026-07-21, 6 more outstanding as of 2026-07-22," not "done."
|
||||
|
||||
**Docs updated this round:** `Accounting_System_Design_LKAS.md` (§11.4 full rewrite, §11.5 updated, §11.7 full rewrite + §11.7† for the superseded design, §9/§12.2 table updates), `04_API_Reference_And_Scenarios.md` (reports table, six new/updated retrofit notes, `Csv` added throughout), `05_LKAS_Report_PDF_Templates.md` (§0 expanded, §1/§2/§4/§5/§8 rewritten, §8† added, §9 rewritten, §10 new), `01_Full_Development_Plan.md` (appsettings table, Phase 17 endpoint row), `02_Phase_By_Phase_Development_Plan.md` (Phase 17's query params/spec notes/dispatch/repository list/acceptance test, Phase 1 §1.9 expanded + §1.11/§1.12 new), this file (Phase 17 row reopened, Phase 1 rows 1.9/1.10/1.11/1.12, this entry). **Not yet updated, deliberately deferred to keep this response scoped:** the two frontend-project documents (`12-GENERAL-LEDGER-INTEGRATION.md`, `21-GENERAL-LEDGER-FRONTEND.md`) — CSV button, new Tax Report screen, Cash Flow screen's shape change, and the five templates' visual layout all still need to be threaded through those two docs.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-22 (same day, follow-up #2) — Cash/Bank `accountType` discriminator; frontend's Cash/Bank create page surfaced a real gap
|
||||
|
||||
User flagged a real problem while looking at the frontend's Cash/Bank Accounts create page: it needs to create either a Cash or a Bank account, but only `bank_account`/`POST /bank-accounts` existed — nothing modeled "Cash" as a distinct, valid thing to create, and there was no mechanism restricting the create flow to exactly those two concepts.
|
||||
|
||||
**The fix:** new required `accountType` (`CASH` | `BANK`) field on `bank_account` and its create request — same discriminator-over-near-duplicate-structures pattern already used throughout this project (`ReportType`, `CorrectionType`, `TransactionType`). `bankName`/`accountNumber` (already nullable from the original design, just never had a documented meaning for being `NULL`) are now enforced conditionally: required for `BANK`, forbidden for `CASH` — enforced at **both** the API layer (`BadRequestException`) and the database layer (a new `CHECK` constraint, `Accounting_System_Design_LKAS.md` §5.8) — defense in depth, not just a Service-layer rule, since this is a real data-integrity condition, not a UX nicety.
|
||||
|
||||
**Naming decision, made explicitly rather than defaulted:** the table and endpoints stay named `bank_account`/`bank-accounts` despite now covering Cash too — same reasoning as keeping `TaxSummary`'s name after its meaning changed entirely earlier today. Phases 23 (create), 24 (statement import), 25 (reconcile), and 34 (list) are all already built and live-tested; renaming would ripple through four working phases for a cosmetic gain. Flagged in `Accounting_System_Design_LKAS.md` §5.8 as reversible later (`cash_and_bank_account` would be the more accurate name) if ever revisited.
|
||||
|
||||
**Relationship to `CASH_ACCOUNT_ROOT_ID` (yesterday's Cash Flow work) clarified, not changed:** these are two different, complementary mechanisms — `CASH_ACCOUNT_ROOT_ID` is a *reporting* classification (which GL accounts count as cash for the Cash Flow statement), `bank_account.account_type` is *operational* metadata (the record an operator creates to track a specific account). Every `bank_account.gl_account_id` should be a descendant of `CASH_ACCOUNT_ROOT_ID` for the Cash Flow report to include it correctly — an expectation, not a DB-enforced link, same "documented, not enforced" posture as `tax_code`'s output/input account uniqueness.
|
||||
|
||||
**Status: not yet implemented.** `03_Progress_Tracker.md` Phases 23 and 34 reopened (both keep their original 2026-07-18 verified-live history, not overwritten) with a new outstanding item each.
|
||||
|
||||
**Docs updated this round:** `Accounting_System_Design_LKAS.md` (§5.8 rewritten with the `CHECK` constraint + naming note, §11.5's `CASH_ACCOUNT_ROOT_ID` section gains the relationship note), `04_API_Reference_And_Scenarios.md` (`POST /bank-accounts` request/validation table rewritten, `GET /bank-accounts` note added, Scenario 7's worked example updated to a realistic `BANK`-type payload), `01_Full_Development_Plan.md` (Phase 23/34 rows), `02_Phase_By_Phase_Development_Plan.md` (Phase 23/34 rows + new spec note), this file (Phase 23/34 rows, this entry). **Frontend docs (`12-GENERAL-LEDGER-INTEGRATION.md`, `21-GENERAL-LEDGER-FRONTEND.md`) not yet updated** — the create form's Account Type selector and conditional field visibility still need to be specified there.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-22 (same day, follow-up #3) — All six report retrofits from the "five reference templates" entry implemented and verified live; Cash/Bank rebuilt as two separate tables, deviating from the discriminator spec above
|
||||
|
||||
User approved the full six-item plan from the entry above in one batch ("do the modifications" / one consolidated approval, not per-file), then redirected the Cash/Bank shape mid-implementation once the discriminator design was actually built out — both are recorded here together since the redirection happened before the discriminator version was ever verified live.
|
||||
|
||||
**1–6. All six report items implemented and verified live**, in `05_LKAS_Report_PDF_Templates.md` §9's recommended order:
|
||||
- **Trial Balance flattened** — `TrialBalanceRowDto` drops `depth`/`indentedCode`, gains `accountCode`; `AccountRepository.GetTrialBalanceAsync` no longer needs the recursive tree at all (a flat postable-leaf listing doesn't need one). PDF/CSV both flat, portrait.
|
||||
- **Profit & Loss restructured** — new `accounts.income_statement_section` column (migration `AddIncomeStatementSectionTaxDeductibleAndCashAccounts`); `AccountRepository.GetProfitAndLossLeafRowsAsync` returns raw leaf rows tagged with the inherited section; `ReportService.BuildProfitAndLossReportAsync` groups them into the new nested `ProfitAndLossReportDto` (Sales/Cost of Sales/Gross Profit/Other Income/four expense groups/Unclassified/Net Profit). **Real sign bug found and fixed in the process:** the pre-existing `GetProfitAndLossAsync` summed Expense accounts as `credit − debit` with no flip — negative for a normal debit-balance expense — so `totalExpenses` (and therefore `profitForPeriod`, reused by both `BalanceSheet`'s Current Year Earnings and `CashFlow`) was silently wrong before this retrofit. Fixed by flipping sign for every Expense-tagged section's total; verified live: `Sales(730,000) − CostOfSales(80,000) = GrossProfit(650,000)` exactly, and retagging accounts between "Unclassified" and named sections doesn't change `netProfitForPeriod` (confirmed identical before/after tagging four test accounts, `-1,875,000` both times) — proof the regrouping is a pure presentation change, not a second data bug layered on top of the fix.
|
||||
- **Cash Flow simplified** — `direction` (`"Increase"`/`"Decrease"`) added to `CashFlowWorkingCapitalChangeDto`, computed from the raw balance-change sign in `GetCashFlowWorkingCapitalChangesAsync`'s SQL (independent of `changeAmount`'s cash-impact sign, per the design doc's note on why these differ). PDF/CSV no longer print `openingCashBalance`/`closingCashBalance` (still returned in `Json`/`Csv` data for the reconciliation check); non-cash adjustments and working-capital changes combined and re-bucketed by sign into "Additions to Cash"/"Subtractions From Cash" at render time.
|
||||
- **Tax Report redesigned wholesale into Income Tax Computation** — new `accounts.is_tax_deductible` column (same migration); five new `system_config` keys (`CORPORATE_INCOME_TAX_RATE`/`STATUTORY_RELIEF_AMOUNT` seeded via `HasData`, `APIT_CREDIT_ACCOUNT_ID`/`WHT_CREDIT_ACCOUNT_ID`/`QUARTERLY_TAX_PAYMENT_ACCOUNT_ID` seeded empty); new `ITaxRepository.GetNonDeductibleExpensesAsync`/`GetAccountCreditActivityAsync` (the latter reused three times, once per credit account); `TaxCode`-per-row `GetTaxSummaryAsync` removed entirely (the 2026-07-21 VAT/WHT/NBT design lives on only as reference text in `Accounting_System_Design_LKAS.md` §11.7†, not as live code). New `IncomeTaxComputationDto`/`IncomeTaxComputationPdfDocument`/`IncomeTaxComputationCsvDocument`, the latter two using the fuller `Company:Address`/`Tin`/`Brn` header block. Verified live end-to-end with both default config values and full request-param overrides (`allowableDeductions`, `otherTaxableIncome`, `surchargeAmount`, `taxRateOverride` all confirmed flowing through the 11-step computation correctly); a negative `balanceTaxPayable` renders as "BALANCE TAX REFUNDABLE" rather than a negative "payable" figure, confirmed on the seeded loss-making test dataset.
|
||||
- **`Company:Address`/`Tin`/`Brn`** — added to all three `appsettings.*.json`, read only by the Income Tax Computation report; `ReportPageHeader` gained a second `ComposeWithEntityDetails` overload for this one report's fuller header, rather than changing the shared header used by the other six.
|
||||
- **`outputFormat=Csv`** — new `ReportOutputFormat.Csv = 3`; new `Services/CsvDocuments/` folder (`CsvBuilder` shared row-building/escaping helper + one writer per report, parallel structure to `Services/PdfDocuments/`), same `ApiResponse` base64 envelope as `Pdf`. Verified live for all seven report types — valid CSV bytes returned for each, spot-checked `ProfitAndLoss.csv`'s Section-column flattening against the JSON response.
|
||||
|
||||
**Cash/Bank rebuilt as two separate tables, deviating from this file's own 2026-07-22 follow-up #2 entry (above) before that design was ever shipped.** While implementing the `bank_account.account_type` discriminator exactly as specified there, the user reviewed it and redirected: a cash account is not a kind of bank account, it's a peer concept, and cramming both into one table with a discriminator (plus `bankName`/`accountNumber` conditionally meaningless depending on the discriminator) was judged confusing for future developers rather than clarifying. Corrected design, built and verified live instead:
|
||||
- **`BankAccount`/`bank_account`** reverted to exactly its original pre-discriminator shape (`accountName`, `bankName`, `accountNumber`, `glAccountId`, `currencyCode`) — the discriminator migration was rolled back (`dotnet ef database update` to the prior migration + `migrations remove`) before it was ever applied beyond this session's own testing, so there's no stray discriminator column or check constraint left behind.
|
||||
- **New `CashAccount`/`cash_account` table**, structurally parallel but independent: `accountName`, `cashAccountTypeId` (FK to a new small **growable** reference table, `cash_account_type` — seeded with Petty Cash/Till Cash/Safe Cash/Cash in Transit, and gaining a new row automatically whenever `POST /cash-accounts` is called with a `cashAccountTypeName` that doesn't match an existing one, so a frontend "Other, please specify" field becomes a permanent selectable option from then on — confirmed live: creating a "Vault Reserve" cash account added `cash_account_type_id = 5`, immediately visible in a follow-up `GET /cash-account-types` call), `accountNumber`, `glAccountId`, `currencyCode`.
|
||||
- **`accountNumber` requirement changed for Cash, per explicit user direction:** unlike the discriminator design (which made it `NULL` for `CASH`), a cash account's `accountNumber` is required overall but optional on input — if the caller omits it, `BankService.CreateCashAccountAsync` auto-generates a sequential `CASH-{000001}`-style reference (probe-and-advance uniqueness check, same defensive pattern as the account/asset code generators, `CLAUDE.md` Rule 8.1); if supplied, it's used as-is. Verified live for both paths.
|
||||
- **`GET /bank-accounts` is now a unified, filterable list** — new optional `accountType` query param (`Cash`/`Bank`/`Both`, default `Both`), returning a combined, flattened `CashAndBankAccountDto` list (`accountType` discriminator field on the *response* only, not stored) rather than two separate list calls. Verified live: default call returned both existing bank accounts and the two newly-created cash accounts together; `accountType=Cash`/`accountType=Bank` each correctly filtered; an invalid value returned 400.
|
||||
- **Migration history correction:** the discriminator migration (`AddIncomeStatementSectionTaxDeductibleAndBankAccountType`) was fully rolled back and removed before this session ended — the live migration history only ever contains the final two-table design (`AddIncomeStatementSectionTaxDeductibleAndCashAccounts`), not an add-then-immediately-reverse pair.
|
||||
|
||||
**Docs still needing this same "mark as done" pass, not yet done as part of this entry:** `04_API_Reference_And_Scenarios.md` (Reporting module retrofit notes, plus a new Cash/Bank Accounts module section describing the two-table design — currently still describes the discriminator design from follow-up #2 above, which was never shipped), `05_LKAS_Report_PDF_Templates.md` (§2/§4/§5/§8 still describe pre-retrofit shapes in places, §10 CSV section still needs writing), `02_Phase_By_Phase_Development_Plan.md` (§1.9–§1.12 status flip, Phase 17's six retrofit notes, Phases 23/34 need rewriting around the two-table design instead of the discriminator), `Accounting_System_Design_LKAS.md` (§5.8 needs a full rewrite — the discriminator design documented there was never actually built).
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-22 (same day, follow-up #4) — Documentation sync pass: all four outstanding docs from the entry above brought in line with the actual two-table Cash/Bank design and the six report retrofits
|
||||
|
||||
Closes out the "docs still needing this pass" note directly above. No code changed in this entry — documentation only, checked against the live codebase and the live-verification results from follow-up #3, not rewritten from memory:
|
||||
|
||||
- **`01_Full_Development_Plan.md`** — Phase 23's row split into 23 (`bank_account`, unchanged) / 23a (`cash_account`, new) / 23b (`cash_account_type` list, new); Phase 34's row corrected to describe the actual unified-list retrofit (`accountType` query filter, response-only discriminator) instead of the never-shipped per-row `accountType` column.
|
||||
- **`02_Phase_By_Phase_Development_Plan.md`** — §1.9–§1.12 headers flipped from "not yet implemented" to "implemented and verified live"; Phase 17's six 2026-07-22 retrofit notes (`Csv`, `TrialBalance`, `ProfitAndLoss`, `CashFlow`, Income Tax Computation) all flipped to "implemented and verified live" with live-verification figures added; the dispatch/repository method list corrected to the actual method names used (`BuildProfitAndLossReportAsync` shared by three callers, `BuildTaxSummaryAsync` kept rather than renamed to `GetIncomeTaxComputationAsync` as originally planned — a cosmetic difference between the plan and the shipped code, now documented rather than left silently mismatched); the acceptance-test paragraph's "once implemented" conditionals replaced with actual verified figures.
|
||||
- **`05_LKAS_Report_PDF_Templates.md`** — Section 0 (`Company:*`) flipped to implemented, expanded to cover the 2026-07-22 `Address`/`Tin`/`Brn` addition; intro/scope note updated to stop describing `TaxSummary` as a VAT/WHT/NBT report (superseded); Section 9's stale bits fixed (`TaxSummary`'s "empty array" note no longer applies now that it's a single object; Trial Balance's orientation corrected from Landscape to Portrait, matching Section 1's own already-corrected table); new **Section 10** written (didn't exist before this entry) — per-report CSV column layout and flattening rules, matching what `Services/CsvDocuments/` actually implements.
|
||||
- **`Accounting_System_Design_LKAS.md`** — §5.8 fully rewritten: the discriminator design (`bank_account.account_type`, never shipped) replaced with the actual two-table design (`bank_account` unchanged, new `cash_account`/`cash_account_type`), including the corrected table/endpoint-naming rationale and the note that statement import/reconcile stay `bank_account`-only, not extended to `cash_account`. §11.5's cross-reference to `bank_account.account_type` corrected to reference both tables.
|
||||
- **`04_API_Reference_And_Scenarios.md`** — checked against the live codebase and found already accurate (its Bank module and Reporting module retrofit notes were updated in an earlier pass of this same work session) — no further changes needed here.
|
||||
|
||||
**This closes the documentation debt from follow-up #3.** Every doc now describes what's actually running, not a superseded discriminator design or a "not yet implemented" spec that was, in fact, implemented and verified live earlier in this same 2026-07-22 session.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-22 (same day, follow-up #5) — Journal entry create+post merge; request-body FK ids switched to codes project-wide; two endpoints removed
|
||||
|
||||
User flagged two real problems while reviewing `Controllers/GeneralLedgerController.cs`: (1) `PUT /journal-entries/{journalNo}/lines` and `PUT /journal-entries/{journalNo}/post` will never actually be called — the client (and any core backend integrating with this service) always sends a complete, ready-to-post journal entry in one call, never a draft it comes back to finish later; (2) request bodies across the API accept raw numeric ids (`accountId`, `costCenterId`, `taxCodeId`, ...) where the referenced entity has its own unique code — those should be codes too, the same as path parameters already are (`CLAUDE.md` Rule 8).
|
||||
|
||||
**1. Journal entries — `CLAUDE.md` Rule 14 (new).** `PUT .../lines` and `PUT .../post` removed entirely; `POST /journal-entries` now validates, inserts, balance-checks, period-lock-checks, and posts, all in one Unit-of-Work transaction. `lines` is now required for a plain create and for a `PartialAdjustment` correction (previously both deferred lines to the now-removed `PUT .../lines` call) — `FullReversal` still auto-builds its lines by mirroring the original. There is no client-facing `DRAFT` state reachable via the API anymore. `IJournalEntryService.SetLinesAsync`/`PostAsync` removed from the interface; the now-fully-unused `IJournalEntryRepository.DeleteJournalEntryLinesByJournalIdAsync`, `GetJournalEntryByIdAsync`, and `GetJournalCorrectionByCorrectionJournalIdAsync` removed too, rather than left dead.
|
||||
|
||||
Every other Service that generates a journal entry internally updated to match:
|
||||
- **`FixedAssetService`** (`Depreciate`/`Dispose` transactions) — previously called `_journalEntryService.CreateAsync(...)` then a separate `PostAsync(...)`; now calls `CreateAsync` once and uses its already-posted result directly. Also gained `ResolveAccountCodeAsync(long accountId)` — a small helper to turn the asset's own persisted numeric account-id fields (`AssetAccountId`, `AccumDeprAccountId`, etc. — unaffected, storage stays numeric) back into a code when building a `JournalEntryLineRequest`.
|
||||
- **`RecurringTemplateService.GenerateAsync`** — was already only calling `CreateAsync` once (no separate `PostAsync`, so a generated recurring entry used to sit as `DRAFT` for later review) — now the same call auto-posts, so a generated entry comes back already `Posted`. Confirmed as the correct new behavior (no separate review step exists anymore for any journal entry, recurring or otherwise), not a regression.
|
||||
|
||||
**2. Request-body/query-string FK ids → codes — `CLAUDE.md` Rule 8.2 (new), reverses Rule 8's original "request bodies stay numeric" carve-out for leaf references.** Every field in the table below was a numeric FK id before this retrofit:
|
||||
|
||||
| DTO | Field(s) changed |
|
||||
|---|---|
|
||||
| `JournalEntryLineRequest` | `accountId`→`accountCode`, `costCenterId`→`costCenterCode` |
|
||||
| `JournalEntryCorrectionRequest` | `originalJournalId`→`originalJournalNo` |
|
||||
| `BudgetLineRequest` | `accountId`→`accountCode`, `costCenterId`→`costCenterCode` (`periodId` stays numeric — no unique code column) |
|
||||
| `CreateTaxCodeRequest` | `taxTypeId`→`taxTypeCode`, `outputTaxAccountId`/`inputTaxAccountId`→`...AccountCode` |
|
||||
| `CalculateTaxRequest` | `taxCodeId`→`taxCode` |
|
||||
| `CreateFixedAssetRequest` | all 4 GL account fields →`...Code` |
|
||||
| `CreateFixedAssetTransactionRequest` | `proceedsAccountId`→`proceedsAccountCode` (`periodId` stays numeric) |
|
||||
| `RecurringTemplateLineRequest` | `accountId`→`accountCode` |
|
||||
| `CreateBankAccountRequest`/`CreateCashAccountRequest` | `glAccountId`→`glAccountCode` |
|
||||
| `ReconcileStatementLineRequest` | `matchedJournalId`→`matchedJournalNo` |
|
||||
| `ListReportsRequest` (query string) | `accountId`→`accountCode` (`GeneralLedger` report) |
|
||||
|
||||
**Explicitly NOT changed, confirmed rather than silently swept in:** `parentAccountId`/`parentCostCenterId` (recursive-tree fields, Rule 8's original exception — still stands) and every FK referencing an entity with no unique code column (`periodId`, `fiscalYearId`, `budgetId`).
|
||||
|
||||
**Verified live, one request per changed endpoint:** plain journal entry create with `accountCode`s → returned `isPosted=true` in the same response; `FullReversal` correction by `originalJournalNo` → auto-posted, original flipped to `Reversed`; `PartialAdjustment` correction with inline `lines` (against a still-`Posted` original) → auto-posted; both removed `PUT` routes → `404`; budget line add with `accountCode`+`costCenterCode`; tax code create with `taxTypeCode`+`outputTaxAccountCode`, then `GET .../calculate?taxCode=...`; fixed asset create with the 4 `...Code` fields, then a `Depreciate` transaction (confirms the internal id→code resolution path); recurring template create with `accountCode` lines, then `generate` (confirms the entry comes back posted); bank account create with `glAccountCode`; reconcile with a non-existent `matchedJournalNo` → `404` (confirms the lookup switched from id to `journalNo`); `GeneralLedger` report with `accountCode` query param.
|
||||
|
||||
**Docs updated:** this file (Phases 11/12/13 rows rewritten, 15/16/19/20/21/22/23/23a/25/27 rows gain a retrofit note, this entry), `CLAUDE.md` (new Rule 8.2, new Rule 14). **Not yet updated as part of this entry, flagged as the next step:** `04_API_Reference_And_Scenarios.md` (Journal Entries module needs its Set Lines/Post sections removed and Create rewritten; Budget/Tax/Fixed Assets/Recurring Templates/Bank/Reports modules need their changed field names updated) and `02_Phase_By_Phase_Development_Plan.md` (the Phases 11–13 worked example needs rewriting around the merged single-endpoint design).
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-30 — Cheque Management added (Phase 37, new module, beyond the original 36-phase plan)
|
||||
|
||||
User supplied a raw field spec for three tables — `ChequeBook`, `ChequePage` (issued cheques, from this company's own cheque books) and `ReceivedCheque` (cheques received from others, explicitly with **no** `ChequeBook` link) — and asked for cheque management to be added to the service. Three genuinely open design questions were resolved via direct clarification before any code was written (per `CLAUDE.md` Rule 3):
|
||||
|
||||
1. **GL integration: purely operational tracking, no automatic journal postings.** Issuing/clearing/bouncing/cancelling a cheque never calls or triggers `POST /journal-entries` itself — it only stores caller-supplied references (`voucherId`, `referenceNo` on `ChequePage`; `referenceType`/`referenceId` on `ReceivedCheque`). The caller posts the actual journal entry separately and links back via those fields, the same relationship `PUT /bank-statement-lines/{id}/reconcile` already has with `matchedJournalNo` — reconciliation doesn't create a journal entry either, it matches an existing one.
|
||||
2. **Leaf generation: auto-generated.** `POST /cheque-books` generates all `totalLeaves` `ChequePage` rows (status `Unused`) immediately, numbered sequentially from `startChequeNo` to `endChequeNo` (zero-padded to `startChequeNo`'s width) — mirrors the existing fiscal-year period auto-generation (Phase 7).
|
||||
3. **Company/Branch scope, asymmetric per user direction:** `ChequeBook` gets `branchId` (required) and no `companyId`; `ReceivedCheque` gets `companyId` (required) and `branchId` (nullable). Neither is a FK — no `Company`/`Branch` table exists anywhere in this system (confirmed by grep before asking), so both are plain, unvalidated `long`/`long?` columns, same treatment as every other loose external reference in this module.
|
||||
|
||||
**Deviations from the client's raw field spec, each forced by something that doesn't exist in this codebase, not a stylistic choice:**
|
||||
- **`PayeeId` (`ChequePage`), `ReceivedFromId`/`ReferenceId` (`ReceivedCheque`), `VoucherId` (`ChequePage`)** — no `Supplier`/`Customer`/`Employee`/`Voucher` master tables exist in this GL service (this service receives already-decided data from a core backend/other modules, per this project's founding premise). All four stay as loose `long?` references with no FK constraint, same treatment as `AuditLog.EntityId`.
|
||||
- **`BankId`/`BankBranch` (`ReceivedCheque`)** — renamed `DrawerBankName`/`DrawerBankBranch`, plain free-text `string?`, no FK — no bank-master table exists; follows the exact precedent of `BankAccount.BankName` already being free text rather than a FK.
|
||||
- **`ChequeBookNo`/`StartChequeNo`/`EndChequeNo`** stay caller-supplied (unlike `AccountCode`/`AssetCode`'s system-generated scheme, Rule 8.1) — these are printed on the physical cheque book by the actual bank, not invented by this service. `ChequeBookNo` and `ChequeNo` are still usable as Rule-8-style unique lookup keys once stored (`GET /cheque-books/{chequeBookNo}`, `GET /cheque-pages/{chequeNo}`).
|
||||
- **`CreatedBy`/`ModifiedBy`/`PrintedBy`** — spec said `bigint` (a user id); this system has no user/identity table anywhere, so these are free-text `string?`, matching the established `JournalEntry.PreparedBy` pattern.
|
||||
- **`CurrencyId` (`ChequePage`)** → `CurrencyCode` (`string?`, FK to `currency.currency_code`, defaults `LKR`) — same code-based FK convention used everywhere else in this API (`CLAUDE.md` Rule 8.2).
|
||||
- **`BankAccountId` (`ChequeBook`) / `DepositBankAccountId` (`ReceivedCheque`)** stay numeric, not converted to a code — `BankAccount` has no unique code column (`AccountNumber` is optional), so this falls under Rule 8.2's explicit "no unique code column" carve-out, same as `periodId`/`fiscalYearId`/`budgetId` elsewhere.
|
||||
- **Amounts (`ChequePage.Amount`, `ReceivedCheque.Amount`)** — no `HasPrecision` call, unbounded `numeric` at the DB layer, per `CLAUDE.md` Rule 13.
|
||||
|
||||
**Built:** `Enums/ChequeBookStatus.cs`, `ChequeIssueStatus.cs`, `ChequePayeeType.cs`, `ReceivedChequeFromType.cs`, `ReceivedChequeStatus.cs`, `ChequePageAction.cs`, `ReceivedChequeAction.cs` (the last two are `PUT .../status` action discriminators, not stored columns — same role as `PeriodLockAction`). `Models/Cheque.cs` (`ChequeBook`, `ChequePage`, `ReceivedCheque` — one file, one cluster, per Rule 2's existing precedent). `AppDBContext.cs` (3 `DbSet`s + 3 `Configure*` methods), migration `AddChequeManagement`, applied to the local database. `Interfaces/Repositories/IChequeRepository.cs` + `Repositories/ChequeRepository.cs` (one repository for the whole cluster, per Rule 2). `Interfaces/Services/IChequeService.cs` + `Services/ChequeService.cs`. 9 new DTOs under `DTOs/Cheques/`. 11 new controller actions on `GeneralLedgerController` (Phase 37.1–37.11 above). DI registrations added to `Program.cs`.
|
||||
|
||||
**Enum-in-JSON-body lesson relearned mid-implementation:** `IssueChequePageRequest.PayeeType` and `CreateReceivedChequeRequest.ReceivedFromType` were initially declared as the C# enum type directly — this project has no global `JsonStringEnumConverter` registered (confirmed in `Program.cs`), so a JSON body enum field must be bound as `string` and parsed manually in the Service via `Enum.TryParse<T>(raw, ignoreCase: true, out var parsed)`, throwing `BadRequestException` with the valid-values list on failure — exactly the existing `RecurrencePattern`/`PeriodLockAction` pattern. Query-string-bound enums (the `List*Request` DTOs' `status`/`issueStatus`/`payeeType`/`receivedFromType` filters) don't have this problem — ASP.NET Core's route/query binder parses enum names natively — so those stayed as the real enum type.
|
||||
|
||||
**Verified live, one request per endpoint:** created a 10-leaf cheque book (`branchId`, `bankAccountId`, `chequeBookNo`, numeric `startChequeNo`/`endChequeNo` range) → 10 `Unused` pages auto-generated in the same response; `GET` single book (no expand) and list by `bankAccountId`; issued two pages (`Supplier`/`Customer` payee types); `Clear` on an `Issued` page (requires `clearedDate`) → `Cleared`; `Bounce` on an `Issued` page → `Bounced`; `Cancel` on an `Unused` page (requires `cancelReason`) → `Cancelled`; `Void` on an `Unused` page → `Void`; attempted `Clear` on a still-`Unused` page → `400` with the current-status message; `GET` single page and list filtered by `issueStatus=Cleared`; created a received cheque (`Customer`, `HNB`/`Colombo 03` drawer bank as free text, `referenceType`/`referenceId`); `Deposit` (requires `depositBankAccountId`+`depositDate`, validated against a real bank account) → `Deposited`; `Clear` → `Cleared`; `GET` single and list filtered by `companyId`; duplicate `chequeBookNo` → `400`; mismatched `totalLeaves` vs. the numeric range → `400`; nonexistent `bankAccountId` → `404`. Dev server stopped on port 5200 after testing, per this project's standing convention.
|
||||
|
||||
**Docs updated:** this file (new Phase 37 section, Overall Progress Summary counts, this entry). **Not yet updated as part of this entry, flagged as the next step:** `04_API_Reference_And_Scenarios.md` (needs a new Cheque Management module section) and `02_Phase_By_Phase_Development_Plan.md` (needs a new Phase 37 worked-example section, matching the style of Phases 30–36's appended-endpoint sections).
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-31 — `BalanceSheet` restructured into a classified statement (Non-Current/Current Assets and Liabilities)
|
||||
|
||||
User supplied a reference template (a Sri Lankan NGO's "Statement of Financial Position") showing a **classified** balance sheet — Non-Current Assets and Current Assets each subtotaled separately, likewise Non-Current Liabilities and Current Liabilities, with Equity as its own block — and asked for the `BalanceSheet` `reportType`'s response/PDF/CSV to match. The flat recursive-rollup array this report used since Phase 5 had no such split; `05_LKAS_Report_PDF_Templates.md` Section 3 had explicitly documented this as a known limitation ("classification is achieved entirely through how the company structures its own Chart of Accounts... this template renders that hierarchy as-is; it does not independently classify anything").
|
||||
|
||||
Two open design questions resolved via direct clarification before any code was written (per `CLAUDE.md` Rule 3):
|
||||
1. **Untagged Asset/Liability leaf accounts land in an `unclassifiedAssets`/`unclassifiedLiabilities` bucket**, not a `400` — same rule `ProfitAndLoss`'s `unclassified` section already established, so an incomplete tagging pass is visible in the report rather than silently excluded.
|
||||
2. **Equity stays one flat section**, no second sub-classification tag — the reference template's visual two-group split within Equity (accumulated fund vs. joining/membership fees) is an NGO/fund-accounting convention, not something this schema's single Equity account type needs a second tag dimension to express.
|
||||
|
||||
**New mechanism: `accounts.balance_sheet_classification`, same recursive-tag-and-inherit pattern as `cash_flow_category`/`income_statement_section`.** New nullable `VARCHAR(25)` column (migration `AddBalanceSheetClassificationToAccount`), values `NON_CURRENT_ASSET`/`CURRENT_ASSET`/`NON_CURRENT_LIABILITY`/`CURRENT_LIABILITY`, settable only via `PUT /accounts/{accountCode}` (`CLAUDE.md` Rule 8.1-style: never accepted on create). Equity accounts need no tag — `account_type_id` alone already puts them in one `equity` section.
|
||||
|
||||
**Response reshaped from a flat `{depth, lineItem, accountType, balance}` array into:** `{ asOfDate, nonCurrentAssets: {lines[], total}, currentAssets: {lines[], total}, unclassifiedAssets: {lines[], total}, totalAssets, equity: {lines[], total}, nonCurrentLiabilities: {lines[], total}, currentLiabilities: {lines[], total}, unclassifiedLiabilities: {lines[], total}, totalEquityAndLiabilities }`. The existing "Current Year Earnings" synthetic row (cumulative-since-inception net profit, Phase 5's balancing fix) is now folded into `equity.lines[]` rather than a separate top-level field.
|
||||
|
||||
**Built:** `Models/Account.cs` (+`BalanceSheetClassification`), `AppDBContext.cs` (`ConfigureAccount`), `DTOs/Accounts/UpdateAccountRequest.cs` + `AccountService.UpdateAccountAsync` (parse/apply the new field, same as `cashFlowCategory`/`incomeStatementSection` — no enum-level validation, consistent with those two, since the SQL just compares literal strings). `DTOs/Reports/ReportRowDtos.cs` — `BalanceSheetRowDto` removed, replaced by `BalanceSheetLineDto`/`BalanceSheetSectionDto`/`BalanceSheetReportDto` (public) + `BalanceSheetLeafRowDto` (raw projection). `AccountRepository.GetBalanceSheetAsync` (recursive rollup) removed, replaced by `GetBalanceSheetLeafRowsAsync` (leaf/postable rows only, same shape as `GetProfitAndLossLeafRowsAsync`) — interface updated to match. `ReportService.BuildBalanceSheetAsync` rewritten to group leaf rows by `AccountTypeId`+`BalanceSheetClassification` into the new sections and compute both grand totals. `Services/PdfDocuments/BalanceSheetPdfDocument.cs` and `Services/CsvDocuments/BalanceSheetCsvDocument.cs` rewritten around the new section shape (empty sections skipped entirely in the PDF, same rule as `ProfitAndLoss`).
|
||||
|
||||
**Verified live** against the already-seeded comprehensive test dataset: tagged `1400` (Fixed Assets header) `NON_CURRENT_ASSET`; `1000`/`1100`/`1200`/`1300` (Cash and Bank, Accounts Receivable, Inventory, Prepaid Expenses) `CURRENT_ASSET`; `2300` (Bank Loan) `NON_CURRENT_LIABILITY`; `2000`/`2100`/`2200` (Accounts Payable, VAT Payable, Accrued Expenses) `CURRENT_LIABILITY`. `GET /reports?reportType=BalanceSheet&outputFormat=Json&asOfDate=2026-12-31` returned `totalAssets = 5,363,714` and `totalEquityAndLiabilities = 5,363,714` — exact match. `outputFormat=Pdf` produced a valid 55KB PDF; `outputFormat=Csv` produced a valid, correctly-sectioned CSV. `currentLiabilities` came back empty for this dataset (all three tagged current-liability accounts were fully settled by year end) — confirmed as correct given the seeded transactions, not a bug; it renders as an omitted section in the PDF per the "skip empty sections" rule.
|
||||
|
||||
**Docs updated:** this file (this entry), `04_API_Reference_And_Scenarios.md` (`BalanceSheet` reportType row rewritten, new retrofit note, `PUT /accounts/{accountCode}` field list corrected — also picked up the already-missing `incomeStatementSection`/`isTaxDeductible` fields while touching that line — old bugfix notes marked superseded rather than rewritten), `Accounting_System_Design_LKAS.md` (§11.3 gained the new column + leaf-row query design), `05_LKAS_Report_PDF_Templates.md` (Section 3 response shape, template, implementation notes, and the CSV column table all updated).
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-31 (same day, follow-up) — `GeneralLedger` PDF moved from landscape to portrait A4
|
||||
|
||||
User reported receiving a landscape-oriented `GeneralLedger` PDF and wanted portrait A4, matching every other report. `Services/PdfDocuments/GeneralLedgerPdfDocument.cs`'s `page.Size(PageSizes.A4.Landscape())` changed to plain `page.Size(PageSizes.A4)` — no other layout change (same six columns, same group-header-row account boundaries). Verified live: rendered PDF's `/MediaBox` is `[0 0 595 842]` (595×842pt — A4 portrait). `TrialBalance` (the other report `05_LKAS_Report_PDF_Templates.md` had listed as landscape) had already moved to portrait back on 2026-07-22 for an unrelated reason (its own flattening retrofit); `GeneralLedger` was the only report still landscape, so after this change every report PDF is portrait A4.
|
||||
|
||||
**Docs updated:** this file (this entry), `04_API_Reference_And_Scenarios.md` (new retrofit note), `05_LKAS_Report_PDF_Templates.md` (orientation table's `General Ledger` row updated).
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-31 (same day, follow-up #2) — `glAccountCode` removed from Bank/Cash account creation; backing GL account now always auto-created
|
||||
|
||||
User pointed out that requiring a caller to select an existing GL account when creating a bank or cash account is unnecessary friction — a bank account is definitionally a bank account, and a cash account of a given type is definitionally under that type, so there's no real choice for the caller to make. Two design questions resolved via direct clarification before any code was written (per `CLAUDE.md` Rule 3):
|
||||
|
||||
1. **The "Bank"/"Cash" GL header roots (and each cash type's own node under "Cash") are auto-created on first use**, not gated behind a `system_config` setup step — found-or-created by exact name, same convention `cash_account_type` already uses for its own "grows at runtime" rows. **User specified the exact names: `Bank` and `Cash`** (not "Bank Accounts"/"Cash Accounts").
|
||||
2. **`glAccountCode` removed entirely** from both `CreateBankAccountRequest`/`CreateCashAccountRequest` — no optional override kept.
|
||||
|
||||
**New auto-created GL hierarchy:** `Bank` (root Asset, non-postable) → one postable leaf per `BankAccount`, never reused. `Cash` (root Asset, non-postable) → one non-postable node per `cash_account_type` (created once, then reused for every cash account of that type) → one postable leaf per `CashAccount`, never reused. Full design: `Accounting_System_Design_LKAS.md` §5.8's new retrofit note.
|
||||
|
||||
**Built:** `Services/AccountCodeGenerator.cs` — a new shared static helper (same shape as `RecursiveTreeQueries`/`DepthGuard`, `CLAUDE.md` Rule 11) extracted from `AccountService`'s previously-private `GenerateAccountCodeAsync`, so both `AccountService.CreateAccountAsync` and `BankService`'s new auto-creation logic use the exact same Rule 8.1 code-gen scheme without duplicating it (`AccountService.cs` itself has no behavior change, pure extraction). `Interfaces/Repositories/IAccountRepository.cs` + `AccountRepository.cs` gained `GetByNameAndParentAsync` (exact-name lookup used to find-or-create `Bank`/`Cash`). `Models/Bank.cs`'s `CashAccountType` gained a nullable `GlAccountId` (+ nav `GlAccount`), migration `AddGlAccountIdToCashAccountType` (nullable column + FK + index, existing 4 seeded types left `NULL`). `DTOs/Bank/CreateBankAccountRequest.cs`/`CreateCashAccountRequest.cs` — `GlAccountCode` field removed. `Services/BankService.cs` — `CreateBankAccountAsync`/`CreateCashAccountAsync` rewritten around two new private helpers, `GetOrCreateSystemRootAsync` (find-or-create a root by exact name) and `CreateSystemChildAccountAsync` (always creates a new child, used for both the cash-type node and every leaf) — both call `AccountCodeGenerator.GenerateAsync` for their code and write their own `Ledger`/`Create` audit row per new `Account`, in addition to the existing `Administrative`/`Create` audit row for the `BankAccount`/`CashAccount` itself. No `DepthGuard` check — the structure is fixed at 2-3 levels, well under `COA_MAX_DEPTH`'s default of 6.
|
||||
|
||||
**Verified live** against the already-seeded dataset (which already occupied Asset root slots `1000`/`1100`/`1200`/`1300`/`1400`): `POST /bank-accounts` with no `glAccountCode` auto-created `1500 Bank` → `1500-01 HNB Corporate Account`, returned nested as `data.glAccount`/`data.glAccount.parentAccount`. Two `POST /cash-accounts` calls with `cashAccountTypeName: "Petty Cash"` auto-created `1600 Cash` → `1600-01 Petty Cash` (created once) → `1600-01-01`/`1600-01-02` (one leaf each) — confirmed via `GET /cash-account-types` that `Petty Cash`'s `glAccountId` was `null` before the first call and populated (unchanged by the second) afterward.
|
||||
|
||||
**Docs updated:** this file (this entry), `04_API_Reference_And_Scenarios.md` (`POST /bank-accounts`/`POST /cash-accounts` sections rewritten, worked-scenario example fixed), `Accounting_System_Design_LKAS.md` (§5.8 gained the new retrofit note).
|
||||
|
||||
**2026-10-06 — Database provider migration: PostgreSQL → MySQL (Pomelo), raw SQL → EF Core LINQ.** User-directed, not a numbered phase. Status: `◐ In progress`. Code converted and building; `InitialMySql` applied to a fresh local database (`ERPAcc_Dev`, XAMPP **MariaDB 10.4.32**, not MySQL 8) and every endpoint exercised (2026-10-06): 245 of 249 scripted checks passed; the 4 non-passes were test-script mistakes (expected 200 vs actual 201, and a single-element JSON array that PowerShell unwrapped), re-checked and confirmed correct. Trial balance, P&L, balance sheet, general ledger running balances, budget vs actual, tax non-deductibles and the cash-flow category/working-capital queries were cross-checked row-for-row against the original SQL run directly on the database and matched. **Not yet done:** testing against a real MySQL 8 server, and copying the existing PostgreSQL data across. One pre-existing gap seen, unrelated to the migration: `POST /accounts` accepts an empty `accountName` (no validation).
|
||||
- **Packages:** `Npgsql.EntityFrameworkCore.PostgreSQL` removed; `Pomelo.EntityFrameworkCore.MySql` 9.0.0 added. Pomelo's latest release targets EF Core 9, so `Microsoft.EntityFrameworkCore`, `.Design`, `.Tools` and the HealthChecks EF package were moved from 10.0.9 to 9.0.9 (the app still targets .NET 10). `Program.cs` uses `UseMySql` with a fixed `MySqlServerVersion(8.0.36)` so startup needs no live DB for version detection.
|
||||
- **Schema:** `timestamptz`/`now()` → `datetime(6)`/`CURRENT_TIMESTAMP(6)`. Every decimal column is now `DECIMAL(38,10)` (MySQL has no unbounded numeric — `CLAUDE.md` Rule 13 updated). The partial unique index on `currency.is_base_currency` (unsupported in MySQL) became a functional unique index created by raw SQL inside the migration. All 14 PostgreSQL migrations and the model snapshot were deleted and replaced by a single `InitialMySql` migration (old history remains in git).
|
||||
- **Raw SQL removed entirely:** all 16 `SqlQuery`/`SqlQueryRaw` methods (`AccountRepository` reports, `JournalEntryRepository` general ledger, `BudgetRepository`, `TaxRepository`, `FixedAssetRepository`, and `RecursiveTreeQueries`) were rewritten as EF Core LINQ. Sums, joins and filters run in the database; recursive tree walks, tag inheritance and GL running balances are computed in C#. `Helpers/RecursiveTreeQueries.cs` is now a set of pure in-memory functions (`CLAUDE.md` Rule 11 updated). `ROUND(...,10)` guards were dropped — C# `decimal` arithmetic has no float noise. No public request/response shape changed.
|
||||
- **Config:** the three-file layout is kept, with a clearer split. `appsettings.json` holds only settings common to every environment (Kestrel URL, logging, allowed hosts, JWT issuer/audience/token lifetime). `appsettings.Development.json` holds development-only values (local MariaDB/MySQL connection string `ERPAcc_Dev`, JWT signing key, API key, demo `Company` details). `appsettings.Production.json` holds production-only values and is unchanged (blank `ConnectionStrings`/`Jwt:SigningKey`/`ApiKey:Value`, placeholder `Company`), to be filled at deploy time — no production credentials were written anywhere. The old Postgres connection string and the `ApiKey:Value` that were in `appsettings.json` were removed from it.
|
||||
- **Known behavior differences to watch when testing:** MySQL's default collation is case-insensitive (unique codes now compare case-insensitively); report row ordering by code is now an ordinal (case-sensitive) sort in C# instead of the database collation.
|
||||
|
||||
**2026-10-20 - Trial Balance fix (found after the MySQL migration, but a pre-existing defect, not caused by it).** `AccountRepository.GetTrialBalanceAsync` returned each account's GROSS debit and credit totals side by side, so an account with movement on both sides, or an entry plus its reversal, showed equal or duplicated figures in both columns. It now returns the NET balance in one column only (debit balance in Debit, credit balance in Credit, the other 0) and omits accounts whose net is zero, as `05_LKAS_Report_PDF_Templates.md` Section 2 specifies. PDF and CSV already blanked zero cells, so no change there. Verified on the local test database (JSON, PDF, CSV): no row has both columns filled, the net-zero Interest Income account no longer appears, total debit equals total credit.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,403 @@
|
||||
# LKAS-Compliant PDF Report Templates
|
||||
|
||||
**Purpose:** replace the original generic, reflection-based table renderer shared by all reports with a real, standards-appropriate layout per report — **done**: seven bespoke `IDocument` layouts (`Services/PdfDocuments/`) plus seven parallel CSV writers (`Services/CsvDocuments/`, Section 10), implemented and verified live 2026-07-21/2026-07-22. `TaxSummary` (Section 8, redesigned 2026-07-22 into a full Income Tax Computation) is the one genuinely new report added since the original six, alongside the API's own 7th `reportType`.
|
||||
|
||||
**Scope note, upfront:** only two of these seven are actually LKAS-*prescribed* financial statements — **Balance Sheet** (LKAS 1, *Statement of Financial Position*) and **Profit & Loss** (LKAS 1, *Statement of Profit or Loss*). **Cash Flow** is fully LKAS 7-governed (Section 5, implemented and verified live). **Trial Balance**, **General Ledger**, **Budget vs Actual**, and the Income Tax Computation are not LKAS statements at all — the first three are internal working papers/management reports with no prescribed format, and the Income Tax Computation is closer to LKAS 12 territory but still not a genuine LKAS 12 disclosure note (Section 8 explains why: no deferred tax anywhere in this schema) — so their templates below follow standard professional/regional accounting practice rather than a specific LKAS clause.
|
||||
|
||||
---
|
||||
|
||||
## 0. Entity name — `Company:Name` in `appsettings.json` (implemented and verified live)
|
||||
|
||||
LKAS 1.51 requires every financial statement to display, at minimum: the name of the reporting entity, whether the statements are for an individual entity or a group, the date/period covered, the presentation currency, and the level of rounding used. Everything except the entity's name is already derivable from existing data (period dates from the request, currency from the seeded `currency` table's `is_base_currency` row). The entity/company name lives in `appsettings.json` as `Company:Name` — a deployment-time constant, not a runtime-editable `system_config` value, since it isn't the kind of thing a company changes via API once deployed. Every environment's file gets a real value (unlike `Jwt:SigningKey`/`ApiKey:Value`, this isn't a secret, so there's no blank-in-Production convention here). **Expanded 2026-07-22:** `Company:Address`/`Tin`/`Brn` added alongside `Company:Name`, read only by the Income Tax Computation report's fuller identity block (Section 8) — the other six reports still use just `Company:Name` via the plain `ReportPageHeader.Compose` overload. `ReportService` reads `Company:Name` via `IConfiguration`, falling back to `"Company Name Not Configured"` rather than crashing PDF generation if it's somehow missing.
|
||||
|
||||
---
|
||||
|
||||
## 1. Shared header/footer — used by all seven templates
|
||||
|
||||
Every report's PDF opens with the same three-line block (LKAS 1.51's minimum, applied even to the non-LKAS reports for consistency) and closes with the same footer:
|
||||
|
||||
```
|
||||
[Company Name — from appsettings.json Company:Name]
|
||||
[Report Title]
|
||||
[Period line — see per-report wording below]
|
||||
(Amounts in [Currency] — from currency.is_base_currency row's currency_code)
|
||||
|
||||
...report body...
|
||||
|
||||
──────────────────────────────────────────────
|
||||
Generated [timestamp] Page [n] of [N]
|
||||
```
|
||||
|
||||
Two period-line conventions, applied consistently per report type:
|
||||
- **Point-in-time reports** (Trial Balance, Balance Sheet): `As at [asOfDate]`
|
||||
- **Period reports** (Profit & Loss, Cash Flow, General Ledger, Budget vs Actual, Tax Summary): `For the period [periodStart] to [periodEnd]`
|
||||
|
||||
### Page size — A4, every report (spec 2026-07-21)
|
||||
|
||||
```csharp
|
||||
page.Size(PageSizes.A4); // QuestPDF built-in constant — 210mm × 297mm, no manual math
|
||||
page.Margin(2, Unit.Centimetre); // all four sides
|
||||
```
|
||||
|
||||
**Orientation — portrait by default, landscape only where a report's column count genuinely doesn't fit:**
|
||||
|
||||
| Report | Orientation | Why |
|
||||
|---|---|---|
|
||||
| Trial Balance | Portrait (retrofit 2026-07-22, moved from Landscape) | Flattened to `{ accountCode, accountName, debit, credit }` (Section 2) — no more indentation eating horizontal space, so the original Landscape justification no longer applies |
|
||||
| Balance Sheet | Portrait | One amount column, short line items — fits comfortably |
|
||||
| Profit & Loss | Portrait | Same reasoning as Balance Sheet — restructured into named sections (Section 4), still one amount column |
|
||||
| General Ledger | Portrait (retrofit 2026-07-31, moved from Landscape) | Landscape was the original width-based justification for six columns (`Date, Particulars, J.R., Debit, Credit, Balance`) plus the group-header account name, but the user explicitly wants this report on a standard portrait A4 page like the rest — narrower columns rather than a wider page |
|
||||
| Cash Flow | Portrait | One amount column throughout, longer line-item labels but no width pressure |
|
||||
| Budget vs Actual | Landscape if a "Variance %" column is added (Section 7's optional enhancement); portrait is fine with just the four core columns |
|
||||
| Tax Summary (Income Tax Computation) | Portrait | Single computation column, no table at all — fits portrait easily |
|
||||
|
||||
`page.Size(PageSizes.A4.Landscape())` for the landscape rows above — same constant, QuestPDF provides the rotated variant directly, no separate width/height literals needed.
|
||||
|
||||
---
|
||||
|
||||
## 2. Trial Balance
|
||||
|
||||
**Response shape (retrofit 2026-07-22 — flat, implemented and verified live):** array of `{ accountCode, accountName, debit, credit }`. Previously `{ depth, indentedCode, accountName, debit, credit }`; a user-supplied reference template showed a plain list with no account-tree indentation at all, so `depth`/`indentedCode` were dropped from the response, the PDF, and the CSV — presentation-only, the underlying query (leaf/postable accounts only, same balance computation) is unchanged. The recursive tree the old query used to compute `depth` wasn't needed for anything else, so `AccountRepository.GetTrialBalanceAsync` dropped it entirely rather than keeping unused columns around.
|
||||
|
||||
Not an LKAS statement — a working paper proving total debits equal total credits before other statements are trusted. Standard format:
|
||||
|
||||
```
|
||||
[Company Name]
|
||||
Trial Balance
|
||||
As at [asOfDate]
|
||||
(Amounts in LKR)
|
||||
|
||||
Account Code Account Name Debit Credit
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
1101 Cash and Cash Equivalents 125,000.00
|
||||
1102 Trade Receivables 84,500.00
|
||||
1201 Property, Plant & Equipment 650,000.00
|
||||
2101 Trade Payables 42,300.00
|
||||
3101 Share Capital 500,000.00
|
||||
4101 Sales Revenue 280,000.00
|
||||
5101 Rent Expense 25,000.00
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
TOTAL 684,500.00 822,300.00
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Portrait, not landscape — flattening removed the indentation that originally justified the wider landscape layout (`05_LKAS_Report_PDF_Templates.md` Section 1).
|
||||
- **Column-align, not row-align** the Debit/Credit split — a row with a debit balance leaves the Credit cell blank (not `0.00`), and vice versa. A zero-balance account shouldn't appear at all (the underlying query already excludes these).
|
||||
- Totals row is the one place this report **must** show `SUM(debit) = SUM(credit)` — if a PDF ever renders with those unequal, that's a rendering bug, not a data issue (the posting engine guarantees balance at the ledger level).
|
||||
- Real output only ever contains postable leaf accounts with a nonzero balance, one row each, in `accountCode` order.
|
||||
|
||||
---
|
||||
|
||||
## 3. Balance Sheet (Statement of Financial Position) — LKAS 1
|
||||
|
||||
**Response shape (retrofit 2026-07-31 — nested, implemented and verified live, supersedes the original flat `{depth, lineItem, accountType, balance}` recursive-rollup array):**
|
||||
```
|
||||
{ asOfDate,
|
||||
nonCurrentAssets: { lines[], total }, currentAssets: { lines[], total }, unclassifiedAssets: { lines[], total },
|
||||
totalAssets,
|
||||
equity: { lines[], total },
|
||||
nonCurrentLiabilities: { lines[], total }, currentLiabilities: { lines[], total }, unclassifiedLiabilities: { lines[], total },
|
||||
totalEquityAndLiabilities }
|
||||
```
|
||||
Each `lines[]` entry is `{ accountCode, accountName, balance }`. `equity.lines[]` always includes one synthetic `{ accountCode: "", accountName: "Current Year Earnings", balance }` line (Retrofit 2026-07-20's fix, folded into the `equity` section by this retrofit rather than a separate top-level field).
|
||||
|
||||
LKAS 1.54 lists the minimum line items a Statement of Financial Position must include (PP&E, inventories, trade receivables, cash, trade payables, provisions, issued capital and reserves, and others), and LKAS 1.60 requires a **classified presentation** — current assets/liabilities separated from non-current — unless a liquidity-based presentation is more relevant (e.g. for a financial institution).
|
||||
|
||||
**Previously (superseded 2026-07-31):** the schema had no `is_current`/`is_non_current` flag on `accounts` at all — classification was achieved entirely through how the company structured its own Chart of Accounts, and the flat response's `depth`/`lineItem` fields just rendered that hierarchy as-is. A user-supplied reference template (a classified Statement of Financial Position, current/non-current subtotaled independent of COA depth) needed real classification instead, so a new tagged column, `accounts.balance_sheet_classification` (`NON_CURRENT_ASSET`/`CURRENT_ASSET`/`NON_CURRENT_LIABILITY`/`CURRENT_LIABILITY`, `PUT /accounts/{accountCode}`-only, same recursive-tag-and-inherit pattern as `cash_flow_category`/`income_statement_section`), now drives the section split explicitly. Equity accounts need no tag — `account_type_id` alone already puts them in one `equity` section. Untagged Asset/Liability leaf rows land in `unclassifiedAssets`/`unclassifiedLiabilities` rather than being silently dropped, same rule as `ProfitAndLoss`'s `unclassified` bucket. Full design: `Accounting_System_Design_LKAS.md` §11.3.
|
||||
|
||||
```
|
||||
[Company Name]
|
||||
Statement of Financial Position
|
||||
As at [asOfDate]
|
||||
(Amounts in LKR)
|
||||
|
||||
ASSETS
|
||||
Non-Current Assets
|
||||
Property, Plant & Equipment 650,000.00
|
||||
─────────────────────────────────────────────────────────────────
|
||||
Total Non-Current Assets 650,000.00
|
||||
Current Assets
|
||||
Cash and Cash Equivalents 125,000.00
|
||||
Trade Receivables 84,500.00
|
||||
─────────────────────────────────────────────────────────────────
|
||||
Total Current Assets 209,500.00
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
TOTAL ASSETS 859,500.00
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
EQUITY AND LIABILITIES
|
||||
Equity
|
||||
Share Capital 500,000.00
|
||||
Current Year Earnings 252,700.00
|
||||
─────────────────────────────────────────────────────────────────
|
||||
Total Equity 752,700.00
|
||||
(Non-Current Liabilities omitted entirely here - see implementation notes below)
|
||||
Current Liabilities
|
||||
Trade Payables 42,300.00
|
||||
─────────────────────────────────────────────────────────────────
|
||||
Total Current Liabilities 42,300.00
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
TOTAL EQUITY AND LIABILITIES 859,500.00
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- The response is already pre-split into sections (`nonCurrentAssets`, `currentAssets`, `unclassifiedAssets`, `equity`, `nonCurrentLiabilities`, `currentLiabilities`, `unclassifiedLiabilities`) — the renderer just lists each section's `lines[]` and prints its server-computed `total`, no grouping logic needed (retrofit 2026-07-31, unlike the pre-retrofit flat array which required grouping by `accountType` in the renderer).
|
||||
- **A section with an empty `lines[]` is skipped entirely** — same "no `Total X 0.00` clutter" rule as `ProfitAndLoss`'s empty sections. `unclassifiedAssets`/`unclassifiedLiabilities` are the ones worth surfacing prominently when non-empty (an incomplete tagging pass), same as `ProfitAndLoss`'s `unclassified`.
|
||||
- `totalAssets` and `totalEquityAndLiabilities` **must render as the identical figure** — this is the whole point of the 2026-07-20 sign-convention and current-year-earnings fixes (`04_API_Reference_And_Scenarios.md`'s bugfix notes), still true under the classified shape (verified live 2026-07-31: `5,363,714 = 5,363,714`). If they don't match in a rendered PDF, treat that as a rendering-layer bug first (e.g. a sign flipped again in formatting), not a data bug — the underlying figures are already verified balanced.
|
||||
- "Current Year Earnings" always appears in `equity.lines[]` (even at `0.00`) per the 2026-07-20 retrofit note — don't conditionally hide a zero-value row here; a `0.00` Current Year Earnings row is meaningful (it confirms no unclosed prior-period profit is silently missing), not clutter.
|
||||
- Negative balances (a rare but valid case — e.g. an overdrawn contra account) render in parentheses per standard accounting convention: `(12,500.00)`, not `-12,500.00`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Profit & Loss (Statement of Profit or Loss) — LKAS 1
|
||||
|
||||
**Response shape (retrofit 2026-07-22 — nested, implemented and verified live, supersedes the original flat `{depth, lineItem, accountType, amount}` array):**
|
||||
```
|
||||
{ periodStart, periodEnd,
|
||||
sales: { lines[], total }, costOfSales: { lines[], total }, grossProfit,
|
||||
otherIncome: { lines[], total },
|
||||
distributionExpenses: { lines[], total }, administrationExpenses: { lines[], total },
|
||||
otherExpenses: { lines[], total }, financialExpenses: { lines[], total },
|
||||
unclassified: { lines[], total },
|
||||
netProfitForPeriod }
|
||||
```
|
||||
Each `lines[]` entry is `{ accountCode, accountName, amount }`. Driven by the new `accounts.income_statement_section` tag (`Accounting_System_Design_LKAS.md` §11.4, same recursive-tag pattern as `cash_flow_category`) — a user-supplied reference template needed Sales separated from Other Income, and Expenses split into Cost of Sales (subtracted for Gross Profit) plus Distribution/Administration/Other/Financial groups (subtracted for Net Profit), none of which the old flat Income/Expense split could express. Untagged Income/Expense rows land in `unclassified` rather than being silently dropped — a company that hasn't tagged its COA yet gets every row bucketed there, which is meant to be visible (an incomplete tagging pass), not a quiet fallback.
|
||||
|
||||
**Sign convention, the one subtlety in this retrofit:** the underlying query's raw `net_movement` (`credit − debit`) is positive-normal for Income but negative-normal for Expense (a normal debit-balance expense). `ReportService.BuildProfitAndLossReportAsync` flips every Expense-tagged section's lines/total to positive before computing `grossProfit`/`netProfitForPeriod`, so every section total in the response and every rendered figure is already a positive "subtract this" number — the renderer never needs its own sign logic, unlike the pre-retrofit template below required (`PROFIT FOR THE PERIOD = Total Revenue − Total Expenses`, computed in the renderer). `unclassified` is the one section that keeps its raw, already-net-correct sign (mixed Income-positive/Expense-negative rows), since it feeds directly into `netProfitForPeriod` as a straight sum.
|
||||
|
||||
LKAS 1.82 sets minimum line items (revenue, finance costs, tax expense, profit or loss, and others) and permits presentation **by nature of expense** (rent, salaries, depreciation, listed as their own lines) or **by function** (cost of sales, distribution costs, administrative expenses — grouping expenses by what they were incurred *for*). This retrofit adopts the **by function** presentation, per the reference template — which method this system produces still depends entirely on how the company tags its `income_statement_section` values, not on anything the schema enforces.
|
||||
|
||||
**Also worth stating plainly:** LKAS 1 technically titles this a "Statement of Profit or Loss **and Other Comprehensive Income**" when OCI items exist (revaluation surpluses, certain FX translation differences, etc.). This system has no OCI concept anywhere in the schema — no revaluation-reserve movements are tracked outside normal P&L. Titling the PDF "Statement of Profit or Loss" (without "and Other Comprehensive Income") is the **correct** choice given current scope, not a shortfall — LKAS 1.81 explicitly permits presenting just a P&L statement when there's no OCI to report, rather than an empty OCI section for its own sake.
|
||||
|
||||
```
|
||||
[Company Name]
|
||||
Statement of Profit or Loss
|
||||
For the period [periodStart] to [periodEnd]
|
||||
(Amounts in LKR)
|
||||
|
||||
Sales
|
||||
Sales Revenue 730,000.00
|
||||
Total Sales 730,000.00
|
||||
|
||||
Cost of Sales
|
||||
Cost of Goods Sold 80,000.00
|
||||
Total Cost of Sales 80,000.00
|
||||
|
||||
GROSS PROFIT 650,000.00
|
||||
|
||||
Other Income
|
||||
Other Income 29,500.00
|
||||
Total Other Income 29,500.00
|
||||
|
||||
Distribution Expenses
|
||||
Marketing Expense 45,000.00
|
||||
Total Distribution Expenses 45,000.00
|
||||
|
||||
Administration Expenses
|
||||
Salaries and Wages 1,370,000.00
|
||||
Rent Expense 170,000.00
|
||||
Total Administration Expenses 1,540,000.00
|
||||
|
||||
NET PROFIT FOR THE PERIOD (1,875,000.00)
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Empty sections (no `lines[]`) are skipped entirely — no "Total X 0.00" clutter for a section the company hasn't tagged any account into. `Unclassified` is the exception: rendered only when non-empty, same rule, but worth surfacing prominently when it does appear (see above).
|
||||
- `GROSS PROFIT = sales.total − costOfSales.total` and `NET PROFIT FOR THE PERIOD` are both already computed server-side — the renderer just displays them, no rendering-layer arithmetic needed (unlike the pre-retrofit version of this template).
|
||||
- A loss period renders `NET PROFIT FOR THE PERIOD` in parentheses, same negative-number convention as the Balance Sheet — verified live on a loss-making seeded dataset.
|
||||
- This figure should reconcile with the Balance Sheet's "Current Year Earnings" row **only when run for the same period** — they're computed by different underlying calls (`BuildProfitAndLossReportAsync(periodStart, periodEnd)` here vs. `BuildProfitAndLossReportAsync(DateOnly.MinValue, asOfDate)` for Current Year Earnings), so don't expect them to match unless `periodStart` is the beginning of time and `periodEnd` equals the Balance Sheet's `asOfDate`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Cash Flow Statement — LKAS 7 (full statement, implemented and verified live)
|
||||
|
||||
**Response shape (retrofit 2026-07-22 — `direction` added per working-capital line, supersedes the 2026-07-21 nested shape):**
|
||||
```
|
||||
{ periodStart, periodEnd,
|
||||
operatingActivities: { profitForPeriod, nonCashAdjustments[], workingCapitalChanges[ { accountCode, accountName, direction, changeAmount } ], netCashFromOperatingActivities },
|
||||
investingActivities: { lines[], netCashFromInvestingActivities },
|
||||
financingActivities: { lines[], netCashFromFinancingActivities },
|
||||
netIncreaseDecreaseInCash, openingCashBalance, closingCashBalance }
|
||||
```
|
||||
|
||||
`openingCashBalance`/`closingCashBalance` stay in the `Json`/`Csv` response for the reconciliation check (see below) but are **not printed** in the `Pdf`/`Csv` display layout — a user-supplied reference template ends at the combined Operating+Investing+Financing total, with no opening/closing balance section at all. `direction` (`"Increase"`/`"Decrease"`) is the account balance's own movement, independent of `changeAmount`'s sign (which reflects cash impact — an operating asset's increase and an operating liability's increase are both `direction: "Increase"` despite having opposite `changeAmount` signs). Full design: `Accounting_System_Design_LKAS.md` §11.5.
|
||||
|
||||
```
|
||||
[Company Name]
|
||||
Statement of Cash Flows
|
||||
For the period [periodStart] to [periodEnd]
|
||||
(Amounts in LKR)
|
||||
|
||||
Cash Flows from Operating Activities
|
||||
Profit for the period 252,700.00
|
||||
Operating profit before working capital changes 255,000.00
|
||||
Additions to Cash
|
||||
Depreciation 2,300.00
|
||||
Increase in trade payables 5,000.00
|
||||
Subtractions From Cash
|
||||
Increase in trade receivables (10,000.00)
|
||||
─────────────────────────────────────────────────────────────────
|
||||
Net Cash from Operating Activities 250,000.00
|
||||
|
||||
Cash Flows from Investing Activities
|
||||
Purchase of property, plant & equipment (80,000.00)
|
||||
─────────────────────────────────────────────────────────────────
|
||||
Net Cash used in Investing Activities (80,000.00)
|
||||
|
||||
Cash Flows from Financing Activities
|
||||
Proceeds from bank loan 50,000.00
|
||||
─────────────────────────────────────────────────────────────────
|
||||
Net Cash from Financing Activities 50,000.00
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
Net Increase in Cash and Cash Equivalents 220,000.00
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- `operatingActivities.nonCashAdjustments[]` and `operatingActivities.workingCapitalChanges[]` are **combined and re-bucketed by sign at render time** — every entry with a positive `amount`/`changeAmount` lands under "Additions to Cash," every non-positive entry under "Subtractions From Cash" — rather than kept as two separate response arrays for display purposes (retrofit 2026-07-22, matching the reference template's own grouping). A working-capital line's label is `{direction} in {accountName}` (e.g. *"Increase in Trade Receivables"*); a non-cash-adjustment line's label is just its `description` (e.g. *"Depreciation"*).
|
||||
- Investing/Financing sections: one line per entry in `investingActivities.lines[]` / `financingActivities.lines[]` — no hardcoded line count, these arrays can hold any number of tagged accounts.
|
||||
- **The closing reconciliation is a real correctness check, not decoration — it just isn't printed.** `closingCashBalance` must equal `openingCashBalance + netIncreaseDecreaseInCash`, computed independently by the underlying query (`Accounting_System_Design_LKAS.md` §11.5) — same cross-check discipline as the Balance Sheet's `Assets = Liabilities + Equity` verification, still run and still returned in `Json`/`Csv`, just not rendered on the `Pdf`/`Csv` table per the reference template.
|
||||
- **COA-design caveat, confirmed during live verification:** `cash_flow_category` must be tagged on individual leaf accounts with a genuine cash-relevant movement, never on a broad header — tagging a header cascades to every descendant, including contra-accounts that have no real cash movement of their own, and there's no way to "un-tag" a descendant once inherited. See `03_Progress_Tracker.md`'s 2026-07-21 entry for the full diagnosis.
|
||||
|
||||
---
|
||||
|
||||
## 6. General Ledger
|
||||
|
||||
**Response shape:** array of `{ entryDate, journalNo, accountCode, accountName, narration, debitAmount, creditAmount, runningBalance }`, sorted by `accountCode` then `entryDate` — either one account (+descendants) or every postable account, depending on whether `accountId` was supplied (Retrofit 2026-07-20).
|
||||
|
||||
**Corrected 2026-07-21 — single continuous table, not separate boxed sections per account.** The prior draft of this template grouped the report into separate mini-tables per account, each with its own repeated column-header row. That's not the convention actually used in Sri Lanka — this isn't an LKAS-prescribed format (LKAS doesn't govern internal bookkeeping ledgers), but the standard regional/practical convention for a running-balance ledger account is **one continuous table**, columns: `Date | Particulars | Description/Reference (J.R.) | Debit | Credit | Balance`. `J.R.` = Journal Reference (the same concept as the "J.F. — Journal Folio" column in classic British-style bookkeeping, which Sri Lankan practice inherited).
|
||||
|
||||
**Reconciling "one table" with a running balance that only means something per account:** a `Balance` column is only meaningful *within* one account's own transaction stream — mixing debits/credits from different accounts into one undifferentiated running total would produce a meaningless number. So this template is one continuous table (one header row, no repeated per-account headers, no separate boxed sections) with account boundaries marked by an inline **group-header row** — bold/shaded, spanning the full width — rather than by starting a new table. The balance still resets at each group-header row; only the visual structure changes, not the underlying grouping logic.
|
||||
|
||||
**Column mapping to the existing response fields:** `Date` ← `entryDate`, `Particulars` ← `narration`, `Description/Reference (J.R.)` ← `journalNo`, `Debit` ← `debitAmount`, `Credit` ← `creditAmount`, `Balance` ← `runningBalance`. No new fields needed — `GeneralLedger`'s `outputFormat=Json` shape is unchanged by this fix; only the PDF layout changes.
|
||||
|
||||
```
|
||||
[Company Name]
|
||||
General Ledger
|
||||
For the period [periodStart] to [periodEnd]
|
||||
(Amounts in LKR)
|
||||
|
||||
Date Particulars Description/Reference (J.R.) Debit Credit Balance
|
||||
──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Account: 1101 — Cash and Cash Equivalents
|
||||
2026-07-01 Opening balance JV-2026-000012 100,000.00 100,000.00
|
||||
2026-07-05 Rent paid JV-2026-000018 25,000.00 75,000.00
|
||||
2026-07-15 Sales receipt JV-2026-000031 50,000.00 125,000.00
|
||||
|
||||
Account: 1102 — Trade Receivables
|
||||
2026-07-10 Credit sale JV-2026-000025 84,500.00 84,500.00
|
||||
──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- One `GroupBy(accountCode)` over the response (already sorted correctly for this — no re-sorting needed), rendered as consecutive sections **within the same table**, not as separate `QuestPDF` table components — this is the actual layout change from the prior draft.
|
||||
- The group-header row (`Account: [accountCode] — [accountName]`) is its own full-width row inside the one table, not a section title outside it — keeps everything under the single running column-header row at the top of the page.
|
||||
- No explicit "Closing Balance" row per account in this format (unlike the prior per-account-block draft) — the last row in each account's group *is* its closing balance, in the `Balance` column, consistent with how a real running-balance ledger page ends (the last entry's balance figure, not a separately labeled total line). Add one back only if a per-account subtotal is specifically wanted, but it's not part of the standard format being matched here.
|
||||
- When `accountId` was supplied (single-account mode), there's only one group-header row — the template is identical either way, it just naturally renders one group instead of many.
|
||||
- Consider repeating the `Date | Particulars | ... | Balance` column-header row at the top of each new PDF page (standard practice for a multi-page table), not per account group — the header row belongs to the table as a whole, not to each account section.
|
||||
- **`Particulars` alternative worth knowing about, not applied here:** classic double-entry ledger convention has `Particulars` show the *contra account* name for each entry (e.g. `"To Sales A/c"` for a debit, `"By Rent A/c"` for a credit), not the transaction narration. This system's `narration` field is a single free-text value per journal entry, and a journal entry can have more than two lines, so there's no single unambiguous contra account to derive automatically without extra logic (the traditional fallback for a multi-line entry is `"To Sundries"`/`"By Sundries"`). Using `narration` directly (as this template does) requires no additional computation; the "To/By contra account" convention is a possible future enhancement, not assumed here.
|
||||
|
||||
---
|
||||
|
||||
## 7. Budget vs Actual
|
||||
|
||||
**Response shape:** array of `{ budgetLineId, accountCode, accountName, periodId, budgetedAmount, actualAmount, variance }`.
|
||||
|
||||
Not an LKAS statement — a management report. Standard format:
|
||||
|
||||
```
|
||||
[Company Name]
|
||||
Budget vs Actual Report
|
||||
Budget: [budget name/id]
|
||||
(Amounts in LKR)
|
||||
|
||||
Account Code Account Name Budgeted Actual Variance
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
5101 Rent Expense 24,000.00 25,000.00 (1,000.00)
|
||||
5102 Utilities 10,000.00 8,500.00 1,500.00
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
Total 34,000.00 33,500.00 500.00
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- `variance = budgetedAmount − actualAmount` (per the response field, matching how it's computed server-side) — an **unfavorable** variance (spent more than budgeted) is negative and renders in parentheses, same convention as every other negative figure in this document; a favorable variance (under budget) is positive.
|
||||
- Optionally add a "Variance %" column (`variance / budgetedAmount`) if useful — **not** in the current response, so this would need a small Service-layer addition first if wanted; flagged here as an option, not assumed.
|
||||
- `periodId` in the response isn't shown in the illustration above — if a single `budgetId` can span multiple `periodId`s, group by `periodId` the same way the General Ledger groups by `accountCode`, with a period sub-header; if a budget is always single-period in practice, a flat table like the one above is sufficient. Worth confirming which is actually true before implementing.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tax Summary Report — redesigned into Income Tax Computation (retrofit 2026-07-22, implemented and verified live)
|
||||
|
||||
**Response shape:** single object, `{ periodStart, periodEnd, profitBeforeTax, nonDeductibleExpenses, allowableDeductions, adjustedBusinessProfit, otherTaxableIncome, assessableIncome, qualifyingPaymentsReliefs, taxableIncome, taxRatePercent, corporateIncomeTax, surchargeAmount, grossTaxLiability, apitCredit, whtCredit, quarterlyTaxPayments, balanceTaxPayable }` — not an array, a single computation.
|
||||
|
||||
**Wholesale content replacement, not an extension.** The originally-built report (2026-07-21, one row per active `tax_code` — output tax, input tax, net payable) doesn't match what "Tax Report" turned out to mean: a user-supplied reference template is a full **Corporate Income Tax Computation**, Profit Before Tax reconciled through to a Balance Tax Payable/(Refund) figure — the standard shape a Sri Lankan corporate tax return computation follows. Still **not** an LKAS 12 disclosure note and still **current tax only** — no deferred tax asset/liability recognition exists anywhere in this schema. The superseded VAT/WHT/NBT design is kept, not deleted, in `Accounting_System_Design_LKAS.md` §11.7† and this document's git history — it's still a real, useful report for periodic indirect-tax return preparation, just not what this endpoint's `TaxSummary` name turned out to mean. Full query design: `Accounting_System_Design_LKAS.md` §11.7.
|
||||
|
||||
**Uses the fuller company identity block** (`ReportPageHeader.ComposeWithEntityDetails`), not the plain header the other six reports use — a corporate tax computation conventionally carries the filer's address and tax reference numbers (`Company:Address`/`Tin`/`Brn`), which none of the other reports need.
|
||||
|
||||
```
|
||||
[Company Name]
|
||||
[Company Address]
|
||||
TIN: [tin] BRN: [brn]
|
||||
Income Tax Computation
|
||||
For the period [periodStart] to [periodEnd]
|
||||
(Amounts in LKR)
|
||||
|
||||
Profit Before Tax 12,500,000.00
|
||||
Add: Non-Deductible Expenses 350,000.00
|
||||
Less: Allowable Deductions (150,000.00)
|
||||
Adjusted Business Profit 12,700,000.00
|
||||
|
||||
Add: Other Taxable Income 300,000.00
|
||||
Assessable Income 13,000,000.00
|
||||
|
||||
Less: Qualifying Payments / Reliefs (150,000.00)
|
||||
Taxable Income 12,850,000.00
|
||||
|
||||
Corporate Income Tax @ 30.00% 3,855,000.00
|
||||
Add: Surcharge / Education Levy 0.00
|
||||
Gross Tax Liability 3,855,000.00
|
||||
|
||||
Less: APIT Credit (200,000.00)
|
||||
Less: WHT Credit (150,000.00)
|
||||
Less: Quarterly Tax Payments (1,000,000.00)
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
BALANCE TAX PAYABLE 2,505,000.00
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Single-column computation, no table/grouping logic — every line is `label` + one right-aligned amount, in the fixed order shown above.
|
||||
- `balanceTaxPayable < 0` renders the final line as **"BALANCE TAX REFUNDABLE"** instead of "BALANCE TAX PAYABLE" — a negative figure is a real, valid refund position (credits/payments exceeded the gross liability), not an error state; verified live on a loss-making seeded dataset (`profitBeforeTax` negative flowed through to a negative `balanceTaxPayable`, correctly labelled "REFUNDABLE").
|
||||
- Four request-time parameters (`allowableDeductions`, `otherTaxableIncome`, `qualifyingPaymentsReliefs`, `surchargeAmount`) plus `taxRateOverride` are genuinely not GL-derived — a tax preparer determines them at filing time — and all default to `0` (or, for `qualifyingPaymentsReliefs`/`taxRateOverride`, to the `STATUTORY_RELIEF_AMOUNT`/`CORPORATE_INCOME_TAX_RATE` `system_config` values) when omitted from the request. Verified live with both the default path and full overrides.
|
||||
- Reuses the same rounding rule as every other report (`04_API_Reference_And_Scenarios.md`'s 2026-07-20 retrofit: PDF rounds to 2dp round-half-up, `Json` stays full precision) — no report-specific rounding logic needed here.
|
||||
|
||||
---
|
||||
|
||||
## 9. Shared implementation notes (all seven templates)
|
||||
|
||||
- **Number formatting:** thousands separator, 2 decimal places, negative numbers in parentheses — consistently, across every template. This matches the `outputFormat=Pdf`/`Csv` rounding rule (`04_API_Reference_And_Scenarios.md`'s 2026-07-20 retrofit note: PDF/CSV round to 2dp round-half-up; `Json` stays full precision) — `PdfFormatting.FormatAmount`/`FormatAmountOrBlank`/`FormatAmountOrDash` implement this once, shared by every `IDocument`.
|
||||
- **Page size and orientation:** A4 for every report, portrait by default, landscape for General Ledger only (Trial Balance moved back to portrait 2026-07-22 once flattened; optionally Budget vs Actual, if the Variance % column is added) — full spec in Section 1.
|
||||
- **Zero/empty data:** every template renders its header/footer and section labels even with zero data rows (e.g. `CashFlow`'s `netProfitForPeriod: 0` case, `ProfitAndLoss` sections with an empty `lines[]` skipped rather than rendered as clutter) — an empty report is a valid report, not an error state, consistent with how the JSON responses already behave.
|
||||
- **QuestPDF structure:** one `IDocument` class per report (seven total, `Services/PdfDocuments/`), sharing one static `ReportPageHeader` for the header/footer block (Section 1 above; `ComposeWithEntityDetails` for the Income Tax Computation's fuller identity block) — avoids duplicating the company-name/period-line/currency/page-size logic seven times while still giving each report its own real layout, consistent with `CLAUDE.md`'s "keep it simple, don't introduce unnecessary abstraction" rule: one shared header component is justified reuse, not premature abstraction, since all seven genuinely share the identical block.
|
||||
- **Implemented and verified live, in the order recommended below** (a real, scoped implementation task, not a drop-in change): Trial Balance first (simplest), then Budget vs Actual, then the Income Tax Computation (no grouping, simple column set), then General Ledger's single-table-with-group-headers layout, then Balance Sheet/P&L's section-splitting, then Cash Flow last (the most new Service/repository logic behind it — `CASH_ACCOUNT_ROOT_ID`, the working-capital-changes query, the depreciation add-back — so it had the most to go wrong and the most value in being tackled once everything else was proven working).
|
||||
|
||||
---
|
||||
|
||||
## 10. CSV export (`outputFormat=Csv`, new 2026-07-22, implemented and verified live)
|
||||
|
||||
Every `reportType` also supports `outputFormat=Csv`, alongside `Json`/`Pdf` — same `ApiResponse`-wrapped base64 envelope as `Pdf`: `data: { fileName, contentType: "text/csv", contentBase64 }`. `Services/CsvDocuments/` mirrors `Services/PdfDocuments/`'s structure: one writer per report (a static `Generate(...)` method, not an `IDocument` — CSV has no page/layout concerns) plus one shared `CsvBuilder` helper (row-building and RFC-4180-style escaping — quotes a value containing a comma/quote/newline, doubling internal quotes; UTF-8 with a BOM so Excel, the realistic consumer of a downloaded `.csv`, doesn't guess at a locale codepage).
|
||||
|
||||
**Flattening rule, applied consistently:** a nested/hierarchical JSON shape becomes one row per line item, with a `Section`/`Category` column carrying whatever the PDF would otherwise show via a header or indentation — nesting is not preserved in the CSV.
|
||||
|
||||
| Report | Columns | Flattening notes |
|
||||
|---|---|---|
|
||||
| Trial Balance | `Account Code, Account Name, Debit, Credit` | Already flat (Section 2) — one row per response row, plus a `TOTAL` row |
|
||||
| Balance Sheet | `Section, Account Code, Account Name, Balance` | One row per `lines[]` entry per section (`Section` = `Non-Current Assets`/`Current Assets`/`Equity`/etc., retrofit 2026-07-31), plus a `TOTAL ASSETS` row (`Section: Asset`) and a `TOTAL EQUITY AND LIABILITIES` row (`Section: Liability/Equity`), both with blank `Account Code`/`Account Name` |
|
||||
| Profit & Loss | `Section, Account Code, Account Name, Amount` | One row per `lines[]` entry per section (`Section` = `Sales`/`Cost of Sales`/`Other Income`/etc.), one `Total {Section}` row per section, one `GROSS PROFIT` row (blank `Account Code`/`Account Name`) and one `NET PROFIT FOR THE PERIOD` row |
|
||||
| Cash Flow | `Category, Description, Amount` | `Category` = `Operating`/`Operating - Additions to Cash`/`Operating - Subtractions From Cash`/`Investing`/`Financing`; one row per line plus the `Net Cash from...` subtotal rows and the final `Net Increase/(Decrease)...` row (blank `Category`). No opening/closing cash balance rows, same omission as the PDF (Section 5) |
|
||||
| General Ledger | `Date, Particulars, Description/Reference (J.R.), Debit, Credit, Balance` | One row per transaction line; an account-boundary row (`Account: [code] — [name]`, other columns blank) stands in for the PDF's group-header row |
|
||||
| Budget vs Actual | `Account Code, Account Name, Period Id, Budgeted, Actual, Variance` | One row per response row, plus a `Total` row |
|
||||
| Income Tax Computation (`TaxSummary`) | `Line, Amount` | One row per computation step, in the same fixed order as the PDF (Section 8) — no grouping, since the response is already a single flat object, not an array |
|
||||
|
||||
**Rounding:** same as `Pdf` — every `decimal` cell rounds to 2dp round-half-up via `CsvBuilder`'s `decimal` formatting branch (`ToString(CultureInfo.InvariantCulture)` on the already-rounded value passed in by each writer) — `Json` alone stays full precision.
|
||||
@@ -0,0 +1,95 @@
|
||||
# General Ledger Service — Enums Reference
|
||||
|
||||
Every `enum` in `Enums/` (one file per enum, `CLAUDE.md` Rule 9), what it's for, and exactly how a caller supplies its value over the API. This file exists because that last part is **not uniform** across this codebase — the same enum can be bound three different ways depending on where it appears in a request — and getting it wrong is a real, previously-hit bug class (see "The JSON-body binding gotcha" below).
|
||||
|
||||
**Maintenance rule (`CLAUDE.md` Rule 15): this file is updated in the same change that adds or modifies any enum.** Adding a new file to `Enums/`, adding/renaming/renumbering a member, or changing which DB column/DTO field an enum backs all count — update the relevant table below before considering that work done, the same way `03_Progress_Tracker.md` is updated before moving to the next phase (Rule 0).
|
||||
|
||||
---
|
||||
|
||||
## The JSON-body binding gotcha
|
||||
|
||||
**This project has no global `JsonStringEnumConverter` registered in `Program.cs`.** That means:
|
||||
|
||||
- A DTO field bound from a **JSON request body** must be declared `string`, not the enum type — ASP.NET Core's default `System.Text.Json` binder cannot deserialize a JSON string (`"Monthly"`) directly into a C# enum member without that converter. The Service then parses it manually: `Enum.TryParse<T>(raw, ignoreCase: true, out var parsed)`, throwing `BadRequestException` with the valid-names list (`string.Join(", ", Enum.GetNames<T>())`) on failure.
|
||||
- A DTO field bound from a **query string** (`?status=Cleared`) works fine as the **real enum type** (optionally nullable, `TStatus?`) — ASP.NET Core's route/query model binder parses enum names natively; this path never needed the converter and was never broken.
|
||||
- An **action discriminator for a `PUT .../status`-style endpoint** (`action: "Lock"`) is typically not a named DTO field at all — the raw JSON body is bound as `Dictionary<string, JsonElement>` (`CLAUDE.md` Rule 1's `DataExtractor`/`DeserializePayload` pattern) and the Service pulls the raw string straight out of the dictionary before the same `Enum.TryParse` call.
|
||||
|
||||
Getting this wrong (declaring a JSON-body field as the enum type directly) was a real bug found and fixed twice already — `IssueChequePageRequest.PayeeType`/`CreateReceivedChequeRequest.ReceivedFromType` (Phase 37, Cheque Management) both had to be changed from the enum type to `string` after binding silently failed for values like `"Supplier"`. **Before adding a new enum-backed JSON-body field, check the "How values reach the API" table below for the pattern already established — don't rediscover this the hard way a third time.**
|
||||
|
||||
---
|
||||
|
||||
## Persisted enums (backed by a real DB column)
|
||||
|
||||
EF Core's plain enum-to-int mapping (no `.HasConversion<...>()`), per `CLAUDE.md` Rule 9 — the column is a plain `integer`, every member has an **explicit** value (never left as implicit 0/1/2 auto-numbering).
|
||||
|
||||
| Enum | Values | Model.Property (DB column) | Module |
|
||||
|---|---|---|---|
|
||||
| `AuditCategory` | `Ledger=1, Administrative=2` | `AuditLog.Category` | Audit Log (`CLAUDE.md` Rule 10) |
|
||||
| `AuditAction` | `Create=1, Update=2, Post=3, Reverse=4, Adjust=5, Lock=6, Unlock=7, SoftClose=8, Close=9` | `AuditLog.Action` | Audit Log |
|
||||
| `PeriodStatus` | `Open=1, SoftClose=2, Locked=3, Closed=4` | `AccountingPeriod.Status` | Fiscal Calendar |
|
||||
| `JournalEntryStatus` | `Draft=1, Posted=2, Reversed=3` | `JournalEntry.EntryStatus` | Journal Entries — note: no client-facing `Draft` is reachable since Rule 14's create+post merge; the member still exists because a journal entry is briefly `Draft` in memory before the same transaction posts it |
|
||||
| `JournalCorrectionType` | `FullReversal=1, PartialAdjustment=2` | `JournalEntry.CorrectionType` | Journal Entries |
|
||||
| `RecurrencePattern` | `Monthly=1, Quarterly=2, Annual=3` | `RecurringJournalTemplate.RecurrencePattern` | Recurring Entries |
|
||||
| `TaxCalculationBasis` | `Exclusive=1, Inclusive=2` | `TaxCode.CalculationBasis` | Tax |
|
||||
| `TaxAppliesTo` | `Sales=1, Purchase=2, Both=3` | `TaxCode.AppliesTo` | Tax |
|
||||
| `DepreciationMethod` | `StraightLine=1, ReducingBalance=2` | `FixedAsset.DepreciationMethod` | Fixed Assets |
|
||||
| `FixedAssetStatus` | `Active=1, Disposed=2, FullyDepreciated=3` | `FixedAsset.Status` | Fixed Assets |
|
||||
| `ChequeBookStatus` | `Active=1, Completed=2, Cancelled=3` | `ChequeBook.Status` | Cheque Management |
|
||||
| `ChequeIssueStatus` | `Unused=1, Issued=2, Cleared=3, Bounced=4, Cancelled=5, Void=6` | `ChequePage.IssueStatus` | Cheque Management |
|
||||
| `ChequePayeeType` | `Supplier=1, Customer=2, Employee=3, Other=4` | `ChequePage.PayeeType` (nullable — only set once a page is issued) | Cheque Management |
|
||||
| `ReceivedChequeFromType` | `Customer=1, Supplier=2, Other=3` | `ReceivedCheque.ReceivedFromType` | Cheque Management |
|
||||
| `ReceivedChequeStatus` | `Received=1, Deposited=2, Cleared=3, Returned=4, Cancelled=5` | `ReceivedCheque.Status` | Cheque Management |
|
||||
|
||||
**Not in this table, and deliberately not enums:** `Account.AccountTypeId`, `TaxCode.TaxTypeId`, `Currency.CurrencyCode` and similar — these are FK references to a real reference table (`account_type`, `tax_type`, `currency`), which is already the source of truth. A parallel C# enum would duplicate it and risk drifting out of sync (`CLAUDE.md` Rule 9's "skip the enum if a real table already is the enumeration").
|
||||
|
||||
---
|
||||
|
||||
## Dispatch-only enums (not a stored column — in-memory discriminators)
|
||||
|
||||
These never reach a DB column. They exist purely to validate/dispatch a request-time choice — a report type, an output format, a `PUT .../status` action name.
|
||||
|
||||
| Enum | Values | Used for | Module |
|
||||
|---|---|---|---|
|
||||
| `ReportType` | `GeneralLedger=1, TrialBalance=2, BalanceSheet=3, ProfitAndLoss=4, CashFlow=5, BudgetVsActual=6, TaxSummary=7` | `GET /reports?reportType=...` dispatch | Reporting |
|
||||
| `ReportOutputFormat` | `Json=1, Pdf=2, Csv=3` | `GET /reports?outputFormat=...` dispatch | Reporting |
|
||||
| `FixedAssetTransactionType` | `Depreciate=1, Dispose=2` | `POST /fixed-assets/{code}/transactions` body dispatch | Fixed Assets |
|
||||
| `PeriodLockAction` | `Lock=1, Unlock=2, SoftClose=3, Close=4` | `PUT /periods/{id}/lock` body `action` — maps to a `PeriodStatus` transition + an `AuditAction` | Fiscal Calendar |
|
||||
| `ChequePageAction` | `Clear=1, Bounce=2, Cancel=3, Void=4` | `PUT /cheque-pages/{chequeNo}/status` body `action` | Cheque Management |
|
||||
| `ReceivedChequeAction` | `Deposit=1, Clear=2, Return=3, Cancel=4` | `PUT /received-cheques/{id}/status` body `action` | Cheque Management |
|
||||
|
||||
---
|
||||
|
||||
## How values reach the API (binding pattern per enum)
|
||||
|
||||
The definitive answer to "should this DTO field be `string` or the enum type" for every enum that appears anywhere in a request.
|
||||
|
||||
| Enum | JSON-body field(s) (declared `string`, parsed via `Enum.TryParse`) | Query-string field(s) (declared as the real enum type, nullable) | Parsed straight from raw dictionary payload (no named DTO field) |
|
||||
|---|---|---|---|
|
||||
| `JournalCorrectionType` | `JournalEntryCorrectionRequest.CorrectionType` | — | — |
|
||||
| `TaxCalculationBasis` | `CreateTaxCodeRequest.CalculationBasis` | — | — |
|
||||
| `TaxAppliesTo` | `CreateTaxCodeRequest.AppliesTo` | — | — |
|
||||
| `RecurrencePattern` | `CreateRecurringTemplateRequest.RecurrencePattern` | — | — |
|
||||
| `DepreciationMethod` | `CreateFixedAssetRequest.DepreciationMethod` | — | — |
|
||||
| `FixedAssetTransactionType` | `CreateFixedAssetTransactionRequest.TransactionType` | — | — |
|
||||
| `ReportType` | `ListReportsRequest.ReportType` | — | — |
|
||||
| `ReportOutputFormat` | `ListReportsRequest.OutputFormat` | — | — |
|
||||
| `ChequePayeeType` | `IssueChequePageRequest.PayeeType` | `ListChequePagesRequest.PayeeType` (`ChequePayeeType?`) | — |
|
||||
| `ReceivedChequeFromType` | `CreateReceivedChequeRequest.ReceivedFromType` | `ListReceivedChequesRequest.ReceivedFromType` (`ReceivedChequeFromType?`) | — |
|
||||
| `ChequeBookStatus` | — | `ListChequeBooksRequest.Status` (`ChequeBookStatus?`) | — |
|
||||
| `ChequeIssueStatus` | — | `ListChequePagesRequest.IssueStatus` (`ChequeIssueStatus?`) | — |
|
||||
| `ReceivedChequeStatus` | — | `ListReceivedChequesRequest.Status` (`ReceivedChequeStatus?`) | — |
|
||||
| `PeriodLockAction` | — | — | `FiscalCalendarService.LockPeriodAsync` — `DataExtractor` pulls `action` out of the raw payload |
|
||||
| `ChequePageAction` | — | — | `ChequeService.UpdateChequePageStatusAsync` — same pattern, `action` field |
|
||||
| `ReceivedChequeAction` | — | — | `ChequeService.UpdateReceivedChequeStatusAsync` — same pattern, `action` field |
|
||||
| `JournalEntryStatus`, `PeriodStatus`, `FixedAssetStatus` | Never appear in a request at all — response-only fields (or, for `PeriodStatus`, driven entirely by `PeriodLockAction`'s mapping, never set directly) | | |
|
||||
|
||||
**A fourth pattern, distinct from all three columns above:** `AuditCategory`/`AuditAction` on `ListAuditLogRequest` are query-string filters, but declared `List<string>?`/`string?` (not the enum type, not even `Enum.TryParse`-with-`400`) — `AuditLogService.ListAsync` parses each and **silently drops** whichever entries don't match a real enum member, returning an empty/narrowed result (`200`) rather than a `400`. This is a deliberate, documented design choice (`04_API_Reference_And_Scenarios.md`'s Audit Log module: "a typo'd filter returns nothing, not a 400") — don't copy this pattern for a new enum without a similar explicit reason; every other query-string enum filter in this codebase uses the strict native-binding pattern (column 2 above) or the strict `Enum.TryParse`-with-`400` pattern (column 1), both of which surface a bad value immediately instead of silently dropping it.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new enum — checklist
|
||||
|
||||
1. One file per enum in `Enums/`, namespace `General_Ledger_Service.Enums` (`CLAUDE.md` Rule 9).
|
||||
2. **Explicit integer value on every member** if it backs a DB column — never implicit auto-numbering, since a silent renumbering would corrupt already-stored rows.
|
||||
3. Decide the binding shape *before* wiring up the DTO: JSON-body field → `string` + `Enum.TryParse` in the Service; query-string field → the real enum type (`TEnum?`); `PUT .../status`-style action → pulled from the raw `Dictionary<string, JsonElement>` payload, no named field. Check the table above for the closest existing precedent rather than guessing.
|
||||
4. Add the enum to the appropriate table(s) above in the same change.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,324 @@
|
||||
erDiagram
|
||||
|
||||
%% ===================== CORE LEDGER =====================
|
||||
account_type {
|
||||
smallint id PK
|
||||
varchar name
|
||||
char normal_balance
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
accounts {
|
||||
bigint account_id PK
|
||||
varchar account_code
|
||||
varchar account_name
|
||||
bigint parent_account_id FK "self-referencing, recursive"
|
||||
smallint account_type_id FK
|
||||
boolean is_control_account
|
||||
boolean is_postable
|
||||
char currency_code
|
||||
varchar cash_flow_category
|
||||
boolean is_active
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
fiscal_year {
|
||||
int fiscal_year_id PK
|
||||
varchar year_name
|
||||
date start_date
|
||||
date end_date
|
||||
boolean is_closed
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
accounting_period {
|
||||
int period_id PK
|
||||
int fiscal_year_id FK
|
||||
smallint period_number
|
||||
date start_date
|
||||
date end_date
|
||||
integer status "PeriodStatus enum: Open=1,SoftClose=2,Locked=3,Closed=4 (converted from varchar 2026-07-18)"
|
||||
boolean lock_gl
|
||||
varchar lock_reason
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
period_lock_log {
|
||||
bigint log_id PK
|
||||
int period_id FK
|
||||
integer action "PeriodLockAction enum (converted from varchar 2026-07-18)"
|
||||
varchar module_locked
|
||||
varchar reason
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
journal_entry {
|
||||
bigint journal_id PK
|
||||
varchar journal_no
|
||||
int period_id FK
|
||||
date entry_date
|
||||
varchar journal_type
|
||||
varchar source_module
|
||||
varchar reference
|
||||
text narration "approved addition, Phase 1 build 2026-07-17 - not in the original ER diagram/design doc"
|
||||
boolean is_posted
|
||||
boolean is_reversed
|
||||
bigint reversal_of_id FK "self-referencing"
|
||||
integer entry_status "JournalEntryStatus enum: Draft=1,Posted=2,Reversed=3 (converted from varchar 2026-07-18)"
|
||||
varchar prepared_by
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
journal_entry_line {
|
||||
bigint line_id PK
|
||||
bigint journal_id FK
|
||||
bigint account_id FK
|
||||
numeric debit_amount
|
||||
numeric credit_amount
|
||||
char currency_code FK
|
||||
numeric exchange_rate
|
||||
numeric base_debit
|
||||
numeric base_credit
|
||||
bigint cost_center_id FK
|
||||
text memo
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
journal_correction {
|
||||
bigint correction_id PK
|
||||
bigint original_journal_id FK
|
||||
bigint correction_journal_id FK
|
||||
integer correction_type "JournalCorrectionType enum: FullReversal=1,PartialAdjustment=2 (converted from varchar 2026-07-18)"
|
||||
text reason
|
||||
varchar corrected_by
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
cost_center {
|
||||
bigint cost_center_id PK
|
||||
varchar code
|
||||
varchar name
|
||||
bigint parent_cost_center_id FK "self-referencing, recursive"
|
||||
boolean is_active
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ===================== FIXED ASSETS =====================
|
||||
fixed_asset {
|
||||
bigint asset_id PK
|
||||
varchar asset_code
|
||||
varchar asset_name
|
||||
bigint asset_account_id FK
|
||||
bigint accum_depr_account_id FK
|
||||
bigint depr_expense_account_id FK
|
||||
bigint gain_loss_account_id FK "added Phase 21/22 build 2026-07-18 - not in the original design doc; needed to post a balanced disposal entry"
|
||||
date acquisition_date
|
||||
numeric acquisition_cost
|
||||
numeric residual_value
|
||||
int useful_life_months
|
||||
integer depreciation_method "DepreciationMethod enum: StraightLine=1,ReducingBalance=2 (converted from varchar 2026-07-18)"
|
||||
numeric depreciation_rate_percent "nullable; added Phase 21/22 build 2026-07-18, required only when depreciation_method=ReducingBalance"
|
||||
integer status "FixedAssetStatus enum: Active=1,Disposed=2,FullyDepreciated=3 (converted from varchar 2026-07-18)"
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
depreciation_schedule {
|
||||
bigint schedule_id PK
|
||||
bigint asset_id FK
|
||||
int period_id FK
|
||||
numeric depreciation_amount
|
||||
numeric accumulated_depreciation
|
||||
numeric net_book_value
|
||||
bigint journal_id FK
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
asset_disposal {
|
||||
bigint disposal_id PK
|
||||
bigint asset_id FK
|
||||
date disposal_date
|
||||
numeric proceeds
|
||||
numeric net_book_value_at_disposal
|
||||
numeric gain_loss
|
||||
bigint journal_id FK
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ===================== BANK & CASH =====================
|
||||
bank_account {
|
||||
bigint bank_account_id PK
|
||||
varchar account_name
|
||||
varchar bank_name
|
||||
varchar account_number
|
||||
bigint gl_account_id FK
|
||||
char currency_code FK
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
bank_statement_line {
|
||||
bigint stmt_line_id PK
|
||||
bigint bank_account_id FK
|
||||
date txn_date
|
||||
varchar description
|
||||
numeric amount
|
||||
boolean is_reconciled
|
||||
bigint matched_journal_id FK
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ===================== TAX CONFIGURATION =====================
|
||||
tax_type {
|
||||
int tax_type_id PK
|
||||
varchar code
|
||||
varchar name
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
tax_code {
|
||||
bigint tax_code_id PK
|
||||
int tax_type_id FK
|
||||
varchar code
|
||||
varchar name
|
||||
numeric rate_percent
|
||||
integer calculation_basis "TaxCalculationBasis enum: Exclusive=1,Inclusive=2 (converted from varchar 2026-07-18)"
|
||||
integer applies_to "TaxAppliesTo enum: Sales=1,Purchase=2,Both=3 (converted from varchar 2026-07-18)"
|
||||
bigint output_tax_account_id FK
|
||||
bigint input_tax_account_id FK
|
||||
date effective_from
|
||||
date effective_to
|
||||
boolean is_active
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ===================== CURRENCY CONFIGURATION =====================
|
||||
currency {
|
||||
char currency_code PK
|
||||
varchar currency_name
|
||||
varchar symbol
|
||||
smallint decimal_places
|
||||
boolean is_base_currency
|
||||
boolean is_active
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
exchange_rate {
|
||||
bigint rate_id PK
|
||||
char currency_code FK
|
||||
date rate_date
|
||||
numeric rate_to_base
|
||||
varchar rate_source
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
currency_rounding_rule {
|
||||
char currency_code PK,FK
|
||||
varchar rounding_method
|
||||
bigint rounding_account_id FK
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ===================== SYSTEM CONFIG =====================
|
||||
system_config {
|
||||
varchar config_key PK
|
||||
varchar config_value
|
||||
text description
|
||||
boolean is_editable
|
||||
boolean is_locked
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ===================== BUDGETING =====================
|
||||
budget {
|
||||
bigint budget_id PK
|
||||
int fiscal_year_id FK
|
||||
varchar name
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
budget_line {
|
||||
bigint budget_line_id PK
|
||||
bigint budget_id FK
|
||||
bigint account_id FK
|
||||
int period_id FK
|
||||
bigint cost_center_id FK
|
||||
numeric budgeted_amount
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ===================== MANUAL ENTRY / RECURRING =====================
|
||||
recurring_journal_template {
|
||||
bigint template_id PK
|
||||
varchar template_name
|
||||
integer recurrence_pattern "RecurrencePattern enum: Monthly=1,Quarterly=2,Annual=3 (converted from varchar 2026-07-18)"
|
||||
date next_run_date
|
||||
date end_date
|
||||
boolean is_active
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
recurring_journal_template_line {
|
||||
bigint line_id PK
|
||||
bigint template_id FK
|
||||
bigint account_id FK
|
||||
numeric debit_amount
|
||||
numeric credit_amount
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ===================== AUDIT & CHANGE TRACKING =====================
|
||||
audit_log {
|
||||
bigint audit_log_id PK
|
||||
integer category "AuditCategory enum: Ledger=1,Administrative=2 - decided at first use 2026-07-18, never a varchar in this table"
|
||||
varchar entity_name
|
||||
varchar entity_id "loose reference, no FK - spans every table by design"
|
||||
integer action "AuditAction enum: Create=1,Update=2,Post=3,Reverse=4,Adjust=5,Lock=6,Unlock=7,SoftClose=8,Close=9"
|
||||
text description
|
||||
varchar performed_by
|
||||
timestamptz occurred_at
|
||||
}
|
||||
|
||||
%% ===================== RELATIONSHIPS =====================
|
||||
account_type ||--o{ accounts : "classifies"
|
||||
accounts ||--o{ accounts : "parent_account_id (recursive)"
|
||||
accounts ||--o{ journal_entry_line : "posted to"
|
||||
accounts ||--o{ fixed_asset : "asset/accum-depr/depr-expense/gain-loss accounts"
|
||||
accounts ||--o{ bank_account : "gl_account_id"
|
||||
accounts ||--o{ tax_code : "output/input tax account"
|
||||
accounts ||--o{ budget_line : "account_id"
|
||||
accounts ||--o{ recurring_journal_template_line : "account_id"
|
||||
accounts ||--o{ currency_rounding_rule : "rounding_account_id"
|
||||
|
||||
fiscal_year ||--o{ accounting_period : "contains"
|
||||
fiscal_year ||--o{ budget : "budgeted for"
|
||||
accounting_period ||--o{ journal_entry : "posted within"
|
||||
accounting_period ||--o{ period_lock_log : "lock history"
|
||||
accounting_period ||--o{ depreciation_schedule : "period_id"
|
||||
accounting_period ||--o{ budget_line : "period_id"
|
||||
|
||||
journal_entry ||--o{ journal_entry_line : "has lines"
|
||||
journal_entry ||--o{ journal_entry : "reversal_of_id (self-referencing)"
|
||||
journal_entry ||--o{ journal_correction : "original / correction"
|
||||
journal_entry ||--o| depreciation_schedule : "journal_id"
|
||||
journal_entry ||--o| asset_disposal : "journal_id"
|
||||
journal_entry ||--o| bank_statement_line : "matched_journal_id"
|
||||
|
||||
cost_center ||--o{ cost_center : "parent_cost_center_id (recursive)"
|
||||
cost_center ||--o{ journal_entry_line : "tags"
|
||||
cost_center ||--o{ budget_line : "cost_center_id"
|
||||
|
||||
fixed_asset ||--o{ depreciation_schedule : "scheduled"
|
||||
fixed_asset ||--o{ asset_disposal : "disposed via"
|
||||
|
||||
bank_account ||--o{ bank_statement_line : "statement lines"
|
||||
|
||||
tax_type ||--o{ tax_code : "defines"
|
||||
|
||||
currency ||--o{ exchange_rate : "rated"
|
||||
currency ||--o| currency_rounding_rule : "rounding rule"
|
||||
currency ||--o{ accounts : "currency_code"
|
||||
currency ||--o{ bank_account : "currency_code"
|
||||
currency ||--o{ journal_entry_line : "currency_code"
|
||||
|
||||
budget ||--o{ budget_line : "line items"
|
||||
|
||||
recurring_journal_template ||--o{ recurring_journal_template_line : "line items"
|
||||
Reference in New Issue
Block a user