Download the PHP package othmanhaba/ledger-core without Composer
On this page you can find all versions of the php package othmanhaba/ledger-core. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download othmanhaba/ledger-core
More information about othmanhaba/ledger-core
Files in othmanhaba/ledger-core
Package ledger-core
Short Description Generic double-entry ledger core package for Laravel applications.
License MIT
Homepage https://github.com/othmanhaba/ledger-core
Informations about the package ledger-core
Ledger Core
ledger-core is a generic, reusable double-entry ledger package for Laravel applications.
It provides the accounting core that many applications need: books, accounts, journal entries, journal lines, balances, currencies, reversals, idempotency, posting validation, reporting, auditability, and optional Filament administration.
The package is intentionally domain-neutral. It does not contain models or services for transfers, customers, providers, branches, remittance, wallets, invoices, orders, subscriptions, payouts, exchanges, or any other host-application workflow. Your application owns those workflows and translates them into ledger journal entries.
Table Of Contents
- What This Package Solves
- Core Principles
- Requirements
- Installation
- Configuration
- Database Model
- Concepts
- Quick Start
- Creating Ledger Entities
- Creating Accounts
- Posting Journal Entries
- Opening Balances
- Idempotency
- Reversals
- Balances
- Reports
- Filament Integration
- Host Application Use Cases
- Extending The Package
- Events
- Exceptions
- Testing
- Operational Guidance
- FAQ
What This Package Solves
Most applications eventually need an audit-friendly financial record. A simple balance column is easy to start with, but it becomes difficult to reason about when you need corrections, historical reporting, idempotency, multiple accounts, multiple currencies, or audit trails.
ledger-core gives you a generic double-entry ledger:
- Every posting has debit and credit lines.
- Every journal entry must balance.
- Every posting is atomic.
- Every posting is idempotent.
- Posted entries are immutable.
- Corrections are made with reversals.
- Balances are cached for fast reads but derived from journal lines.
- Business workflows stay outside the package.
Use this package when your Laravel app needs accounting-style records without hardcoding your business domain into the ledger.
Core Principles
- The ledger core is generic.
- Business-specific logic belongs in the host application.
- Every posted journal entry must be balanced.
- Posted journal entries and lines are immutable.
- Reversal entries correct mistakes.
- Idempotency is mandatory.
- Cached balances are updated only by the posting service.
- Amounts are decimal strings, not PHP floats.
- Posting is transactional.
- Filament support is optional.
Requirements
- PHP 8.3 or newer
- Laravel 11 or 12
- Eloquent
ext-bcmath- A database supported by Laravel migrations
- Optional: Filament, if you want admin resources and reports
Installation
Install the package:
The package service provider is auto-discovered by Laravel through Composer.
Publish the config:
Publish the migrations:
Run migrations:
Configuration
The published config file is config/ledger.php.
Important Config Options
currency.base_currency
: Default base currency for your installation.
currency.require_base_amount_for_multi_currency
: When enabled, multi-currency entries must include base_amount on every line.
currency.scale
: Decimal precision used by the package. The default is 8.
posting.return_existing_on_duplicate_idempotency_key
: If enabled, reposting the same idempotency key with the same payload returns the existing entry.
posting.allow_cross_entity_entries
: If disabled, all accounts in one entry must belong to the same ledger entity.
posting.prevent_negative_balances_by_default
: If enabled, accounts cannot go negative unless the account has allow_negative = true.
posting.lock_accounts_during_posting
: If enabled, affected accounts and balances are locked during posting.
posting.allow_manual_entries_from_filament
: Controls whether users may create manual journal entries from Filament.
posting.allow_posted_metadata_updates
: Controls whether posted entry metadata can be edited.
Database Model
ledger_entities
Represents an accounting book or entity. This can map to a company, tenant, organization, store, project, department, or any other book owner in the host application. The package does not care what the entity means.
Important fields:
uuidparent_idnamecodetypebase_currencymetadatais_active
ledger_accounts
Represents generic accounts inside a ledger entity.
Supported account types:
assetliabilityequityrevenueexpense
Important fields:
ledger_entity_idparent_idcodenametypenormal_balancecurrencycounterparty_typecounterparty_idis_control_accountis_postableallow_negativemetadatais_active
The package does not provide a chart of accounts. Your application creates whatever accounts it needs.
journal_entries
Represents the header for a posted accounting event.
Important fields:
ledger_entity_ididempotency_keypayload_hashreference_typereference_iddescriptionstatusposted_atreversed_atreversed_by_entry_idmetadata
journal_lines
Represents the debit and credit lines for a journal entry.
Important fields:
journal_entry_idledger_account_iddirectionamountcurrencybase_amountexchange_ratememometadata
account_balances
Stores cached totals and balances for fast reads.
Important fields:
ledger_account_iddebit_totalcredit_totalbalancecurrencylast_journal_entry_id
Do not update this table directly from application code. It is maintained by JournalPostingService.
Concepts
Ledger Entity
A ledger entity is the book that owns accounts and journal entries.
Examples in a host application might be:
- A company
- A tenant
- A project
- A legal entity
- A regional book
- A separate internal accounting book
The package stores the generic entity only. Your app decides what it represents.
Account
An account is a bucket where debit and credit movements are posted.
Examples:
- Cash
- Bank
- Accounts receivable
- Accounts payable
- Clearing
- Revenue
- Expense
- Equity
These are examples only. The package does not create or enforce a chart of accounts.
Normal Balance
The normal balance decides how the cached balance is calculated.
Debit-normal accounts:
Credit-normal accounts:
Typical defaults:
- Assets: debit
- Expenses: debit
- Liabilities: credit
- Equity: credit
- Revenue: credit
Journal Entry
A journal entry is an accounting event. It contains two or more lines. Total debits must equal total credits.
Journal Line
A journal line is one debit or credit movement against one account.
Reversal
A reversal is a new posted journal entry with opposite lines. Reversal is how you correct posted records. Posted entries are not edited or deleted.
Idempotency
Idempotency prevents duplicate posting when a job, webhook, command, or API request is retried.
Every journal entry requires an idempotencyKey.
Quick Start
Creating Ledger Entities
Use LedgerManager::createEntity().
The type field is a free string. It is intentionally not an enum because different applications organize books differently.
Creating Accounts
Use LedgerManager::createAccount() or inject AccountService.
Parent Accounts And Postable Accounts
You may create account hierarchies:
Post only to postable accounts. Control accounts are useful for grouping and reporting.
Counterparty Fields
Accounts include optional generic counterparty fields:
These fields are generic. They let the host application associate accounts with any model without the package knowing what that model means.
Posting Journal Entries
Use LedgerManager::post() with JournalEntryData.
What Happens During Posting
The posting service:
- Starts a database transaction.
- Checks the
idempotency_key. - Computes and stores a payload hash.
- Dispatches
JournalEntryPosting. - Runs custom posting validators.
- Validates that the entry has at least two lines.
- Validates debit and credit totals.
- Resolves and locks affected accounts when configured.
- Ensures accounts are active and postable.
- Ensures account currencies are compatible.
- Creates the journal entry.
- Creates the journal lines.
- Updates cached account balances.
- Dispatches
JournalEntryPosted. - Commits the transaction.
If any step fails, nothing is partially posted.
Single-Currency Entries
For single-currency entries, total debit amount must equal total credit amount.
Split Entries
A single debit can be balanced by multiple credits:
Multiple debits can also be balanced by one credit, or by multiple credits.
Multi-Currency Entries
When entries contain more than one currency and require_base_amount_for_multi_currency is enabled, every line must include baseAmount.
For multi-currency entries, debit baseAmount totals must equal credit baseAmount totals.
Opening Balances
Opening balances are normal journal entries. Do not insert rows directly into account_balances.
Internally, postOpeningBalance() calls post(). The same validation, idempotency, immutability, and balance rules apply.
Idempotency
Every journal entry must have a unique idempotencyKey.
If the same key is used with a different payload, the package throws IdempotencyConflictException.
Choosing Idempotency Keys
Good keys are stable, unique, and based on the business event in the host app.
Examples:
Avoid keys based only on timestamps or random values when retry safety matters.
Reversals
Posted entries cannot be edited or deleted. To correct a posted entry, reverse it.
The reversal service:
- Locks the original entry.
- Ensures it is posted.
- Creates a new posted journal entry.
- Uses opposite debit and credit directions.
- Links the reversal to the original entry.
- Marks the original entry as reversed.
- Updates balances through normal posting.
- Dispatches
JournalEntryReversed.
Original entry:
Reversal entry:
Balances
Balances are stored in account_balances for fast reads.
Use:
Or inject BalanceService:
Balance Formula
For debit-normal accounts:
For credit-normal accounts:
Negative Balances
By default, the package prevents negative balances when prevent_negative_balances_by_default is enabled.
Allow a specific account to go negative:
Reports
Use LedgerManager for common reports:
Use LedgerReportService for more control:
Trial Balance
Returns account-level totals:
- account id
- code
- name
- type
- currency
- debit total
- credit total
- balance
General Ledger
Returns journal lines with entry and account information, filterable by:
- account
- date range
- reference type
- reference id
Account Statement
Returns movements for one account over a period.
Filament Integration
Filament support is optional. The package can run without Filament installed.
If Filament is installed, register the plugin:
The plugin registers:
LedgerEntityResourceLedgerAccountResourceJournalEntryResourceTrialBalancePageGeneralLedgerPageAccountStatementPageLedgerStatsWidgetAccountBalanceOverviewWidget
Filament Safety
- Destructive actions are omitted or require confirmation.
- Posted journal entries are not editable unless metadata updates are explicitly allowed.
- Posted journal lines are read-only.
- Manual journal creation is disabled by default.
- Account
type,normal_balance, andcurrencyare disabled when the account already has lines.
Enable manual journal creation only if your operators understand double-entry posting:
Host Application Use Cases
The package does not know your domain. The host app should create posting services or recipes that convert business events into journal entries.
Use Case: Generic Cash Receipt
Host application concept:
Ledger entry:
Code:
Use Case: Fee Split
Host application concept:
Ledger entry:
Code:
Use Case: Expense Recognition
Ledger entry:
Code:
Use Case: Liability Settlement
Ledger entry:
Code:
Use Case: Currency Exchange
Host application concept:
Important ledger design:
- Use separate accounts per currency when possible.
- Provide
baseAmountandexchangeRatefor multi-currency entries. - Keep exchange quote, rate source, and execution details in the host application.
- Store only generic
referenceType,referenceId, and metadata in the ledger entry.
Example:
Ledger entry:
Code:
The ledger package does not contain an exchange model, quote engine, rate provider, or settlement workflow. Those belong in the host app.
Use Case: Order Workflow
Host application concept:
The host app can post one journal entry per meaningful accounting event.
Order Payment Captured
Ledger entry:
Code:
Order Fulfilled And Revenue Recognized
Ledger entry:
Code:
Order Refund
Ledger entry:
Code:
This package does not decide when an order is fulfilled or whether revenue should be recognized at capture, shipment, delivery, or acceptance. The host application owns that policy.
Use Case: Marketplace Or Platform Payout
Host application concept:
Ledger entry when funds are collected:
Code:
Ledger entry when payout is sent:
Code:
Use Case: Customer Prepayment And Later Application
Host application concept:
When money is received:
When the deposit is applied:
Code:
Use Case: Internal Reclassification
Host application concept:
Ledger entry:
Code:
Use a reversal instead when the original posted journal entry itself is wrong and should be explicitly negated.
Use Case: Host-Specific Workflow Service
Create a service in your application:
This service belongs in your application, not in ledger-core.
Extending The Package
Custom Posting Validator
Bind PostingValidatorContract in your application:
Example validator:
Custom Account Resolver
Bind AccountResolverContract if your app needs custom account lookup.
Currency Converter
Bind CurrencyConverterContract if your app wants package-level conversion utilities.
Posting itself does not guess exchange rates. Your app should provide baseAmount and exchangeRate when needed.
Custom Models
You may replace model classes through config:
Custom models should extend the package models unless you are deliberately replacing behavior.
Events
The package dispatches Laravel events:
JournalEntryPosting
Dispatched before a journal entry is created, after idempotency has been checked.
JournalEntryPosted
Dispatched after the journal entry, lines, and balances have been created.
JournalEntryReversed
Dispatched after a reversal entry is posted and the original entry is marked reversed.
Exceptions
Common exceptions:
LedgerExceptionUnbalancedJournalEntryExceptionDuplicateJournalEntryExceptionIdempotencyConflictExceptionAccountCurrencyMismatchExceptionAccountNotPostableExceptionInsufficientBalanceExceptionInvalidReversalException
Example:
Testing
From the package directory:
The test suite covers:
- Entity creation
- Account creation
- Balanced posting
- Unbalanced posting rejection
- Inactive account rejection
- Non-postable account rejection
- Currency mismatch rejection
- Debit-normal balances
- Credit-normal balances
- Idempotent reposting
- Idempotency conflicts
- Reversal posting
- Opposite reversal lines
- Balance updates after reversal
- Posted line immutability
- Account currency immutability after posting
- Trial balance reports
- Account statement reports
- Opening balance posting
Operational Guidance
Do Not Update Balances Manually
Never run application code like this:
Always post a journal entry.
Do Not Edit Posted Lines
Posted lines are the audit record. If something is wrong, reverse the entry and post a corrected entry.
Keep Account Mapping In Your App
Your app should decide which accounts are used for each workflow.
Good pattern:
Avoid putting receipt, invoice, transfer, wallet, or provider logic inside the ledger package.
Use Database Transactions Around Host Workflow State
If your application updates business state and posts to the ledger, wrap both in one transaction when possible.
The ledger posting service also runs its own transaction. Laravel safely nests transactions using savepoints where supported.
Queue And Webhook Safety
For jobs and webhooks:
- Use a deterministic idempotency key.
- Catch idempotency conflicts.
- Retry only transient database failures.
- Treat unbalanced entries as application bugs.
- Log the
reference_type,reference_id, andidempotency_key.
FAQ
Does this package create a chart of accounts?
No. The package provides account storage and posting rules. Your application creates the accounts it needs.
Can I use this package for wallets?
Yes, but wallet concepts belong in your application. The package should only receive journal entries against generic accounts.
Can I delete a posted journal entry?
No. Reverse it.
Can I edit a posted amount?
No. Reverse the entry and post a corrected one.
Can I update cached balances directly?
No. Cached balances are maintained by JournalPostingService.
Can one journal entry include accounts from multiple entities?
Only if posting.allow_cross_entity_entries is enabled. The default is false.
Can accounts have no currency?
Yes. A null account currency means the package will accept line currencies for that account.
Can I use UUIDs externally?
Yes. Core tables include UUID columns for public references while keeping integer primary keys for database relationships.
How should I store business references?
Use reference_type and reference_id.
These are intentionally strings so the host application can reference any model or external event.
Why are amounts strings?
PHP floats are unsafe for money. This package uses decimal strings, decimal database columns, and bcmath.
Minimal Service Reference
LedgerManager
AccountService
JournalPostingService
BalanceService
ReversalService
LedgerReportService
License
MIT
All versions of ledger-core with dependencies
ext-bcmath Version *
illuminate/database Version ^11.0|^12.0|^13.0
illuminate/support Version ^11.0|^12.0|^13.0