10 KiB
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 defaultSystem.Text.Jsonbinder 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), throwingBadRequestExceptionwith 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 asDictionary<string, JsonElement>(CLAUDE.mdRule 1'sDataExtractor/DeserializePayloadpattern) and the Service pulls the raw string straight out of the dictionary before the sameEnum.TryParsecall.
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
- One file per enum in
Enums/, namespaceGeneral_Ledger_Service.Enums(CLAUDE.mdRule 9). - 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.
- Decide the binding shape before wiring up the DTO: JSON-body field →
string+Enum.TryParsein the Service; query-string field → the real enum type (TEnum?);PUT .../status-style action → pulled from the rawDictionary<string, JsonElement>payload, no named field. Check the table above for the closest existing precedent rather than guessing. - Add the enum to the appropriate table(s) above in the same change.