PHPackages                             laratusk/cashflow - 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. laratusk/cashflow

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

laratusk/cashflow
=================

Double-entry balance transaction ledger for Laravel

v0.4.0(2mo ago)2265MITPHPPHP ^8.2CI passing

Since Mar 6Pushed 2mo agoCompare

[ Source](https://github.com/laratusk/cashflow)[ Packagist](https://packagist.org/packages/laratusk/cashflow)[ RSS](/packages/laratusk-cashflow/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (3)Dependencies (16)Versions (5)Used By (0)

Cashflow
========

[](#cashflow)

A double-entry balance transaction ledger for Laravel. Track credits, debits, fees, and derived amounts with type-safe balance items, validation, and dependency enforcement.

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

[](#installation)

```
composer require laratusk/cashflow
```

Publish the migration and run it:

```
php artisan vendor:publish --tag=cashflow-migrations
php artisan migrate
```

Optionally publish the config (only needed if you want to override the `BalanceTransaction` model):

```
php artisan vendor:publish --tag=cashflow-config
```

Quick Start
-----------

[](#quick-start)

```
use Laratusk\Cashflow\Balance;
$balance = Balance::for($account)
    ->reference($order)
    ->currency('USD');

$balance->insert(new Payment(amount: 10000));
$balance->insert(new GatewayFee(amount: 290));
$balance->insert(new GatewayFeeTax(amount: 52));
$balance->insert(new SalesTax(taxRate: 8.0));

$balance->save();

$balance->batchId();                  // "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
$balance->amountOf(Payment::class);   // 10000
$balance->amountOf(GatewayFee::class); // 290
```

Setup
-----

[](#setup)

Add `HasBalanceTransactions` to any model that holds a balance:

```
use Laratusk\Cashflow\Concerns\HasBalanceTransactions;

class Account extends Model
{
    use HasBalanceTransactions;
}
```

This adds a `balanceTransactions()` morph-many relationship.

Why Balance Items Are Not Shipped
---------------------------------

[](#why-balance-items-are-not-shipped)

Cashflow does not ship any `BalanceItem` classes. Every business has different transaction types -- a SaaS platform has subscriptions and usage fees, a marketplace has commissions and payouts, a payment processor has gateway fees and dispute charges.

Balance items are **your domain logic**. The package provides the base class (`BalanceItem`), attributes (`#[Unique]`, `#[Rule]`, `#[Requires]`), and the `Balance` engine. You define the items that match your business.

Generating Balance Items
------------------------

[](#generating-balance-items)

Use the artisan command to scaffold new items interactively:

```
php artisan make:balance-item
```

The command asks:

1. **Class name** -- e.g. `Payment`, `GatewayFee`, `Refund`
2. **Direction** -- Debit or Credit
3. **Amount source** -- Passed from outside, fixed value, or derived from another item
4. **Unique?** -- Whether to add `#[Unique]`
5. **Requires?** -- Dependency on another balance item

Files are created in `app/Cashflow/BalanceItems/`. Add validation rules directly in the generated class.

Defining Balance Items
----------------------

[](#defining-balance-items)

Extend `BalanceItem` and define `direction()`. If the constructor has an `$amount` parameter, it is resolved automatically -- no need to override `amount()`:

```
use Laratusk\Cashflow\Contracts\BalanceItem;
use Laratusk\Cashflow\Enums\Direction;
use Laratusk\Cashflow\Attributes\Rule;

final class Payment extends BalanceItem
{
    public function __construct(
        #[Rule('required', 'integer', 'min:1')]
        private readonly int $amount,
    ) {}

    public function direction(): Direction
    {
        return Direction::Credit;
    }
}
```

The base class auto-resolves `$amount` and `$direction` from constructor properties. Override `amount()` only when you need custom logic (derived amounts, fixed values).

### Fixed vs Derived Amounts

[](#fixed-vs-derived-amounts)

Items can receive their amount directly or calculate it from siblings:

```
// Fixed amount (e.g. Stripe returns the fee value)
$balance->insert(new GatewayFee(amount: 290));

// Derived amount (e.g. TrustPayment charges a percentage)
$balance->insert(new GatewayFee(percentage: 2.9));
```

Implementation with `required_without` validation:

```
#[Unique]
final class GatewayFee extends BalanceItem
{
    public function __construct(
        #[Rule('required_without:percentage', 'nullable', 'integer', 'min:1')]
        private readonly ?int $amount = null,

        #[Rule('required_without:amount', 'nullable', 'numeric', 'gt:0', 'max:100')]
        private readonly ?float $percentage = null,
    ) {}

    public function amount(): int
    {
        if ($this->amount !== null) {
            return $this->amount;
        }

        return (int) ceil(
            $this->balance()->amountOf(Payment::class) * $this->percentage / 100
        );
    }

    public function direction(): Direction
    {
        return Direction::Debit;
    }
}
```

### Override Protection

[](#override-protection)

- **Lock a value** -- hardcode it in the method, not the constructor.
- **Allow override** -- accept it as a constructor parameter.

```
// Hardcoded: always 2000, not overridable
final class EvidenceFee extends BalanceItem
{
    public function amount(): int { return 2000; }
    public function direction(): Direction { return Direction::Debit; }
}

// Overridable: amount comes from outside (auto-resolved from constructor)
final class Payment extends BalanceItem
{
    public function __construct(private readonly int $amount) {}
    public function direction(): Direction { return Direction::Credit; }
}
```

### Multi-Currency Items

[](#multi-currency-items)

Override `currency()` and provide an exchange rate:

```
#[Unique]
final class DisputeFee extends BalanceItem
{
    public function __construct(
        private readonly float $exchangeRate,
        private readonly Direction $direction = Direction::Debit,
    ) {}

    public function amount(): int { return 2000; }
    public function direction(): Direction { return $this->direction; }
    public function exchangeRate(): float { return $this->exchangeRate; }
    public function currency(): ?string { return 'EUR'; }
}
```

When an item's currency differs from the balance currency, an exchange rate other than `1.0` is required -- otherwise `save()` throws `MissingExchangeRateException`.

Attributes
----------

[](#attributes)

### `#[Rule(...)]`

[](#rule)

Laravel validation rules for constructor parameters. Validated on `insert()`. Supports all Laravel rules including cross-field rules like `required_without`.

```
public function __construct(
    #[Rule('required', 'integer', 'min:1')]
    private readonly int $amount,
) {}
```

### `#[Unique]`

[](#unique)

One instance per reference morph. Inserting a duplicate throws `DuplicateBalanceItemException`. Checked against in-memory and DB items.

```
#[Unique]
final class GatewayFee extends BalanceItem { ... }
```

### `#[Requires(ClassName::class)]`

[](#requiresclassnameclass)

Enforces dependency on another balance item. Repeatable. Checked against in-memory and DB items.

```
#[Requires(EvidenceFee::class)]
final class EvidenceFeeReversal extends BalanceItem { ... }
```

Metadata
--------

[](#metadata)

Attach free-form data to any balance item. Stored as JSON, restored when loading from DB.

```
// Public property
$payment = new Payment(amount: 5000);
$payment->metadata = ['stripe_charge_id' => 'ch_123', 'source' => 'api'];

// Fluent setter
$balance->insert(
    (new Payment(amount: 5000))->withMetadata(['stripe_charge_id' => 'ch_123'])
);
```

Reading metadata back:

```
$balance = Balance::for($account)->reference($order)->get();
$balance->items()->first()->metadata; // ['stripe_charge_id' => 'ch_123', ...]
```

Batch Operations
----------------

[](#batch-operations)

Every `save()` groups items under a single `batch_id` (UUID).

```
$balance->save();
$batchId = $balance->batchId();

// Delete an entire batch
$deleted = Balance::dropBatch($batchId); // returns number of deleted rows
```

Reading from DB
---------------

[](#reading-from-db)

```
$balance = Balance::for($account)->reference($order)->get();

$balance->items();                    // Collection
$balance->has(Payment::class);        // true
$balance->amountOf(Payment::class);   // 10000
```

Items loaded from DB are read-only `BalanceItem` instances with `saved = true`.

Uniqueness Scoping
------------------

[](#uniqueness-scoping)

Uniqueness is scoped per **reference morph**:

```
// Different references -- OK
Balance::for($account)->reference($order1)->insert(new GatewayFee(amount: 290));
Balance::for($account)->reference($order2)->insert(new GatewayFee(amount: 145));

// Same reference -- DuplicateBalanceItemException
Balance::for($account)->reference($order1)->insert(new GatewayFee(amount: 290));
Balance::for($account)->reference($order1)->insert(new GatewayFee(amount: 145)); // throws
```

Real-World Examples
-------------------

[](#real-world-examples)

### Stripe Payment

[](#stripe-payment)

```
// Stripe provides exact fee amounts via API
$balance = Balance::for($account)->reference($order)->currency('USD');

$balance->insert((new Payment(amount: 10000))->withMetadata(['charge_id' => 'ch_xxx']));
$balance->insert(new GatewayFee(amount: 290));
$balance->insert(new GatewayFeeTax(amount: 52));
$balance->insert(new SalesTax(taxRate: 8.0));

$balance->save();
```

### TrustPayment (Rate-Based)

[](#trustpayment-rate-based)

```
// TrustPayment charges percentage-based fees
$balance = Balance::for($account)->reference($order)->currency('USD');

$balance->insert(new Payment(amount: 10000));
$balance->insert(new GatewayFee(percentage: 2.9));      // ceil(10000 * 2.9%) = 290
$balance->insert(new GatewayFeeTax(taxRate: 20.0));      // ceil(290 * 20%) = 58

$balance->save();
```

### Refund

[](#refund)

```
$balance = Balance::for($account)->reference($order)->currency('USD');

$balance->insert(new Refund(amount: 5000)); // requires Payment to exist in balance

$balance->save();
```

### Dispute with Evidence Fee

[](#dispute-with-evidence-fee)

```
// Evidence submission
$b1 = Balance::for($account)->reference($order)->currency('USD');
$b1->insert(new EvidenceFee);     // always 2000
$b1->insert(new EvidenceFeeTax);  // always 400, requires EvidenceFee
$b1->save();

// Dispute won -- reverse the evidence fee
$b2 = Balance::for($account)->reference($order)->currency('USD');
$b2->insert(new EvidenceFeeReversal); // requires EvidenceFee in DB
$b2->save();
```

API Reference
-------------

[](#api-reference)

### `Balance`

[](#balance)

MethodDescription`Balance::for(Model $balanceable)`Create a new balance instance`->reference(Model $ref)`Scope to a reference morph`->currency(string $currency)`Set the balance currency (ISO 4217)`->insert(BalanceItem $item)`Add an item to the batch`->save()`Persist unsaved items in a DB transaction`->batchId()`Get the batch UUID`->get()`Load saved items from DB`->items()`All items (saved + unsaved)`->saved()`Only persisted items`->unsaved()`Only pending items`->has(string $class)`Check if an item key exists`->amountOf(string $class)`Get amount of a specific item`Balance::dropBatch(string $id)`Delete all transactions in a batch### `BalanceItem`

[](#balanceitem)

MethodDefaultOverride`key()`FQCNOptional`amount()`Auto-resolved from `$amount` propertyOverride for derived/fixed amounts`direction()`Auto-resolved from `$direction` propertyOverride to hardcode direction`currency()``null` (uses balance currency)Optional`exchangeRate()``1.0`Optional`withMetadata(?array $data)`Fluent setter--### Exceptions

[](#exceptions)

ExceptionWhen`MissingCurrencyException`No currency on balance or item`MissingExchangeRateException`Different currency but rate is 1.0`DuplicateBalanceItemException``#[Unique]` item already exists`MissingRequiredBalanceItemException``#[Requires]` dependency missing`BatchAlreadySavedException``save()` or `insert()` after commit`EmptyBatchException``save()` with no unsaved items`BalanceItemNotFoundException``amountOf()` for missing key`BalanceNotSetException``balance()` called before `insert()`Currency
--------

[](#currency)

Cashflow uses plain ISO 4217 strings (`'USD'`, `'EUR'`, `'TRY'`) for currency codes. No currency enum is shipped -- use whichever currency library fits your project.

For validation, [squirephp/currencies-en](https://github.com/squirephp/currencies-en) works well:

```
composer require squirephp/currencies-en
```

```
use Squire\Models\Currency;

// Look up currency data
Currency::find('USD')->name;   // "US Dollar"
Currency::find('USD')->symbol; // "$"
```

Database Schema
---------------

[](#database-schema)

ColumnTypeNotesidbigint (PK)auto-incrementbatch\_iduuidindexed, groups itemsbalanceable\_idbigintmorph FKbalanceable\_typestringmorph typereference\_idbigint/nullmorph FKreference\_typestring/nullmorph typedirectionstring`debit` / `credit`amountbigint unsignedminor units (cents)currencystring(3)ISO 4217exchange\_ratedecimal(12,6)default 1.0keystringFQCN by defaultmetadatajson/nullfree-form datacreated\_attimestampauto-setConfiguration
-------------

[](#configuration)

```
// config/cashflow.php
return [
    'model' => \Laratusk\Cashflow\Models\BalanceTransaction::class,
];
```

Override the model to customize the table name, casts, or add behavior.

License
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 85% of packages

Maintenance83

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 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

Every ~8 days

Total

4

Last Release

85d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/755245aa5e4ba6c690b039cedcce5a86cd01b4f00d490cd71f03e6377ac302d5?d=identicon)[laratusk](/maintainers/laratusk)

---

Top Contributors

[![azer1ghost](https://avatars.githubusercontent.com/u/27803185?v=4)](https://github.com/azer1ghost "azer1ghost (7 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/laratusk-cashflow/health.svg)

```
[![Health](https://phpackages.com/badges/laratusk-cashflow/health.svg)](https://phpackages.com/packages/laratusk-cashflow)
```

###  Alternatives

[yajra/laravel-oci8

Oracle DB driver for Laravel via OCI8

8723.1M23](/packages/yajra-laravel-oci8)[kirschbaum-development/eloquent-power-joins

The Laravel magic applied to joins.

1.6k29.9M42](/packages/kirschbaum-development-eloquent-power-joins)[watson/validating

Eloquent model validating trait.

9733.4M53](/packages/watson-validating)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[glushkovds/phpclickhouse-laravel

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

2051.4M2](/packages/glushkovds-phpclickhouse-laravel)[aedart/athenaeum

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

245.2k](/packages/aedart-athenaeum)

PHPackages © 2026

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