PHPackages                             olaoluwasipe/multi-wallet-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. olaoluwasipe/multi-wallet-ledger

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

olaoluwasipe/multi-wallet-ledger
================================

Double-entry multi-currency wallet ledger for Laravel

v0.1.0(2mo ago)026MITPHPPHP ^8.2

Since Apr 27Pushed 2mo agoCompare

[ Source](https://github.com/olaoluwasipe/laravel-wallet-ledger)[ Packagist](https://packagist.org/packages/olaoluwasipe/multi-wallet-ledger)[ RSS](/packages/olaoluwasipe-multi-wallet-ledger/feed)WikiDiscussions master Synced 3w ago

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

Laravel Wallet Ledger
=====================

[](#laravel-wallet-ledger)

Double-entry, multi-currency ledger primitives for Laravel: **wallets**, **accounts**, **journals**, and optional **business-level transactions**. Amounts are stored in **minor units** (e.g. cents) per ISO currency code; each journal must balance **per currency** (total debits equal total credits for that currency).

The package focuses on persistence and invariants (balanced postings, line shape, idempotency). It does **not** ship UI or balance-sheet views—you build those on top of the models and `journal_lines`.

---

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

[](#requirements)

- PHP **8.2+**
- Laravel **11**, **12**, or **13** (`illuminate/*` ^11|^12|^13)

---

What it provides
----------------

[](#what-it-provides)

LayerRole**Wallet**A logical holder: `user`, `system`, or `provider` (extensible via config). May be tied to an **owner** (`owner_type` / `owner_id` morph) or stand alone (e.g. treasury). Optional `code` disambiguates system wallets (`treasury`, `fees`, `float`).**Account**A posting target inside a wallet: `account_kind` (e.g. `available`, `pending_in`) maps to an **accounting\_type** (`asset`, `liability`, …) from config. Accounts are identified in practice by wallet + generated `code` such as `USD:available`.**Journal**One balanced posting event: header + many **journal lines**.**Journal line**Debit **or** credit to one account, in one currency, in minor units.**Ledger** (`WalletLedger\Services\Ledger`)Validates and persists a `JournalDraft` inside a DB transaction; optional **idempotency** via `idempotency_key` on the journal. Dispatches `JournalPosted`.**WalletRegistry**`getOrCreateWallet()` and `ensureAccount()` using your published `wallet-ledger` config (wallet types, allowed codes, account kinds).**TransactionService** (optional)Posts a draft `LedgerTransaction` + lines to the ledger, links `journal_id` and `journal_line_id`, sets status to `posted`. Requires extra migrations and `features.business_transactions`. Dispatches `TransactionPosted`.**Strict vs lenient validation:** `validation_mode` is `strict` (default) or `lenient`. In strict mode, unknown `wallet_type`, `account_kind`, or `transaction_type` values throw. In lenient mode, those checks are skipped (useful while prototyping); unknown account kinds still default `accounting_type` to `asset` when missing from config.

---

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

[](#installation)

### 1. Require the package

[](#1-require-the-package)

```
composer require olaoluwasipe/multi-wallet-ledger
```

Laravel auto-discovers `WalletLedger\WalletLedgerServiceProvider`, which:

- Merges `config/wallet-ledger.php`
- Registers `Ledger`, `LedgerContract`, `TransactionService`, `TransactionServiceContract`, `WalletRegistry`, `ConfigValidator`
- **Loads core migrations** from the package (`database/migrations`) on every boot—no publish step is required for the base tables

### 2. Run migrations

[](#2-run-migrations)

```
php artisan migrate
```

This creates tables whose names use the configured prefix (default **`wl_`**): `wl_wallets`, `wl_accounts`, `wl_journals`, `wl_journal_lines`.

### 3. (Recommended) Publish configuration

[](#3-recommended-publish-configuration)

Publish once so you can edit wallet types, account kinds, transaction types, table prefix, and flags:

```
php artisan vendor:publish --tag=wallet-ledger-config
```

Edit `config/wallet-ledger.php`. You can mirror important values in `.env`:

EnvConfig pathPurpose`WALLET_LEDGER_VALIDATION_MODE``validation_mode``strict` or `lenient``WALLET_LEDGER_BUSINESS_TRANSACTIONS``features.business_transactions`Enable `TransactionService`(none by default)`table_prefix`DB table prefix (default `wl_`)**Order matters:** set `table_prefix` **before** the first migration if you do not want the default `wl_`. Changing prefix after tables exist requires a manual rename or new install.

### 4. Optional: copy migrations into your app

[](#4-optional-copy-migrations-into-your-app)

Normally you rely on the package’s `loadMigrationsFrom`. If you prefer owning migration files (e.g. to tweak indexes):

```
php artisan vendor:publish --tag=wallet-ledger-migrations
php artisan migrate
```

Avoid duplicating the same schema: either use package-loaded migrations **or** published copies, not both.

### 5. Optional: business transactions

[](#5-optional-business-transactions)

Extra tables: `wl_transactions`, `wl_transaction_lines`, and `transaction_id` on `wl_journals`.

```
php artisan vendor:publish --tag=wallet-ledger-transaction-migrations
php artisan migrate
```

Then enable the feature:

```
WALLET_LEDGER_BUSINESS_TRANSACTIONS=true
```

or in `config/wallet-ledger.php`:

```
'features' => [
    'business_transactions' => true,
],
```

Without both migrations and this flag, `TransactionService::post()` throws a clear runtime exception.

---

Configuration reference
-----------------------

[](#configuration-reference)

After publishing, `config/wallet-ledger.php` controls:

- **`table_prefix`** — Prefix for all package tables (via `WalletLedger\Support\Table::name()`).
- **`validation_mode`** — `strict` | `lenient`.
- **`features.business_transactions`** — Must be true to use `TransactionService`.
- **`wallet_types`** — Each key is a allowed `wallet_type`. Options include:
    - **`owner_required`** — If true, `getOrCreateWallet()` requires an owner model.
    - **`allowed_codes`** — For system wallets, optional whitelist of `code` values (default includes `treasury`, `fees`, `float`).
- **`account_kinds`** — Each key is an allowed `account_kind`; **`accounting_type`** drives reporting (asset vs liability, etc.).
- **`transaction_types`** — Keys are allowed `transaction_type` values on `LedgerTransaction` when strict.

Inject `WalletLedger\Services\WalletRegistry` and resolve `WalletLedger\Contracts\LedgerContract` (or `Ledger`) in your services, seeders, and jobs.

---

Adding wallets and accounts in code
-----------------------------------

[](#adding-wallets-and-accounts-in-code)

Use **`WalletRegistry`** so config rules (owner required, allowed codes, account kinds) are enforced in strict mode.

**System wallet** (no owner; use `code` from `allowed_codes`):

```
use WalletLedger\Services\WalletRegistry;

$registry = app(WalletRegistry::class);

$treasury = $registry->getOrCreateWallet('system', null, 'treasury', 'Treasury');
$treasuryUsd = $registry->ensureAccount($treasury, 'available', 'USD', 'Treasury USD');
```

**User wallet** (owner required):

```
$wallet = $registry->getOrCreateWallet('user', $user, null, 'Primary wallet');
$usdAvailable = $registry->ensureAccount($wallet, 'available', 'USD');
```

**Adding more wallet types** (e.g. `merchant`):

1. Add a block under `wallet_types` in `config/wallet-ledger.php` with `owner_required` / `allowed_codes` as needed.
2. Clear config cache if applicable: `php artisan config:clear`.

**Adding more account kinds** (e.g. `escrow`):

1. Add an entry under `account_kinds` with the correct **`accounting_type`** (`asset`, `liability`, etc.) for how you want balances and reports to behave.
2. Call `ensureAccount($wallet, 'escrow', 'EUR', 'Optional display name')`.

Account **codes** are generated as `{CURRENCY}:{account_kind}` (currency is uppercased). One row per wallet + code combination.

---

Posting journals (core API)
---------------------------

[](#posting-journals-core-api)

Resolve `WalletLedger\Contracts\LedgerContract` and pass a **`JournalDraft`** with **`JournalLineDraft`** lines:

```
use WalletLedger\Contracts\LedgerContract;
use WalletLedger\DTO\JournalDraft;
use WalletLedger\DTO\JournalLineDraft;

$ledger = app(LedgerContract::class);

$journal = $ledger->post(new JournalDraft(
    lines: [
        new JournalLineDraft($debitAccountId, 'USD', debitMinor: 1000, creditMinor: 0),
        new JournalLineDraft($creditAccountId, 'USD', debitMinor: 0, creditMinor: 1000),
    ],
    idempotencyKey: 'optional_unique_key', // duplicate post returns same journal
    description: 'Transfer',
    referenceType: \App\Models\Order::class, // optional morph
    referenceId: 123,
    meta: ['source' => 'api'],
));
```

Rules enforced before insert:

- At least one line; **per currency**, sum of `debit_minor` equals sum of `credit_minor`.
- Each line: non-negative amounts, not both debit and credit positive, ISO **3-letter** currency.
- All `account_id` values must exist.

**Idempotency:** If `idempotency_key` is set and non-empty, a second `post()` with the same key returns the existing journal (with lines), without double-posting.

**Events:** `WalletLedger\Events\JournalPosted` carries the saved `Journal` (with relations as loaded).

---

Business transactions (optional)
--------------------------------

[](#business-transactions-optional)

1. Publish and run transaction migrations (see above).
2. Set `features.business_transactions` to true.

Create a `WalletLedger\Models\LedgerTransaction` with `status = draft`, a valid `transaction_type`, and related `LedgerTransactionLine` rows (balanced, same shape as journal lines). Then:

```
use WalletLedger\Contracts\TransactionServiceContract;

$posted = app(TransactionServiceContract::class)->post($transaction);
```

The service creates the journal, attaches `journal_id` to the transaction, sets `posted_at` and `status = posted`, and links each transaction line to its `journal_line_id`. Idempotency is supported on the transaction via `idempotency_key` when status is already `posted`.

---

Inspecting what is in the database
----------------------------------

[](#inspecting-what-is-in-the-database)

Table names below assume the default prefix **`wl_`**. If you changed `table_prefix`, substitute accordingly (or query `config('wallet-ledger.table_prefix')` in Tinker).

**Laravel Tinker:**

```
php artisan tinker
```

```
use WalletLedger\Models\Wallet;
use WalletLedger\Models\Account;
use WalletLedger\Models\Journal;
use WalletLedger\Models\JournalLine;

Wallet::with('accounts')->get();
Journal::with('lines.account.wallet')->latest('id')->take(5)->get();
```

**Useful SQL (MySQL / SQLite style):**

List wallets:

```
SELECT id, uuid, wallet_type, owner_type, owner_id, code, name
FROM wl_wallets
ORDER BY id;
```

List accounts with wallet:

```
SELECT a.id, a.code, a.name, a.account_kind, a.accounting_type,
       w.wallet_type, w.code AS wallet_code
FROM wl_accounts a
JOIN wl_wallets w ON w.id = a.wallet_id
ORDER BY w.id, a.id;
```

Recent journals with line count:

```
SELECT j.id, j.uuid, j.description, j.posted_at, COUNT(l.id) AS line_count
FROM wl_journals j
LEFT JOIN wl_journal_lines l ON l.journal_id = j.id
GROUP BY j.id
ORDER BY j.id DESC
LIMIT 20;
```

If business transactions are enabled:

```
SELECT id, uuid, transaction_type, status, journal_id, posted_at
FROM wl_transactions
ORDER BY id DESC;
```

---

Balances, trial balance, and “balance sheet” style views
--------------------------------------------------------

[](#balances-trial-balance-and-balance-sheet-style-views)

The package does **not** compute running balances on write. You derive them by **aggregating `wl_journal_lines`** (and optionally filtering by `posted_at` or journal id for “as of” reports).

**Trial balance (per account and currency)** — raw debit and credit totals:

```
SELECT
    a.id AS account_id,
    a.code,
    a.name,
    a.accounting_type,
    jl.currency,
    SUM(jl.debit_minor) AS debit_minor,
    SUM(jl.credit_minor) AS credit_minor
FROM wl_journal_lines jl
JOIN wl_accounts a ON a.id = jl.account_id
GROUP BY a.id, a.code, a.name, a.accounting_type, jl.currency
ORDER BY a.accounting_type, a.code, jl.currency;
```

**Signed balance in minor units** depends on `accounting_type` (normal balance):

- **Asset** (normal debit): `debit_minor - credit_minor`
- **Liability** (normal credit): `credit_minor - debit_minor`

Example grouped by accounting side:

```
SELECT
    a.accounting_type,
    jl.currency,
    SUM(CASE WHEN a.accounting_type = 'asset'
        THEN jl.debit_minor - jl.credit_minor
        ELSE jl.credit_minor - jl.debit_minor
    END) AS balance_minor
FROM wl_journal_lines jl
JOIN wl_accounts a ON a.id = jl.account_id
GROUP BY a.accounting_type, jl.currency;
```

For a **wallet-level “available USD”** balance, filter accounts (e.g. `account_kind = 'available'`) and restrict to the wallet’s `account_id` set, or join through `wl_accounts.wallet_id`.

**Display tips:**

- Convert **minor → major** in the UI: `amount_major = balance_minor / 10^exponent` (often 2 for USD; use a money library or ISO currency metadata for exotic exponents).
- For a traditional **balance sheet**, map your `accounting_type` values to line items (and split **current vs non-current** in your own metadata or naming convention—the package only stores the string `accounting_type` on `accounts`).
- For **multi-currency**, never sum different `currency` values without FX logic; the ledger already isolates balances per currency.

---

Events
------

[](#events)

EventWhen`WalletLedger\Events\JournalPosted`After a successful `Ledger::post()`.`WalletLedger\Events\TransactionPosted`After `TransactionService::post()` completes.Subscribe in `EventServiceProvider` or use Laravel’s auto-discovery for listeners.

---

Testing (package developers)
----------------------------

[](#testing-package-developers)

Default `composer test` runs fast unit tests (no database).

```
composer test:integration
```

Integration tests expect **MySQL** and create a database named `wallet_ledger_package_test` unless overridden with `WALLET_LEDGER_DB_*` variables—see `tests/bootstrap.php`.

---

Summary
-------

[](#summary)

1. `composer require olaoluwasipe/multi-wallet-ledger` → `php artisan migrate`
2. `php artisan vendor:publish --tag=wallet-ledger-config` → adjust wallet/account/transaction definitions
3. Use **`WalletRegistry`** for wallets/accounts and **`LedgerContract`** for postings (or **`TransactionService`** when business transactions are enabled)
4. Inspect data via Eloquent models or SQL on `wl_*` tables
5. Build balance sheet and P&amp;L views by aggregating `journal_lines` with your `accounting_type` and currency rules

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance83

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

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

88d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b89d421b7eb74c8e99918070ca2090eb1146ea279490572bb3951a9a66046858?d=identicon)[olaoluwasipe](/maintainers/olaoluwasipe)

---

Top Contributors

[![olaoluwasipe](https://avatars.githubusercontent.com/u/123474859?v=4)](https://github.com/olaoluwasipe "olaoluwasipe (5 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/olaoluwasipe-multi-wallet-ledger/health.svg)

```
[![Health](https://phpackages.com/badges/olaoluwasipe-multi-wallet-ledger/health.svg)](https://phpackages.com/packages/olaoluwasipe-multi-wallet-ledger)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/pulse

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

1.7k15.1M136](/packages/laravel-pulse)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45444.2k1](/packages/pressbooks-pressbooks)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M246](/packages/laravel-ai)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

255.2k](/packages/aedart-athenaeum)

PHPackages © 2026

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