improve import / export flow and clean up relationships
This commit is contained in:
hackerESQ
2024-08-28 22:06:47 -05:00
parent e684c1f4a3
commit 69c43dc41f
15 changed files with 104 additions and 74 deletions
+3 -3
View File
@@ -13,10 +13,10 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle
{ {
return [ return [
'Date', 'Date',
'Portfolio', 'Portfolio ID',
'Total Market Value', 'Total Market Value',
'Total Cost Basis', 'Total Cost Basis',
'Total Gain Loss', 'Total Gain',
'Total Dividends', 'Total Dividends',
'Realized Gains', 'Realized Gains',
'Annotation' 'Annotation'
@@ -28,7 +28,7 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle
*/ */
public function collection() public function collection()
{ {
return auth()->user()->daily_changes; return DailyChange::myDailyChanges()->get();
} }
/** /**
+1 -1
View File
@@ -26,7 +26,7 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle
*/ */
public function collection() public function collection()
{ {
return auth()->user()->portfolios; return Portfolio::myPortfolios()->get();
} }
/** /**
+5 -11
View File
@@ -2,10 +2,11 @@
namespace App\Exports\Sheets; namespace App\Exports\Sheets;
use App\Models\Portfolio;
use App\Models\Transaction; use App\Models\Transaction;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithTitle; use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\FromCollection;
class TransactionsSheet implements FromCollection, WithHeadings, WithTitle class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
{ {
@@ -22,14 +23,7 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
'Split', 'Split',
'Date', 'Date',
'Created', 'Created',
'Updated', 'Updated'
'Company Name',
'Portfolio Title',
'Market Value',
'52 Week Low',
'52 Week High',
'Market Data Refresh Date',
'Gain/Loss Dollars'
]; ];
} }
@@ -38,7 +32,7 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
*/ */
public function collection() public function collection()
{ {
return auth()->user()->transactions; return Transaction::myTransactions()->get();
} }
/** /**
+6 -9
View File
@@ -2,7 +2,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Holding;
use App\Models\Portfolio; use App\Models\Portfolio;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -15,14 +14,12 @@ class HoldingController extends Controller
public function show(Request $request, Portfolio $portfolio, String $symbol) public function show(Request $request, Portfolio $portfolio, String $symbol)
{ {
$holding = $portfolio->holdings()
->with(['market_data'])
->symbol($symbol)
->portfolio($portfolio->id)
->first();
$holding = Holding::query() return view('holding.show', compact(['portfolio', 'holding']));
->portfolio($portfolio->id)
->symbol($symbol)
->first();
$market_data = $holding->market_data;
return view('holding.show', compact(['portfolio', 'holding', 'market_data']));
} }
} }
+6 -5
View File
@@ -22,16 +22,17 @@ class PortfolioController extends Controller
*/ */
public function show(Portfolio $portfolio) public function show(Portfolio $portfolio)
{ {
$portfolio->load(['transactions', 'holdings']);
// get portfolio metrics // get portfolio metrics
$metrics = cache()->tags(['metrics', 'portfolio', auth()->user()->id, $portfolio->id])->remember( $metrics = cache()->tags(['metrics', 'portfolio', auth()->user()->id, $portfolio->id])->remember(
'portfolio-metrics-' . $portfolio->id, 'portfolio-metrics-' . $portfolio->id,
60, 60,
function () use ($portfolio) { function () use ($portfolio) {
return return Holding::query()
Holding::query() ->portfolio($portfolio->id)
->portfolio($portfolio->id) ->getPortfolioMetrics()
->getPortfolioMetrics() ->first();
->first();
} }
); );
+3 -1
View File
@@ -20,8 +20,10 @@ class BackupImport implements WithMultipleSheets
{ {
return [ return [
'Portfolios' => new PortfoliosSheet, 'Portfolios' => new PortfoliosSheet,
// 'Transactions' => new TransactionsSheet, 'Transactions' => new TransactionsSheet,
// 'Daily Changes' => new DailyChangesSheet, // 'Daily Changes' => new DailyChangesSheet,
]; ];
// Can listen for AfterSheet to run clean up afterwards
} }
} }
+14 -12
View File
@@ -15,23 +15,25 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, SkipsEmptyRows
public function collection(Collection $dailyChanges) public function collection(Collection $dailyChanges)
{ {
foreach ($dailyChanges->sortBy('date') as $row) { foreach ($dailyChanges as $dailyChange) {
if ($row['user'] != auth()->user()->id) {
if ($dailyChange['user'] != auth()->user()->id) {
throw new Exception('Can\'t do that.'); throw new Exception('Can\'t do that.');
} }
DailyChange::updateOrCreate([ DailyChange::updateOrCreate([
'date' => $row['date'], 'date' => $dailyChange['date'],
'user_id' => $row['user'], 'portfolio_id' => $dailyChange['portfolio_id'],
],[ ],[
'user_id' => $row['user'], 'portfolio_id' => $dailyChange['portfolio_id'],
'date' => $row['date'], 'date' => $dailyChange['date'],
'total_market_value' => $row['total_market_value'], 'total_market_value' => $dailyChange['total_market_value'],
'total_cost_basis' => $row['total_cost_basis'], 'total_cost_basis' => $dailyChange['total_cost_basis'],
'total_gain' => $row['total_gain'], 'total_gain' => $dailyChange['total_gain'],
'total_dividends_earned' => $row['total_dividends_earned'], 'total_dividends' => $dailyChange['total_dividends'],
'realized_gains' => $row['realized_gains'], 'realized_gains' => $dailyChange['realized_gains'],
'notes' => $row['notes'], 'annotation' => $dailyChange['annotation'],
]); ]);
} }
} }
+1 -1
View File
@@ -14,7 +14,7 @@ class PortfoliosSheet implements ToCollection, WithHeadingRow, SkipsEmptyRows
public function collection(Collection $portfolios) public function collection(Collection $portfolios)
{ {
foreach ($portfolios->sortBy('date') as $portfolio) { foreach ($portfolios as $portfolio) {
auth()->user()->portfolios() auth()->user()->portfolios()
->where(['id' => $portfolio['id']]) ->where(['id' => $portfolio['id']])
+23 -15
View File
@@ -7,28 +7,36 @@ use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection; use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\SkipsEmptyRows; use Maatwebsite\Excel\Concerns\SkipsEmptyRows;
use Maatwebsite\Excel\Concerns\WithHeadingRow; use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithChunkReading;
class TransactionsSheet implements ToCollection, WithHeadingRow, SkipsEmptyRows class TransactionsSheet implements ToCollection, WithHeadingRow, SkipsEmptyRows, WithChunkReading
{ {
// use Importable; // use Importable;
public function collection(Collection $transactions) public function collection(Collection $transactions)
{ {
foreach ($transactions->sortBy('date') as $row) { foreach ($transactions as $transaction) {
Transaction::updateOrCreate([ Transaction::where('id', $transaction['transaction_id'])
'id' => $row['id'], ->firstOr(function () use ($transaction) {
],[
'id' => $row['id'], return Transaction::make()->forceFill([
'symbol' => $row['symbol'], 'id' => $transaction['transaction_id'],
'portfolio_id' => $row['portfolio'], 'symbol' => $transaction['symbol'],
'transaction_type' => $row['transaction'], 'portfolio_id' => $transaction['portfolio_id'],
'quantity' => $row['quantity'], 'transaction_type' => $transaction['transaction_type'],
'cost_basis' => $row['cost_basis'] ?? 0, 'quantity' => $transaction['quantity'],
'sale_price' => $row['sale_price'], 'cost_basis' => $transaction['cost_basis'] ?? 0,
'split' => $row['split'] ?? null, 'sale_price' => $transaction['sale_price'],
'date' => $row['date'], 'split' => $transaction['split'] ?? null,
]); 'date' => $transaction['date'],
])->save();
});
} }
} }
public function chunkSize(): int
{
return 500;
}
} }
+9
View File
@@ -37,6 +37,15 @@ class DailyChange extends Model
{ {
return $query->where('portfolio_id', $portfolio); return $query->where('portfolio_id', $portfolio);
} }
public function scopeMyDailyChanges()
{
return $this->whereHas('portfolio', function ($query) {
$query->whereHas('users', function ($query) {
$query->where('id', auth()->id());
});
});
}
public function portfolio() public function portfolio()
{ {
+3 -2
View File
@@ -56,7 +56,8 @@ class Holding extends Model
public function transactions() public function transactions()
{ {
return $this->hasMany(Transaction::class, 'symbol', 'symbol') return $this->hasMany(Transaction::class, 'symbol', 'symbol')
->where('transactions.portfolio_id', $this->portfolio_id); ->where('portfolio_id', $this->portfolio_id)
->withAggregate('portfolio', 'title');
} }
/** /**
@@ -141,7 +142,7 @@ class Holding extends Model
public function scopeSymbol($query, $symbol) public function scopeSymbol($query, $symbol)
{ {
return $query->where('symbol', $symbol); return $query->where('holdings.symbol', $symbol);
} }
public function scopeWithoutWishlists($query) { public function scopeWithoutWishlists($query) {
+7
View File
@@ -62,6 +62,13 @@ class Portfolio extends Model
return $this->hasMany(DailyChange::class); return $this->hasMany(DailyChange::class);
} }
public function scopeMyPortfolios()
{
return $this->whereHas('users', function ($query) {
$query->where('user_id', auth()->user()->id);
});
}
public function scopeWithoutWishlists() public function scopeWithoutWishlists()
{ {
return $this->where(['wishlist' => false]); return $this->where(['wishlist' => false]);
+9 -1
View File
@@ -90,7 +90,6 @@ class Transaction extends Model
public function scopeWithMarketData($query) public function scopeWithMarketData($query)
{ {
$query->withAggregate('market_data', 'name') $query->withAggregate('market_data', 'name')
->withAggregate('portfolio', 'title')
->withAggregate('market_data', 'market_value') ->withAggregate('market_data', 'market_value')
->withAggregate('market_data', 'fifty_two_week_low') ->withAggregate('market_data', 'fifty_two_week_low')
->withAggregate('market_data', 'fifty_two_week_high') ->withAggregate('market_data', 'fifty_two_week_high')
@@ -108,6 +107,15 @@ class Transaction extends Model
return $query->where('symbol', $symbol); return $query->where('symbol', $symbol);
} }
public function scopeMyTransactions()
{
return $this->whereHas('portfolio', function ($query) {
$query->whereHas('users', function ($query) {
$query->where('id', auth()->id());
});
});
}
public function refreshMarketData() public function refreshMarketData()
{ {
return MarketData::getMarketData($this->attributes['symbol']); return MarketData::getMarketData($this->attributes['symbol']);
+1
View File
@@ -72,6 +72,7 @@ class User extends Authenticatable
{ {
return $this->hasManyDeep(Transaction::class, ['portfolio_user', Portfolio::class]) return $this->hasManyDeep(Transaction::class, ['portfolio_user', Portfolio::class])
->withMarketData() ->withMarketData()
->withAggregate('portfolio', 'title')
->selectRaw(' ->selectRaw('
CASE CASE
WHEN transaction_type = \'SELL\' WHEN transaction_type = \'SELL\'
+13 -13
View File
@@ -7,7 +7,7 @@
> >
@livewire('manage-transaction-form', [ @livewire('manage-transaction-form', [
'portfolio' => $portfolio, 'portfolio' => $portfolio,
'symbol' => $market_data->symbol, 'symbol' => $holding->market_data->symbol,
]) ])
</x-ib-modal> </x-ib-modal>
@@ -16,7 +16,7 @@
<x-slot:title> <x-slot:title>
<a href="{{ route('portfolio.show', ['portfolio' => $portfolio->id]) }}" title="{{ __('Portfolio') }}"> <a href="{{ route('portfolio.show', ['portfolio' => $portfolio->id]) }}" title="{{ __('Portfolio') }}">
{{ $portfolio->title }} {{ $portfolio->title }}
</a> » <span title="{{ __('Holding') }}">{{ $market_data->symbol }}</span> </a> » <span title="{{ __('Holding') }}">{{ $holding->market_data->symbol }}</span>
</x-slot:title> </x-slot:title>
<x-ib-flex-spacer /> <x-ib-flex-spacer />
@@ -35,16 +35,16 @@
<x-ib-card class="md:col-span-5"> <x-ib-card class="md:col-span-5">
<x-slot:title class="pb-2"> <x-slot:title class="pb-2">
{{ $market_data->symbol }} {{ $holding->market_data->symbol }}
<span class="text-sm"> {{ $market_data->name }} </span> <span class="text-sm"> {{ $holding->market_data->name }} </span>
</x-slot:title> </x-slot:title>
<div class="font-bold text-2xl py-1 flex items-center"> <div class="font-bold text-2xl py-1 flex items-center">
{{ Number::currency($market_data->market_value ?? 0) }} {{ Number::currency($holding->market_data->market_value ?? 0) }}
<x-gain-loss-arrow-badge <x-gain-loss-arrow-badge
:cost-basis="$holding->average_cost_basis" :cost-basis="$holding->average_cost_basis"
:market-value="$market_data->market_value" :market-value="$holding->market_data->market_value"
/> />
</div> </div>
@@ -75,7 +75,7 @@
<p class="pt-2 text-sm"> <p class="pt-2 text-sm">
{{ __('Market Data Age') }}: {{ __('Market Data Age') }}:
{{ \Carbon\Carbon::parse($market_data->updated_at)->diffForHumans() }} {{ \Carbon\Carbon::parse($holding->market_data->updated_at)->diffForHumans() }}
</p> </p>
</x-ib-card> </x-ib-card>
@@ -84,26 +84,26 @@
<p> <p>
<span class="font-bold">{{ __('Forward PE') }}: </span> <span class="font-bold">{{ __('Forward PE') }}: </span>
{{ $market_data->forward_pe }} {{ $holding->market_data->forward_pe }}
</p> </p>
<p> <p>
<span class="font-bold">{{ __('Trailing PE') }}: </span> <span class="font-bold">{{ __('Trailing PE') }}: </span>
{{ $market_data->trailing_pe }} {{ $holding->market_data->trailing_pe }}
</p> </p>
<p> <p>
<span class="font-bold">{{ __('Market Cap') }}: </span> <span class="font-bold">{{ __('Market Cap') }}: </span>
${{ Number::forHumans($market_data->market_cap ?? 0) }} ${{ Number::forHumans($holding->market_data->market_cap ?? 0) }}
</p> </p>
<p> <p>
<span class="font-bold">{{ __('52 week') }}: </span> <span class="font-bold">{{ __('52 week') }}: </span>
<x-fifty-two-week-range <x-fifty-two-week-range
:low="$market_data->fifty_two_week_low" :low="$holding->market_data->fifty_two_week_low"
:high="$market_data->fifty_two_week_high" :high="$holding->market_data->fifty_two_week_high"
:current="$market_data->market_value" :current="$holding->market_data->market_value"
/> />
</p> </p>