Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 219018b1d9 | |||
| 4b780fd6d2 | |||
| 1faa22897b | |||
| 7e1899d8ff | |||
| 878c668696 | |||
| 8c94fbf299 | |||
| 4ece09368e | |||
| 0f135f4024 | |||
| eac5de0d4a | |||
| 399858d09b | |||
| 7694d8a241 | |||
| 9bd406c5b1 | |||
| d23d28afd8 | |||
| 0a6b2d844f | |||
| be325d31b6 | |||
| e08c1880c6 | |||
| 5f9f6f01c5 | |||
| 65388238c3 | |||
| cdce46b6df | |||
| 8320b54332 | |||
| e8ef0921ad | |||
| c4736fae70 | |||
| 1748f49ee6 | |||
| c32641ec34 | |||
| 53ebe28b14 | |||
| 465686dbaf | |||
| 58604c1e5a | |||
| 3e4f055a4a | |||
| 92586d7466 | |||
| 94c90b8a7c | |||
| f866baa37a | |||
| da72c17cd0 | |||
| 1c5c4af477 | |||
| 83d5ad213b | |||
| ea22c27710 | |||
| 32bf256c84 | |||
| e498e7668e | |||
| f58fbf9d6d | |||
| 5e56c97bf9 | |||
| ea4602abc7 | |||
| 169eabd800 | |||
| 62dcae48bb | |||
| b8f24d4b67 | |||
| 6d9e0008b8 | |||
| b9d41f9ac0 | |||
| f724f450f2 | |||
| cc447c5fb0 | |||
| b3f0f89d16 |
@@ -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
|
||||||
|
|||||||
@@ -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,12 +1,14 @@
|
|||||||
<?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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +45,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ class RefreshDividendData extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -53,9 +56,13 @@ class RefreshMarketData extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($holdings->get() as $holding) {
|
foreach ($holdings->get() as $holding) {
|
||||||
$this->line('Refreshing ' . $holding->symbol);
|
$this->line('Refreshing '.$holding->symbol);
|
||||||
|
|
||||||
|
try {
|
||||||
MarketData::getMarketData($holding->symbol, $force);
|
MarketData::getMarketData($holding->symbol, $force);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Could not refresh '.$holding->symbol.' ('.$e->getMessage().')');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +44,12 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -68,7 +71,7 @@ class SyncDailyChange extends Command implements PromptsForMissingInput
|
|||||||
|
|
||||||
$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) {
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +25,7 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle
|
|||||||
'Total Gain',
|
'Total Gain',
|
||||||
'Total Dividends Earned',
|
'Total Dividends Earned',
|
||||||
'Realized Gains',
|
'Realized Gains',
|
||||||
'Annotation'
|
'Annotation',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +37,6 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle
|
|||||||
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';
|
||||||
|
|||||||
@@ -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,7 +13,7 @@ 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
|
||||||
{
|
{
|
||||||
@@ -21,7 +23,7 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle
|
|||||||
'Notes',
|
'Notes',
|
||||||
'Wishlist',
|
'Wishlist',
|
||||||
'Created',
|
'Created',
|
||||||
'Updated'
|
'Updated',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,9 +35,6 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle
|
|||||||
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';
|
||||||
|
|||||||
@@ -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,7 +29,7 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
|
|||||||
'Reinvested Dividend',
|
'Reinvested Dividend',
|
||||||
'Date',
|
'Date',
|
||||||
'Created',
|
'Created',
|
||||||
'Updated'
|
'Updated',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +41,6 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
|
|||||||
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';
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -47,19 +45,19 @@ 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,12 +66,12 @@ 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;
|
||||||
@@ -100,7 +98,7 @@ class ConnectedAccountController extends Controller
|
|||||||
|
|
||||||
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.');
|
||||||
}
|
}
|
||||||
@@ -108,7 +106,7 @@ class ConnectedAccountController extends Controller
|
|||||||
|
|
||||||
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',
|
||||||
]
|
],
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
abstract class Controller
|
abstract class Controller
|
||||||
|
|||||||
@@ -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,7 +18,7 @@ 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
|
||||||
|
|||||||
@@ -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,17 +10,16 @@ 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)
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,15 +24,13 @@ 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()
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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')
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -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};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,40 @@
|
|||||||
<?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();
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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,17 +77,17 @@ 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,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<?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)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ trait ValidatesPortfolioAccess
|
|||||||
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,31 +1,31 @@
|
|||||||
<?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,
|
||||||
@@ -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,21 +49,21 @@ 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,
|
||||||
@@ -73,17 +73,17 @@ class AlphaVantageMarketData implements MarketDataInterface
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -93,7 +93,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
||||||
@@ -105,15 +105,15 @@ class AlphaVantageMarketData implements MarketDataInterface
|
|||||||
|
|
||||||
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'),
|
||||||
]) ];
|
])];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,7 +59,7 @@ class FakeMarketData implements MarketDataInterface
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function splits(String $symbol, $startDate, $endDate): Collection
|
public function splits(string $symbol, $startDate, $endDate): Collection
|
||||||
{
|
{
|
||||||
|
|
||||||
return collect([
|
return collect([
|
||||||
@@ -65,11 +67,11 @@ class FakeMarketData implements MarketDataInterface
|
|||||||
'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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<?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)
|
||||||
@@ -19,8 +20,9 @@ class FallbackInterface
|
|||||||
$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.");
|
||||||
}
|
}
|
||||||
@@ -37,6 +39,13 @@ class FallbackInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -18,27 +20,26 @@ class FinnhubMarketData implements MarketDataInterface
|
|||||||
{
|
{
|
||||||
|
|
||||||
$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');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ class FinnhubMarketData implements MarketDataInterface
|
|||||||
{
|
{
|
||||||
$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,
|
||||||
@@ -76,7 +77,7 @@ class FinnhubMarketData implements MarketDataInterface
|
|||||||
|
|
||||||
$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,
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ class Quote extends MarketDataType
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@ class Quote extends MarketDataType
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +60,7 @@ class Quote extends MarketDataType
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +72,7 @@ class Quote extends MarketDataType
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +84,7 @@ class Quote extends MarketDataType
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +108,7 @@ class Quote extends MarketDataType
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +132,7 @@ class Quote extends MarketDataType
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +1,59 @@
|
|||||||
<?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,
|
||||||
@@ -62,11 +63,11 @@ class YahooMarketData implements MarketDataInterface
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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([
|
||||||
@@ -77,19 +78,19 @@ class YahooMarketData implements MarketDataInterface
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
||||||
]) ];
|
])];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -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()));
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -47,7 +47,7 @@ class BackupImport extends Model
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'has_errors' => 'boolean',
|
'has_errors' => 'boolean',
|
||||||
'completed_at' => 'datetime'
|
'completed_at' => 'datetime',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -47,7 +49,8 @@ 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);
|
||||||
});
|
});
|
||||||
|
|||||||
+17
-16
@@ -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,7 +65,7 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -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()]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ class Dividend extends Model
|
|||||||
->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'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -145,15 +146,15 @@ class Dividend extends Model
|
|||||||
'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,
|
||||||
|
|||||||
+32
-33
@@ -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,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,7 +60,7 @@ class Holding extends Model
|
|||||||
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
|
||||||
@@ -100,7 +91,7 @@ class Holding 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')
|
||||||
->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)')
|
||||||
@@ -169,7 +160,8 @@ 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);
|
||||||
});
|
});
|
||||||
@@ -177,7 +169,7 @@ class Holding extends Model
|
|||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -222,15 +214,17 @@ class Holding extends Model
|
|||||||
'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,13 +236,17 @@ 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') {
|
||||||
|
|
||||||
@@ -290,7 +288,7 @@ 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')
|
||||||
@@ -306,13 +304,14 @@ class Holding extends Model
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,7 +41,7 @@ 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()
|
||||||
@@ -53,13 +57,13 @@ class MarketData extends Model
|
|||||||
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')
|
||||||
) {
|
) {
|
||||||
|
|||||||
+62
-24
@@ -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
|
||||||
{
|
{
|
||||||
@@ -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'];
|
||||||
@@ -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,7 +116,7 @@ class Portfolio extends Model
|
|||||||
|
|
||||||
public function getOwnerAttribute()
|
public function getOwnerAttribute()
|
||||||
{
|
{
|
||||||
if (!$this->relationLoaded('user')) {
|
if (! $this->relationLoaded('user')) {
|
||||||
|
|
||||||
$this->load('users');
|
$this->load('users');
|
||||||
}
|
}
|
||||||
@@ -124,18 +127,19 @@ class Portfolio extends Model
|
|||||||
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);
|
||||||
})
|
})
|
||||||
@@ -147,7 +151,7 @@ class Portfolio extends Model
|
|||||||
|
|
||||||
$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,
|
||||||
@@ -167,7 +171,7 @@ 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);
|
||||||
@@ -183,7 +187,7 @@ class Portfolio extends Model
|
|||||||
'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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,7 +208,7 @@ 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(
|
||||||
@@ -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,7 +230,7 @@ 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++;
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-14
@@ -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,18 +29,19 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -71,7 +73,7 @@ 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());
|
||||||
@@ -94,7 +96,7 @@ class Split extends Model
|
|||||||
'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,
|
||||||
@@ -105,12 +107,12 @@ class Split extends Model
|
|||||||
->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) -
|
||||||
@@ -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(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-25
@@ -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,12 +92,12 @@ 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')
|
||||||
@@ -103,32 +107,32 @@ class Transaction extends Model
|
|||||||
->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,17 +141,15 @@ 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,
|
||||||
@@ -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,
|
||||||
|
|||||||
+12
-10
@@ -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',
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -35,10 +37,10 @@ class ImportFailedNotification extends Notification implements ShouldQueue
|
|||||||
{
|
{
|
||||||
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.
|
||||||
@@ -35,9 +35,9 @@ class ImportSucceededNotification extends Notification implements ShouldQueue
|
|||||||
{
|
{
|
||||||
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.
|
||||||
@@ -45,9 +47,9 @@ class InvitedOnboardingNotification extends Notification implements ShouldQueue
|
|||||||
->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.
|
||||||
@@ -44,7 +46,7 @@ class VerifyConnectedAccountNotification extends Notification implements ShouldQ
|
|||||||
->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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|||||||
@@ -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,10 +16,10 @@ 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;
|
||||||
@@ -26,14 +29,14 @@ class QuantityValidationRule 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
|
||||||
{
|
{
|
||||||
|
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()
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?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
|
||||||
@@ -13,7 +13,7 @@ class Spotlight
|
|||||||
|
|
||||||
$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,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,13 +40,13 @@ class Spotlight
|
|||||||
})
|
})
|
||||||
->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,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Traits;
|
namespace App\Traits;
|
||||||
|
|
||||||
trait HasCompositePrimaryKey
|
trait HasCompositePrimaryKey
|
||||||
@@ -24,10 +26,11 @@ trait HasCompositePrimaryKey
|
|||||||
{
|
{
|
||||||
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;
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,7 +15,7 @@ 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),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'key' => env('ALPHAVANTAGE_API_KEY'),
|
'key' => env('ALPHAVANTAGE_API_KEY'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
+4
-2
@@ -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;
|
||||||
|
|
||||||
@@ -161,8 +163,8 @@ return [
|
|||||||
*/
|
*/
|
||||||
'cells' => [
|
'cells' => [
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
//\Maatwebsite\Excel\Middleware\TrimCellValue::class,
|
// \Maatwebsite\Excel\Middleware\TrimCellValue::class,
|
||||||
//\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
|
// \Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'key' => env('FINNHUB_API_KEY'),
|
'key' => env('FINNHUB_API_KEY'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user