From d4636b4772b0143486d23b06e27d34662e44f24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 21 Oct 2025 09:20:17 +0200 Subject: [PATCH 1/5] Add support for configurable invoice sequence scopes (global, monthly, annually) --- .gitignore | 1 + config/config.yaml | 2 + config/services/generators.xml | 1 + src/Entity/InvoiceSequence.php | 24 +++++++++++ src/Entity/InvoiceSequenceInterface.php | 8 ++++ src/Enum/InvoiceSequenceScopeEnum.php | 30 +++++++++++++ .../SequentialInvoiceNumberGenerator.php | 42 +++++++++++++++++-- tests/TestApplication/.env | 2 + 8 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 src/Enum/InvoiceSequenceScopeEnum.php diff --git a/.gitignore b/.gitignore index 012f2eeb..830c0a03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/var/ /vendor/ /node_modules/ /composer.lock diff --git a/config/config.yaml b/config/config.yaml index 87569df3..47fe4ced 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -5,6 +5,8 @@ imports: parameters: sylius_invoicing.invoice_save_path: "%kernel.project_dir%/private/invoices/" sylius_invoicing.filesystem_adapter.invoice: "sylius_invoicing_invoice" + sylius_invoicing.sequence_scope: '%env(default::SYLIUS_INVOICING_SEQUENCE_SCOPE)%' + env(SYLIUS_INVOICING_SEQUENCE_SCOPE): 'global' sylius_invoicing: pdf_generator: diff --git a/config/services/generators.xml b/config/services/generators.xml index cfca0642..f6fd5566 100644 --- a/config/services/generators.xml +++ b/config/services/generators.xml @@ -22,6 +22,7 @@ + %sylius_invoicing.sequence_scope% diff --git a/src/Entity/InvoiceSequence.php b/src/Entity/InvoiceSequence.php index 064eb8e8..e70d2681 100644 --- a/src/Entity/InvoiceSequence.php +++ b/src/Entity/InvoiceSequence.php @@ -23,6 +23,10 @@ class InvoiceSequence implements InvoiceSequenceInterface protected ?int $version = 1; + protected int $year; + + protected int $month; + /** @return mixed */ public function getId() { @@ -48,4 +52,24 @@ public function setVersion(?int $version): void { $this->version = $version; } + + public function getYear(): int + { + return $this->year; + } + + public function getMonth(): int + { + return $this->month; + } + + public function setYear(int $year): void + { + $this->year = $year; + } + + public function setMonth(int $month): void + { + $this->month = $month; + } } diff --git a/src/Entity/InvoiceSequenceInterface.php b/src/Entity/InvoiceSequenceInterface.php index a263fe9a..377f0c16 100644 --- a/src/Entity/InvoiceSequenceInterface.php +++ b/src/Entity/InvoiceSequenceInterface.php @@ -21,4 +21,12 @@ interface InvoiceSequenceInterface extends ResourceInterface, VersionedInterface public function getIndex(): int; public function incrementIndex(): void; + + public function getYear(): int; + + public function getMonth(): int; + + public function setYear(int $year): void; + + public function setMonth(int $month): void; } diff --git a/src/Enum/InvoiceSequenceScopeEnum.php b/src/Enum/InvoiceSequenceScopeEnum.php new file mode 100644 index 00000000..a2b727f2 --- /dev/null +++ b/src/Enum/InvoiceSequenceScopeEnum.php @@ -0,0 +1,30 @@ + self::MONTHLY, + 'annually' => self::ANNUALLY, + default => self::GLOBAL, + }; + } +} diff --git a/src/Generator/SequentialInvoiceNumberGenerator.php b/src/Generator/SequentialInvoiceNumberGenerator.php index 89e41349..43398a84 100644 --- a/src/Generator/SequentialInvoiceNumberGenerator.php +++ b/src/Generator/SequentialInvoiceNumberGenerator.php @@ -18,6 +18,7 @@ use Sylius\Component\Resource\Factory\FactoryInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\InvoicingPlugin\Entity\InvoiceSequenceInterface; +use Sylius\InvoicingPlugin\Enum\InvoiceSequenceScopeEnum; use Symfony\Component\Clock\ClockInterface; final class SequentialInvoiceNumberGenerator implements InvoiceNumberGenerator @@ -29,7 +30,17 @@ public function __construct( private readonly ClockInterface $clock, private readonly int $startNumber = 1, private readonly int $numberLength = 9, + private readonly ?string $scope = null, ) { + if (null === $this->scope) { + trigger_deprecation( + 'sylius/invoicing-plugin', + '2.1', + 'Not passing a "%s" to "%s" is deprecated and will be required in SyliusInvoicingPlugin 3.0.', + InvoiceSequenceScopeEnum::class, + self::class, + ); + } } public function generate(): string @@ -56,15 +67,38 @@ private function generateNumber(int $index): string private function getSequence(): InvoiceSequenceInterface { - /** @var InvoiceSequenceInterface $sequence */ - $sequence = $this->sequenceRepository->findOneBy([]); - - if (null != $sequence) { + $now = $this->clock->now(); + $scope = InvoiceSequenceScopeEnum::tryFrom($this->scope ?? '') ?? InvoiceSequenceScopeEnum::GLOBAL; + + $criteria = match ($scope) { + InvoiceSequenceScopeEnum::MONTHLY => [ + 'year' => (int) $now->format('Y'), + 'month' => (int) $now->format('m'), + ], + InvoiceSequenceScopeEnum::ANNUALLY => [ + 'year' => (int) $now->format('Y'), + ], + InvoiceSequenceScopeEnum::GLOBAL => [], + }; + + /** @var InvoiceSequenceInterface|null $sequence */ + $sequence = $this->sequenceRepository->findOneBy($criteria); + + if (null !== $sequence) { return $sequence; } /** @var InvoiceSequenceInterface $sequence */ $sequence = $this->sequenceFactory->createNew(); + + if (isset($criteria['year'])) { + $sequence->setYear($criteria['year']); + } + + if (isset($criteria['month'])) { + $sequence->setMonth($criteria['month']); + } + $this->sequenceManager->persist($sequence); return $sequence; diff --git a/tests/TestApplication/.env b/tests/TestApplication/.env index 657afa80..ef506f6c 100644 --- a/tests/TestApplication/.env +++ b/tests/TestApplication/.env @@ -9,3 +9,5 @@ WKHTMLTOPDF_PATH=/usr/local/bin/wkhtmltopdf ###< knplabs/knp-snappy-bundle ### TEST_SYLIUS_INVOICING_PDF_GENERATION_DISABLED=false +TEST_SYLIUS_INVOICING_PDF_GENERATION_DISABLED=false +SYLIUS_INVOICING_SEQUENCE_SCOPE='monthly' From 92fa666cff330c3d80b7ef988a7386738f378603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 21 Oct 2025 09:46:28 +0200 Subject: [PATCH 2/5] Add year and month support to invoice sequence configuration --- CHANGELOG-2.0.md | 5 +++ UPGRADE-2.0.md | 14 ++++++++ config/doctrine/InvoiceSequence.orm.xml | 3 ++ config/services/generators.xml | 2 +- src/Entity/InvoiceSequence.php | 16 ++++----- src/Entity/InvoiceSequenceInterface.php | 8 ++--- .../SequentialInvoiceNumberGenerator.php | 4 +-- src/Migrations/Version20251021074051.php | 35 +++++++++++++++++++ 8 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 src/Migrations/Version20251021074051.php diff --git a/CHANGELOG-2.0.md b/CHANGELOG-2.0.md index c68dbd6f..22127eec 100644 --- a/CHANGELOG-2.0.md +++ b/CHANGELOG-2.0.md @@ -1,5 +1,10 @@ # CHANGELOG +### v2.0.3 (2025-10-21) + +- [#373](https://github.com/Sylius/InvoicingPlugin/pull/373) Add configurable invoice sequence scope (`monthly`/`annually`/`global`) + via SYLIUS_INVOICING_SEQUENCE_SCOPE ENV ([@tomkalon](https://github.com/tomkalon)) + ### v2.0.2 (2025-07-03) - [#373](https://github.com/Sylius/InvoicingPlugin/pull/373) Add sylius/test-application ([@Wojdylak](https://github.com/Wojdylak)) diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 052c0a91..771821f3 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -1,3 +1,17 @@ +# UPGRADE FROM 2.0.2 TO 2.0.3 + +## Changes + +1. Added support for configurable invoice sequence scoping via the SYLIUS_INVOICING_SEQUENCE_SCOPE environment variable: + +- monthly: resets invoice numbering each month +- annually: resets invoice numbering each year +- global or unset (default): uses a single global sequence (as previously) + +## Deprecations + +1. Not passing the $scope argument (of type InvoiceSequenceScopeEnum) to the constructor of SequentialInvoiceNumberGenerator is deprecated and will be required starting from version 3.0. + # UPGRADE FROM 1.X TO 2.0 1. Support for Sylius 2.0 has been added, it is now the recommended Sylius version to use with InvoicingPlugin. diff --git a/config/doctrine/InvoiceSequence.orm.xml b/config/doctrine/InvoiceSequence.orm.xml index ed04a5e4..a6e79c89 100644 --- a/config/doctrine/InvoiceSequence.orm.xml +++ b/config/doctrine/InvoiceSequence.orm.xml @@ -11,6 +11,9 @@ + + + diff --git a/config/services/generators.xml b/config/services/generators.xml index f6fd5566..c01a26b6 100644 --- a/config/services/generators.xml +++ b/config/services/generators.xml @@ -22,7 +22,7 @@ - %sylius_invoicing.sequence_scope% + %sylius_invoicing.sequence_scope% diff --git a/src/Entity/InvoiceSequence.php b/src/Entity/InvoiceSequence.php index e70d2681..71e578da 100644 --- a/src/Entity/InvoiceSequence.php +++ b/src/Entity/InvoiceSequence.php @@ -23,9 +23,9 @@ class InvoiceSequence implements InvoiceSequenceInterface protected ?int $version = 1; - protected int $year; + protected ?int $year; - protected int $month; + protected ?int $month; /** @return mixed */ public function getId() @@ -53,22 +53,22 @@ public function setVersion(?int $version): void $this->version = $version; } - public function getYear(): int + public function getYear(): ?int { return $this->year; } - public function getMonth(): int + public function setYear(?int $year): void { - return $this->month; + $this->year = $year; } - public function setYear(int $year): void + public function getMonth(): ?int { - $this->year = $year; + return $this->month; } - public function setMonth(int $month): void + public function setMonth(?int $month): void { $this->month = $month; } diff --git a/src/Entity/InvoiceSequenceInterface.php b/src/Entity/InvoiceSequenceInterface.php index 377f0c16..c81c3976 100644 --- a/src/Entity/InvoiceSequenceInterface.php +++ b/src/Entity/InvoiceSequenceInterface.php @@ -22,11 +22,11 @@ public function getIndex(): int; public function incrementIndex(): void; - public function getYear(): int; + public function getYear(): ?int; - public function getMonth(): int; + public function getMonth(): ?int; - public function setYear(int $year): void; + public function setYear(?int $year): void; - public function setMonth(int $month): void; + public function setMonth(?int $month): void; } diff --git a/src/Generator/SequentialInvoiceNumberGenerator.php b/src/Generator/SequentialInvoiceNumberGenerator.php index 43398a84..3e5b7b9b 100644 --- a/src/Generator/SequentialInvoiceNumberGenerator.php +++ b/src/Generator/SequentialInvoiceNumberGenerator.php @@ -36,8 +36,8 @@ public function __construct( trigger_deprecation( 'sylius/invoicing-plugin', '2.1', - 'Not passing a "%s" to "%s" is deprecated and will be required in SyliusInvoicingPlugin 3.0.', - InvoiceSequenceScopeEnum::class, + 'Not passing the "%s" argument to "%s::__construct()" is deprecated and will be required in version 3.0. Pass a valid scope explicitly (e.g. "monthly", "annually", or "global").', + 'scope', self::class, ); } diff --git a/src/Migrations/Version20251021074051.php b/src/Migrations/Version20251021074051.php new file mode 100644 index 00000000..172cf4c6 --- /dev/null +++ b/src/Migrations/Version20251021074051.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE sylius_invoicing_plugin_sequence ADD year INT DEFAULT NULL, ADD month INT NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_invoicing_plugin_sequence DROP year, DROP month'); + } +} From 3353c9a1e92dde9c26d32faf203c564ee893044a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 21 Oct 2025 15:03:00 +0200 Subject: [PATCH 3/5] Extend GLOBAL invoice sequence scope with null year and month fields --- UPGRADE-2.0.md | 2 +- src/Generator/SequentialInvoiceNumberGenerator.php | 5 ++++- tests/TestApplication/.env | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 771821f3..667c4aa9 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -1,4 +1,4 @@ -# UPGRADE FROM 2.0.2 TO 2.0.3 +# UPGRADE FROM 2.0 TO 2.1 ## Changes diff --git a/src/Generator/SequentialInvoiceNumberGenerator.php b/src/Generator/SequentialInvoiceNumberGenerator.php index 3e5b7b9b..dc04b4e3 100644 --- a/src/Generator/SequentialInvoiceNumberGenerator.php +++ b/src/Generator/SequentialInvoiceNumberGenerator.php @@ -78,7 +78,10 @@ private function getSequence(): InvoiceSequenceInterface InvoiceSequenceScopeEnum::ANNUALLY => [ 'year' => (int) $now->format('Y'), ], - InvoiceSequenceScopeEnum::GLOBAL => [], + InvoiceSequenceScopeEnum::GLOBAL => [ + 'year' => null, + 'month' => null, + ], }; /** @var InvoiceSequenceInterface|null $sequence */ diff --git a/tests/TestApplication/.env b/tests/TestApplication/.env index ef506f6c..8ce5f4f4 100644 --- a/tests/TestApplication/.env +++ b/tests/TestApplication/.env @@ -10,4 +10,5 @@ WKHTMLTOPDF_PATH=/usr/local/bin/wkhtmltopdf TEST_SYLIUS_INVOICING_PDF_GENERATION_DISABLED=false TEST_SYLIUS_INVOICING_PDF_GENERATION_DISABLED=false + SYLIUS_INVOICING_SEQUENCE_SCOPE='monthly' From ff5ed371aa1c1d01cfb8fb75b0af14e9cd2075b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 21 Oct 2025 15:30:16 +0200 Subject: [PATCH 4/5] Add tests for monthly and annual invoice sequence scopes --- .../SequentialInvoiceNumberGeneratorTest.php | 92 ++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/tests/Unit/Generator/SequentialInvoiceNumberGeneratorTest.php b/tests/Unit/Generator/SequentialInvoiceNumberGeneratorTest.php index f04a331e..b654ec7a 100644 --- a/tests/Unit/Generator/SequentialInvoiceNumberGeneratorTest.php +++ b/tests/Unit/Generator/SequentialInvoiceNumberGeneratorTest.php @@ -69,7 +69,10 @@ public function it_generates_invoice_number(): void $dateTime = new \DateTimeImmutable('now'); $this->clock->method('now')->willReturn($dateTime); - $this->sequenceRepository->method('findOneBy')->with([])->willReturn($sequence); + $this->sequenceRepository + ->method('findOneBy') + ->with(['year' => null, 'month' => null]) + ->willReturn($sequence); $sequence->method('getVersion')->willReturn(1); $sequence->method('getIndex')->willReturn(0); @@ -96,7 +99,10 @@ public function it_generates_invoice_number_when_sequence_is_null(): void $dateTime = new \DateTimeImmutable('now'); $this->clock->method('now')->willReturn($dateTime); - $this->sequenceRepository->method('findOneBy')->with([])->willReturn(null); + $this->sequenceRepository + ->method('findOneBy') + ->with(['year' => null, 'month' => null]) + ->willReturn(null); $this->sequenceFactory->method('createNew')->willReturn($sequence); @@ -119,6 +125,86 @@ public function it_generates_invoice_number_when_sequence_is_null(): void $result = $this->generator->generate(); - $this->assertSame($dateTime->format('Y/m') . '/000000001', $result); + self::assertSame($dateTime->format('Y/m') . '/000000001', $result); + } + + #[Test] + public function it_generates_invoice_number_with_monthly_scope(): void + { + $sequence = $this->createMock(InvoiceSequenceInterface::class); + + $dateTime = new \DateTimeImmutable('2025-10-15'); + $this->clock->method('now')->willReturn($dateTime); + + $generator = new SequentialInvoiceNumberGenerator( + $this->sequenceRepository, + $this->sequenceFactory, + $this->sequenceManager, + $this->clock, + 1, + 9, + 'monthly' + ); + + $this->sequenceRepository + ->method('findOneBy') + ->with(['year' => 2025, 'month' => 10]) + ->willReturn($sequence); + + $sequence->method('getVersion')->willReturn(1); + $sequence->method('getIndex')->willReturn(0); + + $this->sequenceManager + ->expects(self::once()) + ->method('lock') + ->with($sequence, LockMode::OPTIMISTIC, 1); + + $sequence + ->expects(self::once()) + ->method('incrementIndex'); + + $result = $generator->generate(); + + self::assertSame('2025/10/000000001', $result); + } + + #[Test] + public function it_generates_invoice_number_with_annually_scope(): void + { + $sequence = $this->createMock(InvoiceSequenceInterface::class); + + $dateTime = new \DateTimeImmutable('2025-11-15'); + $this->clock->method('now')->willReturn($dateTime); + + $generator = new SequentialInvoiceNumberGenerator( + $this->sequenceRepository, + $this->sequenceFactory, + $this->sequenceManager, + $this->clock, + 1, + 9, + 'annually' + ); + + $this->sequenceRepository + ->method('findOneBy') + ->with(['year' => 2025]) + ->willReturn($sequence); + + $sequence->method('getVersion')->willReturn(1); + $sequence->method('getIndex')->willReturn(0); + + $this->sequenceManager + ->expects(self::once()) + ->method('lock') + ->with($sequence, LockMode::OPTIMISTIC, 1); + + $sequence + ->expects(self::once()) + ->method('incrementIndex'); + + $result = $generator->generate(); + + self::assertSame('2025/11/000000001', $result); } } From efd462a049d8fc6a216c86b4c6d7e6c657016d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Wed, 22 Oct 2025 09:54:34 +0200 Subject: [PATCH 5/5] Add `type` field to invoice sequences and update related logic --- config/doctrine/InvoiceSequence.orm.xml | 4 +- src/Entity/InvoiceSequence.php | 18 +++++- src/Entity/InvoiceSequenceInterface.php | 5 ++ src/Enum/InvoiceSequenceScopeEnum.php | 9 --- .../SequentialInvoiceNumberGenerator.php | 6 ++ src/Migrations/Version20251021074051.php | 6 +- .../SequentialInvoiceNumberGeneratorTest.php | 58 ++++++++++++++++++- 7 files changed, 88 insertions(+), 18 deletions(-) diff --git a/config/doctrine/InvoiceSequence.orm.xml b/config/doctrine/InvoiceSequence.orm.xml index a6e79c89..4d6591d2 100644 --- a/config/doctrine/InvoiceSequence.orm.xml +++ b/config/doctrine/InvoiceSequence.orm.xml @@ -12,8 +12,8 @@ - - + + diff --git a/src/Entity/InvoiceSequence.php b/src/Entity/InvoiceSequence.php index 71e578da..a8ec4226 100644 --- a/src/Entity/InvoiceSequence.php +++ b/src/Entity/InvoiceSequence.php @@ -13,6 +13,8 @@ namespace Sylius\InvoicingPlugin\Entity; +use Sylius\InvoicingPlugin\Enum\InvoiceSequenceScopeEnum; + /** @final */ class InvoiceSequence implements InvoiceSequenceInterface { @@ -23,9 +25,11 @@ class InvoiceSequence implements InvoiceSequenceInterface protected ?int $version = 1; - protected ?int $year; + protected ?InvoiceSequenceScopeEnum $type = null; + + protected ?int $year = null; - protected ?int $month; + protected ?int $month = null; /** @return mixed */ public function getId() @@ -53,6 +57,16 @@ public function setVersion(?int $version): void $this->version = $version; } + public function getType(): ?InvoiceSequenceScopeEnum + { + return $this->type; + } + + public function setType(?InvoiceSequenceScopeEnum $type): void + { + $this->type = $type; + } + public function getYear(): ?int { return $this->year; diff --git a/src/Entity/InvoiceSequenceInterface.php b/src/Entity/InvoiceSequenceInterface.php index c81c3976..51efcd57 100644 --- a/src/Entity/InvoiceSequenceInterface.php +++ b/src/Entity/InvoiceSequenceInterface.php @@ -15,6 +15,7 @@ use Sylius\Component\Resource\Model\ResourceInterface; use Sylius\Component\Resource\Model\VersionedInterface; +use Sylius\InvoicingPlugin\Enum\InvoiceSequenceScopeEnum; interface InvoiceSequenceInterface extends ResourceInterface, VersionedInterface { @@ -22,6 +23,10 @@ public function getIndex(): int; public function incrementIndex(): void; + public function getType(): ?InvoiceSequenceScopeEnum; + + public function setType(?InvoiceSequenceScopeEnum $type): void; + public function getYear(): ?int; public function getMonth(): ?int; diff --git a/src/Enum/InvoiceSequenceScopeEnum.php b/src/Enum/InvoiceSequenceScopeEnum.php index a2b727f2..110876b9 100644 --- a/src/Enum/InvoiceSequenceScopeEnum.php +++ b/src/Enum/InvoiceSequenceScopeEnum.php @@ -18,13 +18,4 @@ enum InvoiceSequenceScopeEnum: string case GLOBAL = 'global'; case MONTHLY = 'monthly'; case ANNUALLY = 'annually'; - - public static function fromString(?string $value): self - { - return match ($value) { - 'monthly' => self::MONTHLY, - 'annually' => self::ANNUALLY, - default => self::GLOBAL, - }; - } } diff --git a/src/Generator/SequentialInvoiceNumberGenerator.php b/src/Generator/SequentialInvoiceNumberGenerator.php index dc04b4e3..b5754949 100644 --- a/src/Generator/SequentialInvoiceNumberGenerator.php +++ b/src/Generator/SequentialInvoiceNumberGenerator.php @@ -74,9 +74,11 @@ private function getSequence(): InvoiceSequenceInterface InvoiceSequenceScopeEnum::MONTHLY => [ 'year' => (int) $now->format('Y'), 'month' => (int) $now->format('m'), + 'type' => $scope, ], InvoiceSequenceScopeEnum::ANNUALLY => [ 'year' => (int) $now->format('Y'), + 'type' => $scope, ], InvoiceSequenceScopeEnum::GLOBAL => [ 'year' => null, @@ -102,6 +104,10 @@ private function getSequence(): InvoiceSequenceInterface $sequence->setMonth($criteria['month']); } + if (isset($criteria['type'])) { + $sequence->setType($criteria['type']); + } + $this->sequenceManager->persist($sequence); return $sequence; diff --git a/src/Migrations/Version20251021074051.php b/src/Migrations/Version20251021074051.php index 172cf4c6..c8933206 100644 --- a/src/Migrations/Version20251021074051.php +++ b/src/Migrations/Version20251021074051.php @@ -20,16 +20,16 @@ final class Version20251021074051 extends AbstractMigration { public function getDescription(): string { - return 'Add year and month columns to sylius_invoicing_plugin_sequence table'; + return 'Add year, month and type columns to sylius_invoicing_plugin_sequence table'; } public function up(Schema $schema): void { - $this->addSql('ALTER TABLE sylius_invoicing_plugin_sequence ADD year INT DEFAULT NULL, ADD month INT NOT NULL'); + $this->addSql('ALTER TABLE sylius_invoicing_plugin_sequence ADD year INT DEFAULT NULL, ADD month INT DEFAULT NULL, ADD type VARCHAR(255) DEFAULT NULL'); } public function down(Schema $schema): void { - $this->addSql('ALTER TABLE sylius_invoicing_plugin_sequence DROP year, DROP month'); + $this->addSql('ALTER TABLE sylius_invoicing_plugin_sequence DROP year, DROP month, DROP type'); } } diff --git a/tests/Unit/Generator/SequentialInvoiceNumberGeneratorTest.php b/tests/Unit/Generator/SequentialInvoiceNumberGeneratorTest.php index b654ec7a..21f7c89d 100644 --- a/tests/Unit/Generator/SequentialInvoiceNumberGeneratorTest.php +++ b/tests/Unit/Generator/SequentialInvoiceNumberGeneratorTest.php @@ -21,6 +21,7 @@ use Sylius\Component\Resource\Factory\FactoryInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\InvoicingPlugin\Entity\InvoiceSequenceInterface; +use Sylius\InvoicingPlugin\Enum\InvoiceSequenceScopeEnum; use Sylius\InvoicingPlugin\Generator\InvoiceNumberGenerator; use Sylius\InvoicingPlugin\Generator\SequentialInvoiceNumberGenerator; use Symfony\Component\Clock\ClockInterface; @@ -148,7 +149,7 @@ public function it_generates_invoice_number_with_monthly_scope(): void $this->sequenceRepository ->method('findOneBy') - ->with(['year' => 2025, 'month' => 10]) + ->with(['year' => 2025, 'month' => 10, 'type' => InvoiceSequenceScopeEnum::MONTHLY]) ->willReturn($sequence); $sequence->method('getVersion')->willReturn(1); @@ -188,7 +189,7 @@ public function it_generates_invoice_number_with_annually_scope(): void $this->sequenceRepository ->method('findOneBy') - ->with(['year' => 2025]) + ->with(['year' => 2025, 'type' => InvoiceSequenceScopeEnum::ANNUALLY]) ->willReturn($sequence); $sequence->method('getVersion')->willReturn(1); @@ -207,4 +208,57 @@ public function it_generates_invoice_number_with_annually_scope(): void self::assertSame('2025/11/000000001', $result); } + + #[Test] + public function it_generates_invoice_number_when_monthly_sequence_is_null(): void + { + $sequence = $this->createMock(InvoiceSequenceInterface::class); + + $dateTime = new \DateTimeImmutable('2025-10-15'); + $this->clock->method('now')->willReturn($dateTime); + + $generator = new SequentialInvoiceNumberGenerator( + $this->sequenceRepository, + $this->sequenceFactory, + $this->sequenceManager, + $this->clock, + 1, + 9, + 'monthly' + ); + + $scope = InvoiceSequenceScopeEnum::MONTHLY; + + $this->sequenceRepository + ->expects(self::once()) + ->method('findOneBy') + ->with(['year' => 2025, 'month' => 10, 'type' => $scope]) + ->willReturn(null); + + $this->sequenceFactory->expects(self::once())->method('createNew')->willReturn($sequence); + $sequence->expects(self::once())->method('setYear')->with(2025); + $sequence->expects(self::once())->method('setMonth')->with(10); + $sequence->expects(self::once())->method('setType')->with($scope); + + $this->sequenceManager + ->expects(self::once()) + ->method('persist') + ->with($sequence); + + $sequence->method('getVersion')->willReturn(1); + $sequence->method('getIndex')->willReturn(0); + + $this->sequenceManager + ->expects(self::once()) + ->method('lock') + ->with($sequence, LockMode::OPTIMISTIC, 1); + + $sequence + ->expects(self::once()) + ->method('incrementIndex'); + + $result = $generator->generate(); + + self::assertSame('2025/10/000000001', $result); + } }