Compare commits

...

48 Commits

Author SHA1 Message Date
hackerESQ 219018b1d9 Merge pull request #59 from investbrainapp/docker-permissions
fix: ensure storage path permissions are set in entry script
2025-01-29 22:54:08 -06:00
hackerESQ 4b780fd6d2 fix: ensure storage path permissions are set in entry script 2025-01-29 22:53:32 -06:00
hackerESQ 1faa22897b clean up 2025-01-28 21:16:51 -06:00
hackerESQ 7e1899d8ff fix: adds view only icon 2025-01-28 21:15:29 -06:00
hackerESQ 878c668696 Merge branch 'multi-curr-support' into main 2025-01-28 20:34:59 -06:00
hackerESQ 8c94fbf299 fix: ensure failed exists() is boolean 2025-01-28 20:33:28 -06:00
hackerESQ 4ece09368e fix: upgrade the exists() market data provider method 2025-01-28 20:32:43 -06:00
hackerESQ 0f135f4024 fix: gracefully fail if symbol not found 2025-01-28 19:48:20 -06:00
hackerESQ eac5de0d4a fix: adds appropriate return types 2025-01-28 19:46:37 -06:00
hackerESQ 399858d09b fix: strongly type symbol for market data and quote 2025-01-28 19:35:15 -06:00
hackerESQ 7694d8a241 Create 2025_12_28_000001_add_multi_currency_support.php 2025-01-28 19:34:49 -06:00
hackerESQ 9bd406c5b1 fix: adds appropriate return types 2025-01-28 19:03:06 -06:00
hackerESQ d23d28afd8 feat: prepare for api token capabilities 2025-01-28 18:20:03 -06:00
hackerESQ 0a6b2d844f fix: hide terms on self hosted 2025-01-28 18:06:03 -06:00
hackerESQ be325d31b6 chore: lint 2025-01-28 17:46:59 -06:00
hackerESQ e08c1880c6 chore: update deps 2025-01-28 17:46:42 -06:00
hackerESQ 5f9f6f01c5 fix: re-format token color 2025-01-28 17:41:05 -06:00
hackerESQ 65388238c3 fix: allow carbon as date for qty validation 2025-01-28 17:39:08 -06:00
hackerESQ cdce46b6df chore: add script type rule to pint 2025-01-28 17:33:54 -06:00
hackerESQ 8320b54332 fix: require valid email for import and api tokens 2025-01-28 17:28:01 -06:00
hackerESQ e8ef0921ad chore: code style 2025-01-28 17:14:49 -06:00
hackerESQ c4736fae70 chore: cleanup 2025-01-28 13:31:39 -06:00
hackerESQ 1748f49ee6 fix: remove test route 2025-01-28 13:31:02 -06:00
hackerESQ c32641ec34 fix: use requested symbol name for market data providers 2025-01-28 13:30:12 -06:00
hackerESQ 53ebe28b14 fix: makes portfolio available to form request 2025-01-27 23:08:23 -06:00
hackerESQ 465686dbaf fix: simplify the portfolio_id verification 2025-01-27 23:03:33 -06:00
hackerESQ 58604c1e5a chore: update deps 2025-01-27 22:44:03 -06:00
hackerESQ 3e4f055a4a trim size of market data seeder 2025-01-27 20:54:23 -06:00
hackerESQ 92586d7466 fix: update icon for api tokens 2025-01-27 20:44:28 -06:00
hackerESQ 94c90b8a7c fix: add menu item to create api token 2025-01-27 20:42:51 -06:00
hackerESQ f866baa37a chore: update deps 2025-01-27 20:34:16 -06:00
hackerESQ da72c17cd0 Merge pull request #56 from investbrainapp/api-wip
feat: Add Investbrain API capabilities
2025-01-27 20:32:29 -06:00
hackerESQ 1c5c4af477 Merge branch 'main' into api-wip 2025-01-27 20:29:21 -06:00
hackerESQ 83d5ad213b wip 2025-01-27 20:26:09 -06:00
hackerESQ ea22c27710 wip 2025-01-27 20:04:03 -06:00
hackerESQ 32bf256c84 chore: update deps 2025-01-27 16:47:25 -06:00
hackerESQ e498e7668e docs: clean up install guide 2025-01-27 16:41:49 -06:00
hackerESQ f58fbf9d6d docs: clean up install guide 2025-01-27 16:41:40 -06:00
hackerESQ 5e56c97bf9 docs: fix typo 2025-01-27 15:28:16 -06:00
hackerESQ ea4602abc7 wip 2025-01-26 22:56:05 -06:00
hackerESQ 169eabd800 fix: disable api for 1p packages 2025-01-25 16:57:35 -06:00
hackerESQ 62dcae48bb wip 2025-01-24 22:45:28 -06:00
hackerESQ b8f24d4b67 add filter-models for api controllers 2025-01-24 19:29:15 -06:00
hackerESQ 6d9e0008b8 wip 2025-01-24 19:24:16 -06:00
hackerESQ b9d41f9ac0 chore: clean up unneeded attributes 2025-01-24 19:18:27 -06:00
hackerESQ f724f450f2 fix: make default for currency values not nullable 2025-01-24 19:17:55 -06:00
hackerESQ cc447c5fb0 fix: force boolean columns to be false 2025-01-24 19:15:28 -06:00
hackerESQ b3f0f89d16 wip 2025-01-23 22:47:16 -06:00
178 changed files with 2805 additions and 36387 deletions
+9 -7
View File
@@ -38,21 +38,23 @@ Before getting started, you should already have [Docker Engine](https://docs.doc
Ready? Let's get started! Ready? Let's get started!
**1. Copy Docker Compose file** **1. Download copy of Docker Compose file**
Grab a copy of the [docker-compose.yml](https://github.com/investbrainapp/investbrain/blob/main/docker-compose.yml)** file and paste the contents into the directory where you plan to install Investbrain. Grab a copy of the [docker-compose.yml](https://github.com/investbrainapp/investbrain/blob/main/docker-compose.yml) using `wget`, `curl` or similar:
```bash
curl -O https://raw.githubusercontent.com/investbrainapp/investbrain/main/docker-compose.yml
```
**2. Set your environment** **2. Set your environment**
Adjust the `environment` properties in the Docker Compose file to your preferences. Alternatively, create a .env file in the same directory as your compose file, then reference the .env file using the `env_file` property. Adjust the `environment` properties in the compose file to your preferences.
_Importantly_, you need to set the `APP_KEY` value to a complex random value. If you're unsure, Investbrain will generate an `APP_KEY` for you on first run - but you must **manually** update your environment configuration with this generated value! **Importantly**, you need to set the `APP_KEY` value. If you're unsure, Investbrain will generate an `APP_KEY` for you on first run, but it will not persist. You must _manually_ update your environment configuration with this generated value!
> Tip: Want to know what other configuration options are available? You can reference the [.env.example](https://github.com/investbrainapp/investbrain/blob/main/.env.example) file in this respository for available environment configurations.
**3. Run `docker compose up`** **3. Run `docker compose up`**
This might take a few minutes to pull the Docker images. But assuming everything worked as expected, you should now be able to access Investbrain in the browser by visiting: It might take a few minutes to pull the Docker images. But assuming everything worked as expected, you should now be able to access Investbrain in the browser by visiting:
```bash ```bash
http://localhost:8000/register http://localhost:8000/register
+3 -1
View File
@@ -1,13 +1,15 @@
<?php <?php
declare(strict_types=1);
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Models\User; use App\Models\User;
use App\Traits\WithTrimStrings; use App\Traits\WithTrimStrings;
use Laravel\Jetstream\Jetstream;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers; use Laravel\Fortify\Contracts\CreatesNewUsers;
use Laravel\Jetstream\Jetstream;
class CreateNewUser implements CreatesNewUsers class CreateNewUser implements CreatesNewUsers
{ {
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use Illuminate\Validation\Rules\Password; use Illuminate\Validation\Rules\Password;
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Models\User; use App\Models\User;
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Models\User; use App\Models\User;
@@ -1,18 +1,20 @@
<?php <?php
declare(strict_types=1);
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Models\User; use App\Models\User;
use App\Traits\WithTrimStrings; use App\Traits\WithTrimStrings;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Validator;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation; use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{ {
use WithTrimStrings; use WithTrimStrings;
/** /**
* Validate and update the given user's profile information. * Validate and update the given user's profile information.
* *
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Actions\Jetstream; namespace App\Actions\Jetstream;
use App\Models\User; use App\Models\User;
+6 -4
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Portfolio; use App\Models\Portfolio;
@@ -38,9 +40,9 @@ class CaptureDailyChange extends Command
*/ */
public function handle() public function handle()
{ {
Portfolio::with('holdings.market_data')->get()->each(function($portfolio){ Portfolio::with('holdings.market_data')->get()->each(function ($portfolio) {
$this->line('Capturing daily change for ' . $portfolio->title); $this->line('Capturing daily change for '.$portfolio->title);
$total_cost_basis = $portfolio->holdings->sum('total_cost_basis'); $total_cost_basis = $portfolio->holdings->sum('total_cost_basis');
@@ -48,7 +50,7 @@ class CaptureDailyChange extends Command
$realized_gains = $portfolio->holdings->sum('realized_gain_dollars'); $realized_gains = $portfolio->holdings->sum('realized_gain_dollars');
$total_market_value = $portfolio->holdings->sum(function($holding) { $total_market_value = $portfolio->holdings->sum(function ($holding) {
return $holding->market_data->market_value * $holding->quantity; return $holding->market_data->market_value * $holding->quantity;
}); });
@@ -58,7 +60,7 @@ class CaptureDailyChange extends Command
'total_cost_basis' => $total_cost_basis, 'total_cost_basis' => $total_cost_basis,
'total_gain' => $total_market_value - $total_cost_basis, 'total_gain' => $total_market_value - $total_cost_basis,
'total_dividends_earned' => $total_dividends, 'total_dividends_earned' => $total_dividends,
'realized_gains' => $realized_gains 'realized_gains' => $realized_gains,
]); ]);
}); });
} }
+7 -5
View File
@@ -1,9 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Holding;
use App\Models\Dividend; use App\Models\Dividend;
use App\Models\Holding;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class RefreshDividendData extends Command class RefreshDividendData extends Command
@@ -43,17 +45,17 @@ class RefreshDividendData extends Command
{ {
$holdings = Holding::distinct(); $holdings = Holding::distinct();
if (!($this->option('force') ?? false)) { if (! ($this->option('force') ?? false)) {
$holdings->where('quantity', '>', 0); $holdings->where('quantity', '>', 0);
} }
if ($this->option('user')) { if ($this->option('user')) {
$holdings->myHoldings($this->option('user')); $holdings->myHoldings($this->option('user'));
} }
foreach ($holdings->get(['symbol']) as $holding) { foreach ($holdings->get(['symbol']) as $holding) {
$this->line('Refreshing ' . $holding->symbol); $this->line('Refreshing '.$holding->symbol);
Dividend::refreshDividendData($holding->symbol); Dividend::refreshDividendData($holding->symbol);
} }
} }
+13 -6
View File
@@ -1,10 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Holding; use App\Models\Holding;
use App\Models\MarketData; use App\Models\MarketData;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class RefreshMarketData extends Command class RefreshMarketData extends Command
{ {
@@ -42,20 +45,24 @@ class RefreshMarketData extends Command
public function handle() public function handle()
{ {
$force = $this->option('force') ?? false; $force = $this->option('force') ?? false;
// get all symbols from market data // get all symbols from market data
$holdings = Holding::where('quantity', '>', 0) $holdings = Holding::where('quantity', '>', 0)
->select(['symbol']) ->select(['symbol'])
->distinct(); ->distinct();
if ($this->option('user')) { if ($this->option('user')) {
$holdings->myHoldings($this->option('user')); $holdings->myHoldings($this->option('user'));
} }
foreach ($holdings->get() as $holding) { foreach ($holdings->get() as $holding) {
$this->line('Refreshing ' . $holding->symbol); $this->line('Refreshing '.$holding->symbol);
MarketData::getMarketData($holding->symbol, $force); try {
MarketData::getMarketData($holding->symbol, $force);
} catch (\Throwable $e) {
Log::error('Could not refresh '.$holding->symbol.' ('.$e->getMessage().')');
}
} }
} }
} }
+8 -6
View File
@@ -1,9 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Split;
use App\Models\Holding; use App\Models\Holding;
use App\Models\Split;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class RefreshSplitData extends Command class RefreshSplitData extends Command
@@ -42,14 +44,14 @@ class RefreshSplitData extends Command
{ {
$holdings = Holding::distinct(); $holdings = Holding::distinct();
if (!($this->option('force') ?? false)) { if (! ($this->option('force') ?? false)) {
$holdings->where('quantity', '>', 0); $holdings->where('quantity', '>', 0);
} }
foreach ($holdings->get(['symbol']) as $holding) { foreach ($holdings->get(['symbol']) as $holding) {
$this->line('Refreshing ' . $holding->symbol); $this->line('Refreshing '.$holding->symbol);
Split::refreshSplitData($holding->symbol); Split::refreshSplitData($holding->symbol);
} }
} }
} }
+5 -2
View File
@@ -1,10 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Portfolio; use App\Models\Portfolio;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput; use Illuminate\Contracts\Console\PromptsForMissingInput;
use function Laravel\Prompts\search; use function Laravel\Prompts\search;
class SyncDailyChange extends Command implements PromptsForMissingInput class SyncDailyChange extends Command implements PromptsForMissingInput
@@ -61,14 +64,14 @@ class SyncDailyChange extends Command implements PromptsForMissingInput
public function handle() public function handle()
{ {
try { try {
$portfolio = Portfolio::findOrFail($this->argument('portfolio_id')); $portfolio = Portfolio::findOrFail($this->argument('portfolio_id'));
$this->line('Syncing daily change history... This may take a moment.'); $this->line('Syncing daily change history... This may take a moment.');
$portfolio->syncDailyChanges(); $portfolio->syncDailyChanges();
$this->line('Awesome! Daily change history for '. $portfolio->title .' has been completed.'); $this->line('Awesome! Daily change history for '.$portfolio->title.' has been completed.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
+3 -1
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Holding; use App\Models\Holding;
@@ -47,7 +49,7 @@ class SyncHoldingData extends Command
} }
foreach ($holdings->get() as $holding) { foreach ($holdings->get() as $holding) {
$this->line('Refreshing ' . $holding->symbol); $this->line('Refreshing '.$holding->symbol);
$holding->syncTransactionsAndDividends(); $holding->syncTransactionsAndDividends();
} }
+8 -10
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Exports; namespace App\Exports;
use App\Exports\Sheets\DailyChangesSheet; use App\Exports\Sheets\DailyChangesSheet;
@@ -14,18 +16,14 @@ class BackupExport implements WithMultipleSheets
public function __construct( public function __construct(
public bool $empty = false public bool $empty = false
) ) {}
{ }
/**
* @return array
*/
public function sheets(): array public function sheets(): array
{ {
return [ return [
new PortfoliosSheet($this->empty), new PortfoliosSheet($this->empty),
new TransactionsSheet($this->empty), new TransactionsSheet($this->empty),
new DailyChangesSheet($this->empty) new DailyChangesSheet($this->empty),
]; ];
} }
} }
+6 -7
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Exports\Sheets; namespace App\Exports\Sheets;
use App\Models\DailyChange; use App\Models\DailyChange;
@@ -11,7 +13,7 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle
{ {
public function __construct( public function __construct(
public bool $empty = false public bool $empty = false
) { } ) {}
public function headings(): array public function headings(): array
{ {
@@ -23,21 +25,18 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle
'Total Gain', 'Total Gain',
'Total Dividends Earned', 'Total Dividends Earned',
'Realized Gains', 'Realized Gains',
'Annotation' 'Annotation',
]; ];
} }
/** /**
* @return \Illuminate\Support\Collection * @return \Illuminate\Support\Collection
*/ */
public function collection() public function collection()
{ {
return $this->empty ? collect() : DailyChange::myDailyChanges()->get(); return $this->empty ? collect() : DailyChange::myDailyChanges()->get();
} }
/**
* @return string
*/
public function title(): string public function title(): string
{ {
return 'Daily Changes'; return 'Daily Changes';
+7 -8
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Exports\Sheets; namespace App\Exports\Sheets;
use App\Models\Portfolio; use App\Models\Portfolio;
@@ -11,8 +13,8 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle
{ {
public function __construct( public function __construct(
public bool $empty = false public bool $empty = false
) { } ) {}
public function headings(): array public function headings(): array
{ {
return [ return [
@@ -21,21 +23,18 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle
'Notes', 'Notes',
'Wishlist', 'Wishlist',
'Created', 'Created',
'Updated' 'Updated',
]; ];
} }
/** /**
* @return \Illuminate\Support\Collection * @return \Illuminate\Support\Collection
*/ */
public function collection() public function collection()
{ {
return $this->empty ? collect() : Portfolio::myPortfolios()->get(); return $this->empty ? collect() : Portfolio::myPortfolios()->get();
} }
/**
* @return string
*/
public function title(): string public function title(): string
{ {
return 'Portfolios'; return 'Portfolios';
+8 -9
View File
@@ -1,17 +1,19 @@
<?php <?php
declare(strict_types=1);
namespace App\Exports\Sheets; namespace App\Exports\Sheets;
use App\Models\Transaction; use App\Models\Transaction;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\FromCollection; use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithTitle;
class TransactionsSheet implements FromCollection, WithHeadings, WithTitle class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
{ {
public function __construct( public function __construct(
public bool $empty = false public bool $empty = false
) { } ) {}
public function headings(): array public function headings(): array
{ {
@@ -27,21 +29,18 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
'Reinvested Dividend', 'Reinvested Dividend',
'Date', 'Date',
'Created', 'Created',
'Updated' 'Updated',
]; ];
} }
/** /**
* @return \Illuminate\Support\Collection * @return \Illuminate\Support\Collection
*/ */
public function collection() public function collection()
{ {
return $this->empty ? collect() : Transaction::myTransactions()->get(); return $this->empty ? collect() : Transaction::myTransactions()->get();
} }
/**
* @return string
*/
public function title(): string public function title(): string
{ {
return 'Transactions'; return 'Transactions';
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Http\ApiControllers;
abstract class Controller
{
//
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Http\ApiControllers;
use App\Http\ApiControllers\Controller as ApiController;
use App\Http\Requests\HoldingRequest;
use App\Http\Resources\HoldingResource;
use App\Models\Holding;
use App\Models\Portfolio;
use HackerEsq\FilterModels\FilterModels;
use Illuminate\Support\Facades\Gate;
class HoldingController extends ApiController
{
public function index(FilterModels $filters)
{
$filters->setQuery(Holding::query());
$filters->setScopes(['myHoldings']);
$filters->setEagerRelations(['market_data', 'transactions']);
$filters->setSearchableColumns(['symbol']);
return HoldingResource::collection($filters->paginated());
}
public function show(Portfolio $portfolio, string $symbol)
{
Gate::authorize('readOnly', $portfolio);
$holding = $portfolio->holdings()->symbol($symbol)->firstOrFail();
return HoldingResource::make($holding);
}
public function update(HoldingRequest $request, Portfolio $portfolio, string $symbol)
{
Gate::authorize('fullAccess', $portfolio);
$holding = $portfolio->holdings()->symbol($symbol)->firstOrFail();
$holding->update($request->validated());
return HoldingResource::make($holding);
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Http\ApiControllers;
use App\Http\ApiControllers\Controller as ApiController;
use App\Http\Resources\MarketDataResource;
use App\Models\MarketData;
use Illuminate\Http\Request;
class MarketDataController extends ApiController
{
public function show(Request $request, string $symbol)
{
try {
return MarketDataResource::make(
MarketData::getMarketData($symbol)
);
} catch (\Throwable $e) {
return response([
'message' => 'Symbol '.$symbol.' not found.',
], 404);
}
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Http\ApiControllers;
use App\Http\ApiControllers\Controller as ApiController;
use App\Http\Requests\PortfolioRequest;
use App\Http\Resources\PortfolioResource;
use App\Models\Portfolio;
use HackerEsq\FilterModels\FilterModels;
use Illuminate\Support\Facades\Gate;
class PortfolioController extends ApiController
{
public function index(FilterModels $filters)
{
$filters->setQuery(Portfolio::query());
$filters->setScopes(['myPortfolios']);
$filters->setEagerRelations(['users', 'transactions', 'holdings']);
$filters->setFilterableRelations(['holdings.symbol']);
$filters->setSearchableColumns(['title', 'notes']);
return PortfolioResource::collection($filters->paginated());
}
public function store(PortfolioRequest $request)
{
$portfolio = Portfolio::create($request->validated());
return PortfolioResource::make($portfolio);
}
public function show(Portfolio $portfolio)
{
Gate::authorize('readOnly', $portfolio);
return PortfolioResource::make($portfolio);
}
public function update(PortfolioRequest $request, Portfolio $portfolio)
{
Gate::authorize('fullAccess', $portfolio);
$portfolio->update($request->validated());
return PortfolioResource::make($portfolio);
}
public function destroy(Portfolio $portfolio)
{
Gate::authorize('fullAccess', $portfolio);
$portfolio->delete();
return response()->noContent();
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Http\ApiControllers;
use App\Http\ApiControllers\Controller as ApiController;
use App\Http\Requests\TransactionRequest;
use App\Http\Resources\TransactionResource;
use App\Models\Transaction;
use HackerEsq\FilterModels\FilterModels;
use Illuminate\Support\Facades\Gate;
class TransactionController extends ApiController
{
public function index(FilterModels $filters)
{
$filters->setQuery(Transaction::query());
$filters->setScopes(['myTransactions']);
$filters->setSearchableColumns(['symbol']);
return TransactionResource::collection($filters->paginated());
}
public function store(TransactionRequest $request)
{
Gate::authorize('fullAccess', $request->portfolio);
$transaction = Transaction::create($request->validated());
return TransactionResource::make($transaction);
}
public function show(Transaction $transaction)
{
Gate::authorize('readOnly', $transaction->portfolio);
return TransactionResource::make($transaction);
}
public function update(TransactionRequest $request, Transaction $transaction)
{
Gate::authorize('fullAccess', $transaction->portfolio);
$transaction->update($request->validated());
return TransactionResource::make($transaction);
}
public function destroy(Transaction $transaction)
{
Gate::authorize('fullAccess', $transaction->portfolio);
$transaction->delete();
return response()->noContent();
}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Http\ApiControllers;
use App\Http\ApiControllers\Controller as ApiController;
use App\Http\Resources\UserResource;
use Illuminate\Http\Request;
class UserController extends ApiController
{
public function me(Request $request)
{
return UserResource::make($request->user());
}
}
@@ -1,23 +1,22 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Exception;
use App\Models\User;
use App\Models\ConnectedAccount; use App\Models\ConnectedAccount;
use Illuminate\Support\MessageBag; use App\Models\User;
use App\Http\Controllers\Controller; use App\Notifications\VerifyConnectedAccountNotification;
use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\MessageBag;
use Laravel\Socialite\Facades\Socialite; use Laravel\Socialite\Facades\Socialite;
use App\Notifications\VerifyConnectedAccountNotification;
class ConnectedAccountController extends Controller class ConnectedAccountController extends Controller
{ {
/** /**
* Redirect the user to the GitHub authentication page. * Redirect the user to the GitHub authentication page.
*
*/ */
public function redirectToProvider(string $provider) public function redirectToProvider(string $provider)
{ {
@@ -28,7 +27,6 @@ class ConnectedAccountController extends Controller
/** /**
* Obtain the user information from GitHub. * Obtain the user information from GitHub.
*
*/ */
public function handleProviderCallback(string $provider) public function handleProviderCallback(string $provider)
{ {
@@ -45,21 +43,21 @@ class ConnectedAccountController extends Controller
} }
// check if this account is already linked // check if this account is already linked
$connected_account = ConnectedAccount::firstOrNew([ $connected_account = ConnectedAccount::firstOrNew([
'provider' => $provider, 'provider' => $provider,
'provider_id' => $providerUser->id 'provider_id' => $providerUser->id,
], [ ], [
'token' => $providerUser->token, 'token' => $providerUser->token,
'secret' => $providerUser->tokenSecret, 'secret' => $providerUser->tokenSecret,
'refresh_token' => $providerUser->refreshToken, 'refresh_token' => $providerUser->refreshToken,
'expires_at' => $providerUser->expiresIn, 'expires_at' => $providerUser->expiresIn,
'verified_at' => false 'verified_at' => false,
]); ]);
// already linked and verified, let's go login! // already linked and verified, let's go login!
if ( if (
$connected_account->exists $connected_account->exists
&& !is_null($connected_account->verified_at) && ! is_null($connected_account->verified_at)
) { ) {
Auth::login($connected_account->user, true); Auth::login($connected_account->user, true);
@@ -68,20 +66,20 @@ class ConnectedAccountController extends Controller
} }
// new user, let's create one // new user, let's create one
if (!$user = User::where('email', $providerUser->email)->first()) { if (! $user = User::where('email', $providerUser->email)->first()) {
$user = User::create([ $user = User::create([
'name' => $providerUser->name, 'name' => $providerUser->name,
'email' => $providerUser->email, 'email' => $providerUser->email,
'email_verified_at' => now() 'email_verified_at' => now(),
]); ]);
$connected_account->user_id = $user->id; $connected_account->user_id = $user->id;
$connected_account->verified_at = now(); $connected_account->verified_at = now();
$connected_account->save(); $connected_account->save();
Auth::login($user, true); Auth::login($user, true);
return redirect(route('dashboard')); return redirect(route('dashboard'));
} }
@@ -92,23 +90,23 @@ class ConnectedAccountController extends Controller
$user->notify(new VerifyConnectedAccountNotification($connected_account->id)); $user->notify(new VerifyConnectedAccountNotification($connected_account->id));
return redirect(route('login')) return redirect(route('login'))
->with('status', __( ->with('status', __(
'Account already exists. Check your email to connect your :provider account.', 'Account already exists. Check your email to connect your :provider account.',
['provider' => config("services.$provider.name")] ['provider' => config("services.$provider.name")]
)); ));
} }
protected function validateProvider($provider): void protected function validateProvider($provider): void
{ {
if (!in_array($provider, explode(',', config('services.enabled_login_providers')))) { if (! in_array($provider, explode(',', config('services.enabled_login_providers')))) {
throw new Exception('Please provide a valid social provider.'); throw new Exception('Please provide a valid social provider.');
} }
} }
public function verify(ConnectedAccount $connected_account) public function verify(ConnectedAccount $connected_account)
{ {
if (!$connected_account->verified_at) { if (! $connected_account->verified_at) {
// mark request as verified // mark request as verified
$connected_account->verified_at = now(); $connected_account->verified_at = now();
@@ -128,8 +126,8 @@ class ConnectedAccountController extends Controller
'css' => 'alert-success', 'css' => 'alert-success',
'icon' => Blade::render("<x-mary-icon class='w-7 h-7' name='o-check-circle' />"), 'icon' => Blade::render("<x-mary-icon class='w-7 h-7' name='o-check-circle' />"),
'position' => 'toast-top toast-end', 'position' => 'toast-top toast-end',
'timeout' => '5000' 'timeout' => '5000',
] ],
])); ]));
} }
} }
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
abstract class Controller abstract class Controller
+8 -6
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Holding; use App\Models\Holding;
@@ -16,15 +18,15 @@ class DashboardController extends Controller
// get portfolio metrics // get portfolio metrics
$metrics = cache()->remember( $metrics = cache()->remember(
'dashboard-metrics-' . $user->id, 'dashboard-metrics-'.$user->id,
10, 10,
function () { function () {
return return
Holding::query() Holding::query()
->myHoldings() ->myHoldings()
->withoutWishlists() ->withoutWishlists()
->withPortfolioMetrics() ->withPortfolioMetrics()
->first(); ->first();
} }
); );
+11 -10
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Holding; use App\Models\Holding;
@@ -8,21 +10,20 @@ use Illuminate\Http\Request;
class HoldingController extends Controller class HoldingController extends Controller
{ {
/** /**
* Display the specified resource. * Display the specified resource.
*/ */
public function show(Request $request, Portfolio $portfolio, String $symbol) public function show(Request $request, Portfolio $portfolio, string $symbol)
{ {
$holding = Holding::with([ $holding = Holding::with([
'market_data', 'market_data',
'transactions' => function ($query) use ($symbol) { 'transactions' => function ($query) use ($symbol) {
$query->where('transactions.symbol', $symbol); $query->where('transactions.symbol', $symbol);
} },
]) ])
->symbol($symbol) ->symbol($symbol)
->portfolio($portfolio->id) ->portfolio($portfolio->id)
->firstOrFail(); ->firstOrFail();
$formattedTransactions = $holding->getFormattedTransactions(); $formattedTransactions = $holding->getFormattedTransactions();
@@ -1,23 +1,22 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Portfolio; use App\Models\Portfolio;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class InvitedOnboardingController extends Controller class InvitedOnboardingController extends Controller
{ {
/** /**
* Check if the invited user needs a password? * Check if the invited user needs a password?
*
*/ */
public function __invoke(Request $request, Portfolio $portfolio, User $user) public function __invoke(Request $request, Portfolio $portfolio, User $user)
{ {
if (!$request->hasValidSignature()) { if (! $request->hasValidSignature()) {
abort(401, 'Invalid signature'); abort(401, 'Invalid signature');
} }
@@ -27,7 +26,7 @@ class InvitedOnboardingController extends Controller
// route to create password form // route to create password form
return view('auth.invited-onboarding', [ return view('auth.invited-onboarding', [
'portfolio' => $portfolio, 'portfolio' => $portfolio,
'user' => $user 'user' => $user,
]); ]);
} }
+11 -11
View File
@@ -1,14 +1,16 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Holding; use App\Models\Holding;
use App\Models\Portfolio; use App\Models\Portfolio;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PortfolioController extends Controller class PortfolioController extends Controller
{ {
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
*/ */
@@ -22,26 +24,24 @@ class PortfolioController extends Controller
*/ */
public function show(Request $request, Portfolio $portfolio) public function show(Request $request, Portfolio $portfolio)
{ {
if ($request->user()->cannot('readOnly', $portfolio)) { Gate::authorize('readOnly', $portfolio);
abort(403);
}
$portfolio->load(['transactions', 'holdings']); $portfolio->load(['transactions', 'holdings']);
// get portfolio metrics // get portfolio metrics
$metrics = cache()->remember( $metrics = cache()->remember(
'portfolio-metrics-' . $portfolio->id, 'portfolio-metrics-'.$portfolio->id,
60, 60,
function () use ($portfolio) { function () use ($portfolio) {
return Holding::query() return Holding::query()
->portfolio($portfolio->id) ->portfolio($portfolio->id)
->withPortfolioMetrics() ->withPortfolioMetrics()
->first(); ->first();
} }
); );
$formattedHoldings = $portfolio->getFormattedHoldings(); $formattedHoldings = $portfolio->getFormattedHoldings();
return view('portfolio.show', compact(['portfolio', 'metrics', 'formattedHoldings'])); return view('portfolio.show', compact(['portfolio', 'metrics', 'formattedHoldings']));
} }
} }
@@ -1,10 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
class TransactionController extends Controller class TransactionController extends Controller
{ {
/** /**
* Display the specified resource. * Display the specified resource.
*/ */
+4 -2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Closure; use Closure;
@@ -14,7 +16,7 @@ class SetLocale
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if (!session()->has('locale')) { if (! session()->has('locale')) {
session()->put('locale', $request->getPreferredLanguage( session()->put('locale', $request->getPreferredLanguage(
config('app.available_locales') config('app.available_locales')
)); ));
@@ -24,4 +26,4 @@ class SetLocale
return $next($request); return $next($request);
} }
} }
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;
class FormRequest extends BaseFormRequest
{
public function requestOrModelValue($key, $model): mixed
{
return $this->request->get($key) ?? $this->{$model}?->{$key};
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
class HoldingRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$rules = [
'reinvest_dividends' => ['sometimes', 'boolean'],
];
return $rules;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
class PortfolioRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$rules = [
'title' => ['required', 'string', 'min:5', 'max:255'],
'notes' => ['sometimes', 'nullable', 'string'],
'wishlist' => ['sometimes', 'nullable', 'boolean'],
];
if (! is_null($this->portfolio)) {
$rules['title'][0] = 'sometimes';
}
return $rules;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use App\Models\Portfolio;
use App\Rules\QuantityValidationRule;
use App\Rules\SymbolValidationRule;
class TransactionRequest extends FormRequest
{
protected function prepareForValidation(): void
{
$this->merge([
'portfolio' => Portfolio::find($this->requestOrModelValue('portfolio_id', 'transaction')),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$rules = [
'portfolio_id' => ['required', 'exists:portfolios,id'],
'symbol' => ['required', 'string', new SymbolValidationRule],
'transaction_type' => ['required', 'string', 'in:BUY,SELL'],
'date' => ['required', 'date_format:Y-m-d', 'before_or_equal:'.now()->format('Y-m-d')],
'quantity' => [
'required',
'numeric',
'min:0',
new QuantityValidationRule(
$this->input('portfolio'),
$this->requestOrModelValue('symbol', 'transaction'),
$this->requestOrModelValue('transaction_type', 'transaction'),
$this->requestOrModelValue('date', 'transaction')
),
],
'cost_basis' => ['exclude_if:transaction_type,SELL', 'min:0', 'numeric'],
'sale_price' => ['exclude_if:transaction_type,BUY', 'min:0', 'numeric'],
];
if (! is_null($this->transaction)) {
$rules['portfolio_id'][0] = 'sometimes';
$rules['symbol'][0] = 'sometimes';
$rules['transaction_type'][0] = 'sometimes';
$rules['date'][0] = 'sometimes';
$rules['quantity'][0] = 'sometimes';
if (
$this->requestOrModelValue('transaction_type', 'transaction') == 'SELL'
&& $this->requestOrModelValue('sale_price', 'transaction') == null
) {
$rules['sale_price'][0] = 'required';
} elseif (
$this->requestOrModelValue('transaction_type', 'transaction') == 'BUY'
&& $this->requestOrModelValue('cost_basis', 'transaction') == null
) {
$rules['cost_basis'][0] = 'required';
}
}
return $rules;
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class HoldingResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol,
'quantity' => $this->quantity,
'reinvest_dividends' => $this->reinvest_dividends,
'average_cost_basis' => $this->average_cost_basis,
'total_cost_basis' => $this->total_cost_basis,
'realized_gain_dollars' => $this->realized_gain_dollars,
'dividends_earned' => $this->dividends_earned,
'splits_synced_at' => $this->splits_synced_at,
'total_market_value' => $this->total_market_value,
'market_gain_dollars' => $this->market_gain_dollars,
'market_gain_percent' => $this->market_gain_percent,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class MarketDataResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'symbol' => $this->symbol,
'name' => $this->name,
'market_value' => $this->market_value,
'fifty_two_week_low' => $this->fifty_two_week_low,
'fifty_two_week_high' => $this->fifty_two_week_high,
'last_dividend_date' => $this->last_dividend_date,
'last_dividend_amount' => $this->last_dividend_amount,
'dividend_yield' => $this->dividend_yield,
'market_cap' => $this->market_cap,
'trailing_pe' => $this->trailing_pe,
'forward_pe' => $this->forward_pe,
'book_value' => $this->book_value,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PortfolioResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'notes' => $this->notes,
'wishlist' => $this->wishlist,
'owner' => UserResource::make($this->owner),
'transactions' => TransactionResource::collection($this->whenLoaded('transactions')),
'holdings' => HoldingResource::collection($this->whenLoaded('holdings')),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TransactionResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'symbol' => $this->symbol,
'portfolio_id' => $this->portfolio_id,
'transaction_type' => $this->transaction_type,
'quantity' => $this->quantity,
'cost_basis' => $this->cost_basis,
'sale_price' => $this->sale_price,
'split' => $this->split,
'reinvested_dividend' => $this->reinvested_dividend,
'date' => $this->date,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'profile_photo_url' => $this->profile_photo_url,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
+22 -24
View File
@@ -1,40 +1,38 @@
<?php <?php
declare(strict_types=1);
namespace App\Imports; namespace App\Imports;
use App\Models\User; use App\Console\Commands\RefreshDividendData;
use App\Imports\Sheets\PortfoliosSheet; use App\Console\Commands\RefreshMarketData;
use Illuminate\Support\Facades\Artisan;
use App\Console\Commands\SyncDailyChange; use App\Console\Commands\SyncDailyChange;
use App\Console\Commands\SyncHoldingData; use App\Console\Commands\SyncHoldingData;
use App\Imports\Sheets\DailyChangesSheet; use App\Imports\Sheets\DailyChangesSheet;
use App\Imports\Sheets\PortfoliosSheet;
use App\Imports\Sheets\TransactionsSheet; use App\Imports\Sheets\TransactionsSheet;
use Maatwebsite\Excel\Events\AfterImport; use App\Models\BackupImport as BackupImportModel;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Maatwebsite\Excel\Concerns\Importable; use Maatwebsite\Excel\Concerns\Importable;
use Maatwebsite\Excel\Concerns\WithEvents; use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Events\AfterImport;
use Maatwebsite\Excel\Events\BeforeImport; use Maatwebsite\Excel\Events\BeforeImport;
use Maatwebsite\Excel\Events\ImportFailed; use Maatwebsite\Excel\Events\ImportFailed;
use App\Console\Commands\RefreshMarketData;
use App\Console\Commands\RefreshDividendData;
use App\Models\BackupImport as BackupImportModel;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class BackupImport implements WithMultipleSheets, WithEvents class BackupImport implements WithEvents, WithMultipleSheets
{ {
use Importable; use Importable;
public function __construct( public function __construct(
public BackupImportModel $backupImportModel public BackupImportModel $backupImportModel
) { } ) {}
/**
* @return array
*/
public function registerEvents(): array public function registerEvents(): array
{ {
return [ return [
BeforeImport::class => fn() => $this->backupImportModel->update([ BeforeImport::class => fn () => $this->backupImportModel->update([
'status' => 'in_progress', 'status' => 'in_progress',
'message' => __('Import is in progress...'), 'message' => __('Import is in progress...'),
]), ]),
@@ -43,24 +41,24 @@ class BackupImport implements WithMultipleSheets, WithEvents
$this->backupImportModel->update([ $this->backupImportModel->update([
'status' => 'success', 'status' => 'success',
'message' => 'Import completed successfully!', 'message' => 'Import completed successfully!',
'completed_at' => now() 'completed_at' => now(),
]); ]);
Artisan::queue(RefreshMarketData::class, ['--user' => $this->backupImportModel->user_id, '--force' => true]) Artisan::queue(RefreshMarketData::class, ['--user' => $this->backupImportModel->user_id, '--force' => true])
->chain([ ->chain([
fn() => Artisan::call(RefreshDividendData::class, ['--user' => $this->backupImportModel->user_id, '--force' => true]), fn () => Artisan::call(RefreshDividendData::class, ['--user' => $this->backupImportModel->user_id, '--force' => true]),
fn() => Artisan::call(SyncHoldingData::class, ['--user' => $this->backupImportModel->user_id]), fn () => Artisan::call(SyncHoldingData::class, ['--user' => $this->backupImportModel->user_id]),
fn() => User::find($this->backupImportModel->user_id)->portfolios->each(function($portfolio) { fn () => User::find($this->backupImportModel->user_id)->portfolios->each(function ($portfolio) {
Artisan::queue(SyncDailyChange::class, ['portfolio_id' => $portfolio->id]); Artisan::queue(SyncDailyChange::class, ['portfolio_id' => $portfolio->id]);
}) }),
]); ]);
}, },
ImportFailed::class => fn(ImportFailed $event) => $this->backupImportModel->update([ ImportFailed::class => fn (ImportFailed $event) => $this->backupImportModel->update([
'status' => 'failed', 'status' => 'failed',
'message' => 'Error: '. substr($event->getException()->getMessage(), 0, 220), 'message' => 'Error: '.substr($event->getException()->getMessage(), 0, 220),
'has_errors' => true, 'has_errors' => true,
'completed_at' => now() 'completed_at' => now(),
]), ]),
]; ];
} }
+14 -15
View File
@@ -1,44 +1,43 @@
<?php <?php
declare(strict_types=1);
namespace App\Imports\Sheets; namespace App\Imports\Sheets;
use App\Imports\ValidatesPortfolioAccess; use App\Imports\ValidatesPortfolioAccess;
use App\Models\DailyChange;
use App\Models\BackupImport; use App\Models\BackupImport;
use App\Models\DailyChange;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Events\BeforeSheet;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\SkipsEmptyRows; use Maatwebsite\Excel\Concerns\SkipsEmptyRows;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadingRow; use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithValidation; use Maatwebsite\Excel\Concerns\WithValidation;
use Maatwebsite\Excel\Events\BeforeSheet;
class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, SkipsEmptyRows, WithEvents class DailyChangesSheet implements SkipsEmptyRows, ToCollection, WithEvents, WithHeadingRow, WithValidation
{ {
use ValidatesPortfolioAccess; use ValidatesPortfolioAccess;
public function __construct( public function __construct(
public BackupImport $backupImport public BackupImport $backupImport
) { } ) {}
/**
* @return array
*/
public function registerEvents(): array public function registerEvents(): array
{ {
return [ return [
BeforeSheet::class => function(BeforeSheet $event) { BeforeSheet::class => function (BeforeSheet $event) {
DB::commit(); DB::commit();
$this->backupImport->update([ $this->backupImport->update([
'message' => __('Importing daily changes...'), 'message' => __('Importing daily changes...'),
]); ]);
DB::beginTransaction(); DB::beginTransaction();
} },
]; ];
} }
public function collection(Collection $dailyChanges) public function collection(Collection $dailyChanges)
{ {
$dailyChanges->chunk($this->batchSize())->each(function ($chunk) { $dailyChanges->chunk($this->batchSize())->each(function ($chunk) {
@@ -56,7 +55,7 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation,
'realized_gains' => $dailyChange['realized_gains'], 'realized_gains' => $dailyChange['realized_gains'],
'annotation' => $dailyChange['annotation'], 'annotation' => $dailyChange['annotation'],
'portfolio_id' => $dailyChange['portfolio_id'], 'portfolio_id' => $dailyChange['portfolio_id'],
'date' => Carbon::parse($dailyChange['date'])->format('Y-m-d') 'date' => Carbon::parse($dailyChange['date'])->format('Y-m-d'),
]; ];
}); });
@@ -71,7 +70,7 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation,
'realized_gains', 'realized_gains',
'annotation', 'annotation',
'portfolio_id', 'portfolio_id',
'date' 'date',
] ]
); );
}); });
@@ -85,7 +84,7 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation,
public function rules(): array public function rules(): array
{ {
return [ return [
'portfolio_id' => ['required', 'uuid'], 'portfolio_id' => ['required', 'uuid'],
'date' => ['required', 'date'], 'date' => ['required', 'date'],
'total_market_value' => ['sometimes', 'nullable', 'numeric'], 'total_market_value' => ['sometimes', 'nullable', 'numeric'],
'total_cost_basis' => ['sometimes', 'nullable', 'min:0', 'numeric'], 'total_cost_basis' => ['sometimes', 'nullable', 'min:0', 'numeric'],
+12 -13
View File
@@ -1,37 +1,36 @@
<?php <?php
declare(strict_types=1);
namespace App\Imports\Sheets; namespace App\Imports\Sheets;
use App\Models\Portfolio;
use App\Models\BackupImport; use App\Models\BackupImport;
use App\Models\Portfolio;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Events\BeforeSheet;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\SkipsEmptyRows; use Maatwebsite\Excel\Concerns\SkipsEmptyRows;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadingRow; use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithValidation; use Maatwebsite\Excel\Concerns\WithValidation;
use Maatwebsite\Excel\Events\BeforeSheet;
class PortfoliosSheet implements ToCollection, WithValidation, WithHeadingRow, SkipsEmptyRows, WithEvents class PortfoliosSheet implements SkipsEmptyRows, ToCollection, WithEvents, WithHeadingRow, WithValidation
{ {
public function __construct( public function __construct(
public BackupImport $backupImport public BackupImport $backupImport
) { } ) {}
/**
* @return array
*/
public function registerEvents(): array public function registerEvents(): array
{ {
return [ return [
BeforeSheet::class => function(BeforeSheet $event) { BeforeSheet::class => function (BeforeSheet $event) {
DB::commit(); DB::commit();
$this->backupImport->update([ $this->backupImport->update([
'message' => __('Importing portfolios...'), 'message' => __('Importing portfolios...'),
]); ]);
DB::beginTransaction(); DB::beginTransaction();
} },
]; ];
} }
@@ -42,7 +41,7 @@ class PortfoliosSheet implements ToCollection, WithValidation, WithHeadingRow, S
Portfolio::unguard(); // ensures we can set an owner for the portfolio Portfolio::unguard(); // ensures we can set an owner for the portfolio
$portfolio = Portfolio::fullAccess($this->backupImport->user_id)->updateOrCreate([ $portfolio = Portfolio::fullAccess($this->backupImport->user_id)->updateOrCreate([
'id' => $portfolio['portfolio_id'] 'id' => $portfolio['portfolio_id'],
], [ ], [
'id' => $portfolio['portfolio_id'] ?? null, 'id' => $portfolio['portfolio_id'] ?? null,
'title' => $portfolio['title'], 'title' => $portfolio['title'],
+25 -27
View File
@@ -1,44 +1,42 @@
<?php <?php
declare(strict_types=1);
namespace App\Imports\Sheets; namespace App\Imports\Sheets;
use App\Imports\ValidatesPortfolioAccess; use App\Imports\ValidatesPortfolioAccess;
use App\Models\BackupImport;
use App\Models\Holding; use App\Models\Holding;
use App\Models\Transaction; use App\Models\Transaction;
use Illuminate\Support\Str;
use App\Models\BackupImport;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Events\BeforeSheet; use Illuminate\Support\Str;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\SkipsEmptyRows; use Maatwebsite\Excel\Concerns\SkipsEmptyRows;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadingRow; use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithValidation; use Maatwebsite\Excel\Concerns\WithValidation;
use Maatwebsite\Excel\Events\BeforeSheet;
class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation, SkipsEmptyRows, WithEvents class TransactionsSheet implements SkipsEmptyRows, ToCollection, WithEvents, WithHeadingRow, WithValidation
{ {
use ValidatesPortfolioAccess; use ValidatesPortfolioAccess;
public function __construct( public function __construct(
public BackupImport $backupImport public BackupImport $backupImport
) { } ) {}
/**
* @return array
*/
public function registerEvents(): array public function registerEvents(): array
{ {
return [ return [
BeforeSheet::class => function(BeforeSheet $event) { BeforeSheet::class => function (BeforeSheet $event) {
DB::commit(); DB::commit();
$this->backupImport->update([ $this->backupImport->update([
'message' => __('Importing transactions...'), 'message' => __('Importing transactions...'),
]); ]);
DB::beginTransaction(); DB::beginTransaction();
} },
]; ];
} }
@@ -62,7 +60,7 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation,
'sale_price' => $transaction['sale_price'], 'sale_price' => $transaction['sale_price'],
'split' => boolval($transaction['split']) ? 1 : 0, 'split' => boolval($transaction['split']) ? 1 : 0,
'reinvested_dividend' => boolval($transaction['reinvested_dividend']) ? 1 : 0, 'reinvested_dividend' => boolval($transaction['reinvested_dividend']) ? 1 : 0,
'date' => Carbon::parse($transaction['date'])->format('Y-m-d') 'date' => Carbon::parse($transaction['date'])->format('Y-m-d'),
]; ];
}); });
@@ -79,23 +77,23 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation,
'sale_price', 'sale_price',
'split', 'split',
'reinvested_dividend', 'reinvested_dividend',
'date' 'date',
] ]
); );
// stub out related holdings // stub out related holdings
$chunk->unique(fn($item) => $item['symbol'] . $item['portfolio_id']) $chunk->unique(fn ($item) => $item['symbol'].$item['portfolio_id'])
->each(function($holding) { ->each(function ($holding) {
Holding::firstOrCreate([ Holding::firstOrCreate([
'symbol' => $holding['symbol'], 'symbol' => $holding['symbol'],
'portfolio_id' => $holding['portfolio_id'] 'portfolio_id' => $holding['portfolio_id'],
], [ ], [
'quantity' => 0, 'quantity' => 0,
'average_cost_basis' => 0, 'average_cost_basis' => 0,
'splits_synced_at' => now(), 'splits_synced_at' => now(),
]); ]);
}); });
}); });
} }
+5 -4
View File
@@ -1,24 +1,25 @@
<?php <?php
declare(strict_types=1);
namespace App\Imports; namespace App\Imports;
use App\Models\Portfolio; use App\Models\Portfolio;
trait ValidatesPortfolioAccess trait ValidatesPortfolioAccess
{ {
public function validatePortfolioAccess($collection) public function validatePortfolioAccess($collection)
{ {
$uniquePortfolios = $collection->unique('portfolio_id')->pluck('portfolio_id'); $uniquePortfolios = $collection->unique('portfolio_id')->pluck('portfolio_id');
$countPortfoliosWithAccess = Portfolio::fullAccess($this->backupImport->user_id) $countPortfoliosWithAccess = Portfolio::fullAccess($this->backupImport->user_id)
->whereIn('id', $uniquePortfolios) ->whereIn('id', $uniquePortfolios)
->count(); ->count();
if ( if (
$countPortfoliosWithAccess < $uniquePortfolios->count() $countPortfoliosWithAccess < $uniquePortfolios->count()
) { ) {
throw new \Exception(__("You do not have access to that portfolio.")); throw new \Exception(__('You do not have access to that portfolio.'));
} }
} }
} }
@@ -1,34 +1,34 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData; namespace App\Interfaces\MarketData;
use App\Interfaces\MarketData\Types\Dividend;
use App\Interfaces\MarketData\Types\Ohlc;
use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Split;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Split;
use App\Interfaces\MarketData\Types\Dividend;
use App\Interfaces\MarketData\Types\Ohlc;
use Tschucki\Alphavantage\Facades\Alphavantage; use Tschucki\Alphavantage\Facades\Alphavantage;
class AlphaVantageMarketData implements MarketDataInterface class AlphaVantageMarketData implements MarketDataInterface
{ {
public function exists(String $symbol): Bool public function exists(string $symbol): bool
{ {
return $this->quote($symbol)->isNotEmpty(); return (bool) $this->quote($symbol);
} }
public function quote(String $symbol): Quote public function quote(string $symbol): Quote
{ {
$quote = Alphavantage::core()->quoteEndpoint($symbol); $quote = Alphavantage::core()->quoteEndpoint($symbol);
$quote = Arr::get($quote, 'Global Quote', []); $quote = Arr::get($quote, 'Global Quote', []);
if (empty($quote)) return new Quote();
$fundamental = cache()->remember( $fundamental = cache()->remember(
'av-symbol-'.$symbol, 'av-symbol-'.$symbol,
1440, 1440,
function () use ($symbol) { function () use ($symbol) {
return Alphavantage::fundamentals()->overview($symbol); return Alphavantage::fundamentals()->overview($symbol);
} }
@@ -36,7 +36,7 @@ class AlphaVantageMarketData implements MarketDataInterface
return new Quote([ return new Quote([
'name' => Arr::get($fundamental, 'Name'), 'name' => Arr::get($fundamental, 'Name'),
'symbol' => Arr::get($fundamental, 'Symbol'), 'symbol' => $symbol,
'market_value' => Arr::get($quote, '05. price'), 'market_value' => Arr::get($quote, '05. price'),
'fifty_two_week_high' => Arr::get($fundamental, '52WeekHigh'), 'fifty_two_week_high' => Arr::get($fundamental, '52WeekHigh'),
'fifty_two_week_low' => Arr::get($fundamental, '52WeekLow'), 'fifty_two_week_low' => Arr::get($fundamental, '52WeekLow'),
@@ -49,71 +49,71 @@ class AlphaVantageMarketData implements MarketDataInterface
: null, : null,
'dividend_yield' => Arr::get($fundamental, 'DividendYield') != 'None' 'dividend_yield' => Arr::get($fundamental, 'DividendYield') != 'None'
? Arr::get($fundamental, 'DividendYield') ? Arr::get($fundamental, 'DividendYield')
: null : null,
]); ]);
} }
public function dividends(String $symbol, $startDate, $endDate): Collection public function dividends(string $symbol, $startDate, $endDate): Collection
{ {
$dividends = Alphavantage::fundamentals()->dividends($symbol); $dividends = Alphavantage::fundamentals()->dividends($symbol);
$dividends = Arr::get($dividends, 'data', []); $dividends = Arr::get($dividends, 'data', []);
return collect($dividends) return collect($dividends)
->filter(function($dividend) use ($startDate, $endDate) { ->filter(function ($dividend) use ($startDate, $endDate) {
return Carbon::parse(Arr::get($dividend, 'ex_dividend_date'))->between($startDate, $endDate); return Carbon::parse(Arr::get($dividend, 'ex_dividend_date'))->between($startDate, $endDate);
}) })
->map(function($dividend) use ($symbol) { ->map(function ($dividend) use ($symbol) {
return new Dividend([ return new Dividend([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => Carbon::parse(Arr::get($dividend, 'ex_dividend_date')), 'date' => Carbon::parse(Arr::get($dividend, 'ex_dividend_date')),
'dividend_amount' => Arr::get($dividend, 'amount'), 'dividend_amount' => Arr::get($dividend, 'amount'),
]); ]);
}); });
} }
public function splits(String $symbol, $startDate, $endDate): Collection public function splits(string $symbol, $startDate, $endDate): Collection
{ {
$splits = Alphavantage::fundamentals()->splits($symbol); $splits = Alphavantage::fundamentals()->splits($symbol);
$splits = Arr::get($splits, 'data', []); $splits = Arr::get($splits, 'data', []);
return collect($splits) return collect($splits)
->filter(function($split) use ($startDate, $endDate) { ->filter(function ($split) use ($startDate, $endDate) {
return Carbon::parse(Arr::get($split, 'effective_date'))->between($startDate, $endDate); return Carbon::parse(Arr::get($split, 'effective_date'))->between($startDate, $endDate);
}) })
->map(function($split) use ($symbol) { ->map(function ($split) use ($symbol) {
return new Split([ return new Split([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => Carbon::parse(Arr::get($split, 'effective_date')), 'date' => Carbon::parse(Arr::get($split, 'effective_date')),
'split_amount' => Arr::get($split, 'split_factor'), 'split_amount' => Arr::get($split, 'split_factor'),
]); ]);
}); });
} }
public function history(String $symbol, $startDate, $endDate): Collection public function history(string $symbol, $startDate, $endDate): Collection
{ {
$history = Alphavantage::timeSeries()->daily($symbol, 'full'); $history = Alphavantage::timeSeries()->daily($symbol, 'full');
$history = Arr::get($history, 'Time Series (Daily)', []); $history = Arr::get($history, 'Time Series (Daily)', []);
return collect($history) return collect($history)
->filter(function ($history, $date) use ($startDate, $endDate) { ->filter(function ($history, $date) use ($startDate, $endDate) {
return Carbon::parse($date)->between($startDate, $endDate); return Carbon::parse($date)->between($startDate, $endDate);
}) })
->mapWithKeys(function($history, $date) use ($symbol) { ->mapWithKeys(function ($history, $date) use ($symbol) {
$date = Carbon::parse($date)->format('Y-m-d'); $date = Carbon::parse($date)->format('Y-m-d');
return [ $date => new Ohlc([ return [$date => new Ohlc([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => $date, 'date' => $date,
'close' => Arr::get($history, '4. close') 'close' => Arr::get($history, '4. close'),
]) ]; ])];
}); });
} }
} }
+16 -14
View File
@@ -1,23 +1,25 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData; namespace App\Interfaces\MarketData;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Dividend; use App\Interfaces\MarketData\Types\Dividend;
use App\Interfaces\MarketData\Types\Ohlc; use App\Interfaces\MarketData\Types\Ohlc;
use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Split; use App\Interfaces\MarketData\Types\Split;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
class FakeMarketData implements MarketDataInterface class FakeMarketData implements MarketDataInterface
{ {
public function exists(String $symbol): Bool public function exists(string $symbol): bool
{ {
return true; return true;
} }
public function quote(String $symbol): Quote public function quote(string $symbol): Quote
{ {
return new Quote([ return new Quote([
@@ -31,11 +33,11 @@ class FakeMarketData implements MarketDataInterface
'market_cap' => 9800700600, 'market_cap' => 9800700600,
'book_value' => 4.7, 'book_value' => 4.7,
'last_dividend_date' => now()->subDays(45), 'last_dividend_date' => now()->subDays(45),
'dividend_yield' => 0.033 'dividend_yield' => 0.033,
]); ]);
} }
public function dividends(String $symbol, $startDate, $endDate): Collection public function dividends(string $symbol, $startDate, $endDate): Collection
{ {
return collect([ return collect([
@@ -57,23 +59,23 @@ class FakeMarketData implements MarketDataInterface
]); ]);
} }
public function splits(String $symbol, $startDate, $endDate): Collection public function splits(string $symbol, $startDate, $endDate): Collection
{ {
return collect([ return collect([
new Split([ new Split([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => now()->subMonths(36), 'date' => now()->subMonths(36),
'split_amount' => 10, 'split_amount' => 10,
]) ]),
]); ]);
} }
public function history(String $symbol, $startDate, $endDate): Collection public function history(string $symbol, $startDate, $endDate): Collection
{ {
$numDays = Carbon::parse($startDate)->diffInDays($endDate, true); $numDays = Carbon::parse($startDate)->diffInDays($endDate, true);
for ($i = 0; $i < $numDays; $i++) { for ($i = 0; $i < $numDays; $i++) {
$date = now()->subDays($i)->format('Y-m-d'); $date = now()->subDays($i)->format('Y-m-d');
@@ -83,7 +85,7 @@ class FakeMarketData implements MarketDataInterface
'close' => rand(150, 400), 'close' => rand(150, 400),
]); ]);
} }
return collect($series); return collect($series);
} }
} }
@@ -1,26 +1,28 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData; namespace App\Interfaces\MarketData;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class FallbackInterface class FallbackInterface
{ {
protected string $latest_error; protected string $latest_error;
public function __call($method, $arguments) public function __call($method, $arguments)
{ {
$providers = explode(',', config('investbrain.provider', 'yahoo')); $providers = explode(',', config('investbrain.provider', 'yahoo'));
foreach ($providers as $provider) { foreach ($providers as $provider) {
$provider = trim($provider); $provider = trim($provider);
try { try {
Log::warning("Calling method {$method} ({$provider})");
if (!in_array($provider, array_keys(config('investbrain.interfaces', [])))) { if (! in_array($provider, array_keys(config('investbrain.interfaces', [])))) {
throw new \Exception("Provider [{$provider}] is not a valid market data interface."); throw new \Exception("Provider [{$provider}] is not a valid market data interface.");
} }
@@ -30,13 +32,20 @@ class FallbackInterface
return app()->make($provider_class_name)->$method(...$arguments); return app()->make($provider_class_name)->$method(...$arguments);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->latest_error = $e->getMessage(); $this->latest_error = $e->getMessage();
Log::warning("Failed calling method {$method} ({$provider}): {$this->latest_error}"); Log::warning("Failed calling method {$method} ({$provider}): {$this->latest_error}");
} }
} }
// don't need to throw error if calling exists
if ($method == 'exists') {
// symbol prob just doesn't exist
return false;
}
throw new \Exception("Could not get market data: {$this->latest_error}"); throw new \Exception("Could not get market data: {$this->latest_error}");
} }
} }
+28 -26
View File
@@ -1,14 +1,16 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData; namespace App\Interfaces\MarketData;
use Illuminate\Support\Arr; use App\Interfaces\MarketData\Types\Dividend;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use App\Interfaces\MarketData\Types\Ohlc; use App\Interfaces\MarketData\Types\Ohlc;
use App\Interfaces\MarketData\Types\Quote; use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Split; use App\Interfaces\MarketData\Types\Split;
use App\Interfaces\MarketData\Types\Dividend; use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
class FinnhubMarketData implements MarketDataInterface class FinnhubMarketData implements MarketDataInterface
{ {
@@ -16,36 +18,35 @@ class FinnhubMarketData implements MarketDataInterface
public function __construct() public function __construct()
{ {
$this->client = new \Finnhub\Api\DefaultApi( $this->client = new \Finnhub\Api\DefaultApi(
new \GuzzleHttp\Client(), new \GuzzleHttp\Client,
\Finnhub\Configuration::getDefaultConfiguration()->setApiKey('token', config('finnhub.key')) \Finnhub\Configuration::getDefaultConfiguration()->setApiKey('token', config('finnhub.key'))
); );
} }
public function exists(String $symbol): Bool
public function exists(string $symbol): bool
{ {
return $this->quote($symbol)->isNotEmpty(); return (bool) $this->quote($symbol);
} }
public function quote(string $symbol): Quote public function quote(string $symbol): Quote
{ {
$quote = $this->client->quote($symbol); $quote = $this->client->quote($symbol);
if (empty($quote)) return new Quote();
$fundamental = cache()->remember( $fundamental = cache()->remember(
'fh-symbol-'.$symbol, 'fh-symbol-'.$symbol,
1440, 1440,
function () use ($symbol) { function () use ($symbol) {
return $this->client->companyBasicFinancials($symbol, "all"); return $this->client->companyBasicFinancials($symbol, 'all');
} }
); );
return new Quote([ return new Quote([
'name' => Arr::get($fundamental, 'metric.name'), 'name' => Arr::get($fundamental, 'metric.name'),
'symbol' => $symbol, 'symbol' => $symbol,
'market_value' => Arr::get($quote, 'c'), 'market_value' => Arr::get($quote, 'c'),
'fifty_two_week_high' => Arr::get($fundamental, 'metric.52WeekHigh'), 'fifty_two_week_high' => Arr::get($fundamental, 'metric.52WeekHigh'),
'fifty_two_week_low' => Arr::get($fundamental, 'metric.52WeekLow'), 'fifty_two_week_low' => Arr::get($fundamental, 'metric.52WeekLow'),
'forward_pe' => Arr::get($fundamental, 'metric.forwardPE'), // confirm 'forward_pe' => Arr::get($fundamental, 'metric.forwardPE'), // confirm
@@ -54,15 +55,15 @@ class FinnhubMarketData implements MarketDataInterface
'book_value' => Arr::get($fundamental, 'metric.bookValuePerShare'), // confirm 'book_value' => Arr::get($fundamental, 'metric.bookValuePerShare'), // confirm
'last_dividend_date' => Arr::get($fundamental, 'metric.lastDivDate'), // confirm 'last_dividend_date' => Arr::get($fundamental, 'metric.lastDivDate'), // confirm
'dividend_yield' => Arr::get($fundamental, 'metric.dividendYield'), // confirm 'dividend_yield' => Arr::get($fundamental, 'metric.dividendYield'), // confirm
]); ]);
} }
public function dividends($symbol, $startDate, $endDate): Collection public function dividends($symbol, $startDate, $endDate): Collection
{ {
$dividends = $this->client->stockDividends($symbol, $startDate->format('Y-m-d'), $endDate->format('Y-m-d')); $dividends = $this->client->stockDividends($symbol, $startDate->format('Y-m-d'), $endDate->format('Y-m-d'));
return collect($dividends)->map(function($dividend) use ($symbol) { return collect($dividends)->map(function ($dividend) use ($symbol) {
return new Dividend([ return new Dividend([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => Carbon::parse(Arr::get($dividend, 'date')), 'date' => Carbon::parse(Arr::get($dividend, 'date')),
@@ -72,12 +73,12 @@ class FinnhubMarketData implements MarketDataInterface
} }
public function splits($symbol, $startDate, $endDate): Collection public function splits($symbol, $startDate, $endDate): Collection
{ {
$splits = $this->client->stockSplits($symbol, $startDate->format('Y-m-d'), $endDate->format('Y-m-d')); $splits = $this->client->stockSplits($symbol, $startDate->format('Y-m-d'), $endDate->format('Y-m-d'));
return collect($splits)->map(function($split) use ($symbol) { return collect($splits)->map(function ($split) use ($symbol) {
return new Split([ return new Split([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => Carbon::parse(Arr::get($split, 'date')), 'date' => Carbon::parse(Arr::get($split, 'date')),
@@ -89,18 +90,19 @@ class FinnhubMarketData implements MarketDataInterface
public function history($symbol, $startDate, $endDate): Collection public function history($symbol, $startDate, $endDate): Collection
{ {
$history = $this->client->stockCandles($symbol, "D", $startDate->timestamp, $endDate->timestamp); $history = $this->client->stockCandles($symbol, 'D', $startDate->timestamp, $endDate->timestamp);
$timestamps = Arr::get($history, 't', []); $timestamps = Arr::get($history, 't', []);
$closes = Arr::get($history, 'c', []); $closes = Arr::get($history, 'c', []);
return collect($timestamps)->mapWithKeys(function ($timestamp, $index) use ($symbol, $closes) { return collect($timestamps)->mapWithKeys(function ($timestamp, $index) use ($symbol, $closes) {
$date = Carbon::createFromTimestamp($timestamp)->format('Y-m-d'); $date = Carbon::createFromTimestamp($timestamp)->format('Y-m-d');
return [ $date => new Ohlc([
return [$date => new Ohlc([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => $date, 'date' => $date,
'close' => $closes[$index], 'close' => $closes[$index],
]) ]; ])];
}); });
} }
} }
@@ -1,60 +1,36 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData; namespace App\Interfaces\MarketData;
use Illuminate\Support\Collection;
use App\Interfaces\MarketData\Types\Quote; use App\Interfaces\MarketData\Types\Quote;
use Illuminate\Support\Collection;
interface MarketDataInterface interface MarketDataInterface
{ {
/** /**
* Does this symbol actually exist? * Does this symbol actually exist?
*
* @param String $symbol
*
* @return Bool
*/ */
public function exists(String $symbol): Bool; public function exists(string $symbol): bool;
/** /**
* Get quote data * Get quote data
*
* @param String $symbol
*
* @return Quote
*/ */
public function quote(String $symbol): Quote; public function quote(string $symbol): Quote;
/** /**
* Get dividend data * Get dividend data
*
* @param String $symbol
* @param \DateTimeInterface $startDate
* @param \DateTimeInterface $endDate
*
* @return Collection
*/ */
public function dividends(String $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection; public function dividends(string $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection;
/** /**
* Get split data * Get split data
*
* @param String $symbol
* @param \DateTimeInterface $startDate
* @param \DateTimeInterface $endDate
*
* @return Collection
*/ */
public function splits(String $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection; public function splits(string $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection;
/** /**
* Get historical close data * Get historical close data
*
* @param String $symbol
* @param \DateTimeInterface $startDate
* @param \DateTimeInterface $endDate
*
* @return Collection
*/ */
public function history(String $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection; public function history(string $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection;
} }
+7 -3
View File
@@ -1,16 +1,18 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData\Types; namespace App\Interfaces\MarketData\Types;
use DateTime; use DateTime;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use App\Interfaces\MarketData\Types\MarketDataType;
class Dividend extends MarketDataType class Dividend extends MarketDataType
{ {
public function setSymbol(string $symbol): self public function setSymbol(string $symbol): self
{ {
$this->items['symbol'] = $symbol; $this->items['symbol'] = $symbol;
return $this; return $this;
} }
@@ -22,6 +24,7 @@ class Dividend extends MarketDataType
public function setDividendAmount($dividendAmount): self public function setDividendAmount($dividendAmount): self
{ {
$this->items['dividend_amount'] = (float) $dividendAmount; $this->items['dividend_amount'] = (float) $dividendAmount;
return $this; return $this;
} }
@@ -30,9 +33,10 @@ class Dividend extends MarketDataType
return $this->items['dividend_amount'] ?? 0.0; return $this->items['dividend_amount'] ?? 0.0;
} }
public function setDate(String|DateTime $date): self public function setDate(string|DateTime $date): self
{ {
$this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s'); $this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s');
return $this; return $this;
} }
@@ -40,4 +44,4 @@ class Dividend extends MarketDataType
{ {
return $this->items['date'] ?? null; return $this->items['date'] ?? null;
} }
} }
@@ -1,19 +1,18 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData\Types; namespace App\Interfaces\MarketData\Types;
use Illuminate\Support\Str;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class MarketDataType extends Collection class MarketDataType extends Collection
{ {
/**
*
*/
public function __construct($items = []) public function __construct($items = [])
{ {
foreach($this->getArrayableItems($items) as $key => $value) { foreach ($this->getArrayableItems($items) as $key => $value) {
$this->{$key} = $value; $this->{$key} = $value;
} }
@@ -33,4 +32,4 @@ class MarketDataType extends Collection
{ {
return $this->items[$key] ?? null; return $this->items[$key] ?? null;
} }
} }
+10 -3
View File
@@ -1,16 +1,18 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData\Types; namespace App\Interfaces\MarketData\Types;
use DateTime; use DateTime;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use App\Interfaces\MarketData\Types\MarketDataType;
class Ohlc extends MarketDataType class Ohlc extends MarketDataType
{ {
public function setSymbol(string $symbol): self public function setSymbol(string $symbol): self
{ {
$this->items['symbol'] = $symbol; $this->items['symbol'] = $symbol;
return $this; return $this;
} }
@@ -22,6 +24,7 @@ class Ohlc extends MarketDataType
public function setOpen($open): self public function setOpen($open): self
{ {
$this->items['open'] = (float) $open; $this->items['open'] = (float) $open;
return $this; return $this;
} }
@@ -33,6 +36,7 @@ class Ohlc extends MarketDataType
public function setHigh($high): self public function setHigh($high): self
{ {
$this->items['high'] = (float) $high; $this->items['high'] = (float) $high;
return $this; return $this;
} }
@@ -44,6 +48,7 @@ class Ohlc extends MarketDataType
public function setLow($low): self public function setLow($low): self
{ {
$this->items['low'] = (float) $low; $this->items['low'] = (float) $low;
return $this; return $this;
} }
@@ -55,6 +60,7 @@ class Ohlc extends MarketDataType
public function setClose($close): self public function setClose($close): self
{ {
$this->items['close'] = (float) $close; $this->items['close'] = (float) $close;
return $this; return $this;
} }
@@ -63,9 +69,10 @@ class Ohlc extends MarketDataType
return $this->items['close'] ?? 0.0; return $this->items['close'] ?? 0.0;
} }
public function setDate(String|DateTime $date): self public function setDate(string|DateTime $date): self
{ {
$this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s'); $this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s');
return $this; return $this;
} }
@@ -73,4 +80,4 @@ class Ohlc extends MarketDataType
{ {
return $this->items['date'] ?? null; return $this->items['date'] ?? null;
} }
} }
+24 -12
View File
@@ -1,16 +1,18 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData\Types; namespace App\Interfaces\MarketData\Types;
use DateTime; use DateTime;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use App\Interfaces\MarketData\Types\MarketDataType;
class Quote extends MarketDataType class Quote extends MarketDataType
{ {
public function setName($name): self public function setName(string $name): self
{ {
$this->items['name'] = (string) $name; $this->items['name'] = (string) $name;
return $this; return $this;
} }
@@ -19,9 +21,10 @@ class Quote extends MarketDataType
return $this->items['name'] ?? ''; return $this->items['name'] ?? '';
} }
public function setSymbol($symbol): self public function setSymbol(string $symbol): self
{ {
$this->items['symbol'] = (string) $symbol; $this->items['symbol'] = (string) $symbol;
return $this; return $this;
} }
@@ -30,9 +33,10 @@ class Quote extends MarketDataType
return $this->items['symbol'] ?? ''; return $this->items['symbol'] ?? '';
} }
public function setMarketValue($marketValue): self public function setMarketValue($marketValue): self
{ {
$this->items['market_value'] = (float) $marketValue; $this->items['market_value'] = (float) $marketValue;
return $this; return $this;
} }
@@ -41,9 +45,10 @@ class Quote extends MarketDataType
return $this->items['market_value'] ?? 0.0; return $this->items['market_value'] ?? 0.0;
} }
public function setFiftyTwoWeekHigh($high): self public function setFiftyTwoWeekHigh($high): self
{ {
$this->items['fifty_two_week_high'] = (float) $high; $this->items['fifty_two_week_high'] = (float) $high;
return $this; return $this;
} }
@@ -52,9 +57,10 @@ class Quote extends MarketDataType
return $this->items['fifty_two_week_high'] ?? 0.0; return $this->items['fifty_two_week_high'] ?? 0.0;
} }
public function setFiftyTwoWeekLow($low): self public function setFiftyTwoWeekLow($low): self
{ {
$this->items['fifty_two_week_low'] = (float) $low; $this->items['fifty_two_week_low'] = (float) $low;
return $this; return $this;
} }
@@ -63,9 +69,10 @@ class Quote extends MarketDataType
return $this->items['fifty_two_week_low'] ?? 0.0; return $this->items['fifty_two_week_low'] ?? 0.0;
} }
public function setForwardPE($pe): self public function setForwardPE($pe): self
{ {
$this->items['forward_pe'] = (float) $pe; $this->items['forward_pe'] = (float) $pe;
return $this; return $this;
} }
@@ -74,9 +81,10 @@ class Quote extends MarketDataType
return $this->items['forward_pe'] ?? 0.0; return $this->items['forward_pe'] ?? 0.0;
} }
public function setTrailingPE($pe): self public function setTrailingPE($pe): self
{ {
$this->items['trailing_pe'] = (float) $pe; $this->items['trailing_pe'] = (float) $pe;
return $this; return $this;
} }
@@ -88,6 +96,7 @@ class Quote extends MarketDataType
public function setMarketCap($cap): self public function setMarketCap($cap): self
{ {
$this->items['market_cap'] = (int) $cap; $this->items['market_cap'] = (int) $cap;
return $this; return $this;
} }
@@ -96,9 +105,10 @@ class Quote extends MarketDataType
return $this->items['market_cap'] ?? 0; return $this->items['market_cap'] ?? 0;
} }
public function setBookValue($value): self public function setBookValue($value): self
{ {
$this->items['book_value'] = (float) $value; $this->items['book_value'] = (float) $value;
return $this; return $this;
} }
@@ -110,6 +120,7 @@ class Quote extends MarketDataType
public function setLastDividendDate(mixed $date): self public function setLastDividendDate(mixed $date): self
{ {
$this->items['last_dividend_date'] = is_null($date) ? null : Carbon::parse($date)->format('Y-m-d H:i:s'); $this->items['last_dividend_date'] = is_null($date) ? null : Carbon::parse($date)->format('Y-m-d H:i:s');
return $this; return $this;
} }
@@ -118,9 +129,10 @@ class Quote extends MarketDataType
return $this->items['last_dividend_date'] ?? null; return $this->items['last_dividend_date'] ?? null;
} }
public function setDividendYield($yield): self public function setDividendYield($yield): self
{ {
$this->items['dividend_yield'] = (float) $yield; $this->items['dividend_yield'] = (float) $yield;
return $this; return $this;
} }
@@ -128,4 +140,4 @@ class Quote extends MarketDataType
{ {
return $this->items['dividend_yield'] ?? 0.0; return $this->items['dividend_yield'] ?? 0.0;
} }
} }
+7 -3
View File
@@ -1,16 +1,18 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData\Types; namespace App\Interfaces\MarketData\Types;
use DateTime; use DateTime;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use App\Interfaces\MarketData\Types\MarketDataType;
class Split extends MarketDataType class Split extends MarketDataType
{ {
public function setSymbol(string $symbol): self public function setSymbol(string $symbol): self
{ {
$this->items['symbol'] = $symbol; $this->items['symbol'] = $symbol;
return $this; return $this;
} }
@@ -22,6 +24,7 @@ class Split extends MarketDataType
public function setSplitAmount($splitAmount): self public function setSplitAmount($splitAmount): self
{ {
$this->items['split_amount'] = (float) $splitAmount; $this->items['split_amount'] = (float) $splitAmount;
return $this; return $this;
} }
@@ -30,9 +33,10 @@ class Split extends MarketDataType
return $this->items['split_amount'] ?? 0.0; return $this->items['split_amount'] ?? 0.0;
} }
public function setDate(String|DateTime $date): self public function setDate(string|DateTime $date): self
{ {
$this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s'); $this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s');
return $this; return $this;
} }
@@ -40,4 +44,4 @@ class Split extends MarketDataType
{ {
return $this->items['date'] ?? null; return $this->items['date'] ?? null;
} }
} }
+48 -47
View File
@@ -1,95 +1,96 @@
<?php <?php
declare(strict_types=1);
namespace App\Interfaces\MarketData; namespace App\Interfaces\MarketData;
use Illuminate\Support\Collection; use App\Interfaces\MarketData\Types\Dividend;
use Scheb\YahooFinanceApi\ApiClient;
use App\Interfaces\MarketData\Types\Ohlc; use App\Interfaces\MarketData\Types\Ohlc;
use App\Interfaces\MarketData\Types\Quote; use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Split; use App\Interfaces\MarketData\Types\Split;
use App\Interfaces\MarketData\Types\Dividend; use Illuminate\Support\Collection;
use Scheb\YahooFinanceApi\ApiClient;
use Scheb\YahooFinanceApi\ApiClientFactory as YahooFinance; use Scheb\YahooFinanceApi\ApiClientFactory as YahooFinance;
class YahooMarketData implements MarketDataInterface class YahooMarketData implements MarketDataInterface
{ {
public ApiClient $client; public ApiClient $client;
public function __construct() { public function __construct()
{
// create yahoo finance client factory // create yahoo finance client factory
$this->client = YahooFinance::createApiClient(); $this->client = YahooFinance::createApiClient();
} }
public function exists(String $symbol): Bool public function exists(string $symbol): bool
{ {
return $this->quote($symbol)->isNotEmpty(); return (bool) $this->quote($symbol);
} }
public function quote(String $symbol): Quote public function quote(string $symbol): Quote
{ {
$quote = $this->client->getQuote($symbol); $quote = $this->client->getQuote($symbol);
if (empty($quote)) return collect();
return new Quote([ return new Quote([
'name' => $quote->getLongName() ?? $quote->getShortName(), 'name' => $quote?->getLongName() ?? $quote?->getShortName(),
'symbol' => $quote->getSymbol(), 'symbol' => $symbol,
'market_value' => $quote->getRegularMarketPrice(), 'market_value' => $quote?->getRegularMarketPrice(),
'fifty_two_week_high' => $quote->getFiftyTwoWeekHigh(), 'fifty_two_week_high' => $quote?->getFiftyTwoWeekHigh(),
'fifty_two_week_low' => $quote->getFiftyTwoWeekLow(), 'fifty_two_week_low' => $quote?->getFiftyTwoWeekLow(),
'forward_pe' => $quote->getForwardPE(), 'forward_pe' => $quote?->getForwardPE(),
'trailing_pe' => $quote->getTrailingPE(), 'trailing_pe' => $quote?->getTrailingPE(),
'market_cap' => $quote->getMarketCap(), 'market_cap' => $quote?->getMarketCap(),
'book_value' => $quote->getBookValue(), 'book_value' => $quote?->getBookValue(),
'last_dividend_date' => $quote->getDividendDate(), 'last_dividend_date' => $quote?->getDividendDate(),
'dividend_yield' => $quote->getTrailingAnnualDividendYield() * 100 'dividend_yield' => $quote?->getTrailingAnnualDividendYield() * 100,
]); ]);
} }
public function dividends(String $symbol, $startDate, $endDate): Collection public function dividends(string $symbol, $startDate, $endDate): Collection
{ {
return collect($this->client->getHistoricalDividendData($symbol, $startDate, $endDate)) return collect($this->client->getHistoricalDividendData($symbol, $startDate, $endDate))
->map(function($dividend) use ($symbol) { ->map(function ($dividend) use ($symbol) {
return new Dividend([ return new Dividend([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => $dividend->getDate(), 'date' => $dividend->getDate(),
'dividend_amount' => $dividend->getDividends(), 'dividend_amount' => $dividend->getDividends(),
]); ]);
}); });
} }
public function splits(String $symbol, $startDate, $endDate): Collection public function splits(string $symbol, $startDate, $endDate): Collection
{ {
return collect($this->client->getHistoricalSplitData($symbol, $startDate, $endDate)) return collect($this->client->getHistoricalSplitData($symbol, $startDate, $endDate))
->map(function($split) use ($symbol) { ->map(function ($split) use ($symbol) {
$split_amount = explode(':', $split->getStockSplits()); $split_amount = explode(':', $split->getStockSplits());
return new Split([ return new Split([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => $split->getDate(), 'date' => $split->getDate(),
'split_amount' => $split_amount[0] / $split_amount[1], 'split_amount' => $split_amount[0] / $split_amount[1],
]); ]);
}); });
} }
public function history(String $symbol, $startDate, $endDate): Collection public function history(string $symbol, $startDate, $endDate): Collection
{ {
return collect($this->client->getHistoricalQuoteData($symbol, ApiClient::INTERVAL_1_DAY, $startDate, $endDate)) return collect($this->client->getHistoricalQuoteData($symbol, ApiClient::INTERVAL_1_DAY, $startDate, $endDate))
->mapWithKeys(function($history) use ($symbol) { ->mapWithKeys(function ($history) use ($symbol) {
$date = $history->getDate()->format('Y-m-d'); $date = $history->getDate()->format('Y-m-d');
return [ $date => new Ohlc([ return [$date => new Ohlc([
'symbol' => $symbol, 'symbol' => $symbol,
'date' => $date, 'date' => $date,
'close' => $history->getClose(), 'close' => $history->getClose(),
]) ]; ])];
}); });
} }
} }
+15 -13
View File
@@ -1,16 +1,18 @@
<?php <?php
declare(strict_types=1);
namespace App\Jobs; namespace App\Jobs;
use Throwable;
use App\Models\User;
use App\Models\BackupImport;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Notifications\ImportSucceededNotification;
use App\Notifications\ImportFailedNotification;
use App\Imports\BackupImport as BackupImportExcel; use App\Imports\BackupImport as BackupImportExcel;
use App\Models\BackupImport;
use App\Models\User;
use App\Notifications\ImportFailedNotification;
use App\Notifications\ImportSucceededNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Maatwebsite\Excel\Facades\Excel;
use Throwable;
class BackupImportJob implements ShouldQueue class BackupImportJob implements ShouldQueue
{ {
@@ -19,7 +21,7 @@ class BackupImportJob implements ShouldQueue
/** /**
* The number of times the job may be attempted. * The number of times the job may be attempted.
*/ */
public $tries = 1; public $tries = 1;
/** /**
* The number of seconds the job can run before timing out. * The number of seconds the job can run before timing out.
@@ -42,7 +44,7 @@ class BackupImportJob implements ShouldQueue
*/ */
public function __construct( public function __construct(
public BackupImport $backupImport public BackupImport $backupImport
) { ) {
$this->user = User::find($this->backupImport->user_id); $this->user = User::find($this->backupImport->user_id);
} }
@@ -50,7 +52,7 @@ class BackupImportJob implements ShouldQueue
* Execute the job. * Execute the job.
*/ */
public function handle(): void public function handle(): void
{ {
Excel::import(new BackupImportExcel($this->backupImport), $this->backupImport->path, config('livewire.temporary_file_upload.disk', null)); Excel::import(new BackupImportExcel($this->backupImport), $this->backupImport->path, config('livewire.temporary_file_upload.disk', null));
$this->user->notify(new ImportSucceededNotification); $this->user->notify(new ImportSucceededNotification);
@@ -63,9 +65,9 @@ class BackupImportJob implements ShouldQueue
{ {
$this->backupImport->update([ $this->backupImport->update([
'status' => 'failed', 'status' => 'failed',
'message' => 'Error: '. substr($e->getMessage(), 0, 220), 'message' => 'Error: '.substr($e->getMessage(), 0, 220),
'has_errors' => true, 'has_errors' => true,
'completed_at' => now() 'completed_at' => now(),
]); ]);
$this->user->notify(new ImportFailedNotification($e->getMessage())); $this->user->notify(new ImportFailedNotification($e->getMessage()));
+6 -3
View File
@@ -1,9 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class AiChat extends Model class AiChat extends Model
{ {
@@ -11,7 +13,7 @@ class AiChat extends Model
protected $fillable = [ protected $fillable = [
'role', 'role',
'content' 'content',
]; ];
protected $hidden = []; protected $hidden = [];
@@ -26,7 +28,8 @@ class AiChat extends Model
}); });
} }
public function user() { public function user()
{
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
+7 -7
View File
@@ -1,12 +1,12 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Database\Eloquent\Model;
use App\Imports\BackupImport as BackupImportExcel;
use App\Jobs\BackupImportJob; use App\Jobs\BackupImportJob;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class BackupImport extends Model class BackupImport extends Model
{ {
@@ -20,7 +20,7 @@ class BackupImport extends Model
'status', // pending, in_progress, success, failed 'status', // pending, in_progress, success, failed
'message', // Import starting, Import is in progress, Importing portfolios, Importing transactions, Importing daily changes, Import completed successfully 'message', // Import starting, Import is in progress, Importing portfolios, Importing transactions, Importing daily changes, Import completed successfully
'has_errors', 'has_errors',
'completed_at' 'completed_at',
]; ];
protected static function boot() protected static function boot()
@@ -32,9 +32,9 @@ class BackupImport extends Model
$import->status = 'pending'; $import->status = 'pending';
$import->message = __('Import starting...'); $import->message = __('Import starting...');
}); });
static::created(function ($import) { static::created(function ($import) {
BackupImportJob::dispatch($import); BackupImportJob::dispatch($import);
}); });
} }
@@ -47,7 +47,7 @@ class BackupImport extends Model
{ {
return [ return [
'has_errors' => 'boolean', 'has_errors' => 'boolean',
'completed_at' => 'datetime' 'completed_at' => 'datetime',
]; ];
} }
} }
+4 -2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasTimestamps; use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
@@ -29,7 +31,7 @@ class ConnectedAccount extends Model
]; ];
protected $with = [ protected $with = [
'user' 'user',
]; ];
/** /**
@@ -52,4 +54,4 @@ class ConnectedAccount extends Model
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
} }
+9 -6
View File
@@ -1,14 +1,16 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Traits\HasCompositePrimaryKey; use App\Traits\HasCompositePrimaryKey;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class DailyChange extends Model class DailyChange extends Model
{ {
use HasFactory, HasCompositePrimaryKey; use HasCompositePrimaryKey, HasFactory;
public $timestamps = false; public $timestamps = false;
@@ -32,13 +34,13 @@ class DailyChange extends Model
protected $casts = [ protected $casts = [
'date' => 'datetime', 'date' => 'datetime',
]; ];
public function scopePortfolio($query, $portfolio) public function scopePortfolio($query, $portfolio)
{ {
return $query->where('portfolio_id', $portfolio); return $query->where('portfolio_id', $portfolio);
} }
public function scopeMyDailyChanges() public function scopeMyDailyChanges()
{ {
return $this->whereHas('portfolio', function ($query) { return $this->whereHas('portfolio', function ($query) {
$query->whereHas('users', function ($query) { $query->whereHas('users', function ($query) {
@@ -47,12 +49,13 @@ class DailyChange extends Model
}); });
} }
public function scopeWithoutWishlists($query) { public function scopeWithoutWishlists($query)
{
return $query->whereHas('portfolio', function ($query) { return $query->whereHas('portfolio', function ($query) {
$query->where('portfolios.wishlist', 0); $query->where('portfolios.wishlist', 0);
}); });
} }
public function portfolio() public function portfolio()
{ {
return $this->belongsTo(Portfolio::class); return $this->belongsTo(Portfolio::class);
+44 -43
View File
@@ -1,16 +1,15 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Models\Holding;
use App\Models\MarketData;
use App\Models\Transaction;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Model;
use App\Interfaces\MarketData\MarketDataInterface; use App\Interfaces\MarketData\MarketDataInterface;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
class Dividend extends Model class Dividend extends Model
{ {
@@ -30,15 +29,18 @@ class Dividend extends Model
'last_dividend_update' => 'datetime', 'last_dividend_update' => 'datetime',
]; ];
public function marketData() { public function marketData()
{
return $this->belongsTo(MarketData::class, 'symbol', 'symbol'); return $this->belongsTo(MarketData::class, 'symbol', 'symbol');
} }
public function holdings() { public function holdings()
{
return $this->hasMany(Holding::class, 'symbol', 'symbol'); return $this->hasMany(Holding::class, 'symbol', 'symbol');
} }
public function transactions() { public function transactions()
{
return $this->hasMany(Transaction::class, 'symbol', 'symbol'); return $this->hasMany(Transaction::class, 'symbol', 'symbol');
} }
@@ -49,7 +51,6 @@ class Dividend extends Model
/** /**
* Grab new dividend data * Grab new dividend data
*
*/ */
public static function refreshDividendData(string $symbol): void public static function refreshDividendData(string $symbol): void
{ {
@@ -64,11 +65,11 @@ class Dividend extends Model
$end_date = now(); $end_date = now();
// nope, refresh forward looking only // nope, refresh forward looking only
if ( $dividends_meta->total_dividends ) { if ($dividends_meta->total_dividends) {
$start_date = $dividends_meta->last_dividend_update->addHours(24); $start_date = $dividends_meta->last_dividend_update->addHours(24);
} }
// skip refresh if there's already recent data // skip refresh if there's already recent data
if ($start_date->greaterThan($end_date)) { if ($start_date->greaterThan($end_date)) {
@@ -83,7 +84,7 @@ class Dividend extends Model
// ah, we found some dividends... // ah, we found some dividends...
if ($dividend_data->isNotEmpty()) { if ($dividend_data->isNotEmpty()) {
// create mass insert // create mass insert
foreach ($dividend_data as $index => $dividend){ foreach ($dividend_data as $index => $dividend) {
$dividend_data[$index] = [...$dividend, ...['id' => Str::uuid()->toString(), 'updated_at' => now(), 'created_at' => now()]]; $dividend_data[$index] = [...$dividend, ...['id' => Str::uuid()->toString(), 'updated_at' => now(), 'created_at' => now()]];
} }
@@ -109,7 +110,7 @@ class Dividend extends Model
{ {
// group by holdings // group by holdings
$dividends = self::select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) $dividends = self::select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount'])
->selectRaw(' ->selectRaw('
(COALESCE(CASE WHEN transactions.transaction_type = "BUY" (COALESCE(CASE WHEN transactions.transaction_type = "BUY"
AND date(transactions.date) <= date(dividends.date) AND date(transactions.date) <= date(dividends.date)
THEN transactions.quantity ELSE 0 END, 0) THEN transactions.quantity ELSE 0 END, 0)
@@ -119,22 +120,22 @@ class Dividend extends Model
* dividends.dividend_amount * dividends.dividend_amount
AS total_received AS total_received
') ')
->join('transactions', 'transactions.symbol', '=', 'dividends.symbol') ->join('transactions', 'transactions.symbol', '=', 'dividends.symbol')
->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id') ->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id')
->where('dividends.symbol', $symbol) ->where('dividends.symbol', $symbol)
->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received') ->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received')
->havingRaw('total_received > 0') ->havingRaw('total_received > 0')
->get(); ->get();
// iterate through holdings and update // iterate through holdings and update
Holding::where(['symbol' => $symbol]) Holding::where(['symbol' => $symbol])
->get() ->get()
->each(function ($holding) use ($dividends) { ->each(function ($holding) use ($dividends) {
$holding->update([ $holding->update([
'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id) 'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id)
->sum('total_received') ->sum('total_received'),
]); ]);
}); });
} }
public static function reinvestDividends(iterable $dividend_data, MarketData $market_data): void public static function reinvestDividends(iterable $dividend_data, MarketData $market_data): void
@@ -144,21 +145,21 @@ class Dividend extends Model
'symbol' => $market_data->symbol, 'symbol' => $market_data->symbol,
'reinvest_dividends' => true, 'reinvest_dividends' => true,
]) ])
->get() ->get()
->each(function($holding) use ($dividend_data, $market_data) { ->each(function ($holding) use ($dividend_data, $market_data) {
foreach($dividend_data as $dividend) { foreach ($dividend_data as $dividend) {
Transaction::create([ Transaction::create([
'date' => $dividend['date'], 'date' => $dividend['date'],
'portfolio_id' => $holding->portfolio_id, 'portfolio_id' => $holding->portfolio_id,
'symbol' => $holding->symbol, 'symbol' => $holding->symbol,
'transaction_type' => "BUY", 'transaction_type' => 'BUY',
'reinvested_dividend' => true, 'reinvested_dividend' => true,
'cost_basis' => 0, 'cost_basis' => 0,
'quantity' => ($dividend['dividend_amount'] * $holding->qtyOwned(Carbon::parse($dividend['date']))) / $market_data->market_value, 'quantity' => ($dividend['dividend_amount'] * $holding->qtyOwned(Carbon::parse($dividend['date']))) / $market_data->market_value,
]); ]);
} }
}); });
} }
} }
+91 -92
View File
@@ -1,17 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Models\Split;
use App\Models\AiChat;
use App\Models\Dividend;
use App\Models\Portfolio;
use App\Models\MarketData;
use App\Models\Transaction;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class Holding extends Model class Holding extends Model
{ {
@@ -27,18 +23,13 @@ class Holding extends Model
'realized_gain_dollars', 'realized_gain_dollars',
'dividends_earned', 'dividends_earned',
'splits_synced_at', 'splits_synced_at',
'reinvest_dividends' 'reinvest_dividends',
]; ];
protected $casts = [ protected $casts = [
'splits_synced_at' => 'datetime', 'splits_synced_at' => 'datetime',
'first_transaction_date' => 'datetime', 'first_transaction_date' => 'datetime',
'reinvest_dividends' => 'boolean' 'reinvest_dividends' => 'boolean',
];
protected $attributes = [
'realized_gain_dollars' => 0,
'dividends_earned' => 0,
]; ];
/** /**
@@ -46,7 +37,7 @@ class Holding extends Model
* *
* @return void * @return void
*/ */
public function market_data() public function market_data()
{ {
return $this->hasOne(MarketData::class, 'symbol', 'symbol'); return $this->hasOne(MarketData::class, 'symbol', 'symbol');
} }
@@ -56,7 +47,7 @@ class Holding extends Model
* *
* @return void * @return void
*/ */
public function transactions() public function transactions()
{ {
return $this->hasManyThrough(Transaction::class, Portfolio::class, 'id', 'portfolio_id', 'portfolio_id', 'id')->orderBy('date', 'DESC'); return $this->hasManyThrough(Transaction::class, Portfolio::class, 'id', 'portfolio_id', 'portfolio_id', 'id')->orderBy('date', 'DESC');
} }
@@ -66,11 +57,11 @@ class Holding extends Model
* *
* @return void * @return void
*/ */
public function dividends() public function dividends()
{ {
return $this->hasMany(Dividend::class, 'symbol', 'symbol') return $this->hasMany(Dividend::class, 'symbol', 'symbol')
->select(['dividends.symbol','dividends.date','dividends.dividend_amount']) ->select(['dividends.symbol', 'dividends.date', 'dividends.dividend_amount'])
->selectRaw("SUM( ->selectRaw("SUM(
CASE WHEN transaction_type = 'BUY' CASE WHEN transaction_type = 'BUY'
AND transactions.symbol = dividends.symbol AND transactions.symbol = dividends.symbol
AND transactions.portfolio_id = '$this->portfolio_id' AND transactions.portfolio_id = '$this->portfolio_id'
@@ -78,7 +69,7 @@ class Holding extends Model
THEN transactions.quantity THEN transactions.quantity
ELSE 0 END ELSE 0 END
) AS purchased") ) AS purchased")
->selectRaw("SUM( ->selectRaw("SUM(
CASE WHEN transaction_type = 'SELL' CASE WHEN transaction_type = 'SELL'
AND transactions.symbol = dividends.symbol AND transactions.symbol = dividends.symbol
AND transactions.portfolio_id = '$this->portfolio_id' AND transactions.portfolio_id = '$this->portfolio_id'
@@ -86,7 +77,7 @@ class Holding extends Model
THEN transactions.quantity THEN transactions.quantity
ELSE 0 END ELSE 0 END
) AS sold") ) AS sold")
->selectRaw("SUM( ->selectRaw("SUM(
(CASE WHEN transaction_type = 'BUY' (CASE WHEN transaction_type = 'BUY'
AND transactions.symbol = dividends.symbol AND transactions.symbol = dividends.symbol
AND transactions.portfolio_id = '$this->portfolio_id' AND transactions.portfolio_id = '$this->portfolio_id'
@@ -99,16 +90,16 @@ class Holding extends Model
THEN transactions.quantity ELSE 0 END) THEN transactions.quantity ELSE 0 END)
* dividends.dividend_amount * dividends.dividend_amount
) AS total_received") ) AS total_received")
->join('transactions', 'transactions.symbol', 'dividends.symbol') ->join('transactions', 'transactions.symbol', 'dividends.symbol')
->groupBy(['dividends.symbol','dividends.date','dividends.dividend_amount']) ->groupBy(['dividends.symbol', 'dividends.date', 'dividends.dividend_amount'])
->orderBy('dividends.date', 'DESC') ->orderBy('dividends.date', 'DESC')
->where('dividends.date', '>=', function ($query) { ->where('dividends.date', '>=', function ($query) {
$query->selectRaw('min(transactions.date)') $query->selectRaw('min(transactions.date)')
->from('transactions') ->from('transactions')
->whereRaw("transactions.portfolio_id = '$this->portfolio_id'") ->whereRaw("transactions.portfolio_id = '$this->portfolio_id'")
->whereRaw("transactions.symbol = '$this->symbol'"); ->whereRaw("transactions.symbol = '$this->symbol'");
}) })
->having('total_received', '>', 0); ->having('total_received', '>', 0);
} }
/** /**
@@ -116,7 +107,7 @@ class Holding extends Model
* *
* @return void * @return void
*/ */
public function portfolio() public function portfolio()
{ {
return $this->belongsTo(Portfolio::class); return $this->belongsTo(Portfolio::class);
} }
@@ -126,7 +117,7 @@ class Holding extends Model
* *
* @return void * @return void
*/ */
public function splits() public function splits()
{ {
return $this->hasMany(Split::class, 'symbol', 'symbol') return $this->hasMany(Split::class, 'symbol', 'symbol')
->orderBy('date', 'DESC'); ->orderBy('date', 'DESC');
@@ -145,11 +136,11 @@ class Holding extends Model
public function scopeWithMarketData($query) public function scopeWithMarketData($query)
{ {
return $query->withAggregate('market_data', 'name') return $query->withAggregate('market_data', 'name')
->withAggregate('market_data', 'market_value') ->withAggregate('market_data', 'market_value')
->withAggregate('market_data', 'fifty_two_week_low') ->withAggregate('market_data', 'fifty_two_week_low')
->withAggregate('market_data', 'fifty_two_week_high') ->withAggregate('market_data', 'fifty_two_week_high')
->withAggregate('market_data', 'updated_at') ->withAggregate('market_data', 'updated_at')
->join('market_data', 'holdings.symbol', 'market_data.symbol'); ->join('market_data', 'holdings.symbol', 'market_data.symbol');
} }
public function scopeWithPerformance($query) public function scopeWithPerformance($query)
@@ -169,49 +160,50 @@ class Holding extends Model
return $query->where('holdings.symbol', $symbol); return $query->where('holdings.symbol', $symbol);
} }
public function scopeWithoutWishlists($query) { public function scopeWithoutWishlists($query)
{
return $query->whereHas('portfolio', function ($query) { return $query->whereHas('portfolio', function ($query) {
$query->where('portfolios.wishlist', 0); $query->where('portfolios.wishlist', 0);
}); });
} }
public function scopeMyHoldings($query, $userId = null) public function scopeMyHoldings($query, $userId = null)
{ {
return $query->whereHas('portfolio', function($query) use ($userId) { return $query->whereHas('portfolio', function ($query) use ($userId) {
$query->whereRelation('users', 'id', $userId ?? auth()->user()->id); $query->whereRelation('users', 'id', $userId ?? auth()->user()->id);
}); });
} }
public function scopeWithPortfolioMetrics($query) public function scopeWithPortfolioMetrics($query)
{ {
return $query->selectRaw('COALESCE(SUM(holdings.dividends_earned), 0) AS total_dividends_earned') return $query->selectRaw('COALESCE(SUM(holdings.dividends_earned), 0) AS total_dividends_earned')
->selectRaw('COALESCE(SUM(holdings.realized_gain_dollars), 0) AS realized_gain_dollars') ->selectRaw('COALESCE(SUM(holdings.realized_gain_dollars), 0) AS realized_gain_dollars')
->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) AS total_market_value') ->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) AS total_market_value')
->selectRaw('COALESCE(SUM(holdings.total_cost_basis), 0) AS total_cost_basis') ->selectRaw('COALESCE(SUM(holdings.total_cost_basis), 0) AS total_cost_basis')
->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) - COALESCE(SUM(holdings.total_cost_basis), 0) AS total_gain_dollars') ->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) - COALESCE(SUM(holdings.total_cost_basis), 0) AS total_gain_dollars')
// ->selectRaw('COALESCE((@total_gain_dollars / @sum_total_cost_basis) * 100,0) AS total_gain_percent') // ->selectRaw('COALESCE((@total_gain_dollars / @sum_total_cost_basis) * 100,0) AS total_gain_percent')
->join('market_data', 'market_data.symbol', '=', 'holdings.symbol'); ->join('market_data', 'market_data.symbol', '=', 'holdings.symbol');
} }
public function syncTransactionsAndDividends() public function syncTransactionsAndDividends()
{ {
// pull existing transaction data // pull existing transaction data
$query = Transaction::where([ $query = Transaction::where([
'portfolio_id' => $this->portfolio_id, 'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol, 'symbol' => $this->symbol,
])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`') ])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`')
->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`') ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`')
->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `total_cost_basis`') ->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `total_cost_basis`')
->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN (quantity * sale_price) ELSE 0 END) AS `total_sale_price`') ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN (quantity * sale_price) ELSE 0 END) AS `total_sale_price`')
->first(); ->first();
$total_quantity = round($query->qty_purchases - $query->qty_sales, 3); $total_quantity = round($query->qty_purchases - $query->qty_sales, 3);
$average_cost_basis = ( $average_cost_basis = (
$query->qty_purchases > 0 $query->qty_purchases > 0
&& $total_quantity > 0 && $total_quantity > 0
) )
? $query->total_cost_basis / $query->qty_purchases ? $query->total_cost_basis / $query->qty_purchases
: 0; : 0;
// update holding // update holding
@@ -219,18 +211,20 @@ class Holding extends Model
'quantity' => $total_quantity, 'quantity' => $total_quantity,
'average_cost_basis' => $average_cost_basis, 'average_cost_basis' => $average_cost_basis,
'total_cost_basis' => $total_quantity * $average_cost_basis, 'total_cost_basis' => $total_quantity * $average_cost_basis,
'realized_gain_dollars' => $query->qty_purchases > 0 && $query->total_sale_price > 0 'realized_gain_dollars' => $query->qty_purchases > 0 && $query->total_sale_price > 0
? $query->total_sale_price - ($query->qty_sales * ($query->total_cost_basis / $query->qty_purchases)) ? $query->total_sale_price - ($query->qty_sales * ($query->total_cost_basis / $query->qty_purchases))
: 0, : 0,
'dividends_earned' => $this->dividends->sum('total_received') 'dividends_earned' => $this->dividends->sum('total_received'),
]); ]);
$this->save(); $this->save();
} }
public function qtyOwned(\Illuminate\Support\Carbon $date = null) public function qtyOwned(?\Illuminate\Support\Carbon $date = null)
{ {
if ($date == null) $date = now(); if ($date == null) {
$date = now();
}
$transactions = $this->transactions->where('date', '<=', $date); $transactions = $this->transactions->where('date', '<=', $date);
@@ -242,16 +236,20 @@ class Holding extends Model
} }
public function dailyPerformance( public function dailyPerformance(
\Illuminate\Support\Carbon $start_date = null, ?\Illuminate\Support\Carbon $start_date = null,
\Illuminate\Support\Carbon $end_date = null, ?\Illuminate\Support\Carbon $end_date = null,
) { ) {
if ($start_date == null) $start_date = now(); if ($start_date == null) {
if ($end_date == null) $end_date = now(); $start_date = now();
}
if ($end_date == null) {
$end_date = now();
}
$date_interval = "DATE_ADD(date, INTERVAL 1 DAY)"; $date_interval = 'DATE_ADD(date, INTERVAL 1 DAY)';
if (config('database.default') === 'sqlite') { if (config('database.default') === 'sqlite') {
$date_interval = "date(date, '+1 day')"; $date_interval = "date(date, '+1 day')";
} else { } else {
@@ -270,14 +268,14 @@ class Holding extends Model
FROM date_series FROM date_series
) as date_series") ) as date_series")
) )
->select([ ->select([
'date_series.date', 'date_series.date',
DB::raw(" DB::raw("
ROUND( ROUND(
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) - COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0), 3) AS `owned` COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0), 3) AS `owned`
"), "),
DB::raw(" DB::raw("
COALESCE(CASE COALESCE(CASE
WHEN ( WHEN (
ROUND( ROUND(
@@ -290,29 +288,30 @@ class Holding extends Model
END) END)
END, 0) AS cost_basis END, 0) AS cost_basis
"), "),
DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`") DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`"),
]) ])
->leftJoin('transactions', function ($join) { ->leftJoin('transactions', function ($join) {
$join->on(DB::raw('DATE(transactions.date)'), '<=', 'date_series.date') $join->on(DB::raw('DATE(transactions.date)'), '<=', 'date_series.date')
->where('transactions.symbol', '=', $this->symbol) ->where('transactions.symbol', '=', $this->symbol)
->where('transactions.portfolio_id', '=', $this->portfolio_id); ->where('transactions.portfolio_id', '=', $this->portfolio_id);
}) })
->groupBy('date_series.date') ->groupBy('date_series.date')
->orderBy('date_series.date') ->orderBy('date_series.date')
->get() ->get()
->keyBy('date'); ->keyBy('date');
} }
public function getFormattedTransactions() public function getFormattedTransactions()
{ {
$formattedTransactions = ''; $formattedTransactions = '';
foreach($this->transactions->sortByDesc('date') as $transaction) { foreach ($this->transactions->sortByDesc('date') as $transaction) {
$formattedTransactions .= " * ".$transaction->date->format('Y-m-d') $formattedTransactions .= ' * '.$transaction->date->format('Y-m-d')
." ". $transaction->transaction_type .' '.$transaction->transaction_type
." ". $transaction->quantity .' '.$transaction->quantity
." @ ". $transaction->cost_basis .' @ '.$transaction->cost_basis
." each \n\n"; ." each \n\n";
} }
return $formattedTransactions; return $formattedTransactions;
} }
} }
+13 -9
View File
@@ -1,17 +1,21 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Interfaces\MarketData\MarketDataInterface; use App\Interfaces\MarketData\MarketDataInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MarketData extends Model class MarketData extends Model
{ {
use HasFactory; use HasFactory;
protected $primaryKey = 'symbol'; protected $primaryKey = 'symbol';
protected $keyType = 'string'; protected $keyType = 'string';
public $incrementing = false; public $incrementing = false;
protected $fillable = [ protected $fillable = [
@@ -25,7 +29,7 @@ class MarketData extends Model
'market_cap', 'market_cap',
'book_value', 'book_value',
'last_dividend_date', 'last_dividend_date',
'dividend_yield' 'dividend_yield',
]; ];
protected $casts = [ protected $casts = [
@@ -37,10 +41,10 @@ class MarketData extends Model
'trailing_pe' => 'float', 'trailing_pe' => 'float',
'market_cap' => 'float', 'market_cap' => 'float',
'book_value' => 'float', 'book_value' => 'float',
'dividend_yield' => 'float' 'dividend_yield' => 'float',
]; ];
public function holdings() public function holdings()
{ {
return $this->hasMany(Holding::class, 'symbol', 'symbol'); return $this->hasMany(Holding::class, 'symbol', 'symbol');
} }
@@ -50,20 +54,20 @@ class MarketData extends Model
return $query->where('symbol', $symbol); return $query->where('symbol', $symbol);
} }
public static function getMarketData($symbol, $force = false) public static function getMarketData($symbol, $force = false)
{ {
$market_data = self::firstOrNew([ $market_data = self::firstOrNew([
'symbol' => $symbol 'symbol' => $symbol,
]); ]);
// check if new or stale // check if new or stale
if ( if (
$force $force
|| !$market_data->exists || ! $market_data->exists
|| is_null($market_data->updated_at) || is_null($market_data->updated_at)
|| $market_data->updated_at->diffInMinutes(now()) >= config('investbrain.refresh') || $market_data->updated_at->diffInMinutes(now()) >= config('investbrain.refresh')
) { ) {
// get quote // get quote
$quote = app(MarketDataInterface::class)->quote($symbol); $quote = app(MarketDataInterface::class)->quote($symbol);
@@ -76,4 +80,4 @@ class MarketData extends Model
return $market_data; return $market_data;
} }
} }
+89 -51
View File
@@ -1,16 +1,19 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Models\AiChat; use App\Interfaces\MarketData\MarketDataInterface;
use App\Notifications\InvitedOnboardingNotification;
use Carbon\CarbonPeriod; use Carbon\CarbonPeriod;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str;
use App\Interfaces\MarketData\MarketDataInterface;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Portfolio extends Model class Portfolio extends Model
{ {
@@ -28,7 +31,7 @@ class Portfolio extends Model
protected static function boot() protected static function boot()
{ {
parent::boot(); parent::boot();
static::saved(function ($portfolio) { static::saved(function ($portfolio) {
self::ensurePortfolioHasOwner($portfolio); self::ensurePortfolioHasOwner($portfolio);
@@ -38,7 +41,7 @@ class Portfolio extends Model
protected $hidden = []; protected $hidden = [];
protected $casts = [ protected $casts = [
'wishlist' => 'boolean' 'wishlist' => 'boolean',
]; ];
protected $with = ['users', 'transactions']; protected $with = ['users', 'transactions'];
@@ -51,8 +54,8 @@ class Portfolio extends Model
public function holdings() public function holdings()
{ {
return $this->hasMany(Holding::class, 'portfolio_id') return $this->hasMany(Holding::class, 'portfolio_id')
->withMarketData() ->withMarketData()
->withPerformance(); ->withPerformance();
} }
public function transactions() public function transactions()
@@ -75,25 +78,25 @@ class Portfolio extends Model
return $this->morphMany(AiChat::class, 'chatable')->where('user_id', auth()->user()->id); return $this->morphMany(AiChat::class, 'chatable')->where('user_id', auth()->user()->id);
} }
public function scopeMyPortfolios() public function scopeMyPortfolios()
{ {
return $this->whereHas('users', function ($query) { return $this->whereHas('users', function ($query) {
$query->where('user_id', auth()->user()->id); $query->where('user_id', auth()->user()->id);
}); });
} }
public function scopeFullAccess($query, $user_id = null) public function scopeFullAccess($query, $user_id = null)
{ {
return $query->whereHas('users', function ($query) use ($user_id) { return $query->whereHas('users', function ($query) use ($user_id) {
$query->where('user_id', $user_id ?? auth()->user()->id) $query->where('user_id', $user_id ?? auth()->user()->id)
->where(function ($query) { ->where(function ($query) {
$query->where('full_access', true) $query->where('full_access', true)
->orWhere('owner', true); ->orWhere('owner', true);
}); });
}); });
} }
public function scopeWithoutWishlists() public function scopeWithoutWishlists()
{ {
return $this->where(['wishlist' => false]); return $this->where(['wishlist' => false]);
} }
@@ -101,7 +104,7 @@ class Portfolio extends Model
public function setOwnerIdAttribute($value) public function setOwnerIdAttribute($value)
{ {
// enable queued jobs to create portfolios with owners // enable queued jobs to create portfolios with owners
if (!auth()->user()?->id && !$this->owner_id) { if (! auth()->user()?->id && ! $this->owner_id) {
static::$owner_id = $value; static::$owner_id = $value;
} }
} }
@@ -113,46 +116,47 @@ class Portfolio extends Model
public function getOwnerAttribute() public function getOwnerAttribute()
{ {
if (!$this->relationLoaded('user')) { if (! $this->relationLoaded('user')) {
$this->load('users'); $this->load('users');
} }
return $this->users->where('pivot.owner', true)->first(); return $this->users->where('pivot.owner', true)->first();
} }
public static function ensurePortfolioHasOwner(self $portfolio) public static function ensurePortfolioHasOwner(self $portfolio)
{ {
// make sure we don't remove owner access // make sure we don't remove owner access
if (!$portfolio->owner_id) { if (! $portfolio->owner_id) {
$owner[static::$owner_id ?? auth()->user()->id] = ['owner' => true]; $owner[static::$owner_id ?? auth()->user()->id] = ['owner' => true];
// save // save
$portfolio->users()->sync($owner); $portfolio->users()->sync($owner);
static::$owner_id = null;
} }
} }
public function syncDailyChanges(): void public function syncDailyChanges(): void
{ {
$holdings = $this->holdings() $holdings = $this->holdings()
->join('transactions', function($join) { ->join('transactions', function ($join) {
$join->on('transactions.symbol', '=', 'holdings.symbol') $join->on('transactions.symbol', '=', 'holdings.symbol')
->where('transactions.portfolio_id', '=', $this->id); ->where('transactions.portfolio_id', '=', $this->id);
}) })
->select('holdings.symbol', 'holdings.portfolio_id', DB::raw('min(transactions.date) as first_transaction_date')) // get first transaction date ->select('holdings.symbol', 'holdings.portfolio_id', DB::raw('min(transactions.date) as first_transaction_date')) // get first transaction date
->groupBy(['holdings.symbol', 'holdings.portfolio_id']) ->groupBy(['holdings.symbol', 'holdings.portfolio_id'])
->get(); ->get();
$dividends = Dividend::whereIn('symbol', $holdings->pluck('symbol'))->get(); $dividends = Dividend::whereIn('symbol', $holdings->pluck('symbol'))->get();
$total_performance = []; $total_performance = [];
$holdings->each(function($holding) use (&$total_performance, $dividends) { $holdings->each(function ($holding) use (&$total_performance, $dividends) {
$period = CarbonPeriod::create( $period = CarbonPeriod::create(
$holding->first_transaction_date, $holding->first_transaction_date,
now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day'))) now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day')))
? now()->subDay() ? now()->subDay()
: now() : now()
); );
@@ -167,11 +171,11 @@ class Portfolio extends Model
$dividends_earned = 0; $dividends_earned = 0;
$holding_performance = []; $holding_performance = [];
foreach($period as $date) { foreach ($period as $date) {
$date = $date->format('Y-m-d'); $date = $date->format('Y-m-d');
$close = $this->getMostRecentCloseData($all_history, $date); $close = $this->getMostRecentCloseData($all_history, $date);
$total_market_value = $daily_performance->get($date)->owned * $close; $total_market_value = $daily_performance->get($date)->owned * $close;
$dividends_earned += $daily_performance->get($date)->owned * ($dividends->get($date)?->dividend_amount ?? 0); $dividends_earned += $daily_performance->get($date)->owned * ($dividends->get($date)?->dividend_amount ?? 0);
@@ -179,18 +183,18 @@ class Portfolio extends Model
$holding_performance[$date] = [ $holding_performance[$date] = [
'date' => $date, 'date' => $date,
'portfolio_id' => $this->id, 'portfolio_id' => $this->id,
'total_market_value' => $total_market_value, 'total_market_value' => $total_market_value,
'total_cost_basis' => $daily_performance->get($date)->cost_basis, 'total_cost_basis' => $daily_performance->get($date)->cost_basis,
'total_gain' => $total_market_value - $daily_performance->get($date)->cost_basis, 'total_gain' => $total_market_value - $daily_performance->get($date)->cost_basis,
'realized_gains' => $daily_performance->get($date)->realized_gains, 'realized_gains' => $daily_performance->get($date)->realized_gains,
'total_dividends_earned' => $dividends_earned 'total_dividends_earned' => $dividends_earned,
]; ];
} }
} }
foreach ($holding_performance as $date => $performance) { foreach ($holding_performance as $date => $performance) {
if (Arr::get($total_performance, $date) == null) { if (Arr::get($total_performance, $date) == null) {
$total_performance[$date] = $performance; $total_performance[$date] = $performance;
} else { } else {
@@ -204,9 +208,9 @@ class Portfolio extends Model
} }
}); });
if (!empty($total_performance)) { if (! empty($total_performance)) {
DB::transaction(function () use ($total_performance) { DB::transaction(function () use ($total_performance) {
$this->daily_change()->upsert( $this->daily_change()->upsert(
$total_performance, $total_performance,
['date', 'portfolio_id'], ['date', 'portfolio_id'],
@@ -215,7 +219,7 @@ class Portfolio extends Model
'total_cost_basis', 'total_cost_basis',
'total_gain', 'total_gain',
'realized_gains', 'realized_gains',
'total_dividends_earned' 'total_dividends_earned',
] ]
); );
}); });
@@ -226,10 +230,10 @@ class Portfolio extends Model
{ {
$close = Arr::get($history, "$date.close", 0); $close = Arr::get($history, "$date.close", 0);
if (!$close && $i < $max_attempts) { if (! $close && $i < $max_attempts) {
$i++; $i++;
$date = Carbon::parse($date)->subDay()->format('Y-m-d'); $date = Carbon::parse($date)->subDay()->format('Y-m-d');
return $this->getMostRecentCloseData($history, $date, $i); return $this->getMostRecentCloseData($history, $date, $i);
@@ -241,16 +245,50 @@ class Portfolio extends Model
public function getFormattedHoldings() public function getFormattedHoldings()
{ {
$formattedHoldings = ''; $formattedHoldings = '';
foreach($this->holdings as $holding) { foreach ($this->holdings as $holding) {
$formattedHoldings .= " * Holding of ".$holding->market_data->name." (".$holding->symbol.")" $formattedHoldings .= ' * Holding of '.$holding->market_data->name.' ('.$holding->symbol.')'
."; with ". ($holding->quantity > 0 ? $holding->quantity : 'ZERO') . " shares" .'; with '.($holding->quantity > 0 ? $holding->quantity : 'ZERO').' shares'
."; avg cost basis ". $holding->average_cost_basis .'; avg cost basis '.$holding->average_cost_basis
."; curr market value ". $holding->market_data->market_value .'; curr market value '.$holding->market_data->market_value
."; unrealized gains ". $holding->market_gain_dollars .'; unrealized gains '.$holding->market_gain_dollars
."; realized gains ". $holding->realized_gain_dollars .'; realized gains '.$holding->realized_gain_dollars
."; dividends earned ". $holding->dividends_earned .'; dividends earned '.$holding->dividends_earned
."\n\n"; ."\n\n";
} }
return $formattedHoldings; return $formattedHoldings;
} }
/**
* Share a portfolio with a user
*/
public function share(string $email, bool $fullAccess = false): void
{
$user = User::firstOrCreate([
'email' => $email,
], [
'name' => Str::title(Str::before($email, '@')),
]);
$permissions[$user->id] = [
'full_access' => $fullAccess,
];
$sync = $this->users()->syncWithoutDetaching($permissions);
if (! empty($sync['attached'])) {
foreach ($sync['attached'] as $newUserId) {
User::find($newUserId)->notify(new InvitedOnboardingNotification($this, auth()->user()));
}
}
}
/**
* Un-share a portfolio
*/
public function unShare(string $userId): void
{
$this->users()->detach($userId);
}
} }
+38 -36
View File
@@ -1,14 +1,15 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Models\Transaction;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
use App\Interfaces\MarketData\MarketDataInterface; use App\Interfaces\MarketData\MarketDataInterface;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class Split extends Model class Split extends Model
{ {
@@ -28,22 +29,23 @@ class Split extends Model
'last_date' => 'datetime', 'last_date' => 'datetime',
]; ];
public function holdings() { public function holdings()
{
return $this->hasMany(Holding::class, 'symbol', 'symbol'); return $this->hasMany(Holding::class, 'symbol', 'symbol');
} }
public function transactions() { public function transactions()
{
return $this->hasMany(Transaction::class, 'symbol', 'symbol'); return $this->hasMany(Transaction::class, 'symbol', 'symbol');
} }
/** /**
* Grab new split data * Grab new split data
* *
* @param string $symbol * @param \DateTimeInterface|null $start_date
* @param \DateTimeInterface|null $start_date
* @return void * @return void
*/ */
public static function refreshSplitData(string $symbol) public static function refreshSplitData(string $symbol)
{ {
// dates for split data // dates for split data
$splits_meta = self::where(['symbol' => $symbol]) $splits_meta = self::where(['symbol' => $symbol])
@@ -58,9 +60,9 @@ class Split extends Model
// nope, need to populate newer split data // nope, need to populate newer split data
if ($splits_meta->total_splits) { if ($splits_meta->total_splits) {
$start_date = $splits_meta->last_date->addHours(48); $start_date = $splits_meta->last_date->addHours(48);
$end_date = now(); $end_date = now();
} }
// get some data // get some data
@@ -71,10 +73,10 @@ class Split extends Model
if ($split_data->isNotEmpty()) { if ($split_data->isNotEmpty()) {
// insert records // insert records
(new self)->insert($split_data->map(function($split) { (new self)->insert($split_data->map(function ($split) {
return [...$split, ...['id' => Str::uuid()->toString()]]; return [...$split, ...['id' => Str::uuid()->toString()]];
})->toArray()); })->toArray());
} }
// sync to transactions // sync to transactions
@@ -84,39 +86,39 @@ class Split extends Model
/** /**
* Syncs all transactions of symbol with split data * Syncs all transactions of symbol with split data
* *
* @param string $symbol * @param string $symbol
* @return void * @return void
*/ */
public static function syncToTransactions($symbol) public static function syncToTransactions($symbol)
{ {
// get splits joined with matching holdings // get splits joined with matching holdings
$splits = self::select([ $splits = self::select([
'splits.date', 'splits.date',
'splits.symbol', 'splits.symbol',
'splits.split_amount', 'splits.split_amount',
'holdings.portfolio_id' 'holdings.portfolio_id',
]) ])
->where([ ->where([
'splits.symbol' => $symbol, 'splits.symbol' => $symbol,
]) ])
->whereDate('splits.date', '>', DB::raw('IFNULL(holdings.splits_synced_at, "0000-00-00")')) ->whereDate('splits.date', '>', DB::raw('IFNULL(holdings.splits_synced_at, "0000-00-00")'))
->where('holdings.quantity', '>', 0) ->where('holdings.quantity', '>', 0)
->join('holdings', 'splits.symbol', 'holdings.symbol') ->join('holdings', 'splits.symbol', 'holdings.symbol')
->orderBy('splits.date', 'ASC') ->orderBy('splits.date', 'ASC')
->get(); ->get();
foreach($splits as $split) { foreach ($splits as $split) {
// get qty owned when split was issued // get qty owned when split was issued
$qty_owned = Transaction::where([ $qty_owned = Transaction::where([
'symbol' => $split->symbol, 'symbol' => $split->symbol,
'portfolio_id' => $split->portfolio_id 'portfolio_id' => $split->portfolio_id,
]) ])
->whereDate('transactions.date', '<', $split->date->format('Y-m-d')) ->whereDate('transactions.date', '<', $split->date->format('Y-m-d'))
->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) - ->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) -
SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS qty_owned') SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS qty_owned')
->value('qty_owned'); ->value('qty_owned');
if ($qty_owned > 0) { if ($qty_owned > 0) {
Transaction::create([ Transaction::create([
@@ -128,14 +130,14 @@ class Split extends Model
'cost_basis' => 0, 'cost_basis' => 0,
'split' => true, 'split' => true,
'created_at' => now(), 'created_at' => now(),
'updated_at' => now() 'updated_at' => now(),
]); ]);
Holding::where([ Holding::where([
'symbol' => $split->symbol, 'symbol' => $split->symbol,
'portfolio_id' => $split->portfolio_id 'portfolio_id' => $split->portfolio_id,
])->update([ ])->update([
'splits_synced_at' => now() 'splits_synced_at' => now(),
]); ]);
} }
} }
+34 -33
View File
@@ -1,13 +1,17 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Models\MarketData; use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Arr;
class Transaction extends Model class Transaction extends Model
{ {
@@ -23,7 +27,7 @@ class Transaction extends Model
'cost_basis', 'cost_basis',
'sale_price', 'sale_price',
'split', 'split',
'reinvested_dividend' 'reinvested_dividend',
]; ];
protected $hidden = []; protected $hidden = [];
@@ -31,7 +35,7 @@ class Transaction extends Model
protected $casts = [ protected $casts = [
'date' => 'datetime', 'date' => 'datetime',
'split' => 'boolean', 'split' => 'boolean',
'reinvested_dividend' => 'boolean' 'reinvested_dividend' => 'boolean',
]; ];
protected static function boot() protected static function boot()
@@ -52,14 +56,14 @@ class Transaction extends Model
$transaction->refreshMarketData(); $transaction->refreshMarketData();
cache()->forget('portfolio-metrics-' . $transaction->portfolio_id); cache()->forget('portfolio-metrics-'.$transaction->portfolio_id);
}); });
static::deleted(function ($transaction) { static::deleted(function ($transaction) {
$transaction->syncToHolding(); $transaction->syncToHolding();
cache()->forget('portfolio-metrics-' . $transaction->portfolio_id); cache()->forget('portfolio-metrics-'.$transaction->portfolio_id);
}); });
} }
@@ -78,7 +82,7 @@ class Transaction extends Model
* *
* @return void * @return void
*/ */
public function market_data() public function market_data(): HasOne
{ {
return $this->hasOne(MarketData::class, 'symbol', 'symbol'); return $this->hasOne(MarketData::class, 'symbol', 'symbol');
} }
@@ -88,47 +92,47 @@ class Transaction extends Model
* *
* @return void * @return void
*/ */
public function portfolio() public function portfolio(): BelongsTo
{ {
return $this->belongsTo(Portfolio::class); return $this->belongsTo(Portfolio::class);
} }
public function scopeWithMarketData($query) public function scopeWithMarketData($query): Builder
{ {
return $query->withAggregate('market_data', 'name') return $query->withAggregate('market_data', 'name')
->withAggregate('market_data', 'market_value') ->withAggregate('market_data', 'market_value')
->withAggregate('market_data', 'fifty_two_week_low') ->withAggregate('market_data', 'fifty_two_week_low')
->withAggregate('market_data', 'fifty_two_week_high') ->withAggregate('market_data', 'fifty_two_week_high')
->withAggregate('market_data', 'updated_at') ->withAggregate('market_data', 'updated_at')
->join('market_data', 'transactions.symbol', 'market_data.symbol'); ->join('market_data', 'transactions.symbol', 'market_data.symbol');
} }
public function scopePortfolio($query, $portfolio) public function scopePortfolio($query, $portfolio): Builder
{ {
return $query->where('portfolio_id', $portfolio); return $query->where('portfolio_id', $portfolio);
} }
public function scopeSymbol($query, $symbol) public function scopeSymbol($query, $symbol): Builder
{ {
return $query->where('symbol', $symbol); return $query->where('symbol', $symbol);
} }
public function scopeBuy($query) public function scopeBuy($query): Builder
{ {
return $query->where('transaction_type', 'BUY'); return $query->where('transaction_type', 'BUY');
} }
public function scopeSell($query) public function scopeSell($query): Builder
{ {
return $query->where('transaction_type', 'SELL'); return $query->where('transaction_type', 'SELL');
} }
public function scopeBeforeDate($query, $date) public function scopeBeforeDate($query, $date): Builder
{ {
return $query->whereDate('date', '<=', $date); return $query->whereDate('date', '<=', $date);
} }
public function scopeMyTransactions() public function scopeMyTransactions(): Builder
{ {
return $this->whereHas('portfolio', function ($query) { return $this->whereHas('portfolio', function ($query) {
$query->whereHas('users', function ($query) { $query->whereHas('users', function ($query) {
@@ -137,24 +141,22 @@ class Transaction extends Model
}); });
} }
public function refreshMarketData() public function refreshMarketData(): void
{ {
return MarketData::getMarketData($this->attributes['symbol']); MarketData::getMarketData($this->attributes['symbol']);
} }
/** /**
* Writes average cost basis to a sale transaction * Writes average cost basis to a sale transaction
*
* @return Transaction
*/ */
public function ensureCostBasisIsAddedToSale() public function ensureCostBasisIsAddedToSale(): Transaction
{ {
$average_cost_basis = Transaction::where([ $average_cost_basis = Transaction::where([
'portfolio_id' => $this->portfolio_id, 'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol, 'symbol' => $this->symbol,
'transaction_type' => 'BUY', 'transaction_type' => 'BUY',
])->whereDate('date', '<=', $this->date) ])->whereDate('date', '<=', $this->date)
->average('cost_basis'); ->average('cost_basis');
$this->cost_basis = $average_cost_basis ?? 0; $this->cost_basis = $average_cost_basis ?? 0;
@@ -163,10 +165,9 @@ class Transaction extends Model
/** /**
* Syncs the holding related to this transaction * Syncs the holding related to this transaction
*
* @return void
*/ */
public function syncToHolding() { public function syncToHolding(): void
{
// if symbol name changed, sync previous symbol too // if symbol name changed, sync previous symbol too
if (Arr::has($this->changes, 'symbol')) { if (Arr::has($this->changes, 'symbol')) {
@@ -181,7 +182,7 @@ class Transaction extends Model
// get the holding for a symbol and portfolio (or create one) // get the holding for a symbol and portfolio (or create one)
Holding::firstOrNew([ Holding::firstOrNew([
'portfolio_id' => $this->portfolio_id, 'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol 'symbol' => $this->symbol,
], [ ], [
'portfolio_id' => $this->portfolio_id, 'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol, 'symbol' => $this->symbol,
@@ -191,4 +192,4 @@ class Transaction extends Model
'splits_synced_at' => now(), 'splits_synced_at' => now(),
])->syncTransactionsAndDividends(); ])->syncTransactionsAndDividends();
} }
} }
+14 -12
View File
@@ -1,29 +1,31 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Traits\HasConnectedAccounts; use App\Traits\HasConnectedAccounts;
use Laravel\Sanctum\HasApiTokens; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Laravel\Jetstream\HasProfilePhoto;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens;
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
class User extends Authenticatable implements MustVerifyEmail class User extends Authenticatable implements MustVerifyEmail
{ {
use HasApiTokens; use HasApiTokens;
use HasConnectedAccounts;
use HasFactory; use HasFactory;
use HasProfilePhoto; use HasProfilePhoto;
use HasRelationships;
use HasUuids;
use Notifiable; use Notifiable;
use TwoFactorAuthenticatable; use TwoFactorAuthenticatable;
use HasUuids;
use HasRelationships;
use HasConnectedAccounts;
protected $fillable = [ protected $fillable = [
'name', 'name',
@@ -65,7 +67,7 @@ class User extends Authenticatable implements MustVerifyEmail
{ {
return $this->hasManyDeep(Holding::class, ['portfolio_user', Portfolio::class]) return $this->hasManyDeep(Holding::class, ['portfolio_user', Portfolio::class])
->withMarketData() ->withMarketData()
->withPerformance(); ->withPerformance();
} }
public function transactions(): HasManyDeep public function transactions(): HasManyDeep
@@ -78,6 +80,6 @@ class User extends Authenticatable implements MustVerifyEmail
WHEN transaction_type = \'SELL\' WHEN transaction_type = \'SELL\'
THEN COALESCE(transactions.sale_price - transactions.cost_basis, 0) THEN COALESCE(transactions.sale_price - transactions.cost_basis, 0)
ELSE COALESCE(market_data.market_value - transactions.cost_basis, 0) ELSE COALESCE(market_data.market_value - transactions.cost_basis, 0)
END AS gain_dollars'); END AS gain_dollars');
} }
} }
+10 -8
View File
@@ -1,11 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace App\Notifications; namespace App\Notifications;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ImportFailedNotification extends Notification implements ShouldQueue class ImportFailedNotification extends Notification implements ShouldQueue
{ {
@@ -16,7 +18,7 @@ class ImportFailedNotification extends Notification implements ShouldQueue
*/ */
public function __construct( public function __construct(
public string $errorMessage public string $errorMessage
) { } ) {}
/** /**
* Get the notification's delivery channels. * Get the notification's delivery channels.
@@ -34,12 +36,12 @@ class ImportFailedNotification extends Notification implements ShouldQueue
public function toMail(object $notifiable): MailMessage public function toMail(object $notifiable): MailMessage
{ {
return (new MailMessage) return (new MailMessage)
->greeting('Oh no!') ->greeting('Oh no!')
->subject("Your Investbrain import failed!") ->subject('Your Investbrain import failed!')
->line("Heads up, your Investbrain import was unable to successfully complete. There were errors which caused the import to fail.") ->line('Heads up, your Investbrain import was unable to successfully complete. There were errors which caused the import to fail.')
->action("Try again?", route('import-export')) ->action('Try again?', route('import-export'))
->line("**Technical details:**") ->line('**Technical details:**')
->line($this->errorMessage); ->line($this->errorMessage);
} }
/** /**
@@ -1,13 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace App\Notifications; namespace App\Notifications;
use App\Models\User;
use App\Models\Portfolio;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ImportSucceededNotification extends Notification implements ShouldQueue class ImportSucceededNotification extends Notification implements ShouldQueue
{ {
@@ -16,7 +16,7 @@ class ImportSucceededNotification extends Notification implements ShouldQueue
/** /**
* Create a new notification instance. * Create a new notification instance.
*/ */
public function __construct() { } public function __construct() {}
/** /**
* Get the notification's delivery channels. * Get the notification's delivery channels.
@@ -34,10 +34,10 @@ class ImportSucceededNotification extends Notification implements ShouldQueue
public function toMail(object $notifiable): MailMessage public function toMail(object $notifiable): MailMessage
{ {
return (new MailMessage) return (new MailMessage)
->greeting('Woot! 🎉') ->greeting('Woot! 🎉')
->subject("Your Investbrain import was successful!") ->subject('Your Investbrain import was successful!')
->line("Just a heads up that your Investbrain import succeeded! Your portfolios, transactions, and daily changes are now available in your account.") ->line('Just a heads up that your Investbrain import succeeded! Your portfolios, transactions, and daily changes are now available in your account.')
->action("Get Started", route('dashboard')); ->action('Get Started', route('dashboard'));
} }
/** /**
@@ -1,13 +1,15 @@
<?php <?php
declare(strict_types=1);
namespace App\Notifications; namespace App\Notifications;
use App\Models\User;
use App\Models\Portfolio; use App\Models\Portfolio;
use App\Models\User;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class InvitedOnboardingNotification extends Notification implements ShouldQueue class InvitedOnboardingNotification extends Notification implements ShouldQueue
{ {
@@ -19,7 +21,7 @@ class InvitedOnboardingNotification extends Notification implements ShouldQueue
public function __construct( public function __construct(
public Portfolio $portfolio, public Portfolio $portfolio,
public User $sender, public User $sender,
) { } ) {}
/** /**
* Get the notification's delivery channels. * Get the notification's delivery channels.
@@ -40,14 +42,14 @@ class InvitedOnboardingNotification extends Notification implements ShouldQueue
$url = url()->signedRoute('invited_onboarding', ['portfolio' => $this->portfolio->id, 'user' => $notifiable->id], now()->addDays(90)); $url = url()->signedRoute('invited_onboarding', ['portfolio' => $this->portfolio->id, 'user' => $notifiable->id], now()->addDays(90));
return (new MailMessage) return (new MailMessage)
->replyTo($this->sender->email, $this->sender->name) ->replyTo($this->sender->email, $this->sender->name)
->greeting('Hey there! 👋') ->greeting('Hey there! 👋')
->subject("You've been invited to {$this->portfolio->title} on Investbrain!") ->subject("You've been invited to {$this->portfolio->title} on Investbrain!")
->line("{$this->sender->name} has invited you to **{$this->portfolio->title}** on Investbrain, a smart open-source investment tracker that consolidates and monitors market performance across your different brokerages.") ->line("{$this->sender->name} has invited you to **{$this->portfolio->title}** on Investbrain, a smart open-source investment tracker that consolidates and monitors market performance across your different brokerages.")
->line("Once you're in, you'll be able to see all the holdings, dividends, market performance and more for {$this->portfolio->title}!") ->line("Once you're in, you'll be able to see all the holdings, dividends, market performance and more for {$this->portfolio->title}!")
->action("Get Started", $url) ->action('Get Started', $url)
->line("If you have any questions, you can reply to this email.") ->line('If you have any questions, you can reply to this email.')
->salutation("See you there,\n". e($this->sender->name)); ->salutation("See you there,\n".e($this->sender->name));
} }
/** /**
@@ -1,12 +1,14 @@
<?php <?php
declare(strict_types=1);
namespace App\Notifications; namespace App\Notifications;
use Illuminate\Bus\Queueable;
use App\Models\ConnectedAccount; use App\Models\ConnectedAccount;
use Illuminate\Notifications\Notification; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class VerifyConnectedAccountNotification extends Notification implements ShouldQueue class VerifyConnectedAccountNotification extends Notification implements ShouldQueue
{ {
@@ -17,7 +19,7 @@ class VerifyConnectedAccountNotification extends Notification implements ShouldQ
*/ */
public function __construct( public function __construct(
public string $connected_account_id public string $connected_account_id
) { } ) {}
/** /**
* Get the notification's delivery channels. * Get the notification's delivery channels.
@@ -40,11 +42,11 @@ class VerifyConnectedAccountNotification extends Notification implements ShouldQ
$url = url()->signedRoute('oauth.verify_connected_account', ['connected_account' => $this->connected_account_id], now()->days($days = 7)); $url = url()->signedRoute('oauth.verify_connected_account', ['connected_account' => $this->connected_account_id], now()->days($days = 7));
return (new MailMessage) return (new MailMessage)
->greeting('Welcome back!') ->greeting('Welcome back!')
->subject("Connect your $provider account with Investbrain") ->subject("Connect your $provider account with Investbrain")
->line("You recently attempted to log into an existing Investbrain account using $provider. To safeguard your Investbrain account, please confirm this was you by pressing the 'Connect $provider' button below:") ->line("You recently attempted to log into an existing Investbrain account using $provider. To safeguard your Investbrain account, please confirm this was you by pressing the 'Connect $provider' button below:")
->action("Connect $provider", $url) ->action("Connect $provider", $url)
->line("If you do not recognize this activity, we recommend [changing your password](".route('profile.show').") as soon as possible. Otherwise, you can disregard this message. This link will expire in {$days} days."); ->line('If you do not recognize this activity, we recommend [changing your password]('.route('profile.show').") as soon as possible. Otherwise, you can disregard this message. This link will expire in {$days} days.");
} }
/** /**
+4 -12
View File
@@ -1,26 +1,21 @@
<?php <?php
declare(strict_types=1);
namespace App\Policies; namespace App\Policies;
use App\Models\User;
use App\Models\Portfolio; use App\Models\Portfolio;
use App\Models\User;
class PortfolioPolicy class PortfolioPolicy
{ {
/**
*
*/
public function readOnly(User $user, Portfolio $portfolio) public function readOnly(User $user, Portfolio $portfolio)
{ {
$pivot = $portfolio->users()->where('user_id', $user->id)->first(); $pivot = $portfolio->users()->where('user_id', $user->id)->first();
return !!$pivot; return (bool) $pivot;
} }
/**
*
*/
public function fullAccess(User $user, Portfolio $portfolio) public function fullAccess(User $user, Portfolio $portfolio)
{ {
$pivot = $portfolio->users()->where('user_id', $user->id)->first(); $pivot = $portfolio->users()->where('user_id', $user->id)->first();
@@ -28,9 +23,6 @@ class PortfolioPolicy
return $pivot && ($pivot->pivot->full_access || $pivot->pivot->owner); return $pivot && ($pivot->pivot->full_access || $pivot->pivot->owner);
} }
/**
*
*/
public function owner(User $user, Portfolio $portfolio) public function owner(User $user, Portfolio $portfolio)
{ {
$pivot = $portfolio->users()->where('user_id', $user->id)->first(); $pivot = $portfolio->users()->where('user_id', $user->id)->first();
+4 -1
View File
@@ -1,7 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@@ -22,6 +25,6 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
// JsonResource::withoutWrapping();
} }
} }
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
use App\Actions\Fortify\CreateNewUser; use App\Actions\Fortify\CreateNewUser;
+16 -16
View File
@@ -1,11 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\Arr;
use Laravel\Jetstream\Features;
use App\Actions\Jetstream\DeleteUser; use App\Actions\Jetstream\DeleteUser;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Jetstream;
@@ -28,14 +27,6 @@ class JetstreamServiceProvider extends ServiceProvider
$this->configurePermissions(); $this->configurePermissions();
Jetstream::deleteUsersUsing(DeleteUser::class); Jetstream::deleteUsersUsing(DeleteUser::class);
if ( config('investbrain.self_hosted', false) ) {
Config::set(
'jetstream.features',
array_keys(Arr::except(array_values(config('jetstream.features')), Features::termsAndPrivacyPolicy()))
);
}
} }
/** /**
@@ -43,13 +34,22 @@ class JetstreamServiceProvider extends ServiceProvider
*/ */
protected function configurePermissions(): void protected function configurePermissions(): void
{ {
Jetstream::defaultApiTokenPermissions(['read']); Jetstream::defaultApiTokenPermissions([
// 'portfolio:read',
// 'portfolio:write',
// 'holding:read',
// 'holding:write',
// 'transaction:read',
// 'transaction:write',
]);
Jetstream::permissions([ Jetstream::permissions([
'create', // 'Read Portfolios' => 'portfolio:read',
'read', // 'Create Portfolios' => 'portfolio:write',
'update', // 'Read Holdings' => 'holding:read',
'delete', // 'Update Holdings' => 'holding:write',
// 'Read Transactions' => 'transaction:read',
// 'Create Transactions' => 'transaction:write',
]); ]);
} }
} }
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
+22 -19
View File
@@ -1,9 +1,12 @@
<?php <?php
declare(strict_types=1);
namespace App\Rules; namespace App\Rules;
use App\Models\Portfolio; use App\Models\Portfolio;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Carbon;
class QuantityValidationRule implements ValidationRule class QuantityValidationRule implements ValidationRule
{ {
@@ -13,41 +16,41 @@ class QuantityValidationRule implements ValidationRule
* @return void * @return void
*/ */
public function __construct( public function __construct(
protected Portfolio $portfolio, protected ?Portfolio $portfolio,
protected string $symbol, protected ?string $symbol,
protected string $transactionType, protected ?string $transactionType,
protected string $date protected string|Carbon|null $date
) { ) {
$this->portfolio = $portfolio; $this->portfolio = $portfolio;
$this->symbol = $symbol; $this->symbol = $symbol;
$this->transactionType = $transactionType; $this->transactionType = $transactionType;
$this->date = $date; $this->date = $date;
} }
/** /**
* Validate the attribute. * Validate the attribute.
*
* @param string $attribute
* @param mixed $value
* @param \Closure $fail
* @return void
*/ */
public function validate(string $attribute, mixed $value, \Closure $fail): void public function validate(string $attribute, mixed $value, \Closure $fail): void
{ {
if (is_null($this->portfolio) || is_null($this->symbol) || is_null($this->transactionType) || is_null($this->date)) {
//
$fail(__('The quantity must not be greater than the available quantity.'));
}
if ($this->transactionType == 'SELL') { if ($this->transactionType == 'SELL') {
$purchase_qty = $this->portfolio->transactions() $purchase_qty = $this->portfolio->transactions()
->symbol($this->symbol) ->symbol($this->symbol)
->buy() ->buy()
->beforeDate($this->date) ->beforeDate($this->date)
->sum('quantity'); ->sum('quantity');
$sales_qty = $this->portfolio->transactions() $sales_qty = $this->portfolio->transactions()
->symbol($this->symbol) ->symbol($this->symbol)
->sell() ->sell()
->beforeDate($this->date) ->beforeDate($this->date)
->sum('quantity'); ->sum('quantity');
$maxQuantity = $purchase_qty - $sales_qty; $maxQuantity = $purchase_qty - $sales_qty;
if (round($value, 3) > round($maxQuantity, 3)) { if (round($value, 3) > round($maxQuantity, 3)) {
+7 -9
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Rules; namespace App\Rules;
use App\Interfaces\MarketData\MarketDataInterface; use App\Interfaces\MarketData\MarketDataInterface;
@@ -22,24 +24,20 @@ class SymbolValidationRule implements ValidationRule
/** /**
* Validate the attribute. * Validate the attribute.
*
* @param string $attribute
* @param mixed $value
* @param \Closure $fail
* @return void
*/ */
public function validate(string $attribute, mixed $value, \Closure $fail): void public function validate(string $attribute, mixed $value, \Closure $fail): void
{ {
$this->symbol = $value; $this->symbol = $value;
// Check if the symbol exists in the Market Data table first (avoid API call)
if (MarketData::find($this->symbol)) { if (MarketData::find($this->symbol)) {
return; return;
} }
// Check if the symbol exists in the Market Data table first (avoid API call) // Then check against market data provider
if (!app(MarketDataInterface::class)->exists($value)) { if (! app(MarketDataInterface::class)->exists($value)) {
$fail('The symbol provided (' . $this->symbol . ') is not valid'); $fail('The symbol provided ('.$this->symbol.') is not valid');
} }
} }
} }
+3 -1
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
// if (!function_exists('formatMoney')) { // if (!function_exists('formatMoney')) {
// /** // /**
// * Returns a formatted string for currency // * Returns a formatted string for currency
+12 -12
View File
@@ -1,19 +1,19 @@
<?php <?php
declare(strict_types=1);
namespace App\Support; namespace App\Support;
use App\Models\Holding;
use App\Models\Portfolio;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class Spotlight class Spotlight
{ {
public function search(Request $request) public function search(Request $request)
{ {
$results = collect(); $results = collect();
if (!$request->user()) { if (! $request->user()) {
return $results; return $results;
} }
@@ -22,13 +22,13 @@ class Spotlight
->where('title', 'LIKE', '%'.$request->input('search').'%') ->where('title', 'LIKE', '%'.$request->input('search').'%')
->limit(5) ->limit(5)
->get(); ->get();
$portfolios->each(function($portfolio) use ($results) { $portfolios->each(function ($portfolio) use ($results) {
$results->push([ $results->push([
'name' => 'Portfolio: '. $portfolio->title, 'name' => 'Portfolio: '.$portfolio->title,
'description' => null, 'description' => null,
'link' => route('portfolio.show', ['portfolio' => $portfolio->id]), 'link' => route('portfolio.show', ['portfolio' => $portfolio->id]),
'avatar' => null 'avatar' => null,
]); ]);
}); });
@@ -36,20 +36,20 @@ class Spotlight
->where('holdings.quantity', '>', 0) ->where('holdings.quantity', '>', 0)
->where(function ($query) use ($request) { ->where(function ($query) use ($request) {
return $query->where('holdings.symbol', 'LIKE', '%'.$request->input('search').'%') return $query->where('holdings.symbol', 'LIKE', '%'.$request->input('search').'%')
->orWhere('market_data.name', 'LIKE', '%'.$request->input('search').'%'); ->orWhere('market_data.name', 'LIKE', '%'.$request->input('search').'%');
}) })
->limit(5) ->limit(5)
->get(); ->get();
$holdings->each(function($holding) use ($results) { $holdings->each(function ($holding) use ($results) {
$results->push([ $results->push([
'name' => 'Holding: '.$holding->market_data->name.' ('.$holding->symbol.')', 'name' => 'Holding: '.$holding->market_data->name.' ('.$holding->symbol.')',
'description' => $holding->portfolio->title, 'description' => $holding->portfolio->title,
'link' => route('holding.show', ['portfolio' => $holding->portfolio->id, 'symbol' => $holding->symbol]), 'link' => route('holding.show', ['portfolio' => $holding->portfolio->id, 'symbol' => $holding->symbol]),
'avatar' => null 'avatar' => null,
]); ]);
}); });
return $results; return $results;
} }
} }
+11 -7
View File
@@ -1,6 +1,8 @@
<?php <?php
namespace App\Traits; declare(strict_types=1);
namespace App\Traits;
trait HasCompositePrimaryKey trait HasCompositePrimaryKey
{ {
@@ -17,17 +19,18 @@ trait HasCompositePrimaryKey
/** /**
* Set the keys for a save update query. * Set the keys for a save update query.
* *
* @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
protected function setKeysForSaveQuery($query) protected function setKeysForSaveQuery($query)
{ {
foreach ($this->getKeyName() as $key) { foreach ($this->getKeyName() as $key) {
// UPDATE: Added isset() per devflow's comment. // UPDATE: Added isset() per devflow's comment.
if (isset($this->$key)) if (isset($this->$key)) {
$query->where($key, '=', $this->$key); $query->where($key, '=', $this->$key);
else } else {
throw new \Exception(__METHOD__ . 'Missing part of the primary key: ' . $key); throw new \Exception(__METHOD__.'Missing part of the primary key: '.$key);
}
} }
return $query; return $query;
@@ -37,7 +40,7 @@ trait HasCompositePrimaryKey
/** /**
* Execute a query for a single record by ID. * Execute a query for a single record by ID.
* *
* @param array $ids Array of keys, like [column => value]. * @param array $ids Array of keys, like [column => value].
* @param array $columns * @param array $columns
* @return mixed|static * @return mixed|static
*/ */
@@ -48,6 +51,7 @@ trait HasCompositePrimaryKey
foreach ($me->getKeyName() as $key) { foreach ($me->getKeyName() as $key) {
$query->where($key, '=', $ids[$key]); $query->where($key, '=', $ids[$key]);
} }
return $query->first($columns); return $query->first($columns);
} }
} }
+4 -2
View File
@@ -1,12 +1,14 @@
<?php <?php
declare(strict_types=1);
namespace App\Traits; namespace App\Traits;
use App\Models\ConnectedAccount; use App\Models\ConnectedAccount;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
/** /**
* @property Collection $connectedAccounts * @property Collection $connectedAccounts
@@ -63,4 +65,4 @@ trait HasConnectedAccounts
{ {
return $this->hasMany(ConnectedAccount::class); return $this->hasMany(ConnectedAccount::class);
} }
} }
+4 -2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Traits; namespace App\Traits;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -13,10 +15,10 @@ trait WithTrimStrings
public function updatedWithTrimStrings(string $property, mixed $value): void public function updatedWithTrimStrings(string $property, mixed $value): void
{ {
if (is_string($value) && !in_array($property, $this->trimExceptions())) { if (is_string($value) && ! in_array($property, $this->trimExceptions())) {
$this->fill([ $this->fill([
$property => Str::trim($value), $property => Str::trim($value),
]); ]);
} }
} }
} }
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\View\Components; namespace App\View\Components;
use Illuminate\View\Component; use Illuminate\View\Component;
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\View\Components; namespace App\View\Components;
use Illuminate\View\Component; use Illuminate\View\Component;
+3 -1
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\View\Components; namespace App\View\Components;
use Illuminate\View\Component; use Illuminate\View\Component;
@@ -11,7 +13,7 @@ class MainLayout extends Component
// Slots // Slots
public mixed $body = null, public mixed $body = null,
) { } ) {}
/** /**
* Get the view / contents that represents the component. * Get the view / contents that represents the component.
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
use App\Http\Middleware\SetLocale; use App\Http\Middleware\SetLocale;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
return [ return [
App\Providers\AppServiceProvider::class, App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class, App\Providers\FortifyServiceProvider::class,
+7
View File
@@ -10,6 +10,7 @@
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-zip": "*", "ext-zip": "*",
"finnhub/client": "master@dev", "finnhub/client": "master@dev",
"hackeresq/filter-models": "dev-main",
"laravel/framework": "^11.35", "laravel/framework": "^11.35",
"laravel/jetstream": "^5.1", "laravel/jetstream": "^5.1",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
@@ -37,6 +38,12 @@
"repositories": [ "repositories": [
{ {
"type": "vcs", "type": "vcs",
"no-api": true,
"url": "https://github.com/hackeresq/filter-models"
},
{
"type": "vcs",
"no-api": true,
"url": "https://github.com/investbrainapp/finnhub-php" "url": "https://github.com/investbrainapp/finnhub-php"
} }
], ],
Generated
+230 -107
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "d1b7456f149ebd4a89f5666f931c03fd", "content-hash": "61f4684da15779e44eb45ce4e90aecb1",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@@ -62,16 +62,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.338.2", "version": "3.339.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "7a52364e053d74363f9976dfb4473bace5b7790e" "reference": "3675e58c8fa971f4b4a24e7b0bee8673bda1ba00"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7a52364e053d74363f9976dfb4473bace5b7790e", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3675e58c8fa971f4b4a24e7b0bee8673bda1ba00",
"reference": "7a52364e053d74363f9976dfb4473bace5b7790e", "reference": "3675e58c8fa971f4b4a24e7b0bee8673bda1ba00",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -154,9 +154,9 @@
"support": { "support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions", "forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues", "issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.338.2" "source": "https://github.com/aws/aws-sdk-php/tree/3.339.1"
}, },
"time": "2025-01-24T19:09:22+00:00" "time": "2025-01-28T19:05:47+00:00"
}, },
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@@ -491,6 +491,85 @@
], ],
"time": "2024-02-09T16:56:22+00:00" "time": "2024-02-09T16:56:22+00:00"
}, },
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{ {
"name": "composer/semver", "name": "composer/semver",
"version": "3.4.3", "version": "3.4.3",
@@ -1063,7 +1142,7 @@
"version": "dev-master", "version": "dev-master",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/investbrainapp/finnhub-php.git", "url": "https://github.com/investbrainapp/finnhub-php",
"reference": "1f1b35a0c0a6a68f9a791e3ac5cdb6f44ff69d80" "reference": "1f1b35a0c0a6a68f9a791e3ac5cdb6f44ff69d80"
}, },
"dist": { "dist": {
@@ -1115,9 +1194,6 @@
"rest", "rest",
"sdk" "sdk"
], ],
"support": {
"source": "https://github.com/investbrainapp/finnhub-php/tree/master"
},
"time": "2024-09-13T01:29:18+00:00" "time": "2024-09-13T01:29:18+00:00"
}, },
{ {
@@ -1727,6 +1803,50 @@
], ],
"time": "2023-12-03T19:50:20+00:00" "time": "2023-12-03T19:50:20+00:00"
}, },
{
"name": "hackeresq/filter-models",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://github.com/hackeresq/filter-models",
"reference": "847950d3277fe7df3a2dcdcdd3ba37d3b07ee667"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/hackeresq/filter-models/zipball/847950d3277fe7df3a2dcdcdd3ba37d3b07ee667",
"reference": "847950d3277fe7df3a2dcdcdd3ba37d3b07ee667",
"shasum": ""
},
"require": {
"laravel/framework": "^11.9",
"php": "^8.2"
},
"require-dev": {
"orchestra/testbench": "^9.9",
"phpunit/phpunit": "^10.0|^11.0"
},
"default-branch": true,
"type": "library",
"extra": {
"laravel": {
"providers": [
"HackerEsq\\FilterModels\\FilterModelsServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"HackerEsq\\FilterModels\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HackerESQ\\FilterModels\\Tests\\": "tests"
}
},
"description": "Simple package to filter your Laravel models with query parameters",
"time": "2025-01-27T23:18:08+00:00"
},
{ {
"name": "jfcherng/php-color-output", "name": "jfcherng/php-color-output",
"version": "3.0.0", "version": "3.0.0",
@@ -1966,31 +2086,31 @@
}, },
{ {
"name": "laravel/fortify", "name": "laravel/fortify",
"version": "v1.25.3", "version": "v1.25.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/fortify.git", "url": "https://github.com/laravel/fortify.git",
"reference": "ee35e5b8ea25cc51f8323e27a839283becd44160" "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/fortify/zipball/ee35e5b8ea25cc51f8323e27a839283becd44160", "url": "https://api.github.com/repos/laravel/fortify/zipball/f185600e2d3a861834ad00ee3b7863f26ac25d3f",
"reference": "ee35e5b8ea25cc51f8323e27a839283becd44160", "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"bacon/bacon-qr-code": "^3.0", "bacon/bacon-qr-code": "^3.0",
"ext-json": "*", "ext-json": "*",
"illuminate/support": "^10.0|^11.0", "illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.1", "php": "^8.1",
"pragmarx/google2fa": "^8.0", "pragmarx/google2fa": "^8.0",
"symfony/console": "^6.0|^7.0" "symfony/console": "^6.0|^7.0"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.0", "mockery/mockery": "^1.0",
"orchestra/testbench": "^8.16|^9.0", "orchestra/testbench": "^8.16|^9.0|^10.0",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.4" "phpunit/phpunit": "^10.4|^11.3"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@@ -2027,20 +2147,20 @@
"issues": "https://github.com/laravel/fortify/issues", "issues": "https://github.com/laravel/fortify/issues",
"source": "https://github.com/laravel/fortify" "source": "https://github.com/laravel/fortify"
}, },
"time": "2025-01-17T15:17:57+00:00" "time": "2025-01-26T19:34:46+00:00"
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v11.40.0", "version": "v11.41.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "599a28196d284fee158cc10086fd56ac625ad7a3" "reference": "42d6ae000c868c2abfa946da46702f2358493482"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/599a28196d284fee158cc10086fd56ac625ad7a3", "url": "https://api.github.com/repos/laravel/framework/zipball/42d6ae000c868c2abfa946da46702f2358493482",
"reference": "599a28196d284fee158cc10086fd56ac625ad7a3", "reference": "42d6ae000c868c2abfa946da46702f2358493482",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2242,7 +2362,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2025-01-24T16:17:42+00:00" "time": "2025-01-28T15:22:55+00:00"
}, },
{ {
"name": "laravel/jetstream", "name": "laravel/jetstream",
@@ -2313,16 +2433,16 @@
}, },
{ {
"name": "laravel/prompts", "name": "laravel/prompts",
"version": "v0.3.3", "version": "v0.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/prompts.git", "url": "https://github.com/laravel/prompts.git",
"reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea" "reference": "abeaa2ba4294247d5409490d1ca1bc6248087011"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/prompts/zipball/749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", "url": "https://api.github.com/repos/laravel/prompts/zipball/abeaa2ba4294247d5409490d1ca1bc6248087011",
"reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", "reference": "abeaa2ba4294247d5409490d1ca1bc6248087011",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2336,7 +2456,7 @@
"laravel/framework": ">=10.17.0 <10.25.0" "laravel/framework": ">=10.17.0 <10.25.0"
}, },
"require-dev": { "require-dev": {
"illuminate/collections": "^10.0|^11.0", "illuminate/collections": "^10.0|^11.0|^12.0",
"mockery/mockery": "^1.5", "mockery/mockery": "^1.5",
"pestphp/pest": "^2.3|^3.4", "pestphp/pest": "^2.3|^3.4",
"phpstan/phpstan": "^1.11", "phpstan/phpstan": "^1.11",
@@ -2366,38 +2486,38 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.", "description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": { "support": {
"issues": "https://github.com/laravel/prompts/issues", "issues": "https://github.com/laravel/prompts/issues",
"source": "https://github.com/laravel/prompts/tree/v0.3.3" "source": "https://github.com/laravel/prompts/tree/v0.3.4"
}, },
"time": "2024-12-30T15:53:31+00:00" "time": "2025-01-24T15:41:01+00:00"
}, },
{ {
"name": "laravel/sanctum", "name": "laravel/sanctum",
"version": "v4.0.7", "version": "v4.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/sanctum.git", "url": "https://github.com/laravel/sanctum.git",
"reference": "698064236a46df016e64a7eb059b1414e0b281df" "reference": "ec1dd9ddb2ab370f79dfe724a101856e0963f43c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/698064236a46df016e64a7eb059b1414e0b281df", "url": "https://api.github.com/repos/laravel/sanctum/zipball/ec1dd9ddb2ab370f79dfe724a101856e0963f43c",
"reference": "698064236a46df016e64a7eb059b1414e0b281df", "reference": "ec1dd9ddb2ab370f79dfe724a101856e0963f43c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
"illuminate/console": "^11.0", "illuminate/console": "^11.0|^12.0",
"illuminate/contracts": "^11.0", "illuminate/contracts": "^11.0|^12.0",
"illuminate/database": "^11.0", "illuminate/database": "^11.0|^12.0",
"illuminate/support": "^11.0", "illuminate/support": "^11.0|^12.0",
"php": "^8.2", "php": "^8.2",
"symfony/console": "^7.0" "symfony/console": "^7.0"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0", "orchestra/testbench": "^9.0|^10.0",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.5" "phpunit/phpunit": "^11.3"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@@ -2432,29 +2552,29 @@
"issues": "https://github.com/laravel/sanctum/issues", "issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum" "source": "https://github.com/laravel/sanctum"
}, },
"time": "2024-12-11T16:40:21+00:00" "time": "2025-01-26T19:34:36+00:00"
}, },
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",
"version": "v2.0.1", "version": "v2.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/serializable-closure.git", "url": "https://github.com/laravel/serializable-closure.git",
"reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8" "reference": "2e1a362527783bcab6c316aad51bf36c5513ae44"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/613b2d4998f85564d40497e05e89cb6d9bd1cbe8", "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2e1a362527783bcab6c316aad51bf36c5513ae44",
"reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8", "reference": "2e1a362527783bcab6c316aad51bf36c5513ae44",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^8.1" "php": "^8.1"
}, },
"require-dev": { "require-dev": {
"illuminate/support": "^10.0|^11.0", "illuminate/support": "^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.67|^3.0", "nesbot/carbon": "^2.67|^3.0",
"pestphp/pest": "^2.36", "pestphp/pest": "^2.36|^3.0",
"phpstan/phpstan": "^2.0", "phpstan/phpstan": "^2.0",
"symfony/var-dumper": "^6.2.0|^7.0.0" "symfony/var-dumper": "^6.2.0|^7.0.0"
}, },
@@ -2493,38 +2613,38 @@
"issues": "https://github.com/laravel/serializable-closure/issues", "issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure" "source": "https://github.com/laravel/serializable-closure"
}, },
"time": "2024-12-16T15:26:28+00:00" "time": "2025-01-24T15:42:37+00:00"
}, },
{ {
"name": "laravel/socialite", "name": "laravel/socialite",
"version": "v5.17.0", "version": "v5.17.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/socialite.git", "url": "https://github.com/laravel/socialite.git",
"reference": "77be8be7ee5099aed8ca7cfddc1bf6f9ab3fc159" "reference": "4b44c97c04da28e5aabb73df70b0999e9976382f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/77be8be7ee5099aed8ca7cfddc1bf6f9ab3fc159", "url": "https://api.github.com/repos/laravel/socialite/zipball/4b44c97c04da28e5aabb73df70b0999e9976382f",
"reference": "77be8be7ee5099aed8ca7cfddc1bf6f9ab3fc159", "reference": "4b44c97c04da28e5aabb73df70b0999e9976382f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
"firebase/php-jwt": "^6.4", "firebase/php-jwt": "^6.4",
"guzzlehttp/guzzle": "^6.0|^7.0", "guzzlehttp/guzzle": "^6.0|^7.0",
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"league/oauth1-client": "^1.11", "league/oauth1-client": "^1.11",
"php": "^7.2|^8.0", "php": "^7.2|^8.0",
"phpseclib/phpseclib": "^3.0" "phpseclib/phpseclib": "^3.0"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.0", "mockery/mockery": "^1.0",
"orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^8.0|^9.3|^10.4" "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@@ -2565,26 +2685,26 @@
"issues": "https://github.com/laravel/socialite/issues", "issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite" "source": "https://github.com/laravel/socialite"
}, },
"time": "2025-01-17T15:17:00+00:00" "time": "2025-01-28T15:16:52+00:00"
}, },
{ {
"name": "laravel/tinker", "name": "laravel/tinker",
"version": "v2.10.0", "version": "v2.10.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/tinker.git", "url": "https://github.com/laravel/tinker.git",
"reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5" "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/tinker/zipball/ba4d51eb56de7711b3a37d63aa0643e99a339ae5", "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3",
"reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5", "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.2.5|^8.0", "php": "^7.2.5|^8.0",
"psy/psysh": "^0.11.1|^0.12.0", "psy/psysh": "^0.11.1|^0.12.0",
"symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0"
@@ -2592,10 +2712,10 @@
"require-dev": { "require-dev": {
"mockery/mockery": "~1.3.3|^1.4.2", "mockery/mockery": "~1.3.3|^1.4.2",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^8.5.8|^9.3.3" "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0"
}, },
"suggest": { "suggest": {
"illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0)." "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)."
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@@ -2629,9 +2749,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/laravel/tinker/issues", "issues": "https://github.com/laravel/tinker/issues",
"source": "https://github.com/laravel/tinker/tree/v2.10.0" "source": "https://github.com/laravel/tinker/tree/v2.10.1"
}, },
"time": "2024-09-23T13:32:56+00:00" "time": "2025-01-27T14:24:01+00:00"
}, },
{ {
"name": "league/commonmark", "name": "league/commonmark",
@@ -3317,16 +3437,16 @@
}, },
{ {
"name": "livewire/livewire", "name": "livewire/livewire",
"version": "v3.5.18", "version": "v3.5.19",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/livewire/livewire.git", "url": "https://github.com/livewire/livewire.git",
"reference": "62f0fa6b340a467c25baa590a567d9a134b357da" "reference": "3f1b2c134cde537bb7666e490ea21a8ffc8bbf14"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/livewire/livewire/zipball/62f0fa6b340a467c25baa590a567d9a134b357da", "url": "https://api.github.com/repos/livewire/livewire/zipball/3f1b2c134cde537bb7666e490ea21a8ffc8bbf14",
"reference": "62f0fa6b340a467c25baa590a567d9a134b357da", "reference": "3f1b2c134cde537bb7666e490ea21a8ffc8bbf14",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3381,7 +3501,7 @@
"description": "A front-end framework for Laravel.", "description": "A front-end framework for Laravel.",
"support": { "support": {
"issues": "https://github.com/livewire/livewire/issues", "issues": "https://github.com/livewire/livewire/issues",
"source": "https://github.com/livewire/livewire/tree/v3.5.18" "source": "https://github.com/livewire/livewire/tree/v3.5.19"
}, },
"funding": [ "funding": [
{ {
@@ -3389,7 +3509,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-12-23T15:05:02+00:00" "time": "2025-01-28T21:39:51+00:00"
}, },
{ {
"name": "livewire/volt", "name": "livewire/volt",
@@ -3546,31 +3666,32 @@
}, },
{ {
"name": "maennchen/zipstream-php", "name": "maennchen/zipstream-php",
"version": "3.1.1", "version": "3.1.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git", "url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "6187e9cc4493da94b9b63eb2315821552015fca9" "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9", "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
"reference": "6187e9cc4493da94b9b63eb2315821552015fca9", "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-zlib": "*", "ext-zlib": "*",
"php-64bit": "^8.1" "php-64bit": "^8.2"
}, },
"require-dev": { "require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*", "ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16", "friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5", "guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6", "mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5", "php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^10.0", "phpunit/phpunit": "^11.0",
"vimeo/psalm": "^5.0" "vimeo/psalm": "^6.0"
}, },
"suggest": { "suggest": {
"guzzlehttp/psr7": "^2.4", "guzzlehttp/psr7": "^2.4",
@@ -3611,7 +3732,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues", "issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1" "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
}, },
"funding": [ "funding": [
{ {
@@ -3619,7 +3740,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-10-10T12:33:01+00:00" "time": "2025-01-27T12:07:53+00:00"
}, },
{ {
"name": "markbaker/complex", "name": "markbaker/complex",
@@ -4706,19 +4827,20 @@
}, },
{ {
"name": "phpoffice/phpspreadsheet", "name": "phpoffice/phpspreadsheet",
"version": "1.29.8", "version": "1.29.9",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "089ffdfc04b5fcf25a3503d81a4e589f247e20e3" "reference": "ffb47b639649fc9c8a6fa67977a27b756592ed85"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/089ffdfc04b5fcf25a3503d81a4e589f247e20e3", "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/ffb47b639649fc9c8a6fa67977a27b756592ed85",
"reference": "089ffdfc04b5fcf25a3503d81a4e589f247e20e3", "reference": "ffb47b639649fc9c8a6fa67977a27b756592ed85",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"composer/pcre": "^3.3",
"ext-ctype": "*", "ext-ctype": "*",
"ext-dom": "*", "ext-dom": "*",
"ext-fileinfo": "*", "ext-fileinfo": "*",
@@ -4805,9 +4927,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.8" "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.9"
}, },
"time": "2025-01-12T03:16:27+00:00" "time": "2025-01-26T04:55:00+00:00"
}, },
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
@@ -9015,28 +9137,28 @@
}, },
{ {
"name": "laravel/sail", "name": "laravel/sail",
"version": "v1.40.0", "version": "v1.41.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/sail.git", "url": "https://github.com/laravel/sail.git",
"reference": "237e70656d8eface4839de51d101284bd5d0cf71" "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/237e70656d8eface4839de51d101284bd5d0cf71", "url": "https://api.github.com/repos/laravel/sail/zipball/fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec",
"reference": "237e70656d8eface4839de51d101284bd5d0cf71", "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"illuminate/console": "^9.52.16|^10.0|^11.0", "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0",
"illuminate/contracts": "^9.52.16|^10.0|^11.0", "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0",
"illuminate/support": "^9.52.16|^10.0|^11.0", "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0",
"php": "^8.0", "php": "^8.0",
"symfony/console": "^6.0|^7.0", "symfony/console": "^6.0|^7.0",
"symfony/yaml": "^6.0|^7.0" "symfony/yaml": "^6.0|^7.0"
}, },
"require-dev": { "require-dev": {
"orchestra/testbench": "^7.0|^8.0|^9.0", "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
"phpstan/phpstan": "^1.10" "phpstan/phpstan": "^1.10"
}, },
"bin": [ "bin": [
@@ -9074,7 +9196,7 @@
"issues": "https://github.com/laravel/sail/issues", "issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail" "source": "https://github.com/laravel/sail"
}, },
"time": "2025-01-13T16:57:11+00:00" "time": "2025-01-24T15:45:36+00:00"
}, },
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
@@ -9760,16 +9882,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "11.5.3", "version": "11.5.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "30e319e578a7b5da3543073e30002bf82042f701" "reference": "e0da3559ec50a91f6a6a201473b607b5ccfd9a1b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/30e319e578a7b5da3543073e30002bf82042f701", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e0da3559ec50a91f6a6a201473b607b5ccfd9a1b",
"reference": "30e319e578a7b5da3543073e30002bf82042f701", "reference": "e0da3559ec50a91f6a6a201473b607b5ccfd9a1b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9841,7 +9963,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.3" "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.4"
}, },
"funding": [ "funding": [
{ {
@@ -9857,7 +9979,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-01-13T09:36:00+00:00" "time": "2025-01-28T15:03:46+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",
@@ -10963,7 +11085,8 @@
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": { "stability-flags": {
"finnhub/client": 20 "finnhub/client": 20,
"hackeresq/filter-models": 20
}, },
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
return [ return [
'key' => env('ALPHAVANTAGE_API_KEY'), 'key' => env('ALPHAVANTAGE_API_KEY'),
]; ];
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
return [ return [
/* /*
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
return [ return [
/* /*
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
use Illuminate\Support\Str; use Illuminate\Support\Str;
return [ return [
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
use Illuminate\Support\Str; use Illuminate\Support\Str;
return [ return [
+65 -63
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
use Maatwebsite\Excel\Excel; use Maatwebsite\Excel\Excel;
use PhpOffice\PhpSpreadsheet\Reader\Csv; use PhpOffice\PhpSpreadsheet\Reader\Csv;
@@ -15,7 +17,7 @@ return [
| Here you can specify how big the chunk should be. | Here you can specify how big the chunk should be.
| |
*/ */
'chunk_size' => 1000, 'chunk_size' => 1000,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -42,15 +44,15 @@ return [
| Configure e.g. delimiter, enclosure and line ending for CSV exports. | Configure e.g. delimiter, enclosure and line ending for CSV exports.
| |
*/ */
'csv' => [ 'csv' => [
'delimiter' => ',', 'delimiter' => ',',
'enclosure' => '"', 'enclosure' => '"',
'line_ending' => PHP_EOL, 'line_ending' => PHP_EOL,
'use_bom' => false, 'use_bom' => false,
'include_separator_line' => false, 'include_separator_line' => false,
'excel_compatibility' => false, 'excel_compatibility' => false,
'output_encoding' => '', 'output_encoding' => '',
'test_auto_detect' => true, 'test_auto_detect' => true,
], ],
/* /*
@@ -61,20 +63,20 @@ return [
| Configure e.g. default title, creator, subject,... | Configure e.g. default title, creator, subject,...
| |
*/ */
'properties' => [ 'properties' => [
'creator' => '', 'creator' => '',
'lastModifiedBy' => '', 'lastModifiedBy' => '',
'title' => '', 'title' => '',
'description' => '', 'description' => '',
'subject' => '', 'subject' => '',
'keywords' => '', 'keywords' => '',
'category' => '', 'category' => '',
'manager' => '', 'manager' => '',
'company' => '', 'company' => '',
], ],
], ],
'imports' => [ 'imports' => [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -87,7 +89,7 @@ return [
| you can enable it by setting read_only to false. | you can enable it by setting read_only to false.
| |
*/ */
'read_only' => true, 'read_only' => true,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -111,7 +113,7 @@ return [
| Available options: none|slug|custom | Available options: none|slug|custom
| |
*/ */
'heading_row' => [ 'heading_row' => [
'formatter' => 'slug', 'formatter' => 'slug',
], ],
@@ -123,12 +125,12 @@ return [
| Configure e.g. delimiter, enclosure and line ending for CSV imports. | Configure e.g. delimiter, enclosure and line ending for CSV imports.
| |
*/ */
'csv' => [ 'csv' => [
'delimiter' => null, 'delimiter' => null,
'enclosure' => '"', 'enclosure' => '"',
'escape_character' => '\\', 'escape_character' => '\\',
'contiguous' => false, 'contiguous' => false,
'input_encoding' => Csv::GUESS_ENCODING, 'input_encoding' => Csv::GUESS_ENCODING,
], ],
/* /*
@@ -139,16 +141,16 @@ return [
| Configure e.g. default title, creator, subject,... | Configure e.g. default title, creator, subject,...
| |
*/ */
'properties' => [ 'properties' => [
'creator' => '', 'creator' => '',
'lastModifiedBy' => '', 'lastModifiedBy' => '',
'title' => '', 'title' => '',
'description' => '', 'description' => '',
'subject' => '', 'subject' => '',
'keywords' => '', 'keywords' => '',
'category' => '', 'category' => '',
'manager' => '', 'manager' => '',
'company' => '', 'company' => '',
], ],
/* /*
@@ -159,10 +161,10 @@ return [
| Configure middleware that is executed on getting a cell value | Configure middleware that is executed on getting a cell value
| |
*/ */
'cells' => [ 'cells' => [
'middleware' => [ 'middleware' => [
//\Maatwebsite\Excel\Middleware\TrimCellValue::class, // \Maatwebsite\Excel\Middleware\TrimCellValue::class,
//\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class, // \Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
], ],
], ],
@@ -178,21 +180,21 @@ return [
| |
*/ */
'extension_detector' => [ 'extension_detector' => [
'xlsx' => Excel::XLSX, 'xlsx' => Excel::XLSX,
'xlsm' => Excel::XLSX, 'xlsm' => Excel::XLSX,
'xltx' => Excel::XLSX, 'xltx' => Excel::XLSX,
'xltm' => Excel::XLSX, 'xltm' => Excel::XLSX,
'xls' => Excel::XLS, 'xls' => Excel::XLS,
'xlt' => Excel::XLS, 'xlt' => Excel::XLS,
'ods' => Excel::ODS, 'ods' => Excel::ODS,
'ots' => Excel::ODS, 'ots' => Excel::ODS,
'slk' => Excel::SLK, 'slk' => Excel::SLK,
'xml' => Excel::XML, 'xml' => Excel::XML,
'gnumeric' => Excel::GNUMERIC, 'gnumeric' => Excel::GNUMERIC,
'htm' => Excel::HTML, 'htm' => Excel::HTML,
'html' => Excel::HTML, 'html' => Excel::HTML,
'csv' => Excel::CSV, 'csv' => Excel::CSV,
'tsv' => Excel::TSV, 'tsv' => Excel::TSV,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -203,7 +205,7 @@ return [
| Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF | Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
| |
*/ */
'pdf' => Excel::DOMPDF, 'pdf' => Excel::DOMPDF,
], ],
/* /*
@@ -223,11 +225,11 @@ return [
| [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class | [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
| |
*/ */
'value_binder' => [ 'value_binder' => [
'default' => Maatwebsite\Excel\DefaultValueBinder::class, 'default' => Maatwebsite\Excel\DefaultValueBinder::class,
], ],
'cache' => [ 'cache' => [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Default cell caching driver | Default cell caching driver
@@ -244,7 +246,7 @@ return [
| Drivers: memory|illuminate|batch | Drivers: memory|illuminate|batch
| |
*/ */
'driver' => 'memory', 'driver' => 'memory',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -256,7 +258,7 @@ return [
| Here you can tweak the memory limit to your liking. | Here you can tweak the memory limit to your liking.
| |
*/ */
'batch' => [ 'batch' => [
'memory_limit' => 60000, 'memory_limit' => 60000,
], ],
@@ -272,7 +274,7 @@ return [
| at "null" it will use the default store. | at "null" it will use the default store.
| |
*/ */
'illuminate' => [ 'illuminate' => [
'store' => null, 'store' => null,
], ],
@@ -308,7 +310,7 @@ return [
*/ */
'transactions' => [ 'transactions' => [
'handler' => 'db', 'handler' => 'db',
'db' => [ 'db' => [
'connection' => null, 'connection' => null,
], ],
], ],
@@ -326,7 +328,7 @@ return [
| and the create file (file). | and the create file (file).
| |
*/ */
'local_path' => storage_path('framework/cache/laravel-excel'), 'local_path' => storage_path('framework/cache/laravel-excel'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -338,7 +340,7 @@ return [
| If omitted the default permissions of the filesystem will be used. | If omitted the default permissions of the filesystem will be used.
| |
*/ */
'local_permissions' => [ 'local_permissions' => [
// 'dir' => 0755, // 'dir' => 0755,
// 'file' => 0644, // 'file' => 0644,
], ],
@@ -357,8 +359,8 @@ return [
| in conjunction with queued imports and exports. | in conjunction with queued imports and exports.
| |
*/ */
'remote_disk' => env('TEMP_UPLOAD_DISK', null), 'remote_disk' => env('TEMP_UPLOAD_DISK', null),
'remote_prefix' => 'excel-tmp', 'remote_prefix' => 'excel-tmp',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
return [ return [
/* /*
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
return [ return [
'key' => env('FINNHUB_API_KEY'), 'key' => env('FINNHUB_API_KEY'),
]; ];
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
use Laravel\Fortify\Features; use Laravel\Fortify\Features;
return [ return [

Some files were not shown because too many files have changed in this diff Show More