From e8ef0921adcfdd92b3dd6dda788184fb6d0049dd Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Tue, 28 Jan 2025 17:14:49 -0600 Subject: [PATCH] chore: code style --- app/Actions/Fortify/CreateNewUser.php | 2 +- .../Fortify/UpdateUserProfileInformation.php | 6 +- app/Console/Commands/CaptureDailyChange.php | 8 +- app/Console/Commands/RefreshDividendData.php | 10 +- app/Console/Commands/RefreshMarketData.php | 10 +- app/Console/Commands/RefreshSplitData.php | 12 +- app/Console/Commands/SyncDailyChange.php | 5 +- app/Console/Commands/SyncHoldingData.php | 2 +- app/Exports/BackupExport.php | 16 +- app/Exports/Sheets/DailyChangesSheet.php | 11 +- app/Exports/Sheets/PortfoliosSheet.php | 13 +- app/Exports/Sheets/TransactionsSheet.php | 15 +- app/Http/ApiControllers/Controller.php | 6 +- app/Http/ApiControllers/HoldingController.php | 11 +- .../ApiControllers/MarketDataController.php | 6 +- .../ApiControllers/PortfolioController.php | 14 +- .../ApiControllers/TransactionController.php | 15 +- app/Http/ApiControllers/UserController.php | 6 +- .../ConnectedAccountController.php | 49 +++-- app/Http/Controllers/Controller.php | 6 +- app/Http/Controllers/DashboardController.php | 12 +- app/Http/Controllers/HoldingController.php | 19 +- .../InvitedOnboardingController.php | 8 +- app/Http/Controllers/PortfolioController.php | 15 +- .../Controllers/TransactionController.php | 1 - app/Http/Middleware/SetLocale.php | 4 +- app/Http/Requests/FormRequest.php | 5 +- app/Http/Requests/HoldingRequest.php | 5 +- app/Http/Requests/PortfolioRequest.php | 7 +- app/Http/Requests/TransactionRequest.php | 22 +-- app/Http/Resources/HoldingResource.php | 2 +- app/Http/Resources/UserResource.php | 2 +- app/Imports/BackupImport.php | 44 ++--- app/Imports/Sheets/DailyChangesSheet.php | 27 ++- app/Imports/Sheets/PortfoliosSheet.php | 23 +-- app/Imports/Sheets/TransactionsSheet.php | 50 +++-- app/Imports/ValidatesPortfolioAccess.php | 7 +- .../MarketData/AlphaVantageMarketData.php | 108 +++++------ app/Interfaces/MarketData/FakeMarketData.php | 28 +-- .../MarketData/FallbackInterface.php | 7 +- .../MarketData/FinnhubMarketData.php | 54 +++--- .../MarketData/MarketDataInterface.php | 38 +--- app/Interfaces/MarketData/Types/Dividend.php | 8 +- .../MarketData/Types/MarketDataType.php | 9 +- app/Interfaces/MarketData/Types/Ohlc.php | 11 +- app/Interfaces/MarketData/Types/Quote.php | 30 ++- app/Interfaces/MarketData/Types/Split.php | 8 +- app/Interfaces/MarketData/YahooMarketData.php | 73 ++++---- app/Jobs/BackupImportJob.php | 26 +-- app/Models/AiChat.php | 7 +- app/Models/BackupImport.php | 12 +- app/Models/ConnectedAccount.php | 4 +- app/Models/DailyChange.php | 13 +- app/Models/Dividend.php | 85 +++++---- app/Models/Holding.php | 176 +++++++++--------- app/Models/MarketData.php | 20 +- app/Models/Portfolio.php | 125 ++++++------- app/Models/Split.php | 72 +++---- app/Models/Transaction.php | 38 ++-- app/Models/User.php | 24 +-- .../ImportFailedNotification.php | 16 +- .../ImportSucceededNotification.php | 14 +- .../InvitedOnboardingNotification.php | 22 +-- .../VerifyConnectedAccountNotification.php | 16 +- app/Policies/PortfolioPolicy.php | 14 +- app/Providers/AppServiceProvider.php | 2 +- app/Providers/JetstreamServiceProvider.php | 6 +- app/Rules/QuantityValidationRule.php | 29 ++- app/Rules/SymbolValidationRule.php | 11 +- app/Support/Helpers.php | 2 +- app/Support/Spotlight.php | 22 +-- app/Traits/HasCompositePrimaryKey.php | 16 +- app/Traits/HasConnectedAccounts.php | 4 +- app/Traits/WithTrimStrings.php | 4 +- app/View/Components/MainLayout.php | 2 +- config/excel.php | 126 ++++++------- config/investbrain.php | 4 +- config/mary.php | 5 +- config/services.php | 10 +- database/factories/TransactionFactory.php | 14 +- database/factories/UserFactory.php | 2 +- .../0001_01_01_000000_create_users_table.php | 2 +- ...1_01_30_102537_create_portfolios_table.php | 4 +- ...30_112537_create_portfolio_users_table.php | 8 +- ..._02_25_041221_create_market_data_table.php | 8 +- ...02_25_041227_create_daily_change_table.php | 4 +- ...21_02_25_041236_create_dividends_table.php | 2 +- .../2021_02_25_041246_create_splits_table.php | 6 +- ...02_25_041257_create_transactions_table.php | 8 +- ...021_09_06_014744_create_holdings_table.php | 8 +- ...155635_create_connected_accounts_table.php | 6 +- ..._10_23_000001_create_import_jobs_table.php | 6 +- ...024_10_30_000001_create_ai_chats_table.php | 8 +- database/seeders/MarketDataSeeder.php | 20 +- routes/api.php | 8 +- routes/console.php | 11 +- routes/web.php | 14 +- tests/Api/HoldingsTest.php | 20 +- tests/Api/PortfoliosTest.php | 25 +-- tests/Api/TransactionsTest.php | 23 +-- tests/ApiTokenPermissionsTest.php | 1 - tests/AuthenticationTest.php | 1 - tests/BrowserSessionsTest.php | 1 - tests/CaptureDailyChangeTest.php | 15 +- tests/ConnectedAccountTest.php | 20 +- tests/DashboardTest.php | 21 +-- tests/DeleteAccountTest.php | 1 - tests/DividendsTest.php | 30 ++- tests/EmailVerificationTest.php | 1 - tests/FallbackInterfaceTest.php | 31 ++- tests/ImportExportTest.php | 25 +-- tests/PasswordConfirmationTest.php | 1 - tests/PasswordResetTest.php | 1 - tests/PortfolioPolicyTest.php | 13 +- tests/PortfoliosTest.php | 11 +- tests/ProfileInformationTest.php | 1 - tests/RegistrationTest.php | 1 - tests/SplitsTest.php | 17 +- tests/SyncDailyChangeTest.php | 45 ++--- tests/TestCase.php | 6 +- tests/TransactionsTest.php | 19 +- tests/TwoFactorAuthenticationSettingsTest.php | 1 - tests/UpdatePasswordTest.php | 1 - 123 files changed, 1051 insertions(+), 1197 deletions(-) diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 1396fec..90e961e 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -4,10 +4,10 @@ namespace App\Actions\Fortify; use App\Models\User; use App\Traits\WithTrimStrings; -use Laravel\Jetstream\Jetstream; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Laravel\Fortify\Contracts\CreatesNewUsers; +use Laravel\Jetstream\Jetstream; class CreateNewUser implements CreatesNewUsers { diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php index 195ff57..4162e5f 100644 --- a/app/Actions/Fortify/UpdateUserProfileInformation.php +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -4,15 +4,15 @@ namespace App\Actions\Fortify; use App\Models\User; use App\Traits\WithTrimStrings; -use Illuminate\Validation\Rule; -use Illuminate\Support\Facades\Validator; use Illuminate\Contracts\Auth\MustVerifyEmail; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; use Laravel\Fortify\Contracts\UpdatesUserProfileInformation; class UpdateUserProfileInformation implements UpdatesUserProfileInformation { use WithTrimStrings; - + /** * Validate and update the given user's profile information. * diff --git a/app/Console/Commands/CaptureDailyChange.php b/app/Console/Commands/CaptureDailyChange.php index a4cca40..c148bbf 100644 --- a/app/Console/Commands/CaptureDailyChange.php +++ b/app/Console/Commands/CaptureDailyChange.php @@ -38,9 +38,9 @@ class CaptureDailyChange extends Command */ 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'); @@ -48,7 +48,7 @@ class CaptureDailyChange extends Command $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; }); @@ -58,7 +58,7 @@ class CaptureDailyChange extends Command 'total_cost_basis' => $total_cost_basis, 'total_gain' => $total_market_value - $total_cost_basis, 'total_dividends_earned' => $total_dividends, - 'realized_gains' => $realized_gains + 'realized_gains' => $realized_gains, ]); }); } diff --git a/app/Console/Commands/RefreshDividendData.php b/app/Console/Commands/RefreshDividendData.php index fa97c9b..a501614 100644 --- a/app/Console/Commands/RefreshDividendData.php +++ b/app/Console/Commands/RefreshDividendData.php @@ -2,8 +2,8 @@ namespace App\Console\Commands; -use App\Models\Holding; use App\Models\Dividend; +use App\Models\Holding; use Illuminate\Console\Command; class RefreshDividendData extends Command @@ -43,17 +43,17 @@ class RefreshDividendData extends Command { $holdings = Holding::distinct(); - if (!($this->option('force') ?? false)) { + if (! ($this->option('force') ?? false)) { $holdings->where('quantity', '>', 0); - } + } if ($this->option('user')) { $holdings->myHoldings($this->option('user')); } foreach ($holdings->get(['symbol']) as $holding) { - $this->line('Refreshing ' . $holding->symbol); - + $this->line('Refreshing '.$holding->symbol); + Dividend::refreshDividendData($holding->symbol); } } diff --git a/app/Console/Commands/RefreshMarketData.php b/app/Console/Commands/RefreshMarketData.php index f36a090..4e3fd0c 100644 --- a/app/Console/Commands/RefreshMarketData.php +++ b/app/Console/Commands/RefreshMarketData.php @@ -42,18 +42,18 @@ class RefreshMarketData extends Command public function handle() { $force = $this->option('force') ?? false; - + // get all symbols from market data $holdings = Holding::where('quantity', '>', 0) - ->select(['symbol']) - ->distinct(); - + ->select(['symbol']) + ->distinct(); + if ($this->option('user')) { $holdings->myHoldings($this->option('user')); } foreach ($holdings->get() as $holding) { - $this->line('Refreshing ' . $holding->symbol); + $this->line('Refreshing '.$holding->symbol); MarketData::getMarketData($holding->symbol, $force); } diff --git a/app/Console/Commands/RefreshSplitData.php b/app/Console/Commands/RefreshSplitData.php index a358630..fa8dcee 100644 --- a/app/Console/Commands/RefreshSplitData.php +++ b/app/Console/Commands/RefreshSplitData.php @@ -2,8 +2,8 @@ namespace App\Console\Commands; -use App\Models\Split; use App\Models\Holding; +use App\Models\Split; use Illuminate\Console\Command; class RefreshSplitData extends Command @@ -42,14 +42,14 @@ class RefreshSplitData extends Command { $holdings = Holding::distinct(); - if (!($this->option('force') ?? false)) { + if (! ($this->option('force') ?? false)) { $holdings->where('quantity', '>', 0); - } + } foreach ($holdings->get(['symbol']) as $holding) { - $this->line('Refreshing ' . $holding->symbol); - + $this->line('Refreshing '.$holding->symbol); + Split::refreshSplitData($holding->symbol); - } + } } } diff --git a/app/Console/Commands/SyncDailyChange.php b/app/Console/Commands/SyncDailyChange.php index 3171aab..fb77a1a 100644 --- a/app/Console/Commands/SyncDailyChange.php +++ b/app/Console/Commands/SyncDailyChange.php @@ -5,6 +5,7 @@ namespace App\Console\Commands; use App\Models\Portfolio; use Illuminate\Console\Command; use Illuminate\Contracts\Console\PromptsForMissingInput; + use function Laravel\Prompts\search; class SyncDailyChange extends Command implements PromptsForMissingInput @@ -61,14 +62,14 @@ class SyncDailyChange extends Command implements PromptsForMissingInput public function handle() { try { - + $portfolio = Portfolio::findOrFail($this->argument('portfolio_id')); $this->line('Syncing daily change history... This may take a moment.'); $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) { diff --git a/app/Console/Commands/SyncHoldingData.php b/app/Console/Commands/SyncHoldingData.php index fe82398..0ffb6bb 100644 --- a/app/Console/Commands/SyncHoldingData.php +++ b/app/Console/Commands/SyncHoldingData.php @@ -47,7 +47,7 @@ class SyncHoldingData extends Command } foreach ($holdings->get() as $holding) { - $this->line('Refreshing ' . $holding->symbol); + $this->line('Refreshing '.$holding->symbol); $holding->syncTransactionsAndDividends(); } diff --git a/app/Exports/BackupExport.php b/app/Exports/BackupExport.php index e2e8242..d4dcb23 100644 --- a/app/Exports/BackupExport.php +++ b/app/Exports/BackupExport.php @@ -14,18 +14,14 @@ class BackupExport implements WithMultipleSheets public function __construct( public bool $empty = false - ) - { } + ) {} - /** - * @return array - */ public function sheets(): array { - return [ - new PortfoliosSheet($this->empty), - new TransactionsSheet($this->empty), - new DailyChangesSheet($this->empty) - ]; + return [ + new PortfoliosSheet($this->empty), + new TransactionsSheet($this->empty), + new DailyChangesSheet($this->empty), + ]; } } diff --git a/app/Exports/Sheets/DailyChangesSheet.php b/app/Exports/Sheets/DailyChangesSheet.php index dd05530..9e6868c 100644 --- a/app/Exports/Sheets/DailyChangesSheet.php +++ b/app/Exports/Sheets/DailyChangesSheet.php @@ -11,7 +11,7 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle { public function __construct( public bool $empty = false - ) { } + ) {} public function headings(): array { @@ -23,21 +23,18 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle 'Total Gain', 'Total Dividends Earned', 'Realized Gains', - 'Annotation' + 'Annotation', ]; } /** - * @return \Illuminate\Support\Collection - */ + * @return \Illuminate\Support\Collection + */ public function collection() { return $this->empty ? collect() : DailyChange::myDailyChanges()->get(); } - /** - * @return string - */ public function title(): string { return 'Daily Changes'; diff --git a/app/Exports/Sheets/PortfoliosSheet.php b/app/Exports/Sheets/PortfoliosSheet.php index f2bd293..b9ece9a 100644 --- a/app/Exports/Sheets/PortfoliosSheet.php +++ b/app/Exports/Sheets/PortfoliosSheet.php @@ -11,8 +11,8 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle { public function __construct( public bool $empty = false - ) { } - + ) {} + public function headings(): array { return [ @@ -21,21 +21,18 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle 'Notes', 'Wishlist', 'Created', - 'Updated' + 'Updated', ]; } /** - * @return \Illuminate\Support\Collection - */ + * @return \Illuminate\Support\Collection + */ public function collection() { return $this->empty ? collect() : Portfolio::myPortfolios()->get(); } - /** - * @return string - */ public function title(): string { return 'Portfolios'; diff --git a/app/Exports/Sheets/TransactionsSheet.php b/app/Exports/Sheets/TransactionsSheet.php index 9abd820..e135bce 100644 --- a/app/Exports/Sheets/TransactionsSheet.php +++ b/app/Exports/Sheets/TransactionsSheet.php @@ -3,15 +3,15 @@ namespace App\Exports\Sheets; use App\Models\Transaction; -use Maatwebsite\Excel\Concerns\WithTitle; -use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\FromCollection; +use Maatwebsite\Excel\Concerns\WithHeadings; +use Maatwebsite\Excel\Concerns\WithTitle; class TransactionsSheet implements FromCollection, WithHeadings, WithTitle { public function __construct( public bool $empty = false - ) { } + ) {} public function headings(): array { @@ -27,21 +27,18 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle 'Reinvested Dividend', 'Date', 'Created', - 'Updated' + 'Updated', ]; } /** - * @return \Illuminate\Support\Collection - */ + * @return \Illuminate\Support\Collection + */ public function collection() { return $this->empty ? collect() : Transaction::myTransactions()->get(); } - /** - * @return string - */ public function title(): string { return 'Transactions'; diff --git a/app/Http/ApiControllers/Controller.php b/app/Http/ApiControllers/Controller.php index bc0e11d..01da444 100644 --- a/app/Http/ApiControllers/Controller.php +++ b/app/Http/ApiControllers/Controller.php @@ -1,8 +1,8 @@ validated()); - + return PortfolioResource::make($portfolio); } @@ -55,4 +55,4 @@ class PortfolioController extends ApiController return response()->noContent(); } -} \ No newline at end of file +} diff --git a/app/Http/ApiControllers/TransactionController.php b/app/Http/ApiControllers/TransactionController.php index 64924bb..5c24eba 100644 --- a/app/Http/ApiControllers/TransactionController.php +++ b/app/Http/ApiControllers/TransactionController.php @@ -2,13 +2,12 @@ namespace App\Http\ApiControllers; -use App\Models\Transaction; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Gate; -use HackerEsq\FilterModels\FilterModels; +use App\Http\ApiControllers\Controller as ApiController; use App\Http\Requests\TransactionRequest; use App\Http\Resources\TransactionResource; -use App\Http\ApiControllers\Controller as ApiController; +use App\Models\Transaction; +use HackerEsq\FilterModels\FilterModels; +use Illuminate\Support\Facades\Gate; class TransactionController extends ApiController { @@ -23,11 +22,11 @@ class TransactionController extends ApiController } public function store(TransactionRequest $request) - { + { Gate::authorize('fullAccess', $request->portfolio); $transaction = Transaction::create($request->validated()); - + return TransactionResource::make($transaction); } @@ -55,4 +54,4 @@ class TransactionController extends ApiController return response()->noContent(); } -} \ No newline at end of file +} diff --git a/app/Http/ApiControllers/UserController.php b/app/Http/ApiControllers/UserController.php index 468ca52..4472e42 100644 --- a/app/Http/ApiControllers/UserController.php +++ b/app/Http/ApiControllers/UserController.php @@ -2,9 +2,9 @@ namespace App\Http\ApiControllers; -use Illuminate\Http\Request; -use App\Http\Resources\UserResource; use App\Http\ApiControllers\Controller as ApiController; +use App\Http\Resources\UserResource; +use Illuminate\Http\Request; class UserController extends ApiController { @@ -12,4 +12,4 @@ class UserController extends ApiController { return UserResource::make($request->user()); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/ConnectedAccountController.php b/app/Http/Controllers/ConnectedAccountController.php index 0551b89..d242e0d 100644 --- a/app/Http/Controllers/ConnectedAccountController.php +++ b/app/Http/Controllers/ConnectedAccountController.php @@ -2,21 +2,19 @@ namespace App\Http\Controllers; -use Exception; -use App\Models\User; use App\Models\ConnectedAccount; -use Illuminate\Support\MessageBag; +use App\Models\User; +use App\Notifications\VerifyConnectedAccountNotification; +use Exception; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\MessageBag; use Laravel\Socialite\Facades\Socialite; -use App\Notifications\VerifyConnectedAccountNotification; class ConnectedAccountController extends Controller { - /** * Redirect the user to the GitHub authentication page. - * */ public function redirectToProvider(string $provider) { @@ -27,7 +25,6 @@ class ConnectedAccountController extends Controller /** * Obtain the user information from GitHub. - * */ public function handleProviderCallback(string $provider) { @@ -44,21 +41,21 @@ class ConnectedAccountController extends Controller } // check if this account is already linked - $connected_account = ConnectedAccount::firstOrNew([ + $connected_account = ConnectedAccount::firstOrNew([ 'provider' => $provider, - 'provider_id' => $providerUser->id + 'provider_id' => $providerUser->id, ], [ 'token' => $providerUser->token, 'secret' => $providerUser->tokenSecret, 'refresh_token' => $providerUser->refreshToken, 'expires_at' => $providerUser->expiresIn, - 'verified_at' => false + 'verified_at' => false, ]); // already linked and verified, let's go login! if ( - $connected_account->exists - && !is_null($connected_account->verified_at) + $connected_account->exists + && ! is_null($connected_account->verified_at) ) { Auth::login($connected_account->user, true); @@ -67,20 +64,20 @@ class ConnectedAccountController extends Controller } // 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([ 'name' => $providerUser->name, 'email' => $providerUser->email, - 'email_verified_at' => now() + 'email_verified_at' => now(), ]); - + $connected_account->user_id = $user->id; $connected_account->verified_at = now(); $connected_account->save(); - + Auth::login($user, true); - + return redirect(route('dashboard')); } @@ -91,23 +88,23 @@ class ConnectedAccountController extends Controller $user->notify(new VerifyConnectedAccountNotification($connected_account->id)); return redirect(route('login')) - ->with('status', __( - 'Account already exists. Check your email to connect your :provider account.', - ['provider' => config("services.$provider.name")] - )); + ->with('status', __( + 'Account already exists. Check your email to connect your :provider account.', + ['provider' => config("services.$provider.name")] + )); } 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.'); } } public function verify(ConnectedAccount $connected_account) { - if (!$connected_account->verified_at) { + if (! $connected_account->verified_at) { // mark request as verified $connected_account->verified_at = now(); @@ -127,8 +124,8 @@ class ConnectedAccountController extends Controller 'css' => 'alert-success', 'icon' => Blade::render(""), 'position' => 'toast-top toast-end', - 'timeout' => '5000' - ] + 'timeout' => '5000', + ], ])); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 71116b2..8677cd5 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -1,8 +1,8 @@ remember( - 'dashboard-metrics-' . $user->id, - 10, + 'dashboard-metrics-'.$user->id, + 10, function () { return Holding::query() - ->myHoldings() - ->withoutWishlists() - ->withPortfolioMetrics() - ->first(); + ->myHoldings() + ->withoutWishlists() + ->withPortfolioMetrics() + ->first(); } ); diff --git a/app/Http/Controllers/HoldingController.php b/app/Http/Controllers/HoldingController.php index 95762d5..8a59399 100644 --- a/app/Http/Controllers/HoldingController.php +++ b/app/Http/Controllers/HoldingController.php @@ -8,21 +8,20 @@ use Illuminate\Http\Request; class HoldingController extends Controller { - /** * 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([ - 'market_data', - 'transactions' => function ($query) use ($symbol) { - $query->where('transactions.symbol', $symbol); - } - ]) - ->symbol($symbol) - ->portfolio($portfolio->id) - ->firstOrFail(); + 'market_data', + 'transactions' => function ($query) use ($symbol) { + $query->where('transactions.symbol', $symbol); + }, + ]) + ->symbol($symbol) + ->portfolio($portfolio->id) + ->firstOrFail(); $formattedTransactions = $holding->getFormattedTransactions(); diff --git a/app/Http/Controllers/InvitedOnboardingController.php b/app/Http/Controllers/InvitedOnboardingController.php index f28aead..6a8386a 100644 --- a/app/Http/Controllers/InvitedOnboardingController.php +++ b/app/Http/Controllers/InvitedOnboardingController.php @@ -2,21 +2,19 @@ namespace App\Http\Controllers; -use App\Models\User; use App\Models\Portfolio; +use App\Models\User; use Illuminate\Http\Request; class InvitedOnboardingController extends Controller { - /** * Check if the invited user needs a password? - * */ public function __invoke(Request $request, Portfolio $portfolio, User $user) { - if (!$request->hasValidSignature()) { + if (! $request->hasValidSignature()) { abort(401, 'Invalid signature'); } @@ -26,7 +24,7 @@ class InvitedOnboardingController extends Controller // route to create password form return view('auth.invited-onboarding', [ 'portfolio' => $portfolio, - 'user' => $user + 'user' => $user, ]); } diff --git a/app/Http/Controllers/PortfolioController.php b/app/Http/Controllers/PortfolioController.php index efa9980..24ddb80 100644 --- a/app/Http/Controllers/PortfolioController.php +++ b/app/Http/Controllers/PortfolioController.php @@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Gate; class PortfolioController extends Controller { - /** * Show the form for creating a new resource. */ @@ -26,21 +25,21 @@ class PortfolioController extends Controller Gate::authorize('readOnly', $portfolio); $portfolio->load(['transactions', 'holdings']); - + // get portfolio metrics $metrics = cache()->remember( - 'portfolio-metrics-' . $portfolio->id, - 60, + 'portfolio-metrics-'.$portfolio->id, + 60, function () use ($portfolio) { return Holding::query() - ->portfolio($portfolio->id) - ->withPortfolioMetrics() - ->first(); + ->portfolio($portfolio->id) + ->withPortfolioMetrics() + ->first(); } ); $formattedHoldings = $portfolio->getFormattedHoldings(); - + return view('portfolio.show', compact(['portfolio', 'metrics', 'formattedHoldings'])); } } diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index 900544e..def59a0 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -4,7 +4,6 @@ namespace App\Http\Controllers; class TransactionController extends Controller { - /** * Display the specified resource. */ diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php index 8a8539d..4ebdf94 100644 --- a/app/Http/Middleware/SetLocale.php +++ b/app/Http/Middleware/SetLocale.php @@ -14,7 +14,7 @@ class SetLocale */ public function handle(Request $request, Closure $next) { - if (!session()->has('locale')) { + if (! session()->has('locale')) { session()->put('locale', $request->getPreferredLanguage( config('app.available_locales') )); @@ -24,4 +24,4 @@ class SetLocale return $next($request); } -} \ No newline at end of file +} diff --git a/app/Http/Requests/FormRequest.php b/app/Http/Requests/FormRequest.php index d0f8334..87eb516 100644 --- a/app/Http/Requests/FormRequest.php +++ b/app/Http/Requests/FormRequest.php @@ -6,9 +6,8 @@ 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}; - } -} \ No newline at end of file + } +} diff --git a/app/Http/Requests/HoldingRequest.php b/app/Http/Requests/HoldingRequest.php index a75d82d..c34b3c2 100644 --- a/app/Http/Requests/HoldingRequest.php +++ b/app/Http/Requests/HoldingRequest.php @@ -2,11 +2,8 @@ namespace App\Http\Requests; -use App\Http\Requests\FormRequest; - class HoldingRequest extends FormRequest { - /** * Get the validation rules that apply to the request. * @@ -16,7 +13,7 @@ class HoldingRequest extends FormRequest { $rules = [ - 'reinvest_dividends' => ['sometimes', 'boolean'] + 'reinvest_dividends' => ['sometimes', 'boolean'], ]; return $rules; diff --git a/app/Http/Requests/PortfolioRequest.php b/app/Http/Requests/PortfolioRequest.php index ab94472..57252f4 100644 --- a/app/Http/Requests/PortfolioRequest.php +++ b/app/Http/Requests/PortfolioRequest.php @@ -2,11 +2,8 @@ namespace App\Http\Requests; -use App\Http\Requests\FormRequest; - class PortfolioRequest extends FormRequest { - /** * Get the validation rules that apply to the request. * @@ -21,9 +18,9 @@ class PortfolioRequest extends FormRequest 'wishlist' => ['sometimes', 'nullable', 'boolean'], ]; - if (!is_null($this->portfolio)) { + if (! is_null($this->portfolio)) { $rules['title'][0] = 'sometimes'; - } + } return $rules; } diff --git a/app/Http/Requests/TransactionRequest.php b/app/Http/Requests/TransactionRequest.php index 01907f1..3943ab9 100644 --- a/app/Http/Requests/TransactionRequest.php +++ b/app/Http/Requests/TransactionRequest.php @@ -3,18 +3,16 @@ namespace App\Http\Requests; use App\Models\Portfolio; -use App\Http\Requests\FormRequest; -use App\Rules\SymbolValidationRule; 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')) + 'portfolio' => Portfolio::find($this->requestOrModelValue('portfolio_id', 'transaction')), ]); } @@ -25,28 +23,28 @@ class TransactionRequest extends FormRequest */ 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')], + 'date' => ['required', 'date_format:Y-m-d', 'before_or_equal:'.now()->format('Y-m-d')], 'quantity' => [ - 'required', - 'numeric', - 'min:0', + '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)) { + if (! is_null($this->transaction)) { $rules['portfolio_id'][0] = 'sometimes'; $rules['symbol'][0] = 'sometimes'; $rules['transaction_type'][0] = 'sometimes'; @@ -64,7 +62,7 @@ class TransactionRequest extends FormRequest ) { $rules['cost_basis'][0] = 'required'; } - } + } return $rules; } diff --git a/app/Http/Resources/HoldingResource.php b/app/Http/Resources/HoldingResource.php index 57cd293..a11b0cc 100644 --- a/app/Http/Resources/HoldingResource.php +++ b/app/Http/Resources/HoldingResource.php @@ -31,7 +31,7 @@ class HoldingResource extends JsonResource 'market_gain_dollars' => $this->market_gain_dollars, 'market_gain_percent' => $this->market_gain_percent, 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at + 'updated_at' => $this->updated_at, ]; } } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 41eeff5..4896aa5 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -8,7 +8,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource -{ +{ /** * Transform the resource into an array. * diff --git a/app/Imports/BackupImport.php b/app/Imports/BackupImport.php index aae4f7d..50e1a88 100644 --- a/app/Imports/BackupImport.php +++ b/app/Imports/BackupImport.php @@ -2,39 +2,35 @@ namespace App\Imports; -use App\Models\User; -use App\Imports\Sheets\PortfoliosSheet; -use Illuminate\Support\Facades\Artisan; +use App\Console\Commands\RefreshDividendData; +use App\Console\Commands\RefreshMarketData; use App\Console\Commands\SyncDailyChange; use App\Console\Commands\SyncHoldingData; use App\Imports\Sheets\DailyChangesSheet; +use App\Imports\Sheets\PortfoliosSheet; 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\WithEvents; +use Maatwebsite\Excel\Concerns\WithMultipleSheets; +use Maatwebsite\Excel\Events\AfterImport; use Maatwebsite\Excel\Events\BeforeImport; 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; public function __construct( public BackupImportModel $backupImportModel - ) { } + ) {} - /** - * @return array - */ public function registerEvents(): array { return [ - BeforeImport::class => fn() => $this->backupImportModel->update([ + BeforeImport::class => fn () => $this->backupImportModel->update([ 'status' => 'in_progress', 'message' => __('Import is in progress...'), ]), @@ -43,24 +39,24 @@ class BackupImport implements WithMultipleSheets, WithEvents $this->backupImportModel->update([ 'status' => 'success', 'message' => 'Import completed successfully!', - 'completed_at' => now() + 'completed_at' => now(), ]); - + Artisan::queue(RefreshMarketData::class, ['--user' => $this->backupImportModel->user_id, '--force' => true]) ->chain([ - fn() => Artisan::call(RefreshDividendData::class, ['--user' => $this->backupImportModel->user_id, '--force' => true]), - fn() => Artisan::call(SyncHoldingData::class, ['--user' => $this->backupImportModel->user_id]), - fn() => User::find($this->backupImportModel->user_id)->portfolios->each(function($portfolio) { + fn () => Artisan::call(RefreshDividendData::class, ['--user' => $this->backupImportModel->user_id, '--force' => true]), + fn () => Artisan::call(SyncHoldingData::class, ['--user' => $this->backupImportModel->user_id]), + fn () => User::find($this->backupImportModel->user_id)->portfolios->each(function ($portfolio) { 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', - 'message' => 'Error: '. substr($event->getException()->getMessage(), 0, 220), + 'message' => 'Error: '.substr($event->getException()->getMessage(), 0, 220), 'has_errors' => true, - 'completed_at' => now() + 'completed_at' => now(), ]), ]; } diff --git a/app/Imports/Sheets/DailyChangesSheet.php b/app/Imports/Sheets/DailyChangesSheet.php index d03e382..298719a 100644 --- a/app/Imports/Sheets/DailyChangesSheet.php +++ b/app/Imports/Sheets/DailyChangesSheet.php @@ -3,42 +3,39 @@ namespace App\Imports\Sheets; use App\Imports\ValidatesPortfolioAccess; -use App\Models\DailyChange; use App\Models\BackupImport; +use App\Models\DailyChange; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; 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\ToCollection; +use Maatwebsite\Excel\Concerns\WithEvents; use Maatwebsite\Excel\Concerns\WithHeadingRow; 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; public function __construct( public BackupImport $backupImport - ) { } + ) {} - /** - * @return array - */ public function registerEvents(): array { return [ - BeforeSheet::class => function(BeforeSheet $event) { + BeforeSheet::class => function (BeforeSheet $event) { DB::commit(); $this->backupImport->update([ 'message' => __('Importing daily changes...'), ]); DB::beginTransaction(); - } + }, ]; } - + public function collection(Collection $dailyChanges) { $dailyChanges->chunk($this->batchSize())->each(function ($chunk) { @@ -56,7 +53,7 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, 'realized_gains' => $dailyChange['realized_gains'], 'annotation' => $dailyChange['annotation'], '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 +68,7 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, 'realized_gains', 'annotation', 'portfolio_id', - 'date' + 'date', ] ); }); @@ -85,7 +82,7 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, public function rules(): array { return [ - 'portfolio_id' => ['required', 'uuid'], + 'portfolio_id' => ['required', 'uuid'], 'date' => ['required', 'date'], 'total_market_value' => ['sometimes', 'nullable', 'numeric'], 'total_cost_basis' => ['sometimes', 'nullable', 'min:0', 'numeric'], diff --git a/app/Imports/Sheets/PortfoliosSheet.php b/app/Imports/Sheets/PortfoliosSheet.php index 551e940..9e7dc8e 100644 --- a/app/Imports/Sheets/PortfoliosSheet.php +++ b/app/Imports/Sheets/PortfoliosSheet.php @@ -2,36 +2,33 @@ namespace App\Imports\Sheets; -use App\Models\Portfolio; use App\Models\BackupImport; +use App\Models\Portfolio; use Illuminate\Support\Collection; 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\ToCollection; +use Maatwebsite\Excel\Concerns\WithEvents; use Maatwebsite\Excel\Concerns\WithHeadingRow; 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 BackupImport $backupImport - ) { } - - /** - * @return array - */ + ) {} + public function registerEvents(): array { return [ - BeforeSheet::class => function(BeforeSheet $event) { + BeforeSheet::class => function (BeforeSheet $event) { DB::commit(); $this->backupImport->update([ 'message' => __('Importing portfolios...'), ]); DB::beginTransaction(); - } + }, ]; } @@ -42,7 +39,7 @@ class PortfoliosSheet implements ToCollection, WithValidation, WithHeadingRow, S Portfolio::unguard(); // ensures we can set an owner for the portfolio $portfolio = Portfolio::fullAccess($this->backupImport->user_id)->updateOrCreate([ - 'id' => $portfolio['portfolio_id'] + 'id' => $portfolio['portfolio_id'], ], [ 'id' => $portfolio['portfolio_id'] ?? null, 'title' => $portfolio['title'], diff --git a/app/Imports/Sheets/TransactionsSheet.php b/app/Imports/Sheets/TransactionsSheet.php index 544257d..5599e9e 100644 --- a/app/Imports/Sheets/TransactionsSheet.php +++ b/app/Imports/Sheets/TransactionsSheet.php @@ -3,42 +3,38 @@ namespace App\Imports\Sheets; use App\Imports\ValidatesPortfolioAccess; +use App\Models\BackupImport; use App\Models\Holding; use App\Models\Transaction; -use Illuminate\Support\Str; -use App\Models\BackupImport; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; -use Maatwebsite\Excel\Events\BeforeSheet; -use Maatwebsite\Excel\Concerns\WithEvents; -use Maatwebsite\Excel\Concerns\ToCollection; +use Illuminate\Support\Str; use Maatwebsite\Excel\Concerns\SkipsEmptyRows; +use Maatwebsite\Excel\Concerns\ToCollection; +use Maatwebsite\Excel\Concerns\WithEvents; use Maatwebsite\Excel\Concerns\WithHeadingRow; 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; public function __construct( public BackupImport $backupImport - ) { } + ) {} - /** - * @return array - */ public function registerEvents(): array { return [ - BeforeSheet::class => function(BeforeSheet $event) { + BeforeSheet::class => function (BeforeSheet $event) { DB::commit(); $this->backupImport->update([ 'message' => __('Importing transactions...'), ]); DB::beginTransaction(); - } + }, ]; } @@ -62,7 +58,7 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation, 'sale_price' => $transaction['sale_price'], 'split' => boolval($transaction['split']) ? 1 : 0, 'reinvested_dividend' => boolval($transaction['reinvested_dividend']) ? 1 : 0, - 'date' => Carbon::parse($transaction['date'])->format('Y-m-d') + 'date' => Carbon::parse($transaction['date'])->format('Y-m-d'), ]; }); @@ -79,23 +75,23 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation, 'sale_price', 'split', 'reinvested_dividend', - 'date' + 'date', ] ); // stub out related holdings - $chunk->unique(fn($item) => $item['symbol'] . $item['portfolio_id']) - ->each(function($holding) { - - Holding::firstOrCreate([ - 'symbol' => $holding['symbol'], - 'portfolio_id' => $holding['portfolio_id'] - ], [ - 'quantity' => 0, - 'average_cost_basis' => 0, - 'splits_synced_at' => now(), - ]); - }); + $chunk->unique(fn ($item) => $item['symbol'].$item['portfolio_id']) + ->each(function ($holding) { + + Holding::firstOrCreate([ + 'symbol' => $holding['symbol'], + 'portfolio_id' => $holding['portfolio_id'], + ], [ + 'quantity' => 0, + 'average_cost_basis' => 0, + 'splits_synced_at' => now(), + ]); + }); }); } diff --git a/app/Imports/ValidatesPortfolioAccess.php b/app/Imports/ValidatesPortfolioAccess.php index 74a6185..a684114 100644 --- a/app/Imports/ValidatesPortfolioAccess.php +++ b/app/Imports/ValidatesPortfolioAccess.php @@ -6,19 +6,18 @@ use App\Models\Portfolio; trait ValidatesPortfolioAccess { - public function validatePortfolioAccess($collection) { $uniquePortfolios = $collection->unique('portfolio_id')->pluck('portfolio_id'); $countPortfoliosWithAccess = Portfolio::fullAccess($this->backupImport->user_id) - ->whereIn('id', $uniquePortfolios) - ->count(); + ->whereIn('id', $uniquePortfolios) + ->count(); if ( $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.')); } } } diff --git a/app/Interfaces/MarketData/AlphaVantageMarketData.php b/app/Interfaces/MarketData/AlphaVantageMarketData.php index 31391e5..ab8da8d 100644 --- a/app/Interfaces/MarketData/AlphaVantageMarketData.php +++ b/app/Interfaces/MarketData/AlphaVantageMarketData.php @@ -2,33 +2,35 @@ 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\Carbon; 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; class AlphaVantageMarketData implements MarketDataInterface { - public function exists(String $symbol): Bool + public function exists(string $symbol): bool { return $this->quote($symbol)->isNotEmpty(); } - public function quote(String $symbol): Quote + public function quote(string $symbol): Quote { $quote = Alphavantage::core()->quoteEndpoint($symbol); $quote = Arr::get($quote, 'Global Quote', []); - if (empty($quote)) return new Quote(); + if (empty($quote)) { + return new Quote; + } $fundamental = cache()->remember( - 'av-symbol-'.$symbol, - 1440, + 'av-symbol-'.$symbol, + 1440, function () use ($symbol) { return Alphavantage::fundamentals()->overview($symbol); } @@ -49,71 +51,71 @@ class AlphaVantageMarketData implements MarketDataInterface : null, 'dividend_yield' => Arr::get($fundamental, 'DividendYield') != 'None' ? 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 = Arr::get($dividends, 'data', []); return collect($dividends) - ->filter(function($dividend) use ($startDate, $endDate) { - - return Carbon::parse(Arr::get($dividend, 'ex_dividend_date'))->between($startDate, $endDate); - }) - ->map(function($dividend) use ($symbol) { - - return new Dividend([ - 'symbol' => $symbol, - 'date' => Carbon::parse(Arr::get($dividend, 'ex_dividend_date')), - 'dividend_amount' => Arr::get($dividend, 'amount'), - ]); - }); + ->filter(function ($dividend) use ($startDate, $endDate) { + + return Carbon::parse(Arr::get($dividend, 'ex_dividend_date'))->between($startDate, $endDate); + }) + ->map(function ($dividend) use ($symbol) { + + return new Dividend([ + 'symbol' => $symbol, + 'date' => Carbon::parse(Arr::get($dividend, 'ex_dividend_date')), + 'dividend_amount' => Arr::get($dividend, 'amount'), + ]); + }); } - public function splits(String $symbol, $startDate, $endDate): Collection - { + public function splits(string $symbol, $startDate, $endDate): Collection + { $splits = Alphavantage::fundamentals()->splits($symbol); $splits = Arr::get($splits, 'data', []); return collect($splits) - ->filter(function($split) use ($startDate, $endDate) { - - return Carbon::parse(Arr::get($split, 'effective_date'))->between($startDate, $endDate); - }) - ->map(function($split) use ($symbol) { - - return new Split([ - 'symbol' => $symbol, - 'date' => Carbon::parse(Arr::get($split, 'effective_date')), - 'split_amount' => Arr::get($split, 'split_factor'), - ]); - }); + ->filter(function ($split) use ($startDate, $endDate) { + + return Carbon::parse(Arr::get($split, 'effective_date'))->between($startDate, $endDate); + }) + ->map(function ($split) use ($symbol) { + + return new Split([ + 'symbol' => $symbol, + 'date' => Carbon::parse(Arr::get($split, 'effective_date')), + 'split_amount' => Arr::get($split, 'split_factor'), + ]); + }); } - public function history(String $symbol, $startDate, $endDate): Collection + public function history(string $symbol, $startDate, $endDate): Collection { $history = Alphavantage::timeSeries()->daily($symbol, 'full'); $history = Arr::get($history, 'Time Series (Daily)', []); - + return collect($history) - ->filter(function ($history, $date) use ($startDate, $endDate) { + ->filter(function ($history, $date) use ($startDate, $endDate) { - return Carbon::parse($date)->between($startDate, $endDate); - }) - ->mapWithKeys(function($history, $date) use ($symbol) { + return Carbon::parse($date)->between($startDate, $endDate); + }) + ->mapWithKeys(function ($history, $date) use ($symbol) { - $date = Carbon::parse($date)->format('Y-m-d'); - - return [ $date => new Ohlc([ - 'symbol' => $symbol, - 'date' => $date, - 'close' => Arr::get($history, '4. close') - ]) ]; - }); + $date = Carbon::parse($date)->format('Y-m-d'); + + return [$date => new Ohlc([ + 'symbol' => $symbol, + 'date' => $date, + 'close' => Arr::get($history, '4. close'), + ])]; + }); } -} \ No newline at end of file +} diff --git a/app/Interfaces/MarketData/FakeMarketData.php b/app/Interfaces/MarketData/FakeMarketData.php index dfbdb27..5f32550 100644 --- a/app/Interfaces/MarketData/FakeMarketData.php +++ b/app/Interfaces/MarketData/FakeMarketData.php @@ -2,22 +2,22 @@ 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\Ohlc; +use App\Interfaces\MarketData\Types\Quote; use App\Interfaces\MarketData\Types\Split; +use Illuminate\Support\Carbon; +use Illuminate\Support\Collection; class FakeMarketData implements MarketDataInterface { - public function exists(String $symbol): Bool + public function exists(string $symbol): bool { return true; } - public function quote(String $symbol): Quote + public function quote(string $symbol): Quote { return new Quote([ @@ -31,11 +31,11 @@ class FakeMarketData implements MarketDataInterface 'market_cap' => 9800700600, 'book_value' => 4.7, '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([ @@ -57,23 +57,23 @@ class FakeMarketData implements MarketDataInterface ]); } - public function splits(String $symbol, $startDate, $endDate): Collection - { + public function splits(string $symbol, $startDate, $endDate): Collection + { return collect([ new Split([ 'symbol' => $symbol, 'date' => now()->subMonths(36), '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); - for ($i = 0; $i < $numDays; $i++) { + for ($i = 0; $i < $numDays; $i++) { $date = now()->subDays($i)->format('Y-m-d'); @@ -83,7 +83,7 @@ class FakeMarketData implements MarketDataInterface 'close' => rand(150, 400), ]); } - + return collect($series); } -} \ No newline at end of file +} diff --git a/app/Interfaces/MarketData/FallbackInterface.php b/app/Interfaces/MarketData/FallbackInterface.php index 8deb605..6a1bad5 100644 --- a/app/Interfaces/MarketData/FallbackInterface.php +++ b/app/Interfaces/MarketData/FallbackInterface.php @@ -6,21 +6,20 @@ use Illuminate\Support\Facades\Log; class FallbackInterface { - protected string $latest_error; public function __call($method, $arguments) { $providers = explode(',', config('investbrain.provider', 'yahoo')); - + foreach ($providers as $provider) { $provider = trim($provider); try { - 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."); } @@ -30,7 +29,7 @@ class FallbackInterface return app()->make($provider_class_name)->$method(...$arguments); } catch (\Throwable $e) { - + $this->latest_error = $e->getMessage(); Log::warning("Failed calling method {$method} ({$provider}): {$this->latest_error}"); diff --git a/app/Interfaces/MarketData/FinnhubMarketData.php b/app/Interfaces/MarketData/FinnhubMarketData.php index d55e966..6dd28df 100644 --- a/app/Interfaces/MarketData/FinnhubMarketData.php +++ b/app/Interfaces/MarketData/FinnhubMarketData.php @@ -2,13 +2,13 @@ namespace App\Interfaces\MarketData; -use Illuminate\Support\Arr; -use Illuminate\Support\Carbon; -use Illuminate\Support\Collection; +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 App\Interfaces\MarketData\Types\Dividend; +use Illuminate\Support\Arr; +use Illuminate\Support\Carbon; +use Illuminate\Support\Collection; class FinnhubMarketData implements MarketDataInterface { @@ -16,13 +16,14 @@ class FinnhubMarketData implements MarketDataInterface public function __construct() { - + $this->client = new \Finnhub\Api\DefaultApi( - new \GuzzleHttp\Client(), + new \GuzzleHttp\Client, \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(); @@ -32,20 +33,22 @@ class FinnhubMarketData implements MarketDataInterface { $quote = $this->client->quote($symbol); - if (empty($quote)) return new Quote(); - + if (empty($quote)) { + return new Quote; + } + $fundamental = cache()->remember( - 'fh-symbol-'.$symbol, - 1440, + 'fh-symbol-'.$symbol, + 1440, function () use ($symbol) { - return $this->client->companyBasicFinancials($symbol, "all"); + return $this->client->companyBasicFinancials($symbol, 'all'); } ); - + return new Quote([ 'name' => Arr::get($fundamental, 'metric.name'), 'symbol' => $symbol, - 'market_value' => Arr::get($quote, 'c'), + 'market_value' => Arr::get($quote, 'c'), 'fifty_two_week_high' => Arr::get($fundamental, 'metric.52WeekHigh'), 'fifty_two_week_low' => Arr::get($fundamental, 'metric.52WeekLow'), 'forward_pe' => Arr::get($fundamental, 'metric.forwardPE'), // confirm @@ -54,15 +57,15 @@ class FinnhubMarketData implements MarketDataInterface 'book_value' => Arr::get($fundamental, 'metric.bookValuePerShare'), // confirm 'last_dividend_date' => Arr::get($fundamental, 'metric.lastDivDate'), // confirm 'dividend_yield' => Arr::get($fundamental, 'metric.dividendYield'), // confirm - ]); + ]); } public function dividends($symbol, $startDate, $endDate): Collection { $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([ 'symbol' => $symbol, 'date' => Carbon::parse(Arr::get($dividend, 'date')), @@ -72,12 +75,12 @@ class FinnhubMarketData implements MarketDataInterface } public function splits($symbol, $startDate, $endDate): Collection - { + { $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([ 'symbol' => $symbol, 'date' => Carbon::parse(Arr::get($split, 'date')), @@ -89,18 +92,19 @@ class FinnhubMarketData implements MarketDataInterface 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', []); $closes = Arr::get($history, 'c', []); return collect($timestamps)->mapWithKeys(function ($timestamp, $index) use ($symbol, $closes) { $date = Carbon::createFromTimestamp($timestamp)->format('Y-m-d'); - return [ $date => new Ohlc([ + + return [$date => new Ohlc([ 'symbol' => $symbol, 'date' => $date, 'close' => $closes[$index], - ]) ]; + ])]; }); } -} \ No newline at end of file +} diff --git a/app/Interfaces/MarketData/MarketDataInterface.php b/app/Interfaces/MarketData/MarketDataInterface.php index 20cf8c6..4dcfaab 100644 --- a/app/Interfaces/MarketData/MarketDataInterface.php +++ b/app/Interfaces/MarketData/MarketDataInterface.php @@ -2,59 +2,33 @@ namespace App\Interfaces\MarketData; -use Illuminate\Support\Collection; use App\Interfaces\MarketData\Types\Quote; +use Illuminate\Support\Collection; interface MarketDataInterface { /** * 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 - * - * @param String $symbol - * - * @return Quote */ - public function quote(String $symbol): Quote; + public function quote(string $symbol): Quote; /** * 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 - * - * @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 - * - * @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; } diff --git a/app/Interfaces/MarketData/Types/Dividend.php b/app/Interfaces/MarketData/Types/Dividend.php index eb0d61f..19c928e 100644 --- a/app/Interfaces/MarketData/Types/Dividend.php +++ b/app/Interfaces/MarketData/Types/Dividend.php @@ -4,13 +4,13 @@ namespace App\Interfaces\MarketData\Types; use DateTime; use Illuminate\Support\Carbon; -use App\Interfaces\MarketData\Types\MarketDataType; class Dividend extends MarketDataType { public function setSymbol(string $symbol): self { $this->items['symbol'] = $symbol; + return $this; } @@ -22,6 +22,7 @@ class Dividend extends MarketDataType public function setDividendAmount($dividendAmount): self { $this->items['dividend_amount'] = (float) $dividendAmount; + return $this; } @@ -30,9 +31,10 @@ class Dividend extends MarketDataType 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'); + return $this; } @@ -40,4 +42,4 @@ class Dividend extends MarketDataType { return $this->items['date'] ?? null; } -} \ No newline at end of file +} diff --git a/app/Interfaces/MarketData/Types/MarketDataType.php b/app/Interfaces/MarketData/Types/MarketDataType.php index 1b01ac5..9b0ffef 100644 --- a/app/Interfaces/MarketData/Types/MarketDataType.php +++ b/app/Interfaces/MarketData/Types/MarketDataType.php @@ -2,18 +2,15 @@ namespace App\Interfaces\MarketData\Types; -use Illuminate\Support\Str; use Illuminate\Support\Collection; +use Illuminate\Support\Str; class MarketDataType extends Collection { - /** - * - */ public function __construct($items = []) { - foreach($this->getArrayableItems($items) as $key => $value) { + foreach ($this->getArrayableItems($items) as $key => $value) { $this->{$key} = $value; } @@ -33,4 +30,4 @@ class MarketDataType extends Collection { return $this->items[$key] ?? null; } -} \ No newline at end of file +} diff --git a/app/Interfaces/MarketData/Types/Ohlc.php b/app/Interfaces/MarketData/Types/Ohlc.php index 2f7bf09..b96ad4a 100644 --- a/app/Interfaces/MarketData/Types/Ohlc.php +++ b/app/Interfaces/MarketData/Types/Ohlc.php @@ -4,13 +4,13 @@ namespace App\Interfaces\MarketData\Types; use DateTime; use Illuminate\Support\Carbon; -use App\Interfaces\MarketData\Types\MarketDataType; class Ohlc extends MarketDataType { public function setSymbol(string $symbol): self { $this->items['symbol'] = $symbol; + return $this; } @@ -22,6 +22,7 @@ class Ohlc extends MarketDataType public function setOpen($open): self { $this->items['open'] = (float) $open; + return $this; } @@ -33,6 +34,7 @@ class Ohlc extends MarketDataType public function setHigh($high): self { $this->items['high'] = (float) $high; + return $this; } @@ -44,6 +46,7 @@ class Ohlc extends MarketDataType public function setLow($low): self { $this->items['low'] = (float) $low; + return $this; } @@ -55,6 +58,7 @@ class Ohlc extends MarketDataType public function setClose($close): self { $this->items['close'] = (float) $close; + return $this; } @@ -63,9 +67,10 @@ class Ohlc extends MarketDataType 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'); + return $this; } @@ -73,4 +78,4 @@ class Ohlc extends MarketDataType { return $this->items['date'] ?? null; } -} \ No newline at end of file +} diff --git a/app/Interfaces/MarketData/Types/Quote.php b/app/Interfaces/MarketData/Types/Quote.php index 24109ca..0fcbb23 100644 --- a/app/Interfaces/MarketData/Types/Quote.php +++ b/app/Interfaces/MarketData/Types/Quote.php @@ -4,13 +4,13 @@ namespace App\Interfaces\MarketData\Types; use DateTime; use Illuminate\Support\Carbon; -use App\Interfaces\MarketData\Types\MarketDataType; class Quote extends MarketDataType -{ +{ public function setName($name): self { $this->items['name'] = (string) $name; + return $this; } @@ -22,6 +22,7 @@ class Quote extends MarketDataType public function setSymbol($symbol): self { $this->items['symbol'] = (string) $symbol; + return $this; } @@ -30,9 +31,10 @@ class Quote extends MarketDataType return $this->items['symbol'] ?? ''; } - public function setMarketValue($marketValue): self + public function setMarketValue($marketValue): self { $this->items['market_value'] = (float) $marketValue; + return $this; } @@ -41,9 +43,10 @@ class Quote extends MarketDataType return $this->items['market_value'] ?? 0.0; } - public function setFiftyTwoWeekHigh($high): self + public function setFiftyTwoWeekHigh($high): self { $this->items['fifty_two_week_high'] = (float) $high; + return $this; } @@ -52,9 +55,10 @@ class Quote extends MarketDataType return $this->items['fifty_two_week_high'] ?? 0.0; } - public function setFiftyTwoWeekLow($low): self + public function setFiftyTwoWeekLow($low): self { $this->items['fifty_two_week_low'] = (float) $low; + return $this; } @@ -63,9 +67,10 @@ class Quote extends MarketDataType return $this->items['fifty_two_week_low'] ?? 0.0; } - public function setForwardPE($pe): self + public function setForwardPE($pe): self { $this->items['forward_pe'] = (float) $pe; + return $this; } @@ -74,9 +79,10 @@ class Quote extends MarketDataType return $this->items['forward_pe'] ?? 0.0; } - public function setTrailingPE($pe): self + public function setTrailingPE($pe): self { $this->items['trailing_pe'] = (float) $pe; + return $this; } @@ -88,6 +94,7 @@ class Quote extends MarketDataType public function setMarketCap($cap): self { $this->items['market_cap'] = (int) $cap; + return $this; } @@ -96,9 +103,10 @@ class Quote extends MarketDataType return $this->items['market_cap'] ?? 0; } - public function setBookValue($value): self + public function setBookValue($value): self { $this->items['book_value'] = (float) $value; + return $this; } @@ -110,6 +118,7 @@ class Quote extends MarketDataType 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'); + return $this; } @@ -118,9 +127,10 @@ class Quote extends MarketDataType return $this->items['last_dividend_date'] ?? null; } - public function setDividendYield($yield): self + public function setDividendYield($yield): self { $this->items['dividend_yield'] = (float) $yield; + return $this; } @@ -128,4 +138,4 @@ class Quote extends MarketDataType { return $this->items['dividend_yield'] ?? 0.0; } -} \ No newline at end of file +} diff --git a/app/Interfaces/MarketData/Types/Split.php b/app/Interfaces/MarketData/Types/Split.php index 748bac2..595c811 100644 --- a/app/Interfaces/MarketData/Types/Split.php +++ b/app/Interfaces/MarketData/Types/Split.php @@ -4,13 +4,13 @@ namespace App\Interfaces\MarketData\Types; use DateTime; use Illuminate\Support\Carbon; -use App\Interfaces\MarketData\Types\MarketDataType; class Split extends MarketDataType { public function setSymbol(string $symbol): self { $this->items['symbol'] = $symbol; + return $this; } @@ -22,6 +22,7 @@ class Split extends MarketDataType public function setSplitAmount($splitAmount): self { $this->items['split_amount'] = (float) $splitAmount; + return $this; } @@ -30,9 +31,10 @@ class Split extends MarketDataType 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'); + return $this; } @@ -40,4 +42,4 @@ class Split extends MarketDataType { return $this->items['date'] ?? null; } -} \ No newline at end of file +} diff --git a/app/Interfaces/MarketData/YahooMarketData.php b/app/Interfaces/MarketData/YahooMarketData.php index 1c7f77a..6bc31bd 100644 --- a/app/Interfaces/MarketData/YahooMarketData.php +++ b/app/Interfaces/MarketData/YahooMarketData.php @@ -2,36 +2,39 @@ namespace App\Interfaces\MarketData; -use Illuminate\Support\Collection; -use Scheb\YahooFinanceApi\ApiClient; +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 App\Interfaces\MarketData\Types\Dividend; +use Illuminate\Support\Collection; +use Scheb\YahooFinanceApi\ApiClient; use Scheb\YahooFinanceApi\ApiClientFactory as YahooFinance; class YahooMarketData implements MarketDataInterface { public ApiClient $client; - public function __construct() { + public function __construct() + { // create yahoo finance client factory $this->client = YahooFinance::createApiClient(); } - public function exists(String $symbol): Bool + public function exists(string $symbol): bool { return $this->quote($symbol)->isNotEmpty(); } - public function quote(String $symbol): Quote + public function quote(string $symbol): Quote { $quote = $this->client->getQuote($symbol); - if (empty($quote)) return collect(); + if (empty($quote)) { + return collect(); + } return new Quote([ 'name' => $quote->getLongName() ?? $quote->getShortName(), @@ -44,52 +47,52 @@ class YahooMarketData implements MarketDataInterface 'market_cap' => $quote->getMarketCap(), 'book_value' => $quote->getBookValue(), '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)) - ->map(function($dividend) use ($symbol) { - - return new Dividend([ - 'symbol' => $symbol, - 'date' => $dividend->getDate(), - 'dividend_amount' => $dividend->getDividends(), - ]); - }); + ->map(function ($dividend) use ($symbol) { + + return new Dividend([ + 'symbol' => $symbol, + 'date' => $dividend->getDate(), + 'dividend_amount' => $dividend->getDividends(), + ]); + }); } - public function splits(String $symbol, $startDate, $endDate): Collection - { + public function splits(string $symbol, $startDate, $endDate): Collection + { return collect($this->client->getHistoricalSplitData($symbol, $startDate, $endDate)) - ->map(function($split) use ($symbol) { - $split_amount = explode(':', $split->getStockSplits()); + ->map(function ($split) use ($symbol) { + $split_amount = explode(':', $split->getStockSplits()); - return new Split([ - 'symbol' => $symbol, - 'date' => $split->getDate(), - 'split_amount' => $split_amount[0] / $split_amount[1], - ]); - }); + return new Split([ + 'symbol' => $symbol, + 'date' => $split->getDate(), + 'split_amount' => $split_amount[0] / $split_amount[1], + ]); + }); } - public function history(String $symbol, $startDate, $endDate): Collection + public function history(string $symbol, $startDate, $endDate): Collection { return collect($this->client->getHistoricalQuoteData($symbol, ApiClient::INTERVAL_1_DAY, $startDate, $endDate)) - ->mapWithKeys(function($history) use ($symbol) { + ->mapWithKeys(function ($history) use ($symbol) { $date = $history->getDate()->format('Y-m-d'); - return [ $date => new Ohlc([ - 'symbol' => $symbol, - 'date' => $date, - 'close' => $history->getClose(), - ]) ]; + return [$date => new Ohlc([ + 'symbol' => $symbol, + 'date' => $date, + 'close' => $history->getClose(), + ])]; }); } -} \ No newline at end of file +} diff --git a/app/Jobs/BackupImportJob.php b/app/Jobs/BackupImportJob.php index 5d536da..e3c7c44 100644 --- a/app/Jobs/BackupImportJob.php +++ b/app/Jobs/BackupImportJob.php @@ -2,15 +2,15 @@ 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\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 { @@ -19,7 +19,7 @@ class BackupImportJob implements ShouldQueue /** * The number of times the job may be attempted. */ - public $tries = 1; + public $tries = 1; /** * The number of seconds the job can run before timing out. @@ -42,7 +42,7 @@ class BackupImportJob implements ShouldQueue */ public function __construct( public BackupImport $backupImport - ) { + ) { $this->user = User::find($this->backupImport->user_id); } @@ -50,7 +50,7 @@ class BackupImportJob implements ShouldQueue * Execute the job. */ public function handle(): void - { + { Excel::import(new BackupImportExcel($this->backupImport), $this->backupImport->path, config('livewire.temporary_file_upload.disk', null)); $this->user->notify(new ImportSucceededNotification); @@ -63,9 +63,9 @@ class BackupImportJob implements ShouldQueue { $this->backupImport->update([ 'status' => 'failed', - 'message' => 'Error: '. substr($e->getMessage(), 0, 220), + 'message' => 'Error: '.substr($e->getMessage(), 0, 220), 'has_errors' => true, - 'completed_at' => now() + 'completed_at' => now(), ]); $this->user->notify(new ImportFailedNotification($e->getMessage())); diff --git a/app/Models/AiChat.php b/app/Models/AiChat.php index 3b4e386..c8e1c5b 100644 --- a/app/Models/AiChat.php +++ b/app/Models/AiChat.php @@ -2,8 +2,8 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Concerns\HasUuids; +use Illuminate\Database\Eloquent\Model; class AiChat extends Model { @@ -11,7 +11,7 @@ class AiChat extends Model protected $fillable = [ 'role', - 'content' + 'content', ]; protected $hidden = []; @@ -26,7 +26,8 @@ class AiChat extends Model }); } - public function user() { + public function user() + { return $this->belongsTo(User::class); } diff --git a/app/Models/BackupImport.php b/app/Models/BackupImport.php index 1ddd7fe..a747880 100644 --- a/app/Models/BackupImport.php +++ b/app/Models/BackupImport.php @@ -2,11 +2,9 @@ namespace App\Models; -use Maatwebsite\Excel\Facades\Excel; -use Illuminate\Database\Eloquent\Model; -use App\Imports\BackupImport as BackupImportExcel; use App\Jobs\BackupImportJob; use Illuminate\Database\Eloquent\Concerns\HasUuids; +use Illuminate\Database\Eloquent\Model; class BackupImport extends Model { @@ -20,7 +18,7 @@ class BackupImport extends Model 'status', // pending, in_progress, success, failed 'message', // Import starting, Import is in progress, Importing portfolios, Importing transactions, Importing daily changes, Import completed successfully 'has_errors', - 'completed_at' + 'completed_at', ]; protected static function boot() @@ -32,9 +30,9 @@ class BackupImport extends Model $import->status = 'pending'; $import->message = __('Import starting...'); }); - + static::created(function ($import) { - + BackupImportJob::dispatch($import); }); } @@ -47,7 +45,7 @@ class BackupImport extends Model { return [ 'has_errors' => 'boolean', - 'completed_at' => 'datetime' + 'completed_at' => 'datetime', ]; } } diff --git a/app/Models/ConnectedAccount.php b/app/Models/ConnectedAccount.php index 372544f..e3911a4 100644 --- a/app/Models/ConnectedAccount.php +++ b/app/Models/ConnectedAccount.php @@ -29,7 +29,7 @@ class ConnectedAccount extends Model ]; protected $with = [ - 'user' + 'user', ]; /** @@ -52,4 +52,4 @@ class ConnectedAccount extends Model { return $this->belongsTo(User::class); } -} \ No newline at end of file +} diff --git a/app/Models/DailyChange.php b/app/Models/DailyChange.php index 59df828..21ad0ef 100644 --- a/app/Models/DailyChange.php +++ b/app/Models/DailyChange.php @@ -3,12 +3,12 @@ namespace App\Models; use App\Traits\HasCompositePrimaryKey; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class DailyChange extends Model { - use HasFactory, HasCompositePrimaryKey; + use HasCompositePrimaryKey, HasFactory; public $timestamps = false; @@ -32,13 +32,13 @@ class DailyChange extends Model protected $casts = [ 'date' => 'datetime', ]; - + public function scopePortfolio($query, $portfolio) { return $query->where('portfolio_id', $portfolio); } - public function scopeMyDailyChanges() + public function scopeMyDailyChanges() { return $this->whereHas('portfolio', function ($query) { $query->whereHas('users', function ($query) { @@ -47,12 +47,13 @@ class DailyChange extends Model }); } - public function scopeWithoutWishlists($query) { + public function scopeWithoutWishlists($query) + { return $query->whereHas('portfolio', function ($query) { $query->where('portfolios.wishlist', 0); }); } - + public function portfolio() { return $this->belongsTo(Portfolio::class); diff --git a/app/Models/Dividend.php b/app/Models/Dividend.php index b53b7d9..7755c9c 100644 --- a/app/Models/Dividend.php +++ b/app/Models/Dividend.php @@ -2,15 +2,12 @@ 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 Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Carbon; +use Illuminate\Support\Str; class Dividend extends Model { @@ -30,15 +27,18 @@ class Dividend extends Model 'last_dividend_update' => 'datetime', ]; - public function marketData() { + public function marketData() + { return $this->belongsTo(MarketData::class, 'symbol', 'symbol'); } - public function holdings() { + public function holdings() + { return $this->hasMany(Holding::class, 'symbol', 'symbol'); } - public function transactions() { + public function transactions() + { return $this->hasMany(Transaction::class, 'symbol', 'symbol'); } @@ -49,7 +49,6 @@ class Dividend extends Model /** * Grab new dividend data - * */ public static function refreshDividendData(string $symbol): void { @@ -64,11 +63,11 @@ class Dividend extends Model $end_date = now(); // nope, refresh forward looking only - if ( $dividends_meta->total_dividends ) { - + if ($dividends_meta->total_dividends) { + $start_date = $dividends_meta->last_dividend_update->addHours(24); } - + // skip refresh if there's already recent data if ($start_date->greaterThan($end_date)) { @@ -83,7 +82,7 @@ class Dividend extends Model // ah, we found some dividends... if ($dividend_data->isNotEmpty()) { // 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()]]; } @@ -109,7 +108,7 @@ class Dividend extends Model { // group by holdings $dividends = self::select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) - ->selectRaw(' + ->selectRaw(' (COALESCE(CASE WHEN transactions.transaction_type = "BUY" AND date(transactions.date) <= date(dividends.date) THEN transactions.quantity ELSE 0 END, 0) @@ -119,22 +118,22 @@ class Dividend extends Model * dividends.dividend_amount AS total_received ') - ->join('transactions', 'transactions.symbol', '=', 'dividends.symbol') - ->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id') - ->where('dividends.symbol', $symbol) - ->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received') - ->havingRaw('total_received > 0') - ->get(); + ->join('transactions', 'transactions.symbol', '=', 'dividends.symbol') + ->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id') + ->where('dividends.symbol', $symbol) + ->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received') + ->havingRaw('total_received > 0') + ->get(); - // iterate through holdings and update + // iterate through holdings and update Holding::where(['symbol' => $symbol]) - ->get() - ->each(function ($holding) use ($dividends) { - $holding->update([ - 'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id) - ->sum('total_received') - ]); - }); + ->get() + ->each(function ($holding) use ($dividends) { + $holding->update([ + 'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id) + ->sum('total_received'), + ]); + }); } public static function reinvestDividends(iterable $dividend_data, MarketData $market_data): void @@ -144,21 +143,21 @@ class Dividend extends Model 'symbol' => $market_data->symbol, 'reinvest_dividends' => true, ]) - ->get() - ->each(function($holding) use ($dividend_data, $market_data) { + ->get() + ->each(function ($holding) use ($dividend_data, $market_data) { - foreach($dividend_data as $dividend) { + foreach ($dividend_data as $dividend) { - Transaction::create([ - 'date' => $dividend['date'], - 'portfolio_id' => $holding->portfolio_id, - 'symbol' => $holding->symbol, - 'transaction_type' => "BUY", - 'reinvested_dividend' => true, - 'cost_basis' => 0, - 'quantity' => ($dividend['dividend_amount'] * $holding->qtyOwned(Carbon::parse($dividend['date']))) / $market_data->market_value, - ]); - } - }); + Transaction::create([ + 'date' => $dividend['date'], + 'portfolio_id' => $holding->portfolio_id, + 'symbol' => $holding->symbol, + 'transaction_type' => 'BUY', + 'reinvested_dividend' => true, + 'cost_basis' => 0, + 'quantity' => ($dividend['dividend_amount'] * $holding->qtyOwned(Carbon::parse($dividend['date']))) / $market_data->market_value, + ]); + } + }); } } diff --git a/app/Models/Holding.php b/app/Models/Holding.php index 6cb4d43..122042d 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -2,16 +2,10 @@ 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\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\DB; class Holding extends Model { @@ -27,13 +21,13 @@ class Holding extends Model 'realized_gain_dollars', 'dividends_earned', 'splits_synced_at', - 'reinvest_dividends' + 'reinvest_dividends', ]; protected $casts = [ 'splits_synced_at' => 'datetime', 'first_transaction_date' => 'datetime', - 'reinvest_dividends' => 'boolean' + 'reinvest_dividends' => 'boolean', ]; /** @@ -41,7 +35,7 @@ class Holding extends Model * * @return void */ - public function market_data() + public function market_data() { return $this->hasOne(MarketData::class, 'symbol', 'symbol'); } @@ -51,7 +45,7 @@ class Holding extends Model * * @return void */ - public function transactions() + public function transactions() { return $this->hasManyThrough(Transaction::class, Portfolio::class, 'id', 'portfolio_id', 'portfolio_id', 'id')->orderBy('date', 'DESC'); } @@ -61,11 +55,11 @@ class Holding extends Model * * @return void */ - public function dividends() + public function dividends() { return $this->hasMany(Dividend::class, 'symbol', 'symbol') - ->select(['dividends.symbol','dividends.date','dividends.dividend_amount']) - ->selectRaw("SUM( + ->select(['dividends.symbol', 'dividends.date', 'dividends.dividend_amount']) + ->selectRaw("SUM( CASE WHEN transaction_type = 'BUY' AND transactions.symbol = dividends.symbol AND transactions.portfolio_id = '$this->portfolio_id' @@ -73,7 +67,7 @@ class Holding extends Model THEN transactions.quantity ELSE 0 END ) AS purchased") - ->selectRaw("SUM( + ->selectRaw("SUM( CASE WHEN transaction_type = 'SELL' AND transactions.symbol = dividends.symbol AND transactions.portfolio_id = '$this->portfolio_id' @@ -81,7 +75,7 @@ class Holding extends Model THEN transactions.quantity ELSE 0 END ) AS sold") - ->selectRaw("SUM( + ->selectRaw("SUM( (CASE WHEN transaction_type = 'BUY' AND transactions.symbol = dividends.symbol AND transactions.portfolio_id = '$this->portfolio_id' @@ -94,16 +88,16 @@ class Holding extends Model THEN transactions.quantity ELSE 0 END) * dividends.dividend_amount ) AS total_received") - ->join('transactions', 'transactions.symbol', 'dividends.symbol') - ->groupBy(['dividends.symbol','dividends.date','dividends.dividend_amount']) - ->orderBy('dividends.date', 'DESC') - ->where('dividends.date', '>=', function ($query) { - $query->selectRaw('min(transactions.date)') - ->from('transactions') - ->whereRaw("transactions.portfolio_id = '$this->portfolio_id'") - ->whereRaw("transactions.symbol = '$this->symbol'"); - }) - ->having('total_received', '>', 0); + ->join('transactions', 'transactions.symbol', 'dividends.symbol') + ->groupBy(['dividends.symbol', 'dividends.date', 'dividends.dividend_amount']) + ->orderBy('dividends.date', 'DESC') + ->where('dividends.date', '>=', function ($query) { + $query->selectRaw('min(transactions.date)') + ->from('transactions') + ->whereRaw("transactions.portfolio_id = '$this->portfolio_id'") + ->whereRaw("transactions.symbol = '$this->symbol'"); + }) + ->having('total_received', '>', 0); } /** @@ -111,7 +105,7 @@ class Holding extends Model * * @return void */ - public function portfolio() + public function portfolio() { return $this->belongsTo(Portfolio::class); } @@ -121,7 +115,7 @@ class Holding extends Model * * @return void */ - public function splits() + public function splits() { return $this->hasMany(Split::class, 'symbol', 'symbol') ->orderBy('date', 'DESC'); @@ -140,11 +134,11 @@ class Holding extends Model public function scopeWithMarketData($query) { return $query->withAggregate('market_data', 'name') - ->withAggregate('market_data', 'market_value') - ->withAggregate('market_data', 'fifty_two_week_low') - ->withAggregate('market_data', 'fifty_two_week_high') - ->withAggregate('market_data', 'updated_at') - ->join('market_data', 'holdings.symbol', 'market_data.symbol'); + ->withAggregate('market_data', 'market_value') + ->withAggregate('market_data', 'fifty_two_week_low') + ->withAggregate('market_data', 'fifty_two_week_high') + ->withAggregate('market_data', 'updated_at') + ->join('market_data', 'holdings.symbol', 'market_data.symbol'); } public function scopeWithPerformance($query) @@ -164,49 +158,50 @@ class Holding extends Model return $query->where('holdings.symbol', $symbol); } - public function scopeWithoutWishlists($query) { + public function scopeWithoutWishlists($query) + { return $query->whereHas('portfolio', function ($query) { - $query->where('portfolios.wishlist', 0); - }); + $query->where('portfolios.wishlist', 0); + }); } 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); }); } - public function scopeWithPortfolioMetrics($query) + public function scopeWithPortfolioMetrics($query) { return $query->selectRaw('COALESCE(SUM(holdings.dividends_earned), 0) AS total_dividends_earned') - ->selectRaw('COALESCE(SUM(holdings.realized_gain_dollars), 0) AS realized_gain_dollars') - ->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) AS total_market_value') - ->selectRaw('COALESCE(SUM(holdings.total_cost_basis), 0) AS total_cost_basis') - ->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) - COALESCE(SUM(holdings.total_cost_basis), 0) AS total_gain_dollars') + ->selectRaw('COALESCE(SUM(holdings.realized_gain_dollars), 0) AS realized_gain_dollars') + ->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) AS total_market_value') + ->selectRaw('COALESCE(SUM(holdings.total_cost_basis), 0) AS total_cost_basis') + ->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) - COALESCE(SUM(holdings.total_cost_basis), 0) AS total_gain_dollars') // ->selectRaw('COALESCE((@total_gain_dollars / @sum_total_cost_basis) * 100,0) AS total_gain_percent') - ->join('market_data', 'market_data.symbol', '=', 'holdings.symbol'); + ->join('market_data', 'market_data.symbol', '=', 'holdings.symbol'); } public function syncTransactionsAndDividends() { // pull existing transaction data $query = Transaction::where([ - 'portfolio_id' => $this->portfolio_id, - 'symbol' => $this->symbol, - ])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`') - ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`') - ->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `total_cost_basis`') - ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN (quantity * sale_price) ELSE 0 END) AS `total_sale_price`') - ->first(); + 'portfolio_id' => $this->portfolio_id, + 'symbol' => $this->symbol, + ])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`') + ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`') + ->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `total_cost_basis`') + ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN (quantity * sale_price) ELSE 0 END) AS `total_sale_price`') + ->first(); $total_quantity = round($query->qty_purchases - $query->qty_sales, 3); $average_cost_basis = ( - $query->qty_purchases > 0 - && $total_quantity > 0 - ) - ? $query->total_cost_basis / $query->qty_purchases + $query->qty_purchases > 0 + && $total_quantity > 0 + ) + ? $query->total_cost_basis / $query->qty_purchases : 0; // update holding @@ -214,18 +209,20 @@ class Holding extends Model 'quantity' => $total_quantity, 'average_cost_basis' => $average_cost_basis, 'total_cost_basis' => $total_quantity * $average_cost_basis, - '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)) + '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)) : 0, - 'dividends_earned' => $this->dividends->sum('total_received') + 'dividends_earned' => $this->dividends->sum('total_received'), ]); $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); @@ -237,16 +234,20 @@ class Holding extends Model } public function dailyPerformance( - \Illuminate\Support\Carbon $start_date = null, - \Illuminate\Support\Carbon $end_date = null, + ?\Illuminate\Support\Carbon $start_date = null, + ?\Illuminate\Support\Carbon $end_date = null, ) { - if ($start_date == null) $start_date = now(); - if ($end_date == null) $end_date = now(); + if ($start_date == null) { + $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') { - + $date_interval = "date(date, '+1 day')"; } else { @@ -265,14 +266,14 @@ class Holding extends Model FROM date_series ) as date_series") ) - ->select([ - 'date_series.date', - DB::raw(" + ->select([ + 'date_series.date', + DB::raw(" ROUND( COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) - COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0), 3) AS `owned` "), - DB::raw(" + DB::raw(" COALESCE(CASE WHEN ( ROUND( @@ -285,29 +286,30 @@ class Holding extends Model END) 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`") - ]) - ->leftJoin('transactions', function ($join) { - $join->on(DB::raw('DATE(transactions.date)'), '<=', 'date_series.date') - ->where('transactions.symbol', '=', $this->symbol) - ->where('transactions.portfolio_id', '=', $this->portfolio_id); - }) - ->groupBy('date_series.date') - ->orderBy('date_series.date') - ->get() - ->keyBy('date'); + 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) { + $join->on(DB::raw('DATE(transactions.date)'), '<=', 'date_series.date') + ->where('transactions.symbol', '=', $this->symbol) + ->where('transactions.portfolio_id', '=', $this->portfolio_id); + }) + ->groupBy('date_series.date') + ->orderBy('date_series.date') + ->get() + ->keyBy('date'); } public function getFormattedTransactions() { $formattedTransactions = ''; - foreach($this->transactions->sortByDesc('date') as $transaction) { - $formattedTransactions .= " * ".$transaction->date->format('Y-m-d') - ." ". $transaction->transaction_type - ." ". $transaction->quantity - ." @ ". $transaction->cost_basis + foreach ($this->transactions->sortByDesc('date') as $transaction) { + $formattedTransactions .= ' * '.$transaction->date->format('Y-m-d') + .' '.$transaction->transaction_type + .' '.$transaction->quantity + .' @ '.$transaction->cost_basis ." each \n\n"; } + return $formattedTransactions; } -} \ No newline at end of file +} diff --git a/app/Models/MarketData.php b/app/Models/MarketData.php index d7ce420..69d137b 100644 --- a/app/Models/MarketData.php +++ b/app/Models/MarketData.php @@ -2,16 +2,18 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; use App\Interfaces\MarketData\MarketDataInterface; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class MarketData extends Model { use HasFactory; protected $primaryKey = 'symbol'; + protected $keyType = 'string'; + public $incrementing = false; protected $fillable = [ @@ -25,7 +27,7 @@ class MarketData extends Model 'market_cap', 'book_value', 'last_dividend_date', - 'dividend_yield' + 'dividend_yield', ]; protected $casts = [ @@ -37,10 +39,10 @@ class MarketData extends Model 'trailing_pe' => 'float', 'market_cap' => 'float', 'book_value' => 'float', - 'dividend_yield' => 'float' + 'dividend_yield' => 'float', ]; - public function holdings() + public function holdings() { return $this->hasMany(Holding::class, 'symbol', 'symbol'); } @@ -50,20 +52,20 @@ class MarketData extends Model return $query->where('symbol', $symbol); } - public static function getMarketData($symbol, $force = false) + public static function getMarketData($symbol, $force = false) { $market_data = self::firstOrNew([ - 'symbol' => $symbol + 'symbol' => $symbol, ]); // check if new or stale if ( $force - || !$market_data->exists + || ! $market_data->exists || is_null($market_data->updated_at) || $market_data->updated_at->diffInMinutes(now()) >= config('investbrain.refresh') ) { - + // get quote $quote = app(MarketDataInterface::class)->quote($symbol); @@ -76,4 +78,4 @@ class MarketData extends Model return $market_data; } -} \ No newline at end of file +} diff --git a/app/Models/Portfolio.php b/app/Models/Portfolio.php index 2195fdf..12054ab 100644 --- a/app/Models/Portfolio.php +++ b/app/Models/Portfolio.php @@ -2,17 +2,16 @@ namespace App\Models; -use App\Models\AiChat; +use App\Interfaces\MarketData\MarketDataInterface; +use App\Notifications\InvitedOnboardingNotification; 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\Str; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; -use Illuminate\Database\Eloquent\Model; -use App\Interfaces\MarketData\MarketDataInterface; -use Illuminate\Database\Eloquent\Concerns\HasUuids; -use App\Notifications\InvitedOnboardingNotification; -use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Str; class Portfolio extends Model { @@ -30,7 +29,7 @@ class Portfolio extends Model protected static function boot() { parent::boot(); - + static::saved(function ($portfolio) { self::ensurePortfolioHasOwner($portfolio); @@ -40,7 +39,7 @@ class Portfolio extends Model protected $hidden = []; protected $casts = [ - 'wishlist' => 'boolean' + 'wishlist' => 'boolean', ]; protected $with = ['users', 'transactions']; @@ -53,8 +52,8 @@ class Portfolio extends Model public function holdings() { return $this->hasMany(Holding::class, 'portfolio_id') - ->withMarketData() - ->withPerformance(); + ->withMarketData() + ->withPerformance(); } public function transactions() @@ -77,25 +76,25 @@ class Portfolio extends Model return $this->morphMany(AiChat::class, 'chatable')->where('user_id', auth()->user()->id); } - public function scopeMyPortfolios() + public function scopeMyPortfolios() { return $this->whereHas('users', function ($query) { $query->where('user_id', auth()->user()->id); }); } - public function scopeFullAccess($query, $user_id = null) + public function scopeFullAccess($query, $user_id = null) { return $query->whereHas('users', function ($query) use ($user_id) { $query->where('user_id', $user_id ?? auth()->user()->id) - ->where(function ($query) { - $query->where('full_access', true) - ->orWhere('owner', true); - }); + ->where(function ($query) { + $query->where('full_access', true) + ->orWhere('owner', true); + }); }); } - public function scopeWithoutWishlists() + public function scopeWithoutWishlists() { return $this->where(['wishlist' => false]); } @@ -103,7 +102,7 @@ class Portfolio extends Model public function setOwnerIdAttribute($value) { // 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; } } @@ -115,18 +114,18 @@ class Portfolio extends Model public function getOwnerAttribute() { - if (!$this->relationLoaded('user')) { - + if (! $this->relationLoaded('user')) { + $this->load('users'); } return $this->users->where('pivot.owner', true)->first(); } - public static function ensurePortfolioHasOwner(self $portfolio) + public static function ensurePortfolioHasOwner(self $portfolio) { // make sure we don't remove owner access - if (!$portfolio->owner_id) { + if (! $portfolio->owner_id) { $owner[static::$owner_id ?? auth()->user()->id] = ['owner' => true]; // save @@ -138,24 +137,24 @@ class Portfolio extends Model public function syncDailyChanges(): void { $holdings = $this->holdings() - ->join('transactions', function($join) { - $join->on('transactions.symbol', '=', 'holdings.symbol') - ->where('transactions.portfolio_id', '=', $this->id); - }) - ->select('holdings.symbol', 'holdings.portfolio_id', DB::raw('min(transactions.date) as first_transaction_date')) // get first transaction date - ->groupBy(['holdings.symbol', 'holdings.portfolio_id']) - ->get(); + ->join('transactions', function ($join) { + $join->on('transactions.symbol', '=', 'holdings.symbol') + ->where('transactions.portfolio_id', '=', $this->id); + }) + ->select('holdings.symbol', 'holdings.portfolio_id', DB::raw('min(transactions.date) as first_transaction_date')) // get first transaction date + ->groupBy(['holdings.symbol', 'holdings.portfolio_id']) + ->get(); $dividends = Dividend::whereIn('symbol', $holdings->pluck('symbol'))->get(); - + $total_performance = []; - $holdings->each(function($holding) use (&$total_performance, $dividends) { + $holdings->each(function ($holding) use (&$total_performance, $dividends) { $period = CarbonPeriod::create( - $holding->first_transaction_date, - now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day'))) - ? now()->subDay() + $holding->first_transaction_date, + now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day'))) + ? now()->subDay() : now() ); @@ -170,11 +169,11 @@ class Portfolio extends Model $dividends_earned = 0; $holding_performance = []; - foreach($period as $date) { + foreach ($period as $date) { $date = $date->format('Y-m-d'); $close = $this->getMostRecentCloseData($all_history, $date); - + $total_market_value = $daily_performance->get($date)->owned * $close; $dividends_earned += $daily_performance->get($date)->owned * ($dividends->get($date)?->dividend_amount ?? 0); @@ -182,18 +181,18 @@ class Portfolio extends Model $holding_performance[$date] = [ 'date' => $date, 'portfolio_id' => $this->id, - 'total_market_value' => $total_market_value, + 'total_market_value' => $total_market_value, 'total_cost_basis' => $daily_performance->get($date)->cost_basis, 'total_gain' => $total_market_value - $daily_performance->get($date)->cost_basis, 'realized_gains' => $daily_performance->get($date)->realized_gains, - 'total_dividends_earned' => $dividends_earned + 'total_dividends_earned' => $dividends_earned, ]; } } foreach ($holding_performance as $date => $performance) { if (Arr::get($total_performance, $date) == null) { - + $total_performance[$date] = $performance; } else { @@ -207,9 +206,9 @@ class Portfolio extends Model } }); - if (!empty($total_performance)) { + if (! empty($total_performance)) { DB::transaction(function () use ($total_performance) { - + $this->daily_change()->upsert( $total_performance, ['date', 'portfolio_id'], @@ -218,7 +217,7 @@ class Portfolio extends Model 'total_cost_basis', 'total_gain', 'realized_gains', - 'total_dividends_earned' + 'total_dividends_earned', ] ); }); @@ -229,10 +228,10 @@ class Portfolio extends Model { $close = Arr::get($history, "$date.close", 0); - if (!$close && $i < $max_attempts) { + if (! $close && $i < $max_attempts) { $i++; - + $date = Carbon::parse($date)->subDay()->format('Y-m-d'); return $this->getMostRecentCloseData($history, $date, $i); @@ -244,53 +243,47 @@ class Portfolio extends Model public function getFormattedHoldings() { $formattedHoldings = ''; - foreach($this->holdings as $holding) { - $formattedHoldings .= " * Holding of ".$holding->market_data->name." (".$holding->symbol.")" - ."; with ". ($holding->quantity > 0 ? $holding->quantity : 'ZERO') . " shares" - ."; avg cost basis ". $holding->average_cost_basis - ."; curr market value ". $holding->market_data->market_value - ."; unrealized gains ". $holding->market_gain_dollars - ."; realized gains ". $holding->realized_gain_dollars - ."; dividends earned ". $holding->dividends_earned + foreach ($this->holdings as $holding) { + $formattedHoldings .= ' * Holding of '.$holding->market_data->name.' ('.$holding->symbol.')' + .'; with '.($holding->quantity > 0 ? $holding->quantity : 'ZERO').' shares' + .'; avg cost basis '.$holding->average_cost_basis + .'; curr market value '.$holding->market_data->market_value + .'; unrealized gains '.$holding->market_gain_dollars + .'; realized gains '.$holding->realized_gain_dollars + .'; dividends earned '.$holding->dividends_earned ."\n\n"; } + return $formattedHoldings; } /** * Share a portfolio with a user - * - * @param string $email - * @param boolean $fullAccess - * @return void */ public function share(string $email, bool $fullAccess = false): void { $user = User::firstOrCreate([ - 'email' => $email + 'email' => $email, ], [ - 'name' => Str::title(Str::before($email, '@')) + 'name' => Str::title(Str::before($email, '@')), ]); $permissions[$user->id] = [ - 'full_access' => $fullAccess + 'full_access' => $fullAccess, ]; $sync = $this->users()->syncWithoutDetaching($permissions); - if (!empty($sync['attached'])) { + if (! empty($sync['attached'])) { - foreach($sync['attached'] as $newUserId) { + foreach ($sync['attached'] as $newUserId) { User::find($newUserId)->notify(new InvitedOnboardingNotification($this, auth()->user())); - }; + } } } /** * Un-share a portfolio - * - * @param string $userId - * @return void */ public function unShare(string $userId): void { diff --git a/app/Models/Split.php b/app/Models/Split.php index 092cf07..8429a5f 100644 --- a/app/Models/Split.php +++ b/app/Models/Split.php @@ -2,13 +2,12 @@ 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 Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; class Split extends Model { @@ -28,22 +27,23 @@ class Split extends Model 'last_date' => 'datetime', ]; - public function holdings() { + public function holdings() + { return $this->hasMany(Holding::class, 'symbol', 'symbol'); } - public function transactions() { + public function transactions() + { return $this->hasMany(Transaction::class, 'symbol', 'symbol'); } /** * Grab new split data * - * @param string $symbol - * @param \DateTimeInterface|null $start_date + * @param \DateTimeInterface|null $start_date * @return void */ - public static function refreshSplitData(string $symbol) + public static function refreshSplitData(string $symbol) { // dates for split data $splits_meta = self::where(['symbol' => $symbol]) @@ -58,9 +58,9 @@ class Split extends Model // nope, need to populate newer split data if ($splits_meta->total_splits) { - + $start_date = $splits_meta->last_date->addHours(48); - $end_date = now(); + $end_date = now(); } // get some data @@ -71,10 +71,10 @@ class Split extends Model if ($split_data->isNotEmpty()) { // insert records - (new self)->insert($split_data->map(function($split) { + (new self)->insert($split_data->map(function ($split) { return [...$split, ...['id' => Str::uuid()->toString()]]; - })->toArray()); + })->toArray()); } // sync to transactions @@ -84,39 +84,39 @@ class Split extends Model /** * Syncs all transactions of symbol with split data * - * @param string $symbol + * @param string $symbol * @return void */ - public static function syncToTransactions($symbol) + public static function syncToTransactions($symbol) { // get splits joined with matching holdings $splits = self::select([ - 'splits.date', - 'splits.symbol', - 'splits.split_amount', - 'holdings.portfolio_id' - ]) - ->where([ - 'splits.symbol' => $symbol, - ]) - ->whereDate('splits.date', '>', DB::raw('IFNULL(holdings.splits_synced_at, "0000-00-00")')) - ->where('holdings.quantity', '>', 0) - ->join('holdings', 'splits.symbol', 'holdings.symbol') - ->orderBy('splits.date', 'ASC') - ->get(); + 'splits.date', + 'splits.symbol', + 'splits.split_amount', + 'holdings.portfolio_id', + ]) + ->where([ + 'splits.symbol' => $symbol, + ]) + ->whereDate('splits.date', '>', DB::raw('IFNULL(holdings.splits_synced_at, "0000-00-00")')) + ->where('holdings.quantity', '>', 0) + ->join('holdings', 'splits.symbol', 'holdings.symbol') + ->orderBy('splits.date', 'ASC') + ->get(); - foreach($splits as $split) { + foreach ($splits as $split) { // get qty owned when split was issued $qty_owned = Transaction::where([ - 'symbol' => $split->symbol, - 'portfolio_id' => $split->portfolio_id - ]) + 'symbol' => $split->symbol, + 'portfolio_id' => $split->portfolio_id, + ]) ->whereDate('transactions.date', '<', $split->date->format('Y-m-d')) ->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) - SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS qty_owned') ->value('qty_owned'); - + if ($qty_owned > 0) { Transaction::create([ @@ -128,14 +128,14 @@ class Split extends Model 'cost_basis' => 0, 'split' => true, 'created_at' => now(), - 'updated_at' => now() + 'updated_at' => now(), ]); Holding::where([ 'symbol' => $split->symbol, - 'portfolio_id' => $split->portfolio_id + 'portfolio_id' => $split->portfolio_id, ])->update([ - 'splits_synced_at' => now() + 'splits_synced_at' => now(), ]); } } diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index a4793d0..dc9cc99 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -2,12 +2,11 @@ namespace App\Models; -use App\Models\MarketData; -use Illuminate\Support\Arr; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Arr; class Transaction extends Model { @@ -23,7 +22,7 @@ class Transaction extends Model 'cost_basis', 'sale_price', 'split', - 'reinvested_dividend' + 'reinvested_dividend', ]; protected $hidden = []; @@ -31,7 +30,7 @@ class Transaction extends Model protected $casts = [ 'date' => 'datetime', 'split' => 'boolean', - 'reinvested_dividend' => 'boolean' + 'reinvested_dividend' => 'boolean', ]; protected static function boot() @@ -52,14 +51,14 @@ class Transaction extends Model $transaction->refreshMarketData(); - cache()->forget('portfolio-metrics-' . $transaction->portfolio_id); + cache()->forget('portfolio-metrics-'.$transaction->portfolio_id); }); static::deleted(function ($transaction) { $transaction->syncToHolding(); - cache()->forget('portfolio-metrics-' . $transaction->portfolio_id); + cache()->forget('portfolio-metrics-'.$transaction->portfolio_id); }); } @@ -96,13 +95,13 @@ class Transaction extends Model public function scopeWithMarketData($query) { return $query->withAggregate('market_data', 'name') - ->withAggregate('market_data', 'market_value') - ->withAggregate('market_data', 'fifty_two_week_low') - ->withAggregate('market_data', 'fifty_two_week_high') - ->withAggregate('market_data', 'updated_at') - ->join('market_data', 'transactions.symbol', 'market_data.symbol'); + ->withAggregate('market_data', 'market_value') + ->withAggregate('market_data', 'fifty_two_week_low') + ->withAggregate('market_data', 'fifty_two_week_high') + ->withAggregate('market_data', 'updated_at') + ->join('market_data', 'transactions.symbol', 'market_data.symbol'); } - + public function scopePortfolio($query, $portfolio) { return $query->where('portfolio_id', $portfolio); @@ -128,7 +127,7 @@ class Transaction extends Model return $query->whereDate('date', '<=', $date); } - public function scopeMyTransactions() + public function scopeMyTransactions() { return $this->whereHas('portfolio', function ($query) { $query->whereHas('users', function ($query) { @@ -137,7 +136,7 @@ class Transaction extends Model }); } - public function refreshMarketData() + public function refreshMarketData() { return MarketData::getMarketData($this->attributes['symbol']); } @@ -154,7 +153,7 @@ class Transaction extends Model 'symbol' => $this->symbol, 'transaction_type' => 'BUY', ])->whereDate('date', '<=', $this->date) - ->average('cost_basis'); + ->average('cost_basis'); $this->cost_basis = $average_cost_basis ?? 0; @@ -166,7 +165,8 @@ class Transaction extends Model * * @return void */ - public function syncToHolding() { + public function syncToHolding() + { // if symbol name changed, sync previous symbol too if (Arr::has($this->changes, 'symbol')) { @@ -181,7 +181,7 @@ class Transaction extends Model // get the holding for a symbol and portfolio (or create one) Holding::firstOrNew([ 'portfolio_id' => $this->portfolio_id, - 'symbol' => $this->symbol + 'symbol' => $this->symbol, ], [ 'portfolio_id' => $this->portfolio_id, 'symbol' => $this->symbol, @@ -191,4 +191,4 @@ class Transaction extends Model 'splits_synced_at' => now(), ])->syncTransactionsAndDividends(); } -} \ No newline at end of file +} diff --git a/app/Models/User.php b/app/Models/User.php index be58b82..a51b7a6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,27 +3,27 @@ namespace App\Models; use App\Traits\HasConnectedAccounts; -use Laravel\Sanctum\HasApiTokens; -use Laravel\Jetstream\HasProfilePhoto; -use Illuminate\Notifications\Notifiable; -use Laravel\Fortify\TwoFactorAuthenticatable; -use Staudenmeir\EloquentHasManyDeep\HasManyDeep; +use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Concerns\HasUuids; -use Staudenmeir\EloquentHasManyDeep\HasRelationships; use Illuminate\Database\Eloquent\Factories\HasFactory; 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 { use HasApiTokens; + use HasConnectedAccounts; use HasFactory; use HasProfilePhoto; + use HasRelationships; + use HasUuids; use Notifiable; use TwoFactorAuthenticatable; - use HasUuids; - use HasRelationships; - use HasConnectedAccounts; protected $fillable = [ 'name', @@ -65,7 +65,7 @@ class User extends Authenticatable implements MustVerifyEmail { return $this->hasManyDeep(Holding::class, ['portfolio_user', Portfolio::class]) ->withMarketData() - ->withPerformance(); + ->withPerformance(); } public function transactions(): HasManyDeep @@ -78,6 +78,6 @@ class User extends Authenticatable implements MustVerifyEmail WHEN transaction_type = \'SELL\' THEN COALESCE(transactions.sale_price - transactions.cost_basis, 0) ELSE COALESCE(market_data.market_value - transactions.cost_basis, 0) - END AS gain_dollars'); + END AS gain_dollars'); } } diff --git a/app/Notifications/ImportFailedNotification.php b/app/Notifications/ImportFailedNotification.php index 3d615e2..be01d94 100644 --- a/app/Notifications/ImportFailedNotification.php +++ b/app/Notifications/ImportFailedNotification.php @@ -3,9 +3,9 @@ namespace App\Notifications; use Illuminate\Bus\Queueable; -use Illuminate\Notifications\Notification; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Notification; class ImportFailedNotification extends Notification implements ShouldQueue { @@ -16,7 +16,7 @@ class ImportFailedNotification extends Notification implements ShouldQueue */ public function __construct( public string $errorMessage - ) { } + ) {} /** * Get the notification's delivery channels. @@ -34,12 +34,12 @@ class ImportFailedNotification extends Notification implements ShouldQueue public function toMail(object $notifiable): MailMessage { return (new MailMessage) - ->greeting('Oh no!') - ->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.") - ->action("Try again?", route('import-export')) - ->line("**Technical details:**") - ->line($this->errorMessage); + ->greeting('Oh no!') + ->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.') + ->action('Try again?', route('import-export')) + ->line('**Technical details:**') + ->line($this->errorMessage); } /** diff --git a/app/Notifications/ImportSucceededNotification.php b/app/Notifications/ImportSucceededNotification.php index 12ec924..1ff2773 100644 --- a/app/Notifications/ImportSucceededNotification.php +++ b/app/Notifications/ImportSucceededNotification.php @@ -2,12 +2,10 @@ namespace App\Notifications; -use App\Models\User; -use App\Models\Portfolio; use Illuminate\Bus\Queueable; -use Illuminate\Notifications\Notification; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Notification; class ImportSucceededNotification extends Notification implements ShouldQueue { @@ -16,7 +14,7 @@ class ImportSucceededNotification extends Notification implements ShouldQueue /** * Create a new notification instance. */ - public function __construct() { } + public function __construct() {} /** * Get the notification's delivery channels. @@ -34,10 +32,10 @@ class ImportSucceededNotification extends Notification implements ShouldQueue public function toMail(object $notifiable): MailMessage { return (new MailMessage) - ->greeting('Woot! 🎉') - ->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.") - ->action("Get Started", route('dashboard')); + ->greeting('Woot! 🎉') + ->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.') + ->action('Get Started', route('dashboard')); } /** diff --git a/app/Notifications/InvitedOnboardingNotification.php b/app/Notifications/InvitedOnboardingNotification.php index eea0738..d605111 100644 --- a/app/Notifications/InvitedOnboardingNotification.php +++ b/app/Notifications/InvitedOnboardingNotification.php @@ -2,12 +2,12 @@ namespace App\Notifications; -use App\Models\User; use App\Models\Portfolio; +use App\Models\User; use Illuminate\Bus\Queueable; -use Illuminate\Notifications\Notification; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Notification; class InvitedOnboardingNotification extends Notification implements ShouldQueue { @@ -19,7 +19,7 @@ class InvitedOnboardingNotification extends Notification implements ShouldQueue public function __construct( public Portfolio $portfolio, public User $sender, - ) { } + ) {} /** * Get the notification's delivery channels. @@ -40,14 +40,14 @@ class InvitedOnboardingNotification extends Notification implements ShouldQueue $url = url()->signedRoute('invited_onboarding', ['portfolio' => $this->portfolio->id, 'user' => $notifiable->id], now()->addDays(90)); return (new MailMessage) - ->replyTo($this->sender->email, $this->sender->name) - ->greeting('Hey there! 👋') - ->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("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) - ->line("If you have any questions, you can reply to this email.") - ->salutation("See you there,\n". e($this->sender->name)); + ->replyTo($this->sender->email, $this->sender->name) + ->greeting('Hey there! 👋') + ->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("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) + ->line('If you have any questions, you can reply to this email.') + ->salutation("See you there,\n".e($this->sender->name)); } /** diff --git a/app/Notifications/VerifyConnectedAccountNotification.php b/app/Notifications/VerifyConnectedAccountNotification.php index e2cb48b..a4e034d 100644 --- a/app/Notifications/VerifyConnectedAccountNotification.php +++ b/app/Notifications/VerifyConnectedAccountNotification.php @@ -2,11 +2,11 @@ namespace App\Notifications; -use Illuminate\Bus\Queueable; use App\Models\ConnectedAccount; -use Illuminate\Notifications\Notification; +use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Notification; class VerifyConnectedAccountNotification extends Notification implements ShouldQueue { @@ -17,7 +17,7 @@ class VerifyConnectedAccountNotification extends Notification implements ShouldQ */ public function __construct( public string $connected_account_id - ) { } + ) {} /** * Get the notification's delivery channels. @@ -40,11 +40,11 @@ class VerifyConnectedAccountNotification extends Notification implements ShouldQ $url = url()->signedRoute('oauth.verify_connected_account', ['connected_account' => $this->connected_account_id], now()->days($days = 7)); return (new MailMessage) - ->greeting('Welcome back!') - ->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:") - ->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."); + ->greeting('Welcome back!') + ->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:") + ->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."); } /** diff --git a/app/Policies/PortfolioPolicy.php b/app/Policies/PortfolioPolicy.php index a3228e9..e060100 100644 --- a/app/Policies/PortfolioPolicy.php +++ b/app/Policies/PortfolioPolicy.php @@ -2,25 +2,18 @@ namespace App\Policies; -use App\Models\User; use App\Models\Portfolio; +use App\Models\User; class PortfolioPolicy { - - /** - * - */ public function readOnly(User $user, Portfolio $portfolio) { $pivot = $portfolio->users()->where('user_id', $user->id)->first(); - return !!$pivot; + return (bool) $pivot; } - /** - * - */ public function fullAccess(User $user, Portfolio $portfolio) { $pivot = $portfolio->users()->where('user_id', $user->id)->first(); @@ -28,9 +21,6 @@ class PortfolioPolicy return $pivot && ($pivot->pivot->full_access || $pivot->pivot->owner); } - /** - * - */ public function owner(User $user, Portfolio $portfolio) { $pivot = $portfolio->users()->where('user_id', $user->id)->first(); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 39f79b0..9350699 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,8 +2,8 @@ namespace App\Providers; -use Illuminate\Support\ServiceProvider; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index f7a2ed0..c8e2abc 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -2,11 +2,11 @@ namespace App\Providers; -use Illuminate\Support\Arr; -use Laravel\Jetstream\Features; use App\Actions\Jetstream\DeleteUser; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Config; use Illuminate\Support\ServiceProvider; +use Laravel\Jetstream\Features; use Laravel\Jetstream\Jetstream; class JetstreamServiceProvider extends ServiceProvider @@ -29,7 +29,7 @@ class JetstreamServiceProvider extends ServiceProvider Jetstream::deleteUsersUsing(DeleteUser::class); - if ( config('investbrain.self_hosted', false) ) { + if (config('investbrain.self_hosted', false)) { Config::set( 'jetstream.features', diff --git a/app/Rules/QuantityValidationRule.php b/app/Rules/QuantityValidationRule.php index 74027bc..818b893 100644 --- a/app/Rules/QuantityValidationRule.php +++ b/app/Rules/QuantityValidationRule.php @@ -13,24 +13,19 @@ class QuantityValidationRule implements ValidationRule * @return void */ public function __construct( - protected ?Portfolio $portfolio, - protected ?string $symbol, + protected ?Portfolio $portfolio, + protected ?string $symbol, protected ?string $transactionType, protected ?string $date ) { $this->portfolio = $portfolio; - $this->symbol = $symbol; + $this->symbol = $symbol; $this->transactionType = $transactionType; $this->date = $date; } /** * Validate the attribute. - * - * @param string $attribute - * @param mixed $value - * @param \Closure $fail - * @return void */ public function validate(string $attribute, mixed $value, \Closure $fail): void { @@ -42,17 +37,17 @@ class QuantityValidationRule implements ValidationRule if ($this->transactionType == 'SELL') { $purchase_qty = $this->portfolio->transactions() - ->symbol($this->symbol) - ->buy() - ->beforeDate($this->date) - ->sum('quantity'); + ->symbol($this->symbol) + ->buy() + ->beforeDate($this->date) + ->sum('quantity'); $sales_qty = $this->portfolio->transactions() - ->symbol($this->symbol) - ->sell() - ->beforeDate($this->date) - ->sum('quantity'); - + ->symbol($this->symbol) + ->sell() + ->beforeDate($this->date) + ->sum('quantity'); + $maxQuantity = $purchase_qty - $sales_qty; if (round($value, 3) > round($maxQuantity, 3)) { diff --git a/app/Rules/SymbolValidationRule.php b/app/Rules/SymbolValidationRule.php index 428aefd..ae544e1 100644 --- a/app/Rules/SymbolValidationRule.php +++ b/app/Rules/SymbolValidationRule.php @@ -22,11 +22,6 @@ class SymbolValidationRule implements ValidationRule /** * Validate the attribute. - * - * @param string $attribute - * @param mixed $value - * @param \Closure $fail - * @return void */ public function validate(string $attribute, mixed $value, \Closure $fail): void { @@ -38,8 +33,8 @@ class SymbolValidationRule implements ValidationRule } // Check if the symbol exists in the Market Data table first (avoid API call) - if (!app(MarketDataInterface::class)->exists($value)) { - $fail('The symbol provided (' . $this->symbol . ') is not valid'); + if (! app(MarketDataInterface::class)->exists($value)) { + $fail('The symbol provided ('.$this->symbol.') is not valid'); } } -} \ No newline at end of file +} diff --git a/app/Support/Helpers.php b/app/Support/Helpers.php index 77f2b79..36ea8bb 100644 --- a/app/Support/Helpers.php +++ b/app/Support/Helpers.php @@ -1,5 +1,5 @@ user()) { + if (! $request->user()) { return $results; } @@ -22,13 +20,13 @@ class Spotlight ->where('title', 'LIKE', '%'.$request->input('search').'%') ->limit(5) ->get(); - $portfolios->each(function($portfolio) use ($results) { + $portfolios->each(function ($portfolio) use ($results) { $results->push([ - 'name' => 'Portfolio: '. $portfolio->title, + 'name' => 'Portfolio: '.$portfolio->title, 'description' => null, 'link' => route('portfolio.show', ['portfolio' => $portfolio->id]), - 'avatar' => null + 'avatar' => null, ]); }); @@ -36,20 +34,20 @@ class Spotlight ->where('holdings.quantity', '>', 0) ->where(function ($query) use ($request) { return $query->where('holdings.symbol', 'LIKE', '%'.$request->input('search').'%') - ->orWhere('market_data.name', 'LIKE', '%'.$request->input('search').'%'); + ->orWhere('market_data.name', 'LIKE', '%'.$request->input('search').'%'); }) ->limit(5) ->get(); - $holdings->each(function($holding) use ($results) { + $holdings->each(function ($holding) use ($results) { $results->push([ 'name' => 'Holding: '.$holding->market_data->name.' ('.$holding->symbol.')', 'description' => $holding->portfolio->title, 'link' => route('holding.show', ['portfolio' => $holding->portfolio->id, 'symbol' => $holding->symbol]), - 'avatar' => null + 'avatar' => null, ]); }); return $results; } -} \ No newline at end of file +} diff --git a/app/Traits/HasCompositePrimaryKey.php b/app/Traits/HasCompositePrimaryKey.php index f2af335..31d4306 100644 --- a/app/Traits/HasCompositePrimaryKey.php +++ b/app/Traits/HasCompositePrimaryKey.php @@ -1,6 +1,6 @@ getKeyName() as $key) { // UPDATE: Added isset() per devflow's comment. - if (isset($this->$key)) + if (isset($this->$key)) { $query->where($key, '=', $this->$key); - else - throw new \Exception(__METHOD__ . 'Missing part of the primary key: ' . $key); + } else { + throw new \Exception(__METHOD__.'Missing part of the primary key: '.$key); + } } return $query; @@ -37,7 +38,7 @@ trait HasCompositePrimaryKey /** * Execute a query for a single record by ID. * - * @param array $ids Array of keys, like [column => value]. + * @param array $ids Array of keys, like [column => value]. * @param array $columns * @return mixed|static */ @@ -48,6 +49,7 @@ trait HasCompositePrimaryKey foreach ($me->getKeyName() as $key) { $query->where($key, '=', $ids[$key]); } + return $query->first($columns); } -} \ No newline at end of file +} diff --git a/app/Traits/HasConnectedAccounts.php b/app/Traits/HasConnectedAccounts.php index 4926d4e..3f5215d 100644 --- a/app/Traits/HasConnectedAccounts.php +++ b/app/Traits/HasConnectedAccounts.php @@ -4,9 +4,9 @@ namespace App\Traits; use App\Models\ConnectedAccount; use App\Models\User; -use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Str; /** * @property Collection $connectedAccounts @@ -63,4 +63,4 @@ trait HasConnectedAccounts { return $this->hasMany(ConnectedAccount::class); } -} \ No newline at end of file +} diff --git a/app/Traits/WithTrimStrings.php b/app/Traits/WithTrimStrings.php index 1fc10c4..0795312 100644 --- a/app/Traits/WithTrimStrings.php +++ b/app/Traits/WithTrimStrings.php @@ -13,10 +13,10 @@ trait WithTrimStrings 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([ $property => Str::trim($value), ]); } } -} \ No newline at end of file +} diff --git a/app/View/Components/MainLayout.php b/app/View/Components/MainLayout.php index a145423..8378f86 100644 --- a/app/View/Components/MainLayout.php +++ b/app/View/Components/MainLayout.php @@ -11,7 +11,7 @@ class MainLayout extends Component // Slots public mixed $body = null, - ) { } + ) {} /** * Get the view / contents that represents the component. diff --git a/config/excel.php b/config/excel.php index 5a8a7d1..0ddb85c 100644 --- a/config/excel.php +++ b/config/excel.php @@ -15,7 +15,7 @@ return [ | Here you can specify how big the chunk should be. | */ - 'chunk_size' => 1000, + 'chunk_size' => 1000, /* |-------------------------------------------------------------------------- @@ -42,15 +42,15 @@ return [ | Configure e.g. delimiter, enclosure and line ending for CSV exports. | */ - 'csv' => [ - 'delimiter' => ',', - 'enclosure' => '"', - 'line_ending' => PHP_EOL, - 'use_bom' => false, + 'csv' => [ + 'delimiter' => ',', + 'enclosure' => '"', + 'line_ending' => PHP_EOL, + 'use_bom' => false, 'include_separator_line' => false, - 'excel_compatibility' => false, - 'output_encoding' => '', - 'test_auto_detect' => true, + 'excel_compatibility' => false, + 'output_encoding' => '', + 'test_auto_detect' => true, ], /* @@ -61,20 +61,20 @@ return [ | Configure e.g. default title, creator, subject,... | */ - 'properties' => [ - 'creator' => '', + 'properties' => [ + 'creator' => '', 'lastModifiedBy' => '', - 'title' => '', - 'description' => '', - 'subject' => '', - 'keywords' => '', - 'category' => '', - 'manager' => '', - 'company' => '', + 'title' => '', + 'description' => '', + 'subject' => '', + 'keywords' => '', + 'category' => '', + 'manager' => '', + 'company' => '', ], ], - 'imports' => [ + 'imports' => [ /* |-------------------------------------------------------------------------- @@ -87,7 +87,7 @@ return [ | you can enable it by setting read_only to false. | */ - 'read_only' => true, + 'read_only' => true, /* |-------------------------------------------------------------------------- @@ -111,7 +111,7 @@ return [ | Available options: none|slug|custom | */ - 'heading_row' => [ + 'heading_row' => [ 'formatter' => 'slug', ], @@ -123,12 +123,12 @@ return [ | Configure e.g. delimiter, enclosure and line ending for CSV imports. | */ - 'csv' => [ - 'delimiter' => null, - 'enclosure' => '"', + 'csv' => [ + 'delimiter' => null, + 'enclosure' => '"', 'escape_character' => '\\', - 'contiguous' => false, - 'input_encoding' => Csv::GUESS_ENCODING, + 'contiguous' => false, + 'input_encoding' => Csv::GUESS_ENCODING, ], /* @@ -139,16 +139,16 @@ return [ | Configure e.g. default title, creator, subject,... | */ - 'properties' => [ - 'creator' => '', + 'properties' => [ + 'creator' => '', 'lastModifiedBy' => '', - 'title' => '', - 'description' => '', - 'subject' => '', - 'keywords' => '', - 'category' => '', - 'manager' => '', - 'company' => '', + 'title' => '', + 'description' => '', + 'subject' => '', + 'keywords' => '', + 'category' => '', + 'manager' => '', + 'company' => '', ], /* @@ -159,10 +159,10 @@ return [ | Configure middleware that is executed on getting a cell value | */ - 'cells' => [ + 'cells' => [ 'middleware' => [ - //\Maatwebsite\Excel\Middleware\TrimCellValue::class, - //\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class, + // \Maatwebsite\Excel\Middleware\TrimCellValue::class, + // \Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class, ], ], @@ -178,21 +178,21 @@ return [ | */ 'extension_detector' => [ - 'xlsx' => Excel::XLSX, - 'xlsm' => Excel::XLSX, - 'xltx' => Excel::XLSX, - 'xltm' => Excel::XLSX, - 'xls' => Excel::XLS, - 'xlt' => Excel::XLS, - 'ods' => Excel::ODS, - 'ots' => Excel::ODS, - 'slk' => Excel::SLK, - 'xml' => Excel::XML, + 'xlsx' => Excel::XLSX, + 'xlsm' => Excel::XLSX, + 'xltx' => Excel::XLSX, + 'xltm' => Excel::XLSX, + 'xls' => Excel::XLS, + 'xlt' => Excel::XLS, + 'ods' => Excel::ODS, + 'ots' => Excel::ODS, + 'slk' => Excel::SLK, + 'xml' => Excel::XML, 'gnumeric' => Excel::GNUMERIC, - 'htm' => Excel::HTML, - 'html' => Excel::HTML, - 'csv' => Excel::CSV, - 'tsv' => Excel::TSV, + 'htm' => Excel::HTML, + 'html' => Excel::HTML, + 'csv' => Excel::CSV, + 'tsv' => Excel::TSV, /* |-------------------------------------------------------------------------- @@ -203,7 +203,7 @@ return [ | Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF | */ - 'pdf' => Excel::DOMPDF, + 'pdf' => Excel::DOMPDF, ], /* @@ -223,11 +223,11 @@ return [ | [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class | */ - 'value_binder' => [ + 'value_binder' => [ 'default' => Maatwebsite\Excel\DefaultValueBinder::class, ], - 'cache' => [ + 'cache' => [ /* |-------------------------------------------------------------------------- | Default cell caching driver @@ -244,7 +244,7 @@ return [ | Drivers: memory|illuminate|batch | */ - 'driver' => 'memory', + 'driver' => 'memory', /* |-------------------------------------------------------------------------- @@ -256,7 +256,7 @@ return [ | Here you can tweak the memory limit to your liking. | */ - 'batch' => [ + 'batch' => [ 'memory_limit' => 60000, ], @@ -272,7 +272,7 @@ return [ | at "null" it will use the default store. | */ - 'illuminate' => [ + 'illuminate' => [ 'store' => null, ], @@ -308,7 +308,7 @@ return [ */ 'transactions' => [ 'handler' => 'db', - 'db' => [ + 'db' => [ 'connection' => null, ], ], @@ -326,7 +326,7 @@ return [ | and the create file (file). | */ - 'local_path' => storage_path('framework/cache/laravel-excel'), + 'local_path' => storage_path('framework/cache/laravel-excel'), /* |-------------------------------------------------------------------------- @@ -338,7 +338,7 @@ return [ | If omitted the default permissions of the filesystem will be used. | */ - 'local_permissions' => [ + 'local_permissions' => [ // 'dir' => 0755, // 'file' => 0644, ], @@ -357,8 +357,8 @@ return [ | in conjunction with queued imports and exports. | */ - 'remote_disk' => env('TEMP_UPLOAD_DISK', null), - 'remote_prefix' => 'excel-tmp', + 'remote_disk' => env('TEMP_UPLOAD_DISK', null), + 'remote_prefix' => 'excel-tmp', /* |-------------------------------------------------------------------------- diff --git a/config/investbrain.php b/config/investbrain.php index cb4ad56..0889aa5 100644 --- a/config/investbrain.php +++ b/config/investbrain.php @@ -15,5 +15,5 @@ return [ 'self_hosted' => env('SELF_HOSTED', true), - 'daily_change_time_of_day' => env('DAILY_CHANGE_TIME', '23:00') -]; \ No newline at end of file + 'daily_change_time_of_day' => env('DAILY_CHANGE_TIME', '23:00'), +]; diff --git a/config/mary.php b/config/mary.php index f8e7de7..cd3a390 100644 --- a/config/mary.php +++ b/config/mary.php @@ -13,7 +13,6 @@ return [ * prefix => 'mary-' * * - * */ 'prefix' => '', @@ -40,6 +39,6 @@ return [ 'components' => [ 'spotlight' => [ 'class' => 'App\Support\Spotlight', - ] - ] + ], + ], ]; diff --git a/config/services.php b/config/services.php index d1faef0..3eeb4f7 100644 --- a/config/services.php +++ b/config/services.php @@ -41,7 +41,7 @@ return [ 'redirect' => '/auth/github/callback', 'logo' => 'github-icon', 'color' => '#393939', - 'name' => 'GitHub' + 'name' => 'GitHub', ], 'google' => [ @@ -49,7 +49,7 @@ return [ 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => '/auth/google/callback', 'color' => '#4285F4', - 'name' => 'Google' + 'name' => 'Google', ], 'facebook' => [ @@ -57,7 +57,7 @@ return [ 'client_secret' => env('FACEBOOK_CLIENT_SECRET'), 'redirect' => '/auth/facebook/callback', 'color' => '#0165E1', - 'name' => 'Facebook' + 'name' => 'Facebook', ], 'linkedin-openid' => [ @@ -65,10 +65,10 @@ return [ 'client_secret' => env('LINKEDIN_CLIENT_SECRET'), 'redirect' => '/auth/linkedin-openid/callback', 'color' => '#0a66c2', - 'name' => 'Linkedin' + 'name' => 'Linkedin', ], // 'enabled_login_providers' => env('ENABLED_LOGIN_PROVIDERS', ''), - 'ai_chat_enabled' => env('AI_CHAT_ENABLED', false) + 'ai_chat_enabled' => env('AI_CHAT_ENABLED', false), ]; diff --git a/database/factories/TransactionFactory.php b/database/factories/TransactionFactory.php index 1cf8c49..78c05f1 100644 --- a/database/factories/TransactionFactory.php +++ b/database/factories/TransactionFactory.php @@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; */ class TransactionFactory extends Factory { - protected static ?string $transaction_type; + protected static ?string $transaction_type; /** * Define the model's default state. @@ -24,15 +24,15 @@ class TransactionFactory extends Factory return [ 'symbol' => $this->faker->randomElement(['AAPL', 'GOOG', 'AMZN']), 'transaction_type' => $transaction_type, - 'portfolio_id' => Portfolio::factory()->create()->id, + 'portfolio_id' => Portfolio::factory()->create()->id, 'date' => $this->faker->date('Y-m-d'), 'quantity' => 1, - 'cost_basis' => $transaction_type == 'BUY' + 'cost_basis' => $transaction_type == 'BUY' ? $this->faker->randomFloat(2, 10, 500) - : null, - 'sale_price' => $transaction_type == 'SELL' + : null, + 'sale_price' => $transaction_type == 'SELL' ? $this->faker->randomFloat(2, 10, 500) - : null, + : null, ]; } @@ -83,7 +83,7 @@ class TransactionFactory extends Factory return $this->state(fn (array $attributes) => [ 'transaction_type' => 'BUY', 'cost_basis' => $this->faker->randomFloat(2, 10, 500), - 'sale_price' => null + 'sale_price' => null, ]); } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 76c211a..13a4102 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -31,7 +31,7 @@ class UserFactory extends Factory 'two_factor_secret' => null, 'two_factor_recovery_codes' => null, 'remember_token' => Str::random(10), - 'profile_photo_path' => null + 'profile_photo_path' => null, ]; } diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index f60b77d..2c296af 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -12,7 +12,7 @@ return new class extends Migration public function up(): void { Schema::create('users', function (Blueprint $table) { - $table->uuid('id')->primary(); + $table->uuid('id')->primary(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); diff --git a/database/migrations/2021_01_30_102537_create_portfolios_table.php b/database/migrations/2021_01_30_102537_create_portfolios_table.php index 3ee0a89..186d92f 100644 --- a/database/migrations/2021_01_30_102537_create_portfolios_table.php +++ b/database/migrations/2021_01_30_102537_create_portfolios_table.php @@ -14,7 +14,7 @@ return new class extends Migration public function up() { Schema::create('portfolios', function (Blueprint $table) { - $table->uuid('id')->primary(); + $table->uuid('id')->primary(); $table->string('title'); $table->text('notes')->nullable(); $table->boolean('wishlist')->default(false); @@ -31,4 +31,4 @@ return new class extends Migration { Schema::dropIfExists('portfolios'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2021_01_30_112537_create_portfolio_users_table.php b/database/migrations/2021_01_30_112537_create_portfolio_users_table.php index 6205c04..000dd1d 100644 --- a/database/migrations/2021_01_30_112537_create_portfolio_users_table.php +++ b/database/migrations/2021_01_30_112537_create_portfolio_users_table.php @@ -1,10 +1,10 @@ MarketDataSeeder::class, - '--force' => true + '--force' => true, ]); } diff --git a/database/migrations/2021_02_25_041227_create_daily_change_table.php b/database/migrations/2021_02_25_041227_create_daily_change_table.php index 5e0e413..6259975 100644 --- a/database/migrations/2021_02_25_041227_create_daily_change_table.php +++ b/database/migrations/2021_02_25_041227_create_daily_change_table.php @@ -1,9 +1,9 @@ uuid('id')->primary(); + $table->uuid('id')->primary(); $table->date('date'); $table->foreignIdFor(MarketData::class, 'symbol'); $table->float('dividend_amount', 12, 4); diff --git a/database/migrations/2021_02_25_041246_create_splits_table.php b/database/migrations/2021_02_25_041246_create_splits_table.php index ffcf8b0..774c422 100644 --- a/database/migrations/2021_02_25_041246_create_splits_table.php +++ b/database/migrations/2021_02_25_041246_create_splits_table.php @@ -1,9 +1,9 @@ uuid('id')->primary(); + $table->uuid('id')->primary(); $table->date('date'); $table->foreignIdFor(MarketData::class, 'symbol'); $table->float('split_amount', 12, 4); diff --git a/database/migrations/2021_02_25_041257_create_transactions_table.php b/database/migrations/2021_02_25_041257_create_transactions_table.php index f5a7497..b7640eb 100644 --- a/database/migrations/2021_02_25_041257_create_transactions_table.php +++ b/database/migrations/2021_02_25_041257_create_transactions_table.php @@ -1,10 +1,10 @@ uuid('id')->primary(); + $table->uuid('id')->primary(); $table->foreignIdFor(MarketData::class, 'symbol'); $table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade'); $table->string('transaction_type', 15); diff --git a/database/migrations/2021_09_06_014744_create_holdings_table.php b/database/migrations/2021_09_06_014744_create_holdings_table.php index e43f690..450624e 100644 --- a/database/migrations/2021_09_06_014744_create_holdings_table.php +++ b/database/migrations/2021_09_06_014744_create_holdings_table.php @@ -1,10 +1,10 @@ uuid('id')->primary(); + $table->uuid('id')->primary(); $table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade'); $table->foreignIdFor(MarketData::class, 'symbol'); $table->float('quantity', 12, 4); diff --git a/database/migrations/2024_10_19_155635_create_connected_accounts_table.php b/database/migrations/2024_10_19_155635_create_connected_accounts_table.php index 6e7269b..bd9378c 100644 --- a/database/migrations/2024_10_19_155635_create_connected_accounts_table.php +++ b/database/migrations/2024_10_19_155635_create_connected_accounts_table.php @@ -1,9 +1,9 @@ uuid('id')->primary(); + $table->uuid('id')->primary(); $table->foreignIdFor(User::class, 'user_id')->constrained()->onDelete('cascade'); $table->morphs('chatable'); $table->string('role'); diff --git a/database/seeders/MarketDataSeeder.php b/database/seeders/MarketDataSeeder.php index 90d81b9..8956fca 100644 --- a/database/seeders/MarketDataSeeder.php +++ b/database/seeders/MarketDataSeeder.php @@ -2,9 +2,9 @@ namespace Database\Seeders; +use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; class MarketDataSeeder extends Seeder { @@ -14,7 +14,7 @@ class MarketDataSeeder extends Seeder * Run the database seeds. */ public function run(): void - { + { $chunkSize = 500; // Path to the CSV file @@ -29,7 +29,7 @@ class MarketDataSeeder extends Seeder while (($row = fgetcsv($handle, 0, ',')) !== false) { - if (!$header) { + if (! $header) { // header must be the first row $header = $row; @@ -40,31 +40,31 @@ class MarketDataSeeder extends Seeder $data = array_combine($header, $row); $rows[] = [ - 'symbol' => $data['symbol'], - 'name' => $data['name'], + 'symbol' => $data['symbol'], + 'name' => $data['name'], 'meta_data' => json_encode([ 'country' => $data['country'], 'first_trade_year' => $data['first_trade_year'], 'sector' => $data['sector'], 'industry' => $data['industry'], - ]), + ]), ]; $rowCount++; if ($rowCount % $chunkSize == 0) { DB::table('market_data')->insertOrIgnore($rows); - $rows = []; + $rows = []; } } catch (\Throwable $e) { - - throw new \Exception('Error: '. $e->getMessage()); + + throw new \Exception('Error: '.$e->getMessage()); } } } // final clean up - if (!empty($rows)) { + if (! empty($rows)) { DB::table('market_data')->insertOrIgnore($rows); } diff --git a/routes/api.php b/routes/api.php index 32395b5..a00de46 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,11 +1,11 @@ name('api.')->group(function () { @@ -25,4 +25,4 @@ Route::middleware(['auth:sanctum'])->name('api.')->group(function () { // market data Route::get('/market-data/{symbol}', [MarketDataController::class, 'show'])->name('market-data.show'); -}); \ No newline at end of file +}); diff --git a/routes/console.php b/routes/console.php index 2382db8..b002734 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,35 +1,34 @@ weekdays()->everyMinute(); /** - * * This scheduled job records daily changes to your portfolios every weekday */ Schedule::command(CaptureDailyChange::class)->dailyAt(config('investbrain.daily_change_time_of_day'))->weekdays(); /** - * * Refreshes dividend data for your holdings (and syncs new dividends to holdings) */ Schedule::command(RefreshDividendData::class)->daily()->days([1, 3, 5]); /** - * * Refreshes split data for your holdings (and creates new transactions for new splits) */ Schedule::command(RefreshSplitData::class)->weekly(); /** - * * Periodically reconciles your holdings with transactions and dividends */ Schedule::command(SyncHoldingData::class)->yearly(); diff --git a/routes/web.php b/routes/web.php index 0c60cd7..492a4b3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,19 +1,19 @@ actingAs($this->user); Transaction::factory(10)->create(); - + $this->actingAs($this->user) ->getJson(route('api.holding.index', ['page' => 1, 'itemsPerPage' => 5])) ->assertOk() ->assertJsonStructure([ 'data' => [['id', 'symbol', 'portfolio_id', 'total_market_value', 'dividends_earned']], 'meta' => ['current_page', 'last_page', 'total'], - 'links' => ['first', 'last', 'prev', 'next'] + 'links' => ['first', 'last', 'prev', 'next'], ]); } @@ -45,7 +46,7 @@ class HoldingsTest extends TestCase // create transactions with existing user $this->actingAs($this->user); Transaction::factory(10)->create(); - + // Create a new user $this->actingAs($user = User::factory()->create()); Transaction::factory(1)->create(); @@ -88,14 +89,14 @@ class HoldingsTest extends TestCase $transaction = Transaction::factory()->create(); $data = [ - 'reinvest_dividends' => true + 'reinvest_dividends' => true, ]; $this->actingAs($this->user) ->putJson(route('api.holding.update', ['portfolio' => $transaction->portfolio_id, 'symbol' => $transaction->symbol]), $data) ->assertOk() ->assertJsonFragment([ - 'reinvest_dividends' => true + 'reinvest_dividends' => true, ]); } @@ -105,7 +106,7 @@ class HoldingsTest extends TestCase $transaction = Transaction::factory()->create(); $data = [ - 'reinvest_dividends' => true + 'reinvest_dividends' => true, ]; $otherUser = User::factory()->create(); @@ -113,5 +114,4 @@ class HoldingsTest extends TestCase ->putJson(route('api.holding.update', ['portfolio' => $transaction->portfolio_id, 'symbol' => $transaction->symbol]), $data) ->assertForbidden(); } - -} \ No newline at end of file +} diff --git a/tests/Api/PortfoliosTest.php b/tests/Api/PortfoliosTest.php index 44dfd5b..1339a2f 100644 --- a/tests/Api/PortfoliosTest.php +++ b/tests/Api/PortfoliosTest.php @@ -2,16 +2,17 @@ namespace Tests\Api; -use Tests\TestCase; -use App\Models\User; use App\Models\Portfolio; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; class PortfoliosTest extends TestCase { use RefreshDatabase; protected User $user; + protected Portfolio $portfolio; protected function setUp(): void @@ -27,14 +28,14 @@ class PortfoliosTest extends TestCase $this->actingAs($this->user); Portfolio::factory(10)->create(); - + $this->actingAs($this->user) ->getJson(route('api.portfolio.index', ['page' => 1, 'itemsPerPage' => 5])) ->assertOk() ->assertJsonStructure([ 'data' => [['id', 'title', 'owner', 'holdings', 'transactions']], 'meta' => ['current_page', 'last_page', 'total'], - 'links' => ['first', 'last', 'prev', 'next'] + 'links' => ['first', 'last', 'prev', 'next'], ]); } @@ -43,7 +44,7 @@ class PortfoliosTest extends TestCase // create portfolios with existing user $this->actingAs($this->user); Portfolio::factory(10)->create(); - + // Create a new user $this->actingAs($user = User::factory()->create()); Portfolio::factory(1)->create(); @@ -61,12 +62,12 @@ class PortfoliosTest extends TestCase public function test_can_create_a_portfolio() { $data = Portfolio::factory()->make()->toArray(); - + $this->actingAs($this->user) ->postJson(route('api.portfolio.store'), $data) ->assertCreated() ->assertJsonStructure(['id', 'title', 'owner']); - + $this->assertDatabaseHas('portfolios', ['title' => $data['title']]); } @@ -102,12 +103,12 @@ class PortfoliosTest extends TestCase $this->actingAs($this->user); $portfolio = Portfolio::factory()->create(); - + $this->actingAs($this->user) ->putJson(route('api.portfolio.update', $portfolio), $updatedData) ->assertOk() ->assertJson($updatedData); - + $this->assertDatabaseHas('portfolios', $updatedData); } @@ -126,7 +127,7 @@ class PortfoliosTest extends TestCase ->putJson(route('api.portfolio.update', $portfolio), ['title' => 'A brand new updated title']) ->assertOk() ->assertJsonFragment([ - 'title' => 'A brand new updated title' + 'title' => 'A brand new updated title', ]); } @@ -185,7 +186,7 @@ class PortfoliosTest extends TestCase $this->actingAs($this->user) ->deleteJson(route('api.portfolio.destroy', $portfolio)) ->assertNoContent(); - + $this->assertDatabaseMissing('portfolios', ['id' => $portfolio->id]); } @@ -199,4 +200,4 @@ class PortfoliosTest extends TestCase ->deleteJson(route('api.portfolio.destroy', $portfolio)) ->assertForbidden(); } -} \ No newline at end of file +} diff --git a/tests/Api/TransactionsTest.php b/tests/Api/TransactionsTest.php index 321e101..19435e3 100644 --- a/tests/Api/TransactionsTest.php +++ b/tests/Api/TransactionsTest.php @@ -2,17 +2,18 @@ namespace Tests\Api; -use Tests\TestCase; -use App\Models\User; use App\Models\Portfolio; use App\Models\Transaction; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; class TransactionsTest extends TestCase { use RefreshDatabase; protected User $user; + protected Portfolio $portfolio; protected function setUp(): void @@ -21,7 +22,7 @@ class TransactionsTest extends TestCase // make user $this->user = User::factory()->create(); - + // make portfolio $this->portfolio = Portfolio::factory()->makeOne(); $this->portfolio->setOwnerIdAttribute($this->user->id); @@ -33,14 +34,14 @@ class TransactionsTest extends TestCase $this->actingAs($this->user); Transaction::factory(10)->create(); - + $this->actingAs($this->user) ->getJson(route('api.transaction.index', ['page' => 1, 'itemsPerPage' => 5])) ->assertOk() ->assertJsonStructure([ 'data' => [['id', 'symbol', 'transaction_type', 'portfolio_id', 'date']], 'meta' => ['current_page', 'last_page', 'total'], - 'links' => ['first', 'last', 'prev', 'next'] + 'links' => ['first', 'last', 'prev', 'next'], ]); } @@ -49,7 +50,7 @@ class TransactionsTest extends TestCase // create transactions with existing user $this->actingAs($this->user); Transaction::factory(10)->create(); - + // Create a new user $this->actingAs($user = User::factory()->create()); Transaction::factory(1)->create(); @@ -88,7 +89,7 @@ class TransactionsTest extends TestCase 'quantity', 'date', 'cost_basis', - 'sale_price' + 'sale_price', ]); } @@ -97,7 +98,7 @@ class TransactionsTest extends TestCase $this->actingAs($this->user) ->postJson(route('api.transaction.store'), [ 'portfolio_id' => $this->portfolio->id, - 'symbol' => null + 'symbol' => null, ]) ->assertUnprocessable() ->assertJsonValidationErrors(['symbol']); @@ -133,7 +134,7 @@ class TransactionsTest extends TestCase 'symbol' => 'ZZZ', 'transaction_type' => 'BUY', 'cost_basis' => 200.19, - 'quantity' => 5 + 'quantity' => 5, ]; $this->actingAs($this->user) @@ -162,7 +163,7 @@ class TransactionsTest extends TestCase ->putJson(route('api.transaction.update', $transaction), ['symbol' => 'ZZZ']) ->assertOk() ->assertJsonFragment([ - 'symbol' => 'ZZZ' + 'symbol' => 'ZZZ', ]); } @@ -198,4 +199,4 @@ class TransactionsTest extends TestCase ->deleteJson(route('api.transaction.destroy', $transaction)) ->assertForbidden(); } -} \ No newline at end of file +} diff --git a/tests/ApiTokenPermissionsTest.php b/tests/ApiTokenPermissionsTest.php index 9afd10d..e268ef5 100644 --- a/tests/ApiTokenPermissionsTest.php +++ b/tests/ApiTokenPermissionsTest.php @@ -8,7 +8,6 @@ use Illuminate\Support\Str; use Laravel\Jetstream\Features; use Laravel\Jetstream\Http\Livewire\ApiTokenManager; use Livewire\Livewire; -use Tests\TestCase; class ApiTokenPermissionsTest extends TestCase { diff --git a/tests/AuthenticationTest.php b/tests/AuthenticationTest.php index 97f35d1..b54c9c7 100644 --- a/tests/AuthenticationTest.php +++ b/tests/AuthenticationTest.php @@ -4,7 +4,6 @@ namespace Tests; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; -use Tests\TestCase; class AuthenticationTest extends TestCase { diff --git a/tests/BrowserSessionsTest.php b/tests/BrowserSessionsTest.php index 2b540a4..af975f6 100644 --- a/tests/BrowserSessionsTest.php +++ b/tests/BrowserSessionsTest.php @@ -6,7 +6,6 @@ use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm; use Livewire\Livewire; -use Tests\TestCase; class BrowserSessionsTest extends TestCase { diff --git a/tests/CaptureDailyChangeTest.php b/tests/CaptureDailyChangeTest.php index 41d6596..1b9db54 100644 --- a/tests/CaptureDailyChangeTest.php +++ b/tests/CaptureDailyChangeTest.php @@ -2,19 +2,18 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; -use App\Models\Portfolio; use App\Models\DailyChange; +use App\Models\Portfolio; use App\Models\Transaction; -use Illuminate\Support\Facades\Artisan; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Artisan; class CaptureDailyChangeTest extends TestCase { use RefreshDatabase; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -25,8 +24,6 @@ class CaptureDailyChangeTest extends TestCase $this->transaction = Transaction::factory()->sell()->lastMonth()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); } - /** - */ public function test_daily_change_for_portfolios() { // Run the command @@ -47,10 +44,10 @@ class CaptureDailyChangeTest extends TestCase $this->assertCount(1, $daily_change); $this->assertEqualsWithDelta( - $this->transaction->sale_price - $this->transaction->cost_basis, + $this->transaction->sale_price - $this->transaction->cost_basis, $daily_change->first()->realized_gains, 0.01 ); - + } } diff --git a/tests/ConnectedAccountTest.php b/tests/ConnectedAccountTest.php index f97f600..af5cbde 100644 --- a/tests/ConnectedAccountTest.php +++ b/tests/ConnectedAccountTest.php @@ -2,13 +2,12 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; +use App\Http\Controllers\ConnectedAccountController; use App\Models\ConnectedAccount; +use App\Models\User; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Auth; use Laravel\Socialite\Facades\Socialite; -use Illuminate\Foundation\Testing\RefreshDatabase; -use App\Http\Controllers\ConnectedAccountController; class ConnectedAccountTest extends TestCase { @@ -19,7 +18,7 @@ class ConnectedAccountTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->controller = new ConnectedAccountController(); + $this->controller = new ConnectedAccountController; } public function test_handle_provider_callback_with_already_connected_account() @@ -33,7 +32,7 @@ class ConnectedAccountTest extends TestCase 'email' => 'alice@example.com', 'email_verified_at' => now(), ]); - $providerUser = (object)[ + $providerUser = (object) [ 'id' => '67890', 'name' => 'Alice Smith', 'email' => 'alice@example.com', @@ -47,9 +46,9 @@ class ConnectedAccountTest extends TestCase 'provider_id' => $providerUser->id, 'user_id' => $user->id, 'token' => $providerUser->token, - 'verified_at' => now() + 'verified_at' => now(), ]); - + Socialite::shouldReceive('driver') ->with($provider) ->andReturnSelf() @@ -68,7 +67,7 @@ class ConnectedAccountTest extends TestCase { $provider = 'github'; config(['services.enabled_login_providers' => 'github,google']); - $providerUser = (object)[ + $providerUser = (object) [ 'id' => '12345', 'name' => 'John Doe', 'email' => 'john@example.com', @@ -109,7 +108,7 @@ class ConnectedAccountTest extends TestCase 'email' => 'jane@example.com', 'email_verified_at' => now(), ]); - $providerUser = (object)[ + $providerUser = (object) [ 'id' => '54321', 'name' => 'Jane Doe', 'email' => 'jane@example.com', @@ -164,5 +163,4 @@ class ConnectedAccountTest extends TestCase $response->assertRedirect(route('dashboard')); $response->assertSessionHas('toast'); } - } diff --git a/tests/DashboardTest.php b/tests/DashboardTest.php index 8a73069..c098ec0 100644 --- a/tests/DashboardTest.php +++ b/tests/DashboardTest.php @@ -2,19 +2,16 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; use App\Models\Holding; use App\Models\Portfolio; use App\Models\Transaction; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; class DashboardTest extends TestCase { use RefreshDatabase; - /** - */ public function test_user_has_portfolios(): void { $this->actingAs($user = User::factory()->create()); @@ -24,8 +21,6 @@ class DashboardTest extends TestCase $this->assertCount(5, $user->portfolios); } - /** - */ public function test_user_has_transactions(): void { $this->actingAs($user = User::factory()->create()); @@ -35,8 +30,6 @@ class DashboardTest extends TestCase $this->assertCount(10, $user->transactions); } - /** - */ public function test_user_has_holdings(): void { $this->actingAs($user = User::factory()->create()); @@ -48,22 +41,20 @@ class DashboardTest extends TestCase $this->assertCount(1, $user->holdings); } - /** - */ public function test_user_has_dashboard_metrics(): void { $this->actingAs($user = User::factory()->create()); $portfolio = Portfolio::factory()->create(); - + Transaction::factory(5)->buy()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create(); $transaction = Transaction::factory()->sell()->lastMonth()->portfolio($portfolio->id)->symbol('AAPL')->create(); $metrics = Holding::query() - ->myHoldings() - ->withPortfolioMetrics() - ->first(); - + ->myHoldings() + ->withPortfolioMetrics() + ->first(); + $this->assertEqualsWithDelta( $transaction->sale_price - $transaction->cost_basis, $metrics->realized_gain_dollars, diff --git a/tests/DeleteAccountTest.php b/tests/DeleteAccountTest.php index 0551820..a75aac8 100644 --- a/tests/DeleteAccountTest.php +++ b/tests/DeleteAccountTest.php @@ -7,7 +7,6 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Jetstream\Features; use Laravel\Jetstream\Http\Livewire\DeleteUserForm; use Livewire\Livewire; -use Tests\TestCase; class DeleteAccountTest extends TestCase { diff --git a/tests/DividendsTest.php b/tests/DividendsTest.php index 7478869..e116f5a 100644 --- a/tests/DividendsTest.php +++ b/tests/DividendsTest.php @@ -2,31 +2,27 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; -use App\Models\Split; -use App\Models\Holding; use App\Models\Dividend; -use App\Models\Portfolio; +use App\Models\Holding; use App\Models\MarketData; +use App\Models\Portfolio; use App\Models\Transaction; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; class DividendsTest extends TestCase { use RefreshDatabase; - /** - */ public function test_new_dividends_update_holding(): void { $this->actingAs($user = User::factory()->create()); - + $portfolio = Portfolio::factory()->create(); Transaction::factory()->buy()->yearsAgo()->portfolio($portfolio->id)->symbol('ACME')->create(); $holding = Holding::query()->portfolio($portfolio->id)->symbol('ACME')->first(); - + $this->assertEquals(0, $holding->dividends_earned); Dividend::refreshDividendData('ACME'); @@ -36,19 +32,17 @@ class DividendsTest extends TestCase $this->assertEquals(4.95, $holding->dividends_earned); } - /** - */ public function test_new_dividends_are_reinvested(): void { $this->actingAs($user = User::factory()->create()); - + $portfolio = Portfolio::factory()->create(); Transaction::factory()->buy()->yearsAgo()->portfolio($portfolio->id)->symbol('ACME')->create(); $holding = Holding::query()->portfolio($portfolio->id)->symbol('ACME')->first(); $holding->reinvest_dividends = true; $holding->save(); - + $this->assertEquals(0, $holding->dividends_earned); Dividend::refreshDividendData('ACME'); @@ -56,18 +50,16 @@ class DividendsTest extends TestCase $transactions = Transaction::where(['reinvested_dividend' => true])->symbol('ACME')->portfolio($portfolio->id)->get(); $market_data = MarketData::symbol('ACME')->first(); - $dividendsReinvested = $transactions->sum('quantity'); + $dividendsReinvested = $transactions->sum('quantity'); $this->assertCount(3, $transactions); $this->assertEqualsWithDelta(4.95, $dividendsReinvested * $market_data->market_value, 0.01); } - /** - */ public function test_do_not_duplicate_recent_dividends(): void { $this->actingAs($user = User::factory()->create()); - + $portfolio = Portfolio::factory()->create(); Transaction::factory()->buy()->yearsAgo()->portfolio($portfolio->id)->symbol('ACME')->create(); @@ -76,9 +68,9 @@ class DividendsTest extends TestCase Dividend::create([ 'symbol' => 'ACME', 'date' => now()->subDay(2), - 'dividend_amount' => .01 + 'dividend_amount' => .01, ]); - + Dividend::refreshDividendData('ACME'); $this->assertCount(1, $holding->dividends); diff --git a/tests/EmailVerificationTest.php b/tests/EmailVerificationTest.php index 7d8ab37..4baa2cf 100644 --- a/tests/EmailVerificationTest.php +++ b/tests/EmailVerificationTest.php @@ -8,7 +8,6 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\URL; use Laravel\Fortify\Features; -use Tests\TestCase; class EmailVerificationTest extends TestCase { diff --git a/tests/FallbackInterfaceTest.php b/tests/FallbackInterfaceTest.php index f2a6af2..7354801 100644 --- a/tests/FallbackInterfaceTest.php +++ b/tests/FallbackInterfaceTest.php @@ -1,25 +1,24 @@ -set('investbrain.provider', 'yahoo,alphavantage'); config()->set('investbrain.interfaces', [ @@ -29,16 +28,16 @@ class FallbackInterfaceTest extends TestCase $yahooMock = Mockery::mock(YahooMarketData::class); $yahooMock->shouldReceive('quote') - ->andThrow(new \Exception("Yahoo failed")); + ->andThrow(new \Exception('Yahoo failed')); $alphaMock = Mockery::mock(AlphaVantageMarketData::class); $alphaMock->shouldReceive('quote') - ->andReturn(new Quote(['market_value' => 10])); + ->andReturn(new Quote(['market_value' => 10])); $this->app->instance(YahooMarketData::class, $yahooMock); $this->app->instance(AlphaVantageMarketData::class, $alphaMock); - $fallbackInterface = new FallbackInterface(); + $fallbackInterface = new FallbackInterface; $result = $fallbackInterface->quote('ACME'); @@ -47,7 +46,7 @@ class FallbackInterfaceTest extends TestCase Log::shouldHaveReceived('warning')->with('Failed calling method quote (yahoo): Yahoo failed'); } - public function testAllProvidersFail() + public function test_all_providers_fail() { config()->set('investbrain.provider', 'yahoo,alpha'); config()->set('investbrain.interfaces', [ @@ -57,16 +56,16 @@ class FallbackInterfaceTest extends TestCase $yahooMock = Mockery::mock(YahooMarketData::class); $yahooMock->shouldReceive('quote') - ->andThrow(new \Exception("Yahoo failed")); + ->andThrow(new \Exception('Yahoo failed')); $alphaMock = Mockery::mock(AlphaVantageMarketData::class); $alphaMock->shouldReceive('quote') - ->andThrow(new \Exception("Alpha failed")); + ->andThrow(new \Exception('Alpha failed')); $this->app->instance(YahooMarketData::class, $yahooMock); $this->app->instance(AlphaVantageMarketData::class, $alphaMock); - $fallbackInterface = new FallbackInterface(); + $fallbackInterface = new FallbackInterface; $this->expectException(\Exception::class); $this->expectExceptionMessage('Could not get market data: Provider [alpha] is not a valid market data interface.'); @@ -76,4 +75,4 @@ class FallbackInterfaceTest extends TestCase Log::shouldHaveReceived('warning')->with('Failed calling method quote (yahoo): Yahoo failed'); Log::shouldHaveReceived('warning')->with('Failed calling method quote (alpha): Alpha failed'); } -} \ No newline at end of file +} diff --git a/tests/ImportExportTest.php b/tests/ImportExportTest.php index 74882a6..9e4446a 100644 --- a/tests/ImportExportTest.php +++ b/tests/ImportExportTest.php @@ -2,20 +2,17 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; -use App\Models\Transaction; -use Maatwebsite\Excel\Facades\Excel; use App\Exports\BackupExport; use App\Models\BackupImport as BackupImportModel; +use App\Models\Transaction; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Maatwebsite\Excel\Facades\Excel; class ImportExportTest extends TestCase { use RefreshDatabase; - /** - */ public function test_can_create_exports(): void { Excel::fake(); @@ -24,22 +21,20 @@ class ImportExportTest extends TestCase Transaction::factory(5)->buy()->lastYear()->symbol('AAPL')->create(); - Excel::download(new BackupExport, now()->format('Y_m_d') . '_investbrain_backup.xlsx'); + Excel::download(new BackupExport, now()->format('Y_m_d').'_investbrain_backup.xlsx'); - Excel::assertDownloaded(now()->format('Y_m_d') . '_investbrain_backup.xlsx', function(BackupExport $export) { + Excel::assertDownloaded(now()->format('Y_m_d').'_investbrain_backup.xlsx', function (BackupExport $export) { return true; }); } - /** - */ public function test_backup_job_completes(): void { $this->actingAs($user = User::factory()->create()); $backup_job = BackupImportModel::create([ 'user_id' => auth()->user()->id, - 'path' => __DIR__.'/0000_00_00_import_test.xlsx' + 'path' => __DIR__.'/0000_00_00_import_test.xlsx', ]); $backup_job->refresh(); @@ -47,29 +42,25 @@ class ImportExportTest extends TestCase $this->assertEquals('success', $backup_job->status); } - /** - */ public function test_backup_job_inserts_rows(): void { $this->actingAs($user = User::factory()->create()); BackupImportModel::create([ 'user_id' => auth()->user()->id, - 'path' => __DIR__.'/0000_00_00_import_test.xlsx' + 'path' => __DIR__.'/0000_00_00_import_test.xlsx', ]); $this->assertEquals(3, $user->transactions->count()); } - /** - */ public function test_backup_job_calculates_correct_holding_data(): void { $this->actingAs($user = User::factory()->create()); BackupImportModel::create([ 'user_id' => auth()->user()->id, - 'path' => __DIR__.'/0000_00_00_import_test.xlsx' + 'path' => __DIR__.'/0000_00_00_import_test.xlsx', ]); $holding = $user->holdings->first(); diff --git a/tests/PasswordConfirmationTest.php b/tests/PasswordConfirmationTest.php index 8ba8ad7..f3cb707 100644 --- a/tests/PasswordConfirmationTest.php +++ b/tests/PasswordConfirmationTest.php @@ -4,7 +4,6 @@ namespace Tests; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; -use Tests\TestCase; class PasswordConfirmationTest extends TestCase { diff --git a/tests/PasswordResetTest.php b/tests/PasswordResetTest.php index 1335434..d2d059e 100644 --- a/tests/PasswordResetTest.php +++ b/tests/PasswordResetTest.php @@ -7,7 +7,6 @@ use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Notification; use Laravel\Fortify\Features; -use Tests\TestCase; class PasswordResetTest extends TestCase { diff --git a/tests/PortfolioPolicyTest.php b/tests/PortfolioPolicyTest.php index 4b040e5..00593d6 100644 --- a/tests/PortfolioPolicyTest.php +++ b/tests/PortfolioPolicyTest.php @@ -2,27 +2,29 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; use App\Models\Portfolio; +use App\Models\User; use App\Policies\PortfolioPolicy; -use Illuminate\Support\Facades\Auth; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Auth; class PortfolioPolicyTest extends TestCase { use RefreshDatabase; protected $policy; + protected $owner; + protected $user; + protected $portfolio; protected function setUp(): void { parent::setUp(); - $this->policy = new PortfolioPolicy(); + $this->policy = new PortfolioPolicy; $this->owner = User::factory()->create(); Auth::login($this->owner); @@ -34,7 +36,7 @@ class PortfolioPolicyTest extends TestCase $this->user->id => [ 'full_access' => false, 'owner' => false, - ] + ], ]); } @@ -109,5 +111,4 @@ class PortfolioPolicyTest extends TestCase $result = $this->policy->owner($this->user, $this->portfolio); $this->assertFalse($result, 'User should not be the owner'); } - } diff --git a/tests/PortfoliosTest.php b/tests/PortfoliosTest.php index 607bc4a..b9de91e 100644 --- a/tests/PortfoliosTest.php +++ b/tests/PortfoliosTest.php @@ -2,17 +2,14 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; use App\Models\Portfolio; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; class PortfoliosTest extends TestCase { use RefreshDatabase; - /** - */ public function test_owner_is_assigned_to_portfolio_on_create(): void { $this->actingAs($user = User::factory()->create()); @@ -22,8 +19,6 @@ class PortfoliosTest extends TestCase $this->assertEquals($user->id, $portfolio->owner_id); } - /** - */ public function test_owner_can_be_forced_on_create(): void { $this->actingAs($user = User::factory()->create()); @@ -35,8 +30,6 @@ class PortfoliosTest extends TestCase $this->assertEquals($user->id, $portfolio->owner_id); } - /** - */ public function test_owner_cannot_be_changed_on_update(): void { $this->actingAs($owner = User::factory()->create()); @@ -49,6 +42,4 @@ class PortfoliosTest extends TestCase $this->assertEquals($owner->id, $portfolio->owner_id); } - - } diff --git a/tests/ProfileInformationTest.php b/tests/ProfileInformationTest.php index bf957b2..858983c 100644 --- a/tests/ProfileInformationTest.php +++ b/tests/ProfileInformationTest.php @@ -6,7 +6,6 @@ use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Jetstream\Http\Livewire\UpdateProfileInformationForm; use Livewire\Livewire; -use Tests\TestCase; class ProfileInformationTest extends TestCase { diff --git a/tests/RegistrationTest.php b/tests/RegistrationTest.php index 9cb0487..4cf1154 100644 --- a/tests/RegistrationTest.php +++ b/tests/RegistrationTest.php @@ -5,7 +5,6 @@ namespace Tests; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Fortify\Features; use Laravel\Jetstream\Jetstream; -use Tests\TestCase; class RegistrationTest extends TestCase { diff --git a/tests/SplitsTest.php b/tests/SplitsTest.php index ba1b5eb..e32b100 100644 --- a/tests/SplitsTest.php +++ b/tests/SplitsTest.php @@ -2,32 +2,29 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; -use App\Models\Split; use App\Models\Holding; use App\Models\Portfolio; +use App\Models\Split; use App\Models\Transaction; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; class SplitsTest extends TestCase { use RefreshDatabase; - /** - */ public function test_splits_create_new_transaction(): void { $this->actingAs($user = User::factory()->create()); - + $portfolio = Portfolio::factory()->create(); Transaction::factory()->buy()->yearsAgo()->portfolio($portfolio->id)->symbol('ACME')->create(); // manually reset the split last sync date (which is set when the holding is created) Holding::query()->portfolio($portfolio->id)->symbol('ACME')->update([ - 'splits_synced_at' => null + 'splits_synced_at' => null, ]); - + Split::refreshSplitData('ACME'); $transactions = Transaction::query()->symbol('ACME')->portfolio($portfolio->id)->get(); @@ -35,12 +32,10 @@ class SplitsTest extends TestCase $this->assertCount(2, $transactions); } - /** - */ public function test_splits_do_not_create_new_transaction_if_already_synced(): void { $this->actingAs($user = User::factory()->create()); - + $portfolio = Portfolio::factory()->create(); Transaction::factory()->buy()->yearsAgo()->portfolio($portfolio->id)->symbol('ACME')->create(); diff --git a/tests/SyncDailyChangeTest.php b/tests/SyncDailyChangeTest.php index a816b2d..759c852 100644 --- a/tests/SyncDailyChangeTest.php +++ b/tests/SyncDailyChangeTest.php @@ -2,23 +2,20 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; -use App\Models\Holding; -use Carbon\CarbonPeriod; -use App\Models\Portfolio; use App\Models\DailyChange; +use App\Models\Holding; +use App\Models\Portfolio; use App\Models\Transaction; +use App\Models\User; +use Carbon\CarbonPeriod; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Artisan; -use Illuminate\Foundation\Testing\RefreshDatabase; class SyncDailyChangeTest extends TestCase { use RefreshDatabase; - /** - */ public function test_can_sync_daily_change_history(): void { $this->actingAs($user = User::factory()->create()); @@ -36,16 +33,14 @@ class SyncDailyChangeTest extends TestCase $count_of_daily_changes = $portfolio->daily_change()->count('date'); $days_between_now_and_first_trans = (int) CarbonPeriod::create( - $portfolio->transactions()->min('date'), + $portfolio->transactions()->min('date'), now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day'))) ? now()->subDay() : now() )->filter('isWeekday') - ->count(); + ->count(); $this->assertEquals($count_of_daily_changes, $days_between_now_and_first_trans); } - /** - */ public function test_cost_basis_is_synced(): void { $this->actingAs($user = User::factory()->create()); @@ -56,33 +51,31 @@ class SyncDailyChangeTest extends TestCase Artisan::call('sync:daily-change', ['portfolio_id' => $portfolio->id]); $holding = Holding::symbol('ACME')->portfolio($portfolio->id)->first(); $daily_change = DailyChange::whereDate('date', '<=', $first_transaction->date->addDays(2)) - ->whereDate('date', '>=', $first_transaction->date->subDays(2)) - ->orderByDesc('date') - ->first(); + ->whereDate('date', '>=', $first_transaction->date->subDays(2)) + ->orderByDesc('date') + ->first(); $this->assertEquals($holding->average_cost_basis, $daily_change->total_cost_basis); $second_transaction = Transaction::factory()->buy()->lastYear()->portfolio($portfolio->id)->symbol('ACME')->create(); Artisan::call('sync:daily-change', ['portfolio_id' => $portfolio->id]); $daily_change = DailyChange::whereDate('date', '<=', $second_transaction->date->addDays(2)) - ->whereDate('date', '>=', $second_transaction->date->subDays(2)) - ->orderByDesc('date') - ->first(); - - $this->assertEqualsWithDelta($first_transaction->cost_basis + $second_transaction->cost_basis, $daily_change->total_cost_basis, 0.01); + ->whereDate('date', '>=', $second_transaction->date->subDays(2)) + ->orderByDesc('date') + ->first(); + + $this->assertEqualsWithDelta($first_transaction->cost_basis + $second_transaction->cost_basis, $daily_change->total_cost_basis, 0.01); $third_transaction = Transaction::factory(2)->sell()->lastMonth()->portfolio($portfolio->id)->symbol('ACME')->create()->first(); Artisan::call('sync:daily-change', ['portfolio_id' => $portfolio->id]); $daily_change = DailyChange::whereDate('date', '<=', $third_transaction->date->addDays(2)) - ->whereDate('date', '>=', $third_transaction->date->subDays(2)) - ->orderByDesc('date') - ->first(); + ->whereDate('date', '>=', $third_transaction->date->subDays(2)) + ->orderByDesc('date') + ->first(); - $this->assertEquals(0, $daily_change->total_cost_basis); + $this->assertEquals(0, $daily_change->total_cost_basis); } - /** - */ public function test_sales_are_captured_as_realized_gains(): void { $this->actingAs($user = User::factory()->create()); diff --git a/tests/TestCase.php b/tests/TestCase.php index 43559b9..f6e21ae 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,14 +9,14 @@ abstract class TestCase extends BaseTestCase protected function setUp(): void { parent::setUp(); - + // } - + protected function tearDown(): void { parent::tearDown(); - + // } } diff --git a/tests/TransactionsTest.php b/tests/TransactionsTest.php index 8020499..8c387a3 100644 --- a/tests/TransactionsTest.php +++ b/tests/TransactionsTest.php @@ -2,19 +2,16 @@ namespace Tests; -use Tests\TestCase; -use App\Models\User; use App\Models\Holding; use App\Models\Portfolio; use App\Models\Transaction; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; class TransactionsTest extends TestCase { use RefreshDatabase; - /** - */ public function test_can_create_a_transaction(): void { $this->actingAs($user = User::factory()->create()); @@ -24,8 +21,6 @@ class TransactionsTest extends TestCase $this->assertNotNull($transaction); } - /** - */ public function test_sales_calculate_cost_basis(): void { $this->actingAs($user = User::factory()->create()); @@ -37,8 +32,6 @@ class TransactionsTest extends TestCase $this->assertNotNull($transaction->cost_basis); } - /** - */ public function test_purchases_dont_have_sale_price(): void { $this->actingAs($user = User::factory()->create()); @@ -48,8 +41,6 @@ class TransactionsTest extends TestCase $this->assertNull($transaction->sale_price); } - /** - */ public function test_transaction_synced_to_holding(): void { $this->actingAs($user = User::factory()->create()); @@ -62,17 +53,17 @@ class TransactionsTest extends TestCase $this->assertDatabaseHas('holdings', [ 'portfolio_id' => $portfolio->id, 'symbol' => 'AAPL', - 'quantity' => 4 + 'quantity' => 4, ]); $holding = Holding::where([ 'portfolio_id' => $portfolio->id, - 'symbol' => 'AAPL' + 'symbol' => 'AAPL', ])->first(); $this->assertEqualsWithDelta( - $holding->realized_gain_dollars, - $transaction->sale_price - $transaction->cost_basis, + $holding->realized_gain_dollars, + $transaction->sale_price - $transaction->cost_basis, 0.01 ); } diff --git a/tests/TwoFactorAuthenticationSettingsTest.php b/tests/TwoFactorAuthenticationSettingsTest.php index c380b23..cd5da7e 100644 --- a/tests/TwoFactorAuthenticationSettingsTest.php +++ b/tests/TwoFactorAuthenticationSettingsTest.php @@ -7,7 +7,6 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Fortify\Features; use Laravel\Jetstream\Http\Livewire\TwoFactorAuthenticationForm; use Livewire\Livewire; -use Tests\TestCase; class TwoFactorAuthenticationSettingsTest extends TestCase { diff --git a/tests/UpdatePasswordTest.php b/tests/UpdatePasswordTest.php index 6f5bbde..a9ac139 100644 --- a/tests/UpdatePasswordTest.php +++ b/tests/UpdatePasswordTest.php @@ -7,7 +7,6 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; use Laravel\Jetstream\Http\Livewire\UpdatePasswordForm; use Livewire\Livewire; -use Tests\TestCase; class UpdatePasswordTest extends TestCase {