Skip to content
Draft
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
5 changes: 5 additions & 0 deletions CHANGELOG-2.0.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG-3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# CHANGELOG

### v3.0.0 (2025-10-24)

- [#397](https://github.com/Sylius/InvoicingPlugin/pull/397) Isolate plugin messaging: Introduce dedicated event & command buses for InvoicingPlugin ([@tomkalon](https://github.com/tomkalon))
- [#398](https://github.com/Sylius/InvoicingPlugin/pull/398) Persisted PDF path & file flow
- `Invoice`: added `path` field (UNIQUE).
- `InvoiceFactory`: inject `InvoiceFileNameGeneratorInterface`; use `generateForPdf($number)` to set `path` on creation.
- `InvoiceFileProvider`: removed dependency on file-name generator; added `%sylius_invoicing.pdf_generator.enabled%`; now relies on `Invoice::path()`.
- `InvoiceCreator`: removed `InvoicePdfFileGeneratorInterface` and `InvoiceFileManagerInterface`; PDF is no longer generated on invoice creation—it's generated lazily on first download/provide.
- `InvoiceFileNameGeneratorInterface::generateForPdf()` now accepts `string $invoiceNumber` instead of `InvoiceInterface`.
- `InvoiceFileNameGeneratorInterface::generateForPdf()` can prefix filenames based on the configured sequence scope (`global` – default, `monthly`, `annually`).
- Sequence scope is configured via `sylius_invoicing.sequence.scope` semantic configuration, validated at container build time and injected as `InvoiceSequenceScopeEnum`.
- `InvoiceSequence`: `year`, `month` and `type` columns are `NOT NULL` (defaults: `0`, `0`, `global`) with a unique index on `(type, year, month)` guarding against duplicate sequences under concurrency.
- `InvoicePdfFileGenerator`: removed `InvoiceFileNameGeneratorInterface` from constructor; filename is taken from `Invoice::path()`; update DI to drop the generator argument.
14 changes: 14 additions & 0 deletions UPGRADE-2.0.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# UPGRADE FROM 2.0 TO 2.1

## 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.
Expand Down
137 changes: 137 additions & 0 deletions UPGRADE-3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# UPGRADE FROM 2.2 TO 3.0

## Changes

1. Persisted PDF path on `Invoice`:

- Added to `Invoice` new `path` field (unique) storing the final PDF location (e.g. annually/2025_10_000000001.pdf).

2. Filename generation moved from `InvoiceCreator` to `InvoiceFactory`.

`InvoiceCreator` no longer generates PDFs at creation time.

PDFs are generated on first provide/download via the provider.

```xml
<service id="sylius_invoicing.custom_factory.invoice" class="Sylius\InvoicingPlugin\Factory\InvoiceFactory">
<argument>%sylius_invoicing.model.invoice.class%</argument>
<argument type="service" id="sylius_invoicing.factory.shop_billing_data" />
+ <argument type="service" id="sylius_invoicing.generator.invoice_file_name" />
</service>
```

```xml
<service id="sylius_invoicing.creator.invoice" class="Sylius\InvoicingPlugin\Creator\InvoiceCreator">
<argument type="service" id="sylius_invoicing.repository.invoice" />
<argument type="service" id="sylius.repository.order" />
<argument type="service" id="sylius_invoicing.generator.invoice" />
- <argument type="service" id="sylius_invoicing.generator.invoice_pdf_file" />
- <argument type="service" id="sylius_invoicing.manager.invoice_file" />
- <argument>%sylius_invoicing.pdf_generator.enabled%</argument>
</service>
```

3. `InvoiceFactory` now depends on `InvoiceFileNameGeneratorInterface`.
```xml
<service id="sylius_invoicing.custom_factory.invoice" class="Sylius\InvoicingPlugin\Factory\InvoiceFactory">
<argument>%sylius_invoicing.model.invoice.class%</argument>
<argument type="service" id="sylius_invoicing.factory.shop_billing_data" />
+ <argument type="service" id="sylius_invoicing.generator.invoice_file_name" />
</service>
```

On creation, it calls:
```php
$fileName = $invoiceFileNameGenerator->generateForPdf($number);
```
and passes it to the `Invoice` constructor as `$path`.

4. `InvoiceFileProvider` is now the primary orchestrator of PDF generation

Removed `InvoiceFileNameGeneratorInterface` from `InvoiceFileProvider`.

Added `sylius_invoicing.pdf_generator.enabled` parameter to constructor.

```xml
<service id="sylius_invoicing.provider.invoice_file" class="Sylius\InvoicingPlugin\Provider\InvoiceFileProvider">
- <argument type="service" id="sylius_invoicing.generator.invoice_file_name" />
<argument type="service" id="gaufrette.sylius_invoicing_invoice_filesystem" />
<argument type="service" id="sylius_invoicing.generator.invoice_pdf_file" />
<argument type="service" id="sylius_invoicing.manager.invoice_file" />
<argument>%sylius_invoicing.invoice_save_path%</argument>
+ <argument>%sylius_invoicing.pdf_generator.enabled%</argument>
</service>
```

5. `InvoiceFileNameGenerator` signature & scoping

BC break: `generateForPdf()` now accepts string $invoiceNumber (not `InvoiceInterface`).

```php
// before:
public function generateForPdf(InvoiceInterface $invoice): string;

// after:
public function generateForPdf(string $invoiceNumber): string;
```

6. Invoice sequence scope is configured via semantic configuration (validated at container build time):

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

An invalid value fails at container compilation instead of silently falling back to `global`.
The `SYLIUS_INVOICING_SEQUENCE_SCOPE` environment variable and the `sylius_invoicing.sequence_scope`
parameter are no longer supported.

`SequentialInvoiceNumberGenerator` and `InvoiceFileNameGenerator` now receive the scope as an
`InvoiceSequenceScopeEnum` instance (default: `InvoiceSequenceScopeEnum::GLOBAL`) instead of a nullable string.

The generated file name is prefixed based on the configured scope:

>global (default): no prefix
>
>monthly: monthly/…
>
>annually: annually/…

7. `InvoiceSequence` scope columns are non-nullable:

`year`, `month` (default `0`) and `type` (default `global`) are `NOT NULL`, and sequences are guarded
by a unique index on `(type, year, month)` preventing duplicate sequences created by concurrent requests.
`InvoiceSequenceInterface` getters/setters use non-nullable types accordingly:

```php
// before:
public function getType(): ?InvoiceSequenceScopeEnum;
public function getYear(): ?int;
public function getMonth(): ?int;

// after:
public function getType(): InvoiceSequenceScopeEnum;
public function getYear(): int;
public function getMonth(): int;
```

8. `InvoicePdfFileGenerator` simplified:

- Removed dependency on InvoiceFileNameGeneratorInterface.

- Uses `Invoice::path()` as the filename:

```xml
<service id="sylius_invoicing.generator.invoice_pdf_file" class="Sylius\InvoicingPlugin\Generator\InvoicePdfFileGenerator">
<argument type="service" id="sylius_invoicing.generator.twig_to_pdf" />
<argument type="service" id="file_locator" />
- <argument type="service" id="sylius_invoicing.generator.invoice_file_name" />
<argument>@SyliusInvoicingPlugin/shared/download/pdf.html.twig</argument>
<argument>%sylius_invoicing.template.logo_file%</argument>
</service>
```

```php
$filename = $invoice->path();
```
1 change: 1 addition & 0 deletions config/doctrine/Invoice.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<field name="localeCode" column="locale_code" />
<field name="total" column="total" type="integer" />
<field name="paymentState" column="payment_state" />
<field name="path" column="path" unique="true" />

<one-to-one field="billingData" target-entity="Sylius\InvoicingPlugin\Entity\BillingDataInterface">
<cascade>
Expand Down
15 changes: 15 additions & 0 deletions config/doctrine/InvoiceSequence.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@
</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" enum-type="Sylius\InvoicingPlugin\Enum\InvoiceSequenceScopeEnum">
<options>
<option name="default">global</option>
</options>
</field>
</mapped-superclass>

</doctrine-mapping>
3 changes: 2 additions & 1 deletion config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<service id="sylius_invoicing.custom_factory.invoice" class="Sylius\InvoicingPlugin\Factory\InvoiceFactory">
<argument>%sylius_invoicing.model.invoice.class%</argument>
<argument type="service" id="sylius_invoicing.factory.shop_billing_data" />
<argument type="service" id="sylius_invoicing.generator.invoice_file_name" />
</service>
<service id="Sylius\InvoicingPlugin\Factory\InvoiceFactoryInterface" alias="sylius_invoicing.custom_factory.invoice" />

Expand All @@ -60,11 +61,11 @@
<service id="Sylius\InvoicingPlugin\Manager\InvoiceFileManagerInterface" alias="sylius_invoicing.manager.invoice_file" />

<service id="sylius_invoicing.provider.invoice_file" class="Sylius\InvoicingPlugin\Provider\InvoiceFileProvider">
<argument type="service" id="sylius_invoicing.generator.invoice_file_name" />
<argument type="service" id="gaufrette.sylius_invoicing_invoice_filesystem" />
<argument type="service" id="sylius_invoicing.generator.invoice_pdf_file" />
<argument type="service" id="sylius_invoicing.manager.invoice_file" />
<argument>%sylius_invoicing.invoice_save_path%</argument>
<argument>%sylius_invoicing.pdf_generator.enabled%</argument>
</service>
<service id="Sylius\InvoicingPlugin\Provider\InvoiceFileProviderInterface" alias="sylius_invoicing.provider.invoice_file" />

Expand Down
9 changes: 1 addition & 8 deletions config/services/generators.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,12 @@
</service>
<service id="Sylius\InvoicingPlugin\Generator\InvoiceGeneratorInterface" alias="sylius_invoicing.generator.invoice" />

<service
id="sylius_invoicing.generator.invoice_file_name"
class="Sylius\InvoicingPlugin\Generator\InvoiceFileNameGenerator"
/>
<service id="sylius_invoicing.generator.invoice_file_name" class="Sylius\InvoicingPlugin\Generator\InvoiceFileNameGenerator" />
<service id="Sylius\InvoicingPlugin\Generator\InvoiceFileNameGeneratorInterface" alias="sylius_invoicing.generator.invoice_file_name" />

<service id="sylius_invoicing.generator.invoice_pdf_file" class="Sylius\InvoicingPlugin\Generator\InvoicePdfFileGenerator">
<argument type="service" id="sylius_invoicing.generator.twig_to_pdf" />
<argument type="service" id="file_locator" />
<argument type="service" id="sylius_invoicing.generator.invoice_file_name" />
<argument>@SyliusInvoicingPlugin/shared/download/pdf.html.twig</argument>
<argument>%sylius_invoicing.template.logo_file%</argument>
</service>
Expand All @@ -57,9 +53,6 @@
<argument type="service" id="sylius_invoicing.repository.invoice" />
<argument type="service" id="sylius.repository.order" />
<argument type="service" id="sylius_invoicing.generator.invoice" />
<argument type="service" id="sylius_invoicing.generator.invoice_pdf_file" />
<argument type="service" id="sylius_invoicing.manager.invoice_file" />
<argument>%sylius_invoicing.pdf_generator.enabled%</argument>
</service>
<service id="Sylius\InvoicingPlugin\Creator\InvoiceCreatorInterface" alias="sylius_invoicing.creator.invoice" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ Feature: Saving invoices on server during generation
And channel "United States" has shop billing data set as "Ragnarok", "1100110011", "Pacific Coast Hwy", "90806" "Los Angeles", "United States"

@application @pdf_enabled
Scenario: Having invoice saved on the server after the order is placed
Scenario: Having invoice saved on the server once the order is paid
Given there is a customer "lucy@teamlucifer.com" that placed an order "#00000666"
When the customer bought 2 "Angel T-Shirt" products
And the customer "Lucifer Morningstar" addressed it to "Seaside Fwy", "90802" "Los Angeles" in the "United States"
And for the billing address of "Mazikeen Lilim" in the "Pacific Coast Hwy", "90806" "Los Angeles", "United States"
And the customer chose "UPS" shipping method with "Cash on Delivery" payment
And the order "#00000666" has just been paid
Then the invoice for order "#00000666" should be saved on the server
43 changes: 1 addition & 42 deletions src/Creator/InvoiceCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,20 @@

namespace Sylius\InvoicingPlugin\Creator;

use Doctrine\ORM\Exception\ORMException;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\InvoicingPlugin\Doctrine\ORM\InvoiceRepositoryInterface;
use Sylius\InvoicingPlugin\Entity\InvoiceInterface;
use Sylius\InvoicingPlugin\Exception\InvoiceAlreadyGenerated;
use Sylius\InvoicingPlugin\Generator\InvoiceGeneratorInterface;
use Sylius\InvoicingPlugin\Generator\InvoicePdfFileGeneratorInterface;
use Sylius\InvoicingPlugin\Manager\InvoiceFileManagerInterface;
use Sylius\PdfGenerationBundle\Core\Filesystem\Manager\PdfFileManagerInterface;
use Sylius\PdfGenerationBundle\Core\Model\PdfFile;

final class InvoiceCreator implements InvoiceCreatorInterface
{
public function __construct(
private readonly InvoiceRepositoryInterface $invoiceRepository,
private readonly OrderRepositoryInterface $orderRepository,
private readonly InvoiceGeneratorInterface $invoiceGenerator,
private readonly InvoicePdfFileGeneratorInterface $invoicePdfFileGenerator,
private readonly InvoiceFileManagerInterface|PdfFileManagerInterface $invoiceFileManager,
private readonly bool $hasEnabledPdfFileGenerator = true,
) {
if ($this->invoiceFileManager instanceof InvoiceFileManagerInterface) {
trigger_deprecation(
'sylius/invoicing-plugin',
'2.2',
'Passing an instance of %s to %s is deprecated and it will not be supported in 3.0, use an instance of %s instead.',
InvoiceFileManagerInterface::class,
self::class,
PdfFileManagerInterface::class,
);
}
}

public function __invoke(string $orderNumber, \DateTimeInterface $dateTime): void
Expand All @@ -61,29 +43,6 @@ public function __invoke(string $orderNumber, \DateTimeInterface $dateTime): voi

$invoice = $this->invoiceGenerator->generateForOrder($order, $dateTime);

if (!$this->hasEnabledPdfFileGenerator) {
$this->invoiceRepository->add($invoice);

return;
}

$invoicePdf = $this->invoicePdfFileGenerator->generate($invoice);

if ($this->invoiceFileManager instanceof PdfFileManagerInterface) {
$pdfFile = new PdfFile($invoicePdf->filename(), $invoicePdf->content());
$this->invoiceFileManager->save($pdfFile, 'sylius_invoicing');
} else {
$this->invoiceFileManager->save($invoicePdf);
}

try {
$this->invoiceRepository->add($invoice);
} catch (ORMException) {
if ($this->invoiceFileManager instanceof PdfFileManagerInterface) {
$this->invoiceFileManager->remove($invoicePdf->filename(), 'sylius_invoicing');
} else {
$this->invoiceFileManager->remove($invoicePdf);
}
}
$this->invoiceRepository->add($invoice);
}
}
23 changes: 23 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Sylius\InvoicingPlugin\Entity\LineItemInterface;
use Sylius\InvoicingPlugin\Entity\TaxItem;
use Sylius\InvoicingPlugin\Entity\TaxItemInterface;
use Sylius\InvoicingPlugin\Enum\InvoiceSequenceScopeEnum;
use Sylius\InvoicingPlugin\Factory\BillingDataFactory;
use Sylius\InvoicingPlugin\Factory\InvoiceShopBillingDataFactory;
use Sylius\InvoicingPlugin\Factory\LineItemFactory;
Expand All @@ -45,10 +46,32 @@ public function getConfigTreeBuilder(): TreeBuilder

$this->addResourcesSection($rootNode);
$this->addPdfGeneratorSection($rootNode);
$this->addSequenceSection($rootNode);

return $treeBuilder;
}

private function addSequenceSection(ArrayNodeDefinition $node): void
{
$node
->children()
->arrayNode('sequence')
->addDefaultsIfNotSet()
->children()
->enumNode('scope')
->info('Scope of invoice number sequences: "global", "monthly" or "annually".')
->values(array_map(
static fn (InvoiceSequenceScopeEnum $scope): string => $scope->value,
InvoiceSequenceScopeEnum::cases(),
))
->defaultValue(InvoiceSequenceScopeEnum::GLOBAL->value)
->end()
->end()
->end()
->end()
;
}

private function addResourcesSection(ArrayNodeDefinition $node): void
{
$node
Expand Down
Loading
Loading