diff --git a/app/Console/Commands/RefreshHoldingData.php b/app/Console/Commands/RefreshHoldingData.php index bef280b..a186b17 100644 --- a/app/Console/Commands/RefreshHoldingData.php +++ b/app/Console/Commands/RefreshHoldingData.php @@ -2,12 +2,10 @@ namespace App\Console\Commands; -use App\Models\Dividend; use App\Models\Holding; -use App\Models\MarketData; +use App\Models\Dividend; use App\Models\Transaction; use Illuminate\Console\Command; -use Illuminate\Database\Query\Builder; class RefreshHoldingData extends Command { @@ -48,44 +46,7 @@ class RefreshHoldingData extends Command foreach ($holdings as $holding) { $this->line('Refreshing ' . $holding->symbol); - $query = Transaction::where([ - 'portfolio_id' => $holding->portfolio_id, - 'symbol' => $holding->symbol, - ])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`') - ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`') - ->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `cost_basis`') - ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN ((sale_price - cost_basis) * quantity) ELSE 0 END) AS `realized_gains`') - ->first(); - - $total_quantity = $query->qty_purchases - $query->qty_sales; - $average_cost_basis = $query->qty_purchases > 0 - ? $query->cost_basis / $query->qty_purchases - : 0; - - // pull dividend data joined with holdings/transactions - $dividends = Dividend::where([ - 'dividends.symbol' => $holding->symbol, - ]) - ->select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) - ->selectRaw('@purchased:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "BUY" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `purchased`') - ->selectRaw('@sold:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "SELL" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `sold`') - ->selectRaw('@owned:=(@purchased - @sold) AS `owned`') - ->selectRaw('@dividends_received:=(@owned * dividends.dividend_amount) AS `dividends_received`') - ->join('transactions', 'transactions.symbol', 'dividends.symbol') - ->join('holdings', 'transactions.portfolio_id', 'holdings.portfolio_id') - ->groupBy(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) - ->get(); - - // update holding - $holding->fill([ - 'quantity' => $total_quantity, - 'average_cost_basis' => $average_cost_basis, - 'total_cost_basis' => $total_quantity * $average_cost_basis, - 'realized_gain_dollars' => $query->realized_gains, - 'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id)->sum('dividends_received') - ]); - - $holding->save(); + $holding->sync(); } } } diff --git a/app/Exports/Sheets/TransactionsSheet.php b/app/Exports/Sheets/TransactionsSheet.php index de58029..afd1f6d 100644 --- a/app/Exports/Sheets/TransactionsSheet.php +++ b/app/Exports/Sheets/TransactionsSheet.php @@ -2,7 +2,6 @@ namespace App\Exports\Sheets; -use App\Models\Portfolio; use App\Models\Transaction; use Maatwebsite\Excel\Concerns\WithTitle; use Maatwebsite\Excel\Concerns\WithHeadings; @@ -13,7 +12,7 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle public function __construct( public bool $empty = false ) { } - + public function headings(): array { return [ diff --git a/app/Imports/BackupImport.php b/app/Imports/BackupImport.php index 3c0feeb..7e5f07c 100644 --- a/app/Imports/BackupImport.php +++ b/app/Imports/BackupImport.php @@ -2,6 +2,7 @@ namespace App\Imports; +use Illuminate\Support\Facades\DB; use App\Imports\Sheets\SplitsSheet; use App\Imports\Sheets\DividendsSheet; use App\Imports\Sheets\MarketDataSheet; @@ -10,6 +11,7 @@ use Illuminate\Support\Facades\Artisan; use Maatwebsite\Excel\Events\AfterSheet; use App\Imports\Sheets\DailyChangesSheet; use App\Imports\Sheets\TransactionsSheet; +use Maatwebsite\Excel\Events\BeforeSheet; use Maatwebsite\Excel\Concerns\Importable; use Maatwebsite\Excel\Concerns\WithEvents; use App\Console\Commands\RefreshHoldingData; @@ -26,9 +28,9 @@ class BackupImport implements WithMultipleSheets, WithEvents public function registerEvents(): array { return [ - + // BeforeSheet::class => DB::commit(), // AfterSheet::class => Artisan::queue(RefreshHoldingData::class), - AfterSheet::class => Artisan::call(RefreshHoldingData::class) + // AfterSheet::class => Artisan::call(RefreshHoldingData::class) ]; } diff --git a/app/Imports/Sheets/DailyChangesSheet.php b/app/Imports/Sheets/DailyChangesSheet.php index e844add..404bed9 100644 --- a/app/Imports/Sheets/DailyChangesSheet.php +++ b/app/Imports/Sheets/DailyChangesSheet.php @@ -6,6 +6,7 @@ use Exception; use App\Models\DailyChange; use Illuminate\Support\Collection; use Maatwebsite\Excel\Concerns\ToCollection; +use App\Imports\ValidatesPortfolioPermissions; use Maatwebsite\Excel\Concerns\SkipsEmptyRows; use Maatwebsite\Excel\Concerns\WithHeadingRow; use Maatwebsite\Excel\Concerns\WithValidation; @@ -13,44 +14,34 @@ use Maatwebsite\Excel\Concerns\WithChunkReading; class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, SkipsEmptyRows, WithChunkReading { + use ValidatesPortfolioPermissions; + public function collection(Collection $dailyChanges) { + $this->validatePortfolioPermissions($dailyChanges); - $portfolios = auth()->user()->portfolios->pluck('id'); - - $dailyChanges->pluck('portfolio_id')->unique()->each(function($portfolio) use ($portfolios) { + foreach ($dailyChanges as $dailyChange) { - if (!$portfolios->contains($portfolio)) { - - throw new Exception('You do not have permission to access that portfolio.'); - } - }); - - DailyChange::withoutEvents(function () use ($dailyChanges) { - - foreach ($dailyChanges as $dailyChange) { - - DailyChange::updateOrCreate([ - 'date' => $dailyChange['date'], - 'portfolio_id' => $dailyChange['portfolio_id'], - ],[ - 'portfolio_id' => $dailyChange['portfolio_id'], - 'date' => $dailyChange['date'], - 'total_market_value' => $dailyChange['total_market_value'], - 'total_cost_basis' => $dailyChange['total_cost_basis'], - 'total_gain' => $dailyChange['total_gain'], - 'total_dividends' => $dailyChange['total_dividends'], - 'realized_gains' => $dailyChange['realized_gains'], - 'annotation' => $dailyChange['annotation'], - ]); - } - }); + DailyChange::updateOrCreate([ + 'date' => $dailyChange['date'], + 'portfolio_id' => $dailyChange['portfolio_id'], + ],[ + 'portfolio_id' => $dailyChange['portfolio_id'], + 'date' => $dailyChange['date'], + 'total_market_value' => $dailyChange['total_market_value'], + 'total_cost_basis' => $dailyChange['total_cost_basis'], + 'total_gain' => $dailyChange['total_gain'], + 'total_dividends' => $dailyChange['total_dividends'], + 'realized_gains' => $dailyChange['realized_gains'], + 'annotation' => $dailyChange['annotation'], + ]); + } } public function rules(): array { return [ - 'portfolio_id' => ['required'], + 'portfolio_id' => ['required', 'exists:portfolios,id'], 'date' => ['required', 'date'], 'total_market_value' => ['sometimes', 'nullable', 'numeric'], 'total_cost_basis' => ['sometimes', 'nullable', 'min:0', 'numeric'], diff --git a/app/Imports/Sheets/PortfoliosSheet.php b/app/Imports/Sheets/PortfoliosSheet.php index 756d062..b8ed6c5 100644 --- a/app/Imports/Sheets/PortfoliosSheet.php +++ b/app/Imports/Sheets/PortfoliosSheet.php @@ -3,7 +3,6 @@ namespace App\Imports\Sheets; use App\Models\Portfolio; -use Illuminate\Validation\Rule; use Illuminate\Support\Collection; use Maatwebsite\Excel\Concerns\ToCollection; use Maatwebsite\Excel\Concerns\SkipsEmptyRows; @@ -14,24 +13,19 @@ class PortfoliosSheet implements ToCollection, WithValidation, WithHeadingRow, S { public function collection(Collection $portfolios) { - Portfolio::withoutEvents(function () use ($portfolios) { - - foreach ($portfolios as $portfolio) { + foreach ($portfolios as $portfolio) { + + Portfolio::unguard(); - auth()->user()->portfolios() - ->where(['id' => $portfolio['portfolio_id']]) - ->orWhere(['title' => $portfolio['title']]) - ->firstOr(function () use ($portfolio) { - - return Portfolio::make()->forceFill([ - 'id' => $portfolio['portfolio_id'] ?? null, - 'title' => $portfolio['title'], - 'wishlist' => $portfolio['wishlist'] ?? false, - 'notes' => $portfolio['notes'], - ])->save(); - }); - } - }); + Portfolio::updateOrCreate([ + 'id' => $portfolio['portfolio_id'] + ], [ + 'id' => $portfolio['portfolio_id'] ?? null, + 'title' => $portfolio['title'], + 'wishlist' => $portfolio['wishlist'] ?? false, + 'notes' => $portfolio['notes'], + ]); + } } public function rules(): array diff --git a/app/Imports/Sheets/TransactionsSheet.php b/app/Imports/Sheets/TransactionsSheet.php index 48979f3..3613dfa 100644 --- a/app/Imports/Sheets/TransactionsSheet.php +++ b/app/Imports/Sheets/TransactionsSheet.php @@ -2,10 +2,10 @@ namespace App\Imports\Sheets; -use App\Models\Holding; use App\Models\Transaction; use Illuminate\Support\Collection; use Maatwebsite\Excel\Concerns\ToCollection; +use App\Imports\ValidatesPortfolioPermissions; use Maatwebsite\Excel\Concerns\SkipsEmptyRows; use Maatwebsite\Excel\Concerns\WithHeadingRow; use Maatwebsite\Excel\Concerns\WithValidation; @@ -13,16 +13,20 @@ use Maatwebsite\Excel\Concerns\WithChunkReading; class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation, SkipsEmptyRows, WithChunkReading { + use ValidatesPortfolioPermissions; + public function collection(Collection $transactions) { + $this->validatePortfolioPermissions($transactions); + Transaction::withoutEvents(function () use ($transactions) { foreach ($transactions->sortBy('date') as $transaction) { - $transaction = Transaction::where('id', $transaction['transaction_id']) + Transaction::where('id', $transaction['transaction_id']) ->firstOr(function () use ($transaction) { - return Transaction::make()->forceFill([ + $transaction = Transaction::make()->forceFill([ 'id' => $transaction['transaction_id'], 'symbol' => $transaction['symbol'], 'portfolio_id' => $transaction['portfolio_id'], @@ -32,21 +36,16 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation, 'sale_price' => $transaction['sale_price'], 'split' => $transaction['split'] ?? null, 'date' => $transaction['date'], - ])->save(); - }); + ]); - Holding::firstOrCreate([ - 'symbol' => $transaction['symbol'], - 'portfolio_id' => $transaction['portfolio_id'], - ], [ - 'quantity' => 0, - 'average_cost_basis' => 0, - 'total_cost_basis' => 0, - 'realized_gain_dollars' => 0, - 'dividends_earned' => 0 - ]); + $transaction->save(); + + return $transaction; + }) + ->syncHolding(); } }); + } public function rules(): array diff --git a/app/Imports/ValidatesPortfolioPermissions.php b/app/Imports/ValidatesPortfolioPermissions.php new file mode 100644 index 0000000..d982d77 --- /dev/null +++ b/app/Imports/ValidatesPortfolioPermissions.php @@ -0,0 +1,21 @@ +user()->portfolios->pluck('id'); + + $collection->pluck('portfolio_id')->unique()->each(function($portfolio) use ($portfolios) { + + if (!$portfolios->contains($portfolio)) { + + throw new Exception('You do not have permission to access that portfolio.'); + } + }); + } +} diff --git a/app/Models/Holding.php b/app/Models/Holding.php index a802114..085848a 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -164,6 +164,49 @@ class Holding extends Model // ->selectRaw('COALESCE((@total_gain_dollars / @sum_total_cost_basis) * 100,0) AS total_gain_percent') ->join('market_data', 'market_data.symbol', 'holdings.symbol'); } + + public function sync() + { + // 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 `cost_basis`') + ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN ((sale_price - cost_basis) * quantity) ELSE 0 END) AS `realized_gains`') + ->first(); + + $total_quantity = $query->qty_purchases - $query->qty_sales; + $average_cost_basis = $query->qty_purchases > 0 + ? $query->cost_basis / $query->qty_purchases + : 0; + + // pull dividend data joined with holdings/transactions + $dividends = Dividend::where([ + 'dividends.symbol' => $this->symbol, + ]) + ->select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) + ->selectRaw('@purchased:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "BUY" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `purchased`') + ->selectRaw('@sold:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "SELL" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `sold`') + ->selectRaw('@owned:=(@purchased - @sold) AS `owned`') + ->selectRaw('@dividends_received:=(@owned * dividends.dividend_amount) AS `dividends_received`') + ->join('transactions', 'transactions.symbol', 'dividends.symbol') + ->join('holdings', 'transactions.portfolio_id', 'holdings.portfolio_id') + ->groupBy(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) + ->get(); + + // update holding + $this->fill([ + 'quantity' => $total_quantity, + 'average_cost_basis' => $average_cost_basis, + 'total_cost_basis' => $total_quantity * $average_cost_basis, + 'realized_gain_dollars' => $query->realized_gains, + 'dividends_earned' => $dividends->where('portfolio_id', $this->portfolio_id)->sum('dividends_received') + ]); + + $this->save(); + } } \ No newline at end of file diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index cbefc3b..455f1a7 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -46,6 +46,10 @@ class Transaction extends Model $transaction->syncHolding(); + $transaction->refreshMarketData(); + + $transaction->syncDividendsToHolding(); + cache()->tags(['metrics', auth()->user()->id])->flush(); }); @@ -174,33 +178,6 @@ class Transaction extends Model 'total_cost_basis' => $this->quantity * $this->cost_basis, ]); - // pull existing transaction data - $query = self::where([ - 'portfolio_id' => $this->portfolio_id, - 'symbol' => $this->symbol, - ])->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) AS `qty_purchases`') - ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN quantity ELSE 0 END) AS `qty_sales`') - ->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN (quantity * cost_basis) ELSE 0 END) AS `cost_basis`') - ->selectRaw('SUM(CASE WHEN transaction_type = "SELL" THEN ((sale_price - cost_basis) * quantity) ELSE 0 END) AS `realized_gains`') - ->first(); - - $total_quantity = $query->qty_purchases - $query->qty_sales; - $average_cost_basis = $query->qty_purchases > 0 - ? $query->cost_basis / $query->qty_purchases - : 0; - - // update holding - $holding->fill([ - 'quantity' => $total_quantity, - 'average_cost_basis' => $average_cost_basis, - 'total_cost_basis' => $total_quantity * $average_cost_basis, - 'realized_gain_dollars' => $query->realized_gains, - ]); - - $holding->save(); - - $this->refreshMarketData(); - - $this->syncDividendsToHolding(); + $holding->sync(); } } \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 0f7e125..f462886 100644 --- a/lang/en.json +++ b/lang/en.json @@ -14,6 +14,8 @@ "Cancel": "Cancel", "Save": "Save", "Close": "Close", + "or": "or", + "and": "and", "Hang on! You're doing that too much.": "Hang on! You're doing that too much.", "Delete Account": "Delete Account", diff --git a/lang/es.json b/lang/es.json index 648b797..60efdbe 100644 --- a/lang/es.json +++ b/lang/es.json @@ -14,6 +14,8 @@ "Cancel": "Cancelar", "Save": "Guardar", "Close": "Cerrar", + "or": "o", + "and": "y", "Hang on! You're doing that too much.": "¡Por favor espere un momento!", "Delete Account": "Eliminar Cuenta", diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 122edc0..4172538 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -38,7 +38,11 @@ @if ($user->portfolios->isEmpty())
- + + + {{ __('or') }} + +
@endif diff --git a/resources/views/livewire/holdings-table.blade.php b/resources/views/livewire/holdings-table.blade.php index 0640f1e..7a11a3d 100644 --- a/resources/views/livewire/holdings-table.blade.php +++ b/resources/views/livewire/holdings-table.blade.php @@ -40,7 +40,6 @@ new class extends Component { public function holdings(): Collection { - // dd(Holding::toSql()); $holdings = $this->portfolio ->holdings() ->withCount(['transactions as num_transactions' => function($query) { diff --git a/resources/views/livewire/import-portfolios-field.blade.php b/resources/views/livewire/import-portfolios-field.blade.php index d1ce233..7b0acd0 100644 --- a/resources/views/livewire/import-portfolios-field.blade.php +++ b/resources/views/livewire/import-portfolios-field.blade.php @@ -29,7 +29,7 @@ new class extends Component { return $this->error($e->getMessage()); } - $this->success(__('Successfully imported!')); + $this->success(__('Successfully imported!'), redirectTo: route('dashboard')); } public function downloadTemplate() @@ -58,8 +58,12 @@ new class extends Component { + + + {{ __('Saved.') }} + - + {{ __('Import') }}