Journal Entry Manager
Journal Entry Manager
Component File: src/features/accounting/components/JournalEntryManager.tsx Route / navigation: Path /fund-accounting, Zustand accountingTool = journal-entries. See 00-ACCOUNTING-HUB.md. Access Level: Accounting users with JE write permission Last Updated: April 13, 2026 (posting + lock standardization update)
---
Overview
The Journal Entry Manager is the dedicated manual accounting screen for creating, reviewing, editing, and voiding journal entries.
It is one of two active JE UI families:
JournalEntryManager.tsxfor the full accounting page workflowJournalEntryEditDrawer.tsxfor report, donor, reconciliation, and ledger drill-down workflows
Those surfaces are intentionally separate in the UI, but they now share the same data contract:
- create ->
createJournalEntry() - edit ->
updateFullJournalEntry() - void ->
useVoidJournalEntry()->voidJournalEntryWithDependencies()
---
Current Architecture
Data layer
Primary helper file: src/lib/db/journal-entries.ts
Supporting helpers:
src/lib/db/journal-entry-voiding.tssrc/lib/journalEntryVoid.tssrc/lib/voidedEntryDisplay.ts
Query and mutation flow
| Action | Manager UI | Shared helper |
|---|---|---|
| Load paginated entries | `fetchJournalEntriesPaginated()` | `journal-entries.ts` |
| Open details | `fetchJournalEntryByLineId()` | `journal-entries.ts` |
| Create | `createJournalEntry()` | `journal-entries.ts` |
| Edit | `updateFullJournalEntry()` | `journal-entries.ts` |
| Void | `useVoidJournalEntry()` | `useSupabaseData.ts` -> `voidJournalEntryWithDependencies()` |
---
Recent Audit Notes
Date nudge shortcut (Apr 10, 2026)
- The Journal Entry Manager date boxes now inherit the Fund Accounting date shortcut standard from the shared
DatePicker. - Focus a valid date field and press `+` to move forward one day or `-` to move backward one day.
- This applies to create/edit entry dates and JE date filters on the manager screen.
Calculator-style amount entry (Apr 9, 2026)
- The Journal Entry Manager debit and credit fields are part of the live fund-accounting calculator-input rollout.
- Users can type arithmetic such as
152+168directly into a Debit or Credit field; the expression is evaluated and committed on blur. - The debit/credit mutual-clear rule still applies: entering a positive debit clears the credit and vice versa.
- Invalid expressions preserve the last valid amount instead of silently converting to
0.
April 7, 2026
- Nested JE drawer header actions (⋮):
JournalEntryEditDrawernow computes actions first and only renders the ⋮ trigger when at least one header action is available. This prevents dead controls in read-only / voided / edit-mode contexts. - Stacking fix for nested drawer menus: The JE drawer's dropdown content is given a local high z-index override so the menu renders above
StackedDrawerContent(which useszIndex: 100000) instead of opening behind the panel. - Scope: This is a shared-component behavior update and applies to all report/ledger/donor/reconciliation drill-down flows that reuse
JournalEntryEditDrawer.
April 2, 2026
- Line-level edits and React keys: Journal entry UIs key each line row by
journal_entry_lines.id. If the list payload contains duplicate line ids (or empty ids), clearing or editing one line can appear to clear multiple rows. Mitigations in the app: JournalEntryManagerandJournalEntryEditDrawerapply debit/credit/account updates and line removal to the first matching id only (defensive).- New client-side lines use ids like
line-<timestamp>-<random>/new-<timestamp>-<random>to avoid rapid double-click collisions onDate.now()-only ids. warnDuplicateJournalLineIds()insrc/lib/journalEntryValidation.tslogs a console warning when an opened entry’s lines contain duplicate ids — indicates a data or RPC mapping issue to investigate (e.g.get_journal_entries_page/fetchJournalEntriesPaginatedshaping injournal-entries.ts).
March 8, 2026
- JE voids from the manager no longer use a manager-only cleanup path for deposit-linked entries.
- All JE UI entry points now route through the same shared void orchestration hook.
- Manager success state is only committed after the void mutation succeeds.
- Backend void logic now rejects:
- blank reasons
- entries already in reconciliation
- entries in locked periods on any affected line/fund
Existing UI split
The manager still owns its own create/edit form instead of reusing the shared drawer UI. That is a UI-standardization gap, not a persistence-contract gap.
---
Journal Entry Description Standard
All journal entry descriptions follow a single standard. Void status is communicated exclusively through boolean flags and the UI badge — never through the description text.
Canonical rules
| Concern | Standard | Example |
|---------|----------|---------|
| **Normal entries** | Plain business description. No system prefixes. Use ` - ` as the separator between logical segments. | `Donation revenue`, `Check deposit - 3 items` |
| **Voided originals** | `is_voided = true` + UI "Voided" badge. Description stays unchanged — do NOT prepend VOID/VOIDED. | Description: `Donation revenue` (badge shows voided state) |
| **Void reversal lines** | `is_void_reversal = true`. Description follows `VOID: <original description> - <reason> (Voided on <date>)`. | `VOID: Donation revenue - Incorrect donor (Voided on 2026-04-09)` |
| **Non-void reversals** | `reverseJournalEntry()` defaults to `Reverse entry - <original description>` (or user-supplied plain text). No colon-style prefix. | `Reverse entry - Grant revenue - Acme Foundation` |
| **Donation notes** | `VOIDED: <reason>` appended to `donations.notes` by `softDeleteDonation()`. This is a narrative note, not a JE description. | `VOIDED: Duplicate entry` |
Separator and casing rules
- Use
-(space-hyphen-space) between logical segments in system-generated JE descriptions. - Do not use em dashes, bracket prefixes, or colon-style prefixes for new system-generated JE descriptions.
- Use sentence case for the first segment (
Check deposit, notCheck Deposit). Proper nouns and names keep their original casing. - Preferred structure:
{action or source} - {entity/contact name} - {qualifier}where trailing qualifiers are optional.
Partial reversals
Some flows reverse only a subset of a larger logical batch. Those are not standard JE void reversals.
- Use
voidJournalEntry()only when reversing the full logical JE batch and settingis_void_reversal. - When reversing only part of a batch (for example, one donation inside a deposit), create a compensating JE with
createJournalEntry(). - Partial reversals must use plain business descriptions such as
Deposit item reversal - <reason>and must not use theVOID:prefix.
Approved description patterns
| Source module | Entry description | Line description(s) |
|---------------|-------------------|----------------------|
| Manual JE | user-supplied | user-supplied or inherited from entry |
| Donation | `Donation received - $N.NN` | `Donation received` / `Donation revenue` |
| Check deposit | `Check deposit - N check(s)` | `Check deposit - summary` / per-check lines |
| Regular deposit | `Regular deposit - N item(s)` | `Regular deposit - summary` / per-item lines |
| Deposit item reversal | `Deposit item reversal - reason` | `Deposit item reversal - donationId` |
| Pledge accrual | `Pledge accrual - Donor - Campaign` | receivable / revenue lines |
| Pledge payment | `Pledge payment - Donor - Campaign` | payment / receivable lines |
| Pledge write-off | `Pledge write-off - Donor - reason` | bad debt / receivable lines |
| Grant awarded | `Grant awarded - Grantor - Name` | receivable / revenue lines |
| Grant payment | `Grant payment - Grantor - Name` | payment / receivable lines |
| Grant write-off | `Grant write-off - Grantor - reason` | bad debt / receivable lines |
| Bill opened | `Bill opened - Vendor - Desc` | `Vendor - desc` / `Payable to Vendor` |
| Bill payment | `Bill payment - Vendor - Desc` | debit line + `method payment - ref` |
| Expense payment | `Expense payment - context` | `Vendor - desc` per expense |
| Event revenue | `Event revenue - Name - Registrant` | same on both lines |
| Admin fee | `Admin fee allocation - Entity (rate)` | per-account lines |
| Stripe payment | `Stripe payment received - Name` | `Donation revenue - Name` |
| Stripe recurring | `Recurring payment - Name` | `Recurring donation revenue - Name` |
| Stripe fee | `Stripe processing fee - chargeId` | same on both lines |
| Quick entry | user-supplied, no prefix | user-supplied or inherited |
| Memorized transaction | template name | template lines or inherited |
| Non-void reversal | `Reverse entry - <original description>` | default from `reverseJournalEntry()` unless user override |
| Void reversal | `VOID: <original> - <reason> (Voided on <date>)` | system-generated by `voidJournalEntry()` only |
What NOT to do
- Never prefix a JE description with
VOIDED:,VOIDED -,VOIDED-TRANCHE:, or similar. Theis_voidedflag and the UI badge are the single source of truth. - Never use bracket prefixes such as
[Cash]or[Check]in persisted JE descriptions. - Never use em dashes or colon-style prefixes for new system-generated JE descriptions.
- Never rely on description-text parsing to determine void status. Use
is_voided/is_void_reversalbooleans. - Never manually set
is_voided = truewithout also settingvoided_at. Use the shared void helpers. - The
VOID:prefix is reserved for system-generated reversal line descriptions only (created byvoidJournalEntry()).
Migration / import hygiene
When writing migrations or imports that void entries, always:
1. Set is_voided = true and voided_at on the original lines. 2. Create proper reversal lines via VOID-JE-* entry numbers with is_void_reversal = true. 3. Do not embed void status in the description text. 4. Call sync_je_sequence_to_max() if hardcoding entry numbers.
---
Title Field
journal_entry_lines.title (added 2026-07-24) is an optional short header label for the journal entry — for example "March Payroll", "Donation — Acme Foundation". It renders above the per-line description in the JE manager list, the JE drawer (view + edit), the General Ledger, the Balance Sheet by Fund drill-down drawer, and the Reconciliation drawer. When title is NULL the description renders alone (no behavior change vs the pre-2026-07-24 UI).
Storage / replication invariant
- Stored on every
journal_entry_linesrow sharing anentry_number. Replicated identically across all lines. - Read pattern:
lines[0].titleis the entry-level value. Adapters insrc/lib/db/journal-entries.tsandsrc/lib/db/ledger-reads.tsalready pull from the first line. - See `documentation/contracts/JOURNAL-ENTRY-STANDARDS.md` §"Title Field" for the persistence contract.
Manual entry behavior
- Both the full-page Journal Entry Manager create/edit form and the shared
JournalEntryEditDrawerexpose a single optional Title input above the description input. - Empty / whitespace input is normalized to
NULLbycreateJournalEntry()/updateFullJournalEntry()(NULLIF(btrim(...), '')semantics). - To clear an existing title, the user clears the input and saves;
updateFullJournalEntry()maps the cleared field to''soupdate_journal_entry_batch_atomicruns the titleUPDATE(passingNULLwould mean "leave untouched"). - Period locks still apply to title-only edits (v1 decision).
Auto-titles for system writers
Every system-generated JE writer populates title so the list/search experience is consistent. Catalog as of 2026-07-24:
| Source module | Auto-title formula |
|---------------|--------------------|
| Quick Entry | `Quick Entry — {payeeOrPayer}` (or `Quick Entry` if no contact) || Donation | `Donation — {donorName}` || Regular deposit | `Regular Deposit — {N} item(s)` || Check deposit | `Check Deposit — {N} check(s)` || Bill opened | `Bill — {vendor} #{billNumber}` || Bill payment | `Bill Payment — {vendor} #{billNumber}` || Pledge accrual | `Pledge — {donorName} ({campaign})` || Pledge payment | `Pledge Payment — {donorName}` || Grant awarded | `Grant — {grantorName} ({grantName})` || Grant payment | `Grant Payment — {grantorName}` || Admin fee allocation | `Admin Fee — {entityName} ({rate}%)` || Expense payment | `Expense — {vendorOrContext}` || Memorized transaction | `{memTx.name}` || Stripe payment | `Stripe — {donorName} ($amount)` || Stripe recurring fee | `Stripe Recurring Fee — {YYYY-MM}` || Reversal (`reverseJournalEntry`) | `Reversal — {originalTitle \|\| originalDescription}` || Void reversal (`voidJournalEntry`) | `Reversal — {originalTitle \|\| originalDescription}` |Writer source-of-truth lives in src/lib/db/*.ts and supabase/functions/*/index.ts. When adding a new writer, populate title from the same fields you use for description, prefixed by the source action.
Search
The JE Manager and General Ledger search inputs are routed through get_journal_entries_page's p_search parameter, which (since migration 20260724000001_add_title_to_journal_entry_rpcs.sql) ORs against jel.title. Users can therefore find entries by title with no UI changes; matches are backed by the partial GIN trigram index journal_entry_lines_title_trgm_idx.
---
Business Rules
Entry numbering
- Standard format for new entries:
JE-YYYY-0000001 - Generated by
get_next_journal_entry_number()RPC (atomic, usespg_advisory_xact_lock) - Reversal entries use
VOID-JE-YYYY-0000001 - Entry list status filtering uses line booleans (
is_voided,is_void_reversal) rather than legacy*-VOIDEDentry-number suffixes. - The RPC includes a collision safety check: if the generated number already exists in
journal_entry_lines, it increments and retries (up to 100 attempts) - Client recovery (March 30, 2026):
createJournalEntry()injournal-entries.tsmirrors the Stripe webhook: if the RPC fails with the repeated-collision exhaustion message (Failed to find unused journal entry number after ... attempts), PostgresP0001, or an empty number, it runssync_je_sequence_to_max(p_org_id, p_year)and retries the RPC once before using the timestamp-style fallback number. - Sequence sync requirement: The
journal_entry_sequencestable tracks the last-used number per org per year. Any raw SQLINSERTintojournal_entry_lineswith a hardcodedentry_numberMUST update this counter. After bulk imports or migrations, callsync_je_sequence_to_max(p_org_id, p_year)to ensure the counter is correct. Failing to sync causes entry_number collisions on subsequent RPC calls (see DEVELOPER-PLAYBOOK §16, §20.2) - Legacy entry numbers remain visible in historical data, including shorter numeric values (
JE-2026-327) and split / repair suffixes (JE-2026-327-A,...-RESTORE-BULK-...). The manager must read and search those values correctly, but new writes should use the 7-digit canonical format. - Historical renumbering is intentionally out of scope for normal JE manager work. Re-numbering legacy entries requires a dedicated migration and explicit approval because JE numbers are coupled to void metadata, sibling grouping, reporting, and repair tooling.
- Browser JE writes are atomic at the database boundary: create/edit/void operations call transactional RPC wrappers so partial writes cannot persist when any step fails.
Validation
- description required
- at least one line required
- debits must equal credits within 0.01
- each line must have an account
- each line must resolve to a valid fund
- create and new-line update payloads must precheck
get_period_lock_datefor each posting fund + line date before RPC write
Parent-org posting normalization
- Parent-org JE writes resolve through
organizations.settings.default_posting_fund_idonly. - The configured posting fund must be a direct child
fund/accounting_fund, active (or null status), and visible. - Writes are blocked when the mapping is missing or invalid; parent-org name/slug heuristics are not allowed.
Edit restrictions
Editing is blocked when the target lines are in a locked accounting period or already reconciled.
Void restrictions
Voiding is blocked when:
- no reason is provided
- the entry is already voided
- the entry is a void reversal
- any target line is in reconciliation
- any target line is in a locked period
- the original entry is unbalanced
---
Deposit-Linked Void Behavior
A JE that belongs to a deposit batch is not voided as a standalone ledger-only action.
The shared hook resolves whether the line belongs to a deposit batch and, if so, routes through voidDepositBatch() so that:
1. the JE is reversed 2. linked donations are voided 3. the deposit batch returns to pending
This keeps JE Manager behavior aligned with General Ledger, reports, donor drill-downs, and any other surface using the shared JE drawer.
---
Read vs Write Ownership
Manager-owned UI responsibilities
- accounting-page filters
- entry list pagination
- full-page create/edit form
- dedicated void dialog
Shared logic responsibilities
- JE numbering
- balancing rules
- period-lock enforcement
- reconciliation enforcement
- void metadata and reversal linkage
- deposit-aware void orchestration
---
Related Files
src/features/accounting/components/JournalEntryManager.tsxsrc/components/shared/JournalEntryEditDrawer.tsxsrc/hooks/useSupabaseData.tssrc/lib/db/journal-entries.tssrc/lib/db/journal-entry-voiding.tsdocumentation/pages/accounting/02-ACCOUNTING-SYSTEM-INTEGRATION.md
Synced from IFMmvp-Frontend documentation: pages/accounting/05-JOURNAL-ENTRY-MANAGER.md
Ready to Get Started?
See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.
Questions? Email us at steven@getalignmint.org