Journal Entries

Journal entries are the atomic unit of the general ledger. Every financial event in CEF Core -- whether posted manually or generated automatically by a subledger -- is recorded as a balanced journal entry with one or more debit and credit lines.

Types of Journal Entries

CEF Core distinguishes between two categories of journal entries based on their source.

Manual Entries

Created by authorized users through the UI or API for adjustments, corrections, accruals, and reclassifications that are not generated by a subledger.

  • Source: MANUAL
  • Require ADMIN, TREASURY, or STAFF role
  • Entries over $10,000 require maker-checker approval

Automatic Entries

Generated by subledger transactions. When you record a loan payment, issue a note, or process a cash receipt, the subledger posts a balanced journal entry to the GL automatically.

  • Source: LOANS, INVESTOR_NOTES, CASH, FEES
  • Created within the subledger transaction
  • Cannot be edited directly -- reverse and re-enter

Creating a Manual Journal Entry

Follow these steps to create a manual journal entry through the UI.

  1. Navigate to GL → Journal Entries: Open the sidebar, expand the Accounting section, and click Journal Entries. You will see the list of recent entries with date range filters.
  2. Click "+ New Entry": Opens the journal entry form.
  3. Enter the description: A clear, concise description of the purpose of the entry (e.g., "Monthly loan loss provision adjustment").
  4. Set the value date: The date the entry takes effect, in YYYY-MM-DD format. Must fall within an open accounting period. Backdated entries to closed periods will be rejected.
  5. Add journal lines: Each line requires:
    • gl_code -- The GL account number (e.g., 1200, 6000)
    • debit -- Amount to debit (or 0)
    • credit -- Amount to credit (or 0)

    Each line must have either a debit or a credit amount, not both. Add as many lines as needed.

  6. Verify the balance: The form displays a running total of debits and credits. Both totals must match exactly before the Save button becomes active.
  7. Save the entry: Click Save. The system calls POST /api/v1/gl/journal-entries with an idempotency key to prevent duplicate postings.

Important: Journal entries over $10,000 automatically trigger the maker-checker approval workflow. The entry will not post immediately. Instead, it is queued for approval and a second authorized user must approve it in Admin → Approvals before it posts to the GL.

Balance Enforcement

CEF Core enforces strict double-entry balance rules at multiple levels. Every journal entry must balance to the penny before it can be posted.

Frontend Validation

The journal entry form calculates the debit and credit totals in real time. The Save button is disabled until sum(debit) = sum(credit).

API Validation

The backend Zod schema validates that the lines array contains at least two entries and that debits equal credits. Unbalanced entries return HTTP 400 with a clear error message.

Database Trigger

As a final safeguard, a PostgreSQL trigger on the gl.journal_entry_line table rejects any insert batch where debits do not equal credits. This prevents imbalanced entries even if the API validation is bypassed.

Example: To record a $5,000 loan loss provision, create a two-line entry: Debit 6100 (Provision Expense) $5,000 and Credit 1400 (Allowance for Loan Losses) $5,000. Total debits = total credits = $5,000.

Automatic Entries from Subledgers

When you perform a transaction in a subledger (Loans, Investor Notes, Cash, or Fees), the subledger service automatically creates a journal entry in the GL. You do not need to create these entries manually.

Loans Subledger

  • Disbursement: DR 1200 (Loans Receivable) / CR 1010 (Operating Checking)
  • Payment - principal: DR 1010 (Operating Checking) / CR 1200 (Loans Receivable)
  • Payment - interest: DR 1010 (Operating Checking) / CR 1300 (Accrued Interest Receivable)
  • Interest accrual: DR 1300 (Accrued Interest Receivable) / CR 4000 (Interest Income)

Investor Notes Subledger

  • Note issued: DR 1010 (Operating Checking) / CR 2000 (Notes Payable)
  • Note redeemed: DR 2000 (Notes Payable) / CR 1010 (Operating Checking)
  • Interest accrual: DR 6000 (Interest Expense) / CR 2100 (Accrued Interest Payable)
  • Interest payment: DR 2100 (Accrued Interest Payable) / CR 1010 (Operating Checking)

Cash Subledger

  • Receipt: DR 1010 (Operating Checking) / CR varies by source
  • Disbursement: DR varies by purpose / CR 1010 (Operating Checking)

Fees Subledger

  • Fee charged: DR receivable / CR fee income account
  • Fee collected: DR 1010 (Operating Checking) / CR receivable

Viewing and Filtering Entries

The journal entries list provides filters to locate specific entries. Navigate to Accounting → Journal Entries to access the list.

Available Filters

  • Date Range: Filter by startDate and endDate to view entries within a specific period. Defaults to the current month.
  • Source: Filter by entry source (MANUAL, LOANS, INVESTOR_NOTES, CASH, FEES) to see only entries from a specific subledger.
  • GL Code: Search for entries that include a specific GL account to see all activity on that account.
  • Pagination: Results are paginated using limit and offset parameters. Default limit is 50 entries per page.

Click on any entry row to expand it and see the full list of journal lines with GL codes, descriptions, and debit/credit amounts.

Approval Workflow for High-Value Entries

CEF Core enforces the four-eyes principle for journal entries exceeding $10,000. This applies to the total debit amount of the entry.

  1. Maker creates the entry: The API returns HTTP 202 Accepted with an approval_id. The entry is not yet posted to the GL.
  2. Notification sent: Authorized approvers are notified via the approval notification job (runs every 30 minutes).
  3. Checker reviews: Navigate to Admin → Approvals, review the entry description, lines, and amounts, then Approve or Reject.
  4. Entry posts: Once approved, the scheduled job (every 15 minutes) executes the approved request and the journal entry appears in the GL.

Note: The same user who created the entry cannot approve it. The checker must be a different user with ADMIN or TREASURY role.

API Reference

Journal entries are managed through the GL API. All requests require JWT authentication and an idempotency key header for POST operations.

MethodEndpointDescription
GET/api/v1/gl/journal-entriesList entries (query: ?startDate, endDate, limit, offset)
POST/api/v1/gl/journal-entriesCreate a new journal entry (idempotency key required)
GET/api/v1/gl/journal-entries/:idGet a single entry with all lines

POST Request Body

{
  "description": "Monthly loan loss provision",
  "value_date": "2026-03-31",
  "lines": [
    { "gl_code": "6100", "debit": 5000.00, "credit": 0 },
    { "gl_code": "1400", "debit": 0, "credit": 5000.00 }
  ]
}

Pro Tip: Always include the x-idempotency-key header with a unique UUID for every POST request. If a network error causes a retry, the idempotency key prevents the entry from being posted twice.

Best Practices

  • Use Descriptive Descriptions: Include enough detail to understand the entry without seeing the lines. Good: "Q1 2026 loan loss provision increase". Bad: "Adjustment".
  • Post to the Correct Period: Set the value_date to the accounting period the entry belongs to. Do not backdate into closed periods.
  • Never Edit Posted Entries: Journal entries are immutable once posted. To correct an error, create a reversing entry with opposite debits and credits, then post the correct entry.
  • Review Before Month-End Close: Run the trial balance report and review all manual entries for the period before closing. Once the period is closed, no new entries can be posted to it.
  • Let Subledgers Do the Work: Avoid manually posting entries for loan payments, note issuances, or cash receipts. Use the subledger interface and let the system generate the GL entries automatically.

Next Steps

After recording journal entries, use the financial reports to verify account balances and generate statements for stakeholders.

Financial Reports Guide