Skip to content

Commit

Permalink
fixed DisposalBuilder and ReversionFinder
Browse files Browse the repository at this point in the history
  • Loading branch information
osteel committed Sep 14, 2023
1 parent 26e8c90 commit ecc1a3d
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
final class DisposalBuilder
{
public static function process(
public static function make(
DisposeOfSharePoolingAsset $disposal,
SharePoolingAssetTransactions $transactions,
): SharePoolingAssetDisposal {
Expand Down Expand Up @@ -108,11 +108,11 @@ private static function processSameDayAcquisitions(
$remainder = $availableSameDayQuantity;
foreach ($sameDayAcquisitions as $acquisition) {
$quantityToAllocate = Quantity::minimum($remainder, $acquisition->availableSameDayQuantity());
$sameDayQuantityAllocation->allocateQuantity($quantityToAllocate, $acquisition);

$acquisition->increaseSameDayQuantityUpToAvailableQuantity($remainder);

$sameDayQuantityAllocation->allocateQuantity($quantityToAllocate, $acquisition);
$acquisition->increaseSameDayQuantityUpToAvailableQuantity($quantityToAllocate);
$remainder = $remainder->minus($quantityToAllocate);

if ($remainder->isZero()) {
break;
}
Expand Down Expand Up @@ -146,36 +146,15 @@ 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 = Quantity::minimum($acquisition->availableThirtyDayQuantity(), $remainingQuantity);
$averageCostBasisPerUnit = $acquisition->averageCostBasisPerUnit();
$costBasis = $costBasis->plus($averageCostBasisPerUnit->multipliedBy($quantityToAllocate));

$costBasis = $costBasis->plus($averageCostBasisPerUnit->multipliedBy($thirtyDayQuantityToApply));

$thirtyDayQuantityAllocation->allocateQuantity($thirtyDayQuantityToApply, $acquisition);
$acquisition->increaseThirtyDayQuantityUpToAvailableQuantity($thirtyDayQuantityToApply);
$remainingQuantity = $remainingQuantity->minus($thirtyDayQuantityToApply);
$thirtyDayQuantityAllocation->allocateQuantity($quantityToAllocate, $acquisition);
$acquisition->increaseThirtyDayQuantityUpToAvailableQuantity($quantityToAllocate);
$remainingQuantity = $remainingQuantity->minus($quantityToAllocate);

// Continue until there are no more transactions or we've covered all disposed tokens
// Continue until there are no more transactions or we've covered all disposed of tokens
if ($remainingQuantity->isZero()) {
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand Down Expand Up @@ -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();

Expand Down
8 changes: 4 additions & 4 deletions domain/src/Aggregates/SharePoolingAsset/SharePoolingAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

Expand All @@ -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(),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,6 @@
expect($disposals->getIterator()[2])->toBe($disposal1);
});

it('can return the unprocessed disposals from a collection of disposals', function () {
/** @var list<SharePoolingAssetDisposal> */
$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<SharePoolingAssetDisposal> */
$items = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
forFiat: false,
);

$disposal = DisposalBuilder::process(
$disposal = DisposalBuilder::make(
disposal: $action,
transactions: SharePoolingAssetTransactions::make(SharePoolingAssetAcquisition::factory()->make()),
);
Expand All @@ -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);
});
Original file line number Diff line number Diff line change
@@ -1,28 +1,114 @@
<?php

use Domain\Aggregates\SharePoolingAsset\Entities\Exceptions\SharePoolingAssetAcquisitionException;
use Domain\Aggregates\SharePoolingAsset\Entities\SharePoolingAssetAcquisition;
use Domain\Aggregates\SharePoolingAsset\Entities\SharePoolingAssetDisposal;
use Domain\Aggregates\SharePoolingAsset\Entities\SharePoolingAssetTransactions;
use Domain\Aggregates\SharePoolingAsset\Services\QuantityAdjuster\Exceptions\QuantityAdjusterException;
use Domain\Aggregates\SharePoolingAsset\Services\QuantityAdjuster\QuantityAdjuster;
use Domain\Aggregates\SharePoolingAsset\ValueObjects\SharePoolingAssetTransactionId;
use Domain\ValueObjects\Quantity;

it('cannot revert a disposal because an acquisition cannot be found', function () {
it('cannot process a disposal because an acquisition cannot be found', function (string $method) {
$disposal = SharePoolingAssetDisposal::factory()
->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'],
]);
Loading

0 comments on commit ecc1a3d

Please sign in to comment.