diff --git a/src/DataFixtures/InstrumentFixture.php b/src/DataFixtures/InstrumentFixture.php index ede3a27..c66ef99 100644 --- a/src/DataFixtures/InstrumentFixture.php +++ b/src/DataFixtures/InstrumentFixture.php @@ -30,6 +30,7 @@ public function load(ObjectManager $manager): void $appl_inst->setEusipa(Instrument::EUSIPA_KNOCKOUT); $appl_inst->setCurrency("EUR"); $appl_inst->setUnderlying($appl); + $appl_inst->setExecutionTaxRate(0.0012); // 0.12 % $manager->persist($appl_inst); $appl_terms = new InstrumentTerms(); @@ -48,6 +49,7 @@ public function load(ObjectManager $manager): void $msft_inst->setEusipa(Instrument::EUSIPA_UNDERLYING); $msft_inst->setCurrency("USD"); $msft_inst->setUnderlying($msft); + $appl_inst->setExecutionTaxRate(0.0035); // 0.35 % $manager->persist($msft_inst); $manager->flush(); diff --git a/src/Entity/Instrument.php b/src/Entity/Instrument.php index 45676cf..6f455ce 100644 --- a/src/Entity/Instrument.php +++ b/src/Entity/Instrument.php @@ -106,6 +106,10 @@ class Instrument #[ORM\Column(type: "text", length: 50000, nullable: true)] private $notes; + #[ORM\Column(type: "decimal", precision: 5, scale: 4, nullable: true, options: ["unsigned" => true])] + #[Assert\Range(min: 0, max: 1)] + private $executionTaxRate; + public function getId(): ?int { return $this->id; @@ -200,6 +204,18 @@ public function setNotes(?string $notes): self return $this; } + public function getExecutionTaxRate(): ?string + { + return $this->executionTaxRate; + } + + public function setExecutionTaxRate(?string $executionTaxRate): self + { + $this->executionTaxRate = $executionTaxRate; + + return $this; + } + public function getUnderlying(): ?Asset { return $this->underlying; diff --git a/src/Form/ExecutionType.php b/src/Form/ExecutionType.php index cc1bd1b..b72d3ee 100644 --- a/src/Form/ExecutionType.php +++ b/src/Form/ExecutionType.php @@ -62,6 +62,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $user = $this->token->getToken()->getUser(); if ($data->instrument) { + $executionTaxRate = $data->instrument->getExecutionTaxRate(); + $direction = $data->instrument->getDirection(); $builder->add('instrument', TextType::class, ['disabled' =>'true']); } else { $builder->add('instrument', EntityType::class, ['class' => Instrument::class]); @@ -109,7 +111,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('execution_id', NumberType::class, ['label' => 'Execution ID', 'html5' => true, 'required' => false, 'help' => 'Execution ID used by the broker']) ->add('marketplace', TextType::class, ['required' => false, 'help' => 'Location of the exchange']) ->add('commission', MoneyType::class, ['required' => false, 'html5' => false, 'currency' => $currency, 'scale' => 4, 'help' => 'Commission cost (negative amount)']) - ->add('tax', MoneyType::class, ['required' => false, 'html5' => false, 'currency' => $currency, 'scale' => 4, 'help' => 'paid tax is negative, refunded tax positive']) + ->add('tax', MoneyType::class, ['required' => false, 'html5' => false, 'currency' => $currency, 'scale' => 4, 'help' => 'Paid tax is negative, refunded tax positive' . (!empty($executionTaxRate) && !empty($direction) ? ' (applying execution tax rate of -' . $executionTaxRate * 100 . '% when empty)' : '')]) ->add('interest', MoneyType::class, ['required' => false, 'html5' => false, 'currency' => $currency, 'scale' => 4, 'help' => 'Paid interest (negative amount)']) ->add('notes', TextareaType::class, ['required' => false]) ->add('consolidated', CheckboxType::class, ['required' => false, 'help' => 'Check if this transaction matches with your broker']) diff --git a/src/Form/InstrumentType.php b/src/Form/InstrumentType.php index 50de485..4f2525f 100644 --- a/src/Form/InstrumentType.php +++ b/src/Form/InstrumentType.php @@ -14,6 +14,7 @@ use Symfony\Component\Form\Extension\Core\Type\ResetType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -89,6 +90,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('terminationdate', DateType::class, ['required' => false, 'label'=>'Termination date', 'widget' => 'single_text']) ->add('url', UrlType::class, ['required' => false, 'label' => 'Instrument website']) ->add('notes', TextareaType::class, ['required' => false]) + ->add('executiontaxrate', TextType::class, ['required' => false, 'label'=>'Execution tax rate %', 'help' => 'Tax rate percentage applicable to executed trades (e.g. 0.12% is 0.0012)']) ->add('save', SubmitType::class, ['label' => 'Submit', 'attr' => ['class' => 'btn btn-primary']]) ->add('reset', ResetType::class, ['label' => 'Reset', 'attr' => ['class' => 'btn btn-secondary']]) ->add('back', ButtonType::class, ['label' => 'Back', 'attr' => ['class' => 'btn btn-secondary']]) diff --git a/src/Form/Model/ExecutionFormModel.php b/src/Form/Model/ExecutionFormModel.php index 3551e80..5124f88 100644 --- a/src/Form/Model/ExecutionFormModel.php +++ b/src/Form/Model/ExecutionFormModel.php @@ -102,8 +102,11 @@ private function populateTransaction(Transaction $transaction) } else { $transaction->setCommission(null); } - if ($this->tax) { + if ($this->tax !== null) { $transaction->setTax($this->tax); + } elseif($this->instrument->getExecutionTaxRate() != null && $this->direction != 0 && isset($total)) { + // apply execution tax rate of the instrument if no tax provided in the tax field + $transaction->setTax($this->direction * $total * $this->instrument->getExecutionTaxRate()); } else { $transaction->setTax(null); } diff --git a/templates/instrument/edit.html.twig b/templates/instrument/edit.html.twig index 806fc8a..61554a7 100644 --- a/templates/instrument/edit.html.twig +++ b/templates/instrument/edit.html.twig @@ -19,6 +19,7 @@
{{ form_row(form.issuer) }}
{{ form_row(form.url) }}
{{ form_row(form.notes) }}
+
{{ form_row(form.executiontaxrate) }}
{{ form_errors(form) }}
diff --git a/tests/ExecutionFormModelTest.php b/tests/ExecutionFormModelTest.php index de6b9b0..777c34e 100644 --- a/tests/ExecutionFormModelTest.php +++ b/tests/ExecutionFormModelTest.php @@ -3,6 +3,7 @@ namespace App\Tests; use App\Entity\Execution; +use App\Entity\Instrument; use App\Entity\Transaction; use App\Form\Model\ExecutionFormModel; use PHPUnit\Framework\TestCase; @@ -45,4 +46,57 @@ public function testRoundtrip(): void $this->assertSame($transaction2->getCommission(), $transaction->getCommission()); $this->assertSame($transaction2->getInterest(), $transaction->getInterest()); } + + public function testTaxCalculation(): void + { + $execution = new Execution(); + $transaction = new Transaction(); + $instrument = new Instrument(); + $execution->setTransaction($transaction); + $execution->setInstrument($instrument); + $instrument->setExecutionTaxRate(0.0012); // 0.12 % + + $transaction->setTime(new \DateTime()); + $execution->setPrice(123); + $execution->setVolume(15); + $execution->setCurrency("EUR"); + + // should keep tax 0 + $transaction->setTax(0); + $data = new ExecutionFormModel(); + $data->fromExecution($execution); + $data->populateExecution($execution); + $this->assertSame($transaction->getTax(), strval(0)); + + // should keep the inserted tax value + $transaction->setTax(-1.23); + $data = new ExecutionFormModel(); + $data->fromExecution($execution); + $data->populateExecution($execution); + $this->assertSame($transaction->getTax(), strval(-1.23)); + + // should add calculated tax (open) + $calculatedTax = is_numeric($transaction->getPortfolio()) && is_numeric($execution->getInstrument()->getExecutionTaxRate()) ? $transaction->getPortfolio() * $execution->getInstrument()->getExecutionTaxRate() : null; // -1845 * 0.0012 = '-2.214' + $transaction->setTax(null); + $data = new ExecutionFormModel(); + $data->fromExecution($execution); + $data->populateExecution($execution); + $this->assertSame($transaction->getTax(), strval($calculatedTax)); + + // should add calculated tax (close) + $transaction->setTax(null); + $execution->setDirection(-1); + $data = new ExecutionFormModel(); + $data->fromExecution($execution); + $data->populateExecution($execution); + $this->assertSame($transaction->getTax(), strval($calculatedTax)); + + // should add calculated tax (neutral) + $transaction->setTax(null); + $execution->setDirection(0); + $data = new ExecutionFormModel(); + $data->fromExecution($execution); + $data->populateExecution($execution); + $this->assertSame($transaction->getTax(), null); + } }