96 lines
10 KiB
Markdown
96 lines
10 KiB
Markdown
# 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.
|