PHPackages                             michael-orenda/general-ledger - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. michael-orenda/general-ledger

ActiveLibrary[Database &amp; ORM](/categories/database)

michael-orenda/general-ledger
=============================

Audit-grade, append-only General Ledger for Laravel 12 with strict accounting invariants, period locking, and sub-ledger integration.

v0.0.1(6mo ago)00MITPHPPHP ^8.2

Since Dec 17Pushed 6mo agoCompare

[ Source](https://github.com/michael-orenda/general-ledger)[ Packagist](https://packagist.org/packages/michael-orenda/general-ledger)[ RSS](/packages/michael-orenda-general-ledger/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (8)Versions (2)Used By (0)

General Ledger Package
======================

[](#general-ledger-package)

> A robust, test-driven General Ledger (GL) package for Laravel 12 designed as a reusable component for accounting systems. Written to be embedded as a package in larger host applications.

---

Table of contents
-----------------

[](#table-of-contents)

1. [Overview](#overview)
2. [Key Concepts](#key-concepts)
3. [Requirements](#requirements)
4. [Installation](#installation)
5. [Configuration](#configuration)
6. [Database Migrations &amp; Seeds](#database-migrations--seeds)
7. [Models &amp; Relationships](#models--relationships)
8. [Services](#services)
9. [Controllers &amp; Routes (API)](#controllers--routes-api)
10. [Validation &amp; Business Rules](#validation--business-rules)
11. [Database Constraints &amp; Triggers](#database-constraints--triggers)
12. [Events &amp; Listeners](#events--listeners)
13. [Testing](#testing)
14. [Security &amp; Concurrency](#security--concurrency)
15. [Packaging &amp; Release](#packaging--release)
16. [Troubleshooting](#troubleshooting)
17. [Examples &amp; Use-Cases](#examples--use-cases)
18. [Contributing](#contributing)
19. [Changelog](#changelog)
20. [License](#license)

---

Overview
--------

[](#overview)

This package provides a General Ledger implementation that follows standard double-entry accounting principles, shaped for multi-organisation, multi-fiscal-period systems. It aims to be:

- **Composable**: usable as a Laravel package or integrated directly into your mono-repo.
- **Test-first**: shipped with Testbench-powered tests.
- **Safe**: includes DB-level protections (checks/triggers), API guards, and service-layer validation.
- **Extensible**: sub-ledgers can post to the GL through a clear contract.

Primary capabilities:

- Chart of Accounts (COA) -&gt; auto-created ledger accounts
- Posting journal entries (balanced debit/credit)
- Ledger accounts and entries listing &amp; reporting
- Period locking and enforcement
- Opening/closing period handling
- Services to generate opening balances

---

Key Concepts
------------

[](#key-concepts)

- **Organisation**: top-level tenant scoping most data.
- **FiscalYear / FiscalPeriod**: fiscal time boundaries. Periods can be `open` or `closed`.
- **LedgerAccount**: representation of an account in the GL (asset, liability, equity, revenue, expense).
- **LedgerEntry**: posted transactional rows tied to a ledger account. Entries are created by posting a `JournalEntry` which contains multiple debit/credit `JournalLine`s.
- **Sub-ledger**: domain modules (loans, payments, payroll) that use a `PostsToGeneralLedger` contract to post entries.

---

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 12
- Database: MySQL 5.7+/MariaDB or Postgres (some DB-triggers examples provided are for MySQL; adapt for Postgres)
- ext-mbstring, ext-json

---

Installation
------------

[](#installation)

Install via composer (package name to replace with the published packagist name):

```
composer require michaelorenda/general-ledger
```

Publish config, migrations and seeds (if the package provides publishable assets):

```
php artisan vendor:publish --provider="MichaelOrenda\GeneralLedger\GeneralLedgerServiceProvider" --tag="config"
php artisan vendor:publish --provider="MichaelOrenda\GeneralLedger\GeneralLedgerServiceProvider" --tag="migrations"
php artisan vendor:publish --provider="MichaelOrenda\GeneralLedger\GeneralLedgerServiceProvider" --tag="seeds"
```

Run migrations:

```
php artisan migrate
```

Seed default Chart of Accounts and a sample organisation (optional):

```
php artisan db:seed --class="\MichaelOrenda\GeneralLedger\Database\Seeders\ChartOfAccountsSeeder"
php artisan db:seed --class="\MichaelOrenda\GeneralLedger\Database\Seeders\OrganisationSeeder"
```

---

Configuration
-------------

[](#configuration)

`config/general-ledger.php` (sensible defaults):

```
return [
    'default_currency' => env('GL_DEFAULT_CURRENCY', 'KES'),
    'default_start_of_day' => '00:00:00',
    'journal_prefix' => env('GL_JOURNAL_PREFIX', 'JE'),
    'locking' => [
        'enable_db_triggers' => true,
    ],
    // mapping of account classes/types
    'accounts' => [
        'asset' => 1,
        'liability' => 2,
        'equity' => 3,
        'revenue' => 4,
        'expense' => 5,
    ],
];
```

Explaination of key options:

- `default_currency`: currency used when posting amounts that do not specify currency explicitly.
- `locking.enable_db_triggers`: set to `false` if you prefer application-level enforcement only.

---

Database Migrations &amp; Seeds
-------------------------------

[](#database-migrations--seeds)

The package includes the following recommended tables:

- `ledger_accounts` (id, organisation\_id, code, name, account\_class, normal\_balance, created\_at, updated\_at)
- `ledger_entries` (id, organisation\_id, ledger\_account\_id, journal\_id, amount, type (debit|credit), description, fiscal\_period\_id, created\_at)
- `journals` or `journal_entries` (id, organisation\_id, reference, narration, posted\_by, posted\_at)
- `fiscal_years` (id, organisation\_id, start\_date, end\_date)
- `fiscal_periods` (id, organisation\_id, fiscal\_year\_id, start\_date, end\_date, is\_closed, created\_at)

Migrations should include indexes on `organisation_id`, `fiscal_period_id` and `ledger_account_id` for performance.

### Seeders

[](#seeders)

- `ChartOfAccountsSeeder` — seeds a canonical set of ledger accounts.
- `LedgerAccountsSeeder` — creates account instances per organisation from the COA.

---

Models &amp; Relationships
--------------------------

[](#models--relationships)

Outline of essential Eloquent models and their core relationships:

- `LedgerAccount`

    - belongsTo `Organisation`
    - hasMany `LedgerEntry`
- `LedgerEntry`

    - belongsTo `LedgerAccount`
    - belongsTo `FiscalPeriod`
    - belongsTo `JournalEntry`
- `JournalEntry` (or `Journal`)

    - hasMany `JournalLine` or `LedgerEntry`
- `FiscalPeriod`

    - belongsTo `FiscalYear`
    - hasMany `LedgerEntry`

Add docblocks and typed properties on each model for clarity and static analysis support.

---

Services
--------

[](#services)

This section documents **all service classes** in the General Ledger package, their responsibilities, invariants, and example usage patterns. These services are the *only* supported way to interact with the ledger at the domain level. Controllers, commands, and sub-ledgers must call services — never manipulate models directly.

> **Design rule**: Services encapsulate *business invariants*. Models remain persistence-focused.

---

### AccountLedgerService

[](#accountledgerservice)

**Purpose**

Produces a ledger (running balance) for a single account over a date or fiscal-period range.

**Responsibilities**

- Fetch ledger entries for a given account
- Sort chronologically
- Compute running balances (debit/credit aware)
- Support date-range or fiscal-period filtering

**Typical Use Cases**

- Account detail view in accounting UI
- Audit or reconciliation workflows

**Example**

```
$ledger = $accountLedgerService->getLedger(
    organisationId: 1,
    ledgerAccountId: 101,
    from: '2025-01-01',
    to: '2025-01-31'
);
```

**Returns**

- Collection of ledger rows with:
    - entry date
    - debit / credit
    - running balance

---

### AuditTrailService

[](#audittrailservice)

**Purpose**

Provides a read-only, immutable audit trail of all ledger activity.

**Responsibilities**

- Expose who posted what and when
- Correlate journals, ledger entries, and users
- Support forensic review and compliance

**Important Rule**

Audit data is **never mutated** — even reversals create *new* entries.

**Example**

```
$audit = $auditTrailService->forJournal($journalId);
```

---

### GeneralLedgerReportService

[](#generalledgerreportservice)

**Purpose**

Produces the official General Ledger report used for statutory reporting.

**Responsibilities**

- Aggregate all ledger accounts
- Group by account class
- Respect fiscal-period boundaries
- Produce balances as-of a date or period

**Example**

```
$report = $glReportService->generate(
    organisationId: 1,
    fiscalPeriodId: 12
);
```

---

### LedgerQueryService

[](#ledgerqueryservice)

**Purpose**

Low-level query abstraction for ledger entries.

**Responsibilities**

- Centralize common ledger queries
- Avoid query duplication across services
- Apply organisation and period scoping consistently

**Example**

```
$entries = $ledgerQueryService
    ->forOrganisation(1)
    ->forPeriod(12)
    ->forAccount(101)
    ->get();
```

---

### LedgerPostingService

[](#ledgerpostingservice)

**Purpose**

Lowest-level service responsible for inserting ledger entries.

**Important**

> You should **never call this directly** from controllers. Use `JournalPostingService` instead.

**Responsibilities**

- Insert debit/credit rows
- Enforce period open-state
- Operate strictly inside transactions

Used internally by:

- `JournalPostingService`
- `OpeningBalanceService`
- `ReversalJournalService`

---

### JournalPostingService

[](#journalpostingservice)

**Purpose**

Primary entry point for posting accounting transactions.

**Responsibilities**

- Validate journal structure
- Enforce balancing (debits == credits)
- Validate fiscal period openness
- Persist journal + ledger entries atomically
- Emit `JournalPosted` event

**Example**

```
$journalId = $journalPostingService->post([
    'organisation_id' => 1,
    'fiscal_period_id' => 12,
    'reference' => 'INV-1001',
    'narration' => 'Invoice payment',
    'lines' => [
        ['ledger_account_id' => 10, 'type' => 'debit', 'amount' => 1000],
        ['ledger_account_id' => 200, 'type' => 'credit', 'amount' => 1000],
    ],
]);
```

**Throws**

- `DomainException` — unbalanced journal
- `LogicException` — posting to closed period

---

### JournalReversalService / ReversalJournalService

[](#journalreversalservice--reversaljournalservice)

**Purpose**

Creates a reversing journal for an existing journal.

**Responsibilities**

- Clone original journal lines
- Swap debit ↔ credit
- Preserve auditability
- Prevent double-reversal

**Example**

```
$reversalJournalId = $journalReversalService->reverse(
    journalId: 55,
    reason: 'Invoice cancelled'
);
```

**Rule**

Reversals must always post into an *open* fiscal period.

---

### TrialBalanceService

[](#trialbalanceservice)

**Purpose**

Produces a Trial Balance for a given period or date.

**Responsibilities**

- Aggregate debits and credits per account
- Ensure totals balance
- Act as a validation gate before period close

**Example**

```
$trialBalance = $trialBalanceService->generate(
    organisationId: 1,
    fiscalPeriodId: 12
);
```

If the trial balance does not balance, **period close must be blocked**.

---

### OpeningBalanceService

[](#openingbalanceservice)

**Purpose**

Generates opening balances for a new fiscal period.

**Responsibilities**

- Carry forward asset/liability balances
- Zero revenue and expense accounts
- Transfer net income to retained earnings
- Enforce idempotency

**Example**

```
$openingBalanceService->generate(
    organisationId: 1,
    fromPeriodId: 12,
    toPeriodId: 13
);
```

---

### PeriodCloseService / FiscalPeriodCloseService

[](#periodcloseservice--fiscalperiodcloseservice)

**Purpose**

Safely closes a fiscal period.

**Responsibilities**

1. Generate Trial Balance
2. Validate balance correctness
3. Generate opening balances for next period
4. Lock the period (DB + application)
5. Emit `PeriodClosed` event

**Example**

```
$periodCloseService->close(
    organisationId: 1,
    fiscalPeriodId: 12
);
```

**Hard Guarantees**

- Closed periods are immutable
- Posting attempts fail at *service + DB level*

---

All core business logic lives in service classes. Keep controllers thin.

### JournalPostingService

[](#journalpostingservice-1)

Responsibilities:

- Validate payload shape (presence of `fiscal_period_id`, `organisation_id`, balanced `lines`).
- Ensure the target `FiscalPeriod` is `open` before posting.
- Persist a `JournalEntry` and corresponding `LedgerEntry` rows within a DB transaction.
- Emit domain events (e.g., `JournalPosted`, `LedgerUpdated`).

Example usage:

```
$service->post([
    'organisation_id' => 1,
    'fiscal_period_id' => 12,
    'reference' => 'INV-1001',
    'narration' => 'Payment for invoice 1001',
    'lines' => [
        [ 'ledger_account_id' => 10, 'type' => 'debit', 'amount' => 1000.00, 'description' => 'Cash' ],
        [ 'ledger_account_id' => 200, 'type' => 'credit', 'amount' => 1000.00, 'description' => 'Revenue' ],
    ],
    'posted_by' => 5,
]);
```

Important checks inside `post()`:

- `fiscal_period_id` exists and belongs to organisation.
- Period is not closed (`$period->is_closed === false`).
- Sum of debit amounts equals sum of credit amounts.
- Each `ledger_account_id` exists and belongs to the organisation (or is globally allowed per config).

Transaction handling:

- Use `DB::transaction()` around create operations.
- Lock rows where necessary (see concurrency section) to avoid race conditions.

### PeriodCloseService

[](#periodcloseservice)

Responsibilities:

- Calculate final balances for the period.
- Move balances into next-period opening balances (create `OpeningBalance` entries).
- Prevent further posting to closed periods (DB triggers + application-level flags).

Example:

```
$periodCloseService->close($organisationId, $fiscalPeriodId);
```

### OpeningBalanceService

[](#openingbalanceservice-1)

Generates the opening balances for a new fiscal period from previous period totals and retained earnings transformations. Should be idempotent — calling it multiple times should not duplicate balances.

---

Controllers &amp; Routes (API)
------------------------------

[](#controllers--routes-api)

Provide API endpoints namespaced under `/api/general-ledger` or similar. Use API resources for consistent responses.

Example routes:

```
Route::prefix('api')->group(function () {
    Route::prefix('general-ledger')->middleware(['auth:api'])->group(function () {
        Route::get('accounts', [AccountController::class, 'index']);
        Route::get('accounts/{id}', [AccountController::class, 'show']);

        Route::post('journals', [JournalController::class, 'store']);
        Route::get('journals/{id}', [JournalController::class, 'show']);

        Route::post('periods/{id}/close', [PeriodController::class, 'close']);
    });
});
```

Controller responsibilities:

- Validate requests with FormRequests.
- Use service classes to perform operations.
- Return HTTP 400/422 for business validation failures (e.g., unbalanced journal or closed period) and 500 for unexpected errors.

### Example API Request — Post Journal

[](#example-api-request--post-journal)

**POST** `/api/general-ledger/journals`

Payload:

```
{
  "organisation_id": 1,
  "fiscal_period_id": 12,
  "reference": "INV-1001",
  "narration": "Payment for invoice 1001",
  "lines": [
    {"ledger_account_id": 10, "type": "debit", "amount": 1000.00, "description": "Cash"},
    {"ledger_account_id": 200, "type": "credit", "amount": 1000.00, "description": "Revenue"}
  ]
}
```

Success: HTTP 201 with created journal details. Failure: HTTP 422 with validation errors or HTTP 409 if period closed.

---

Validation &amp; Business Rules
-------------------------------

[](#validation--business-rules)

1. Journals must be balanced (total debits == total credits).
2. Every entry must reference a valid ledger account.
3. Cannot post into a closed period — enforce at service-level and with DB-level constraints/triggers.
4. Opening balances must be posted to the first period of the fiscal year only through the `OpeningBalanceService`.
5. Avoid floating point rounding errors: store amounts as integers representing the smallest currency unit (e.g., cents) or use `decimal(20,4)` depending on requirements.

---

Database Constraints &amp; Triggers
-----------------------------------

[](#database-constraints--triggers)

To prevent accidental posting to closed periods at the DB level (MySQL example):

```
DELIMITER $$
CREATE TRIGGER prevent_closed_period_posting
BEFORE INSERT ON ledger_entries
FOR EACH ROW
BEGIN
    IF EXISTS (
        SELECT 1 FROM fiscal_periods
        WHERE id = NEW.fiscal_period_id
        AND is_closed = 1
    ) THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot post to a closed fiscal period';
    END IF;
END$$
DELIMITER ;
```

Create similar triggers for `BEFORE UPDATE` and `BEFORE DELETE` depending on policy. In the migration files, add these triggers conditionally (guard with `if (Schema::hasTable('fiscal_periods')) { ... }`).

> Note: not all DBs support triggers or `SIGNAL` semantics. If you need Postgres, implement plpgsql triggers or prefer application-level enforcement.

---

Events &amp; Listeners
----------------------

[](#events--listeners)

Emit meaningful domain events:

- `JournalPosted` (payload: journal id, organisation id, posted\_by)
- `PeriodClosed` (period id, organisation id)
- `OpeningBalancesGenerated` (period id)

Listeners can:

- Update cached ledgers or materialized views
- Send notifications to admins
- Integrate sub-ledger reconciliations

Example event dispatch:

```
event(new JournalPosted($journal));
```

---

Testing
-------

[](#testing)

- Use `orchestral/testbench` for package testing with Laravel.
- Include unit tests for: service logic (balanced journals, period checks), model factories, API endpoints.
- Include integration tests that run migrations and seeders.

Suggested tests:

- `JournalPostingServiceTest` — posts valid/invalid journals; asserts DB rows and events emitted.
- `PeriodCloseServiceTest` — closes period; checks opening balances created for subsequent period.
- `TriggersMigrationTest` — if your environment supports triggers, assert that triggers prevent posting to closed periods.

Run tests:

```
composer test
# or
vendor/bin/phpunit
```

---

Security &amp; Concurrency
--------------------------

[](#security--concurrency)

- Always wrap posting in DB transactions.
- Use row-level locks for balances where needed (e.g., `SELECT ... FOR UPDATE`) to prevent concurrent postings causing inconsistent totals.
- Validate `organisation_id` for multi-tenant isolation.
- Avoid storing user-supplied JSON directly — sanitize structured data.

---

Packaging &amp; Release
-----------------------

[](#packaging--release)

- Use semantic versioning (MAJOR.MINOR.PATCH).
- Create a `README.md` (this document), `CHANGELOG.md` and tags for releases.
- Run static analysis (`phpstan`), tests, and `composer validate` before tagging.

Suggested git commit message for major release:

```
chore(release): v1.0.0 — initial stable GL package

```

---

Troubleshooting
---------------

[](#troubleshooting)

- **"Cannot post to closed fiscal period"** — check if period `is_closed` flag is set; if you suspect trigger misfire, check migrations created DB triggers.
- **Undefined variable or method errors** — ensure service injection in constructors and proper imports.
- **Intelephense type complaints** — add docblocks and typed properties; for collection vs int errors, ensure method signatures match usage (pass collections where expected).

If you run into issues, enable detailed logs (set `APP_DEBUG=true`) and inspect `laravel.log`.

---

Examples &amp; Use-Cases
------------------------

[](#examples--use-cases)

### Posting a Sales Invoice (from a SubLedger)

[](#posting-a-sales-invoice-from-a-subledger)

1. The invoice module builds a `Journal` payload.
2. It validates business rules (customer exists, amount positive).
3. Calls `JournalPostingService::post($payload)`.
4. The GL persists entries, emits `JournalPosted` and the invoice marks itself `posted_to_gl`.

### Closing a Period (end of month)

[](#closing-a-period-end-of-month)

1. Admin triggers `POST /api/general-ledger/periods/{id}/close`.
2. Controller calls `PeriodCloseService::close()` inside a transaction.
3. Service computes retained earnings transfers and creates opening balances for the next period.
4. `fiscal_period.is_closed` set to `true` and DB triggers now block further inserts.

---

Contributing
------------

[](#contributing)

1. Fork repository
2. Create feature branch: `feature/awesome-thing`
3. Implement code + tests
4. Submit PR and ensure CI passes

Coding standards: PSR-12, typed properties where helpful, docblocks for public APIs.

---

Changelog
---------

[](#changelog)

- **Unreleased**: initial comprehensive README and structure guidance.

---

License
-------

[](#license)

MIT — see `LICENSE` in the repository.

---

### Appendix: Helpful Snippets

[](#appendix-helpful-snippets)

**DB Transaction skeleton (service):**

```
DB::transaction(function () use ($payload) {
    $journal = Journal::create([...]);
    foreach ($payload['lines'] as $line) {
        LedgerEntry::create([...]);
    }
});
```

**Check balanced:**

```
$totalDebits = array_sum(array_map(fn($l) => $l['type'] === 'debit' ? $l['amount'] : 0, $lines));
$totalCredits = array_sum(array_map(fn($l) => $l['type'] === 'credit' ? $l['amount'] : 0, $lines));
if (bccomp((string)$totalDebits, (string)$totalCredits, 4) !== 0) {
    throw new \DomainException('Journal is not balanced.');
}
```

**Trigger creation in migration (MySQL):**

Add migration content carefully and conditionally — see `up()`/`down()` examples in package migrations.

---

If you'd like, I can also:

- Produce a printable PDF of this README.
- Generate a `README_short.md` (one-page quickstart).
- Create the API OpenAPI (Swagger) spec from the routes.

Tell me which of those you want and I will generate them next.

###  Health Score

28

—

LowBetter than 52% of packages

Maintenance66

Regular maintenance activity

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

199d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1303237?v=4)[Michael Philip Orenda](/maintainers/rminchrist)[@rminchrist](https://github.com/rminchrist)

---

Tags

laravelAuditfinanceAccountingdouble entrymulti-tenantmulti-currencygeneral ledgerfiscal-periodtrial-balance

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/michael-orenda-general-ledger/health.svg)

```
[![Health](https://phpackages.com/badges/michael-orenda-general-ledger/health.svg)](https://phpackages.com/packages/michael-orenda-general-ledger)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.6k29.9M146](/packages/laravel-cashier)[api-platform/laravel

API Platform support for Laravel

58171.5k14](/packages/api-platform-laravel)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M132](/packages/laravel-pulse)[yajra/laravel-oci8

Oracle DB driver for Laravel via OCI8

8793.2M25](/packages/yajra-laravel-oci8)[glushkovds/phpclickhouse-laravel

Adapter of the most popular library https://github.com/smi2/phpClickHouse to Laravel

2051.5M2](/packages/glushkovds-phpclickhouse-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
