chore: code style

This commit is contained in:
hackerESQ
2025-01-28 17:14:49 -06:00
parent c4736fae70
commit e8ef0921ad
123 changed files with 1051 additions and 1197 deletions
+4 -3
View File
@@ -2,8 +2,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class AiChat extends Model
{
@@ -11,7 +11,7 @@ class AiChat extends Model
protected $fillable = [
'role',
'content'
'content',
];
protected $hidden = [];
@@ -26,7 +26,8 @@ class AiChat extends Model
});
}
public function user() {
public function user()
{
return $this->belongsTo(User::class);
}
+5 -7
View File
@@ -2,11 +2,9 @@
namespace App\Models;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Database\Eloquent\Model;
use App\Imports\BackupImport as BackupImportExcel;
use App\Jobs\BackupImportJob;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class BackupImport extends Model
{
@@ -20,7 +18,7 @@ class BackupImport extends Model
'status', // pending, in_progress, success, failed
'message', // Import starting, Import is in progress, Importing portfolios, Importing transactions, Importing daily changes, Import completed successfully
'has_errors',
'completed_at'
'completed_at',
];
protected static function boot()
@@ -32,9 +30,9 @@ class BackupImport extends Model
$import->status = 'pending';
$import->message = __('Import starting...');
});
static::created(function ($import) {
BackupImportJob::dispatch($import);
});
}
@@ -47,7 +45,7 @@ class BackupImport extends Model
{
return [
'has_errors' => 'boolean',
'completed_at' => 'datetime'
'completed_at' => 'datetime',
];
}
}
+2 -2
View File
@@ -29,7 +29,7 @@ class ConnectedAccount extends Model
];
protected $with = [
'user'
'user',
];
/**
@@ -52,4 +52,4 @@ class ConnectedAccount extends Model
{
return $this->belongsTo(User::class);
}
}
}
+7 -6
View File
@@ -3,12 +3,12 @@
namespace App\Models;
use App\Traits\HasCompositePrimaryKey;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class DailyChange extends Model
{
use HasFactory, HasCompositePrimaryKey;
use HasCompositePrimaryKey, HasFactory;
public $timestamps = false;
@@ -32,13 +32,13 @@ class DailyChange extends Model
protected $casts = [
'date' => 'datetime',
];
public function scopePortfolio($query, $portfolio)
{
return $query->where('portfolio_id', $portfolio);
}
public function scopeMyDailyChanges()
public function scopeMyDailyChanges()
{
return $this->whereHas('portfolio', function ($query) {
$query->whereHas('users', function ($query) {
@@ -47,12 +47,13 @@ class DailyChange extends Model
});
}
public function scopeWithoutWishlists($query) {
public function scopeWithoutWishlists($query)
{
return $query->whereHas('portfolio', function ($query) {
$query->where('portfolios.wishlist', 0);
});
}
public function portfolio()
{
return $this->belongsTo(Portfolio::class);
+42 -43
View File
@@ -2,15 +2,12 @@
namespace App\Models;
use App\Models\Holding;
use App\Models\MarketData;
use App\Models\Transaction;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Model;
use App\Interfaces\MarketData\MarketDataInterface;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
class Dividend extends Model
{
@@ -30,15 +27,18 @@ class Dividend extends Model
'last_dividend_update' => 'datetime',
];
public function marketData() {
public function marketData()
{
return $this->belongsTo(MarketData::class, 'symbol', 'symbol');
}
public function holdings() {
public function holdings()
{
return $this->hasMany(Holding::class, 'symbol', 'symbol');
}
public function transactions() {
public function transactions()
{
return $this->hasMany(Transaction::class, 'symbol', 'symbol');
}
@@ -49,7 +49,6 @@ class Dividend extends Model
/**
* Grab new dividend data
*
*/
public static function refreshDividendData(string $symbol): void
{
@@ -64,11 +63,11 @@ class Dividend extends Model
$end_date = now();
// nope, refresh forward looking only
if ( $dividends_meta->total_dividends ) {
if ($dividends_meta->total_dividends) {
$start_date = $dividends_meta->last_dividend_update->addHours(24);
}
// skip refresh if there's already recent data
if ($start_date->greaterThan($end_date)) {
@@ -83,7 +82,7 @@ class Dividend extends Model
// ah, we found some dividends...
if ($dividend_data->isNotEmpty()) {
// create mass insert
foreach ($dividend_data as $index => $dividend){
foreach ($dividend_data as $index => $dividend) {
$dividend_data[$index] = [...$dividend, ...['id' => Str::uuid()->toString(), 'updated_at' => now(), 'created_at' => now()]];
}
@@ -109,7 +108,7 @@ class Dividend extends Model
{
// group by holdings
$dividends = self::select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount'])
->selectRaw('
->selectRaw('
(COALESCE(CASE WHEN transactions.transaction_type = "BUY"
AND date(transactions.date) <= date(dividends.date)
THEN transactions.quantity ELSE 0 END, 0)
@@ -119,22 +118,22 @@ class Dividend extends Model
* dividends.dividend_amount
AS total_received
')
->join('transactions', 'transactions.symbol', '=', 'dividends.symbol')
->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id')
->where('dividends.symbol', $symbol)
->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received')
->havingRaw('total_received > 0')
->get();
->join('transactions', 'transactions.symbol', '=', 'dividends.symbol')
->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id')
->where('dividends.symbol', $symbol)
->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received')
->havingRaw('total_received > 0')
->get();
// iterate through holdings and update
// iterate through holdings and update
Holding::where(['symbol' => $symbol])
->get()
->each(function ($holding) use ($dividends) {
$holding->update([
'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id)
->sum('total_received')
]);
});
->get()
->each(function ($holding) use ($dividends) {
$holding->update([
'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id)
->sum('total_received'),
]);
});
}
public static function reinvestDividends(iterable $dividend_data, MarketData $market_data): void
@@ -144,21 +143,21 @@ class Dividend extends Model
'symbol' => $market_data->symbol,
'reinvest_dividends' => true,
])
->get()
->each(function($holding) use ($dividend_data, $market_data) {
->get()
->each(function ($holding) use ($dividend_data, $market_data) {
foreach($dividend_data as $dividend) {
foreach ($dividend_data as $dividend) {
Transaction::create([
'date' => $dividend['date'],
'portfolio_id' => $holding->portfolio_id,
'symbol' => $holding->symbol,
'transaction_type' => "BUY",
'reinvested_dividend' => true,
'cost_basis' => 0,
'quantity' => ($dividend['dividend_amount'] * $holding->qtyOwned(Carbon::parse($dividend['date']))) / $market_data->market_value,
]);
}
});
Transaction::create([
'date' => $dividend['date'],
'portfolio_id' => $holding->portfolio_id,
'symbol' => $holding->symbol,
'transaction_type' => 'BUY',
'reinvested_dividend' => true,
'cost_basis' => 0,
'quantity' => ($dividend['dividend_amount'] * $holding->qtyOwned(Carbon::parse($dividend['date']))) / $market_data->market_value,
]);
}
});
}
}
+89 -87
View File
@@ -2,16 +2,10 @@
namespace App\Models;
use App\Models\Split;
use App\Models\AiChat;
use App\Models\Dividend;
use App\Models\Portfolio;
use App\Models\MarketData;
use App\Models\Transaction;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class Holding extends Model
{
@@ -27,13 +21,13 @@ class Holding extends Model
'realized_gain_dollars',
'dividends_earned',
'splits_synced_at',
'reinvest_dividends'
'reinvest_dividends',
];
protected $casts = [
'splits_synced_at' => 'datetime',
'first_transaction_date' => 'datetime',
'reinvest_dividends' => 'boolean'
'reinvest_dividends' => 'boolean',
];
/**
@@ -41,7 +35,7 @@ class Holding extends Model
*
* @return void
*/
public function market_data()
public function market_data()
{
return $this->hasOne(MarketData::class, 'symbol', 'symbol');
}
@@ -51,7 +45,7 @@ class Holding extends Model
*
* @return void
*/
public function transactions()
public function transactions()
{
return $this->hasManyThrough(Transaction::class, Portfolio::class, 'id', 'portfolio_id', 'portfolio_id', 'id')->orderBy('date', 'DESC');
}
@@ -61,11 +55,11 @@ class Holding extends Model
*
* @return void
*/
public function dividends()
public function dividends()
{
return $this->hasMany(Dividend::class, 'symbol', 'symbol')
->select(['dividends.symbol','dividends.date','dividends.dividend_amount'])
->selectRaw("SUM(
->select(['dividends.symbol', 'dividends.date', 'dividends.dividend_amount'])
->selectRaw("SUM(
CASE WHEN transaction_type = 'BUY'
AND transactions.symbol = dividends.symbol
AND transactions.portfolio_id = '$this->portfolio_id'
@@ -73,7 +67,7 @@ class Holding extends Model
THEN transactions.quantity
ELSE 0 END
) AS purchased")
->selectRaw("SUM(
->selectRaw("SUM(
CASE WHEN transaction_type = 'SELL'
AND transactions.symbol = dividends.symbol
AND transactions.portfolio_id = '$this->portfolio_id'
@@ -81,7 +75,7 @@ class Holding extends Model
THEN transactions.quantity
ELSE 0 END
) AS sold")
->selectRaw("SUM(
->selectRaw("SUM(
(CASE WHEN transaction_type = 'BUY'
AND transactions.symbol = dividends.symbol
AND transactions.portfolio_id = '$this->portfolio_id'
@@ -94,16 +88,16 @@ class Holding extends Model
THEN transactions.quantity ELSE 0 END)
* dividends.dividend_amount
) AS total_received")
->join('transactions', 'transactions.symbol', 'dividends.symbol')
->groupBy(['dividends.symbol','dividends.date','dividends.dividend_amount'])
->orderBy('dividends.date', 'DESC')
->where('dividends.date', '>=', function ($query) {
$query->selectRaw('min(transactions.date)')
->from('transactions')
->whereRaw("transactions.portfolio_id = '$this->portfolio_id'")
->whereRaw("transactions.symbol = '$this->symbol'");
})
->having('total_received', '>', 0);
->join('transactions', 'transactions.symbol', 'dividends.symbol')
->groupBy(['dividends.symbol', 'dividends.date', 'dividends.dividend_amount'])
->orderBy('dividends.date', 'DESC')
->where('dividends.date', '>=', function ($query) {
$query->selectRaw('min(transactions.date)')
->from('transactions')
->whereRaw("transactions.portfolio_id = '$this->portfolio_id'")
->whereRaw("transactions.symbol = '$this->symbol'");
})
->having('total_received', '>', 0);
}
/**
@@ -111,7 +105,7 @@ class Holding extends Model
*
* @return void
*/
public function portfolio()
public function portfolio()
{
return $this->belongsTo(Portfolio::class);
}
@@ -121,7 +115,7 @@ class Holding extends Model
*
* @return void
*/
public function splits()
public function splits()
{
return $this->hasMany(Split::class, 'symbol', 'symbol')
->orderBy('date', 'DESC');
@@ -140,11 +134,11 @@ class Holding extends Model
public function scopeWithMarketData($query)
{
return $query->withAggregate('market_data', 'name')
->withAggregate('market_data', 'market_value')
->withAggregate('market_data', 'fifty_two_week_low')
->withAggregate('market_data', 'fifty_two_week_high')
->withAggregate('market_data', 'updated_at')
->join('market_data', 'holdings.symbol', 'market_data.symbol');
->withAggregate('market_data', 'market_value')
->withAggregate('market_data', 'fifty_two_week_low')
->withAggregate('market_data', 'fifty_two_week_high')
->withAggregate('market_data', 'updated_at')
->join('market_data', 'holdings.symbol', 'market_data.symbol');
}
public function scopeWithPerformance($query)
@@ -164,49 +158,50 @@ class Holding extends Model
return $query->where('holdings.symbol', $symbol);
}
public function scopeWithoutWishlists($query) {
public function scopeWithoutWishlists($query)
{
return $query->whereHas('portfolio', function ($query) {
$query->where('portfolios.wishlist', 0);
});
$query->where('portfolios.wishlist', 0);
});
}
public function scopeMyHoldings($query, $userId = null)
{
return $query->whereHas('portfolio', function($query) use ($userId) {
return $query->whereHas('portfolio', function ($query) use ($userId) {
$query->whereRelation('users', 'id', $userId ?? auth()->user()->id);
});
}
public function scopeWithPortfolioMetrics($query)
public function scopeWithPortfolioMetrics($query)
{
return $query->selectRaw('COALESCE(SUM(holdings.dividends_earned), 0) AS total_dividends_earned')
->selectRaw('COALESCE(SUM(holdings.realized_gain_dollars), 0) AS realized_gain_dollars')
->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) AS total_market_value')
->selectRaw('COALESCE(SUM(holdings.total_cost_basis), 0) AS total_cost_basis')
->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) - COALESCE(SUM(holdings.total_cost_basis), 0) AS total_gain_dollars')
->selectRaw('COALESCE(SUM(holdings.realized_gain_dollars), 0) AS realized_gain_dollars')
->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) AS total_market_value')
->selectRaw('COALESCE(SUM(holdings.total_cost_basis), 0) AS total_cost_basis')
->selectRaw('COALESCE(SUM(holdings.quantity * market_data.market_value), 0) - COALESCE(SUM(holdings.total_cost_basis), 0) AS total_gain_dollars')
// ->selectRaw('COALESCE((@total_gain_dollars / @sum_total_cost_basis) * 100,0) AS total_gain_percent')
->join('market_data', 'market_data.symbol', '=', 'holdings.symbol');
->join('market_data', 'market_data.symbol', '=', 'holdings.symbol');
}
public function syncTransactionsAndDividends()
{
// pull existing transaction data
$query = Transaction::where([
'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol,
])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`')
->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`')
->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `total_cost_basis`')
->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN (quantity * sale_price) ELSE 0 END) AS `total_sale_price`')
->first();
'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol,
])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`')
->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`')
->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `total_cost_basis`')
->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN (quantity * sale_price) ELSE 0 END) AS `total_sale_price`')
->first();
$total_quantity = round($query->qty_purchases - $query->qty_sales, 3);
$average_cost_basis = (
$query->qty_purchases > 0
&& $total_quantity > 0
)
? $query->total_cost_basis / $query->qty_purchases
$query->qty_purchases > 0
&& $total_quantity > 0
)
? $query->total_cost_basis / $query->qty_purchases
: 0;
// update holding
@@ -214,18 +209,20 @@ class Holding extends Model
'quantity' => $total_quantity,
'average_cost_basis' => $average_cost_basis,
'total_cost_basis' => $total_quantity * $average_cost_basis,
'realized_gain_dollars' => $query->qty_purchases > 0 && $query->total_sale_price > 0
? $query->total_sale_price - ($query->qty_sales * ($query->total_cost_basis / $query->qty_purchases))
'realized_gain_dollars' => $query->qty_purchases > 0 && $query->total_sale_price > 0
? $query->total_sale_price - ($query->qty_sales * ($query->total_cost_basis / $query->qty_purchases))
: 0,
'dividends_earned' => $this->dividends->sum('total_received')
'dividends_earned' => $this->dividends->sum('total_received'),
]);
$this->save();
}
public function qtyOwned(\Illuminate\Support\Carbon $date = null)
public function qtyOwned(?\Illuminate\Support\Carbon $date = null)
{
if ($date == null) $date = now();
if ($date == null) {
$date = now();
}
$transactions = $this->transactions->where('date', '<=', $date);
@@ -237,16 +234,20 @@ class Holding extends Model
}
public function dailyPerformance(
\Illuminate\Support\Carbon $start_date = null,
\Illuminate\Support\Carbon $end_date = null,
?\Illuminate\Support\Carbon $start_date = null,
?\Illuminate\Support\Carbon $end_date = null,
) {
if ($start_date == null) $start_date = now();
if ($end_date == null) $end_date = now();
if ($start_date == null) {
$start_date = now();
}
if ($end_date == null) {
$end_date = now();
}
$date_interval = "DATE_ADD(date, INTERVAL 1 DAY)";
$date_interval = 'DATE_ADD(date, INTERVAL 1 DAY)';
if (config('database.default') === 'sqlite') {
$date_interval = "date(date, '+1 day')";
} else {
@@ -265,14 +266,14 @@ class Holding extends Model
FROM date_series
) as date_series")
)
->select([
'date_series.date',
DB::raw("
->select([
'date_series.date',
DB::raw("
ROUND(
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0), 3) AS `owned`
"),
DB::raw("
DB::raw("
COALESCE(CASE
WHEN (
ROUND(
@@ -285,29 +286,30 @@ class Holding extends Model
END)
END, 0) AS cost_basis
"),
DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`")
])
->leftJoin('transactions', function ($join) {
$join->on(DB::raw('DATE(transactions.date)'), '<=', 'date_series.date')
->where('transactions.symbol', '=', $this->symbol)
->where('transactions.portfolio_id', '=', $this->portfolio_id);
})
->groupBy('date_series.date')
->orderBy('date_series.date')
->get()
->keyBy('date');
DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`"),
])
->leftJoin('transactions', function ($join) {
$join->on(DB::raw('DATE(transactions.date)'), '<=', 'date_series.date')
->where('transactions.symbol', '=', $this->symbol)
->where('transactions.portfolio_id', '=', $this->portfolio_id);
})
->groupBy('date_series.date')
->orderBy('date_series.date')
->get()
->keyBy('date');
}
public function getFormattedTransactions()
{
$formattedTransactions = '';
foreach($this->transactions->sortByDesc('date') as $transaction) {
$formattedTransactions .= " * ".$transaction->date->format('Y-m-d')
." ". $transaction->transaction_type
." ". $transaction->quantity
." @ ". $transaction->cost_basis
foreach ($this->transactions->sortByDesc('date') as $transaction) {
$formattedTransactions .= ' * '.$transaction->date->format('Y-m-d')
.' '.$transaction->transaction_type
.' '.$transaction->quantity
.' @ '.$transaction->cost_basis
." each \n\n";
}
return $formattedTransactions;
}
}
}
+11 -9
View File
@@ -2,16 +2,18 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Interfaces\MarketData\MarketDataInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MarketData extends Model
{
use HasFactory;
protected $primaryKey = 'symbol';
protected $keyType = 'string';
public $incrementing = false;
protected $fillable = [
@@ -25,7 +27,7 @@ class MarketData extends Model
'market_cap',
'book_value',
'last_dividend_date',
'dividend_yield'
'dividend_yield',
];
protected $casts = [
@@ -37,10 +39,10 @@ class MarketData extends Model
'trailing_pe' => 'float',
'market_cap' => 'float',
'book_value' => 'float',
'dividend_yield' => 'float'
'dividend_yield' => 'float',
];
public function holdings()
public function holdings()
{
return $this->hasMany(Holding::class, 'symbol', 'symbol');
}
@@ -50,20 +52,20 @@ class MarketData extends Model
return $query->where('symbol', $symbol);
}
public static function getMarketData($symbol, $force = false)
public static function getMarketData($symbol, $force = false)
{
$market_data = self::firstOrNew([
'symbol' => $symbol
'symbol' => $symbol,
]);
// check if new or stale
if (
$force
|| !$market_data->exists
|| ! $market_data->exists
|| is_null($market_data->updated_at)
|| $market_data->updated_at->diffInMinutes(now()) >= config('investbrain.refresh')
) {
// get quote
$quote = app(MarketDataInterface::class)->quote($symbol);
@@ -76,4 +78,4 @@ class MarketData extends Model
return $market_data;
}
}
}
+59 -66
View File
@@ -2,17 +2,16 @@
namespace App\Models;
use App\Models\AiChat;
use App\Interfaces\MarketData\MarketDataInterface;
use App\Notifications\InvitedOnboardingNotification;
use Carbon\CarbonPeriod;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
use App\Interfaces\MarketData\MarketDataInterface;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use App\Notifications\InvitedOnboardingNotification;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
class Portfolio extends Model
{
@@ -30,7 +29,7 @@ class Portfolio extends Model
protected static function boot()
{
parent::boot();
static::saved(function ($portfolio) {
self::ensurePortfolioHasOwner($portfolio);
@@ -40,7 +39,7 @@ class Portfolio extends Model
protected $hidden = [];
protected $casts = [
'wishlist' => 'boolean'
'wishlist' => 'boolean',
];
protected $with = ['users', 'transactions'];
@@ -53,8 +52,8 @@ class Portfolio extends Model
public function holdings()
{
return $this->hasMany(Holding::class, 'portfolio_id')
->withMarketData()
->withPerformance();
->withMarketData()
->withPerformance();
}
public function transactions()
@@ -77,25 +76,25 @@ class Portfolio extends Model
return $this->morphMany(AiChat::class, 'chatable')->where('user_id', auth()->user()->id);
}
public function scopeMyPortfolios()
public function scopeMyPortfolios()
{
return $this->whereHas('users', function ($query) {
$query->where('user_id', auth()->user()->id);
});
}
public function scopeFullAccess($query, $user_id = null)
public function scopeFullAccess($query, $user_id = null)
{
return $query->whereHas('users', function ($query) use ($user_id) {
$query->where('user_id', $user_id ?? auth()->user()->id)
->where(function ($query) {
$query->where('full_access', true)
->orWhere('owner', true);
});
->where(function ($query) {
$query->where('full_access', true)
->orWhere('owner', true);
});
});
}
public function scopeWithoutWishlists()
public function scopeWithoutWishlists()
{
return $this->where(['wishlist' => false]);
}
@@ -103,7 +102,7 @@ class Portfolio extends Model
public function setOwnerIdAttribute($value)
{
// enable queued jobs to create portfolios with owners
if (!auth()->user()?->id && !$this->owner_id) {
if (! auth()->user()?->id && ! $this->owner_id) {
static::$owner_id = $value;
}
}
@@ -115,18 +114,18 @@ class Portfolio extends Model
public function getOwnerAttribute()
{
if (!$this->relationLoaded('user')) {
if (! $this->relationLoaded('user')) {
$this->load('users');
}
return $this->users->where('pivot.owner', true)->first();
}
public static function ensurePortfolioHasOwner(self $portfolio)
public static function ensurePortfolioHasOwner(self $portfolio)
{
// make sure we don't remove owner access
if (!$portfolio->owner_id) {
if (! $portfolio->owner_id) {
$owner[static::$owner_id ?? auth()->user()->id] = ['owner' => true];
// save
@@ -138,24 +137,24 @@ class Portfolio extends Model
public function syncDailyChanges(): void
{
$holdings = $this->holdings()
->join('transactions', function($join) {
$join->on('transactions.symbol', '=', 'holdings.symbol')
->where('transactions.portfolio_id', '=', $this->id);
})
->select('holdings.symbol', 'holdings.portfolio_id', DB::raw('min(transactions.date) as first_transaction_date')) // get first transaction date
->groupBy(['holdings.symbol', 'holdings.portfolio_id'])
->get();
->join('transactions', function ($join) {
$join->on('transactions.symbol', '=', 'holdings.symbol')
->where('transactions.portfolio_id', '=', $this->id);
})
->select('holdings.symbol', 'holdings.portfolio_id', DB::raw('min(transactions.date) as first_transaction_date')) // get first transaction date
->groupBy(['holdings.symbol', 'holdings.portfolio_id'])
->get();
$dividends = Dividend::whereIn('symbol', $holdings->pluck('symbol'))->get();
$total_performance = [];
$holdings->each(function($holding) use (&$total_performance, $dividends) {
$holdings->each(function ($holding) use (&$total_performance, $dividends) {
$period = CarbonPeriod::create(
$holding->first_transaction_date,
now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day')))
? now()->subDay()
$holding->first_transaction_date,
now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day')))
? now()->subDay()
: now()
);
@@ -170,11 +169,11 @@ class Portfolio extends Model
$dividends_earned = 0;
$holding_performance = [];
foreach($period as $date) {
foreach ($period as $date) {
$date = $date->format('Y-m-d');
$close = $this->getMostRecentCloseData($all_history, $date);
$total_market_value = $daily_performance->get($date)->owned * $close;
$dividends_earned += $daily_performance->get($date)->owned * ($dividends->get($date)?->dividend_amount ?? 0);
@@ -182,18 +181,18 @@ class Portfolio extends Model
$holding_performance[$date] = [
'date' => $date,
'portfolio_id' => $this->id,
'total_market_value' => $total_market_value,
'total_market_value' => $total_market_value,
'total_cost_basis' => $daily_performance->get($date)->cost_basis,
'total_gain' => $total_market_value - $daily_performance->get($date)->cost_basis,
'realized_gains' => $daily_performance->get($date)->realized_gains,
'total_dividends_earned' => $dividends_earned
'total_dividends_earned' => $dividends_earned,
];
}
}
foreach ($holding_performance as $date => $performance) {
if (Arr::get($total_performance, $date) == null) {
$total_performance[$date] = $performance;
} else {
@@ -207,9 +206,9 @@ class Portfolio extends Model
}
});
if (!empty($total_performance)) {
if (! empty($total_performance)) {
DB::transaction(function () use ($total_performance) {
$this->daily_change()->upsert(
$total_performance,
['date', 'portfolio_id'],
@@ -218,7 +217,7 @@ class Portfolio extends Model
'total_cost_basis',
'total_gain',
'realized_gains',
'total_dividends_earned'
'total_dividends_earned',
]
);
});
@@ -229,10 +228,10 @@ class Portfolio extends Model
{
$close = Arr::get($history, "$date.close", 0);
if (!$close && $i < $max_attempts) {
if (! $close && $i < $max_attempts) {
$i++;
$date = Carbon::parse($date)->subDay()->format('Y-m-d');
return $this->getMostRecentCloseData($history, $date, $i);
@@ -244,53 +243,47 @@ class Portfolio extends Model
public function getFormattedHoldings()
{
$formattedHoldings = '';
foreach($this->holdings as $holding) {
$formattedHoldings .= " * Holding of ".$holding->market_data->name." (".$holding->symbol.")"
."; with ". ($holding->quantity > 0 ? $holding->quantity : 'ZERO') . " shares"
."; avg cost basis ". $holding->average_cost_basis
."; curr market value ". $holding->market_data->market_value
."; unrealized gains ". $holding->market_gain_dollars
."; realized gains ". $holding->realized_gain_dollars
."; dividends earned ". $holding->dividends_earned
foreach ($this->holdings as $holding) {
$formattedHoldings .= ' * Holding of '.$holding->market_data->name.' ('.$holding->symbol.')'
.'; with '.($holding->quantity > 0 ? $holding->quantity : 'ZERO').' shares'
.'; avg cost basis '.$holding->average_cost_basis
.'; curr market value '.$holding->market_data->market_value
.'; unrealized gains '.$holding->market_gain_dollars
.'; realized gains '.$holding->realized_gain_dollars
.'; dividends earned '.$holding->dividends_earned
."\n\n";
}
return $formattedHoldings;
}
/**
* Share a portfolio with a user
*
* @param string $email
* @param boolean $fullAccess
* @return void
*/
public function share(string $email, bool $fullAccess = false): void
{
$user = User::firstOrCreate([
'email' => $email
'email' => $email,
], [
'name' => Str::title(Str::before($email, '@'))
'name' => Str::title(Str::before($email, '@')),
]);
$permissions[$user->id] = [
'full_access' => $fullAccess
'full_access' => $fullAccess,
];
$sync = $this->users()->syncWithoutDetaching($permissions);
if (!empty($sync['attached'])) {
if (! empty($sync['attached'])) {
foreach($sync['attached'] as $newUserId) {
foreach ($sync['attached'] as $newUserId) {
User::find($newUserId)->notify(new InvitedOnboardingNotification($this, auth()->user()));
};
}
}
}
/**
* Un-share a portfolio
*
* @param string $userId
* @return void
*/
public function unShare(string $userId): void
{
+36 -36
View File
@@ -2,13 +2,12 @@
namespace App\Models;
use App\Models\Transaction;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
use App\Interfaces\MarketData\MarketDataInterface;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class Split extends Model
{
@@ -28,22 +27,23 @@ class Split extends Model
'last_date' => 'datetime',
];
public function holdings() {
public function holdings()
{
return $this->hasMany(Holding::class, 'symbol', 'symbol');
}
public function transactions() {
public function transactions()
{
return $this->hasMany(Transaction::class, 'symbol', 'symbol');
}
/**
* Grab new split data
*
* @param string $symbol
* @param \DateTimeInterface|null $start_date
* @param \DateTimeInterface|null $start_date
* @return void
*/
public static function refreshSplitData(string $symbol)
public static function refreshSplitData(string $symbol)
{
// dates for split data
$splits_meta = self::where(['symbol' => $symbol])
@@ -58,9 +58,9 @@ class Split extends Model
// nope, need to populate newer split data
if ($splits_meta->total_splits) {
$start_date = $splits_meta->last_date->addHours(48);
$end_date = now();
$end_date = now();
}
// get some data
@@ -71,10 +71,10 @@ class Split extends Model
if ($split_data->isNotEmpty()) {
// insert records
(new self)->insert($split_data->map(function($split) {
(new self)->insert($split_data->map(function ($split) {
return [...$split, ...['id' => Str::uuid()->toString()]];
})->toArray());
})->toArray());
}
// sync to transactions
@@ -84,39 +84,39 @@ class Split extends Model
/**
* Syncs all transactions of symbol with split data
*
* @param string $symbol
* @param string $symbol
* @return void
*/
public static function syncToTransactions($symbol)
public static function syncToTransactions($symbol)
{
// get splits joined with matching holdings
$splits = self::select([
'splits.date',
'splits.symbol',
'splits.split_amount',
'holdings.portfolio_id'
])
->where([
'splits.symbol' => $symbol,
])
->whereDate('splits.date', '>', DB::raw('IFNULL(holdings.splits_synced_at, "0000-00-00")'))
->where('holdings.quantity', '>', 0)
->join('holdings', 'splits.symbol', 'holdings.symbol')
->orderBy('splits.date', 'ASC')
->get();
'splits.date',
'splits.symbol',
'splits.split_amount',
'holdings.portfolio_id',
])
->where([
'splits.symbol' => $symbol,
])
->whereDate('splits.date', '>', DB::raw('IFNULL(holdings.splits_synced_at, "0000-00-00")'))
->where('holdings.quantity', '>', 0)
->join('holdings', 'splits.symbol', 'holdings.symbol')
->orderBy('splits.date', 'ASC')
->get();
foreach($splits as $split) {
foreach ($splits as $split) {
// get qty owned when split was issued
$qty_owned = Transaction::where([
'symbol' => $split->symbol,
'portfolio_id' => $split->portfolio_id
])
'symbol' => $split->symbol,
'portfolio_id' => $split->portfolio_id,
])
->whereDate('transactions.date', '<', $split->date->format('Y-m-d'))
->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) -
SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS qty_owned')
->value('qty_owned');
if ($qty_owned > 0) {
Transaction::create([
@@ -128,14 +128,14 @@ class Split extends Model
'cost_basis' => 0,
'split' => true,
'created_at' => now(),
'updated_at' => now()
'updated_at' => now(),
]);
Holding::where([
'symbol' => $split->symbol,
'portfolio_id' => $split->portfolio_id
'portfolio_id' => $split->portfolio_id,
])->update([
'splits_synced_at' => now()
'splits_synced_at' => now(),
]);
}
}
+19 -19
View File
@@ -2,12 +2,11 @@
namespace App\Models;
use App\Models\MarketData;
use Illuminate\Support\Arr;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
class Transaction extends Model
{
@@ -23,7 +22,7 @@ class Transaction extends Model
'cost_basis',
'sale_price',
'split',
'reinvested_dividend'
'reinvested_dividend',
];
protected $hidden = [];
@@ -31,7 +30,7 @@ class Transaction extends Model
protected $casts = [
'date' => 'datetime',
'split' => 'boolean',
'reinvested_dividend' => 'boolean'
'reinvested_dividend' => 'boolean',
];
protected static function boot()
@@ -52,14 +51,14 @@ class Transaction extends Model
$transaction->refreshMarketData();
cache()->forget('portfolio-metrics-' . $transaction->portfolio_id);
cache()->forget('portfolio-metrics-'.$transaction->portfolio_id);
});
static::deleted(function ($transaction) {
$transaction->syncToHolding();
cache()->forget('portfolio-metrics-' . $transaction->portfolio_id);
cache()->forget('portfolio-metrics-'.$transaction->portfolio_id);
});
}
@@ -96,13 +95,13 @@ class Transaction extends Model
public function scopeWithMarketData($query)
{
return $query->withAggregate('market_data', 'name')
->withAggregate('market_data', 'market_value')
->withAggregate('market_data', 'fifty_two_week_low')
->withAggregate('market_data', 'fifty_two_week_high')
->withAggregate('market_data', 'updated_at')
->join('market_data', 'transactions.symbol', 'market_data.symbol');
->withAggregate('market_data', 'market_value')
->withAggregate('market_data', 'fifty_two_week_low')
->withAggregate('market_data', 'fifty_two_week_high')
->withAggregate('market_data', 'updated_at')
->join('market_data', 'transactions.symbol', 'market_data.symbol');
}
public function scopePortfolio($query, $portfolio)
{
return $query->where('portfolio_id', $portfolio);
@@ -128,7 +127,7 @@ class Transaction extends Model
return $query->whereDate('date', '<=', $date);
}
public function scopeMyTransactions()
public function scopeMyTransactions()
{
return $this->whereHas('portfolio', function ($query) {
$query->whereHas('users', function ($query) {
@@ -137,7 +136,7 @@ class Transaction extends Model
});
}
public function refreshMarketData()
public function refreshMarketData()
{
return MarketData::getMarketData($this->attributes['symbol']);
}
@@ -154,7 +153,7 @@ class Transaction extends Model
'symbol' => $this->symbol,
'transaction_type' => 'BUY',
])->whereDate('date', '<=', $this->date)
->average('cost_basis');
->average('cost_basis');
$this->cost_basis = $average_cost_basis ?? 0;
@@ -166,7 +165,8 @@ class Transaction extends Model
*
* @return void
*/
public function syncToHolding() {
public function syncToHolding()
{
// if symbol name changed, sync previous symbol too
if (Arr::has($this->changes, 'symbol')) {
@@ -181,7 +181,7 @@ class Transaction extends Model
// get the holding for a symbol and portfolio (or create one)
Holding::firstOrNew([
'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol
'symbol' => $this->symbol,
], [
'portfolio_id' => $this->portfolio_id,
'symbol' => $this->symbol,
@@ -191,4 +191,4 @@ class Transaction extends Model
'splits_synced_at' => now(),
])->syncTransactionsAndDividends();
}
}
}
+12 -12
View File
@@ -3,27 +3,27 @@
namespace App\Models;
use App\Traits\HasConnectedAccounts;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Jetstream\HasProfilePhoto;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens;
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
class User extends Authenticatable implements MustVerifyEmail
{
use HasApiTokens;
use HasConnectedAccounts;
use HasFactory;
use HasProfilePhoto;
use HasRelationships;
use HasUuids;
use Notifiable;
use TwoFactorAuthenticatable;
use HasUuids;
use HasRelationships;
use HasConnectedAccounts;
protected $fillable = [
'name',
@@ -65,7 +65,7 @@ class User extends Authenticatable implements MustVerifyEmail
{
return $this->hasManyDeep(Holding::class, ['portfolio_user', Portfolio::class])
->withMarketData()
->withPerformance();
->withPerformance();
}
public function transactions(): HasManyDeep
@@ -78,6 +78,6 @@ class User extends Authenticatable implements MustVerifyEmail
WHEN transaction_type = \'SELL\'
THEN COALESCE(transactions.sale_price - transactions.cost_basis, 0)
ELSE COALESCE(market_data.market_value - transactions.cost_basis, 0)
END AS gain_dollars');
END AS gain_dollars');
}
}