chore: code style
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user