Skip to content
Merged
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
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand All @@ -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 |

Expand Down Expand Up @@ -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);
// <img src="{{ $src }}" alt="" style="width: 40mm; height: 40mm;">
```

- **`svg()`** returns an SVG document and **`svgDataUri()`** the same as a `data:` URI for an `<img>` 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.
Expand Down
2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions src/Contracts/Host/QrCodeGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace InvoiceShelf\Modules\Contracts\Host;

use InvalidArgumentException;
use InvoiceShelf\Modules\Qr\ErrorCorrection;

/**
* QR codes drawn by the host, so a module does not bundle a QR library of its
* own. Module API 1.6.0.
*
* Content is UTF-8 text: a URL, or a payment payload such as an EPC or Swiss
* QR-bill one. Plain ASCII is encoded byte for byte; anything else is encoded
* as UTF-8 with an ECI marker saying so, which is what those payment
* specifications expect, rather than being converted to ISO-8859-1.
*
* The output is SVG, which needs no image extension on the server, stays
* sharp at any print size, and is drawn by both of the host's PDF drivers.
*/
interface QrCodeGenerator
{
/**
* An SVG document of the code.
*
* @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 svg(
string $content,
int $size = 200,
ErrorCorrection $errorCorrection = ErrorCorrection::Medium,
int $margin = 4,
): string;

/**
* The same SVG as a `data:image/svg+xml;base64,` URI, ready for the `src`
* of an `<img>` 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;
}
27 changes: 27 additions & 0 deletions src/Qr/ErrorCorrection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace InvoiceShelf\Modules\Qr;

/**
* How much of a QR code can be damaged and still scan, at the price of a
* denser code. Module API 1.6.0.
*
* Medium is right for most printed documents, and it is what the EPC
* (SEPA credit transfer) and Swiss QR-bill specifications require.
*/
enum ErrorCorrection: string
{
/** About 7% of the code can be restored. */
case Low = 'L';

/** About 15%. */
case Medium = 'M';

/** About 25%. */
case Quartile = 'Q';

/** About 30%. */
case High = 'H';
}
52 changes: 52 additions & 0 deletions tests/QrContractsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace InvoiceShelf\Modules\Tests;

use InvoiceShelf\Modules\Contracts\Host\QrCodeGenerator;
use InvoiceShelf\Modules\Qr\ErrorCorrection;
use ReflectionClass;
use ReflectionMethod;

class QrContractsTest extends TestCase
{
public function test_the_host_contract_draws_svg_and_svg_data_uris_only(): void
{
$methods = array_map(
static fn (ReflectionMethod $method): string => $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()),
),
);
}
}
Loading