Files
Distribution_GL/Docs/06_Enums_Reference.md
T

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 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.