Edu-V · EduSaaS API 0.7.0
- Environment
staging- Adapters
mock- Server
https://acc-eduv.boom.nl/api/edusaas/v1- Contract
- /openapi/edusaas-openapi.yaml (verbatim, sha256
d2c57d8273ec219713f40765a2b3cd503bff10e5cc0e6e4206e6d95a7556de7f) - This deployment
- /openapi.yaml · /openapi.json (the contract plus what Edu-V adds)
Data source markers
Every API response says where its data came from. Mock data is fictitious and never reaches a Boom system.
| Marker | Where | Values |
|---|---|---|
X-Edu-V-Data-Source | every API response | mock, mixed, live; none when the answer came before any adapter or registry was consulted (401, 403, 404/405 of unknown routes, 429, 400 decode errors, 422, 37) |
X-Edu-V-Adapter | every response of this host, including this page | the merge of all fulfilment ports: currently mock |
X-Edu-V-Environment | every response | currently staging |
Link: </docs#data-source>; rel="describedby" | mock and mixed responses | points here |
_dataSource | top level of adapter- and registry-backed bodies, functional 400/404 bodies, mock-caused 500 bodies and each non-live activation event | mock or mixed; absent for live |
| Identifiers | mock adapter output | MOCK-SO-…, MOCK-AC-…, MOCK-DLV-…, UUIDs starting with 4d4f434b- |
Authentication
oauth2M2M: OAuth2 client credentials; send the access token as Authorization: Bearer …. Token endpoint: https://acc-account.boom.nl/auth/realms/master/protocol/openid-connect/token.
Missing or invalid tokens answer 401, a token without the operation's scope 403 (RFC 6750, AUTH-001).
basicAuth: uitgeschakeld / disabled (AUTH-002). The contract still lists it; this deployment accepts bearer tokens only.
Deviations from EduSaaS 0.7.0
| Id | Kind | What | Setting | Asked upstream |
|---|---|---|---|---|
AUTH-001 |
added status | 401 and 403 on all operations with a RestErrorResponse body and an RFC 6750 WWW-Authenticate challenge. | Document 401 and 403. | |
AUTH-002 |
subset | The spec's basicAuth alternative is disabled by default; only oauth2M2M bearer tokens are accepted. | auth.basic.enabled |
Sign off on basicAuth being disabled. |
MARK-001 |
added header | X-Edu-V-Data-Source: mock | mixed | live | none on every response. | Accept the header. | |
MARK-002 |
added header | X-Edu-V-Adapter, X-Edu-V-Environment, Link rel=describedby, X-Request-Id and Cache-Control no-store on every response; X-Edu-V-Auth dev-bypass in development and test only. | Accept the headers. | |
MARK-003 |
added property | Top-level _dataSource on adapter- and registry-backed bodies and on mock-caused 500 RestErrorResponse bodies, and on non-live ActivationEvent items. | markers.body |
Accept unknown properties. |
TOL-001 |
tolerance | An explicit null on an optional property is treated as absent. | contract.null_as_absent |
|
STR-001 |
stricter | orderLines has at least one item (shadow by default). | validation.rules.STR-001 |
Add minItems: 1. |
STR-002 |
stricter | OrderLine.quantity and CreditOrderLine.quantity are at least 1 (shadow by default). | validation.rules.STR-002 |
Add minimum: 1. |
STR-003 |
stricter | An explicit CreditOrderRequest.orderLines is not empty (shadow by default). | validation.rules.STR-003 |
Add minItems: 1. |
STR-004 |
stricter | Required identifiers are not blank (shadow by default). | validation.rules.STR-004 |
Add minLength: 1. |
STR-005 |
stricter | No duplicate purchaseOrderLineId or salesOrderLineId within one order or credit request, no duplicate userIds (shadow by default). | validation.rules.STR-005 |
Add uniqueItems. |
STR-006 |
stricter | Strings at most 255 characters, arrays at most 1000 items (shadow by default; review against real ECK-ID lengths before enforcing). | validation.rules.STR-006 |
Add maxLength and maxItems. |
INT-001 |
stricter | A request integer must be written as a JSON integer: 2.0 or 1e2 answers 422 "The value must be an integer.", although JSON Schema 2020-12 counts a number with a zero fraction as an integer. | Confirm that integral floats are not integers. | |
INT-002 |
stricter | Request integers (quantity, totalQuantity, newQuantity, houseNumber) are between -2147483648 and 2147483647; outside that range they answer 422 "The value must be at most 2147483647." or "The value must be at least -2147483648.". Always enforced: the registry stores 32-bit quantities, so the request is refused before any adapter call. | Add format: int32 (or a maximum) to the integer properties. | |
HTTP-001 |
added status | 404 and 405 for paths and methods outside the spec, with a RestErrorResponse body and X-Edu-V-Data-Source none; a 405 carries Allow (RFC 9110). | ||
HTTP-002 |
mapping | A body over 1 MiB and a wrong Content-Type answer 400 RestErrorResponse (no 413 or 415). | ||
HTTP-003 |
added status | 429 RestErrorResponse with Retry-After from the rate limiter. | Acknowledge rate limiting. | |
CODE-001 |
provisional | correctOrder on an unknown order answers status 50 (base ErrorResponse). | contract.credit_not_found_code |
Define a code. |
CODE-002 |
interpretation | correctOrder uses 12, 22 and 37 with their sibling-family meanings. | Add an enum. | |
CODE-003 |
interpretation | correctOrganisationLicenseSpecification on a cancelled specification answers 23. | Define the code. | |
CODE-004 |
interpretation | correctOrder with a salesOrderLineId or purchaseOrderLineId that matches no line, or two that disagree, answers 12 "Order line not found in the order.". | Define a code. | |
SEM-001 |
interpretation | activations: startDate filters registration time, offset is inclusive, nextOffset is always present, page size 100, wait capped at 30 s, filters combine with AND. | activations.* |
Confirm. |
SEM-002 |
interpretation | The idempotency namespace is global across operations; functional 400 and 404 answers are replayed. | Confirm. | |
SEM-003 |
interpretation | No-ops answer 200 without an adapter call (cancel already cancelled, block already blocked, unblock not blocked, unblock cancelled with a warning, ILF correct with the same newQuantity). | contract.noop_success |
Confirm. |
SEM-004 |
interpretation | activations: a negative offset or wait answers 400 (the spec has no minimum); a repeated query parameter answers 400; unknown query parameters are ignored. | Add minimum: 0. | |
SEM-005 |
interpretation | An in-flight duplicate or business-key contention not finished after 15 s answers 500 "Request is still being processed; retry later.", also for a credit whose original order is still processing. | idempotency.wait_secs |
Confirm the retry behaviour. |
SEM-006 |
interpretation | 422 and 500 answers are not stored, so their requestReferenceId may come back with a different payload and is processed normally; after idempotency pruning a registry key answers 37 even with an identical payload. | Confirm. | |
SEM-007 |
interpretation | "Exact same payload" means RFC 8785 canonical JSON after null stripping and UUID lower-casing ({"x":null} equals an absent x). | contract.null_as_absent |
Confirm. |
SEM-008 |
interpretation | A path parameter that is not a UUID answers 400 RestErrorResponse, not 422. | Confirm. | |
SEM-009 |
interpretation | A specification of the other kind on a path (an ILF id on /user-licenses/..., an ELF id on /organisation-licenses/...) is not found (50, or 404 with 62 for block). | Confirm. | |
SPEC-DEF-001 |
spec defect | oneOf [<family>ErrorResponse, RestErrorResponse] in the five *BadRequest responses is unsatisfiable for functional bodies; tests validate against contract/test-overlay.yaml (anyOf). | Change to anyOf or require message in RestErrorResponse. |
Boom-strict rules
shadow records a hit and processes the request; enforce answers 422 and its constraint is part of /openapi.yaml.
| Rule | Mode | What |
|---|---|---|
STR-001 | shadow | orderLines has at least one item (shadow by default). |
STR-002 | shadow | OrderLine.quantity and CreditOrderLine.quantity are at least 1 (shadow by default). |
STR-003 | shadow | An explicit CreditOrderRequest.orderLines is not empty (shadow by default). |
STR-004 | shadow | Required identifiers are not blank (shadow by default). |
STR-005 | shadow | No duplicate purchaseOrderLineId or salesOrderLineId within one order or credit request, no duplicate userIds (shadow by default). |
STR-006 | shadow | Strings at most 255 characters, arrays at most 1000 items (shadow by default; review against real ECK-ID lengths before enforcing). |
Mock-scenario's
Requests that make the mock answer a specific way, so every documented answer can be produced. core rules behave the same with a live adapter.
| Trigger | Operations | Result | Owner |
|---|---|---|---|
| `productId` starting with `UNKNOWN-` (or not in `mock.products` when `mock.strict_catalog=true`) | createOrder, create ELF/ILF | 400 status 12 | mock |
| `organisationId` `000000` | create ELF/ILF | 400 status 6 | mock |
| same `purchaseOrderId` + `purchaseOrderLineId` as an earlier order | createOrder | 400 status 16 | core |
| credit above remaining quantity, or second full credit | correctOrder | 400 status 22 | core |
| credit `productId` not in the order; unknown line id | correctOrder | 400 status 12 | core |
| credit on unknown order | correctOrder | 400 status 50 (CODE-001) | core |
| ILF `newQuantity` below the mock activated count (`min(totalQuantity, 3)`) | correct ILF | 400 status 22 | mock |
| stored contractId `MOCK-NOCORRECT` | correct ILF | 400 status 23 | mock |
| stored contractId `MOCK-NOCANCEL` | both DELETEs | 400 status 23 | mock |
| stored contractId `MOCK-CANCEL-ACTIVATED` | both DELETEs | 400 status 22 | mock |
| stored contractId `MOCK-INACTIVE` | block | 400 status 63 | mock |
| unknown specification | cancel, unblock, correct; block | 400 status 50; block: 404 status 62 | core |
| block on a cancelled specification | block | 400 status 63 | core |
| `contractId` `MOCK-FAIL-500` on the request or the stored record | every mutating operation | 500 "Simulated failure (mock adapter)." with markers; released, a retry processes again | mock |
| `requestReferenceId` starting with `4d4f434b-0500-` | every mutating operation (also those without any contractId) | the same simulated 500 | mock |
| `startDate=1900-01-01T05:00:00Z` | queryActivations | the same simulated 500 | mock |
| `contractId` `MOCK-SLOW-<ms>` (or `purchaseOrderId` `MOCK-SLOW-<ms>`) | every mutating operation | latency injection, capped at the deadline | mock |