diff --git a/app/Models/Holding.php b/app/Models/Holding.php index b217aa8..52c35c0 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -246,8 +246,6 @@ class Holding extends Model return $query->select([ 'holdings.symbol', 'holdings.portfolio_id', - 'transactions_display.total_cost_basis', - 'transactions_display.realized_gain_dollars', 'dividends_display.total_dividends_earned', ]) ->groupBy([ @@ -255,8 +253,6 @@ class Holding extends Model 'holdings.quantity', 'holdings.portfolio_id', 'cr.rate', - 'transactions_display.total_cost_basis', - 'transactions_display.realized_gain_dollars', 'dividends_display.total_dividends_earned', 'market_data.market_value_base', ]) @@ -264,10 +260,10 @@ class Holding extends Model $join->where('cr.currency', '=', $currency); if (config('database.default') === 'sqlite') { - - $join->whereRaw("strftime('%Y-%m-%d', cr.date) = ?", [now()->toDateString()]); + $join->whereRaw("strftime('%Y-%m-%d', cr.date) = ?", [ + now()->toDateString(), + ]); } else { - $join->on('cr.date', '=', DB::raw("'".now()->toDateString()."'")); } }) @@ -277,16 +273,29 @@ class Holding extends Model ->selectRaw( 'holdings.quantity * market_data.market_value_base * COALESCE(cr.rate, 1) AS total_market_value' ) - ->selectRaw('( - holdings.quantity * market_data.market_value_base * COALESCE(cr.rate, 1) - ) - transactions_display.total_cost_basis as total_gain_dollars') + ->selectRaw( + 'SUM(transactions_display.realized_gain_dollars) * COALESCE(cr.rate, 1) AS realized_gain_dollars' + ) + ->selectRaw( + '(SUM(transactions_display.total_cost_basis) / SUM(transactions_display.total_purchases)) * holdings.quantity AS total_cost_basis' + ) + ->selectRaw( + '( + holdings.quantity * market_data.market_value_base * COALESCE(cr.rate, 1) + ) - (SUM(transactions_display.total_cost_basis) / SUM(transactions_display.total_purchases)) * holdings.quantity AS total_gain_dollars' + ) ->leftJoinSub( DB::table('transactions') ->leftJoin('currency_rates as cr', function ($join) use ($currency) { - $join->on('cr.date', '=', 'transactions.date') + $join + ->on('cr.date', '=', 'transactions.date') ->where('cr.currency', '=', $currency); }) - ->select(['transactions.symbol', 'transactions.portfolio_id']) + ->select([ + 'transactions.id', + 'transactions.symbol', + 'transactions.portfolio_id', + ]) ->leftJoinSub( DB::table('transactions') ->leftJoin('currency_rates as cr', function ($join) use ($currency) { @@ -298,71 +307,87 @@ class Holding extends Model 'transactions.symbol', 'transactions.portfolio_id', 'transactions.quantity', + 'transactions.cost_basis_base', 'transactions.date', ]) - ->selectRaw( - "(CASE - WHEN - transactions.transaction_type = 'BUY' - OR SUM(transactions.cost_basis_base) = 0 - THEN - COALESCE(cr.rate, 1) - ELSE ( - SELECT - SUM(COALESCE(cr2.rate, 1) * buy.cost_basis_base) - / SUM(buy.cost_basis_base) - FROM transactions as buy - LEFT JOIN currency_rates as cr2 - ON cr2.date = buy.date - AND cr2.currency = '{$currency}' - WHERE buy.symbol = transactions.symbol - AND buy.portfolio_id = transactions.portfolio_id - AND buy.transaction_type = 'BUY' - AND buy.date <= transactions.date - ) END) - AS rate" - ) - ->selectRaw( - "CASE - WHEN transactions.transaction_type = 'BUY' - THEN transactions.quantity - ELSE -transactions.quantity - END - AS remaining_quantity" - ) + ->selectRaw(" + (CASE + WHEN + transactions.transaction_type = 'BUY' + OR SUM(transactions.cost_basis_base) = 0 + THEN + COALESCE(cr.rate, 1) + ELSE ( + SELECT + SUM(COALESCE(cr2.rate, 1) * buy.cost_basis_base) + / SUM(buy.cost_basis_base) + FROM transactions as buy + LEFT JOIN currency_rates as cr2 + ON cr2.date = buy.date + AND cr2.currency = '{$currency}' + WHERE buy.symbol = transactions.symbol + AND buy.portfolio_id = transactions.portfolio_id + AND buy.transaction_type = 'BUY' + AND buy.date <= transactions.date + ) END) + AS rate") ->groupBy([ + 'transactions.id', 'transactions.symbol', 'transactions.date', 'transactions.portfolio_id', 'transactions.transaction_type', + 'transactions.cost_basis_base', 'transactions.quantity', 'cr.rate', - ]), 'cost_basis_display', function ($join) { - $join->on('transactions.symbol', '=', 'cost_basis_display.symbol') - ->on('transactions.portfolio_id', '=', 'cost_basis_display.portfolio_id') - ->on('transactions.date', '=', 'cost_basis_display.date'); - }) - ->selectRaw( - "SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN (transactions.sale_price_base - transactions.cost_basis_base) * transactions.quantity * COALESCE(cr.rate, 1) ELSE 0 END) AS realized_gain_dollars" + ]), + 'cost_basis_display', + function ($join) { + $join + ->on('transactions.symbol', '=', 'cost_basis_display.symbol') + ->on( + 'transactions.portfolio_id', + '=', + 'cost_basis_display.portfolio_id' + ) + ->on('transactions.date', '=', 'cost_basis_display.date'); + } ) ->selectRaw( - "SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.cost_basis_base * transactions.quantity * cost_basis_display.rate END) / SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity END) * SUM(cost_basis_display.remaining_quantity) AS total_cost_basis" + "CASE WHEN transactions.transaction_type = 'SELL' THEN (transactions.sale_price_base - transactions.cost_basis_base) * transactions.quantity * COALESCE(cr.rate, 1) END AS realized_gain_dollars" ) - ->groupBy(['transactions.symbol', 'transactions.portfolio_id']), + ->selectRaw( + "CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.cost_basis_base * transactions.quantity * cost_basis_display.rate END AS total_cost_basis" + ) + ->selectRaw( + "CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity END AS total_purchases" + ) + ->groupBy([ + 'transactions.id', + 'transactions.symbol', + 'transactions.portfolio_id', + 'transactions.cost_basis_base', + 'transactions.quantity', + 'cost_basis_display.rate', + 'cr.rate', + ]), 'transactions_display', function ($join) { - $join->on('holdings.symbol', '=', 'transactions_display.symbol') + $join + ->on('holdings.symbol', '=', 'transactions_display.symbol') ->on('holdings.portfolio_id', '=', 'transactions_display.portfolio_id'); } ) ->leftJoinSub( DB::table('dividends') ->join('transactions as tx', function ($join) { - $join->on('tx.symbol', '=', 'dividends.symbol') + $join + ->on('tx.symbol', '=', 'dividends.symbol') ->on('tx.date', '<=', 'dividends.date'); }) ->leftJoin('currency_rates as cr', function ($join) use ($currency) { - $join->on('cr.date', '=', 'dividends.date') + $join + ->on('cr.date', '=', 'dividends.date') ->where('cr.currency', '=', $currency); }) ->select(['dividends.symbol']) @@ -375,6 +400,7 @@ class Holding extends Model $join->on('holdings.symbol', '=', 'dividends_display.symbol'); } ); + } public function syncTransactionsAndDividends() diff --git a/tests/HoldingsTest.php b/tests/HoldingsTest.php index 834e702..f0ea865 100644 --- a/tests/HoldingsTest.php +++ b/tests/HoldingsTest.php @@ -43,4 +43,20 @@ class HoldingsTest extends TestCase $holding = Holding::query()->getPortfolioMetrics(); $this->assertEquals(0, $holding->get('total_cost_basis')); } + + public function test_calculates_cost_bases_on_same_day_buy_sell_transaction(): void + { + $this->actingAs($user = User::factory()->create()); + + $portfolio = Portfolio::factory()->create(); + + Transaction::factory(2)->buy()->lastYear()->costBasis(100)->portfolio($portfolio->id)->symbol('AAPL')->create(); + Transaction::factory(2)->buy()->lastYear()->costBasis(300)->portfolio($portfolio->id)->symbol('AAPL')->create(); + + Transaction::factory()->sell()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create(); + Transaction::factory()->sell()->recent()->portfolio($portfolio->id)->symbol('AAPL')->create(); + + $holding = Holding::query()->getPortfolioMetrics(); + $this->assertEquals(400, $holding->get('total_cost_basis')); + } }