Skip to content
This repository has been archived by the owner on Jun 8, 2023. It is now read-only.

Commit

Permalink
Complete VAT Calculator
Browse files Browse the repository at this point in the history
  • Loading branch information
joebailey26 committed May 27, 2023
1 parent 57310c7 commit fabbcf4
Show file tree
Hide file tree
Showing 13 changed files with 547 additions and 1 deletion.
20 changes: 20 additions & 0 deletions config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,23 @@ controllers:
kernel:
resource: ../src/Kernel.php
type: annotation

index:
path: /
controller: App\Controller\IndexController::new
methods: [GET]

new_calculation:
path: /new-calculation
controller: App\Controller\NewCalculationController::new
methods: [GET, POST]

delete_calculations:
path: /delete-calculations
controller: App\Controller\DeleteCalculationsController::new
methods: [GET]

download_calculations:
path: /download-calculations
controller: App\Controller\DownloadCalculationsController::new
methods: [GET]
33 changes: 33 additions & 0 deletions migrations/Version20230527111225.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230527111225 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE vat_calculation (id INT AUTO_INCREMENT NOT NULL, v DOUBLE PRECISION NOT NULL, r DOUBLE PRECISION NOT NULL, is_vat_included TINYINT(1) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT AUTO_INCREMENT NOT NULL, body LONGTEXT NOT NULL, headers LONGTEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL, available_at DATETIME NOT NULL, delivered_at DATETIME DEFAULT NULL, INDEX IDX_75EA56E0FB7336F0 (queue_name), INDEX IDX_75EA56E0E3BD61CE (available_at), INDEX IDX_75EA56E016BA31DB (delivered_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE vat_calculation');
$this->addSql('DROP TABLE messenger_messages');
}
}
1 change: 1 addition & 0 deletions public/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
vat-calculations.csv
30 changes: 30 additions & 0 deletions src/Controller/DeleteCalculationsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/*
What this does
This controller handles the `/delete-calculations` route.
It fetches all calculations from the database using the findAll() method.
We then loop over each calculation and delete it.
We then redirect to the homepage to show that there are no more calculations.
*/

namespace App\Controller;

use App\Entity\VatCalculation;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Doctrine\ORM\EntityManagerInterface;

class DeleteCalculationsController extends AbstractController
{
public function new(EntityManagerInterface $entityManager): Response
{
$calculations = $entityManager->getRepository(VatCalculation::class)->findAll();

foreach ($calculations as $calculation) {
$entityManager->remove($calculation);
}
$entityManager->flush();

return $this->redirectToRoute('index');
}
}
64 changes: 64 additions & 0 deletions src/Controller/DownloadCalculationsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php
/*
What this does
This controller handles the `/download-calculations` route.
It fetches all calculations from the database using the findAll() method.
We then loop over each calculation and add it to a CSV.
We then download that CSV.
*/

namespace App\Controller;

use App\Entity\VatCalculation;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Doctrine\ORM\EntityManagerInterface;

class DownloadCalculationsController extends AbstractController
{
public function new(EntityManagerInterface $entityManager): Response
{
$calculations = $entityManager->getRepository(VatCalculation::class)->findAll();

$csvData = [];
// Set up the column titles
$csvData[] = [
'ID',
'Input: Monetary Value',
'Input: VAT Rate',
'Input: Is VAT included?',
'Output: Monetary value excluding VAT',
'Output: Amount of VAT',
'Output: Monetary value including VAT'
];

foreach ($calculations as $calculation) {
// Add the $calculation to the CSV
$csvData[] = [
$calculation->getId(),
$calculation->getV(),
$calculation->getR(),
$calculation->getIsVatIncluded(),
$calculation->getResultWithoutVat(),
$calculation->getAmountOfVat(),
$calculation->getResultWithVat()
];
}
$entityManager->flush();

// Download the CSV
$csvFileName = 'vat-calculations.csv';
$csvFile = fopen($csvFileName, 'w');
foreach ($csvData as $csvRow) {
fputcsv($csvFile, $csvRow);
}
fclose($csvFile);

$response = new Response();
$response->headers->set('Content-Type', 'text/csv');
$response->headers->set('Content-Disposition', 'attachment; filename="' . $csvFileName . '"');
$response->setContent(file_get_contents($csvFileName));

return $response;
}
}
41 changes: 41 additions & 0 deletions src/Controller/IndexController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/*
What this does
This controller handles the `/` route.
It fetches all calculations from the database using the findAll() method.
We pass this array through to the twig template.
*/

namespace App\Controller;

use App\Entity\VatCalculation;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;

class IndexController extends AbstractController
{
public function new(EntityManagerInterface $entityManager, Request $request): Response
{
$calculations = $entityManager->getRepository(VatCalculation::class)->findAll();
/*
We can pass a highlighted VatCalculation through from the form
to show a user what they have just submitted.
If we do get a highlightId in the query string, we try to find this calculation from the database.
This ensures that only ids that are associated with a calculation get passed through.
We then pass it through to the twig template to let it be highlighted
*/
$highlightId = $request->query->get('highlightId');

$highlightedCalculation = null;
if ($highlightId !== null) {
$highlightedCalculation = $entityManager->getRepository(VatCalculation::class)->find($highlightId);
}

return $this->render('index.html.twig', [
'calculations' => $calculations,
'highlightedCalculation' => $highlightedCalculation,
]);
}
}
42 changes: 42 additions & 0 deletions src/Controller/NewCalculationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/*
What this does
This controller handles the /new-calculation route.
We create the form from the CalculateVatForm class and either return the rendered form or process the form.
We call handleRequest on the form object which validates the inputs.
If the inputs are invalid, it returns the form with the appropriate error messages.
If the inputs are valid, it gets the submitted data from the form, which is a new instance of the VatCalculation class.
Then it passes it through to the EntityManagerInterface to persist the data in the database.
SQL Injection
We handle the isValid() method on the form which only allows data to be persisted if it passes validation.
*/

namespace App\Controller;

use App\Form\CalculateVatForm;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Doctrine\ORM\EntityManagerInterface;

class NewCalculationController extends AbstractController
{
public function new(Request $request, EntityManagerInterface $entityManager): Response
{
$form = $this->createForm(CalculateVatForm::class);

$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
$vatCalculation = $form->getData();
$entityManager->persist($vatCalculation);
$entityManager->flush();
// Pass this calculation to IndexController so that we can highlight it
return $this->redirectToRoute('index', ['highlightId' => $vatCalculation->getId()]);
}

return $this->render('calculateVat.html.twig', [
'form' => $form->createView(),
]);
}
}
131 changes: 131 additions & 0 deletions src/Entity/VatCalculation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php
/*
What this does
This is a class of VAT Calculation.
We use a special commented markup for Doctrine which handles database mapping for us.
Because this is in the entity folder, we can run `php bin/console make:migration` to make a migration.
We can then run `php bin/console doctrine:migrations:migrate` to perform the migrations.
A migration makes sure our database has the correct columns to handle this data.
SQL Injection
Doctrine handles SQL parameterization, making SQL injection attacks less likely
*/

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity()
*/
class VatCalculation
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;

/**
* @ORM\Column(type="float")
*/
private $v;

/**
* @ORM\Column(type="float")
*/
private $r;

/**
* @ORM\Column(type="boolean")
*/
private $isVatIncluded;

// Getters and setters

public function getId(): ?int
{
return $this->id;
}

public function getV(): ?float
{
return $this->v;
}

public function setV(float $v): self
{
$this->v = $v;

return $this;
}

public function getR(): ?float
{
return $this->r;
}

public function setR(float $r): self
{
$this->r = $r;

return $this;
}

public function getIsVatIncluded(): ?bool
{
return $this->isVatIncluded;
}

public function setIsVatIncluded(bool $isVatIncluded): self
{
$this->isVatIncluded = $isVatIncluded;

return $this;
}

/*
We perform the calculations on the fly rather than storing them in the database.
This allows us to add or change calculations in the future without having to update all items in the database.
*/

public function getResultWithVat(): ?float
{
/*
If VAT is included in the original value, we just return the original value rounded to 2 decimal places.
We use a return early style to make the code more readable.
*/
if ($this->isVatIncluded) {
return round($this->getV(), 2);
}
/*
If VAT is not included, we work out the amount to add, which is v * r * .01.
We're asking for the percentage so have to times it by .01 to get a decimal.
We round this to 2 decimal places too.
*/
return round($this->getV() + $this->getV() * $this->getR() * .01, 2);
}

public function getResultWithoutVat(): ?float
{
/*
If VAT is included, we divide the original value by 1 + percentage of VAT.
We round the result.
*/
if ($this->isVatIncluded) {
return round($this->getV() / (1 + $this->getR() * .01), 2);
}
/*
If VAT is not included, we can just return the original value.
*/
return round($this->getV(), 2);
}

public function getAmountOfVat(): ?float
{
/*
The amount of VAT is just the result with VAT minus the result without.
*/
return round($this->getResultWithVat() - $this->getResultWithoutVat(), 2);
}
}
Loading

0 comments on commit fabbcf4

Please sign in to comment.