diff --git a/app/Console/Commands/RefreshHoldingData.php b/app/Console/Commands/RefreshHoldingData.php new file mode 100644 index 0000000..bef280b --- /dev/null +++ b/app/Console/Commands/RefreshHoldingData.php @@ -0,0 +1,91 @@ +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(); + } + } +} diff --git a/app/Exports/BackupExport.php b/app/Exports/BackupExport.php index 9de1ace..e2e8242 100644 --- a/app/Exports/BackupExport.php +++ b/app/Exports/BackupExport.php @@ -12,15 +12,20 @@ class BackupExport implements WithMultipleSheets { use Exportable; + public function __construct( + public bool $empty = false + ) + { } + /** * @return array */ public function sheets(): array { return [ - new PortfoliosSheet, - new TransactionsSheet, - new DailyChangesSheet + new PortfoliosSheet($this->empty), + new TransactionsSheet($this->empty), + new DailyChangesSheet($this->empty) ]; } } diff --git a/app/Exports/Sheets/DailyChangesSheet.php b/app/Exports/Sheets/DailyChangesSheet.php index aa8a768..0e4ed5d 100644 --- a/app/Exports/Sheets/DailyChangesSheet.php +++ b/app/Exports/Sheets/DailyChangesSheet.php @@ -9,6 +9,10 @@ use Maatwebsite\Excel\Concerns\WithTitle; class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle { + public function __construct( + public bool $empty = false + ) { } + public function headings(): array { return [ @@ -28,7 +32,7 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle */ public function collection() { - return DailyChange::myDailyChanges()->get(); + return $this->empty ? collect() : DailyChange::myDailyChanges()->get(); } /** diff --git a/app/Exports/Sheets/PortfoliosSheet.php b/app/Exports/Sheets/PortfoliosSheet.php index 22e70a8..f2bd293 100644 --- a/app/Exports/Sheets/PortfoliosSheet.php +++ b/app/Exports/Sheets/PortfoliosSheet.php @@ -9,6 +9,10 @@ use Maatwebsite\Excel\Concerns\WithTitle; class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle { + public function __construct( + public bool $empty = false + ) { } + public function headings(): array { return [ @@ -26,7 +30,7 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle */ public function collection() { - return Portfolio::myPortfolios()->get(); + return $this->empty ? collect() : Portfolio::myPortfolios()->get(); } /** diff --git a/app/Exports/Sheets/TransactionsSheet.php b/app/Exports/Sheets/TransactionsSheet.php index f4f1a25..de58029 100644 --- a/app/Exports/Sheets/TransactionsSheet.php +++ b/app/Exports/Sheets/TransactionsSheet.php @@ -10,6 +10,10 @@ use Maatwebsite\Excel\Concerns\FromCollection; class TransactionsSheet implements FromCollection, WithHeadings, WithTitle { + public function __construct( + public bool $empty = false + ) { } + public function headings(): array { return [ @@ -32,7 +36,7 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle */ public function collection() { - return Transaction::myTransactions()->get(); + return $this->empty ? collect() : Transaction::myTransactions()->get(); } /** diff --git a/app/Imports/BackupImport.php b/app/Imports/BackupImport.php index 1fcc68c..3c0feeb 100644 --- a/app/Imports/BackupImport.php +++ b/app/Imports/BackupImport.php @@ -6,11 +6,13 @@ use App\Imports\Sheets\SplitsSheet; use App\Imports\Sheets\DividendsSheet; use App\Imports\Sheets\MarketDataSheet; use App\Imports\Sheets\PortfoliosSheet; +use Illuminate\Support\Facades\Artisan; use Maatwebsite\Excel\Events\AfterSheet; use App\Imports\Sheets\DailyChangesSheet; use App\Imports\Sheets\TransactionsSheet; use Maatwebsite\Excel\Concerns\Importable; use Maatwebsite\Excel\Concerns\WithEvents; +use App\Console\Commands\RefreshHoldingData; use Maatwebsite\Excel\Concerns\WithMultipleSheets; class BackupImport implements WithMultipleSheets, WithEvents @@ -25,7 +27,8 @@ class BackupImport implements WithMultipleSheets, WithEvents { return [ - // AfterSheet::class => dd('test') + // AfterSheet::class => Artisan::queue(RefreshHoldingData::class), + AfterSheet::class => Artisan::call(RefreshHoldingData::class) ]; } diff --git a/app/Imports/Sheets/DailyChangesSheet.php b/app/Imports/Sheets/DailyChangesSheet.php index 1b242dc..e844add 100644 --- a/app/Imports/Sheets/DailyChangesSheet.php +++ b/app/Imports/Sheets/DailyChangesSheet.php @@ -2,18 +2,17 @@ namespace App\Imports\Sheets; -use App\Models\DailyChange; use Exception; +use App\Models\DailyChange; use Illuminate\Support\Collection; use Maatwebsite\Excel\Concerns\ToCollection; use Maatwebsite\Excel\Concerns\SkipsEmptyRows; use Maatwebsite\Excel\Concerns\WithHeadingRow; use Maatwebsite\Excel\Concerns\WithValidation; +use Maatwebsite\Excel\Concerns\WithChunkReading; -class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, SkipsEmptyRows +class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, SkipsEmptyRows, WithChunkReading { - // use Importable; - public function collection(Collection $dailyChanges) { @@ -27,22 +26,25 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, } }); - foreach ($dailyChanges as $dailyChange) { + 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 @@ -58,4 +60,9 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, 'annotation' => ['sometimes', 'nullable', 'string'], ]; } + + public function chunkSize(): int + { + return 500; + } } diff --git a/app/Imports/Sheets/PortfoliosSheet.php b/app/Imports/Sheets/PortfoliosSheet.php index a963634..756d062 100644 --- a/app/Imports/Sheets/PortfoliosSheet.php +++ b/app/Imports/Sheets/PortfoliosSheet.php @@ -12,25 +12,26 @@ use Maatwebsite\Excel\Concerns\WithValidation; class PortfoliosSheet implements ToCollection, WithValidation, WithHeadingRow, SkipsEmptyRows { - // use Importable; - public function collection(Collection $portfolios) { - foreach ($portfolios as $portfolio) { + Portfolio::withoutEvents(function () use ($portfolios) { + + foreach ($portfolios as $portfolio) { - auth()->user()->portfolios() - ->where(['id' => $portfolio['portfolio_id']]) - ->orWhere(['title' => $portfolio['title']]) - ->firstOr(function () use ($portfolio) { + 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(); - }); - } + return Portfolio::make()->forceFill([ + 'id' => $portfolio['portfolio_id'] ?? null, + 'title' => $portfolio['title'], + 'wishlist' => $portfolio['wishlist'] ?? false, + 'notes' => $portfolio['notes'], + ])->save(); + }); + } + }); } public function rules(): array diff --git a/app/Imports/Sheets/TransactionsSheet.php b/app/Imports/Sheets/TransactionsSheet.php index 310d443..48979f3 100644 --- a/app/Imports/Sheets/TransactionsSheet.php +++ b/app/Imports/Sheets/TransactionsSheet.php @@ -2,6 +2,7 @@ namespace App\Imports\Sheets; +use App\Models\Holding; use App\Models\Transaction; use Illuminate\Support\Collection; use Maatwebsite\Excel\Concerns\ToCollection; @@ -12,28 +13,40 @@ use Maatwebsite\Excel\Concerns\WithChunkReading; class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation, SkipsEmptyRows, WithChunkReading { - // use Importable; - public function collection(Collection $transactions) { - foreach ($transactions->sortBy('date') as $transaction) { + Transaction::withoutEvents(function () use ($transactions) { - Transaction::where('id', $transaction['transaction_id']) - ->firstOr(function () use ($transaction) { + foreach ($transactions->sortBy('date') as $transaction) { - return Transaction::make()->forceFill([ - 'id' => $transaction['transaction_id'], - 'symbol' => $transaction['symbol'], - 'portfolio_id' => $transaction['portfolio_id'], - 'transaction_type' => $transaction['transaction_type'], - 'quantity' => $transaction['quantity'], - 'cost_basis' => $transaction['cost_basis'] ?? 0, - 'sale_price' => $transaction['sale_price'], - 'split' => $transaction['split'] ?? null, - 'date' => $transaction['date'], - ])->save(); - }); - } + $transaction = Transaction::where('id', $transaction['transaction_id']) + ->firstOr(function () use ($transaction) { + + return Transaction::make()->forceFill([ + 'id' => $transaction['transaction_id'], + 'symbol' => $transaction['symbol'], + 'portfolio_id' => $transaction['portfolio_id'], + 'transaction_type' => $transaction['transaction_type'], + 'quantity' => $transaction['quantity'], + 'cost_basis' => $transaction['cost_basis'] ?? 0, + '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 + ]); + } + }); } public function rules(): array diff --git a/lang/en.json b/lang/en.json index 9450d90..0f7e125 100644 --- a/lang/en.json +++ b/lang/en.json @@ -94,6 +94,8 @@ "Successfully imported!": "Successfully imported!", "Select a file": "Select a file", "Download Export": "Download Export", + "Click to download import template.": "Click to download import template.", + "Download import template.": "Download import template.", "Your email address is unverified.": "Your email address is unverified.", "Click here to re-send the verification email.": "Click here to re-send the verification email.", "A new verification link has been sent to your email address.": "A new verification link has been sent to your email address.", diff --git a/lang/es.json b/lang/es.json index e3f4a71..648b797 100644 --- a/lang/es.json +++ b/lang/es.json @@ -94,6 +94,8 @@ "Successfully imported!": "¡Importado con éxito!", "Select a file": "Seleccionar un archivo", "Download Export": "Descargar Exportación", + "Click to download import template.": "Haz clic para descargar la plantilla de importación.", + "Download import template.": "Descargar la plantilla de importación.", "Your email address is unverified.": "Tu dirección de correo electrónico no está verificada.", "Click here to re-send the verification email.": "Haz clic aquí para reenviar el correo de verificación.", "A new verification link has been sent to your email address.": "Se ha enviado un nuevo enlace de verificación a tu dirección de correo electrónico.", diff --git a/resources/views/import-export.blade.php b/resources/views/import-export.blade.php index e3735b5..58a6eaf 100644 --- a/resources/views/import-export.blade.php +++ b/resources/views/import-export.blade.php @@ -2,7 +2,7 @@
- @livewire('import-portfolios-field') + @livewire('import-portfolios-field')
diff --git a/resources/views/livewire/import-portfolios-field.blade.php b/resources/views/livewire/import-portfolios-field.blade.php index 49bcc9e..d1ce233 100644 --- a/resources/views/livewire/import-portfolios-field.blade.php +++ b/resources/views/livewire/import-portfolios-field.blade.php @@ -4,6 +4,7 @@ use Livewire\WithFileUploads; use Livewire\Volt\Component; use Mary\Traits\Toast; use App\Imports\BackupImport; +use App\Exports\BackupExport; use Livewire\Attributes\Rule; new class extends Component { @@ -17,7 +18,6 @@ new class extends Component { // methods public function import() { - $this->validate(); try { @@ -25,13 +25,16 @@ new class extends Component { $import = (new BackupImport)->import($this->file); } catch (\Throwable $e) { - dd($e); + return $this->error($e->getMessage()); } $this->success(__('Successfully imported!')); + } - // Artisan::queue(RefreshHoldingData::class); + public function downloadTemplate() + { + return Excel::download(new BackupExport(empty: true), now()->format('Y_m_d') . '_investbrain_template.xlsx'); } }; ?> @@ -42,7 +45,8 @@ new class extends Component { - {{ __('Upload or recover your Investbrain portfolio and holdings.') }} + {{ __('Upload or recover your Investbrain portfolio and holdings.') }} + {{ __('Download import template.') }}