From 7f5bcc947829f2bc378982266bd004031a8def18 Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski Date: Tue, 27 Oct 2026 18:56:31 +0100 Subject: [PATCH] Let modules draw QR codes through the host Module API 1.6.0: QrCodeGenerator returns an SVG, or the same as a data URI for an in a PDF template, so a module never bundles a QR library of its own. ErrorCorrection names the four levels, Medium by default as the EPC and Swiss QR-bill specifications require. Text beyond ASCII is encoded as UTF-8. --- README.md | 25 ++++++++++-- RELEASING.md | 2 +- src/Contracts/Host/QrCodeGenerator.php | 54 ++++++++++++++++++++++++++ src/Qr/ErrorCorrection.php | 27 +++++++++++++ tests/QrContractsTest.php | 52 +++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 src/Contracts/Host/QrCodeGenerator.php create mode 100644 src/Qr/ErrorCorrection.php create mode 100644 tests/QrContractsTest.php diff --git a/README.md b/README.md index ed01e21..f7ecb45 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The MIT-licensed SDK for official [InvoiceShelf](https://invoiceshelf.com) v3 modules. It extends [`nwidart/laravel-modules`](https://github.com/nWidart/laravel-modules) with the contracts used by InvoiceShelf's module host and signed marketplace packages. -SDK **3.5** supports Module API **1.4**. First-party module packages live in their own repositories and are licensed `AGPL-3.0-only`; this SDK remains MIT. +SDK **3.7** supports Module API **1.6**. First-party module packages live in their own repositories and are licensed `AGPL-3.0-only`; this SDK remains MIT. ## What a module can add @@ -12,6 +12,7 @@ SDK **3.5** supports Module API **1.4**. First-party module packages live in the - Actions, panels, form sections and save hooks on host records (invoices, estimates, customers, payments and more), plus entries in the global New menu. - Company abilities contributed to the host's authorization catalogue, so a module can ship its own permissions instead of reusing host ones. - Whole invoices to read and structured XML to embed in invoice PDFs (Module API 1.5.0), for modules that produce e-invoices; see [Documents and invoice PDFs](#documents-and-invoice-pdfs). +- QR codes drawn by the host (Module API 1.6.0), so a module needs no QR library of its own; see [QR codes](#qr-codes). - Narrow host contracts under [`src/Contracts/Host`](src/Contracts/Host) for settings, authorization, and read-only company-data queries. `CompanyDataReader` covers the AI assistant's built-in queries plus `companyMembers()` and `existingInvoiceIds()` for modules that assign work to members or stamp their entries against invoices. Module code must not depend on InvoiceShelf Eloquent models. - AI drivers through [`src/Ai`](src/Ai): extend `AiDriver`, return `AiChatResponse`, and throw `AiException` for safe, localizable provider failures. @@ -23,8 +24,8 @@ Declare compatibility in `module.json`, then test it against the host versions y | Concern | Current contract | | --- | --- | -| SDK | `invoiceshelf/modules` `^3.6` | -| Module API | `^1.3.0`; `^1.4.0` for bundled dependencies and record slots; `^1.5.0` for documents and invoice PDF attachments | +| SDK | `invoiceshelf/modules` `^3.7` | +| Module API | `^1.3.0`; `^1.4.0` for bundled dependencies and record slots; `^1.5.0` for documents and invoice PDF attachments; `^1.6.0` for QR codes | | Host application | Your explicit InvoiceShelf 3.x range | | PHP | The range your module actually supports | @@ -253,6 +254,24 @@ A module that turns invoices into other documents, such as an EN 16931 e-invoice These are Module API **1.5.0** additions; declare `"module_api": "^1.5.0"` to use them. +### QR codes + +Ask the container for `QrCodeGenerator` instead of bundling a QR library: + +```php +use InvoiceShelf\Modules\Contracts\Host\QrCodeGenerator; + +$src = app(QrCodeGenerator::class)->svgDataUri($payload, size: 160); +// +``` + +- **`svg()`** returns an SVG document and **`svgDataUri()`** the same as a `data:` URI for an `` in a PDF template or a Blade view. Both PDF drivers draw it without fetching anything, and no image extension is needed on the server. +- **Content** is UTF-8 text. Plain ASCII is encoded as it is; anything else is encoded as UTF-8 with an ECI marker, which is what EPC (SEPA credit transfer) and Swiss QR-bill payloads expect. +- **Options:** `$size` is the edge in pixels with the quiet zone included, `$margin` the quiet zone in modules (4 by default, as the QR standard asks), and `ErrorCorrection` ([`src/Qr`](src/Qr)) runs from `Low` to `High`, `Medium` by default, which the payment specifications above require. +- Content that is empty or too long for a QR code at the chosen level throws `InvalidArgumentException`. + +These are Module API **1.6.0** additions; declare `"module_api": "^1.6.0"` to use them. + ### Responsive classes Your stylesheet is a Tailwind build of its own, loaded next to the host's, and the two share class names: both may define `.hidden` or `.w-full`. Between rules of equal weight the later sheet wins, so the host adds its stylesheet again after the module sheets, and a module can never break the app's layout. The cost lands on your markup: the host's plain utilities win over your breakpoint variants. `hidden sm:flex` stays hidden on desktop unless the host's own build happens to contain `sm:flex` too, and that changes from release to release. diff --git a/RELEASING.md b/RELEASING.md index 680a626..f9a46b3 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -18,7 +18,7 @@ An official module normally calls the tagged SDK workflow from its own tag workf ```yaml jobs: release: - uses: InvoiceShelf/modules/.github/workflows/module-release.yml@3.6.0 + uses: InvoiceShelf/modules/.github/workflows/module-release.yml@3.7.0 with: channel: stable secrets: inherit diff --git a/src/Contracts/Host/QrCodeGenerator.php b/src/Contracts/Host/QrCodeGenerator.php new file mode 100644 index 0000000..b1ee435 --- /dev/null +++ b/src/Contracts/Host/QrCodeGenerator.php @@ -0,0 +1,54 @@ +` in a PDF template: the renderer never fetches anything. + * + * @param int $size the edge of the square image in pixels, quiet zone included + * @param int $margin the quiet zone around the code, in modules; 4 is what the QR standard asks for + * + * @throws InvalidArgumentException when the content is empty or too long for a QR code at that level + */ + public function svgDataUri( + string $content, + int $size = 200, + ErrorCorrection $errorCorrection = ErrorCorrection::Medium, + int $margin = 4, + ): string; +} diff --git a/src/Qr/ErrorCorrection.php b/src/Qr/ErrorCorrection.php new file mode 100644 index 0000000..e87245e --- /dev/null +++ b/src/Qr/ErrorCorrection.php @@ -0,0 +1,27 @@ + $method->getName(), + (new ReflectionClass(QrCodeGenerator::class))->getMethods(), + ); + sort($methods); + + $this->assertSame(['svg', 'svgDataUri'], $methods); + + foreach ($methods as $name) { + $method = new ReflectionMethod(QrCodeGenerator::class, $name); + + $this->assertSame('string', (string) $method->getReturnType()); + $this->assertSame( + ['content' => 'string', 'size' => 'int', 'errorCorrection' => ErrorCorrection::class, 'margin' => 'int'], + array_combine( + array_map(static fn ($parameter): string => $parameter->getName(), $method->getParameters()), + array_map(static fn ($parameter): string => (string) $parameter->getType(), $method->getParameters()), + ), + ); + $this->assertSame([200, ErrorCorrection::Medium, 4], array_map( + static fn ($parameter) => $parameter->getDefaultValue(), + array_slice($method->getParameters(), 1), + )); + } + } + + public function test_error_correction_levels_carry_the_standard_letters(): void + { + $this->assertSame( + ['Low' => 'L', 'Medium' => 'M', 'Quartile' => 'Q', 'High' => 'H'], + array_combine( + array_map(static fn (ErrorCorrection $level): string => $level->name, ErrorCorrection::cases()), + array_map(static fn (ErrorCorrection $level): string => $level->value, ErrorCorrection::cases()), + ), + ); + } +}