diff --git a/src/Invoice/Calculator/AbstractSumInvoiceCalculator.php b/src/Invoice/Calculator/AbstractSumInvoiceCalculator.php index bf7bb17c..38b0219a 100644 --- a/src/Invoice/Calculator/AbstractSumInvoiceCalculator.php +++ b/src/Invoice/Calculator/AbstractSumInvoiceCalculator.php @@ -20,6 +20,17 @@ abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator imp { abstract protected function calculateSumIdentifier(InvoiceItemInterface $invoiceItem): string; + protected function calculateIdentifier(InvoiceItemInterface $entry): string + { + $prefix = $this->calculateSumIdentifier($entry); + + if (null !== $entry->getFixedRate()) { + return $prefix . '_fixed_' . (string) $entry->getFixedRate(); + } + + return $prefix . '_hourly_' . (string) $entry->getHourlyRate(); + } + /** * @return InvoiceItem[] */ @@ -34,13 +45,7 @@ abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator imp $invoiceItems = []; foreach ($entries as $entry) { - $id = $this->calculateSumIdentifier($entry); - - if (null !== $entry->getFixedRate()) { - $id = $id . '_fixed_' . (string) $entry->getFixedRate(); - } else { - $id = $id . '_hourly_' . (string) $entry->getHourlyRate(); - } + $id = $this->calculateIdentifier($entry); if (!isset($invoiceItems[$id])) { $invoiceItems[$id] = new InvoiceItem(); diff --git a/src/Invoice/NumberGenerator/DateNumberGenerator.php b/src/Invoice/NumberGenerator/DateNumberGenerator.php index 68a5e529..f58ecfae 100644 --- a/src/Invoice/NumberGenerator/DateNumberGenerator.php +++ b/src/Invoice/NumberGenerator/DateNumberGenerator.php @@ -11,17 +11,27 @@ namespace App\Invoice\NumberGenerator; use App\Invoice\InvoiceModel; use App\Invoice\NumberGeneratorInterface; +use App\Repository\InvoiceRepository; /** * Class DateNumberGenerator generates the invoice number based on the current day. - * It will create duplicate IDs if you create multiple invoices per day. + * It will create duplicate IDs if you create more then 99 invoices per day. */ -class DateNumberGenerator implements NumberGeneratorInterface +final class DateNumberGenerator implements NumberGeneratorInterface { /** * @var InvoiceModel */ - protected $model; + private $model; + /** + * @var InvoiceRepository + */ + private $repository; + + public function __construct(InvoiceRepository $repository) + { + $this->repository = $repository; + } /** * @return string @@ -44,6 +54,17 @@ class DateNumberGenerator implements NumberGeneratorInterface */ public function getInvoiceNumber(): string { - return date('ymd', $this->model->getInvoiceDate()->getTimestamp()); + $loops = 0; + $increaseBy = 0; + + $result = date('ymd', $this->model->getInvoiceDate()->getTimestamp()); + + // in the case that someone configured a weird format, that should not result in an endless loop + while ($this->repository->hasInvoice($result) && $loops++ < 99) { + $suffix = str_pad((string) ++$increaseBy, 2, '0', STR_PAD_LEFT); + $result = date('ymd', $this->model->getInvoiceDate()->getTimestamp()) . '-' . $suffix; + } + + return $result; } } diff --git a/src/Repository/InvoiceRepository.php b/src/Repository/InvoiceRepository.php index ae9f256e..7def647c 100644 --- a/src/Repository/InvoiceRepository.php +++ b/src/Repository/InvoiceRepository.php @@ -40,6 +40,20 @@ class InvoiceRepository extends EntityRepository $entityManager->flush(); } + public function hasInvoice(string $invoiceNumber): bool + { + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->select('count(i.id) as counter') + ->from(Invoice::class, 'i') + ->andWhere($qb->expr()->eq('i.invoiceNumber', ':number')) + ->setParameter('number', $invoiceNumber) + ; + + $counter = (int) $qb->getQuery()->getSingleScalarResult(); + + return $counter > 0; + } + private function getCounterFor(\DateTime $start, \DateTime $end, ?Customer $customer = null): int { $qb = $this->getEntityManager()->createQueryBuilder(); diff --git a/tests/Entity/InvoiceTest.php b/tests/Entity/InvoiceTest.php index 205bd4e7..20cc87f0 100644 --- a/tests/Entity/InvoiceTest.php +++ b/tests/Entity/InvoiceTest.php @@ -23,6 +23,7 @@ use App\Entity\UserPreference; use App\Invoice\Calculator\DefaultCalculator; use App\Invoice\InvoiceModel; use App\Invoice\NumberGenerator\DateNumberGenerator; +use App\Repository\InvoiceRepository; use App\Repository\Query\InvoiceQuery; use App\Tests\Invoice\DebugFormatter; use PHPUnit\Framework\TestCase; @@ -159,11 +160,22 @@ class InvoiceTest extends TestCase $model->setCalculator($calculator); - $numberGenerator = new DateNumberGenerator(); + $numberGenerator = $this->getNumberGeneratorSut(); $numberGenerator->setModel($model); $model->setNumberGenerator($numberGenerator); return $model; } + + private function getNumberGeneratorSut() + { + $repository = $this->createMock(InvoiceRepository::class); + $repository + ->expects($this->any()) + ->method('hasInvoice') + ->willReturn(false); + + return new DateNumberGenerator($repository); + } } diff --git a/tests/Invoice/InvoiceFilenameTest.php b/tests/Invoice/InvoiceFilenameTest.php index 4584757b..f0d1e379 100644 --- a/tests/Invoice/InvoiceFilenameTest.php +++ b/tests/Invoice/InvoiceFilenameTest.php @@ -15,6 +15,7 @@ use App\Entity\Project; use App\Invoice\InvoiceFilename; use App\Invoice\InvoiceModel; use App\Invoice\NumberGenerator\DateNumberGenerator; +use App\Repository\InvoiceRepository; use App\Repository\Query\InvoiceQuery; use PHPUnit\Framework\TestCase; @@ -29,7 +30,7 @@ class InvoiceFilenameTest extends TestCase $template = new InvoiceTemplate(); $model = new InvoiceModel(new DebugFormatter()); - $model->setNumberGenerator(new DateNumberGenerator()); + $model->setNumberGenerator($this->getNumberGeneratorSut()); $model->setTemplate($template); $model->setCustomer($customer); @@ -71,4 +72,15 @@ class InvoiceFilenameTest extends TestCase $sut = new InvoiceFilename($model); self::assertEquals($datePrefix . '-ss_n_--Demo_ProjecT1', $sut->getFilename()); } + + private function getNumberGeneratorSut() + { + $repository = $this->createMock(InvoiceRepository::class); + $repository + ->expects($this->any()) + ->method('hasInvoice') + ->willReturn(false); + + return new DateNumberGenerator($repository); + } } diff --git a/tests/Invoice/NumberGenerator/DateNumberGeneratorTest.php b/tests/Invoice/NumberGenerator/DateNumberGeneratorTest.php index 5f49932c..6b0855b9 100644 --- a/tests/Invoice/NumberGenerator/DateNumberGeneratorTest.php +++ b/tests/Invoice/NumberGenerator/DateNumberGeneratorTest.php @@ -11,6 +11,7 @@ namespace App\Tests\Invoice\NumberGenerator; use App\Invoice\InvoiceModel; use App\Invoice\NumberGenerator\DateNumberGenerator; +use App\Repository\InvoiceRepository; use App\Tests\Invoice\DebugFormatter; use PHPUnit\Framework\TestCase; @@ -19,12 +20,47 @@ use PHPUnit\Framework\TestCase; */ class DateNumberGeneratorTest extends TestCase { + private function getSut(bool $hasInitialInvoice, bool $followingInvoices) + { + $repository = $this->createMock(InvoiceRepository::class); + $repository + ->expects($this->any()) + ->method('hasInvoice') + ->willReturnCallback(function ($number) use ($hasInitialInvoice, $followingInvoices) { + if (stripos($number, '-') === false) { + return $hasInitialInvoice; + } + + return $followingInvoices; + }); + + return new DateNumberGenerator($repository); + } + public function testGetInvoiceNumber() { - $sut = new DateNumberGenerator(); + $sut = $this->getSut(false, false); $sut->setModel(new InvoiceModel(new DebugFormatter())); $this->assertEquals(date('ymd'), $sut->getInvoiceNumber()); $this->assertEquals('date', $sut->getId()); } + + public function testGetInvoiceNumberWithExisting() + { + $sut = $this->getSut(true, false); + $sut->setModel(new InvoiceModel(new DebugFormatter())); + + $this->assertEquals(date('ymd-01'), $sut->getInvoiceNumber()); + $this->assertEquals('date', $sut->getId()); + } + + public function testGetInvoiceNumberWithManyExisting() + { + $sut = $this->getSut(true, true); + $sut->setModel(new InvoiceModel(new DebugFormatter())); + + $this->assertEquals(date('ymd-99'), $sut->getInvoiceNumber()); + $this->assertEquals('date', $sut->getId()); + } } diff --git a/tests/Invoice/Renderer/RendererTestTrait.php b/tests/Invoice/Renderer/RendererTestTrait.php index 36096ebf..f2b29660 100644 --- a/tests/Invoice/Renderer/RendererTestTrait.php +++ b/tests/Invoice/Renderer/RendererTestTrait.php @@ -28,6 +28,7 @@ use App\Invoice\InvoiceFormatter; use App\Invoice\InvoiceModel; use App\Invoice\NumberGenerator\DateNumberGenerator; use App\Invoice\Renderer\AbstractRenderer; +use App\Repository\InvoiceRepository; use App\Repository\Query\InvoiceQuery; trait RendererTestTrait @@ -228,7 +229,7 @@ trait RendererTestTrait $model->setCalculator($calculator); - $numberGenerator = new DateNumberGenerator(); + $numberGenerator = $this->getNumberGeneratorSut(); $numberGenerator->setModel($model); $model->setNumberGenerator($numberGenerator); @@ -236,6 +237,17 @@ trait RendererTestTrait return $model; } + private function getNumberGeneratorSut() + { + $repository = $this->createMock(InvoiceRepository::class); + $repository + ->expects($this->any()) + ->method('hasInvoice') + ->willReturn(false); + + return new DateNumberGenerator($repository); + } + protected function getInvoiceModelOneEntry(): InvoiceModel { $user = new User(); @@ -301,7 +313,7 @@ trait RendererTestTrait $model->setCalculator($calculator); - $numberGenerator = new DateNumberGenerator(); + $numberGenerator = $this->getNumberGeneratorSut(); $numberGenerator->setModel($model); $model->setNumberGenerator($numberGenerator); diff --git a/tests/Invoice/ServiceInvoiceTest.php b/tests/Invoice/ServiceInvoiceTest.php index 13fdc561..a57532bd 100644 --- a/tests/Invoice/ServiceInvoiceTest.php +++ b/tests/Invoice/ServiceInvoiceTest.php @@ -92,7 +92,7 @@ class ServiceInvoiceTest extends TestCase $sut = $this->getSut([]); $sut->addCalculator(new DefaultCalculator()); - $sut->addNumberGenerator(new DateNumberGenerator()); + $sut->addNumberGenerator($this->getNumberGeneratorSut()); $sut->addRenderer( new TwigRenderer( $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock() @@ -132,7 +132,7 @@ class ServiceInvoiceTest extends TestCase $sut = $this->getSut([]); $sut->addCalculator(new DefaultCalculator()); - $sut->addNumberGenerator(new DateNumberGenerator()); + $sut->addNumberGenerator($this->getNumberGeneratorSut()); $model = $sut->createModel($query); @@ -152,10 +152,21 @@ class ServiceInvoiceTest extends TestCase $sut = $this->getSut([]); $sut->addCalculator(new DefaultCalculator()); - $sut->addNumberGenerator(new DateNumberGenerator()); + $sut->addNumberGenerator($this->getNumberGeneratorSut()); $model = $sut->createModel($query); self::assertEquals('de', $model->getTemplate()->getLanguage()); } + + private function getNumberGeneratorSut() + { + $repository = $this->createMock(InvoiceRepository::class); + $repository + ->expects($this->any()) + ->method('hasInvoice') + ->willReturn(false); + + return new DateNumberGenerator($repository); + } }