Files
investbrain/app/Models/Portfolio.php
T

293 lines
9.2 KiB
PHP
Raw Normal View History

2024-08-01 13:53:10 -05:00
<?php
namespace App\Models;
2025-01-28 17:14:49 -06:00
use App\Interfaces\MarketData\MarketDataInterface;
use App\Notifications\InvitedOnboardingNotification;
2024-09-23 19:39:49 -05:00
use Carbon\CarbonPeriod;
2025-01-28 17:14:49 -06:00
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
2024-09-11 22:00:37 -05:00
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
2024-09-11 22:00:37 -05:00
use Illuminate\Support\Facades\DB;
2025-01-28 17:14:49 -06:00
use Illuminate\Support\Str;
2024-08-01 13:53:10 -05:00
class Portfolio extends Model
{
use HasFactory;
use HasUuids;
2024-08-01 13:53:10 -05:00
protected $fillable = [
'title',
'notes',
'wishlist',
];
public static ?string $owner_id = null;
2024-08-01 13:53:10 -05:00
protected static function boot()
{
parent::boot();
2025-01-28 17:14:49 -06:00
2024-10-21 22:23:20 -05:00
static::saved(function ($portfolio) {
2024-08-17 21:33:09 -05:00
2024-10-21 22:23:20 -05:00
self::ensurePortfolioHasOwner($portfolio);
2024-08-01 13:53:10 -05:00
});
}
protected $hidden = [];
2024-08-04 18:27:47 -05:00
protected $casts = [
2025-01-28 17:14:49 -06:00
'wishlist' => 'boolean',
2024-08-04 18:27:47 -05:00
];
2024-08-01 13:53:10 -05:00
2024-08-15 21:35:43 -05:00
protected $with = ['users', 'transactions'];
2024-08-01 13:53:10 -05:00
public function users()
{
2024-10-21 22:23:20 -05:00
return $this->belongsToMany(User::class)->withPivot(['owner', 'full_access', 'invite_accepted_at']);
2024-08-01 13:53:10 -05:00
}
2024-08-15 21:35:43 -05:00
public function holdings()
{
2024-08-28 23:32:01 -05:00
return $this->hasMany(Holding::class, 'portfolio_id')
2025-01-28 17:14:49 -06:00
->withMarketData()
->withPerformance();
2024-08-15 21:35:43 -05:00
}
2024-08-01 13:53:10 -05:00
2024-08-15 21:35:43 -05:00
public function transactions()
{
2024-08-17 21:33:09 -05:00
return $this->hasMany(Transaction::class)->orderBy('created_at', 'DESC');
2024-08-15 21:35:43 -05:00
}
public function daily_change()
{
return $this->hasMany(DailyChange::class);
}
2024-08-01 13:53:10 -05:00
/**
* Related chats for portfolio
*
* @return void
*/
public function chats()
{
2024-11-03 08:41:14 -06:00
return $this->morphMany(AiChat::class, 'chatable')->where('user_id', auth()->user()->id);
}
2025-01-28 17:14:49 -06:00
public function scopeMyPortfolios()
2024-08-28 22:06:47 -05:00
{
return $this->whereHas('users', function ($query) {
$query->where('user_id', auth()->user()->id);
});
}
2025-01-28 17:14:49 -06:00
public function scopeFullAccess($query, $user_id = null)
2024-10-22 16:48:53 -05:00
{
2024-10-24 14:48:24 -05:00
return $query->whereHas('users', function ($query) use ($user_id) {
$query->where('user_id', $user_id ?? auth()->user()->id)
2025-01-28 17:14:49 -06:00
->where(function ($query) {
$query->where('full_access', true)
->orWhere('owner', true);
});
2024-10-22 16:48:53 -05:00
});
}
2025-01-28 17:14:49 -06:00
public function scopeWithoutWishlists()
2024-08-01 13:53:10 -05:00
{
return $this->where(['wishlist' => false]);
}
2024-10-24 17:05:37 -05:00
public function setOwnerIdAttribute($value)
{
// enable queued jobs to create portfolios with owners
2025-01-28 17:14:49 -06:00
if (! auth()->user()?->id && ! $this->owner_id) {
2024-10-24 17:05:37 -05:00
static::$owner_id = $value;
}
}
2024-08-01 13:53:10 -05:00
public function getOwnerIdAttribute()
{
2024-10-21 22:23:20 -05:00
return $this->owner?->id;
2024-08-01 13:53:10 -05:00
}
2024-10-21 22:23:20 -05:00
public function getOwnerAttribute()
2024-09-11 22:00:37 -05:00
{
2025-01-28 17:14:49 -06:00
if (! $this->relationLoaded('user')) {
2024-10-25 22:06:46 -05:00
$this->load('users');
}
return $this->users->where('pivot.owner', true)->first();
2024-10-21 22:23:20 -05:00
}
2024-08-01 13:53:10 -05:00
2025-01-28 17:14:49 -06:00
public static function ensurePortfolioHasOwner(self $portfolio)
2024-10-21 22:23:20 -05:00
{
// make sure we don't remove owner access
2025-01-28 17:14:49 -06:00
if (! $portfolio->owner_id) {
$owner[static::$owner_id ?? auth()->user()->id] = ['owner' => true];
2024-08-01 13:53:10 -05:00
2024-10-21 22:23:20 -05:00
// save
$portfolio->users()->sync($owner);
2025-01-27 20:04:03 -06:00
static::$owner_id = null;
2024-10-21 22:23:20 -05:00
}
2024-08-01 13:53:10 -05:00
}
2024-09-11 22:00:37 -05:00
public function syncDailyChanges(): void
{
$holdings = $this->holdings()
2025-01-28 17:14:49 -06:00
->join('transactions', function ($join) {
$join->on('transactions.symbol', '=', 'holdings.symbol')
->where('transactions.portfolio_id', '=', $this->id);
})
->select('holdings.symbol', 'holdings.portfolio_id', DB::raw('min(transactions.date) as first_transaction_date')) // get first transaction date
->groupBy(['holdings.symbol', 'holdings.portfolio_id'])
->get();
2024-09-15 11:28:13 -05:00
$dividends = Dividend::whereIn('symbol', $holdings->pluck('symbol'))->get();
2025-01-28 17:14:49 -06:00
2024-09-11 22:00:37 -05:00
$total_performance = [];
2025-01-28 17:14:49 -06:00
$holdings->each(function ($holding) use (&$total_performance, $dividends) {
2024-09-15 11:28:13 -05:00
2024-10-28 17:50:45 -05:00
$period = CarbonPeriod::create(
2025-01-28 17:14:49 -06:00
$holding->first_transaction_date,
now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day')))
? now()->subDay()
2024-10-28 17:50:45 -05:00
: now()
);
2024-09-23 19:39:49 -05:00
2024-09-15 11:28:13 -05:00
$holding->setRelation('dividends', $dividends->where('symbol', $holding->symbol));
2024-09-11 22:00:37 -05:00
2024-09-15 11:28:13 -05:00
$daily_performance = $holding->dailyPerformance($holding->first_transaction_date, now());
2024-09-11 22:00:37 -05:00
$dividends = $holding->dividends->keyBy(function ($dividend, $key) {
return $dividend['date']->format('Y-m-d');
});
$all_history = app(MarketDataInterface::class)->history($holding->symbol, $holding->first_transaction_date, now());
2024-09-11 22:00:37 -05:00
$dividends_earned = 0;
2024-09-23 19:39:49 -05:00
$holding_performance = [];
2024-09-11 22:00:37 -05:00
2025-01-28 17:14:49 -06:00
foreach ($period as $date) {
2024-09-23 19:39:49 -05:00
$date = $date->format('Y-m-d');
2024-09-11 22:00:37 -05:00
$close = $this->getMostRecentCloseData($all_history, $date);
2025-01-28 17:14:49 -06:00
2024-09-23 19:39:49 -05:00
$total_market_value = $daily_performance->get($date)->owned * $close;
$dividends_earned += $daily_performance->get($date)->owned * ($dividends->get($date)?->dividend_amount ?? 0);
2024-09-11 22:00:37 -05:00
if (Carbon::parse($date)->isWeekday()) {
2024-09-23 19:39:49 -05:00
$holding_performance[$date] = [
'date' => $date,
'portfolio_id' => $this->id,
2025-01-28 17:14:49 -06:00
'total_market_value' => $total_market_value,
2024-09-23 19:39:49 -05:00
'total_cost_basis' => $daily_performance->get($date)->cost_basis,
'total_gain' => $total_market_value - $daily_performance->get($date)->cost_basis,
'realized_gains' => $daily_performance->get($date)->realized_gains,
2025-01-28 17:14:49 -06:00
'total_dividends_earned' => $dividends_earned,
];
}
2024-09-23 19:39:49 -05:00
}
2024-09-11 22:00:37 -05:00
2024-09-23 19:39:49 -05:00
foreach ($holding_performance as $date => $performance) {
if (Arr::get($total_performance, $date) == null) {
2025-01-28 17:14:49 -06:00
2024-09-11 22:00:37 -05:00
$total_performance[$date] = $performance;
} else {
2024-09-11 22:00:37 -05:00
$total_performance[$date]['total_market_value'] += $performance['total_market_value'];
$total_performance[$date]['total_cost_basis'] += $performance['total_cost_basis'];
$total_performance[$date]['total_gain'] += $performance['total_gain'];
$total_performance[$date]['realized_gains'] += $performance['realized_gains'];
$total_performance[$date]['total_dividends_earned'] += $performance['total_dividends_earned'];
}
}
});
2025-01-28 17:14:49 -06:00
if (! empty($total_performance)) {
2024-09-11 23:04:21 -05:00
DB::transaction(function () use ($total_performance) {
2025-01-28 17:14:49 -06:00
2024-10-25 21:06:45 -05:00
$this->daily_change()->upsert(
$total_performance,
['date', 'portfolio_id'],
[
'total_market_value',
'total_cost_basis',
'total_gain',
'realized_gains',
2025-01-28 17:14:49 -06:00
'total_dividends_earned',
2024-10-25 21:06:45 -05:00
]
);
2024-09-11 23:04:21 -05:00
});
}
2024-09-11 22:00:37 -05:00
}
protected function getMostRecentCloseData($history, $date, $i = 0, $max_attempts = 5)
{
$close = Arr::get($history, "$date.close", 0);
2025-01-28 17:14:49 -06:00
if (! $close && $i < $max_attempts) {
$i++;
2025-01-28 17:14:49 -06:00
$date = Carbon::parse($date)->subDay()->format('Y-m-d');
return $this->getMostRecentCloseData($history, $date, $i);
}
return $close;
}
2024-10-31 17:04:59 -05:00
public function getFormattedHoldings()
{
$formattedHoldings = '';
2025-01-28 17:14:49 -06:00
foreach ($this->holdings as $holding) {
$formattedHoldings .= ' * Holding of '.$holding->market_data->name.' ('.$holding->symbol.')'
.'; with '.($holding->quantity > 0 ? $holding->quantity : 'ZERO').' shares'
.'; avg cost basis '.$holding->average_cost_basis
.'; curr market value '.$holding->market_data->market_value
.'; unrealized gains '.$holding->market_gain_dollars
.'; realized gains '.$holding->realized_gain_dollars
.'; dividends earned '.$holding->dividends_earned
2024-10-31 17:04:59 -05:00
."\n\n";
}
2025-01-28 17:14:49 -06:00
2024-10-31 17:04:59 -05:00
return $formattedHoldings;
}
2025-01-27 20:04:03 -06:00
/**
* Share a portfolio with a user
*/
public function share(string $email, bool $fullAccess = false): void
{
$user = User::firstOrCreate([
2025-01-28 17:14:49 -06:00
'email' => $email,
2025-01-27 20:04:03 -06:00
], [
2025-01-28 17:14:49 -06:00
'name' => Str::title(Str::before($email, '@')),
2025-01-27 20:04:03 -06:00
]);
$permissions[$user->id] = [
2025-01-28 17:14:49 -06:00
'full_access' => $fullAccess,
2025-01-27 20:04:03 -06:00
];
$sync = $this->users()->syncWithoutDetaching($permissions);
2025-01-28 17:14:49 -06:00
if (! empty($sync['attached'])) {
2025-01-27 20:04:03 -06:00
2025-01-28 17:14:49 -06:00
foreach ($sync['attached'] as $newUserId) {
2025-01-27 20:04:03 -06:00
User::find($newUserId)->notify(new InvitedOnboardingNotification($this, auth()->user()));
2025-01-28 17:14:49 -06:00
}
2025-01-27 20:04:03 -06:00
}
}
/**
* Un-share a portfolio
*/
public function unShare(string $userId): void
{
$this->users()->detach($userId);
}
2024-08-01 13:53:10 -05:00
}