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