adds dividend re-investment feature
This commit is contained in:
@@ -24,6 +24,7 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
|
|||||||
'Cost Basis',
|
'Cost Basis',
|
||||||
'Sale Price',
|
'Sale Price',
|
||||||
'Split',
|
'Split',
|
||||||
|
'Reinvested Dividend',
|
||||||
'Date',
|
'Date',
|
||||||
'Created',
|
'Created',
|
||||||
'Updated'
|
'Updated'
|
||||||
|
|||||||
+35
-6
@@ -6,6 +6,7 @@ use App\Models\Holding;
|
|||||||
use App\Models\MarketData;
|
use App\Models\MarketData;
|
||||||
use App\Models\Transaction;
|
use App\Models\Transaction;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use App\Interfaces\MarketData\MarketDataInterface;
|
use App\Interfaces\MarketData\MarketDataInterface;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
@@ -86,10 +87,15 @@ class Dividend extends Model
|
|||||||
(new self)->insert($dividend_data->toArray());
|
(new self)->insert($dividend_data->toArray());
|
||||||
|
|
||||||
// sync to holdings
|
// sync to holdings
|
||||||
self::syncHoldings($dividend_data);
|
self::syncHoldings($symbol);
|
||||||
|
|
||||||
|
// get market data
|
||||||
|
$market_data = MarketData::firstOrNew(['symbol' => $symbol]);
|
||||||
|
|
||||||
|
// re-invest dividends
|
||||||
|
self::reinvestDividends($dividend_data, $market_data);
|
||||||
|
|
||||||
// sync last dividend amount to market data table
|
// sync last dividend amount to market data table
|
||||||
$market_data = MarketData::firstOrNew(['symbol' => $symbol]);
|
|
||||||
$market_data->last_dividend_amount = $dividend_data->sortByDesc('date')->first()['dividend_amount'];
|
$market_data->last_dividend_amount = $dividend_data->sortByDesc('date')->first()['dividend_amount'];
|
||||||
$market_data->save();
|
$market_data->save();
|
||||||
}
|
}
|
||||||
@@ -97,10 +103,8 @@ class Dividend extends Model
|
|||||||
return $dividend_data;
|
return $dividend_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function syncHoldings($dividend_data): void
|
public static function syncHoldings(string $symbol): void
|
||||||
{
|
{
|
||||||
$symbol = $dividend_data->last()['symbol'];
|
|
||||||
|
|
||||||
// group by holdings
|
// group by holdings
|
||||||
$dividends = self::select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount'])
|
$dividends = self::select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount'])
|
||||||
->selectRaw('
|
->selectRaw('
|
||||||
@@ -115,7 +119,7 @@ class Dividend extends Model
|
|||||||
')
|
')
|
||||||
->join('transactions', 'transactions.symbol', '=', 'dividends.symbol')
|
->join('transactions', 'transactions.symbol', '=', 'dividends.symbol')
|
||||||
->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id')
|
->join('holdings', 'transactions.portfolio_id', '=', 'holdings.portfolio_id')
|
||||||
->where('dividends.symbol', $dividend_data->last()['symbol'])
|
->where('dividends.symbol', $symbol)
|
||||||
->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received')
|
->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received')
|
||||||
->havingRaw('total_received > 0')
|
->havingRaw('total_received > 0')
|
||||||
->get();
|
->get();
|
||||||
@@ -130,4 +134,29 @@ class Dividend extends Model
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function reinvestDividends(iterable $dividend_data, MarketData $market_data): void
|
||||||
|
{
|
||||||
|
// re-invest dividends
|
||||||
|
Holding::where([
|
||||||
|
'symbol' => $market_data->symbol,
|
||||||
|
'reinvest_dividends' => true,
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->each(function($holding) use ($dividend_data, $market_data) {
|
||||||
|
|
||||||
|
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' => $market_data->market_value,
|
||||||
|
'quantity' => ($dividend['dividend_amount'] * $holding->qtyOwned(Carbon::parse($dividend['date']))) / $market_data->market_value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -26,11 +26,13 @@ class Holding extends Model
|
|||||||
'realized_gain_dollars',
|
'realized_gain_dollars',
|
||||||
'dividends_earned',
|
'dividends_earned',
|
||||||
'splits_synced_at',
|
'splits_synced_at',
|
||||||
|
'reinvest_dividends'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'splits_synced_at' => 'datetime',
|
'splits_synced_at' => 'datetime',
|
||||||
'first_transaction_date' => 'datetime'
|
'first_transaction_date' => 'datetime',
|
||||||
|
'reinvest_dividends' => 'boolean'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
@@ -209,6 +211,19 @@ class Holding extends Model
|
|||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function qtyOwned(\Illuminate\Support\Carbon $date = null)
|
||||||
|
{
|
||||||
|
if ($date == null) $date = now();
|
||||||
|
|
||||||
|
$transactions = $this->transactions->where('date', '<=', $date);
|
||||||
|
|
||||||
|
$purchases = $transactions->where('transaction_type', 'BUY')->sum('quantity');
|
||||||
|
|
||||||
|
$sales = $transactions->where('transaction_type', 'SELL')->sum('quantity');
|
||||||
|
|
||||||
|
return $purchases - $sales;
|
||||||
|
}
|
||||||
|
|
||||||
public function dailyPerformance(
|
public function dailyPerformance(
|
||||||
\Illuminate\Support\Carbon $start_date = null,
|
\Illuminate\Support\Carbon $start_date = null,
|
||||||
\Illuminate\Support\Carbon $end_date = null,
|
\Illuminate\Support\Carbon $end_date = null,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class Transaction extends Model
|
|||||||
'cost_basis',
|
'cost_basis',
|
||||||
'sale_price',
|
'sale_price',
|
||||||
'split',
|
'split',
|
||||||
|
'reinvested_dividend'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [];
|
protected $hidden = [];
|
||||||
@@ -30,6 +31,7 @@ class Transaction extends Model
|
|||||||
protected $casts = [
|
protected $casts = [
|
||||||
'date' => 'datetime',
|
'date' => 'datetime',
|
||||||
'split' => 'boolean',
|
'split' => 'boolean',
|
||||||
|
'reinvested_dividend' => 'boolean'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function boot()
|
protected static function boot()
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('holdings', function (Blueprint $table) {
|
||||||
|
$table->boolean('reinvest_dividends')->nullable()->after('quantity');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
$table->boolean('reinvested_dividend')->nullable()->after('split');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('holdings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('reinvest_dividends');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('reinvested_dividend');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -11,11 +11,13 @@
|
|||||||
"Log in": "Log in",
|
"Log in": "Log in",
|
||||||
"Register": "Register",
|
"Register": "Register",
|
||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
|
"Update": "Update",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Close": "Close",
|
"Close": "Close",
|
||||||
"or": "or",
|
"or": "or",
|
||||||
"and": "and",
|
"and": "and",
|
||||||
|
"Yes": "Yes",
|
||||||
"Nothing to show here yet": "Nothing to show here yet",
|
"Nothing to show here yet": "Nothing to show here yet",
|
||||||
|
|
||||||
"Hang on! You're doing that too much.": "Hang on! You're doing that too much.",
|
"Hang on! You're doing that too much.": "Hang on! You're doing that too much.",
|
||||||
@@ -116,6 +118,11 @@
|
|||||||
"Total Market Value": "Total Market Value",
|
"Total Market Value": "Total Market Value",
|
||||||
"Realized Gain/Loss": "Realized Gain/Loss",
|
"Realized Gain/Loss": "Realized Gain/Loss",
|
||||||
"Dividends Earned": "Dividends Earned",
|
"Dividends Earned": "Dividends Earned",
|
||||||
|
"Dividends": "Dividends",
|
||||||
|
"Dividend options": "Dividend options",
|
||||||
|
"Dividend options saved": "Dividend options saved",
|
||||||
|
"Reinvest dividends": "Reinvest dividends",
|
||||||
|
"Automatically generate buy transactions for any dividends earned.": "Automatically generate buy transactions for any dividends earned.",
|
||||||
"Split": "Split",
|
"Split": "Split",
|
||||||
"Splits": "Splits",
|
"Splits": "Splits",
|
||||||
"No splits for :symbol yet": "No splits for :symbol yet",
|
"No splits for :symbol yet": "No splits for :symbol yet",
|
||||||
|
|||||||
@@ -11,11 +11,13 @@
|
|||||||
"Log in": "Iniciar sesión",
|
"Log in": "Iniciar sesión",
|
||||||
"Register": "Registrarse",
|
"Register": "Registrarse",
|
||||||
"Create": "Crear",
|
"Create": "Crear",
|
||||||
|
"Update": "Actualizar",
|
||||||
"Cancel": "Cancelar",
|
"Cancel": "Cancelar",
|
||||||
"Save": "Guardar",
|
"Save": "Guardar",
|
||||||
"Close": "Cerrar",
|
"Close": "Cerrar",
|
||||||
"or": "o",
|
"or": "o",
|
||||||
"and": "y",
|
"and": "y",
|
||||||
|
"Yes": "Sí",
|
||||||
"Nothing to show here yet": "No hay nada que mostrar aquí todavía",
|
"Nothing to show here yet": "No hay nada que mostrar aquí todavía",
|
||||||
|
|
||||||
"Hang on! You're doing that too much.": "¡Por favor espere un momento!",
|
"Hang on! You're doing that too much.": "¡Por favor espere un momento!",
|
||||||
@@ -116,6 +118,11 @@
|
|||||||
"Total Market Value": "Valor Total de Mercado",
|
"Total Market Value": "Valor Total de Mercado",
|
||||||
"Realized Gain/Loss": "Ganancia/Pérdida Realizada",
|
"Realized Gain/Loss": "Ganancia/Pérdida Realizada",
|
||||||
"Dividends Earned": "Dividendos Ganados",
|
"Dividends Earned": "Dividendos Ganados",
|
||||||
|
"Dividends": "Dividendos",
|
||||||
|
"Dividend options": "Opciones de dividendos",
|
||||||
|
"Dividend options saved": "Opciones de dividendos guardadas",
|
||||||
|
"Reinvest dividends": "Reinvertir dividendos",
|
||||||
|
"Automatically generate buy transactions for any dividends earned.": "Genere automáticamente transacciones de compra para cualquier dividendo obtenido.",
|
||||||
"Split": "Division",
|
"Split": "Division",
|
||||||
"Splits": "Divisiones",
|
"Splits": "Divisiones",
|
||||||
"No splits for :symbol yet": "No hay divisiones para :symbol",
|
"No splits for :symbol yet": "No hay divisiones para :symbol",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{{ $attributes->merge(['class' => 'bg-slate-100 dark:bg-base-200 rounded-lg']) }}
|
{{ $attributes->merge(['class' => 'bg-slate-100 dark:bg-base-200 rounded-lg']) }}
|
||||||
>
|
>
|
||||||
|
|
||||||
<h2 class="text-xl mb-2"> {{ $title }} </h2>
|
<h2 class="text-xl mb-2 flex items-center truncate"> {{ $title }} </h2>
|
||||||
|
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</x-card>
|
</x-card>
|
||||||
@@ -33,10 +33,10 @@
|
|||||||
<div class="mt-6 grid md:grid-cols-9 gap-5">
|
<div class="mt-6 grid md:grid-cols-9 gap-5">
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
{{ $holding->market_data->symbol }}
|
{{ $holding->market_data->symbol }}
|
||||||
<span class="text-sm"> {{ $holding->market_data->name }} </span>
|
<span class="text-sm ml-2"> {{ $holding->market_data->name }} </span>
|
||||||
</x-slot:title>
|
</x-slot:title>
|
||||||
|
|
||||||
@livewire('holding-market-data', ['holding' => $holding])
|
@livewire('holding-market-data', ['holding' => $holding])
|
||||||
@@ -100,15 +100,27 @@
|
|||||||
|
|
||||||
</x-ib-card>
|
</x-ib-card>
|
||||||
|
|
||||||
<x-ib-card title="{{ __('Dividends') }}" class="md:col-span-3">
|
<x-ib-card class="md:col-span-3">
|
||||||
|
|
||||||
|
<x-slot:title>
|
||||||
|
{{ __('Dividends') }}
|
||||||
|
|
||||||
|
<x-ib-flex-spacer/>
|
||||||
|
|
||||||
|
<x-button
|
||||||
|
title="{{ __('Dividend options') }}"
|
||||||
|
icon="o-ellipsis-vertical"
|
||||||
|
class="btn-circle btn-ghost btn-sm text-secondary"
|
||||||
|
@click="$dispatch('toggle-dividend-options')"
|
||||||
|
/>
|
||||||
|
</x-slot:title>
|
||||||
|
|
||||||
<x-ib-modal
|
<x-ib-modal
|
||||||
key="dividend-options"
|
key="dividend-options"
|
||||||
title="{{ __('Dividend options') }}"
|
title="{{ __('Dividend options') }}"
|
||||||
>
|
>
|
||||||
@livewire('manage-transaction-form', [
|
@livewire('holding-dividend-options-form', [
|
||||||
'portfolio' => $portfolio,
|
'holding' => $holding
|
||||||
'symbol' => $holding->market_data->symbol,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
</x-ib-modal>
|
</x-ib-modal>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Holding;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Livewire\Attributes\{Computed};
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
use Mary\Traits\Toast;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
new class extends Component {
|
||||||
|
use Toast;
|
||||||
|
|
||||||
|
// props
|
||||||
|
public Holding $holding;
|
||||||
|
|
||||||
|
public Bool $reinvest_dividends = false;
|
||||||
|
|
||||||
|
// methods
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
|
||||||
|
return [
|
||||||
|
'reinvest_dividends' => ['required', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
|
||||||
|
$this->reinvest_dividends = $this->holding?->reinvest_dividends ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
$this->holding->update($this->validate());
|
||||||
|
|
||||||
|
$this->success(__('Dividend options saved'));
|
||||||
|
|
||||||
|
$this->dispatch('toggle-dividend-options');
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div class="" x-data="{ }">
|
||||||
|
<x-ib-form wire:submit="save" class="">
|
||||||
|
|
||||||
|
<x-toggle
|
||||||
|
label="{{ __('Reinvest dividends') }}"
|
||||||
|
wire:model="reinvest_dividends"
|
||||||
|
right
|
||||||
|
hint="{{ __('Automatically generate buy transactions for any dividends earned.') }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<x-slot:actions>
|
||||||
|
|
||||||
|
<x-button
|
||||||
|
label="{{ __('Save') }}"
|
||||||
|
type="submit"
|
||||||
|
icon="o-paper-airplane"
|
||||||
|
class="btn-primary"
|
||||||
|
spinner="save"
|
||||||
|
/>
|
||||||
|
</x-slot:actions>
|
||||||
|
</x-ib-form>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -89,7 +89,7 @@ new class extends Component {
|
|||||||
@if (!$hideCancel)
|
@if (!$hideCancel)
|
||||||
<x-button label="{{ __('Cancel') }}" link="/dashboard" />
|
<x-button label="{{ __('Cancel') }}" link="/dashboard" />
|
||||||
@endif
|
@endif
|
||||||
<x-button label="{{ $portfolio ? 'Update' : 'Create' }}" type="submit" icon="o-paper-airplane" class="btn-primary" spinner="save" />
|
<x-button label="{{ $portfolio ? __('Update') : __('Create') }}" type="submit" icon="o-paper-airplane" class="btn-primary" spinner="save" />
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
</x-ib-form>
|
</x-ib-form>
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ new class extends Component {
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
<x-button
|
<x-button
|
||||||
label="{{ $transaction ? 'Update' : 'Create' }}"
|
label="{{ $transaction ? __('Update') : __('Create') }}"
|
||||||
type="submit"
|
type="submit"
|
||||||
icon="o-paper-airplane"
|
icon="o-paper-airplane"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ new class extends Component {
|
|||||||
{{ $row->date->format('M d, Y') }}
|
{{ $row->date->format('M d, Y') }}
|
||||||
@endscope
|
@endscope
|
||||||
@scope('cell_split', $row)
|
@scope('cell_split', $row)
|
||||||
{{ $row->split ? 'Yes' : '' }}
|
{{ $row->split ? __('Yes') : '' }}
|
||||||
@endscope
|
@endscope
|
||||||
@scope('cell_transaction_type', $row)
|
@scope('cell_transaction_type', $row)
|
||||||
<x-badge
|
<x-badge
|
||||||
|
|||||||
@@ -34,4 +34,31 @@ class DividendsTest extends TestCase
|
|||||||
|
|
||||||
$this->assertEquals(4.95, $holding->dividends_earned);
|
$this->assertEquals(4.95, $holding->dividends_earned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
public function test_new_dividends_are_reinvested(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($user = User::factory()->create());
|
||||||
|
|
||||||
|
$portfolio = Portfolio::factory()->create();
|
||||||
|
Transaction::factory()->buy()->yearsAgo()->portfolio($portfolio->id)->symbol('ACME')->create();
|
||||||
|
|
||||||
|
$holding = Holding::query()->portfolio($portfolio->id)->symbol('ACME')->first();
|
||||||
|
$holding->reinvest_dividends = true;
|
||||||
|
$holding->save();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $holding->dividends_earned);
|
||||||
|
|
||||||
|
Dividend::refreshDividendData('ACME');
|
||||||
|
|
||||||
|
$transactions = Transaction::where(['reinvested_dividend' => true])->symbol('ACME')->portfolio($portfolio->id)->get();
|
||||||
|
|
||||||
|
$dividendsReinvested = $transactions->reduce(function ($carry, $transaction) {
|
||||||
|
return $carry + ($transaction->cost_basis * $transaction->quantity);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
$this->assertCount(3, $transactions);
|
||||||
|
$this->assertEqualsWithDelta(4.95, $dividendsReinvested, 0.01);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user