From 4b29ccfc4c9eac0189b7e015500ac7ee5264e8e1 Mon Sep 17 00:00:00 2001 From: osteel Date: Wed, 13 Sep 2023 14:07:05 +0100 Subject: [PATCH] fixed DisposalBuilder and ReversionFinder --- .../Entities/SharePoolingAssetAcquisition.php | 15 +- .../Entities/SharePoolingAssetDisposals.php | 10 -- .../DisposalBuilder/DisposalBuilder.php | 43 +---- .../ReversionFinder/ReversionFinder.php | 19 +- .../SharePoolingAsset/SharePoolingAsset.php | 8 +- .../SharePoolingAssetAcquisitionTest.php | 34 ++-- .../SharePoolingAssetDisposalsTest.php | 16 -- .../DisposalBuilder/DisposalBuilderTest.php | 4 +- .../QuantityAdjuster/QuantityAdjusterTest.php | 98 +++++++++- .../SharePoolingAssetTest.php | 170 +++++++++++++++--- 10 files changed, 296 insertions(+), 121 deletions(-) diff --git a/domain/src/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetAcquisition.php b/domain/src/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetAcquisition.php index af633a3..10673b3 100644 --- a/domain/src/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetAcquisition.php +++ b/domain/src/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetAcquisition.php @@ -74,8 +74,12 @@ public function increaseSameDayQuantity(Quantity $quantity): self return $this; } - /** Increase the same-day quantity and adjust the 30-day quantity accordingly. */ - public function increaseSameDayQuantityUpToAvailableQuantity(Quantity $quantity): self + /** + * Increase the same-day quantity and adjust the 30-day quantity accordingly. + * + * @return Quantity the added quantity + */ + public function increaseSameDayQuantityUpToAvailableQuantity(Quantity $quantity): Quantity { // Adjust same-day quantity $quantityToAdd = Quantity::minimum($quantity, $this->availableSameDayQuantity()); @@ -85,7 +89,7 @@ public function increaseSameDayQuantityUpToAvailableQuantity(Quantity $quantity) $quantityToDeduct = Quantity::minimum($quantityToAdd, $this->thirtyDayQuantity); $this->thirtyDayQuantity = $this->thirtyDayQuantity->minus($quantityToDeduct); - return $this; + return $quantityToAdd; } /** @throws SharePoolingAssetAcquisitionException */ @@ -111,12 +115,13 @@ public function increaseThirtyDayQuantity(Quantity $quantity): self return $this; } - public function increaseThirtyDayQuantityUpToAvailableQuantity(Quantity $quantity): self + /** @return Quantity the added quantity */ + public function increaseThirtyDayQuantityUpToAvailableQuantity(Quantity $quantity): Quantity { $quantityToAdd = Quantity::minimum($quantity, $this->availableThirtyDayQuantity()); $this->thirtyDayQuantity = $this->thirtyDayQuantity->plus($quantityToAdd); - return $this; + return $quantityToAdd; } /** @throws SharePoolingAssetAcquisitionException */ diff --git a/domain/src/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetDisposals.php b/domain/src/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetDisposals.php index 42ae8c0..6d36960 100644 --- a/domain/src/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetDisposals.php +++ b/domain/src/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetDisposals.php @@ -65,16 +65,6 @@ public function reverse(): SharePoolingAssetDisposals return new self(array_reverse($this->disposals)); } - public function unprocessed(): SharePoolingAssetDisposals - { - $disposals = array_filter( - $this->disposals, - fn (SharePoolingAssetDisposal $disposal) => ! $disposal->processed, - ); - - return self::make(...$disposals); - } - public function withAvailableSameDayQuantity(): SharePoolingAssetDisposals { $disposals = array_filter( diff --git a/domain/src/Aggregates/SharePoolingAsset/Services/DisposalBuilder/DisposalBuilder.php b/domain/src/Aggregates/SharePoolingAsset/Services/DisposalBuilder/DisposalBuilder.php index 3835ee6..d44d3b3 100644 --- a/domain/src/Aggregates/SharePoolingAsset/Services/DisposalBuilder/DisposalBuilder.php +++ b/domain/src/Aggregates/SharePoolingAsset/Services/DisposalBuilder/DisposalBuilder.php @@ -18,7 +18,7 @@ */ final class DisposalBuilder { - public static function process( + public static function make( DisposeOfSharePoolingAsset $disposal, SharePoolingAssetTransactions $transactions, ): SharePoolingAssetDisposal { @@ -107,13 +107,10 @@ private static function processSameDayAcquisitions( // Deduct the applied quantity from the same-day acquisitions $remainder = $availableSameDayQuantity; foreach ($sameDayAcquisitions as $acquisition) { - $quantityToAllocate = Quantity::minimum($remainder, $acquisition->availableSameDayQuantity()); + $quantityToAllocate = $acquisition->increaseSameDayQuantityUpToAvailableQuantity($remainder); $sameDayQuantityAllocation->allocateQuantity($quantityToAllocate, $acquisition); - $acquisition->increaseSameDayQuantityUpToAvailableQuantity($remainder); - - $remainder = $remainder->minus($quantityToAllocate); - if ($remainder->isZero()) { + if (($remainder = $remainder->minus($quantityToAllocate))->isZero()) { break; } } @@ -146,37 +143,13 @@ private static function processAcquisitionsWithinThirtyDays( foreach ($withinThirtyDaysAcquisitions as $acquisition) { // Apply the acquisition's cost basis to the disposed of asset up to the remaining quantity - $thirtyDayQuantityToApply = Quantity::minimum($acquisition->availableThirtyDayQuantity(), $remainingQuantity); - - // Also deduct same-day disposals with available same-day quantity that haven't been processed yet - $sameDayDisposals = $transactions->disposalsMadeOn($acquisition->date) - ->unprocessed() - ->withAvailableSameDayQuantity(); - - foreach ($sameDayDisposals as $disposal) { - $sameDayQuantityToApply = Quantity::minimum($disposal->availableSameDayQuantity(), $thirtyDayQuantityToApply); - $disposal->sameDayQuantityAllocation->allocateQuantity($sameDayQuantityToApply, $acquisition); - $acquisition->increaseSameDayQuantityUpToAvailableQuantity($sameDayQuantityToApply); - $thirtyDayQuantityToApply = $thirtyDayQuantityToApply->minus($sameDayQuantityToApply); - if ($thirtyDayQuantityToApply->isZero()) { - break; - } - } - - if ($thirtyDayQuantityToApply->isZero()) { - continue; - } - + $quantityToAllocate = $acquisition->increaseThirtyDayQuantityUpToAvailableQuantity($remainingQuantity); $averageCostBasisPerUnit = $acquisition->averageCostBasisPerUnit(); + $costBasis = $costBasis->plus($averageCostBasisPerUnit->multipliedBy($quantityToAllocate)); + $thirtyDayQuantityAllocation->allocateQuantity($quantityToAllocate, $acquisition); - $costBasis = $costBasis->plus($averageCostBasisPerUnit->multipliedBy($thirtyDayQuantityToApply)); - - $thirtyDayQuantityAllocation->allocateQuantity($thirtyDayQuantityToApply, $acquisition); - $acquisition->increaseThirtyDayQuantityUpToAvailableQuantity($thirtyDayQuantityToApply); - $remainingQuantity = $remainingQuantity->minus($thirtyDayQuantityToApply); - - // Continue until there are no more transactions or we've covered all disposed tokens - if ($remainingQuantity->isZero()) { + // Continue until there are no more transactions or we've covered all disposed of tokens + if (($remainingQuantity = $remainingQuantity->minus($quantityToAllocate))->isZero()) { break; } } diff --git a/domain/src/Aggregates/SharePoolingAsset/Services/ReversionFinder/ReversionFinder.php b/domain/src/Aggregates/SharePoolingAsset/Services/ReversionFinder/ReversionFinder.php index c05046d..7d2a24e 100644 --- a/domain/src/Aggregates/SharePoolingAsset/Services/ReversionFinder/ReversionFinder.php +++ b/domain/src/Aggregates/SharePoolingAsset/Services/ReversionFinder/ReversionFinder.php @@ -38,19 +38,20 @@ private static function addSameDayDisposalsToRevert( return $disposalsToRevert; } - // Get same-day disposals with part of their quantity not allocated to same-day acquisitions - $sameDayDisposals = $transactions->disposalsMadeOn($date)->withAvailableSameDayQuantity(); + // Get all same-day disposals. As the average cost basis of same-day acquisitions is used to + // calculate the cost basis of the disposals, it's simpler to revert them all and start over + $sameDayDisposals = $transactions->disposalsMadeOn($date); if ($sameDayDisposals->isEmpty()) { return self::add30DayDisposalsToRevert($disposalsToRevert, $transactions, $date, $remainingQuantity); } - // As the average cost basis of same-day acquisitions is used to calculate the - // cost basis of the disposals, it's simpler to revert them all and start over $disposalsToRevert->add(...$sameDayDisposals); - // Deduct what's left (either the whole remaining quantity or the disposals' unallocated - // same-day quantity, whichever is smaller) from the remaining quantity to be allocated + // Deduct either the acquisition's remaining quantity or the disposals' unallocated same-day + // quantity (whichever is smaller) from the quantity to be allocated. The trick here is that, + // while we are going to revert and replay all same-day disposals, their same-day quantity + // already allocated to earlier same-day acquisitions must be taken into account, still $quantityToDeduct = Quantity::minimum($remainingQuantity, $sameDayDisposals->availableSameDayQuantity()); $remainingQuantity = $remainingQuantity->minus($quantityToDeduct); @@ -74,8 +75,8 @@ private static function add30DayDisposalsToRevert( foreach ($pastThirtyDaysDisposals as $disposal) { $disposalsToRevert->add($disposal); - // Deduct what's left (either the whole remaining quantity or the disposal's uncallocated - // 30-day quantity, whichever is smaller) from the remaining quantity to be allocated + // Deduct either the acquisition's remaining quantity or the disposal's uncallocated + // 30-day quantity (whichever is smaller) from the quantity to be allocated $quantityToDeduct = Quantity::minimum($remainingQuantity, $disposal->availableThirtyDayQuantity()); $remainingQuantity = $remainingQuantity->minus($quantityToDeduct); @@ -106,7 +107,7 @@ public static function disposalsToRevertOnDisposal( // Add disposals up to the disposal's quantity, starting with the most recent ones. That is // because older disposals get priority when allocating the 30-day quantity of acquisitions // made within 30 days of the disposal, so the last disposals in are the first out - $disposalsWithAllocatedThirtyDayQuantity = $transactions->processed() + $disposalsWithAllocatedThirtyDayQuantity = $transactions ->disposalsWithThirtyDayQuantityAllocatedTo($acquisition) ->reverse(); diff --git a/domain/src/Aggregates/SharePoolingAsset/SharePoolingAsset.php b/domain/src/Aggregates/SharePoolingAsset/SharePoolingAsset.php index 525106f..78c3653 100644 --- a/domain/src/Aggregates/SharePoolingAsset/SharePoolingAsset.php +++ b/domain/src/Aggregates/SharePoolingAsset/SharePoolingAsset.php @@ -66,7 +66,7 @@ public function acquire(AcquireSharePoolingAsset $action): void $disposalsToRevert = ReversionFinder::disposalsToRevertOnAcquisition( acquisition: $action, - transactions: $this->transactions->copy(), + transactions: $this->transactions, ); $disposalsToRevert->isEmpty() || $this->revertDisposals($disposalsToRevert); @@ -96,7 +96,7 @@ public function applySharePoolingAssetAcquired(SharePoolingAssetAcquired $event) // acquisition's same-day and 30-day quantities, these updates occur before the acquisition's event // is recorded, meaning the event is stored with the updated quantities. As a result, whenever the // aggregate is recreated from its events, the acquisition already has a same-day and/or 30-day - // quantity, but upon replaying the subsequent disposals, these quantities are updated *again*. + // quantity, but upon replaying the subsequent disposals, these quantities are updated *again* $this->transactions->add(clone $event->acquisition); } @@ -111,12 +111,12 @@ public function disposeOf(DisposeOfSharePoolingAsset $action): void $disposalsToRevert = ReversionFinder::disposalsToRevertOnDisposal( disposal: $action, - transactions: $this->transactions->copy(), + transactions: $this->transactions, ); $disposalsToRevert->isEmpty() || $this->revertDisposals($disposalsToRevert); - $sharePoolingAssetDisposal = DisposalBuilder::process( + $sharePoolingAssetDisposal = DisposalBuilder::make( disposal: $action, transactions: $this->transactions->copy(), ); diff --git a/domain/tests/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetAcquisitionTest.php b/domain/tests/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetAcquisitionTest.php index df87ba4..dc00334 100644 --- a/domain/tests/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetAcquisitionTest.php +++ b/domain/tests/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetAcquisitionTest.php @@ -109,7 +109,12 @@ 'scenario 2' => ['10', '40'], ]); -it('can increase the same-day quantity up to the available quantity', function (string $increase, string $sameDayQuantity, string $thirtyDayQuantity) { +it('can increase the same-day quantity up to the available quantity', function ( + string $increase, + string $sameDayQuantity, + string $thirtyDayQuantity, + string $addedQuantity, +) { /** @var SharePoolingAssetAcquisition */ $acquisition = SharePoolingAssetAcquisition::factory()->make([ 'quantity' => new Quantity('100'), @@ -117,15 +122,16 @@ 'thirtyDayQuantity' => new Quantity('60'), ]); - $acquisition->increaseSameDayQuantityUpToAvailableQuantity(new Quantity($increase)); + $added = $acquisition->increaseSameDayQuantityUpToAvailableQuantity(new Quantity($increase)); expect((string) $acquisition->sameDayQuantity())->toBe($sameDayQuantity); expect((string) $acquisition->thirtyDayQuantity())->toBe($thirtyDayQuantity); + expect((string) $added)->toBe($addedQuantity); })->with([ - 'scenario 1' => ['5', '35', '55'], - 'scenario 2' => ['10', '40', '50'], - 'scenario 3' => ['70', '100', '0'], - 'scenario 4' => ['71', '100', '0'], + 'scenario 1' => ['5', '35', '55', '5'], + 'scenario 2' => ['10', '40', '50', '10'], + 'scenario 3' => ['70', '100', '0', '70'], + 'scenario 4' => ['71', '100', '0', '70'], ]); it('cannot decrease the same-day quantity because the quantity is too great', function () { @@ -185,7 +191,12 @@ 'scenario 2' => ['10', '70'], ]); -it('can increase the 30-day quantity up to the available quantity', function (string $increase, string $sameDayQuantity, string $thirtyDayQuantity) { +it('can increase the 30-day quantity up to the available quantity', function ( + string $increase, + string $sameDayQuantity, + string $thirtyDayQuantity, + string $addedQuantity, +) { /** @var SharePoolingAssetAcquisition */ $acquisition = SharePoolingAssetAcquisition::factory()->make([ 'quantity' => new Quantity('100'), @@ -193,14 +204,15 @@ 'thirtyDayQuantity' => new Quantity('60'), ]); - $acquisition->increaseThirtyDayQuantityUpToAvailableQuantity(new Quantity($increase)); + $added = $acquisition->increaseThirtyDayQuantityUpToAvailableQuantity(new Quantity($increase)); expect((string) $acquisition->sameDayQuantity())->toBe($sameDayQuantity); expect((string) $acquisition->thirtyDayQuantity())->toBe($thirtyDayQuantity); + expect((string) $added)->toBe($addedQuantity); })->with([ - 'scenario 1' => ['5', '30', '65'], - 'scenario 2' => ['10', '30', '70'], - 'scenario 3' => ['15', '30', '70'], + 'scenario 1' => ['5', '30', '65', '5'], + 'scenario 2' => ['10', '30', '70', '10'], + 'scenario 3' => ['15', '30', '70', '10'], ]); it('cannot decrease the 30-day quantity because the quantity is too great', function () { diff --git a/domain/tests/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetDisposalsTest.php b/domain/tests/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetDisposalsTest.php index dfd88c7..04310c9 100644 --- a/domain/tests/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetDisposalsTest.php +++ b/domain/tests/Aggregates/SharePoolingAsset/Entities/SharePoolingAssetDisposalsTest.php @@ -69,22 +69,6 @@ expect($disposals->getIterator()[2])->toBe($disposal1); }); -it('can return the unprocessed disposals from a collection of disposals', function () { - /** @var list */ - $items = [ - SharePoolingAssetDisposal::factory()->make(), - $unprocessed1 = SharePoolingAssetDisposal::factory()->unprocessed()->make(), - SharePoolingAssetDisposal::factory()->make(), - $unprocessed2 = SharePoolingAssetDisposal::factory()->unprocessed()->make(), - ]; - - $disposals = SharePoolingAssetDisposals::make(...$items)->unprocessed(); - - expect($disposals->count())->toBeInt()->toBe(2); - expect($disposals->getIterator()[0])->toBe($unprocessed1); - expect($disposals->getIterator()[1])->toBe($unprocessed2); -}); - it('can return the disposals with available same-day quantity from a collection of disposals', function () { /** @var list */ $items = [ diff --git a/domain/tests/Aggregates/SharePoolingAsset/Services/DisposalBuilder/DisposalBuilderTest.php b/domain/tests/Aggregates/SharePoolingAsset/Services/DisposalBuilder/DisposalBuilderTest.php index 6a3abde..b0460bc 100644 --- a/domain/tests/Aggregates/SharePoolingAsset/Services/DisposalBuilder/DisposalBuilderTest.php +++ b/domain/tests/Aggregates/SharePoolingAsset/Services/DisposalBuilder/DisposalBuilderTest.php @@ -20,7 +20,7 @@ forFiat: false, ); - $disposal = DisposalBuilder::process( + $disposal = DisposalBuilder::make( disposal: $action, transactions: SharePoolingAssetTransactions::make(SharePoolingAssetAcquisition::factory()->make()), ); @@ -40,7 +40,7 @@ $acquisition = SharePoolingAssetAcquisition::factory()->make(['date' => LocalDate::parse('2021-10-22')]); - $disposal = DisposalBuilder::process($action, SharePoolingAssetTransactions::make($acquisition)); + $disposal = DisposalBuilder::make($action, SharePoolingAssetTransactions::make($acquisition)); expect($disposal->id)->toBe($id); }); diff --git a/domain/tests/Aggregates/SharePoolingAsset/Services/QuantityAdjuster/QuantityAdjusterTest.php b/domain/tests/Aggregates/SharePoolingAsset/Services/QuantityAdjuster/QuantityAdjusterTest.php index f616230..b509ac1 100644 --- a/domain/tests/Aggregates/SharePoolingAsset/Services/QuantityAdjuster/QuantityAdjusterTest.php +++ b/domain/tests/Aggregates/SharePoolingAsset/Services/QuantityAdjuster/QuantityAdjusterTest.php @@ -1,5 +1,7 @@ withSameDayQuantity(Quantity::zero(), $id = SharePoolingAssetTransactionId::fromString('foo')) ->make(); - expect(fn () => QuantityAdjuster::revertDisposal($disposal, SharePoolingAssetTransactions::make())) + expect(fn () => QuantityAdjuster::$method($disposal, SharePoolingAssetTransactions::make())) ->toThrow(QuantityAdjusterException::class, QuantityAdjusterException::transactionNotFound($id)->getMessage()); -}); +})->with(['applyDisposal', 'revertDisposal']); -it('cannot revert a disposal because a transaction is not an acquisition', function () { +it('cannot process a disposal because a transaction is not an acquisition', function (string $method) { $disposal = SharePoolingAssetDisposal::factory() ->withSameDayQuantity(Quantity::zero(), $id = SharePoolingAssetTransactionId::fromString('foo')) ->make(); $transactions = SharePoolingAssetTransactions::make(SharePoolingAssetDisposal::factory()->make(['id' => $id])); - expect(fn () => QuantityAdjuster::revertDisposal($disposal, $transactions)) + expect(fn () => QuantityAdjuster::$method($disposal, $transactions)) ->toThrow(QuantityAdjusterException::class, QuantityAdjusterException::notAnAcquisition($id)->getMessage()); -}); +})->with(['applyDisposal', 'revertDisposal']); + +it('cannot apply a disposal because of insufficient available quantity to increase', function ( + string $method, + string $quantity, + string $exception +) { + $disposal = SharePoolingAssetDisposal::factory() + ->{$method}(new Quantity($quantity), $id = SharePoolingAssetTransactionId::fromString('foo')) + ->make(); + + $transactions = SharePoolingAssetTransactions::make(SharePoolingAssetAcquisition::factory()->make([ + 'id' => $id, + 'quantity' => $available = new Quantity('10'), + ])); + + expect(fn () => QuantityAdjuster::applyDisposal($disposal, $transactions))->toThrow( + SharePoolingAssetAcquisitionException::class, + SharePoolingAssetAcquisitionException::$exception(new Quantity($quantity), $available)->getMessage(), + ); +})->with([ + 'same-day' => ['withSameDayQuantity', '11', 'insufficientSameDayQuantityToIncrease'], + '30-day' => ['withThirtyDayQuantity', '11', 'insufficientThirtyDayQuantityToIncrease'], +]); + +it('can apply a disposal', function (string $factoryMethod, string $method) { + $disposal = SharePoolingAssetDisposal::factory() + ->{$factoryMethod}(new Quantity('10'), $id = SharePoolingAssetTransactionId::fromString('foo')) + ->make(); + + $transactions = SharePoolingAssetTransactions::make(SharePoolingAssetAcquisition::factory()->make([ + 'id' => $id, + 'quantity' => new Quantity('10'), + ])); + + expect((string) $transactions->first()->{$method}())->toBe('0'); + + QuantityAdjuster::applyDisposal($disposal, $transactions); + + expect((string) $transactions->first()->{$method}())->toBe('10'); +})->with([ + 'same-day' => ['withSameDayQuantity', 'sameDayQuantity'], + '30-day' => ['withThirtyDayQuantity', 'thirtyDayQuantity'], +]); + +it('cannot revert a disposal because of insufficient available quantity to decrease', function ( + string $method, + string $quantity, + string $exception +) { + $disposal = SharePoolingAssetDisposal::factory() + ->{$method}(new Quantity($quantity), $id = SharePoolingAssetTransactionId::fromString('foo')) + ->make(); + + $transactions = SharePoolingAssetTransactions::make(SharePoolingAssetAcquisition::factory()->make(['id' => $id])); + + expect(fn () => QuantityAdjuster::revertDisposal($disposal, $transactions))->toThrow( + SharePoolingAssetAcquisitionException::class, + SharePoolingAssetAcquisitionException::$exception(new Quantity($quantity), Quantity::zero())->getMessage(), + ); +})->with([ + 'same-day' => ['withSameDayQuantity', '1', 'insufficientSameDayQuantityToDecrease'], + '30-day' => ['withThirtyDayQuantity', '1', 'insufficientThirtyDayQuantityToDecrease'], +]); + +it('can revert a disposal', function (string $factoryMethod, string $method) { + $disposal = SharePoolingAssetDisposal::factory() + ->{$factoryMethod}(new Quantity('10'), $id = SharePoolingAssetTransactionId::fromString('foo')) + ->make(); + + $transactions = SharePoolingAssetTransactions::make(SharePoolingAssetAcquisition::factory()->make([ + 'id' => $id, + 'sameDayQuantity' => new Quantity('10'), + 'thirtyDayQuantity' => new Quantity('10'), + ])); + + expect((string) $transactions->first()->{$method}())->toBe('10'); + + QuantityAdjuster::revertDisposal($disposal, $transactions); + + expect((string) $transactions->first()->{$method}())->toBe('0'); +})->with([ + 'same-day' => ['withSameDayQuantity', 'sameDayQuantity'], + '30-day' => ['withThirtyDayQuantity', 'thirtyDayQuantity'], +]); diff --git a/domain/tests/Aggregates/SharePoolingAsset/SharePoolingAssetTest.php b/domain/tests/Aggregates/SharePoolingAsset/SharePoolingAssetTest.php index 06b0cc0..e8494ed 100644 --- a/domain/tests/Aggregates/SharePoolingAsset/SharePoolingAssetTest.php +++ b/domain/tests/Aggregates/SharePoolingAsset/SharePoolingAssetTest.php @@ -718,7 +718,7 @@ ); }); -it('can acquire a same-day share pooling asset several times on the same day as its disposal', function () { +it('can acquire a share pooling asset several times on the same day as its disposal', function () { given(new SharePoolingAssetFiatCurrencySet(FiatCurrency::GBP)); given(new SharePoolingAssetAcquired( @@ -792,7 +792,7 @@ ); }); -it('can dispose of a same-day share pooling asset several times on the same day as several acquisitions', function () { +it('can dispose of a share pooling asset several times on the same day as several acquisitions', function () { given(new SharePoolingAssetFiatCurrencySet(FiatCurrency::GBP)); given(new SharePoolingAssetAcquired( @@ -872,7 +872,7 @@ )); }); -it('can acquire and dispose of a share pooling asset several times on the same day and within 30 days of a disposal', function () { +it('can dispose of a share pooling asset several times on the same day and within 30 days of a disposal', function () { given(new SharePoolingAssetFiatCurrencySet(FiatCurrency::GBP)); given(new SharePoolingAssetAcquired( @@ -887,9 +887,9 @@ given($sharePoolingAssetDisposedOf1 = new SharePoolingAssetDisposedOf( disposal: new SharePoolingAssetDisposal( date: LocalDate::parse('2015-10-22'), - quantity: new Quantity('50'), - costBasis: FiatAmount::GBP('50'), - proceeds: FiatAmount::GBP('75'), + quantity: new Quantity('30'), + costBasis: FiatAmount::GBP('30'), + proceeds: FiatAmount::GBP('50'), forFiat: false, ), )); @@ -898,9 +898,9 @@ given($sharePoolingAssetAcquired2 = new SharePoolingAssetAcquired( acquisition: new SharePoolingAssetAcquisition( - date: LocalDate::parse('2015-10-25'), - quantity: new Quantity('25'), - costBasis: FiatAmount::GBP('20'), + date: LocalDate::parse('2015-10-23'), + quantity: new Quantity('20'), + costBasis: FiatAmount::GBP('40'), forFiat: false, ), )); @@ -908,20 +908,20 @@ given($sharePoolingAssetDisposedOf1Corrected1 = new SharePoolingAssetDisposedOf( disposal: SharePoolingAssetDisposal::factory() ->copyFrom($sharePoolingAssetDisposedOf1->disposal) - ->withThirtyDayQuantity(new Quantity('25'), id: $sharePoolingAssetAcquired2->acquisition->id) - ->make(['costBasis' => FiatAmount::GBP('45')]), + ->withThirtyDayQuantity(new Quantity('20'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->make(['costBasis' => FiatAmount::GBP('50')]), )); given(new SharePoolingAssetDisposalReverted(disposal: $sharePoolingAssetDisposedOf1Corrected1->disposal)); given(new SharePoolingAssetDisposedOf( disposal: SharePoolingAssetDisposal::factory() - ->withSameDayQuantity(new Quantity('5'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->withSameDayQuantity(new Quantity('10'), id: $sharePoolingAssetAcquired2->acquisition->id) ->make([ - 'date' => LocalDate::parse('2015-10-25'), - 'quantity' => new Quantity('5'), - 'costBasis' => FiatAmount::GBP('4'), - 'proceeds' => FiatAmount::GBP('10'), + 'date' => LocalDate::parse('2015-10-23'), + 'quantity' => new Quantity('10'), + 'costBasis' => FiatAmount::GBP('20'), + 'proceeds' => FiatAmount::GBP('30'), 'forFiat' => false, ]), )); @@ -929,16 +929,16 @@ given($sharePoolingAssetDisposedOf1Corrected2 = new SharePoolingAssetDisposedOf( disposal: SharePoolingAssetDisposal::factory() ->copyFrom($sharePoolingAssetDisposedOf1->disposal) - ->withThirtyDayQuantity(new Quantity('20'), id: $sharePoolingAssetAcquired2->acquisition->id) - ->make(['costBasis' => FiatAmount::GBP('46')]), + ->withThirtyDayQuantity(new Quantity('10'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->make(['costBasis' => FiatAmount::GBP('40')]), )); when($disposeOfSharePoolingAsset3 = new DisposeOfSharePoolingAsset( transactionId: SharePoolingAssetTransactionId::generate(), asset: $this->aggregateRootId->toAsset(), - date: LocalDate::parse('2015-10-25'), - quantity: new Quantity('20'), - proceeds: FiatAmount::GBP('50'), + date: LocalDate::parse('2015-10-23'), + quantity: new Quantity('15'), + proceeds: FiatAmount::GBP('40'), forFiat: false, )); @@ -946,18 +946,142 @@ new SharePoolingAssetDisposalReverted(disposal: $sharePoolingAssetDisposedOf1Corrected2->disposal), new SharePoolingAssetDisposedOf( disposal: SharePoolingAssetDisposal::factory() - ->withSameDayQuantity(new Quantity('20'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->withSameDayQuantity(new Quantity('10'), id: $sharePoolingAssetAcquired2->acquisition->id) ->make([ 'id' => $disposeOfSharePoolingAsset3->transactionId, 'date' => $disposeOfSharePoolingAsset3->date, 'quantity' => $disposeOfSharePoolingAsset3->quantity, - 'costBasis' => FiatAmount::GBP('16'), + 'costBasis' => FiatAmount::GBP('25'), 'proceeds' => $disposeOfSharePoolingAsset3->proceeds, ]), ), new SharePoolingAssetDisposedOf( disposal: SharePoolingAssetDisposal::factory() ->copyFrom($sharePoolingAssetDisposedOf1->disposal) + ->make(['costBasis' => FiatAmount::GBP('30')]), + ), + ); +}); + +it('can acquire a share pooling asset on the same day as other acquisitions and disposals and within 30 days of a disposal', function () { + given(new SharePoolingAssetFiatCurrencySet(FiatCurrency::GBP)); + + given(new SharePoolingAssetAcquired( + acquisition: new SharePoolingAssetAcquisition( + date: LocalDate::parse('2015-10-21'), + quantity: new Quantity('100'), + costBasis: FiatAmount::GBP('100'), + forFiat: false, + ), + )); + + given($sharePoolingAssetDisposedOf1 = new SharePoolingAssetDisposedOf( + disposal: new SharePoolingAssetDisposal( + date: LocalDate::parse('2015-10-22'), + quantity: new Quantity('30'), + costBasis: FiatAmount::GBP('30'), + proceeds: FiatAmount::GBP('50'), + forFiat: false, + ), + )); + + given(new SharePoolingAssetDisposalReverted(disposal: $sharePoolingAssetDisposedOf1->disposal)); + + given($sharePoolingAssetAcquired2 = new SharePoolingAssetAcquired( + acquisition: new SharePoolingAssetAcquisition( + date: LocalDate::parse('2015-10-23'), + quantity: new Quantity('20'), + costBasis: FiatAmount::GBP('40'), + forFiat: false, + ), + )); + + given($sharePoolingAssetDisposedOf1Corrected1 = new SharePoolingAssetDisposedOf( + disposal: SharePoolingAssetDisposal::factory() + ->copyFrom($sharePoolingAssetDisposedOf1->disposal) + ->withThirtyDayQuantity(new Quantity('20'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->make(['costBasis' => FiatAmount::GBP('50')]), + )); + + given(new SharePoolingAssetDisposalReverted(disposal: $sharePoolingAssetDisposedOf1Corrected1->disposal)); + + given($sharePoolingAssetDisposedOf2 = new SharePoolingAssetDisposedOf( + disposal: SharePoolingAssetDisposal::factory() + ->withSameDayQuantity(new Quantity('10'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->make([ + 'date' => LocalDate::parse('2015-10-23'), + 'quantity' => new Quantity('10'), + 'costBasis' => FiatAmount::GBP('20'), + 'proceeds' => FiatAmount::GBP('30'), + 'forFiat' => false, + ]), + )); + + given($sharePoolingAssetDisposedOf1Corrected2 = new SharePoolingAssetDisposedOf( + disposal: SharePoolingAssetDisposal::factory() + ->copyFrom($sharePoolingAssetDisposedOf1->disposal) + ->withThirtyDayQuantity(new Quantity('10'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->make(['costBasis' => FiatAmount::GBP('40')]), + )); + + given(new SharePoolingAssetDisposalReverted(disposal: $sharePoolingAssetDisposedOf1Corrected2->disposal)); + + given($sharePoolingAssetDisposedOf3 = new SharePoolingAssetDisposedOf( + disposal: SharePoolingAssetDisposal::factory() + ->withSameDayQuantity(new Quantity('10'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->make([ + 'date' => LocalDate::parse('2015-10-23'), + 'quantity' => new Quantity('15'), + 'costBasis' => FiatAmount::GBP('25'), + 'proceeds' => FiatAmount::GBP('40'), + ]), + )); + + given($sharePoolingAssetDisposedOf1Corrected3 = new SharePoolingAssetDisposedOf( + disposal: SharePoolingAssetDisposal::factory() + ->copyFrom($sharePoolingAssetDisposedOf1->disposal) + ->make(['costBasis' => FiatAmount::GBP('30')]), + )); + + when($acquireSharePoolingAsset3 = new AcquireSharePoolingAsset( + transactionId: SharePoolingAssetTransactionId::generate(), + asset: $this->aggregateRootId->toAsset(), + date: LocalDate::parse('2015-10-23'), + quantity: new Quantity('10'), + costBasis: FiatAmount::GBP('50'), + forFiat: false, + )); + + then( + new SharePoolingAssetDisposalReverted(disposal: $sharePoolingAssetDisposedOf2->disposal), + new SharePoolingAssetDisposalReverted(disposal: $sharePoolingAssetDisposedOf3->disposal), + new SharePoolingAssetDisposalReverted(disposal: $sharePoolingAssetDisposedOf1Corrected3->disposal), + new SharePoolingAssetAcquired( + acquisition: new SharePoolingAssetAcquisition( + id: $acquireSharePoolingAsset3->transactionId, + date: $acquireSharePoolingAsset3->date, + quantity: $acquireSharePoolingAsset3->quantity, + costBasis: $acquireSharePoolingAsset3->costBasis, + forFiat: false, + ), + ), + new SharePoolingAssetDisposedOf( + disposal: SharePoolingAssetDisposal::factory() + ->revert($sharePoolingAssetDisposedOf2->disposal) + ->withSameDayQuantity(new Quantity('10'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->make(['costBasis' => FiatAmount::GBP('30')]), + ), + new SharePoolingAssetDisposedOf( + disposal: SharePoolingAssetDisposal::factory() + ->revert($sharePoolingAssetDisposedOf3->disposal) + ->withSameDayQuantity(new Quantity('10'), id: $sharePoolingAssetAcquired2->acquisition->id) + ->withSameDayQuantity(new Quantity('5'), id: $acquireSharePoolingAsset3->transactionId) + ->make(['costBasis' => FiatAmount::GBP('45')]), + ), + new SharePoolingAssetDisposedOf( + disposal: SharePoolingAssetDisposal::factory() + ->copyFrom($sharePoolingAssetDisposedOf1->disposal) + ->withThirtyDayQuantity(new Quantity('5'), id: $acquireSharePoolingAsset3->transactionId) ->make(['costBasis' => FiatAmount::GBP('50')]), ), );