Skip to content

Commit

Permalink
fixed quantity allocation for acquisitions
Browse files Browse the repository at this point in the history
  • Loading branch information
osteel committed Sep 12, 2023
1 parent 9622963 commit 26e8c90
Show file tree
Hide file tree
Showing 14 changed files with 385 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,34 @@ public static function excessiveQuantityAllocated(Quantity $available, Quantity
return new self(sprintf('The allocated quantity %s exceeds the available quantity %s', $allocated, $available));
}

public static function insufficientSameDayQuantity(Quantity $quantity, Quantity $sameDayQuantity): self
public static function insufficientSameDayQuantityToIncrease(Quantity $quantity, Quantity $availableQuantity): self
{
return new self(sprintf(
'Cannot decrease same-day quantity by %s: only %s available',
$quantity,
$sameDayQuantity,
));
return self::insufficientQuantity($quantity, $availableQuantity, 'same-day', 'increase');
}

public static function insufficientSameDayQuantityToDecrease(Quantity $quantity, Quantity $availableQuantity): self
{
return self::insufficientQuantity($quantity, $availableQuantity, 'same-day', 'decrease');
}

public static function insufficientThirtyDayQuantityToIncrease(Quantity $quantity, Quantity $availableQuantity): self
{
return self::insufficientQuantity($quantity, $availableQuantity, '30-day', 'increase');
}

public static function insufficientThirtyDayQuantityToDecrease(Quantity $quantity, Quantity $availableQuantity): self
{
return self::insufficientQuantity($quantity, $availableQuantity, '30-day', 'decrease');
}

public static function insufficientThirtyDayQuantity(Quantity $quantity, Quantity $thirtyDayQuantity): self
private static function insufficientQuantity(Quantity $quantity, Quantity $availableQuantity, string $type, string $action): self
{
return new self(sprintf(
'Cannot decrease 30-day quantity by %s: only %s available',
'Cannot %s %s quantity by %s: only %s available',
$action,
$type,
$quantity,
$thirtyDayQuantity,
$availableQuantity,
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,19 @@ public function section104PoolCostBasis(): FiatAmount
return $this->costBasis->dividedBy($this->quantity)->multipliedBy($this->section104PoolQuantity());
}

/** Increase the same-day quantity and adjust the 30-day quantity accordingly. */
public function increaseSameDayQuantity(Quantity $quantity): self
{
if ($quantity->isGreaterThan($availableQuantity = $this->section104PoolQuantity())) {
throw SharePoolingAssetAcquisitionException::insufficientSameDayQuantityToIncrease($quantity, $availableQuantity);
}

$this->sameDayQuantity = $this->sameDayQuantity->plus($quantity);

return $this;
}

/** Increase the same-day quantity and adjust the 30-day quantity accordingly. */
public function increaseSameDayQuantityUpToAvailableQuantity(Quantity $quantity): self
{
// Adjust same-day quantity
$quantityToAdd = Quantity::minimum($quantity, $this->availableSameDayQuantity());
Expand All @@ -81,7 +92,7 @@ public function increaseSameDayQuantity(Quantity $quantity): self
public function decreaseSameDayQuantity(Quantity $quantity): self
{
if ($quantity->isGreaterThan($this->sameDayQuantity)) {
throw SharePoolingAssetAcquisitionException::insufficientSameDayQuantity($quantity, $this->sameDayQuantity);
throw SharePoolingAssetAcquisitionException::insufficientSameDayQuantityToDecrease($quantity, $this->sameDayQuantity);
}

$this->sameDayQuantity = $this->sameDayQuantity->minus($quantity);
Expand All @@ -90,6 +101,17 @@ public function decreaseSameDayQuantity(Quantity $quantity): self
}

public function increaseThirtyDayQuantity(Quantity $quantity): self
{
if ($quantity->isGreaterThan($availableQuantity = $this->section104PoolQuantity())) {
throw SharePoolingAssetAcquisitionException::insufficientThirtyDayQuantityToIncrease($quantity, $availableQuantity);
}

$this->thirtyDayQuantity = $this->thirtyDayQuantity->plus($quantity);

return $this;
}

public function increaseThirtyDayQuantityUpToAvailableQuantity(Quantity $quantity): self
{
$quantityToAdd = Quantity::minimum($quantity, $this->availableThirtyDayQuantity());
$this->thirtyDayQuantity = $this->thirtyDayQuantity->plus($quantityToAdd);
Expand All @@ -101,7 +123,7 @@ public function increaseThirtyDayQuantity(Quantity $quantity): self
public function decreaseThirtyDayQuantity(Quantity $quantity): self
{
if ($quantity->isGreaterThan($this->thirtyDayQuantity)) {
throw SharePoolingAssetAcquisitionException::insufficientThirtyDayQuantity($quantity, $this->thirtyDayQuantity);
throw SharePoolingAssetAcquisitionException::insufficientThirtyDayQuantityToDecrease($quantity, $this->thirtyDayQuantity);
}

$this->thirtyDayQuantity = $this->thirtyDayQuantity->minus($quantity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ public function hasAvailableSameDayQuantity(): bool
return $this->availableSameDayQuantity()->isGreaterThan('0');
}

/** Return the quantity that is not yet allocated as same-day quantity. */
public function availableSameDayQuantity(): Quantity
{
// Same-day quantity always gets priority, so any quantity that is not yet
// allocated as same-day quantity is technically available as same-day quantity
return $this->quantity->minus($this->sameDayQuantity());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public function count(): int
return count($this->transactions);
}

public function copy(): self
{
return new self(array_map(fn (SharePoolingAssetTransaction $transaction) => clone $transaction, $this->transactions));
}

public function first(): ?SharePoolingAssetTransaction
{
return $this->transactions[0] ?? null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Domain\Aggregates\SharePoolingAsset\Services\DisposalProcessor;
namespace Domain\Aggregates\SharePoolingAsset\Services\DisposalBuilder;

use Brick\DateTime\LocalDate;
use Domain\Aggregates\SharePoolingAsset\Actions\DisposeOfSharePoolingAsset;
Expand All @@ -16,7 +16,7 @@
* This service essentially calculates the cost basis of a disposal by looking at past and future
* transactions, following the various share pooling asset rules (same-day, 30-day, section 104 pool).
*/
final class DisposalProcessor
final class DisposalBuilder
{
public static function process(
DisposeOfSharePoolingAsset $disposal,
Expand Down Expand Up @@ -109,7 +109,9 @@ private static function processSameDayAcquisitions(
foreach ($sameDayAcquisitions as $acquisition) {
$quantityToAllocate = Quantity::minimum($remainder, $acquisition->availableSameDayQuantity());
$sameDayQuantityAllocation->allocateQuantity($quantityToAllocate, $acquisition);
$acquisition->increaseSameDayQuantity($remainder);

$acquisition->increaseSameDayQuantityUpToAvailableQuantity($remainder);

$remainder = $remainder->minus($quantityToAllocate);
if ($remainder->isZero()) {
break;
Expand Down Expand Up @@ -154,7 +156,7 @@ private static function processAcquisitionsWithinThirtyDays(
foreach ($sameDayDisposals as $disposal) {
$sameDayQuantityToApply = Quantity::minimum($disposal->availableSameDayQuantity(), $thirtyDayQuantityToApply);
$disposal->sameDayQuantityAllocation->allocateQuantity($sameDayQuantityToApply, $acquisition);
$acquisition->increaseSameDayQuantity($sameDayQuantityToApply);
$acquisition->increaseSameDayQuantityUpToAvailableQuantity($sameDayQuantityToApply);
$thirtyDayQuantityToApply = $thirtyDayQuantityToApply->minus($sameDayQuantityToApply);
if ($thirtyDayQuantityToApply->isZero()) {
break;
Expand All @@ -170,7 +172,7 @@ private static function processAcquisitionsWithinThirtyDays(
$costBasis = $costBasis->plus($averageCostBasisPerUnit->multipliedBy($thirtyDayQuantityToApply));

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

// Continue until there are no more transactions or we've covered all disposed tokens
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,37 @@
use Domain\Aggregates\SharePoolingAsset\Services\QuantityAdjuster\Exceptions\QuantityAdjusterException;
use Domain\Aggregates\SharePoolingAsset\ValueObjects\QuantityAllocation;

/**
* This service restores acquisition quantities that were previously allocated to a disposal that is now being reverted.
*/
final class QuantityAdjuster
{
/** @throws SharePoolingAssetAcquisitionException */
/**
* Adjust acquisition quantities based on the disposal's allocated quantities.
*
* @throws SharePoolingAssetAcquisitionException
*/
public static function applyDisposal(
SharePoolingAssetDisposal $disposal,
SharePoolingAssetTransactions $transactions,
): void {
foreach (self::getAcquisitions($disposal->sameDayQuantityAllocation, $transactions) as $acquisition) {
$acquisition->increaseSameDayQuantity($disposal->sameDayQuantityAllocation->quantityAllocatedTo($acquisition));
}

foreach (self::getAcquisitions($disposal->thirtyDayQuantityAllocation, $transactions) as $acquisition) {
$acquisition->increaseThirtyDayQuantity($disposal->thirtyDayQuantityAllocation->quantityAllocatedTo($acquisition));
}
}

/**
* Restore acquisition quantities that were previously allocated to a disposal that is now being reverted.
*
* @throws SharePoolingAssetAcquisitionException
*/
public static function revertDisposal(
SharePoolingAssetDisposal $disposal,
SharePoolingAssetTransactions $transactions,
): void {
foreach (self::getAcquisitions($disposal->sameDayQuantityAllocation, $transactions) as $acquisition) {
try {
$acquisition->decreaseSameDayQuantity($disposal->sameDayQuantityAllocation->quantityAllocatedTo($acquisition));
} catch (SharePoolingAssetAcquisitionException) {
// @TODO When re-acquiring within 30 days an asset that was disposed of on the same day it was acquired,
// decreasing the same-day quantity of the concerned acquisitions fails, because at the time the latter
// were recorded within the SharePoolingAssetAcquired events that had no same-day quantity yet
}
$acquisition->decreaseSameDayQuantity($disposal->sameDayQuantityAllocation->quantityAllocatedTo($acquisition));
}

foreach (self::getAcquisitions($disposal->thirtyDayQuantityAllocation, $transactions) as $acquisition) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,24 @@ private static function add30DayDisposalsToRevert(
return $disposalsToRevert;
}

/**
* Get processed disposals with 30-day quantity allocated to acquisitions from the same day as the
* current disposal, with same-day quantity that should be allocated to that disposal instead.
*/
public static function disposalsToRevertOnDisposal(
DisposeOfSharePoolingAsset $disposal,
SharePoolingAssetTransactions $transactions,
): SharePoolingAssetDisposals {
$disposalsToRevert = SharePoolingAssetDisposals::make();

// Get processed disposals with 30-day quantity allocated to acquisitions on the same
// day as the disposal, with same-day quantity about to be allocated to the disposal
// Get the acquisitions from the same day as the disposal and with currently-allocated 30-day quantity
$sameDayAcquisitions = $transactions->acquisitionsMadeOn($disposal->date)->withThirtyDayQuantity();

$remainingQuantity = $disposal->quantity;
foreach ($sameDayAcquisitions as $acquisition) {
// Add disposals up to the disposal's quantity, starting with the most recent ones
// 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()
->disposalsWithThirtyDayQuantityAllocatedTo($acquisition)
->reverse();
Expand All @@ -111,13 +116,15 @@ public static function disposalsToRevertOnDisposal(
$quantityToDeduct = Quantity::minimum($disposal->thirtyDayQuantityAllocatedTo($acquisition), $remainingQuantity);
$remainingQuantity = $remainingQuantity->minus($quantityToDeduct);

// Stop as soon as the disposal's quantity has fully been allocated
// Stop as soon as the disposal's quantity has been fully allocated
if ($remainingQuantity->isZero()) {
break 2;
}
}
}

return $disposalsToRevert;
// To maintain the priority of older disposals over the 30-day quantity of acquisitions made
// within 30 days, however, they need to be reverted and replayed in chronological order
return $disposalsToRevert->reverse();
}
}
Loading

0 comments on commit 26e8c90

Please sign in to comment.