https://github.com/whilesmartphp/eloquent-holdings
Polymorphic holdings register (crypto, stocks, property, any asset) for Laravel: track quantity and value, with pluggable live pricing.
https://github.com/whilesmartphp/eloquent-holdings
Last synced: about 17 hours ago
JSON representation
Polymorphic holdings register (crypto, stocks, property, any asset) for Laravel: track quantity and value, with pluggable live pricing.
- Host: GitHub
- URL: https://github.com/whilesmartphp/eloquent-holdings
- Owner: whilesmartphp
- License: mit
- Created: 2026-06-27T11:02:26.000Z (about 1 month ago)
- Default Branch: dev
- Last Pushed: 2026-07-15T21:53:14.000Z (12 days ago)
- Last Synced: 2026-07-15T23:25:50.032Z (12 days ago)
- Language: PHP
- Size: 23.4 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# whilesmart/eloquent-holdings
A polymorphic holdings register for Laravel: one model for anything you own and want to value, whether crypto, stocks, property, or collectibles. Each holding is scoped to an owner (a workspace, organization, user) through [`whilesmart/eloquent-owner-access`](https://github.com/whilesmartphp/eloquent-owner-access), carries a quantity and a unit price, and can be priced by hand or refreshed automatically from a price provider you bind.
The package never hard codes a price source. It stores an opaque `provider` and `external_ref`, and asks a host-bound `HoldingPriceProvider` to resolve prices, so a CoinGecko, stock, or any other feed lives in your app, not here.
## Install
```
composer require whilesmart/eloquent-holdings
php artisan migrate
```
Routes register automatically under the `api` prefix with `auth:sanctum`. Set `HOLDINGS_REGISTER_ROUTES=false` to mount them yourself.
## Owning model
Add the trait to whatever owns holdings:
```php
use Whilesmart\Holdings\Traits\HasHoldings;
class Workspace extends Model
{
use HasHoldings;
}
// Manual: you maintain the price
$workspace->holdings()->create([
'name' => 'Rental flat',
'quantity' => 1,
'currency' => 'USD',
'unit_price' => 120000,
]);
// Auto: refreshed from a bound provider
$workspace->holdings()->create([
'name' => 'Bitcoin',
'symbol' => 'BTC',
'quantity' => 0.5,
'currency' => 'USD',
'price_source' => 'auto',
'provider' => 'coingecko',
'external_ref' => 'bitcoin',
]);
```
`$holding->value` returns `quantity * unit_price` in the holding's currency.
## Live pricing
Implement `HoldingPriceProvider` in your app and bind it. The package groups auto-priced holdings by `(provider, currency)` and asks for current prices:
```php
use Whilesmart\Holdings\Contracts\HoldingPriceProvider;
class CoingeckoPriceProvider implements HoldingPriceProvider
{
public function prices(string $provider, array $externalRefs, string $currency): array
{
if ($provider !== 'coingecko') {
return [];
}
// ...call CoinGecko, return [externalRef => price] in $currency
}
}
// AppServiceProvider::register()
$this->app->bind(HoldingPriceProvider::class, CoingeckoPriceProvider::class);
```
Refresh prices via the command (schedule it as you like) or the API:
```
php artisan holdings:reprice
POST /api/holdings/reprice
```
A `HoldingsRepriced` event carries the ids of the holdings that changed, so the host can invalidate caches or recompute net worth.
## Authorization
Records are owner-scoped. Bind an `OwnerAuthorizer` (see `whilesmart/eloquent-owner-access`) to decide who can see and modify each owner's holdings. Without one, the permissive default applies.
## Endpoints
| Method | Path | Action |
|---|---|---|
| GET | `/api/holdings` | List (owner-scoped; filter by `owner_type`/`owner_id`, `price_source`, `q`) |
| POST | `/api/holdings` | Create |
| GET | `/api/holdings/{holding}` | Show |
| PUT/PATCH | `/api/holdings/{holding}` | Update |
| DELETE | `/api/holdings/{holding}` | Delete (soft) |
| POST | `/api/holdings/reprice` | Refresh auto-priced holdings |
## Development
```
make bootstrap # one-time: pull the package make targets out of the image
make test # run the test suite in a container
make lint # pint --test
```