diff --git a/app/Models/Holding.php b/app/Models/Holding.php index ed7e350..90fcbb9 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -12,7 +12,7 @@ class Holding extends Model use HasFactory; use HasUuids; - protected $with = []; + protected $with = ['market_data']; /** * The attributes that are mass assignable. @@ -42,7 +42,7 @@ class Holding extends Model ]; /** - * get market data for holding + * Market data for holding * * @return void */ @@ -52,7 +52,7 @@ class Holding extends Model } /** - * get related transactions for holding + * Related transactions for holding * * @return void */ @@ -62,7 +62,7 @@ class Holding extends Model } /** - * get related dividends for holding + * Related dividends for holding * * @return void */ @@ -72,7 +72,7 @@ class Holding extends Model } /** - * get related portfolio for holding + * Related portfolio for holding * * @return void */ @@ -82,7 +82,7 @@ class Holding extends Model } /** - * get related splits for holding + * Related splits for holding * * @return void */ diff --git a/app/Models/Portfolio.php b/app/Models/Portfolio.php index cff312d..b05158a 100644 --- a/app/Models/Portfolio.php +++ b/app/Models/Portfolio.php @@ -11,58 +11,30 @@ class Portfolio extends Model use HasFactory; use HasUuids; - /** - * The attributes that are mass assignable. - * - * @var array - */ protected $fillable = [ 'title', 'notes', 'wishlist', ]; - /** - * - * @return void - */ protected static function boot() { parent::boot(); static::saved(function ($model) { + self::syncUsers($model); }); } - /** - * The attributes that should be hidden for arrays. - * - * @var array - */ protected $hidden = []; - /** - * The attributes that should be cast to native types. - * - * @var array - */ protected $casts = [ 'wishlist' => 'boolean' ]; - /** - * The relationships that should always be eagerly loaded. - * - * @var array - */ protected $with = ['users', 'transactions']; - /** - * The attributes that should be appended. - * - * @var array - */ protected $appends = ['owner_id']; public function users() @@ -77,7 +49,7 @@ class Portfolio extends Model public function transactions() { - return $this->hasMany(Transaction::class); + return $this->hasMany(Transaction::class)->orderBy('created_at', 'DESC'); } public function daily_change() diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 272303c..fcd8b52 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Models\MarketData; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -12,121 +13,57 @@ class Transaction extends Model use HasFactory; use HasUuids; - /** - * The attributes that are mass assignable. - * - * @var array - */ protected $fillable = [ 'symbol', 'date', - 'portfolio_id', 'transaction_type', 'quantity', 'cost_basis', - 'sale_price', - 'split' + 'sale_price' ]; - /** - * The attributes that should be hidden for arrays. - * - * @var array - */ protected $hidden = []; - /** - * The attributes that should be cast to native types. - * - * @var array - */ protected $casts = [ 'date' => 'datetime', - 'first_date' => 'datetime', - 'last_date' => 'datetime', 'split' => 'boolean', ]; - /** - * - * @return void - */ protected static function boot() { parent::boot(); static::saving(function ($transaction) { - // if sale, move cost basis to sale price if ($transaction->transaction_type == 'SELL') { - $transaction->cost_basis = $transaction->holding->average_cost_basis ?? $transaction->cost_basis; + $transaction->ensureCostBasisIsAddedToSale(); } }); static::saved(function ($transaction) { - // static::syncHolding($transaction); + $transaction->syncHolding(); }); static::deleted(function ($transaction) { - // static::syncHolding($transaction); + $transaction->syncHolding(); }); } - public static function syncHolding($transaction) { - // get the holding for a symbol and portfolio (or create one) - $holding = Holding::firstOrNew([ - 'portfolio_id' => $transaction->portfolio_id, - 'symbol' => $transaction->symbol - ], [ - 'portfolio_id' => $transaction->portfolio_id, - 'symbol' => $transaction->symbol, - 'quantity' => $transaction->quantity, - 'average_cost_basis' => $transaction->cost_basis, - 'total_cost_basis' => $transaction->quantity * $transaction->cost_basis, - ]); - - // pull existing transaction data - $query = self::where([ - 'portfolio_id' => $transaction->portfolio_id, - 'symbol' => $transaction->symbol, - ])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`') - ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`') - ->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `cost_basis`') - ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN ((sale_price - cost_basis) * quantity) ELSE 0 END) AS `realized_gains`') - ->first(); - - $total_quantity = $query->qty_purchases - $query->qty_sales; - $average_cost_basis = $query->qty_purchases > 0 - ? $query->cost_basis / $query->qty_purchases - : 0; - - // update holding - $holding->fill([ - 'quantity' => $total_quantity, - 'average_cost_basis' => $average_cost_basis, - 'total_cost_basis' => $total_quantity * $average_cost_basis, - 'realized_gain_loss_dollars' => $query->realized_gains, - ]); - - $holding->save(); - - // load market data while we're here - $transaction->refreshMarketData(); - - // sync dividends to holding - $transaction->syncDividendsToHolding(); - } - - public function setSymbolAttribute($value) + /** + * Ensure transaction symbol is always upper case + */ + protected function symbol(): Attribute { - $this->attributes['symbol'] = strtoupper($value); + return Attribute::make( + set: fn (string $value) => strtoupper($value) + ); } /** - * get market data for transaction + * Related market data for transaction * * @return void */ @@ -136,7 +73,7 @@ class Transaction extends Model } /** - * get portfolio for transaction + * Related portfolio * * @return void */ @@ -176,4 +113,73 @@ class Transaction extends Model { return Dividend::getDividendData($this->attributes['symbol']); } + + /** + * Writes average cost basis to a sale transaction + * + * @return Transaction + */ + public function ensureCostBasisIsAddedToSale() + { + $holding = Holding::firstOrNew([ + 'portfolio_id' => $this->portfolio_id, + 'symbol' => $this->symbol + ],[ + 'average_cost_basis' => null + ]); + + $this->cost_basis = $holding->average_cost_basis ?? 0; + + return $this; + } + + /** + * Syncs the holding related to this transaction + * + * @return void + */ + public function syncHolding() { + // get the holding for a symbol and portfolio (or create one) + $holding = Holding::firstOrNew([ + 'portfolio_id' => $this->portfolio_id, + 'symbol' => $this->symbol + ], [ + 'portfolio_id' => $this->portfolio_id, + 'symbol' => $this->symbol, + 'quantity' => $this->quantity, + 'average_cost_basis' => $this->cost_basis, + 'total_cost_basis' => $this->quantity * $this->cost_basis, + ]); + + // pull existing transaction data + $query = self::where([ + 'portfolio_id' => $this->portfolio_id, + 'symbol' => $this->symbol, + ])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`') + ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`') + ->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `cost_basis`') + ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN ((sale_price - cost_basis) * quantity) ELSE 0 END) AS `realized_gains`') + ->first(); + + $total_quantity = $query->qty_purchases - $query->qty_sales; + $average_cost_basis = $query->qty_purchases > 0 + ? $query->cost_basis / $query->qty_purchases + : 0; + + // update holding + $holding->fill([ + 'quantity' => $total_quantity, + 'average_cost_basis' => $average_cost_basis, + 'total_cost_basis' => $total_quantity * $average_cost_basis, + 'realized_gain_loss_dollars' => $query->realized_gains, + ]); + + $holding->save(); + + // load market data while we're here + // $this->refreshMarketData(); + + // sync dividends to holding + // $this->syncDividendsToHolding(); + } } \ No newline at end of file diff --git a/resources/views/components/ib-skeleton-loader.blade.php b/resources/views/components/ib-skeleton-loader.blade.php new file mode 100644 index 0000000..de612c9 --- /dev/null +++ b/resources/views/components/ib-skeleton-loader.blade.php @@ -0,0 +1,9 @@ +