diff --git a/.gitignore b/.gitignore index 57071a2..72f2e9d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /vendor /composer.lock /tmp -/.php_cs.cache \ No newline at end of file +/.php_cs.cache +/.phpunit.result.cache \ No newline at end of file diff --git a/src/Config/AbstractConfiguration.php b/src/Config/AbstractConfiguration.php new file mode 100644 index 0000000..e0b72a1 --- /dev/null +++ b/src/Config/AbstractConfiguration.php @@ -0,0 +1,26 @@ + + */ + public function getCustomData(): iterable; + + /** + * The precision to be applied to the amount + * + * @return int|null + */ + public function getAmountPrecision(): ?int; + + /** + * The standard does not support setting due date, you can use this to set your own non-standard implementation + * + * @return DueDateHandlerInterface|null + */ + public function getDueDateHandler(): ?DueDateHandlerInterface; +} diff --git a/src/Config/DueDateHandlerInterface.php b/src/Config/DueDateHandlerInterface.php new file mode 100644 index 0000000..cfd64ea --- /dev/null +++ b/src/Config/DueDateHandlerInterface.php @@ -0,0 +1,12 @@ +iban = $iban; + $this->configuration = $configuration; } /** @@ -318,17 +329,28 @@ public function getQrString(): string } } + if ($this->getAmount()) { + $amount = $this->configuration->getAmountPrecision() !== null + ? number_format($this->getAmount(), $this->configuration->getAmountPrecision(), '.', '') + : $this->getAmount(); + } else { + $amount = ''; + } + $result[] = 'BCD'; // the service tag - $result[] = '002'; // version + $result[] = $this->configuration->getVersion(); $result[] = $this->getCharacterSet(); $result[] = 'SCT'; // identification $result[] = $this->getBic(); $result[] = $this->getBeneficiaryName(); $result[] = $this->getIban()->asString(); - $result[] = $this->getAmount() ? $this->getCurrency() . $this->getAmount() : ''; + $result[] = $amount ? $this->getCurrency() . $amount : ''; $result[] = $this->getPurpose(); $result[] = $this->getRemittanceText(); $result[] = $this->getInformation(); + foreach ($this->configuration->getCustomData() as $customDatum) { + $result[] = $customDatum; + } $result = implode("\n", $result); @@ -378,11 +400,19 @@ public function setOptions(array $options) public function setDueDate(DateTimeInterface $dueDate) { + if ($handler = $this->configuration->getDueDateHandler()) { + $handler->setDueDate($dueDate); + + return $this; + } throw new UnsupportedMethodException('The European standard does not support setting due date'); } public function getDueDate(): DateTimeInterface { + if ($handler = $this->configuration->getDueDateHandler()) { + return $handler->getDueDate(); + } throw new UnsupportedMethodException('The European standard does not support setting due date'); } diff --git a/tests/Config/AbstractConfigurationTest.php b/tests/Config/AbstractConfigurationTest.php new file mode 100644 index 0000000..315948c --- /dev/null +++ b/tests/Config/AbstractConfigurationTest.php @@ -0,0 +1,192 @@ +getInstance(); + self::assertEquals('002', $this->getLine($instance, 1)); + + $config = new class extends AbstractConfiguration { + public function getVersion(): string + { + return '001'; + } + }; + $instance = $this->getInstance($config); + self::assertEquals('001', $this->getLine($instance, 1)); + + $config = new class extends AbstractConfiguration { + public function getVersion(): string + { + return '002'; + } + }; + $instance = $this->getInstance($config); + self::assertEquals('002', $this->getLine($instance, 1)); + + $config = new class extends AbstractConfiguration { + public function getVersion(): string + { + return '003'; + } + }; + $instance = $this->getInstance($config); + self::assertEquals('003', $this->getLine($instance, 1)); + } + + public function testGetCustomData() + { + // test default + $instance = $this->getInstance(); + self::assertCount(11, $this->getLines($instance)); + + $config = new class extends AbstractConfiguration { + public function getCustomData(): iterable + { + yield 'A'; + yield 'B'; + } + }; + $instance = $this->getInstance($config); + self::assertCount(13, $this->getLines($instance)); + self::assertEquals('A', $this->getLine($instance, 11)); + self::assertEquals('B', $this->getLine($instance, 12)); + } + + public function testGetAmountPrecision() + { + // test default + $instance = $this->getInstance(); + self::assertEquals('EUR123.4567', $this->getLine($instance, 7)); + + $config = new class extends AbstractConfiguration { + public function getAmountPrecision(): ?int + { + return 2; + } + }; + $instance = $this->getInstance($config); + self::assertEquals('EUR123.46', $this->getLine($instance, 7)); + + $config = new class extends AbstractConfiguration { + public function getAmountPrecision(): ?int + { + return 1; + } + }; + $instance = $this->getInstance($config); + self::assertEquals('EUR123.5', $this->getLine($instance, 7)); + + $config = new class extends AbstractConfiguration { + public function getAmountPrecision(): ?int + { + return 5; + } + }; + $instance = $this->getInstance($config); + self::assertEquals('EUR123.45670', $this->getLine($instance, 7)); + + $config = new class extends AbstractConfiguration { + public function getAmountPrecision(): ?int + { + return null; + } + }; + $instance = $this->getInstance($config); + self::assertEquals('EUR123.4567', $this->getLine($instance, 7)); + } + + public function testGetDueDateHandler() + { + // test default + $instance = $this->getInstance(); + try { + $instance->getDueDate(); + $this->fail('Expected ' . UnsupportedMethodException::class); + } catch (UnsupportedMethodException $e) { + } + try { + $instance->setDueDate(new DateTimeImmutable()); + $this->fail('Expected ' . UnsupportedMethodException::class); + } catch (UnsupportedMethodException $e) { + } + + $dueDateHandler = new class implements DueDateHandlerInterface { + /** + * @var DateTimeInterface + */ + private $dueDate = null; + + public function setDueDate(DateTimeInterface $dueDate): void + { + $this->dueDate = $dueDate; + } + + public function getDueDate(): DateTimeInterface + { + return $this->dueDate ?? new DateTimeImmutable(); + } + }; + $config = new class($dueDateHandler) extends AbstractConfiguration { + /** + * @var DueDateHandlerInterface + */ + private $dueDateHandler; + + public function __construct(DueDateHandlerInterface $dueDateHandler) + { + $this->dueDateHandler = $dueDateHandler; + } + + public function getDueDateHandler(): ?DueDateHandlerInterface + { + return $this->dueDateHandler; + } + + public function getCustomData(): iterable + { + yield $this->dueDateHandler->getDueDate()->format('Ymd'); + } + }; + $instance = $this->getInstance($config); + $instance->setDueDate(new DateTimeImmutable('2025-01-01 12:00:00')); + self::assertEquals('2025-01-01 12:00:00', $instance->getDueDate()->format('Y-m-d H:i:s')); + self::assertEquals('20250101', $this->getLine($instance, 11)); + } + + private function getInstance(AbstractConfiguration $configuration = null): QrPayment + { + return (new QrPayment(new CzechIbanAdapter('1325090010', '3030'), $configuration)) + ->setAmount(123.4567) + ->setBeneficiaryName('Random Dude'); + } + + private function getLine(QrPayment $payment, int $line): ?string + { + $lines = $this->getLines($payment); + if (!isset($lines[$line])) { + return null; + } + + return $lines[$line]; + } + + private function getLines(QrPayment $payment): array + { + $result = $payment->getQrString(); + return explode("\n", $result); + } +}