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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ jobs:
- name: Generated code up to date
run: composer generate:check

- name: JSON examples in the docs up to date
run: composer docs:json:check

- name: Composer audit
run: composer audit

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ name: Docs

on:
pull_request:
paths: ['docs/**', '.github/workflows/docs.yml']
paths: ['docs/**', 'tests/Fixtures/Contract/**', '.github/workflows/docs.yml']
push:
branches: [4.x]
paths: ['docs/**', '.github/workflows/docs.yml']
paths: ['docs/**', 'tests/Fixtures/Contract/**', '.github/workflows/docs.yml']
workflow_dispatch:

permissions:
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ composer mutate # Infection mutation testing (~3 min, all of src
composer audit # Security audit of dependencies
composer generate # Regenerate models/options from docs/spec/billomat.json + tools/codegen/definitions/
composer generate:check # Fail if generated files under src/ are out of date
composer ci # Full local CI: lint + analyse + refactor:dry + generate:check + test:all
composer docs:json # Regenerate the "JSON on the wire" sections of docs/resources/ (requests via the SDK, responses from tests/Fixtures/Contract)
composer docs:json:check # Fail if those JSON examples are out of date
composer ci # Full local CI: lint + analyse + refactor:dry + generate:check + docs:json:check + test:all
```

Single test file/method:
Expand All @@ -44,7 +46,7 @@ CI runs via `.github/workflows/ci.yml` on PHP 8.5 (required) and 8.6 (experiment

### Documentation website

`docs/` is also a VitePress site (`docs/.vitepress/config.mts`, `docs/package.json`, `cd docs && npm ci && npm run build`), deployed by `.github/workflows/docs.yml` to https://billomat-php.justpilot.io (`docs/public/CNAME`). The sidebar is built from the first `# ` heading of each page; `docs/spec/`, `docs/audit.md` and `docs/roadmap-v4.md` are excluded. Relative links to files outside `docs/` are rewritten to GitHub. Dead links fail the build.
`docs/` is also a VitePress site (`docs/.vitepress/config.mts`, `docs/package.json`, `cd docs && npm ci && npm run build`), deployed by `.github/workflows/docs.yml` to https://billomat-php.justpilot.io (`docs/public/CNAME`). The sidebar is built from the first `# ` heading of each page; `docs/spec/`, `docs/audit.md` and `docs/roadmap-v4.md` are excluded. Relative links to files outside `docs/` are rewritten to GitHub. Dead links fail the build. The "JSON on the wire" sections between the `json-examples` markers are generated by `tools/generate-json-examples.php` (request bodies captured from the SDK into `docs/snippets/json/`, responses imported from the contract fixtures); never edit them by hand, add a request or page in the tool and run `composer docs:json`.

## Architecture

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
- **Missing actions:** `mail()` (Pixelletter) on offers, confirmations, credit notes, delivery notes, letters and reminders; `recurrings->stop()`; `recurrings->emailReceivers->update()` with `RecurringEmailReceiverUpdateOptions`; `contacts->avatar()` and `users->avatar()`.
- **`listPage()` and `iterateAll()`** on `contacts` (with the client ID as the first argument), `taxes`, `templates` and `reminderTexts`. `countries->list()` and `currencies->list()` accept filters, e.g. `list(['code' => 'DE'])`.
- **Backward compatibility promise:** [docs/concepts/backward-compatibility.md](docs/concepts/backward-compatibility.md) says what is covered from 4.0.0-beta.1 on. `AbstractApi`, `AbstractSubResourceApi`, the sub-resource API constructors and the `FromApi` trait are marked `@internal`.
- **JSON examples in the docs:** the resource pages have a "JSON on the wire" section with the request body the SDK sends (captured from the SDK, kept up to date by `composer docs:json:check` in CI) and real, anonymized sandbox responses from the contract fixtures. Billomat's own API docs only show XML.
- `Page` is covariant in its item type, so a `Page<Invoice>` can be passed where a `Page<object>` is expected.

### Changed
Expand Down
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,15 @@
"@analyse",
"@refactor:dry",
"@generate:check",
"@docs:json:check",
"@test:all"
],
"extract:spec": "php tools/extract-billomat-spec.php",
"audit:spec": "php tools/audit-spec-vs-code.php",
"generate": "php tools/generate.php",
"generate:check": "php tools/generate.php --check",
"docs:json": "php tools/generate-json-examples.php",
"docs:json:check": "php tools/generate-json-examples.php --check",
"fixtures:record": [
"Composer\\Config::disableProcessTimeout",
"php tools/record-fixtures.php"
Expand All @@ -143,6 +146,8 @@
"audit:spec": "Vergleicht docs/spec/billomat.json gegen src/ und schreibt docs/audit.md",
"generate": "Erzeugt Models und Options aus docs/spec/billomat.json + tools/codegen/definitions/",
"generate:check": "Prüft, ob die generierten Dateien unter src/ aktuell sind",
"docs:json": "Schreibt die JSON-Beispiele der Ressourcen-Doku (docs/snippets/json/ und die Abschnitte „JSON on the wire“)",
"docs:json:check": "Prüft, ob die JSON-Beispiele der Doku aktuell sind",
"fixtures:record": "Nimmt anonymisierte Contract-Fixtures per GET gegen die Billomat-Sandbox auf (benötigt .env.test.local)"
}
}
10 changes: 10 additions & 0 deletions docs/resources/activities.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ foreach ($billomat->activities->iterateAll() as $activity) {

Helper `isSystemActivity(): bool` returns `true` when `userId` is `null`.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/../tests/Fixtures/Contract/activity-feed/list.json [GET /activity-feed (response)]
:::
<!-- json-examples:end -->

## Pitfalls

- **`user_id` can be an empty string.** For system activities Billomat sends `<user_id></user_id>`, which is `""` in JSON. `Activity::fromArray()` normalizes it to `null` via `ScalarCaster::toIntOrNull()`.
Expand Down
10 changes: 10 additions & 0 deletions docs/resources/articles.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,16 @@ Both values are required and set via the constructor.
- [`NetGross`](../../src/Model/Enum/NetGross.php): `NET`, `GROSS`, `SETTINGS` (`purchasePriceNetGross`).
- [`PropertyType`](../../src/Model/Enum/PropertyType.php): `TEXTFIELD`, `TEXTAREA`, `CHECKBOX`, on the property definition ([`ArticleProperty`](../../src/Model/ArticleProperty.php)) and on `ArticlePropertyValue::$type`.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/snippets/json/articles.create.json [POST /articles]
:::
<!-- json-examples:end -->

## Pitfalls

- **No `update()` for tags and property values.** Delete tags and create them again; for property values, another POST with the same `article_property_id` overwrites the existing entry.
Expand Down
10 changes: 10 additions & 0 deletions docs/resources/client-tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ Constructor: `new ClientTagCreateOptions(string $name)`. The object is `final re

None. `name` is free text.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/snippets/json/client-tags.create.json [POST /client-tags]
:::
<!-- json-examples:end -->

## Pitfalls

- **Two views, two models.** `list()` returns `ClientTag`, `cloud()` returns `ClientTagCloudEntry`. Mixing them up leads to "why doesn't this thing have a `clientId`?" moments.
Expand Down
12 changes: 12 additions & 0 deletions docs/resources/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,18 @@ The client ID isn't part of the options: pass it as the first argument of `creat
- [`PaymentType`](../../src/Model/Enum/PaymentType.php): `INVOICE_CORRECTION`, `CREDIT_NOTE`, `BANK_CARD`, `BANK_TRANSFER`, `DEBIT`, `CASH`, `CHECK`, `PAYPAL`, `CREDIT_CARD`, `COUPON`, `MISC` (`defaultPaymentTypes`)
- [`PropertyType`](../../src/Model/Enum/PropertyType.php): `TEXTFIELD`, `TEXTAREA`, `CHECKBOX` (`ClientPropertyValue::$type`)

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/snippets/json/clients.create.json [POST /clients]
<<< @/../tests/Fixtures/Contract/clients/get.json [GET /clients/{id} (response)]
<<< @/../tests/Fixtures/Contract/clients/list.json [GET /clients (response)]
:::
<!-- json-examples:end -->

## Pitfalls

- **E-invoicing (undocumented).** Billomat returns `facturx_identifier` and `seller_identifier` in API responses but doesn't describe them in the API docs. The SDK only reads them (types derived from observed values); writing isn't supported for lack of a specification.
Expand Down
12 changes: 12 additions & 0 deletions docs/resources/confirmations.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,18 @@ Shared class for sending every document type by email (invoices, offers, confirm
- [`PdfType`](../../src/Model/Enum/PdfType.php): `SIGNED` (`signed`), `PRINT` (`print`); the wire values are lowercase.
- [`NetGross`](../../src/Model/Enum/NetGross.php): `NET`, `GROSS`, `SETTINGS`.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/snippets/json/confirmations.create.json [POST /confirmations]
<<< @/../tests/Fixtures/Contract/confirmations/get.json [GET /confirmations/{id} (response)]
<<< @/../tests/Fixtures/Contract/confirmations/list.json [GET /confirmations (response)]
:::
<!-- json-examples:end -->

## Pitfalls

- **Simpler lifecycle than offers.** There is no `win()`/`lose()`: a confirmation is either marked as done (`clear()`, reverted by `unclear()`) or canceled (`cancel()`, reverted by `uncancel()`). Calling `win()` out of habit means looking for the wrong endpoint.
Expand Down
10 changes: 10 additions & 0 deletions docs/resources/contacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ Same fields as `ContactCreateOptions`, **without** `clientId`: Billomat doesn't

None. All fields are strings, including `salutation` and `countryCode`. Billomat expects the latter as an ISO 3166-1 alpha-2 code (validated server-side).

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/snippets/json/contacts.create.json [POST /contacts]
:::
<!-- json-examples:end -->

## Pitfalls

- **`list()` requires a filter.** Billomat's `GET /contacts` without `client_id` returns an error, not a global list. The SDK makes the client ID a required argument.
Expand Down
10 changes: 10 additions & 0 deletions docs/resources/countries.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,13 @@ Lazy iteration over all pages.
| `extra` | `array<string,mixed>` | Fields the SDK doesn't map yet, raw (see [Read models](../concepts/models.md)) |

`Country::fromArray($data)` hydrates from the wire format; `Country::toArray()` writes snake_case back.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/../tests/Fixtures/Contract/countries/list.json [GET /countries (response)]
:::
<!-- json-examples:end -->
10 changes: 10 additions & 0 deletions docs/resources/credit-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,16 @@ Empty recipient arrays are left out of the payload so Billomat falls back to the
- [`PaymentType`](../../src/Model/Enum/PaymentType.php): shared payment types (`BANK_TRANSFER`, `PAYPAL`, `CASH`, …).
- [`PdfType`](../../src/Model/Enum/PdfType.php): `pdf()` accepts the same PDF variants as for invoices.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/snippets/json/credit-notes.create.json [POST /credit-notes]
:::
<!-- json-examples:end -->

## Pitfalls

- **`delete()` isn't limited to `DRAFT`.** Billomat's API deletes credit notes in any status. Once completed, prefer `cancel()`: the document is numbered, and deleting it leaves a gap in the number sequence (GoBD).
Expand Down
10 changes: 10 additions & 0 deletions docs/resources/currencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,13 @@ Lazy iteration over all pages.
| `extra` | `array<string,mixed>` | Fields the SDK doesn't map yet, raw (see [Read models](../concepts/models.md)) |

`Currency::fromArray($data)` hydrates from the wire format; `Currency::toArray()` writes snake_case back.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/../tests/Fixtures/Contract/currencies/list.json [GET /currencies (response)]
:::
<!-- json-examples:end -->
12 changes: 12 additions & 0 deletions docs/resources/delivery-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,18 @@ Shared class for sending every document type by email (invoices, offers, confirm
- [`PdfType`](../../src/Model/Enum/PdfType.php): `SIGNED` (`signed`), `PRINT` (`print`); the wire values are lowercase.
- [`NetGross`](../../src/Model/Enum/NetGross.php): `NET`, `GROSS`, `SETTINGS`.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/snippets/json/delivery-notes.create.json [POST /delivery-notes]
<<< @/../tests/Fixtures/Contract/delivery-notes/get.json [GET /delivery-notes/{id} (response)]
<<< @/../tests/Fixtures/Contract/delivery-notes/list.json [GET /delivery-notes (response)]
:::
<!-- json-examples:end -->

## Pitfalls

- **A delivery note carries little monetary data.** Billomat returns no total or price fields in the read model, and the document creates no receivable. If you expect a payment, you want `$billomat->invoices` or `$billomat->incomings`.
Expand Down
10 changes: 10 additions & 0 deletions docs/resources/inbox-documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ Plus the helper `getBinary(): string`, which decodes `base64file` and returns th

None. The resource has no status enum.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/snippets/json/inbox-documents.create.json [POST /inbox-documents]
:::
<!-- json-examples:end -->

## Pitfalls

- **E-invoicing (undocumented).** Billomat returns `xrechnung_extractions_id` (`InboxDocument::$xrechnungExtractionsId`) in API responses but doesn't describe it in the API docs. The SDK only reads it (type derived from observed values); writing isn't supported for lack of a specification.
Expand Down
11 changes: 11 additions & 0 deletions docs/resources/incoming-categories.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ Note: the ID is a **string** slug (e.g. `goods`, `services`), not a numeric key.
| `description` | `description` | `?string` |
| `extra` | — | `array<string, mixed>` (unmapped fields, see [Models](../concepts/models.md)) |

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/../tests/Fixtures/Contract/incoming-categories/get.json [GET /incoming-categories/{id} (response)]
<<< @/../tests/Fixtures/Contract/incoming-categories/list.json [GET /incoming-categories (response)]
:::
<!-- json-examples:end -->

## Pitfalls

- **String ID.** Unlike most Billomat resources, categories are identified by a slug (`goods`, `services`, …) instead of an integer.
Expand Down
10 changes: 10 additions & 0 deletions docs/resources/incomings.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,16 @@ Constructor: `new IncomingPropertyValueCreateOptions(int $incomingPropertyId, mi
- [`PaymentType`](../../src/Model/Enum/PaymentType.php): enum shared with invoices (`BANK_TRANSFER`, `CASH`, `PAYPAL`, …).
- [`PropertyType`](../../src/Model/Enum/PropertyType.php): data type of the property definition (`IncomingProperty`) and of `IncomingPropertyValue::$type`.

<!-- json-examples:start (generated by tools/generate-json-examples.php, run `composer docs:json`) -->
## JSON on the wire

Billomat's API documentation only shows XML. These are the JSON bodies the SDK sends and receives: requests as the SDK serializes them, responses recorded in the Billomat sandbox and anonymized ([contract fixtures](../../tests/Fixtures/Contract)). Billomat sends every value as a string; the read models cast them (see [Read models](../concepts/models.md)).

::: code-group
<<< @/snippets/json/incomings.create.json [POST /incomings]
:::
<!-- json-examples:end -->

## Pitfalls

- **`upload()` doesn't encode.** The string must already be Base64-encoded; passing `file_get_contents()` directly results in broken PDFs on the server. Always use `base64_encode(file_get_contents(...))`. The same applies to the `base64file` option.
Expand Down
Loading
Loading