Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions UPGRADE-2.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# UPGRADE FROM 2.2 TO 2.3

1. Invoice number sequences can now be scoped. The scope is configurable and defaults to `global`,
which keeps the exact numbering format and counter continuity of previous versions:

```yaml
sylius_invoicing:
sequence:
scope: global # one of "global", "monthly", "annually"
```

- `global` — a single, ever-increasing counter for the whole store, never reset.
- `monthly` — a separate counter per year and month, reset on the 1st of every month.
- `annually` — a separate counter per year, reset on the 1st of January.

All three produce the same `Y/m/index` number format; only the counter's reset behavior differs.

Custom scopes can be added by registering a service implementing
`Sylius\InvoicingPlugin\Resolver\SequenceScopeResolverInterface`, tagged with
`sylius_invoicing.sequence_scope_resolver`, and setting its name as the `scope` option.
Keep in mind that the number prefix returned by `prefix()` must make invoice numbers unique
across all periods of the scope, as invoice files are stored under names derived from invoice numbers.

Changing the `scope` on a store that already has invoices is not risk-free: each scope keeps
its own counter, so switching scopes does not carry over or reset any existing counter — a fresh
one is started instead. If the new scope's counter produces a number that was already issued
under the previous scope for the current period, invoice generation will fail on the database's
unique constraint. To avoid this, only change the `scope` at the very start of a new period (e.g.
right after midnight on the 1st of a month), before any invoice has been issued in it.

1. Run doctrine migrations when upgrading — the `sylius_invoicing_plugin_sequence` table gains
`type`, `year` and `month` columns, and unique indexes are created on the sequence scope and on
the invoice number. Migrations are provided for both MySQL and PostgreSQL. In the unlikely case
your database contains duplicated invoice numbers, the migration will fail and the duplicates
have to be resolved manually first. You can check for duplicates upfront with:

```sql
SELECT number, COUNT(*) FROM sylius_invoicing_plugin_invoice GROUP BY number HAVING COUNT(*) > 1;
```

1. The following interfaces and classes have changed as part of the sequence scoping feature:

- `Sylius\InvoicingPlugin\Entity\InvoiceSequenceInterface` and `Sylius\InvoicingPlugin\Entity\InvoiceSequence`
gained the `getType(): string`, `setType(string $type): void`, `getYear(): int`, `setYear(int $year): void`,
`getMonth(): int` and `setMonth(int $month): void` methods, together with the `SCOPE_GLOBAL`, `SCOPE_MONTHLY`
and `SCOPE_ANNUALLY` constants. Custom implementations must be updated accordingly.

- `Sylius\InvoicingPlugin\Generator\SequentialInvoiceNumberGenerator` — not passing a value
for the `$scopeResolvers` argument is deprecated and the argument will be required in 3.0;
until then, omitting it falls back to the built-in `global`, `monthly` and `annually` resolvers:

```diff
public function __construct(
private readonly RepositoryInterface $sequenceRepository,
private readonly FactoryInterface $sequenceFactory,
private readonly EntityManagerInterface $sequenceManager,
private readonly ClockInterface $clock,
private readonly int $startNumber = 1,
private readonly int $numberLength = 9,
+ ?iterable $scopeResolvers = null,
+ private readonly string $scope = InvoiceSequenceInterface::SCOPE_GLOBAL,
)
```

- `Sylius\InvoicingPlugin\Creator\InvoiceCreator` now persists the invoice before writing its PDF file,
so that a duplicated invoice number fails on the database unique constraint without overwriting
a file belonging to another invoice. Database errors are no longer swallowed and propagate to the caller.
4 changes: 4 additions & 0 deletions config/doctrine/Invoice.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<mapped-superclass name="Sylius\InvoicingPlugin\Entity\Invoice" table="sylius_invoicing_plugin_invoice">
<unique-constraints>
<unique-constraint name="UNIQ_SYLIUS_INVOICING_INVOICE_NUMBER" columns="number" />
</unique-constraints>

<id name="id" column="id" type="string" />

<field name="number" column="number" />
Expand Down
19 changes: 19 additions & 0 deletions config/doctrine/InvoiceSequence.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,30 @@
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

<mapped-superclass name="Sylius\InvoicingPlugin\Entity\InvoiceSequence" table="sylius_invoicing_plugin_sequence">
<unique-constraints>
<unique-constraint name="UNIQ_SYLIUS_INVOICING_SEQUENCE_SCOPE" columns="type,year,month" />
</unique-constraints>

<id name="id" column="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="index" column="idx" type="integer" />
<field name="version" type="integer" version="true" />
<field name="year" type="integer">
<options>
<option name="default">0</option>
</options>
</field>
<field name="month" type="integer">
<options>
<option name="default">0</option>
</options>
</field>
<field name="type" type="string">
<options>
<option name="default">global</option>
</options>
</field>
</mapped-superclass>

</doctrine-mapping>
1 change: 1 addition & 0 deletions config/services/generators.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<argument type="service" id="sylius_invoicing.factory.invoice_sequence" />
<argument type="service" id="sylius_invoicing.manager.invoice_sequence" />
<argument type="service" id="clock" />
<argument key="$scopeResolvers" type="tagged_iterator" tag="sylius_invoicing.sequence_scope_resolver" />
</service>

<service id="sylius_invoicing.generator.invoice_identifier" class="Sylius\InvoicingPlugin\Generator\UuidInvoiceIdentifierGenerator" />
Expand Down
32 changes: 32 additions & 0 deletions config/services/resolvers.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--

This file is part of the Sylius package.

(c) Sylius Sp. z o.o.

For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.

-->

<container
xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<services>
<service id="sylius_invoicing.resolver.sequence_scope.global" class="Sylius\InvoicingPlugin\Resolver\GlobalSequenceScopeResolver">
<tag name="sylius_invoicing.sequence_scope_resolver" />
</service>

<service id="sylius_invoicing.resolver.sequence_scope.monthly" class="Sylius\InvoicingPlugin\Resolver\MonthlySequenceScopeResolver">
<tag name="sylius_invoicing.sequence_scope_resolver" />
</service>

<service id="sylius_invoicing.resolver.sequence_scope.annually" class="Sylius\InvoicingPlugin\Resolver\AnnuallySequenceScopeResolver">
<tag name="sylius_invoicing.sequence_scope_resolver" />
</service>
</services>
</container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@invoice_number_sequence @annually_sequence_scope @pdf_enabled
Feature: Numbering invoices with an annual sequence
In order to have invoice numbers restarting every year
As a Shop Owner
I want invoice numbers to be scoped to the year they are issued in

Background:
Given the store operates on a single channel in "United States"
And the store has a product "Pascaline Calculator Replica" priced at "$60.00"
And the store has "UPS" shipping method with "$10.00" fee
And the store allows paying with "Cash on Delivery"

@application
Scenario: Continuing numbering across months of the same year and restarting in a new year
Given it is "2025-11-20" now
And there is a customer "blaise.pascal@gmail.com" that placed an order "#00000001"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Blaise Pascal" addressed it to "Royal St", "70116" "New Orleans" in the "United States"
And for the billing address of "Gilberte Périer" in the "Chartres St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
And it is "2025-12-31" now
And there is another customer "pierre.fermat@gmail.com" that placed an order "#00000002"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Pierre de Fermat" addressed it to "Toulouse St", "70116" "New Orleans" in the "United States"
And for the billing address of "Pierre de Fermat" in the "Toulouse St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
And it is "2026-01-01" now
And there is another customer "christiaan.huygens@gmail.com" that placed an order "#00000003"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Christiaan Huygens" addressed it to "Dauphine St", "70116" "New Orleans" in the "United States"
And for the billing address of "Christiaan Huygens" in the "Dauphine St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
Then the invoice for order "#00000001" should have number "2025/11/000000001"
And the invoice for order "#00000002" should have number "2025/12/000000002"
And the invoice for order "#00000003" should have number "2026/01/000000001"
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@invoice_number_sequence @global_sequence_scope @pdf_enabled
Feature: Numbering invoices with a global sequence
In order to have gapless, ever-increasing invoice numbers
As a Shop Owner
I want invoice numbers to keep incrementing regardless of the passing months and years

Background:
Given the store operates on a single channel in "United States"
And the store has a product "ES-335 Semi-Hollow Guitar" priced at "$60.00"
And the store has "UPS" shipping method with "$10.00" fee
And the store allows paying with "Cash on Delivery"

@application
Scenario: Continuing numbering across months and years
Given it is "2025-12-20" now
And there is a customer "bb.king@gmail.com" that placed an order "#00000001"
And the customer bought a single "ES-335 Semi-Hollow Guitar"
And the customer "B.B. King" addressed it to "Beale St", "38103" "Memphis" in the "United States"
And for the billing address of "Lucille King" in the "Union Ave", "38103" "Memphis", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
And it is "2026-01-05" now
And there is another customer "chuck.berry@gmail.com" that placed an order "#00000002"
And the customer bought a single "ES-335 Semi-Hollow Guitar"
And the customer "Chuck Berry" addressed it to "Gibson Ave", "49001" "Kalamazoo" in the "United States"
And for the billing address of "Chuck Berry" in the "Gibson Ave", "49001" "Kalamazoo", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
Then the invoice for order "#00000001" should have number "2025/12/000000001"
And the invoice for order "#00000002" should have number "2026/01/000000002"
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@invoice_number_sequence @monthly_sequence_scope @pdf_enabled
Feature: Numbering invoices with a monthly sequence
In order to have invoice numbers restarting every month
As a Shop Owner
I want invoice numbers to be scoped to the month they are issued in

Background:
Given the store operates on a single channel in "United States"
And the store has a product "Pascaline Calculator Replica" priced at "$60.00"
And the store has "UPS" shipping method with "$10.00" fee
And the store allows paying with "Cash on Delivery"

@application
Scenario: Incrementing numbers within the same month
Given it is "2025-10-15" now
And there is a customer "blaise.pascal@gmail.com" that placed an order "#00000001"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Blaise Pascal" addressed it to "Royal St", "70116" "New Orleans" in the "United States"
And for the billing address of "Gilberte Périer" in the "Chartres St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
And there is another customer "pierre.fermat@gmail.com" that placed an order "#00000002"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Pierre de Fermat" addressed it to "Toulouse St", "70116" "New Orleans" in the "United States"
And for the billing address of "Pierre de Fermat" in the "Toulouse St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
Then the invoice for order "#00000001" should have number "2025/10/000000001"
And the invoice for order "#00000002" should have number "2025/10/000000002"

@application
Scenario: Restarting numbering in a new month
Given it is "2025-10-15" now
And there is a customer "blaise.pascal@gmail.com" that placed an order "#00000001"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Blaise Pascal" addressed it to "Royal St", "70116" "New Orleans" in the "United States"
And for the billing address of "Gilberte Périer" in the "Chartres St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
And it is "2025-11-03" now
And there is another customer "pierre.fermat@gmail.com" that placed an order "#00000002"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Pierre de Fermat" addressed it to "Toulouse St", "70116" "New Orleans" in the "United States"
And for the billing address of "Pierre de Fermat" in the "Toulouse St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
Then the invoice for order "#00000001" should have number "2025/10/000000001"
And the invoice for order "#00000002" should have number "2025/11/000000001"

@application
Scenario: Restarting numbering across a year boundary
Given it is "2025-12-31" now
And there is a customer "blaise.pascal@gmail.com" that placed an order "#00000001"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Blaise Pascal" addressed it to "Royal St", "70116" "New Orleans" in the "United States"
And for the billing address of "Gilberte Périer" in the "Chartres St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
And it is "2026-01-01" now
And there is another customer "pierre.fermat@gmail.com" that placed an order "#00000002"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Pierre de Fermat" addressed it to "Toulouse St", "70116" "New Orleans" in the "United States"
And for the billing address of "Pierre de Fermat" in the "Toulouse St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
Then the invoice for order "#00000001" should have number "2025/12/000000001"
And the invoice for order "#00000002" should have number "2026/01/000000001"
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@invoice_number_sequence @global_sequence_scope @pdf_enabled
Feature: Preventing duplicated invoice numbers
In order to never expose an invoice of one customer to another
As a Shop Owner
I want invoice generation to fail loudly when it would reuse an already issued invoice number

Background:
Given the store operates on a single channel in "United States"
And the store has a product "Pascaline Calculator Replica" priced at "$60.00"
And the store has "UPS" shipping method with "$10.00" fee
And the store allows paying with "Cash on Delivery"
And it is "2025-10-15" now
And there is a customer "blaise.pascal@gmail.com" that placed an order "#00000001"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Blaise Pascal" addressed it to "Royal St", "70116" "New Orleans" in the "United States"
And for the billing address of "Gilberte Périer" in the "Chartres St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
And there is another customer "pierre.fermat@gmail.com" that placed an order "#00000002"
And the customer bought a single "Pascaline Calculator Replica"
And the customer "Pierre de Fermat" addressed it to "Toulouse St", "70116" "New Orleans" in the "United States"
And for the billing address of "Pierre de Fermat" in the "Toulouse St", "70116" "New Orleans", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment

@application
Scenario: Failing loudly instead of reusing an already issued invoice number
Given the order "#00000002" has lost all of its invoices
And the invoice number sequences have been reset
Then it should not be possible to generate an invoice for order "#00000002"
And the order "#00000002" should have no invoice
And the invoice for order "#00000001" should have number "2025/10/000000001"
And the invoice for order "#00000001" should be saved on the server
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@invoice_number_sequence @global_sequence_scope @pdf_enabled
Feature: Recovering lost invoice files
In order to always hand invoices out to customers
As a Shop Owner
I want invoice files to be regenerated from the database when they are missing from the storage

Background:
Given the store operates on a single channel in "United States"
And the store has a product "ES-335 Semi-Hollow Guitar" priced at "$60.00"
And the store has "UPS" shipping method with "$10.00" fee
And the store allows paying with "Cash on Delivery"
And it is "2025-10-15" now
And there is a customer "bb.king@gmail.com" that placed an order "#00000001"
And the customer bought a single "ES-335 Semi-Hollow Guitar"
And the customer "B.B. King" addressed it to "Beale St", "38103" "Memphis" in the "United States"
And for the billing address of "Lucille King" in the "Union Ave", "38103" "Memphis", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment

@application
Scenario: Regenerating a lost invoice file on download
Given the invoice file for order "#00000001" has been removed from the server
Then the invoice for order "#00000001" should be downloadable with number "2025/10/000000001"
And the invoice for order "#00000001" should be saved on the server
Loading
Loading