From b3f0f89d16ff632f8993b2df1913cd2011711b81 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Thu, 23 Jan 2025 22:47:16 -0600 Subject: [PATCH 01/12] wip --- app/Http/ApiControllers/Controller.php | 8 + app/Http/ApiControllers/HoldingController.php | 15 ++ .../ApiControllers/PortfolioController.php | 24 ++ .../ApiControllers/TransactionController.php | 15 ++ app/Http/ApiControllers/UserController.php | 15 ++ .../ConnectedAccountController.php | 1 - app/Http/Controllers/Controller.php | 6 +- .../InvitedOnboardingController.php | 1 - app/Http/Resources/HoldingResource.php | 19 ++ app/Http/Resources/PortfolioResource.php | 28 +++ app/Http/Resources/TransactionResource.php | 19 ++ app/Http/Resources/UserResource.php | 27 +++ app/Providers/AppServiceProvider.php | 3 +- app/Providers/JetstreamServiceProvider.php | 9 +- app/Support/FilterRequest.php | 205 ++++++++++++++++++ config/jetstream.php | 2 +- lang/en.json | 2 +- lang/es.json | 2 +- .../views/api/api-token-manager.blade.php | 2 +- routes/api.php | 18 +- routes/web.php | 2 +- 21 files changed, 401 insertions(+), 22 deletions(-) create mode 100644 app/Http/ApiControllers/Controller.php create mode 100644 app/Http/ApiControllers/HoldingController.php create mode 100644 app/Http/ApiControllers/PortfolioController.php create mode 100644 app/Http/ApiControllers/TransactionController.php create mode 100644 app/Http/ApiControllers/UserController.php create mode 100644 app/Http/Resources/HoldingResource.php create mode 100644 app/Http/Resources/PortfolioResource.php create mode 100644 app/Http/Resources/TransactionResource.php create mode 100644 app/Http/Resources/UserResource.php create mode 100644 app/Support/FilterRequest.php diff --git a/app/Http/ApiControllers/Controller.php b/app/Http/ApiControllers/Controller.php new file mode 100644 index 0000000..bc0e11d --- /dev/null +++ b/app/Http/ApiControllers/Controller.php @@ -0,0 +1,8 @@ +user()); + } +} \ No newline at end of file diff --git a/app/Http/ApiControllers/PortfolioController.php b/app/Http/ApiControllers/PortfolioController.php new file mode 100644 index 0000000..72299a4 --- /dev/null +++ b/app/Http/ApiControllers/PortfolioController.php @@ -0,0 +1,24 @@ +setScopes(['myPortfolios']); + $filterRequest->setEagerRelations(['users', 'transactions', 'holdings']); + $filterRequest->setFilterableRelations(['holdings' => 'symbol', 'transactions' => 'symbol']); + $filterRequest->setSearchableColumns(['title', 'notes']); + + return PortfolioResource::collection($filterRequest->get()); + } +} \ No newline at end of file diff --git a/app/Http/ApiControllers/TransactionController.php b/app/Http/ApiControllers/TransactionController.php new file mode 100644 index 0000000..b482720 --- /dev/null +++ b/app/Http/ApiControllers/TransactionController.php @@ -0,0 +1,15 @@ +user()); + } +} \ No newline at end of file diff --git a/app/Http/ApiControllers/UserController.php b/app/Http/ApiControllers/UserController.php new file mode 100644 index 0000000..468ca52 --- /dev/null +++ b/app/Http/ApiControllers/UserController.php @@ -0,0 +1,15 @@ +user()); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ConnectedAccountController.php b/app/Http/Controllers/ConnectedAccountController.php index 8333b01..0551b89 100644 --- a/app/Http/Controllers/ConnectedAccountController.php +++ b/app/Http/Controllers/ConnectedAccountController.php @@ -6,7 +6,6 @@ use Exception; use App\Models\User; use App\Models\ConnectedAccount; use Illuminate\Support\MessageBag; -use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Blade; use Laravel\Socialite\Facades\Socialite; diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 8677cd5..71116b2 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -1,8 +1,8 @@ + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/PortfolioResource.php b/app/Http/Resources/PortfolioResource.php new file mode 100644 index 0000000..8a71071 --- /dev/null +++ b/app/Http/Resources/PortfolioResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'wishlist' => $this->wishlist, + 'owner' => UserResource::make($this->owner), + 'transactions' => TransactionResource::collection($this->whenLoaded('transactions')), + 'holdings' => HoldingResource::collection($this->whenLoaded('holdings')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Http/Resources/TransactionResource.php b/app/Http/Resources/TransactionResource.php new file mode 100644 index 0000000..358bfe5 --- /dev/null +++ b/app/Http/Resources/TransactionResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..b68e3fa --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,27 @@ + + */ + public function toArray(Request $request): array + { + + return [ + 'id' => $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'profile_photo_url' => $this->profile_photo_url, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0450ca0..39f79b0 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Illuminate\Http\Resources\Json\JsonResource; class AppServiceProvider extends ServiceProvider { @@ -22,6 +23,6 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + JsonResource::withoutWrapping(); } } diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index f931bf3..f7a2ed0 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -43,13 +43,8 @@ class JetstreamServiceProvider extends ServiceProvider */ protected function configurePermissions(): void { - Jetstream::defaultApiTokenPermissions(['read']); + Jetstream::defaultApiTokenPermissions([]); - Jetstream::permissions([ - 'create', - 'read', - 'update', - 'delete', - ]); + Jetstream::permissions([]); } } diff --git a/app/Support/FilterRequest.php b/app/Support/FilterRequest.php new file mode 100644 index 0000000..07b64ef --- /dev/null +++ b/app/Support/FilterRequest.php @@ -0,0 +1,205 @@ +query = (new $modelClass)->query(); + } + + /** + * Sets eager loads on the underlying query + * + * @param array $scopes + * @return void + */ + public function setEagerRelations(string|array $relations) + { + $this->query = $this->query->with($relations); + } + + /** + * Sets scopes on the underlying query + * + * @param array $scopes + * @return void + */ + public function setScopes(string|array $scopes): void + { + $this->query = $this->query->scopes($scopes); + } + + /** + * Allows nested json data to be aliased into a virtual column for the query + * + * @param array $columns can be an array of keys (e.g. `body.total_amount`) + * @return void + */ + public function setVirtualColumns(array $columns) + { + foreach($columns as $column) { + $column = Str::replace('.', '->', $column); + $alias = Str::snake(Str::afterLast($column, '->')); + + array_push($this->select, $column . ' as ' . $alias); + } + } + + /** + * Set columns that should be searched + * + * @param array $columns can be an array of keys (e.g. `user.name` or a nested array with + * `relation`, `table`, and `column` attributes) + * @return void + */ + public function setSearchableColumns(array $columns) + { + $this->searchableColumns = $columns; + } + + /** + * Set relations that can be filtered + * + * @param array $relations is an array of keys (relations) and values (columns) + * @return void + */ + public function setFilterableRelations(array $relations) + { + $this->filterableRelations = $relations; + } + + /** + * Set related columns using aggregate function (e.g. `package.label` would become `package_label`) + * which enables filtering, searching, and sorting on the front end + * + * @param array $columns can be an array of keys (e.g. `user.name` or a nested array with + * `relation`, `table`, and `column` attributes) + * @return void + */ + public function setRelationshipColumns(array $columns) + { + foreach($columns as $column) { + + // advanced + if (is_array($column)) { + + $this->query->withAggregate($column['relation'], $column['table'].'.'.$column['column']); + + continue; + } + + // not a relationship + if(!Str::contains($column, '.')) { + continue; + } + + // normal rx + $relationship = Str::before($column, '.'); + $key = Str::after($column, '.'); + + $this->query->withAggregate($relationship, $key); + } + } + + /** + * Get the resulting paginated collection + * + * @return LengthAwarePaginator + */ + public function get(): LengthAwarePaginator + { + // handle sort + if (!empty(request()->query('sortBy'))) { + if (Str::contains(request()->query('sortBy'), '.')) { + $this->query->joinRelation(Str::before(request()->query('sortBy'), '.')); + } + $this->query->orderBy( + request()->query('sortBy'), + request()->query('sortDesc', false) == "true" ? 'DESC' : 'ASC' + ); + } + + // handle filter + if (request()->has('filter')) { + + foreach(request()->query('filter') as $filter => $params) { + + if (array_key_exists($filter, $this->filterableRelations)) { + + // filtered rx + foreach(explode(',', $params) as $param) { + $this->query->whereHas($filter, function ($query) use ($filter, $param) { + $query->where($this->filterableRelations[$filter], $param); + }); + } + + } else { + // traditional filter + foreach(explode(',', $params) as $param) { + $this->query->having($filter, $param); + } + + } + } + } + + // handle search + if (request()->has('search') && !empty($this->searchableColumns)) { + // make searchable relationships aggregate columns + $this->setRelationshipColumns($this->searchableColumns); + + $this->query->where(function($query) { + + foreach($this->searchableColumns as $column) { + + // advanced + if (is_array($column)) { + $query->orWhereHas($column['relation'], function($query) use ($column) { + $query->where($column['table'].'.'.$column['column'], "like", '%' . request()->query('search') . '%'); + }); + + continue; + } + + // normal RX + if(Str::contains($column, '.')) { + + $query->orWhereHas(Str::before($column, '.'), function($query) use ($column) { + $query->where(Str::after($column, '.'), "like", '%' . request()->query('search') . '%'); + }); + + continue; + } + + // not rx + $query->orWhere($column, "like", '%' . request()->query('search') . '%'); + } + }); + } + + // handle per page + if (request()->query('itemsPerPage') == "-1") { + $perPage = $this->query->count(); + } else { + $perPage = request()->query('itemsPerPage', 15); + } + + // run + return $this->query->addSelect($this->select)->paginate($perPage); + } +} diff --git a/config/jetstream.php b/config/jetstream.php index 7b9d0f6..b1f2c73 100644 --- a/config/jetstream.php +++ b/config/jetstream.php @@ -60,7 +60,7 @@ return [ 'features' => [ Features::termsAndPrivacyPolicy(), Features::profilePhotos(), - // Features::api(), + Features::api(), // Features::teams(['invitations' => true]), Features::accountDeletion(), ], diff --git a/lang/en.json b/lang/en.json index f40abc4..c2dead1 100644 --- a/lang/en.json +++ b/lang/en.json @@ -70,7 +70,7 @@ "API Token": "API Token", "Please copy your new API token. For your security, it won\\'t be shown again.": "Please copy your new API token. For your security, it won\\'t be shown again.", "API Token Permissions": "API Token Permissions", - "API tokens allow third-party services to authenticate with our application on your behalf.": "API tokens allow third-party services to authenticate with our application on your behalf.", + "API tokens allow third-party services to authenticate with Investbrain on your behalf.": "API tokens allow third-party services to authenticate with Investbrain on your behalf.", "Delete API Token": "Delete API Token", "Are you sure you would like to delete this API token?": "Are you sure you would like to delete this API token?", "This is a secure area of the application. Please confirm your password before continuing.": "This is a secure area of the application. Please confirm your password before continuing.", diff --git a/lang/es.json b/lang/es.json index 13a81b2..04ef548 100644 --- a/lang/es.json +++ b/lang/es.json @@ -70,7 +70,7 @@ "API Token": "Token API", "Please copy your new API token. For your security, it won't be shown again.": "Por favor, copia tu nuevo token API. Por seguridad, no se mostrará nuevamente.", "API Token Permissions": "Permisos del Token API", - "API tokens allow third-party services to authenticate with our application on your behalf.": "Los tokens API permiten que servicios de terceros se autentiquen con nuestra aplicación en tu nombre.", + "API tokens allow third-party services to authenticate with Investbrain on your behalf.": "Los tokens API permiten que servicios de terceros se autentiquen con Investbrain en tu nombre.", "Delete API Token": "Eliminar Token API", "Are you sure you would like to delete this API token?": "¿Estás seguro de que deseas eliminar este token API?", "This is a secure area of the application. Please confirm your password before continuing.": "Esta es un área segura de la aplicación. Por favor, confirma tu contraseña antes de continuar.", diff --git a/resources/views/api/api-token-manager.blade.php b/resources/views/api/api-token-manager.blade.php index d161ca0..aad3ece 100644 --- a/resources/views/api/api-token-manager.blade.php +++ b/resources/views/api/api-token-manager.blade.php @@ -6,7 +6,7 @@ - {{ __('API tokens allow third-party services to authenticate with our application on your behalf.') }} + {{ __('API tokens allow third-party services to authenticate with Investbrain on your behalf.') }} diff --git a/routes/api.php b/routes/api.php index ccc387f..d19bba6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,18 @@ user(); -})->middleware('auth:sanctum'); +Route::middleware(['auth:sanctum'])->group(function () { + + // user + Route::get('/me', [UserController::class, 'me']); + + // portfolio + Route::get('/portfolio', [PortfolioController::class, 'index']); + + // transaction + + // holding +}); \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index d961542..0c60cd7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,8 +5,8 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\HoldingController; use App\Http\Controllers\DashboardController; use App\Http\Controllers\PortfolioController; -use App\Http\Controllers\ConnectedAccountController; use App\Http\Controllers\TransactionController; +use App\Http\Controllers\ConnectedAccountController; use App\Http\Controllers\InvitedOnboardingController; use Laravel\Jetstream\Http\Controllers\Livewire\PrivacyPolicyController; use Laravel\Jetstream\Http\Controllers\Livewire\TermsOfServiceController; From cc447c5fb054c0e334cbe733aa249d0abf101cd0 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Fri, 24 Jan 2025 19:15:28 -0600 Subject: [PATCH 02/12] fix: force boolean columns to be false --- .../2021_02_25_041257_create_transactions_table.php | 2 +- .../migrations/2024_10_18_000001_add_reinvestment_columns.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 7ed2184..f5a7497 100644 --- a/database/migrations/2021_02_25_041257_create_transactions_table.php +++ b/database/migrations/2021_02_25_041257_create_transactions_table.php @@ -23,7 +23,7 @@ class CreateTransactionsTable extends Migration $table->float('quantity', 12, 4); $table->float('cost_basis', 12, 4); $table->float('sale_price', 12, 4)->nullable(); - $table->boolean('split')->nullable(); + $table->boolean('split')->default(false); $table->date('date'); $table->timestamps(); }); diff --git a/database/migrations/2024_10_18_000001_add_reinvestment_columns.php b/database/migrations/2024_10_18_000001_add_reinvestment_columns.php index 28defcc..da2293f 100644 --- a/database/migrations/2024_10_18_000001_add_reinvestment_columns.php +++ b/database/migrations/2024_10_18_000001_add_reinvestment_columns.php @@ -12,11 +12,11 @@ return new class extends Migration public function up(): void { Schema::table('holdings', function (Blueprint $table) { - $table->boolean('reinvest_dividends')->nullable()->after('quantity'); + $table->boolean('reinvest_dividends')->default(false)->after('quantity'); }); Schema::table('transactions', function (Blueprint $table) { - $table->boolean('reinvested_dividend')->nullable()->after('split'); + $table->boolean('reinvested_dividend')->default(false)->after('split'); }); } From f724f450f2125c000fd3be3708979202a2a28023 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Fri, 24 Jan 2025 19:17:55 -0600 Subject: [PATCH 03/12] fix: make default for currency values not nullable --- .../2021_09_06_014744_create_holdings_table.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 c4a9692..e43f690 100644 --- a/database/migrations/2021_09_06_014744_create_holdings_table.php +++ b/database/migrations/2021_09_06_014744_create_holdings_table.php @@ -20,10 +20,10 @@ class CreateHoldingsTable extends Migration $table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade'); $table->foreignIdFor(MarketData::class, 'symbol'); $table->float('quantity', 12, 4); - $table->float('average_cost_basis', 12, 4); - $table->float('total_cost_basis', 12, 4)->nullable(); - $table->float('realized_gain_dollars', 12, 4)->nullable(); - $table->float('dividends_earned', 12, 4)->nullable(); + $table->float('average_cost_basis', 12, 4)->default(0); + $table->float('total_cost_basis', 12, 4)->default(0); + $table->float('realized_gain_dollars', 12, 4)->default(0); + $table->float('dividends_earned', 12, 4)->default(0); $table->timestamp('splits_synced_at')->nullable(); $table->timestamps(); }); From b9d41f9ac0d9ce426fd5af8cfac1c91009577ba4 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Fri, 24 Jan 2025 19:18:27 -0600 Subject: [PATCH 04/12] chore: clean up unneeded attributes --- app/Models/Holding.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/Models/Holding.php b/app/Models/Holding.php index b8c7d87..6cb4d43 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -36,11 +36,6 @@ class Holding extends Model 'reinvest_dividends' => 'boolean' ]; - protected $attributes = [ - 'realized_gain_dollars' => 0, - 'dividends_earned' => 0, - ]; - /** * Market data for holding * From 6d9e0008b86d3b7d8ccbc986b7176f474369ca2b Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Fri, 24 Jan 2025 19:24:16 -0600 Subject: [PATCH 05/12] wip --- app/Http/ApiControllers/HoldingController.php | 14 +- .../ApiControllers/MarketDataController.php | 21 ++ .../ApiControllers/PortfolioController.php | 20 +- .../ApiControllers/TransactionController.php | 13 +- app/Http/Resources/HoldingResource.php | 20 +- app/Http/Resources/MarketDataResource.php | 36 +++ app/Http/Resources/PortfolioResource.php | 2 + app/Http/Resources/TransactionResource.php | 17 +- app/Http/Resources/UserResource.php | 2 + app/Support/FilterRequest.php | 205 ------------------ routes/api.php | 10 +- 11 files changed, 137 insertions(+), 223 deletions(-) create mode 100644 app/Http/ApiControllers/MarketDataController.php create mode 100644 app/Http/Resources/MarketDataResource.php delete mode 100644 app/Support/FilterRequest.php diff --git a/app/Http/ApiControllers/HoldingController.php b/app/Http/ApiControllers/HoldingController.php index 3fc4e64..15330f0 100644 --- a/app/Http/ApiControllers/HoldingController.php +++ b/app/Http/ApiControllers/HoldingController.php @@ -2,14 +2,22 @@ namespace App\Http\ApiControllers; +use App\Models\Holding; use Illuminate\Http\Request; -use App\Http\Resources\UserResource; +use App\Http\Resources\HoldingResource; +use HackerEsq\FilterModels\FilterModels; use App\Http\ApiControllers\Controller as ApiController; class HoldingController extends ApiController { - public function me(Request $request) + public function index(FilterModels $filters) { - return UserResource::make($request->user()); + + $filters->setQuery(Holding::query()); + $filters->setScopes(['myHoldings']); + $filters->setEagerRelations(['market_data', 'transactions']); + $filters->setSearchableColumns(['symbol']); + + return HoldingResource::collection($filters->paginated()); } } \ No newline at end of file diff --git a/app/Http/ApiControllers/MarketDataController.php b/app/Http/ApiControllers/MarketDataController.php new file mode 100644 index 0000000..31346e9 --- /dev/null +++ b/app/Http/ApiControllers/MarketDataController.php @@ -0,0 +1,21 @@ +setScopes(['myPortfolios']); - $filterRequest->setEagerRelations(['users', 'transactions', 'holdings']); - $filterRequest->setFilterableRelations(['holdings' => 'symbol', 'transactions' => 'symbol']); - $filterRequest->setSearchableColumns(['title', 'notes']); - - return PortfolioResource::collection($filterRequest->get()); + $filters->setQuery(Portfolio::query()); + $filters->setScopes(['myPortfolios']); + $filters->setEagerRelations(['users', 'transactions', 'holdings']); + $filters->setFilterableRelations(['holdings' => 'symbol', 'transactions' => 'symbol']); + $filters->setSearchableColumns(['title', 'notes']); + + return PortfolioResource::collection($filters->paginated()); } } \ No newline at end of file diff --git a/app/Http/ApiControllers/TransactionController.php b/app/Http/ApiControllers/TransactionController.php index b482720..68b8597 100644 --- a/app/Http/ApiControllers/TransactionController.php +++ b/app/Http/ApiControllers/TransactionController.php @@ -2,14 +2,21 @@ namespace App\Http\ApiControllers; +use App\Models\Transaction; use Illuminate\Http\Request; -use App\Http\Resources\UserResource; +use HackerEsq\FilterModels\FilterModels; +use App\Http\Resources\TransactionResource; use App\Http\ApiControllers\Controller as ApiController; class TransactionController extends ApiController { - public function me(Request $request) + public function index(FilterModels $filters) { - return UserResource::make($request->user()); + + $filters->setQuery(Transaction::query()); + $filters->setScopes(['myTransactions']); + $filters->setSearchableColumns(['symbol']); + + return TransactionResource::collection($filters->paginated()); } } \ No newline at end of file diff --git a/app/Http/Resources/HoldingResource.php b/app/Http/Resources/HoldingResource.php index c4f43c8..57cd293 100644 --- a/app/Http/Resources/HoldingResource.php +++ b/app/Http/Resources/HoldingResource.php @@ -1,5 +1,7 @@ $this->id, + 'portfolio_id' => $this->portfolio_id, + 'symbol' => $this->symbol, + 'quantity' => $this->quantity, + 'reinvest_dividends' => $this->reinvest_dividends, + 'average_cost_basis' => $this->average_cost_basis, + 'total_cost_basis' => $this->total_cost_basis, + 'realized_gain_dollars' => $this->realized_gain_dollars, + 'dividends_earned' => $this->dividends_earned, + 'splits_synced_at' => $this->splits_synced_at, + 'total_market_value' => $this->total_market_value, + 'market_gain_dollars' => $this->market_gain_dollars, + 'market_gain_percent' => $this->market_gain_percent, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at + ]; } } diff --git a/app/Http/Resources/MarketDataResource.php b/app/Http/Resources/MarketDataResource.php new file mode 100644 index 0000000..b9744aa --- /dev/null +++ b/app/Http/Resources/MarketDataResource.php @@ -0,0 +1,36 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'symbol' => $this->symbol, + 'name' => $this->name, + 'market_value' => $this->market_value, + 'fifty_two_week_low' => $this->fifty_two_week_low, + 'fifty_two_week_high' => $this->fifty_two_week_high, + 'last_dividend_date' => $this->last_dividend_date, + 'last_dividend_amount' => $this->last_dividend_amount, + 'dividend_yield' => $this->dividend_yield, + 'market_cap' => $this->market_cap, + 'trailing_pe' => $this->trailing_pe, + 'forward_pe' => $this->forward_pe, + 'book_value' => $this->book_value, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Http/Resources/PortfolioResource.php b/app/Http/Resources/PortfolioResource.php index 8a71071..1bd9f06 100644 --- a/app/Http/Resources/PortfolioResource.php +++ b/app/Http/Resources/PortfolioResource.php @@ -1,5 +1,7 @@ $this->id, + 'symbol' => $this->symbol, + 'portfolio_id' => $this->portfolio_id, + 'transaction_type' => $this->transaction_type, + 'quantity' => $this->quantity, + 'cost_basis' => $this->cost_basis, + 'sale_price' => $this->sale_price, + 'split' => $this->split, + 'reinvested_dividend' => $this->reinvested_dividend, + 'date' => $this->date, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; } } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index b68e3fa..41eeff5 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -1,5 +1,7 @@ query = (new $modelClass)->query(); - } - - /** - * Sets eager loads on the underlying query - * - * @param array $scopes - * @return void - */ - public function setEagerRelations(string|array $relations) - { - $this->query = $this->query->with($relations); - } - - /** - * Sets scopes on the underlying query - * - * @param array $scopes - * @return void - */ - public function setScopes(string|array $scopes): void - { - $this->query = $this->query->scopes($scopes); - } - - /** - * Allows nested json data to be aliased into a virtual column for the query - * - * @param array $columns can be an array of keys (e.g. `body.total_amount`) - * @return void - */ - public function setVirtualColumns(array $columns) - { - foreach($columns as $column) { - $column = Str::replace('.', '->', $column); - $alias = Str::snake(Str::afterLast($column, '->')); - - array_push($this->select, $column . ' as ' . $alias); - } - } - - /** - * Set columns that should be searched - * - * @param array $columns can be an array of keys (e.g. `user.name` or a nested array with - * `relation`, `table`, and `column` attributes) - * @return void - */ - public function setSearchableColumns(array $columns) - { - $this->searchableColumns = $columns; - } - - /** - * Set relations that can be filtered - * - * @param array $relations is an array of keys (relations) and values (columns) - * @return void - */ - public function setFilterableRelations(array $relations) - { - $this->filterableRelations = $relations; - } - - /** - * Set related columns using aggregate function (e.g. `package.label` would become `package_label`) - * which enables filtering, searching, and sorting on the front end - * - * @param array $columns can be an array of keys (e.g. `user.name` or a nested array with - * `relation`, `table`, and `column` attributes) - * @return void - */ - public function setRelationshipColumns(array $columns) - { - foreach($columns as $column) { - - // advanced - if (is_array($column)) { - - $this->query->withAggregate($column['relation'], $column['table'].'.'.$column['column']); - - continue; - } - - // not a relationship - if(!Str::contains($column, '.')) { - continue; - } - - // normal rx - $relationship = Str::before($column, '.'); - $key = Str::after($column, '.'); - - $this->query->withAggregate($relationship, $key); - } - } - - /** - * Get the resulting paginated collection - * - * @return LengthAwarePaginator - */ - public function get(): LengthAwarePaginator - { - // handle sort - if (!empty(request()->query('sortBy'))) { - if (Str::contains(request()->query('sortBy'), '.')) { - $this->query->joinRelation(Str::before(request()->query('sortBy'), '.')); - } - $this->query->orderBy( - request()->query('sortBy'), - request()->query('sortDesc', false) == "true" ? 'DESC' : 'ASC' - ); - } - - // handle filter - if (request()->has('filter')) { - - foreach(request()->query('filter') as $filter => $params) { - - if (array_key_exists($filter, $this->filterableRelations)) { - - // filtered rx - foreach(explode(',', $params) as $param) { - $this->query->whereHas($filter, function ($query) use ($filter, $param) { - $query->where($this->filterableRelations[$filter], $param); - }); - } - - } else { - // traditional filter - foreach(explode(',', $params) as $param) { - $this->query->having($filter, $param); - } - - } - } - } - - // handle search - if (request()->has('search') && !empty($this->searchableColumns)) { - // make searchable relationships aggregate columns - $this->setRelationshipColumns($this->searchableColumns); - - $this->query->where(function($query) { - - foreach($this->searchableColumns as $column) { - - // advanced - if (is_array($column)) { - $query->orWhereHas($column['relation'], function($query) use ($column) { - $query->where($column['table'].'.'.$column['column'], "like", '%' . request()->query('search') . '%'); - }); - - continue; - } - - // normal RX - if(Str::contains($column, '.')) { - - $query->orWhereHas(Str::before($column, '.'), function($query) use ($column) { - $query->where(Str::after($column, '.'), "like", '%' . request()->query('search') . '%'); - }); - - continue; - } - - // not rx - $query->orWhere($column, "like", '%' . request()->query('search') . '%'); - } - }); - } - - // handle per page - if (request()->query('itemsPerPage') == "-1") { - $perPage = $this->query->count(); - } else { - $perPage = request()->query('itemsPerPage', 15); - } - - // run - return $this->query->addSelect($this->select)->paginate($perPage); - } -} diff --git a/routes/api.php b/routes/api.php index d19bba6..f1c9894 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,11 @@ group(function () { @@ -13,6 +16,11 @@ Route::middleware(['auth:sanctum'])->group(function () { Route::get('/portfolio', [PortfolioController::class, 'index']); // transaction + Route::get('/transaction', [TransactionController::class, 'index']); // holding + Route::get('/holding', [HoldingController::class, 'index']); + + // market data + Route::get('/market-data/{symbol}', [MarketDataController::class, 'show']); }); \ No newline at end of file From b8f24d4b67b40f88744881c997dc96ea644ec842 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Fri, 24 Jan 2025 19:29:15 -0600 Subject: [PATCH 06/12] add filter-models for api controllers --- composer.json | 7 +- composer.lock | 935 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 610 insertions(+), 332 deletions(-) diff --git a/composer.json b/composer.json index 6c0612c..a02068b 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "robsontenorio/mary": "^1.35", "scheb/yahoo-finance-api": "^4.11", "staudenmeir/eloquent-has-many-deep": "^1.20", - "tschucki/alphavantage-laravel": "^0.0" + "tschucki/alphavantage-laravel": "^0.0", + "hackeresq/filter-models": "dev-main" }, "require-dev": { "fakerphp/faker": "^1.23", @@ -32,6 +33,10 @@ "phpunit/phpunit": "^11.0.1" }, "repositories": [ + { + "type": "vcs", + "url": "https://github.com/hackeresq/filter-models" + }, { "type": "vcs", "url": "https://github.com/investbrainapp/finnhub-php" diff --git a/composer.lock b/composer.lock index edd3321..c5bcd19 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7a08994d8c9bda95b9481a926a63c405", + "content-hash": "e8ee89ca51e5e67c7c812a39455132da", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.334.1", + "version": "3.338.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "3938b3467f64a30fed7ee1762a6785f808a5ae4d" + "reference": "7a52364e053d74363f9976dfb4473bace5b7790e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3938b3467f64a30fed7ee1762a6785f808a5ae4d", - "reference": "3938b3467f64a30fed7ee1762a6785f808a5ae4d", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7a52364e053d74363f9976dfb4473bace5b7790e", + "reference": "7a52364e053d74363f9976dfb4473bace5b7790e", "shasum": "" }, "require": { @@ -79,31 +79,31 @@ "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", - "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "guzzlehttp/promises": "^1.4.0 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", - "mtdowling/jmespath.php": "^2.6", - "php": ">=7.2.5", - "psr/http-message": "^1.0 || ^2.0" + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "psr/http-message": "^2.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", - "composer/composer": "^1.10.22", + "composer/composer": "^2.7.8", "dms/phpunit-arraysubset-asserts": "^0.4.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", "ext-pcntl": "*", "ext-sockets": "*", - "nette/neon": "^2.3", "paragonie/random_compat": ">= 2", "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", - "sebastian/comparator": "^1.2.3 || ^4.0", - "yoast/phpunit-polyfills": "^1.0" + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "symfony/filesystem": "^v6.4.0 || ^v7.1.0", + "yoast/phpunit-polyfills": "^2.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -152,11 +152,11 @@ "sdk" ], "support": { - "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.334.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.338.2" }, - "time": "2024-12-05T01:17:41+00:00" + "time": "2025-01-24T19:09:22+00:00" }, { "name": "bacon/bacon-qr-code", @@ -932,16 +932,16 @@ }, { "name": "egulias/email-validator", - "version": "4.0.2", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e" + "reference": "b115554301161fa21467629f1e1391c1936de517" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e", - "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", + "reference": "b115554301161fa21467629f1e1391c1936de517", "shasum": "" }, "require": { @@ -987,7 +987,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.2" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.3" }, "funding": [ { @@ -995,7 +995,7 @@ "type": "github" } ], - "time": "2023-10-06T06:47:41+00:00" + "time": "2024-12-27T00:36:43+00:00" }, { "name": "ezyang/htmlpurifier", @@ -1122,16 +1122,16 @@ }, { "name": "firebase/php-jwt", - "version": "v6.10.2", + "version": "v6.11.0", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b" + "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/30c19ed0f3264cb660ea496895cfb6ef7ee3653b", - "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/8f718f4dfc9c5d5f0c994cdfd103921b43592712", + "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712", "shasum": "" }, "require": { @@ -1179,9 +1179,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.10.2" + "source": "https://github.com/firebase/php-jwt/tree/v6.11.0" }, - "time": "2024-11-24T11:22:49+00:00" + "time": "2025-01-23T05:11:06+00:00" }, { "name": "fruitcake/php-cors", @@ -1727,6 +1727,45 @@ ], "time": "2023-12-03T19:50:20+00:00" }, + { + "name": "hackeresq/filter-models", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/hackerESQ/filter-models.git", + "reference": "4ddba2483dc3987dcdf3f637be7edb8a2c96606c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hackerESQ/filter-models/zipball/4ddba2483dc3987dcdf3f637be7edb8a2c96606c", + "reference": "4ddba2483dc3987dcdf3f637be7edb8a2c96606c", + "shasum": "" + }, + "require": { + "laravel/framework": "^11.9", + "php": "^8.2" + }, + "default-branch": true, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "HackerEsq\\FilterModels\\FilterModelsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "HackerEsq\\FilterModels\\": "src/" + } + }, + "description": "Simple package to filter your Laravel models with query parameters", + "support": { + "source": "https://github.com/hackerESQ/filter-models/tree/main", + "issues": "https://github.com/hackerESQ/filter-models/issues" + }, + "time": "2025-01-25T01:22:46+00:00" + }, { "name": "jfcherng/php-color-output", "version": "3.0.0", @@ -1966,16 +2005,16 @@ }, { "name": "laravel/fortify", - "version": "v1.25.0", + "version": "v1.25.3", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "047e84ea8afe217061f1afa7cf8c6410c9d6a480" + "reference": "ee35e5b8ea25cc51f8323e27a839283becd44160" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/047e84ea8afe217061f1afa7cf8c6410c9d6a480", - "reference": "047e84ea8afe217061f1afa7cf8c6410c9d6a480", + "url": "https://api.github.com/repos/laravel/fortify/zipball/ee35e5b8ea25cc51f8323e27a839283becd44160", + "reference": "ee35e5b8ea25cc51f8323e27a839283becd44160", "shasum": "" }, "require": { @@ -2027,20 +2066,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2024-11-21T20:06:18+00:00" + "time": "2025-01-17T15:17:57+00:00" }, { "name": "laravel/framework", - "version": "v11.34.2", + "version": "v11.40.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "865da6d73dd353f07a7bcbd778c55966a620121f" + "reference": "599a28196d284fee158cc10086fd56ac625ad7a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/865da6d73dd353f07a7bcbd778c55966a620121f", - "reference": "865da6d73dd353f07a7bcbd778c55966a620121f", + "url": "https://api.github.com/repos/laravel/framework/zipball/599a28196d284fee158cc10086fd56ac625ad7a3", + "reference": "599a28196d284fee158cc10086fd56ac625ad7a3", "shasum": "" }, "require": { @@ -2061,11 +2100,12 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.2.1", + "league/commonmark": "^2.6", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.72.2|^3.4", + "nesbot/carbon": "^2.72.6|^3.8.4", "nunomaduro/termwind": "^2.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", @@ -2075,7 +2115,7 @@ "symfony/console": "^7.0.3", "symfony/error-handler": "^7.0.3", "symfony/finder": "^7.0.3", - "symfony/http-foundation": "^7.0.3", + "symfony/http-foundation": "^7.2.0", "symfony/http-kernel": "^7.0.3", "symfony/mailer": "^7.0.3", "symfony/mime": "^7.0.3", @@ -2089,7 +2129,6 @@ "voku/portable-ascii": "^2.0.2" }, "conflict": { - "mockery/mockery": "1.6.8", "tightenco/collect": "<5.5.33" }, "provide": { @@ -2141,15 +2180,16 @@ "fakerphp/faker": "^1.24", "guzzlehttp/promises": "^2.0.3", "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", "league/flysystem-aws-s3-v3": "^3.25.1", "league/flysystem-ftp": "^3.25.1", "league/flysystem-path-prefixing": "^3.25.1", "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "nyholm/psr7": "^1.2", "orchestra/testbench-core": "^9.6", "pda/pheanstalk": "^5.0.6", + "php-http/discovery": "^1.15", "phpstan/phpstan": "^1.11.5", "phpunit/phpunit": "^10.5.35|^11.3.6", "predis/predis": "^2.3", @@ -2181,8 +2221,8 @@ "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", "mockery/mockery": "Required to use mocking (^1.6).", - "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).", "predis/predis": "Required to use the predis connector (^2.3).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", @@ -2203,6 +2243,7 @@ }, "autoload": { "files": [ + "src/Illuminate/Collections/functions.php", "src/Illuminate/Collections/helpers.php", "src/Illuminate/Events/functions.php", "src/Illuminate/Filesystem/functions.php", @@ -2240,20 +2281,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-11-27T15:43:57+00:00" + "time": "2025-01-24T16:17:42+00:00" }, { "name": "laravel/jetstream", - "version": "v5.3.3", + "version": "v5.3.4", "source": { "type": "git", "url": "https://github.com/laravel/jetstream.git", - "reference": "16859ea11a0bbce631c19d95ca0e172322e52e30" + "reference": "d8d4d83a64d1035b05030e2e97230524b1fa8177" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/jetstream/zipball/16859ea11a0bbce631c19d95ca0e172322e52e30", - "reference": "16859ea11a0bbce631c19d95ca0e172322e52e30", + "url": "https://api.github.com/repos/laravel/jetstream/zipball/d8d4d83a64d1035b05030e2e97230524b1fa8177", + "reference": "d8d4d83a64d1035b05030e2e97230524b1fa8177", "shasum": "" }, "require": { @@ -2261,7 +2302,7 @@ "illuminate/console": "^11.0", "illuminate/support": "^11.0", "laravel/fortify": "^1.20", - "mobiledetect/mobiledetectlib": "^4.8", + "mobiledetect/mobiledetectlib": "^4.8.08", "php": "^8.2.0", "symfony/console": "^7.0" }, @@ -2307,20 +2348,20 @@ "issues": "https://github.com/laravel/jetstream/issues", "source": "https://github.com/laravel/jetstream" }, - "time": "2024-11-13T13:59:38+00:00" + "time": "2024-12-10T15:11:20+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.2", + "version": "v0.3.3", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "0e0535747c6b8d6d10adca8b68293cf4517abb0f" + "reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/0e0535747c6b8d6d10adca8b68293cf4517abb0f", - "reference": "0e0535747c6b8d6d10adca8b68293cf4517abb0f", + "url": "https://api.github.com/repos/laravel/prompts/zipball/749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", + "reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", "shasum": "" }, "require": { @@ -2364,22 +2405,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.2" + "source": "https://github.com/laravel/prompts/tree/v0.3.3" }, - "time": "2024-11-12T14:59:47+00:00" + "time": "2024-12-30T15:53:31+00:00" }, { "name": "laravel/sanctum", - "version": "v4.0.5", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "fe361b9a63407a228f884eb78d7217f680b50140" + "reference": "698064236a46df016e64a7eb059b1414e0b281df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/fe361b9a63407a228f884eb78d7217f680b50140", - "reference": "fe361b9a63407a228f884eb78d7217f680b50140", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/698064236a46df016e64a7eb059b1414e0b281df", + "reference": "698064236a46df016e64a7eb059b1414e0b281df", "shasum": "" }, "require": { @@ -2430,20 +2471,20 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2024-11-26T14:36:23+00:00" + "time": "2024-12-11T16:40:21+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "0d8d3d8086984996df86596a86dea60398093a81" + "reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/0d8d3d8086984996df86596a86dea60398093a81", - "reference": "0d8d3d8086984996df86596a86dea60398093a81", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/613b2d4998f85564d40497e05e89cb6d9bd1cbe8", + "reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8", "shasum": "" }, "require": { @@ -2491,20 +2532,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2024-11-19T01:38:44+00:00" + "time": "2024-12-16T15:26:28+00:00" }, { "name": "laravel/socialite", - "version": "v5.16.0", + "version": "v5.17.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf" + "reference": "77be8be7ee5099aed8ca7cfddc1bf6f9ab3fc159" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", - "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", + "url": "https://api.github.com/repos/laravel/socialite/zipball/77be8be7ee5099aed8ca7cfddc1bf6f9ab3fc159", + "reference": "77be8be7ee5099aed8ca7cfddc1bf6f9ab3fc159", "shasum": "" }, "require": { @@ -2514,7 +2555,7 @@ "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "league/oauth1-client": "^1.10.1", + "league/oauth1-client": "^1.11", "php": "^7.2|^8.0", "phpseclib/phpseclib": "^3.0" }, @@ -2526,16 +2567,16 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - }, "laravel": { - "providers": [ - "Laravel\\Socialite\\SocialiteServiceProvider" - ], "aliases": { "Socialite": "Laravel\\Socialite\\Facades\\Socialite" - } + }, + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" } }, "autoload": { @@ -2563,7 +2604,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2024-09-03T09:46:57+00:00" + "time": "2025-01-17T15:17:00+00:00" }, { "name": "laravel/tinker", @@ -2633,16 +2674,16 @@ }, { "name": "league/commonmark", - "version": "2.5.3", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "b650144166dfa7703e62a22e493b853b58d874b0" + "reference": "d990688c91cedfb69753ffc2512727ec646df2ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b650144166dfa7703e62a22e493b853b58d874b0", - "reference": "b650144166dfa7703e62a22e493b853b58d874b0", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad", + "reference": "d990688c91cedfb69753ffc2512727ec646df2ad", "shasum": "" }, "require": { @@ -2667,8 +2708,9 @@ "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 || ^7.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, @@ -2678,7 +2720,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.6-dev" + "dev-main": "2.7-dev" } }, "autoload": { @@ -2735,7 +2777,7 @@ "type": "tidelift" } ], - "time": "2024-08-16T11:46:16+00:00" + "time": "2024-12-29T14:10:59+00:00" }, { "name": "league/config", @@ -3064,16 +3106,16 @@ }, { "name": "league/oauth1-client", - "version": "v1.10.1", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth1-client.git", - "reference": "d6365b901b5c287dd41f143033315e2f777e1167" + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167", - "reference": "d6365b901b5c287dd41f143033315e2f777e1167", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", "shasum": "" }, "require": { @@ -3134,22 +3176,196 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth1-client/issues", - "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1" + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" }, - "time": "2022-04-15T14:02:14+00:00" + "time": "2024-12-10T19:59:05+00:00" }, { - "name": "livewire/livewire", - "version": "v3.5.17", + "name": "league/uri", + "version": "7.5.1", "source": { "type": "git", - "url": "https://github.com/livewire/livewire.git", - "reference": "7bbf80d93db9b866776bf957ca6229364bca8d87" + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/7bbf80d93db9b866776bf957ca6229364bca8d87", - "reference": "7bbf80d93db9b866776bf957ca6229364bca8d87", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, + { + "name": "livewire/livewire", + "version": "v3.5.18", + "source": { + "type": "git", + "url": "https://github.com/livewire/livewire.git", + "reference": "62f0fa6b340a467c25baa590a567d9a134b357da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/livewire/zipball/62f0fa6b340a467c25baa590a567d9a134b357da", + "reference": "62f0fa6b340a467c25baa590a567d9a134b357da", "shasum": "" }, "require": { @@ -3204,7 +3420,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.5.17" + "source": "https://github.com/livewire/livewire/tree/v3.5.18" }, "funding": [ { @@ -3212,7 +3428,7 @@ "type": "github" } ], - "time": "2024-12-06T13:41:21+00:00" + "time": "2024-12-23T15:05:02+00:00" }, { "name": "livewire/volt", @@ -3242,13 +3458,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - }, "laravel": { "providers": [ "Livewire\\Volt\\VoltServiceProvider" ] + }, + "branch-alias": { + "dev-master": "1.x-dev" } }, "autoload": { @@ -3288,16 +3504,16 @@ }, { "name": "maatwebsite/excel", - "version": "3.1.61", + "version": "3.1.62", "source": { "type": "git", "url": "https://github.com/SpartnerNL/Laravel-Excel.git", - "reference": "62616317c5ec07e885c5d7f6b537f57a7239c2ff" + "reference": "decfb9140161fcc117571e47e35ddf27983189ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/62616317c5ec07e885c5d7f6b537f57a7239c2ff", - "reference": "62616317c5ec07e885c5d7f6b537f57a7239c2ff", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/decfb9140161fcc117571e47e35ddf27983189ce", + "reference": "decfb9140161fcc117571e47e35ddf27983189ce", "shasum": "" }, "require": { @@ -3305,7 +3521,7 @@ "ext-json": "*", "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0", "php": "^7.0||^8.0", - "phpoffice/phpspreadsheet": "^1.29.4", + "phpoffice/phpspreadsheet": "^1.29.7", "psr/simple-cache": "^1.0||^2.0||^3.0" }, "require-dev": { @@ -3316,12 +3532,12 @@ "type": "library", "extra": { "laravel": { - "providers": [ - "Maatwebsite\\Excel\\ExcelServiceProvider" - ], "aliases": { "Excel": "Maatwebsite\\Excel\\Facades\\Excel" - } + }, + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ] } }, "autoload": { @@ -3353,7 +3569,7 @@ ], "support": { "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", - "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.61" + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.62" }, "funding": [ { @@ -3365,7 +3581,7 @@ "type": "github" } ], - "time": "2024-11-25T18:41:59+00:00" + "time": "2025-01-04T12:14:36+00:00" }, { "name": "maennchen/zipstream-php", @@ -3553,28 +3769,29 @@ }, { "name": "mobiledetect/mobiledetectlib", - "version": "4.8.06", + "version": "4.8.09", "source": { "type": "git", "url": "https://github.com/serbanghita/Mobile-Detect.git", - "reference": "af088b54cecc13b3264edca7da93a89ba7aa2d9e" + "reference": "a06fe2e546a06bb8c2639d6823d5250b2efb3209" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/af088b54cecc13b3264edca7da93a89ba7aa2d9e", - "reference": "af088b54cecc13b3264edca7da93a89ba7aa2d9e", + "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/a06fe2e546a06bb8c2639d6823d5250b2efb3209", + "reference": "a06fe2e546a06bb8c2639d6823d5250b2efb3209", "shasum": "" }, "require": { "php": ">=8.0", - "psr/simple-cache": "^2 || ^3" + "psr/cache": "^3.0", + "psr/simple-cache": "^3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.35.1", + "friendsofphp/php-cs-fixer": "^v3.65.0", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.6", - "squizlabs/php_codesniffer": "^3.7" + "phpstan/phpstan": "^1.12.x-dev", + "phpunit/phpunit": "^9.6.18", + "squizlabs/php_codesniffer": "^3.11.1" }, "type": "library", "autoload": { @@ -3605,7 +3822,7 @@ ], "support": { "issues": "https://github.com/serbanghita/Mobile-Detect/issues", - "source": "https://github.com/serbanghita/Mobile-Detect/tree/4.8.06" + "source": "https://github.com/serbanghita/Mobile-Detect/tree/4.8.09" }, "funding": [ { @@ -3613,7 +3830,7 @@ "type": "github" } ], - "time": "2024-03-01T22:28:42+00:00" + "time": "2024-12-10T15:32:06+00:00" }, { "name": "monolog/monolog", @@ -3786,16 +4003,16 @@ }, { "name": "nesbot/carbon", - "version": "3.8.2", + "version": "3.8.4", "source": { "type": "git", - "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947" + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "129700ed449b1f02d70272d2ac802357c8c30c58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e1268cdbc486d97ce23fef2c666dc3c6b6de9947", - "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/129700ed449b1f02d70272d2ac802357c8c30c58", + "reference": "129700ed449b1f02d70272d2ac802357c8c30c58", "shasum": "" }, "require": { @@ -3827,10 +4044,6 @@ ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.x-dev", - "dev-2.x": "2.x-dev" - }, "laravel": { "providers": [ "Carbon\\Laravel\\ServiceProvider" @@ -3840,6 +4053,10 @@ "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" } }, "autoload": { @@ -3888,7 +4105,7 @@ "type": "tidelift" } ], - "time": "2024-11-07T17:46:48+00:00" + "time": "2024-12-27T09:25:35+00:00" }, { "name": "nette/schema", @@ -4040,16 +4257,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -4092,9 +4309,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "nunomaduro/termwind", @@ -4528,16 +4745,16 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "1.29.5", + "version": "1.29.8", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "727cb704d5479fe4ddc291497ee471c4ec08f1b6" + "reference": "089ffdfc04b5fcf25a3503d81a4e589f247e20e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/727cb704d5479fe4ddc291497ee471c4ec08f1b6", - "reference": "727cb704d5479fe4ddc291497ee471c4ec08f1b6", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/089ffdfc04b5fcf25a3503d81a4e589f247e20e3", + "reference": "089ffdfc04b5fcf25a3503d81a4e589f247e20e3", "shasum": "" }, "require": { @@ -4565,7 +4782,7 @@ }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", - "dompdf/dompdf": "^1.0 || ^2.0", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", "friendsofphp/php-cs-fixer": "^3.2", "mitoteam/jpgraph": "^10.3", "mpdf/mpdf": "^8.1.1", @@ -4627,9 +4844,9 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.5" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.8" }, - "time": "2024-11-22T05:57:44+00:00" + "time": "2025-01-12T03:16:27+00:00" }, { "name": "phpoption/phpoption", @@ -4708,16 +4925,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.42", + "version": "3.0.43", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98" + "reference": "709ec107af3cb2f385b9617be72af8cf62441d02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98", - "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02", + "reference": "709ec107af3cb2f385b9617be72af8cf62441d02", "shasum": "" }, "require": { @@ -4798,7 +5015,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.43" }, "funding": [ { @@ -4814,7 +5031,7 @@ "type": "tidelift" } ], - "time": "2024-09-16T03:06:04+00:00" + "time": "2024-12-14T21:12:59+00:00" }, { "name": "pragmarx/google2fa", @@ -4929,6 +5146,55 @@ ], "time": "2024-11-21T20:00:02+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -5343,16 +5609,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.5", + "version": "v0.12.7", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "36a03ff27986682c22985e56aabaf840dd173cb5" + "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/36a03ff27986682c22985e56aabaf840dd173cb5", - "reference": "36a03ff27986682c22985e56aabaf840dd173cb5", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", + "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", "shasum": "" }, "require": { @@ -5416,9 +5682,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.5" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.7" }, - "time": "2024-11-29T06:14:30+00:00" + "time": "2024-12-10T01:58:33+00:00" }, { "name": "ralouphie/getallheaders", @@ -5647,16 +5913,16 @@ }, { "name": "robsontenorio/mary", - "version": "1.41.4", + "version": "1.41.5", "source": { "type": "git", "url": "https://github.com/robsontenorio/mary.git", - "reference": "d88af3a6e52c39e84455927111449035f1ede92d" + "reference": "a92f65bf3b8565ba93338955f3bc5418ed7f8deb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robsontenorio/mary/zipball/d88af3a6e52c39e84455927111449035f1ede92d", - "reference": "d88af3a6e52c39e84455927111449035f1ede92d", + "url": "https://api.github.com/repos/robsontenorio/mary/zipball/a92f65bf3b8565ba93338955f3bc5418ed7f8deb", + "reference": "a92f65bf3b8565ba93338955f3bc5418ed7f8deb", "shasum": "" }, "require": { @@ -5722,7 +5988,7 @@ ], "support": { "issues": "https://github.com/robsontenorio/mary/issues", - "source": "https://github.com/robsontenorio/mary/tree/1.41.4" + "source": "https://github.com/robsontenorio/mary/tree/1.41.5" }, "funding": [ { @@ -5730,20 +5996,20 @@ "type": "github" } ], - "time": "2024-11-12T14:30:56+00:00" + "time": "2025-01-01T14:48:44+00:00" }, { "name": "scheb/yahoo-finance-api", - "version": "v4.11.1", + "version": "v4.11.2", "source": { "type": "git", "url": "https://github.com/scheb/yahoo-finance-api.git", - "reference": "acb9ebacc057a13d69ca2b02fc88096369a5848a" + "reference": "6955d2db9b0c7e0a158dcb6abaca27946affdd18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/yahoo-finance-api/zipball/acb9ebacc057a13d69ca2b02fc88096369a5848a", - "reference": "acb9ebacc057a13d69ca2b02fc88096369a5848a", + "url": "https://api.github.com/repos/scheb/yahoo-finance-api/zipball/6955d2db9b0c7e0a158dcb6abaca27946affdd18", + "reference": "6955d2db9b0c7e0a158dcb6abaca27946affdd18", "shasum": "" }, "require": { @@ -5783,22 +6049,22 @@ ], "support": { "issues": "https://github.com/scheb/yahoo-finance-api/issues", - "source": "https://github.com/scheb/yahoo-finance-api/tree/v4.11.1" + "source": "https://github.com/scheb/yahoo-finance-api/tree/v4.11.2" }, - "time": "2024-10-16T17:35:40+00:00" + "time": "2024-12-29T18:20:46+00:00" }, { "name": "spatie/laravel-package-tools", - "version": "1.16.6", + "version": "1.18.3", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "1f26942dc1e5c49eacfced34fdbc29ed234bd7b3" + "reference": "ba67eee37d86ed775dab7dad58a7cbaf9a6cfe78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1f26942dc1e5c49eacfced34fdbc29ed234bd7b3", - "reference": "1f26942dc1e5c49eacfced34fdbc29ed234bd7b3", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ba67eee37d86ed775dab7dad58a7cbaf9a6cfe78", + "reference": "ba67eee37d86ed775dab7dad58a7cbaf9a6cfe78", "shasum": "" }, "require": { @@ -5807,10 +6073,10 @@ }, "require-dev": { "mockery/mockery": "^1.5", - "orchestra/testbench": "^7.7|^8.0", - "pestphp/pest": "^1.22", - "phpunit/phpunit": "^9.5.24", - "spatie/pest-plugin-test-time": "^1.1" + "orchestra/testbench": "^7.7|^8.0|^9.0", + "pestphp/pest": "^1.22|^2", + "phpunit/phpunit": "^9.5.24|^10.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" }, "type": "library", "autoload": { @@ -5837,7 +6103,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.6" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.18.3" }, "funding": [ { @@ -5845,7 +6111,7 @@ "type": "github" } ], - "time": "2024-11-18T15:02:02+00:00" + "time": "2025-01-22T08:51:18+00:00" }, { "name": "staudenmeir/eloquent-has-many-deep", @@ -6031,16 +6297,16 @@ }, { "name": "symfony/console", - "version": "v7.2.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf" + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", + "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", "shasum": "" }, "require": { @@ -6104,7 +6370,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.0" + "source": "https://github.com/symfony/console/tree/v7.2.1" }, "funding": [ { @@ -6120,7 +6386,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2024-12-11T03:49:26+00:00" }, { "name": "symfony/css-selector", @@ -6206,12 +6472,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -6256,16 +6522,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.2.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "672b3dd1ef8b87119b446d67c58c106c43f965fe" + "reference": "6150b89186573046167796fa5f3f76601d5145f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/672b3dd1ef8b87119b446d67c58c106c43f965fe", - "reference": "672b3dd1ef8b87119b446d67c58c106c43f965fe", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/6150b89186573046167796fa5f3f76601d5145f8", + "reference": "6150b89186573046167796fa5f3f76601d5145f8", "shasum": "" }, "require": { @@ -6311,7 +6577,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.2.0" + "source": "https://github.com/symfony/error-handler/tree/v7.2.1" }, "funding": [ { @@ -6327,7 +6593,7 @@ "type": "tidelift" } ], - "time": "2024-11-05T15:35:02+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/event-dispatcher", @@ -6429,12 +6695,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -6487,16 +6753,16 @@ }, { "name": "symfony/finder", - "version": "v7.2.0", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "6de263e5868b9a137602dd1e33e4d48bfae99c49" + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/6de263e5868b9a137602dd1e33e4d48bfae99c49", - "reference": "6de263e5868b9a137602dd1e33e4d48bfae99c49", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", "shasum": "" }, "require": { @@ -6531,7 +6797,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.2.0" + "source": "https://github.com/symfony/finder/tree/v7.2.2" }, "funding": [ { @@ -6547,20 +6813,20 @@ "type": "tidelift" } ], - "time": "2024-10-23T06:56:12+00:00" + "time": "2024-12-30T19:00:17+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.2.0", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e88a66c3997859532bc2ddd6dd8f35aba2711744" + "reference": "62d1a43796ca3fea3f83a8470dfe63a4af3bc588" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e88a66c3997859532bc2ddd6dd8f35aba2711744", - "reference": "e88a66c3997859532bc2ddd6dd8f35aba2711744", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/62d1a43796ca3fea3f83a8470dfe63a4af3bc588", + "reference": "62d1a43796ca3fea3f83a8470dfe63a4af3bc588", "shasum": "" }, "require": { @@ -6609,7 +6875,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.2.0" + "source": "https://github.com/symfony/http-foundation/tree/v7.2.2" }, "funding": [ { @@ -6625,20 +6891,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T18:58:46+00:00" + "time": "2024-12-30T19:00:17+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.2.0", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6b4722a25e0aed1ccb4914b9bcbd493cc4676b4d" + "reference": "3c432966bd8c7ec7429663105f5a02d7e75b4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6b4722a25e0aed1ccb4914b9bcbd493cc4676b4d", - "reference": "6b4722a25e0aed1ccb4914b9bcbd493cc4676b4d", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3c432966bd8c7ec7429663105f5a02d7e75b4306", + "reference": "3c432966bd8c7ec7429663105f5a02d7e75b4306", "shasum": "" }, "require": { @@ -6723,7 +6989,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.2.0" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.2" }, "funding": [ { @@ -6739,7 +7005,7 @@ "type": "tidelift" } ], - "time": "2024-11-29T08:42:40+00:00" + "time": "2024-12-31T14:59:40+00:00" }, { "name": "symfony/mailer", @@ -6823,16 +7089,16 @@ }, { "name": "symfony/mime", - "version": "v7.2.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d" + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/cc84a4b81f62158c3846ac7ff10f696aae2b524d", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d", + "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283", + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283", "shasum": "" }, "require": { @@ -6887,7 +7153,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.0" + "source": "https://github.com/symfony/mime/tree/v7.2.1" }, "funding": [ { @@ -6903,7 +7169,7 @@ "type": "tidelift" } ], - "time": "2024-11-23T09:19:39+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6931,8 +7197,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -7007,8 +7273,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -7086,8 +7352,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -7168,8 +7434,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -7252,8 +7518,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -7326,8 +7592,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -7406,8 +7672,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -7488,8 +7754,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -7707,12 +7973,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -7855,16 +8121,16 @@ }, { "name": "symfony/translation", - "version": "v7.2.0", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5" + "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/dc89e16b44048ceecc879054e5b7f38326ab6cc5", - "reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5", + "url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923", + "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923", "shasum": "" }, "require": { @@ -7930,7 +8196,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.2.0" + "source": "https://github.com/symfony/translation/tree/v7.2.2" }, "funding": [ { @@ -7946,7 +8212,7 @@ "type": "tidelift" } ], - "time": "2024-11-12T20:47:56+00:00" + "time": "2024-12-07T08:18:10+00:00" }, { "name": "symfony/translation-contracts", @@ -7967,12 +8233,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -8185,31 +8451,33 @@ }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.2.7", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb" + "reference": "0d72ac1c00084279c1816675284073c5a337c20d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/83ee6f38df0a63106a9e4536e3060458b74ccedb", - "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", - "php": "^5.5 || ^7.0 || ^8.0", - "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5 || ^8.5.21 || ^9.5.10" + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -8232,9 +8500,9 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.2.7" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" }, - "time": "2023-12-08T13:03:43+00:00" + "time": "2024-12-21T16:25:41+00:00" }, { "name": "tschucki/alphavantage-laravel", @@ -8720,16 +8988,16 @@ }, { "name": "laravel/pint", - "version": "v1.18.3", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "cef51821608239040ab841ad6e1c6ae502ae3026" + "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026", - "reference": "cef51821608239040ab841ad6e1c6ae502ae3026", + "url": "https://api.github.com/repos/laravel/pint/zipball/53072e8ea22213a7ed168a8a15b96fbb8b82d44b", + "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b", "shasum": "" }, "require": { @@ -8740,10 +9008,10 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.65.0", - "illuminate/view": "^10.48.24", - "larastan/larastan": "^2.9.11", - "laravel-zero/framework": "^10.4.0", + "friendsofphp/php-cs-fixer": "^3.66.0", + "illuminate/view": "^10.48.25", + "larastan/larastan": "^2.9.12", + "laravel-zero/framework": "^10.48.25", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.17.0", "pestphp/pest": "^2.36.0" @@ -8782,20 +9050,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-11-26T15:34:00+00:00" + "time": "2025-01-14T16:20:53+00:00" }, { "name": "laravel/sail", - "version": "v1.39.1", + "version": "v1.40.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "1a3c7291bc88de983b66688919a4d298d68ddec7" + "reference": "237e70656d8eface4839de51d101284bd5d0cf71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/1a3c7291bc88de983b66688919a4d298d68ddec7", - "reference": "1a3c7291bc88de983b66688919a4d298d68ddec7", + "url": "https://api.github.com/repos/laravel/sail/zipball/237e70656d8eface4839de51d101284bd5d0cf71", + "reference": "237e70656d8eface4839de51d101284bd5d0cf71", "shasum": "" }, "require": { @@ -8845,7 +9113,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2024-11-27T15:42:28+00:00" + "time": "2025-01-13T16:57:11+00:00" }, { "name": "mockery/mockery", @@ -8992,37 +9260,37 @@ }, { "name": "nunomaduro/collision", - "version": "v8.5.0", + "version": "v8.6.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "f5c101b929c958e849a633283adff296ed5f38f5" + "reference": "86f003c132143d5a2ab214e19933946409e0cae7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/f5c101b929c958e849a633283adff296ed5f38f5", - "reference": "f5c101b929c958e849a633283adff296ed5f38f5", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/86f003c132143d5a2ab214e19933946409e0cae7", + "reference": "86f003c132143d5a2ab214e19933946409e0cae7", "shasum": "" }, "require": { "filp/whoops": "^2.16.0", - "nunomaduro/termwind": "^2.1.0", + "nunomaduro/termwind": "^2.3.0", "php": "^8.2.0", - "symfony/console": "^7.1.5" + "symfony/console": "^7.2.1" }, "conflict": { - "laravel/framework": "<11.0.0 || >=12.0.0", - "phpunit/phpunit": "<10.5.1 || >=12.0.0" + "laravel/framework": "<11.39.1 || >=13.0.0", + "phpunit/phpunit": "<11.5.3 || >=12.0.0" }, "require-dev": { - "larastan/larastan": "^2.9.8", - "laravel/framework": "^11.28.0", - "laravel/pint": "^1.18.1", - "laravel/sail": "^1.36.0", - "laravel/sanctum": "^4.0.3", + "larastan/larastan": "^2.9.12", + "laravel/framework": "^11.39.1", + "laravel/pint": "^1.20.0", + "laravel/sail": "^1.40.0", + "laravel/sanctum": "^4.0.7", "laravel/tinker": "^2.10.0", - "orchestra/testbench-core": "^9.5.3", - "pestphp/pest": "^2.36.0 || ^3.4.0", + "orchestra/testbench-core": "^9.9.2", + "pestphp/pest": "^3.7.3", "sebastian/environment": "^6.1.0 || ^7.2.0" }, "type": "library", @@ -9060,6 +9328,7 @@ "cli", "command-line", "console", + "dev", "error", "handling", "laravel", @@ -9085,7 +9354,7 @@ "type": "patreon" } ], - "time": "2024-10-15T16:06:32+00:00" + "time": "2025-01-23T13:41:43+00:00" }, { "name": "phar-io/manifest", @@ -9207,16 +9476,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.7", + "version": "11.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f7f08030e8811582cc459871d28d6f5a1a4d35ca" + "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f7f08030e8811582cc459871d28d6f5a1a4d35ca", - "reference": "f7f08030e8811582cc459871d28d6f5a1a4d35ca", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", + "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", "shasum": "" }, "require": { @@ -9235,7 +9504,7 @@ "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^11.4.1" + "phpunit/phpunit": "^11.5.0" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -9273,7 +9542,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.7" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8" }, "funding": [ { @@ -9281,7 +9550,7 @@ "type": "github" } ], - "time": "2024-10-09T06:21:38+00:00" + "time": "2024-12-11T12:34:27+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9530,16 +9799,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.0", + "version": "11.5.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0569902506a6c0878930b87ea79ec3b50ea563f7" + "reference": "30e319e578a7b5da3543073e30002bf82042f701" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0569902506a6c0878930b87ea79ec3b50ea563f7", - "reference": "0569902506a6c0878930b87ea79ec3b50ea563f7", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/30e319e578a7b5da3543073e30002bf82042f701", + "reference": "30e319e578a7b5da3543073e30002bf82042f701", "shasum": "" }, "require": { @@ -9553,14 +9822,14 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.7", + "phpunit/php-code-coverage": "^11.0.8", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.1", - "sebastian/comparator": "^6.2.1", + "sebastian/code-unit": "^3.0.2", + "sebastian/comparator": "^6.3.0", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.0", "sebastian/exporter": "^6.3.0", @@ -9611,7 +9880,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.0" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.3" }, "funding": [ { @@ -9627,7 +9896,7 @@ "type": "tidelift" } ], - "time": "2024-12-06T05:57:38+00:00" + "time": "2025-01-13T09:36:00+00:00" }, { "name": "sebastian/cli-parser", @@ -9688,23 +9957,23 @@ }, { "name": "sebastian/code-unit", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "6bb7d09d6623567178cf54126afa9c2310114268" + "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/6bb7d09d6623567178cf54126afa9c2310114268", - "reference": "6bb7d09d6623567178cf54126afa9c2310114268", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.5" }, "type": "library", "extra": { @@ -9733,7 +10002,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.1" + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" }, "funding": [ { @@ -9741,7 +10010,7 @@ "type": "github" } ], - "time": "2024-07-03T04:44:28+00:00" + "time": "2024-12-12T09:59:06+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -9801,16 +10070,16 @@ }, { "name": "sebastian/comparator", - "version": "6.2.1", + "version": "6.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739" + "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/43d129d6a0f81c78bee378b46688293eb7ea3739", - "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d4e47a769525c4dd38cea90e5dcd435ddbbc7115", + "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115", "shasum": "" }, "require": { @@ -9823,6 +10092,9 @@ "require-dev": { "phpunit/phpunit": "^11.4" }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, "type": "library", "extra": { "branch-alias": { @@ -9866,7 +10138,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.2.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.0" }, "funding": [ { @@ -9874,7 +10146,7 @@ "type": "github" } ], - "time": "2024-10-31T05:30:08+00:00" + "time": "2025-01-06T10:28:19+00:00" }, { "name": "sebastian/complexity", @@ -10730,7 +11002,8 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "finnhub/client": 20 + "finnhub/client": 20, + "hackeresq/filter-models": 20 }, "prefer-stable": true, "prefer-lowest": false, From 62dcae48bb9e5f18e1596323d5cb481dd8054105 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Fri, 24 Jan 2025 22:45:28 -0600 Subject: [PATCH 07/12] wip --- .../ApiControllers/PortfolioController.php | 37 +++++++++++++++++-- app/Http/Requests/StorePortfolioRequest.php | 23 ++++++++++++ app/Http/Requests/UpdatePortfolioRequest.php | 23 ++++++++++++ app/Http/Resources/PortfolioResource.php | 1 + composer.lock | 8 ++-- routes/api.php | 2 +- 6 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 app/Http/Requests/StorePortfolioRequest.php create mode 100644 app/Http/Requests/UpdatePortfolioRequest.php diff --git a/app/Http/ApiControllers/PortfolioController.php b/app/Http/ApiControllers/PortfolioController.php index 92af14e..54e83ec 100644 --- a/app/Http/ApiControllers/PortfolioController.php +++ b/app/Http/ApiControllers/PortfolioController.php @@ -5,22 +5,53 @@ declare(strict_types=1); namespace App\Http\ApiControllers; use App\Models\Portfolio; -use Illuminate\Http\Request; +use Illuminate\Support\Facades\Gate; use HackerEsq\FilterModels\FilterModels; use App\Http\Resources\PortfolioResource; +use App\Http\Requests\StorePortfolioRequest; +use App\Http\Requests\UpdatePortfolioRequest; use App\Http\ApiControllers\Controller as ApiController; class PortfolioController extends ApiController { public function index(FilterModels $filters) { - $filters->setQuery(Portfolio::query()); $filters->setScopes(['myPortfolios']); $filters->setEagerRelations(['users', 'transactions', 'holdings']); - $filters->setFilterableRelations(['holdings' => 'symbol', 'transactions' => 'symbol']); + $filters->setFilterableRelations(['holdings.symbol']); $filters->setSearchableColumns(['title', 'notes']); return PortfolioResource::collection($filters->paginated()); } + + public function store(StorePortfolioRequest $request) + { + $portfolio = Portfolio::create($request->validated()); + + return PortfolioResource::make($portfolio); + } + + public function show(Portfolio $portfolio) + { + Gate::authorize('readOnly', $portfolio); + + return PortfolioResource::make($portfolio); + } + + public function update(UpdatePortfolioRequest $request, Portfolio $portfolio) + { + Gate::authorize('fullAccess', $portfolio); + + $portfolio->update($request->validated()); + + return PortfolioResource::make($portfolio); + } + + public function destroy(Portfolio $portfolio) + { + Gate::authorize('fullAccess', $portfolio); + + return response()->noContent(); + } } \ No newline at end of file diff --git a/app/Http/Requests/StorePortfolioRequest.php b/app/Http/Requests/StorePortfolioRequest.php new file mode 100644 index 0000000..86b0eef --- /dev/null +++ b/app/Http/Requests/StorePortfolioRequest.php @@ -0,0 +1,23 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => ['required', 'string', 'max:255'], + 'notes' => ['sometimes', 'nullable', 'string'], + 'wishlist' => ['sometimes', 'nullable', 'boolean'], + ]; + } +} diff --git a/app/Http/Requests/UpdatePortfolioRequest.php b/app/Http/Requests/UpdatePortfolioRequest.php new file mode 100644 index 0000000..6840a5a --- /dev/null +++ b/app/Http/Requests/UpdatePortfolioRequest.php @@ -0,0 +1,23 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => ['sometimes', 'string', 'max:255'], + 'notes' => ['sometimes', 'nullable', 'string'], + 'wishlist' => ['sometimes', 'nullable', 'boolean'], + ]; + } +} diff --git a/app/Http/Resources/PortfolioResource.php b/app/Http/Resources/PortfolioResource.php index 1bd9f06..19d3b8a 100644 --- a/app/Http/Resources/PortfolioResource.php +++ b/app/Http/Resources/PortfolioResource.php @@ -19,6 +19,7 @@ class PortfolioResource extends JsonResource return [ 'id' => $this->id, 'title' => $this->title, + 'notes' => $this->notes, 'wishlist' => $this->wishlist, 'owner' => UserResource::make($this->owner), 'transactions' => TransactionResource::collection($this->whenLoaded('transactions')), diff --git a/composer.lock b/composer.lock index c5bcd19..c7d511f 100644 --- a/composer.lock +++ b/composer.lock @@ -1733,12 +1733,12 @@ "source": { "type": "git", "url": "https://github.com/hackerESQ/filter-models.git", - "reference": "4ddba2483dc3987dcdf3f637be7edb8a2c96606c" + "reference": "565537120ea01bd73f49051ecde90d05e4127c6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hackerESQ/filter-models/zipball/4ddba2483dc3987dcdf3f637be7edb8a2c96606c", - "reference": "4ddba2483dc3987dcdf3f637be7edb8a2c96606c", + "url": "https://api.github.com/repos/hackerESQ/filter-models/zipball/565537120ea01bd73f49051ecde90d05e4127c6b", + "reference": "565537120ea01bd73f49051ecde90d05e4127c6b", "shasum": "" }, "require": { @@ -1764,7 +1764,7 @@ "source": "https://github.com/hackerESQ/filter-models/tree/main", "issues": "https://github.com/hackerESQ/filter-models/issues" }, - "time": "2025-01-25T01:22:46+00:00" + "time": "2025-01-25T04:44:58+00:00" }, { "name": "jfcherng/php-color-output", diff --git a/routes/api.php b/routes/api.php index f1c9894..6249abb 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,7 +13,7 @@ Route::middleware(['auth:sanctum'])->group(function () { Route::get('/me', [UserController::class, 'me']); // portfolio - Route::get('/portfolio', [PortfolioController::class, 'index']); + Route::apiResource('/portfolio', PortfolioController::class); // transaction Route::get('/transaction', [TransactionController::class, 'index']); From 169eabd80043bfb323597956b5c23b03ca742869 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Sat, 25 Jan 2025 16:57:35 -0600 Subject: [PATCH 08/12] fix: disable api for 1p packages --- composer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer.json b/composer.json index a02068b..90f7206 100644 --- a/composer.json +++ b/composer.json @@ -35,10 +35,12 @@ "repositories": [ { "type": "vcs", + "no-api": true, "url": "https://github.com/hackeresq/filter-models" }, { "type": "vcs", + "no-api": true, "url": "https://github.com/investbrainapp/finnhub-php" } ], From ea4602abc72271a023ead315358503037c2051ea Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Sun, 26 Jan 2025 22:56:05 -0600 Subject: [PATCH 09/12] wip --- .../ApiControllers/PortfolioController.php | 9 +- .../ApiControllers/TransactionController.php | 34 +++++ app/Http/Controllers/PortfolioController.php | 5 +- ...tfolioRequest.php => PortfolioRequest.php} | 13 +- ...olioRequest.php => TransactionRequest.php} | 13 +- composer.lock | 119 ++++++++++++++---- storage/app/.gitignore | 1 - 7 files changed, 157 insertions(+), 37 deletions(-) rename app/Http/Requests/{StorePortfolioRequest.php => PortfolioRequest.php} (63%) rename app/Http/Requests/{UpdatePortfolioRequest.php => TransactionRequest.php} (63%) diff --git a/app/Http/ApiControllers/PortfolioController.php b/app/Http/ApiControllers/PortfolioController.php index 54e83ec..dd487b0 100644 --- a/app/Http/ApiControllers/PortfolioController.php +++ b/app/Http/ApiControllers/PortfolioController.php @@ -8,8 +8,7 @@ use App\Models\Portfolio; use Illuminate\Support\Facades\Gate; use HackerEsq\FilterModels\FilterModels; use App\Http\Resources\PortfolioResource; -use App\Http\Requests\StorePortfolioRequest; -use App\Http\Requests\UpdatePortfolioRequest; +use App\Http\Requests\PortfolioRequest; use App\Http\ApiControllers\Controller as ApiController; class PortfolioController extends ApiController @@ -25,7 +24,7 @@ class PortfolioController extends ApiController return PortfolioResource::collection($filters->paginated()); } - public function store(StorePortfolioRequest $request) + public function store(PortfolioRequest $request) { $portfolio = Portfolio::create($request->validated()); @@ -39,7 +38,7 @@ class PortfolioController extends ApiController return PortfolioResource::make($portfolio); } - public function update(UpdatePortfolioRequest $request, Portfolio $portfolio) + public function update(PortfolioRequest $request, Portfolio $portfolio) { Gate::authorize('fullAccess', $portfolio); @@ -52,6 +51,8 @@ class PortfolioController extends ApiController { Gate::authorize('fullAccess', $portfolio); + $portfolio->delete(); + return response()->noContent(); } } \ No newline at end of file diff --git a/app/Http/ApiControllers/TransactionController.php b/app/Http/ApiControllers/TransactionController.php index 68b8597..3441df0 100644 --- a/app/Http/ApiControllers/TransactionController.php +++ b/app/Http/ApiControllers/TransactionController.php @@ -4,7 +4,9 @@ namespace App\Http\ApiControllers; use App\Models\Transaction; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Gate; use HackerEsq\FilterModels\FilterModels; +use App\Http\Requests\TransactionRequest; use App\Http\Resources\TransactionResource; use App\Http\ApiControllers\Controller as ApiController; @@ -19,4 +21,36 @@ class TransactionController extends ApiController return TransactionResource::collection($filters->paginated()); } + + public function store(TransactionRequest $request) + { + $transaction = Transaction::create($request->validated()); + + return TransactionResource::make($transaction); + } + + public function show(Transaction $transaction) + { + Gate::authorize('readOnly', $transaction); + + return TransactionResource::make($transaction); + } + + public function update(TransactionRequest $request, Transaction $transaction) + { + Gate::authorize('fullAccess', $transaction); + + $transaction->update($request->validated()); + + return TransactionResource::make($transaction); + } + + public function destroy(Transaction $transaction) + { + Gate::authorize('fullAccess', $transaction); + + $transaction->delete(); + + return response()->noContent(); + } } \ No newline at end of file diff --git a/app/Http/Controllers/PortfolioController.php b/app/Http/Controllers/PortfolioController.php index 3f04d12..efa9980 100644 --- a/app/Http/Controllers/PortfolioController.php +++ b/app/Http/Controllers/PortfolioController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use App\Models\Holding; use App\Models\Portfolio; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Gate; class PortfolioController extends Controller { @@ -22,9 +23,7 @@ class PortfolioController extends Controller */ public function show(Request $request, Portfolio $portfolio) { - if ($request->user()->cannot('readOnly', $portfolio)) { - abort(403); - } + Gate::authorize('readOnly', $portfolio); $portfolio->load(['transactions', 'holdings']); diff --git a/app/Http/Requests/StorePortfolioRequest.php b/app/Http/Requests/PortfolioRequest.php similarity index 63% rename from app/Http/Requests/StorePortfolioRequest.php rename to app/Http/Requests/PortfolioRequest.php index 86b0eef..772c6b2 100644 --- a/app/Http/Requests/StorePortfolioRequest.php +++ b/app/Http/Requests/PortfolioRequest.php @@ -4,7 +4,7 @@ namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; -class StorePortfolioRequest extends FormRequest +class PortfolioRequest extends FormRequest { /** @@ -14,10 +14,17 @@ class StorePortfolioRequest extends FormRequest */ public function rules(): array { - return [ - 'title' => ['required', 'string', 'max:255'], + + $rules = [ + 'title' => ['required', 'string', 'min:5', 'max:255'], 'notes' => ['sometimes', 'nullable', 'string'], 'wishlist' => ['sometimes', 'nullable', 'boolean'], ]; + + if (!is_null($this->portfolio)) { + $rules['title'][0] = 'sometimes'; + } + + return $rules; } } diff --git a/app/Http/Requests/UpdatePortfolioRequest.php b/app/Http/Requests/TransactionRequest.php similarity index 63% rename from app/Http/Requests/UpdatePortfolioRequest.php rename to app/Http/Requests/TransactionRequest.php index 6840a5a..2d7d43c 100644 --- a/app/Http/Requests/UpdatePortfolioRequest.php +++ b/app/Http/Requests/TransactionRequest.php @@ -4,7 +4,7 @@ namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; -class UpdatePortfolioRequest extends FormRequest +class TransactionRequest extends FormRequest { /** @@ -14,10 +14,17 @@ class UpdatePortfolioRequest extends FormRequest */ public function rules(): array { - return [ - 'title' => ['sometimes', 'string', 'max:255'], + + $rules = [ + 'title' => ['required', 'string', 'min:5', 'max:255'], 'notes' => ['sometimes', 'nullable', 'string'], 'wishlist' => ['sometimes', 'nullable', 'boolean'], ]; + + if (!is_null($this->portfolio)) { + $rules['title'][0] = 'sometimes'; + } + + return $rules; } } diff --git a/composer.lock b/composer.lock index c7d511f..053e659 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e8ee89ca51e5e67c7c812a39455132da", + "content-hash": "7b8a88dbb7545ee8284282a6dda2ab3f", "packages": [ { "name": "aws/aws-crt-php", @@ -491,6 +491,85 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, { "name": "composer/semver", "version": "3.4.3", @@ -1063,7 +1142,7 @@ "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/investbrainapp/finnhub-php.git", + "url": "https://github.com/investbrainapp/finnhub-php", "reference": "1f1b35a0c0a6a68f9a791e3ac5cdb6f44ff69d80" }, "dist": { @@ -1115,9 +1194,6 @@ "rest", "sdk" ], - "support": { - "source": "https://github.com/investbrainapp/finnhub-php/tree/master" - }, "time": "2024-09-13T01:29:18+00:00" }, { @@ -1732,12 +1808,12 @@ "version": "dev-main", "source": { "type": "git", - "url": "https://github.com/hackerESQ/filter-models.git", + "url": "https://github.com/hackeresq/filter-models", "reference": "565537120ea01bd73f49051ecde90d05e4127c6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hackerESQ/filter-models/zipball/565537120ea01bd73f49051ecde90d05e4127c6b", + "url": "https://api.github.com/repos/hackeresq/filter-models/zipball/565537120ea01bd73f49051ecde90d05e4127c6b", "reference": "565537120ea01bd73f49051ecde90d05e4127c6b", "shasum": "" }, @@ -1760,10 +1836,6 @@ } }, "description": "Simple package to filter your Laravel models with query parameters", - "support": { - "source": "https://github.com/hackerESQ/filter-models/tree/main", - "issues": "https://github.com/hackerESQ/filter-models/issues" - }, "time": "2025-01-25T04:44:58+00:00" }, { @@ -4745,19 +4817,20 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "1.29.8", + "version": "1.29.9", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "089ffdfc04b5fcf25a3503d81a4e589f247e20e3" + "reference": "ffb47b639649fc9c8a6fa67977a27b756592ed85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/089ffdfc04b5fcf25a3503d81a4e589f247e20e3", - "reference": "089ffdfc04b5fcf25a3503d81a4e589f247e20e3", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/ffb47b639649fc9c8a6fa67977a27b756592ed85", + "reference": "ffb47b639649fc9c8a6fa67977a27b756592ed85", "shasum": "" }, "require": { + "composer/pcre": "^3.3", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -4844,9 +4917,9 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.8" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.9" }, - "time": "2025-01-12T03:16:27+00:00" + "time": "2025-01-26T04:55:00+00:00" }, { "name": "phpoption/phpoption", @@ -8866,16 +8939,16 @@ }, { "name": "filp/whoops", - "version": "2.16.0", + "version": "2.17.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "befcdc0e5dce67252aa6322d82424be928214fa2" + "reference": "075bc0c26631110584175de6523ab3f1652eb28e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2", - "reference": "befcdc0e5dce67252aa6322d82424be928214fa2", + "url": "https://api.github.com/repos/filp/whoops/zipball/075bc0c26631110584175de6523ab3f1652eb28e", + "reference": "075bc0c26631110584175de6523ab3f1652eb28e", "shasum": "" }, "require": { @@ -8925,7 +8998,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.16.0" + "source": "https://github.com/filp/whoops/tree/2.17.0" }, "funding": [ { @@ -8933,7 +9006,7 @@ "type": "github" } ], - "time": "2024-09-25T12:00:00+00:00" + "time": "2025-01-25T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", diff --git a/storage/app/.gitignore b/storage/app/.gitignore index 5d91b1f..8f4803c 100644 --- a/storage/app/.gitignore +++ b/storage/app/.gitignore @@ -1,4 +1,3 @@ * !public/ !.gitignore -!market_data_seed.csv From 32bf256c84372155e87e102cca28e9f6f2aa659b Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Mon, 27 Jan 2025 16:47:25 -0600 Subject: [PATCH 10/12] chore: update deps --- composer.lock | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/composer.lock b/composer.lock index 053e659..4067694 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.338.2", + "version": "3.339.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "7a52364e053d74363f9976dfb4473bace5b7790e" + "reference": "41bcd4a555649d276c8fbc0bc1738e59fda2221d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7a52364e053d74363f9976dfb4473bace5b7790e", - "reference": "7a52364e053d74363f9976dfb4473bace5b7790e", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/41bcd4a555649d276c8fbc0bc1738e59fda2221d", + "reference": "41bcd4a555649d276c8fbc0bc1738e59fda2221d", "shasum": "" }, "require": { @@ -154,9 +154,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.338.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.339.0" }, - "time": "2025-01-24T19:09:22+00:00" + "time": "2025-01-27T19:25:50+00:00" }, { "name": "bacon/bacon-qr-code", @@ -3657,31 +3657,32 @@ }, { "name": "maennchen/zipstream-php", - "version": "3.1.1", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "6187e9cc4493da94b9b63eb2315821552015fca9" + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9", - "reference": "6187e9cc4493da94b9b63eb2315821552015fca9", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", "shasum": "" }, "require": { "ext-mbstring": "*", "ext-zlib": "*", - "php-64bit": "^8.1" + "php-64bit": "^8.2" }, "require-dev": { + "brianium/paratest": "^7.7", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.16", "guzzlehttp/guzzle": "^7.5", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.5", - "phpunit/phpunit": "^10.0", - "vimeo/psalm": "^5.0" + "phpunit/phpunit": "^11.0", + "vimeo/psalm": "^6.0" }, "suggest": { "guzzlehttp/psr7": "^2.4", @@ -3722,7 +3723,7 @@ ], "support": { "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1" + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" }, "funding": [ { @@ -3730,7 +3731,7 @@ "type": "github" } ], - "time": "2024-10-10T12:33:01+00:00" + "time": "2025-01-27T12:07:53+00:00" }, { "name": "markbaker/complex", From ea22c27710cc51c1e39c855fcd53f8dae7e6c69b Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Mon, 27 Jan 2025 20:04:03 -0600 Subject: [PATCH 11/12] wip --- app/Http/ApiControllers/HoldingController.php | 13 ++ .../ApiControllers/TransactionController.php | 10 +- app/Http/Requests/FormRequest.php | 14 ++ app/Http/Requests/PortfolioRequest.php | 2 +- app/Http/Requests/TransactionRequest.php | 47 +++- app/Models/Portfolio.php | 43 ++++ app/Rules/QuantityValidationRule.php | 13 +- database/factories/PortfolioFactory.php | 2 +- .../livewire/share-portfolio-form.blade.php | 22 +- routes/api.php | 10 +- tests/Api/PortfoliosTest.php | 202 ++++++++++++++++++ tests/Api/TransactionsTest.php | 200 +++++++++++++++++ tests/ApiTokenPermissionsTest.php | 72 +++---- 13 files changed, 569 insertions(+), 81 deletions(-) create mode 100644 app/Http/Requests/FormRequest.php create mode 100644 tests/Api/PortfoliosTest.php create mode 100644 tests/Api/TransactionsTest.php diff --git a/app/Http/ApiControllers/HoldingController.php b/app/Http/ApiControllers/HoldingController.php index 15330f0..ee345df 100644 --- a/app/Http/ApiControllers/HoldingController.php +++ b/app/Http/ApiControllers/HoldingController.php @@ -3,6 +3,7 @@ namespace App\Http\ApiControllers; use App\Models\Holding; +use App\Models\Portfolio; use Illuminate\Http\Request; use App\Http\Resources\HoldingResource; use HackerEsq\FilterModels\FilterModels; @@ -20,4 +21,16 @@ class HoldingController extends ApiController return HoldingResource::collection($filters->paginated()); } + + public function show(Portfolio $portfolio, string $symbol) + { + + // + } + + public function put(FilterModels $filters) + { + + // + } } \ No newline at end of file diff --git a/app/Http/ApiControllers/TransactionController.php b/app/Http/ApiControllers/TransactionController.php index 3441df0..64924bb 100644 --- a/app/Http/ApiControllers/TransactionController.php +++ b/app/Http/ApiControllers/TransactionController.php @@ -23,7 +23,9 @@ class TransactionController extends ApiController } public function store(TransactionRequest $request) - { + { + Gate::authorize('fullAccess', $request->portfolio); + $transaction = Transaction::create($request->validated()); return TransactionResource::make($transaction); @@ -31,14 +33,14 @@ class TransactionController extends ApiController public function show(Transaction $transaction) { - Gate::authorize('readOnly', $transaction); + Gate::authorize('readOnly', $transaction->portfolio); return TransactionResource::make($transaction); } public function update(TransactionRequest $request, Transaction $transaction) { - Gate::authorize('fullAccess', $transaction); + Gate::authorize('fullAccess', $transaction->portfolio); $transaction->update($request->validated()); @@ -47,7 +49,7 @@ class TransactionController extends ApiController public function destroy(Transaction $transaction) { - Gate::authorize('fullAccess', $transaction); + Gate::authorize('fullAccess', $transaction->portfolio); $transaction->delete(); diff --git a/app/Http/Requests/FormRequest.php b/app/Http/Requests/FormRequest.php new file mode 100644 index 0000000..d0f8334 --- /dev/null +++ b/app/Http/Requests/FormRequest.php @@ -0,0 +1,14 @@ +request->get($key) ?? $this->{$model}?->{$key}; + } +} \ No newline at end of file diff --git a/app/Http/Requests/PortfolioRequest.php b/app/Http/Requests/PortfolioRequest.php index 772c6b2..ab94472 100644 --- a/app/Http/Requests/PortfolioRequest.php +++ b/app/Http/Requests/PortfolioRequest.php @@ -2,7 +2,7 @@ namespace App\Http\Requests; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\FormRequest; class PortfolioRequest extends FormRequest { diff --git a/app/Http/Requests/TransactionRequest.php b/app/Http/Requests/TransactionRequest.php index 2d7d43c..c0412d3 100644 --- a/app/Http/Requests/TransactionRequest.php +++ b/app/Http/Requests/TransactionRequest.php @@ -2,11 +2,16 @@ namespace App\Http\Requests; -use Illuminate\Foundation\Http\FormRequest; +use App\Models\Portfolio; +use App\Http\Requests\FormRequest; +use App\Rules\SymbolValidationRule; +use App\Rules\QuantityValidationRule; class TransactionRequest extends FormRequest { + public ?Portfolio $portfolio; + /** * Get the validation rules that apply to the request. * @@ -14,15 +19,45 @@ class TransactionRequest extends FormRequest */ public function rules(): array { + $this->portfolio = Portfolio::findOrFail($this->requestOrModelValue('portfolio_id', 'transaction')); $rules = [ - 'title' => ['required', 'string', 'min:5', 'max:255'], - 'notes' => ['sometimes', 'nullable', 'string'], - 'wishlist' => ['sometimes', 'nullable', 'boolean'], + 'portfolio_id' => [], // validated by findOrFail() above + 'symbol' => ['required', 'string', new SymbolValidationRule], + 'transaction_type' => ['required', 'string', 'in:BUY,SELL'], + 'date' => ['required', 'date_format:Y-m-d', 'before_or_equal:' . now()->format('Y-m-d')], + 'quantity' => [ + 'required', + 'numeric', + 'min:0', + new QuantityValidationRule( + $this->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->portfolio)) { - $rules['title'][0] = 'sometimes'; + if (!is_null($this->transaction)) { + $rules['symbol'][0] = 'sometimes'; + $rules['transaction_type'][0] = 'sometimes'; + $rules['date'][0] = 'sometimes'; + $rules['quantity'][0] = 'sometimes'; + + if ( + $this->requestOrModelValue('transaction_type', 'transaction') == 'SELL' + && $this->requestOrModelValue('sale_price', 'transaction') == null + ) { + $rules['sale_price'][0] = 'required'; + } elseif ( + $this->requestOrModelValue('transaction_type', 'transaction') == 'BUY' + && $this->requestOrModelValue('cost_basis', 'transaction') == null + ) { + $rules['cost_basis'][0] = 'required'; + } } return $rules; diff --git a/app/Models/Portfolio.php b/app/Models/Portfolio.php index 0660745..2195fdf 100644 --- a/app/Models/Portfolio.php +++ b/app/Models/Portfolio.php @@ -5,11 +5,13 @@ namespace App\Models; use App\Models\AiChat; use Carbon\CarbonPeriod; 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; class Portfolio extends Model @@ -129,6 +131,7 @@ class Portfolio extends Model // save $portfolio->users()->sync($owner); + static::$owner_id = null; } } @@ -253,4 +256,44 @@ class Portfolio extends Model } 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 + ], [ + 'name' => Str::title(Str::before($email, '@')) + ]); + + $permissions[$user->id] = [ + 'full_access' => $fullAccess + ]; + + $sync = $this->users()->syncWithoutDetaching($permissions); + + if (!empty($sync['attached'])) { + + foreach($sync['attached'] as $newUserId) { + User::find($newUserId)->notify(new InvitedOnboardingNotification($this, auth()->user())); + }; + } + } + + /** + * Un-share a portfolio + * + * @param string $userId + * @return void + */ + public function unShare(string $userId): void + { + $this->users()->detach($userId); + } } diff --git a/app/Rules/QuantityValidationRule.php b/app/Rules/QuantityValidationRule.php index 0dea105..74027bc 100644 --- a/app/Rules/QuantityValidationRule.php +++ b/app/Rules/QuantityValidationRule.php @@ -13,10 +13,10 @@ class QuantityValidationRule implements ValidationRule * @return void */ public function __construct( - protected Portfolio $portfolio, - protected string $symbol, - protected string $transactionType, - protected string $date + protected ?Portfolio $portfolio, + protected ?string $symbol, + protected ?string $transactionType, + protected ?string $date ) { $this->portfolio = $portfolio; $this->symbol = $symbol; @@ -34,6 +34,11 @@ class QuantityValidationRule implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { + if (is_null($this->portfolio) || is_null($this->symbol) || is_null($this->transactionType) || is_null($this->date)) { + // + $fail(__('The quantity must not be greater than the available quantity.')); + } + if ($this->transactionType == 'SELL') { $purchase_qty = $this->portfolio->transactions() diff --git a/database/factories/PortfolioFactory.php b/database/factories/PortfolioFactory.php index 85ac7bd..175631d 100644 --- a/database/factories/PortfolioFactory.php +++ b/database/factories/PortfolioFactory.php @@ -17,7 +17,7 @@ class PortfolioFactory extends Factory public function definition(): array { return [ - 'title' => $this->faker->word, + 'title' => $this->faker->words(4, true), 'created_at' => now(), 'updated_at' => now(), ]; diff --git a/resources/views/livewire/share-portfolio-form.blade.php b/resources/views/livewire/share-portfolio-form.blade.php index f80b021..6f2a55c 100644 --- a/resources/views/livewire/share-portfolio-form.blade.php +++ b/resources/views/livewire/share-portfolio-form.blade.php @@ -7,7 +7,6 @@ use Livewire\Attributes\Rule; use Livewire\Volt\Component; use Illuminate\Support\Collection; use Mary\Traits\Toast; -use App\Notifications\InvitedOnboardingNotification; new class extends Component { @@ -75,7 +74,7 @@ new class extends Component { unset($this->permissions[$userId]); - $this->portfolio->users()->sync($this->permissions); + $this->portfolio->unShare($userId); $this->portfolio->refresh(); @@ -92,24 +91,7 @@ new class extends Component { $this->validate(); - $user = User::firstOrCreate([ - 'email' => $this->emailAddress - ], [ - 'name' => Str::title(Str::before($this->emailAddress, '@')) - ]); - - $this->permissions[$user->id] = [ - 'full_access' => $this->fullAccess - ]; - - $sync = $this->portfolio->users()->sync($this->permissions); - - if (!empty($sync['attached'])) { - - foreach($sync['attached'] as $newUserId) { - User::find($newUserId)->notify(new InvitedOnboardingNotification($this->portfolio, auth()->user())); - }; - } + $this->portfolio->share($this->emailAddress, $this->fullAccess); $this->success(__('Shared portfolio with user')); $this->portfolio->refresh(); diff --git a/routes/api.php b/routes/api.php index 6249abb..949c05b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -7,20 +7,20 @@ use App\Http\ApiControllers\PortfolioController; use App\Http\ApiControllers\MarketDataController; use App\Http\ApiControllers\TransactionController; -Route::middleware(['auth:sanctum'])->group(function () { +Route::middleware(['auth:sanctum'])->name('api.')->group(function () { // user - Route::get('/me', [UserController::class, 'me']); + Route::get('/me', [UserController::class, 'me'])->name('me'); // portfolio Route::apiResource('/portfolio', PortfolioController::class); // transaction - Route::get('/transaction', [TransactionController::class, 'index']); + Route::apiResource('/transaction', TransactionController::class); // holding - Route::get('/holding', [HoldingController::class, 'index']); + Route::get('/holding', [HoldingController::class, 'index'])->name('holding.index'); // market data - Route::get('/market-data/{symbol}', [MarketDataController::class, 'show']); + Route::get('/market-data/{symbol}', [MarketDataController::class, 'show'])->name('market-data.show'); }); \ No newline at end of file diff --git a/tests/Api/PortfoliosTest.php b/tests/Api/PortfoliosTest.php new file mode 100644 index 0000000..44dfd5b --- /dev/null +++ b/tests/Api/PortfoliosTest.php @@ -0,0 +1,202 @@ +user = User::factory()->create(); + } + + public function test_can_list_own_portfolios_with_pagination() + { + $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'] + ]); + } + + public function test_cannot_list_others_portfolios() + { + // 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(); + $this->actingAs($user) + ->getJson(route('api.portfolio.index', ['page' => 1, 'itemsPerPage' => 5])) + ->assertOk() + ->assertJsonCount(1, 'data'); + } + + public function test_cannot_access_portfolios_when_unauthenticated() + { + $this->getJson(route('api.portfolio.index'))->assertUnauthorized(); + } + + 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']]); + } + + public function test_cannot_create_portfolio_without_required_fields() + { + $this->actingAs($this->user) + ->postJson(route('api.portfolio.store'), []) + ->assertUnprocessable() + ->assertJsonValidationErrors(['title']); + } + + public function test_can_show_a_portfolio() + { + $this->actingAs($this->user); + $portfolio = Portfolio::factory()->create(); + + $this->actingAs($this->user) + ->getJson(route('api.portfolio.show', $portfolio)) + ->assertOk() + ->assertJsonStructure(['id', 'title', 'owner']); + } + + public function test_cannot_show_nonexistent_portfolio() + { + $this->actingAs($this->user) + ->getJson(route('api.portfolio.show', ['portfolio' => 999])) + ->assertNotFound(); + } + + public function test_can_update_a_portfolio() + { + $updatedData = ['title' => 'Updated Portfolio Title']; + + $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); + } + + public function test_shared_user_can_update_portfolio() + { + // create portfolio + $this->actingAs($this->user); + $portfolio = Portfolio::factory()->create(); + + // share it + $otherUser = User::factory()->create(); + $portfolio->share($otherUser->email, true); + + // shared user tries to update it + $this->actingAs($otherUser) + ->putJson(route('api.portfolio.update', $portfolio), ['title' => 'A brand new updated title']) + ->assertOk() + ->assertJsonFragment([ + 'title' => 'A brand new updated title' + ]); + } + + public function test_removed_user_cannot_update_portfolio() + { + // create portfolio + $this->actingAs($this->user); + $portfolio = Portfolio::factory()->create(); + + // share it + $otherUser = User::factory()->create(); + $portfolio->share($otherUser->email, true); + + // unshare it + $otherUser = User::factory()->create(); + $portfolio->unShare($otherUser->id); + + // shared user tries to update it + $this->actingAs($otherUser) + ->putJson(route('api.portfolio.update', $portfolio), ['Title' => 'A brand new updated title']) + ->assertForbidden(); + } + + public function test_read_only_user_cannot_update_portfolio() + { + // create portfolio + $this->actingAs($this->user); + $portfolio = Portfolio::factory()->create(); + + // share it + $otherUser = User::factory()->create(); + $portfolio->share($otherUser->email, false); + + // shared user tries to update it + $this->actingAs($otherUser) + ->putJson(route('api.portfolio.update', $portfolio), ['Title' => 'A brand new updated title']) + ->assertForbidden(); + } + + public function test_cannot_update_portfolio_without_permission() + { + $this->actingAs($this->user); + $portfolio = Portfolio::factory()->create(); + + $otherUser = User::factory()->create(); + $this->actingAs($otherUser) + ->putJson(route('api.portfolio.update', $portfolio), ['title' => 'New Title']) + ->assertForbidden(); + } + + public function test_can_delete_a_portfolio() + { + $this->actingAs($this->user); + $portfolio = Portfolio::factory()->create(); + + $this->actingAs($this->user) + ->deleteJson(route('api.portfolio.destroy', $portfolio)) + ->assertNoContent(); + + $this->assertDatabaseMissing('portfolios', ['id' => $portfolio->id]); + } + + public function test_cannot_delete_portfolio_without_permission() + { + $this->actingAs($this->user); + $portfolio = Portfolio::factory()->create(); + + $otherUser = User::factory()->create(); + $this->actingAs($otherUser) + ->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 new file mode 100644 index 0000000..c0cfbba --- /dev/null +++ b/tests/Api/TransactionsTest.php @@ -0,0 +1,200 @@ +user = User::factory()->create(); + + // make portfolio + $this->portfolio = Portfolio::factory()->makeOne(); + $this->portfolio->setOwnerIdAttribute($this->user->id); + $this->portfolio->save(); + } + + public function test_can_list_transactions() + { + $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'] + ]); + } + + public function test_cannot_list_others_transactions() + { + // 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(); + $this->actingAs($user) + ->getJson(route('api.transaction.index', ['page' => 1, 'itemsPerPage' => 5])) + ->assertOk() + ->assertJsonCount(1, 'data'); + } + + public function test_cannot_access_transactions_when_unauthenticated() + { + $this->getJson(route('api.transaction.index'))->assertUnauthorized(); + } + + public function test_can_create_transaction() + { + $this->actingAs($this->user); + + $data = [ + 'symbol' => 'AAPL', + 'portfolio_id' => $this->portfolio->id, + 'transaction_type' => 'BUY', + 'quantity' => 10, + 'date' => now()->toDateString(), + 'cost_basis' => 150, + ]; + + $this->postJson(route('api.transaction.store'), $data) + ->assertCreated() + ->assertJsonStructure([ + 'id', + 'symbol', + 'portfolio_id', + 'transaction_type', + 'quantity', + 'date', + 'cost_basis', + 'sale_price' + ]); + } + + public function test_cannot_create_transaction_without_required_fields() + { + $this->actingAs($this->user) + ->postJson(route('api.transaction.store'), [ + 'portfolio_id' => $this->portfolio->id, + 'symbol' => null + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors(['symbol']); + } + + public function test_can_show_a_transaction() + { + $this->actingAs($this->user); + + $transaction = Transaction::factory()->create(); + + $this->getJson(route('api.transaction.show', $transaction)) + ->assertOk() + ->assertJsonFragment([ + 'id' => $transaction->id, + ]); + } + + public function test_cannot_show_nonexistent_transactions() + { + $this->actingAs($this->user) + ->getJson(route('api.transaction.show', ['transaction' => 999])) + ->assertNotFound(); + } + + public function test_can_update_a_transaction() + { + $this->actingAs($this->user); + + $transaction = Transaction::factory()->create(); + + $data = [ + 'symbol' => 'ZZZ', + 'transaction_type' => 'BUY', + 'cost_basis' => 200.19, + 'quantity' => 5 + ]; + + $this->actingAs($this->user) + ->putJson(route('api.transaction.update', $transaction), $data) + ->assertOk() + ->assertJsonFragment([ + 'symbol' => 'ZZZ', + 'transaction_type' => 'BUY', + 'cost_basis' => 200.19, + 'quantity' => 5, + ]); + } + + public function test_shared_user_can_update_transaction() + { + // create transaction (and portfolio) + $this->actingAs($this->user); + $transaction = Transaction::factory()->create(); + + // share it + $otherUser = User::factory()->create(); + $transaction->portfolio->share($otherUser->email, true); + + // shared user tries to update it + $this->actingAs($otherUser) + ->putJson(route('api.transaction.update', $transaction), ['symbol' => 'ZZZ']) + ->assertOk() + ->assertJsonFragment([ + 'symbol' => 'ZZZ' + ]); + } + + public function test_cannot_update_transaction_without_permission() + { + $this->actingAs($this->user); + $transaction = Transaction::factory()->create(); + + $otherUser = User::factory()->create(); + $this->actingAs($otherUser) + ->putJson(route('api.transaction.update', $transaction), ['symbol' => 'AAPL']) + ->assertForbidden(); + } + + public function test_can_delete_a_transaction() + { + $this->actingAs($this->user); + $transaction = Transaction::factory()->create(); + + $this->deleteJson(route('api.transaction.destroy', $transaction)) + ->assertNoContent(); + + $this->assertDatabaseMissing('transactions', ['id' => $transaction->id]); + } + + public function test_cannot_delete_transaction_without_permission() + { + $this->actingAs($this->user); + $transaction = Transaction::factory()->create(); + + $otherUser = User::factory()->create(); + $this->actingAs($otherUser) + ->deleteJson(route('api.transaction.destroy', $transaction)) + ->assertForbidden(); + } +} \ No newline at end of file diff --git a/tests/ApiTokenPermissionsTest.php b/tests/ApiTokenPermissionsTest.php index 70ac458..9afd10d 100644 --- a/tests/ApiTokenPermissionsTest.php +++ b/tests/ApiTokenPermissionsTest.php @@ -14,50 +14,45 @@ class ApiTokenPermissionsTest extends TestCase { use RefreshDatabase; - // public function test_api_tokens_can_be_deleted(): void - // { - // if (! Features::hasApiFeatures()) { - // $this->markTestSkipped('API support is not enabled.'); - // } + public function test_api_tokens_can_be_deleted(): void + { + if (! Features::hasApiFeatures()) { + $this->markTestSkipped('API support is not enabled.'); + } - // $this->actingAs($user = User::factory()->create()); + $this->actingAs($user = User::factory()->create()); - // $token = $user->tokens()->create([ - // 'name' => 'Test Token', - // 'token' => Str::random(40), - // 'abilities' => ['create', 'read'], - // ]); + $token = $user->tokens()->create([ + 'name' => 'Test Token', + 'token' => Str::random(40), + 'abilities' => [], + ]); - // Livewire::test(ApiTokenManager::class) - // ->set(['apiTokenIdBeingDeleted' => $token->id]) - // ->call('deleteApiToken'); + Livewire::test(ApiTokenManager::class) + ->set(['apiTokenIdBeingDeleted' => $token->id]) + ->call('deleteApiToken'); - // $this->assertCount(0, $user->fresh()->tokens); - // } + $this->assertCount(0, $user->fresh()->tokens); + } - // public function test_api_tokens_can_be_created(): void - // { - // if (! Features::hasApiFeatures()) { - // $this->markTestSkipped('API support is not enabled.'); - // } + public function test_api_tokens_can_be_created(): void + { + if (! Features::hasApiFeatures()) { + $this->markTestSkipped('API support is not enabled.'); + } - // $this->actingAs($user = User::factory()->create()); + $this->actingAs($user = User::factory()->create()); - // Livewire::test(ApiTokenManager::class) - // ->set(['createApiTokenForm' => [ - // 'name' => 'Test Token', - // 'permissions' => [ - // 'read', - // 'update', - // ], - // ]]) - // ->call('createApiToken'); + Livewire::test(ApiTokenManager::class) + ->set(['createApiTokenForm' => [ + 'name' => 'Test Token', + 'permissions' => [], + ]]) + ->call('createApiToken'); - // $this->assertCount(1, $user->fresh()->tokens); - // $this->assertEquals('Test Token', $user->fresh()->tokens->first()->name); - // $this->assertTrue($user->fresh()->tokens->first()->can('read')); - // $this->assertFalse($user->fresh()->tokens->first()->can('delete')); - // } + $this->assertCount(1, $user->fresh()->tokens); + $this->assertEquals('Test Token', $user->fresh()->tokens->first()->name); + } // public function test_api_token_permissions_can_be_updated(): void // { @@ -76,10 +71,7 @@ class ApiTokenPermissionsTest extends TestCase // Livewire::test(ApiTokenManager::class) // ->set(['managingPermissionsFor' => $token]) // ->set(['updateApiTokenForm' => [ - // 'permissions' => [ - // 'delete', - // 'missing-permission', - // ], + // 'permissions' => [], // ]]) // ->call('updateApiToken'); From 83d5ad213b22c1476be720b3e7de151aed166af0 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Mon, 27 Jan 2025 20:26:09 -0600 Subject: [PATCH 12/12] wip --- app/Http/ApiControllers/HoldingController.php | 18 ++- app/Http/Requests/HoldingRequest.php | 24 ++++ routes/api.php | 2 + tests/Api/HoldingsTest.php | 117 ++++++++++++++++++ 4 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 app/Http/Requests/HoldingRequest.php create mode 100644 tests/Api/HoldingsTest.php diff --git a/app/Http/ApiControllers/HoldingController.php b/app/Http/ApiControllers/HoldingController.php index ee345df..483fbc3 100644 --- a/app/Http/ApiControllers/HoldingController.php +++ b/app/Http/ApiControllers/HoldingController.php @@ -5,6 +5,8 @@ namespace App\Http\ApiControllers; use App\Models\Holding; use App\Models\Portfolio; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Gate; +use App\Http\Requests\HoldingRequest; use App\Http\Resources\HoldingResource; use HackerEsq\FilterModels\FilterModels; use App\Http\ApiControllers\Controller as ApiController; @@ -25,12 +27,22 @@ class HoldingController extends ApiController public function show(Portfolio $portfolio, string $symbol) { - // + Gate::authorize('readOnly', $portfolio); + + $holding = $portfolio->holdings()->symbol($symbol)->firstOrFail(); + + return HoldingResource::make($holding); } - public function put(FilterModels $filters) + public function update(HoldingRequest $request, Portfolio $portfolio, string $symbol) { - // + Gate::authorize('fullAccess', $portfolio); + + $holding = $portfolio->holdings()->symbol($symbol)->firstOrFail(); + + $holding->update($request->validated()); + + return HoldingResource::make($holding); } } \ No newline at end of file diff --git a/app/Http/Requests/HoldingRequest.php b/app/Http/Requests/HoldingRequest.php new file mode 100644 index 0000000..a75d82d --- /dev/null +++ b/app/Http/Requests/HoldingRequest.php @@ -0,0 +1,24 @@ +|string> + */ + public function rules(): array + { + + $rules = [ + 'reinvest_dividends' => ['sometimes', 'boolean'] + ]; + + return $rules; + } +} diff --git a/routes/api.php b/routes/api.php index 949c05b..32395b5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -20,6 +20,8 @@ Route::middleware(['auth:sanctum'])->name('api.')->group(function () { // holding Route::get('/holding', [HoldingController::class, 'index'])->name('holding.index'); + Route::get('/holding/{portfolio}/{symbol}', [HoldingController::class, 'show'])->name('holding.show')->scopeBindings(); + Route::put('/holding/{portfolio}/{symbol}', [HoldingController::class, 'update'])->name('holding.update')->scopeBindings(); // market data Route::get('/market-data/{symbol}', [MarketDataController::class, 'show'])->name('market-data.show'); diff --git a/tests/Api/HoldingsTest.php b/tests/Api/HoldingsTest.php new file mode 100644 index 0000000..ef1a24b --- /dev/null +++ b/tests/Api/HoldingsTest.php @@ -0,0 +1,117 @@ +user = User::factory()->create(); + } + + public function test_can_list_holdings() + { + $this->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'] + ]); + } + + public function test_cannot_list_others_holdings() + { + // 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(); + $this->actingAs($user) + ->getJson(route('api.holding.index', ['page' => 1, 'itemsPerPage' => 5])) + ->assertOk() + ->assertJsonCount(1, 'data'); + } + + public function test_cannot_access_holdings_when_unauthenticated() + { + $this->getJson(route('api.holding.index'))->assertUnauthorized(); + } + + public function test_can_show_a_holding() + { + $this->actingAs($this->user); + + $transaction = Transaction::factory()->create(); + + $holding = Holding::where(['portfolio_id' => $transaction->portfolio->id, 'symbol' => $transaction->symbol])->firstOrFail(); + + $this->getJson(route('api.holding.show', ['portfolio' => $transaction->portfolio_id, 'symbol' => $transaction->symbol])) + ->assertOk() + ->assertJsonFragment([ + 'id' => $holding->id, + ]); + } + + public function test_cannot_show_nonexistent_holdings() + { + $this->actingAs($this->user) + ->getJson(route('api.holding.show', ['portfolio' => 'abc-123-foo-BAR', 'symbol' => 'AAPL'])) + ->assertNotFound(); + } + + public function test_can_update_holding_options() + { + $this->actingAs($this->user); + $transaction = Transaction::factory()->create(); + + $data = [ + '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 + ]); + } + + public function test_cannot_update_holding_without_permission() + { + $this->actingAs($this->user); + $transaction = Transaction::factory()->create(); + + $data = [ + 'reinvest_dividends' => true + ]; + + $otherUser = User::factory()->create(); + $this->actingAs($otherUser) + ->putJson(route('api.holding.update', ['portfolio' => $transaction->portfolio_id, 'symbol' => $transaction->symbol]), $data) + ->assertForbidden(); + } + +} \ No newline at end of file