diff --git a/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php b/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php index f72db9e2..c1a12f3c 100644 --- a/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php +++ b/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php @@ -62,103 +62,135 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface preg_match_all('/{[^}]*?}/', $format, $matches); foreach ($matches[0] as $part) { - $formatterLength = null; - $increaseBy = 1; - - $tmp = str_replace(['{', '}'], '', $part); - - $parts = preg_split('/[,]+/', $tmp); - $tmp = $parts[0]; - if (\count($parts) === 2) { - $formatterLength = \intval($parts[1]); - if ((string) $formatterLength !== $parts[1]) { - $formatterLength = null; - } - } - - $parts = preg_split("/[\+]+/", $tmp); - $tmp = $parts[0]; - if (\count($parts) === 2) { - $increaseBy = \intval($parts[1]); - if ($increaseBy <= 0) { - $increaseBy = 1; - } - } - - switch ($tmp) { - case 'Y': - $partialResult = $invoiceDate->format('Y'); - break; - - case 'y': - $partialResult = $invoiceDate->format('y'); - break; - - case 'M': - $partialResult = $invoiceDate->format('m'); - break; - - case 'm': - $partialResult = $invoiceDate->format('n'); - break; - - case 'D': - $partialResult = $invoiceDate->format('d'); - break; - - case 'd': - $partialResult = $invoiceDate->format('j'); - break; - - case 'date': - $partialResult = $invoiceDate->format('ymd'); - break; - - // for customer - case 'cc': - $partialResult = $this->repository->getCounterForAllTime($invoiceDate, $this->model->getCustomer()) + 1; - break; - - case 'ccy': - $partialResult = $this->repository->getCounterForYear($invoiceDate, $this->model->getCustomer()) + 1; - break; - - case 'ccm': - $partialResult = $this->repository->getCounterForMonth($invoiceDate, $this->model->getCustomer()) + 1; - break; - - case 'ccd': - $partialResult = $this->repository->getCounterForDay($invoiceDate, $this->model->getCustomer()) + 1; - break; - - // across all invoices - case 'c': - $partialResult = $this->repository->getCounterForAllTime($invoiceDate) + $increaseBy; - break; - - case 'cy': - $partialResult = $this->repository->getCounterForYear($invoiceDate) + $increaseBy; - break; - - case 'cm': - $partialResult = $this->repository->getCounterForMonth($invoiceDate) + $increaseBy; - break; - - case 'cd': - $partialResult = $this->repository->getCounterForDay($invoiceDate) + $increaseBy; - break; - - default: - $partialResult = $part; - } - - if (null !== $formatterLength) { - $partialResult = str_pad($partialResult, $formatterLength, '0', STR_PAD_LEFT); - } - + $partialResult = $this->parseReplacer($invoiceDate, $part); $result = str_replace($part, $partialResult, $result); } return (string) $result; } + + private function parseReplacer(\DateTime $invoiceDate, string $originalFormat): string + { + $formatterLength = null; + $increaseBy = 0; + $formatPattern = str_replace(['{', '}'], '', $originalFormat); + + $parts = preg_split('/([+\-,])+/', $formatPattern, -1, PREG_SPLIT_DELIM_CAPTURE); + $format = array_shift($parts); + + if (\count($parts) % 2 !== 0) { + throw new \InvalidArgumentException('Invalid configuration found'); + } + + while (null !== ($tmp = array_shift($parts))) { + switch ($tmp) { + case '+': + $local = array_shift($parts); + if (!is_numeric($local)) { + throw new \InvalidArgumentException('Unknown increment found'); + } + $increaseBy = $increaseBy + \intval($local); + break; + + case '-': + $local = array_shift($parts); + if (!is_numeric($local)) { + throw new \InvalidArgumentException('Unknown decrement found'); + } + $increaseBy = $increaseBy - \intval($local); + break; + + case ',': + $local = array_shift($parts); + if (!is_numeric($local)) { + throw new \InvalidArgumentException('Unknown format length found'); + } + $formatterLength = \intval($local); + if ((string) $formatterLength !== $local) { + throw new \InvalidArgumentException('Unknown format length found'); + } + break; + + default: + throw new \InvalidArgumentException('Unknown pattern found'); + } + } + + if ($increaseBy === 0) { + $increaseBy = 1; + } + + switch ($format) { + case 'Y': + $partialResult = $invoiceDate->format('Y'); + break; + + case 'y': + $partialResult = $invoiceDate->format('y'); + break; + + case 'M': + $partialResult = $invoiceDate->format('m'); + break; + + case 'm': + $partialResult = $invoiceDate->format('n'); + break; + + case 'D': + $partialResult = $invoiceDate->format('d'); + break; + + case 'd': + $partialResult = $invoiceDate->format('j'); + break; + + case 'date': + $partialResult = $invoiceDate->format('ymd'); + break; + + // for customer + case 'cc': + $partialResult = $this->repository->getCounterForAllTime($invoiceDate, $this->model->getCustomer()) + $increaseBy; + break; + + case 'ccy': + $partialResult = $this->repository->getCounterForYear($invoiceDate, $this->model->getCustomer()) + $increaseBy; + break; + + case 'ccm': + $partialResult = $this->repository->getCounterForMonth($invoiceDate, $this->model->getCustomer()) + $increaseBy; + break; + + case 'ccd': + $partialResult = $this->repository->getCounterForDay($invoiceDate, $this->model->getCustomer()) + $increaseBy; + break; + + // across all invoices + case 'c': + $partialResult = $this->repository->getCounterForAllTime($invoiceDate) + $increaseBy; + break; + + case 'cy': + $partialResult = $this->repository->getCounterForYear($invoiceDate) + $increaseBy; + break; + + case 'cm': + $partialResult = $this->repository->getCounterForMonth($invoiceDate) + $increaseBy; + break; + + case 'cd': + $partialResult = $this->repository->getCounterForDay($invoiceDate) + $increaseBy; + break; + + default: + $partialResult = $originalFormat; + } + + if (null !== $formatterLength) { + $partialResult = str_pad($partialResult, $formatterLength, '0', STR_PAD_LEFT); + } + + return $partialResult; + } } diff --git a/tests/Invoice/NumberGenerator/ConfigurableNumberGeneratorTest.php b/tests/Invoice/NumberGenerator/ConfigurableNumberGeneratorTest.php index 443bd072..d8b7a067 100644 --- a/tests/Invoice/NumberGenerator/ConfigurableNumberGeneratorTest.php +++ b/tests/Invoice/NumberGenerator/ConfigurableNumberGeneratorTest.php @@ -22,7 +22,7 @@ use PHPUnit\Framework\TestCase; */ class ConfigurableNumberGeneratorTest extends TestCase { - private function getSut(string $format) + private function getSut(string $format, int $counter = 1) { $config = $this->createMock(SystemConfiguration::class); $config->expects($this->any()) @@ -33,19 +33,19 @@ class ConfigurableNumberGeneratorTest extends TestCase $repository ->expects($this->any()) ->method('getCounterForAllTime') - ->willReturn(1); + ->willReturn($counter); $repository ->expects($this->any()) ->method('getCounterForYear') - ->willReturn(1); + ->willReturn($counter); $repository ->expects($this->any()) ->method('getCounterForMonth') - ->willReturn(1); + ->willReturn($counter); $repository ->expects($this->any()) ->method('getCounterForDay') - ->willReturn(1); + ->willReturn($counter); return new ConfigurableNumberGenerator($repository, $config); } @@ -68,12 +68,14 @@ class ConfigurableNumberGeneratorTest extends TestCase ['{cy}', '2', $invoiceDate], ['{cm}', '2', $invoiceDate], ['{cd}', '2', $invoiceDate], + ['{cc}', '2', $invoiceDate], + ['{ccy}', '2', $invoiceDate], + ['{ccm}', '2', $invoiceDate], + ['{ccd}', '2', $invoiceDate], // number formatting (not testing the lower case versions, as the tests might break depending on the date) ['{date,10}', '0000' . $invoiceDate->format('ymd'), $invoiceDate], - ['{date,a}', $invoiceDate->format('ymd'), $invoiceDate], // invalid formatter length ['{Y,6}', '00' . $invoiceDate->format('Y'), $invoiceDate], ['{M,3}', '0' . $invoiceDate->format('m'), $invoiceDate], - ['{M,#}', $invoiceDate->format('m'), $invoiceDate], // invalid formatter length ['{D,3}', '0' . $invoiceDate->format('d'), $invoiceDate], // counter across all invoices ['{c,2}', '02', $invoiceDate], @@ -86,32 +88,50 @@ class ConfigurableNumberGeneratorTest extends TestCase ['{ccm,2}', '02', $invoiceDate], ['{ccd,2}', '02', $invoiceDate], // with incrementing counter + ['{c+13,3}', '014', $invoiceDate], ['{c+13,2}', '14', $invoiceDate], - ['{ccy+1,2}', '02', $invoiceDate], - ['{cm+-1,2}', '02', $invoiceDate], // negative is not allowed and set to 1 - ['{cm+0,2}', '02', $invoiceDate], // zero is not allowed and set to 1 - ['{cd+111,2}', '112', $invoiceDate], ['{c+13}', '14', $invoiceDate], - ['{cy+1}', '2', $invoiceDate], - ['{cm+-1}', '2', $invoiceDate], // negative is not allowed and set to 1 - ['{cm+0}', '2', $invoiceDate], // zero is not allowed and set to 1 ['{cd+111}', '112', $invoiceDate], + ['{cd+111,5}', '00112', $invoiceDate], + ['{cd+111,2}', '113', $invoiceDate, 2], + ['{cm+0,2}', '03', $invoiceDate, 2], // zero is not allowed and set to 1 + ['{cm+0}', '2', $invoiceDate], // zero is not allowed and set to 1 + ['{cy+2}', '3', $invoiceDate], + ['{cc+4}', '5', $invoiceDate], + ['{ccy+2,2}', '03', $invoiceDate], + ['{ccm+2,2}', '03', $invoiceDate], + ['{ccd+2,2}', '03', $invoiceDate], // mixing identifiers ['{Y}{cy}', $invoiceDate->format('Y') . '2', $invoiceDate], ['{Y}{cy}{m}', $invoiceDate->format('Y') . '2' . $invoiceDate->format('n'), $invoiceDate], ['{Y}-{cy}/{m}', $invoiceDate->format('Y') . '-2/' . $invoiceDate->format('n'), $invoiceDate], ['{Y}-{cy}/{m}', $invoiceDate->format('Y') . '-2/' . $invoiceDate->format('n'), $invoiceDate], ['{Y,5}/{cy,5}', '0' . $invoiceDate->format('Y') . '/00002', $invoiceDate], - ['{Y,!}/{cy,o}', $invoiceDate->format('Y') . '/2', $invoiceDate], // invalid formatter length + // with decrementing counter + ['{c-1,2}', '00', $invoiceDate], + ['{c-2,2}', '-1', $invoiceDate], + // with incrementing and decrementing counter + ['{c-5+13,1}', '9', $invoiceDate], + ['{c+13-5,2}', '09', $invoiceDate], + // undefined behaviour - can change at any time + ['{cm+-1,2}', '00', $invoiceDate], + ['{cm+-1}', '0', $invoiceDate], + ['{cm-+1,2}', '02', $invoiceDate], + ['{cm-+1}', '2', $invoiceDate], + ['{a-+1+-1+++2}', '{a-+1+-1+++2}', $invoiceDate], + ['{date+2+2+2}', $invoiceDate->format('ymd'), $invoiceDate], + ['{cm+22+2+2-1-13-4}', '9', $invoiceDate], + ['{cm+22+2-2-22}', '6', $invoiceDate, 5], + ['{cm-21+22+2-2}', '6', $invoiceDate, 5], ]; } /** * @dataProvider getTestData */ - public function testGetInvoiceNumber(string $format, string $expectedInvoiceNumber, \DateTime $invoiceDate) + public function testGetInvoiceNumber(string $format, string $expectedInvoiceNumber, \DateTime $invoiceDate, int $counter = 1) { - $sut = $this->getSut($format); + $sut = $this->getSut($format, $counter); $model = new InvoiceModel(new DebugFormatter()); $model->setInvoiceDate($invoiceDate); $model->setCustomer(new Customer()); @@ -120,4 +140,43 @@ class ConfigurableNumberGeneratorTest extends TestCase $this->assertEquals($expectedInvoiceNumber, $sut->getInvoiceNumber()); $this->assertEquals('default', $sut->getId()); } + + public function getInvalidTestData() + { + $invoiceDate = new \DateTime(); + + return [ + ['{cm-}', $invoiceDate, 'decrement'], + ['{cm+}', $invoiceDate, 'increment'], + ['{cm+-}', $invoiceDate, 'decrement'], + ['{cm-+}', $invoiceDate, 'increment'], + ['{cy,}', $invoiceDate, 'format length'], + ['{date,a}', $invoiceDate, 'format length'], + ['{M,#}', $invoiceDate, 'format length'], + ['{,a}', $invoiceDate, 'format length'], + ['{Y,!}/{cy,o}', $invoiceDate, 'format length'], + ['{cm,}', $invoiceDate, 'format length'], + ['{cd+111,050}', $invoiceDate, 'format length'], + ['{-+}', $invoiceDate, 'increment'], + ['{+}', $invoiceDate, 'increment'], + ['{+-}', $invoiceDate, 'decrement'], + ]; + } + + /** + * @dataProvider getInvalidTestData + */ + public function testInvalidGetInvoiceNumber(string $format, \DateTime $invoiceDate, string $brokenPart) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Unknown %s found', $brokenPart)); + + $sut = $this->getSut($format); + $model = new InvoiceModel(new DebugFormatter()); + $model->setInvoiceDate($invoiceDate); + $model->setCustomer(new Customer()); + $sut->setModel($model); + + $sut->getInvoiceNumber(); + } }