Test vectors
The files in protocol/test-vectors/ are plain JSON, so any implementation can load them in its tests. Each file starts with a description of its format. The documentation build checks every vector against small reference implementations of the documented rules (protocol/docs/scripts/validate.mjs), so a vector and the prose cannot disagree silently.
| File | For | Checks |
|---|---|---|
hlc.json |
clients, servers | stamp formatting, byte-wise comparison, invalid stamps, a clock run with ticks and observed stamps |
merge.json |
clients, servers | stored version plus incoming change gives push status, reason and winner |
push.json |
servers | a batch with every outcome and the exact response of the reference server |
licence.json |
clients | the test token, its bytes, signature, public key and verification outcomes |
numbering.json |
clients, servers | number formats on the device and on the server |
penalty.json |
clients | penalty amounts by overdue days with caps |
loan-state.json |
clients | outstanding quantities, serials and accessories, status, overdue days and penalty after each return |
Sample payloads for every entity are in the examples of each JSON Schema in protocol/schemas/ and are validated against their schema by the same build step.
hlc.json
Section titled “hlc.json”{ "description": "Hybrid logical clock vectors. format: fields and their string form. compare: byte-wise comparison of two stamps (-1 a sorts before b, 0 equal, 1 after). invalid: strings that are not stamps. clock: a sequence of tick and observe steps of one device clock, with the stamp each tick must return.", "format": [ { "millis": 1791792000123, "counter": 0, "node": "3F2A9C1B", "hlc": "001791792000123-0000-3F2A9C1B", "time": "2026-10-12T08:00:00.123Z" }, { "millis": 1791792000123, "counter": 10, "node": "3F2A9C1B", "hlc": "001791792000123-000A-3F2A9C1B", "time": "2026-10-12T08:00:00.123Z" }, { "millis": 0, "counter": 0, "node": "00000000", "hlc": "000000000000000-0000-00000000", "time": "1970-01-01T00:00:00.000Z" }, { "millis": 999999999999999, "counter": 65535, "node": "FFFFFFFF", "hlc": "999999999999999-FFFF-FFFFFFFF", "time": "+033658-09-27T01:46:39.999Z" } ], "compare": [ { "a": "001791792000123-0000-3F2A9C1B", "b": "001791792000124-0000-00000000", "expected": -1, "why": "earlier millisecond" }, { "a": "001791792000123-0009-3F2A9C1B", "b": "001791792000123-000A-3F2A9C1B", "expected": -1, "why": "counter 9 before counter 10" }, { "a": "001791792000123-0001-9D04E6A2", "b": "001791792000123-0001-3F2A9C1B", "expected": 1, "why": "same time and counter, node decides" }, { "a": "001791792000123-0001-3F2A9C1B", "b": "001791792000123-0001-3F2A9C1B", "expected": 0, "why": "identical" }, { "a": "001791792060000-0000-00000000", "b": "001791792000123-FFFF-FFFFFFFF", "expected": 1, "why": "time wins over counter and node" }, { "a": "001791792000123-000a-3F2A9C1B", "b": "001791792000123-000B-3F2A9C1B", "expected": 1, "why": "pitfall: lower-case hex sorts after upper case although 0x0a < 0x0b; writers must use upper case" } ], "invalid": [ "1791792000123-0000-3F2A9C1B", "001791792000123-0000-3F2A9C1", "001791792000123-10000-3F2A9C1B", "001791792000123_0000_3F2A9C1B", "0017917920001230-0000-3F2A9C1B", "00179179200012X-0000-3F2A9C1B", "" ], "clock": { "node": "AAAAAAAA", "steps": [ { "op": "tick", "now": 1000, "expected": "000000000001000-0000-AAAAAAAA" }, { "op": "tick", "now": 1000, "expected": "000000000001000-0001-AAAAAAAA" }, { "op": "tick", "now": 999, "expected": "000000000001000-0002-AAAAAAAA", "why": "the wall clock went back; the counter keeps the order" }, { "op": "observe", "hlc": "000000005000000-0003-BBBBBBBB" }, { "op": "tick", "now": 1000, "expected": "000000005000000-0004-AAAAAAAA", "why": "local stamps sort after the observed one" }, { "op": "tick", "now": 6000000, "expected": "000000006000000-0000-AAAAAAAA" }, { "op": "observe", "hlc": "000000004000000-0009-CCCCCCCC", "why": "older than the clock, no effect" }, { "op": "observe", "hlc": "000000006000000-0005-CCCCCCCC" }, { "op": "tick", "now": 6000000, "expected": "000000006000000-0006-AAAAAAAA" } ] }}merge.json
Section titled “merge.json”{ "description": "Merge outcomes. stored is the version a node holds (null: none), incoming the change it receives. status and reason are what a server answers to a push of incoming; winner is the version stored afterwards on server and client alike. Rejected changes leave the stored version untouched.", "cases": [ { "name": "newer upsert wins", "stored": { "entity": "article", "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "op": "upsert", "hlc": "001791792000123-0000-3F2A9C1B", "data": { "sku": "100101", "name": "Access point" } }, "incoming": { "entity": "article", "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "op": "upsert", "hlc": "001791792000124-0000-9D04E6A2", "data": { "sku": "100101", "name": "Wireless access point AP-7" } }, "status": "applied", "winner": "incoming" }, { "name": "older upsert is ignored", "stored": { "entity": "article", "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "op": "upsert", "hlc": "001791792000124-0000-9D04E6A2", "data": { "sku": "100101", "name": "Wireless access point AP-7" } }, "incoming": { "entity": "article", "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "op": "upsert", "hlc": "001791792000123-0000-3F2A9C1B", "data": { "sku": "100101", "name": "Access point" } }, "status": "ignored", "winner": "stored" }, { "name": "the same version again is ignored (idempotent resend)", "stored": { "entity": "article", "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "op": "upsert", "hlc": "001791792000124-0000-9D04E6A2", "data": { "sku": "100101", "name": "Wireless access point AP-7" } }, "incoming": { "entity": "article", "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "op": "upsert", "hlc": "001791792000124-0000-9D04E6A2", "data": { "sku": "100101", "name": "Wireless access point AP-7" } }, "status": "ignored", "winner": "stored" }, { "name": "ids are compared case-insensitively", "stored": { "entity": "article", "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "op": "upsert", "hlc": "001791792000124-0000-9D04E6A2", "data": { "sku": "100101", "name": "Wireless access point AP-7" } }, "incoming": { "entity": "article", "id": "8c1f3d2e-5b7a-4a61-9f0e-2b4c6d8e0a13", "op": "upsert", "hlc": "001791792000123-0000-3F2A9C1B", "data": { "sku": "100101", "name": "Access point" } }, "status": "ignored", "winner": "stored" }, { "name": "newer tombstone wins", "stored": { "entity": "location", "id": "6F3D9A20-1C5B-4E77-8A2D-3B9C0E4F5A61", "op": "upsert", "hlc": "001791792000123-0000-3F2A9C1B", "data": { "code": "V-01", "name": "Service van 1" } }, "incoming": { "entity": "location", "id": "6F3D9A20-1C5B-4E77-8A2D-3B9C0E4F5A61", "op": "delete", "hlc": "001791792060000-0002-9D04E6A2", "data": null }, "status": "applied", "winner": "incoming" }, { "name": "an upsert older than the tombstone is ignored", "stored": { "entity": "location", "id": "6F3D9A20-1C5B-4E77-8A2D-3B9C0E4F5A61", "op": "delete", "hlc": "001791792060000-0002-9D04E6A2", "data": null }, "incoming": { "entity": "location", "id": "6F3D9A20-1C5B-4E77-8A2D-3B9C0E4F5A61", "op": "upsert", "hlc": "001791792050000-0000-3F2A9C1B", "data": { "code": "V-01", "name": "Van 1" } }, "status": "ignored", "winner": "stored" }, { "name": "an upsert newer than the tombstone brings the record back", "stored": { "entity": "location", "id": "6F3D9A20-1C5B-4E77-8A2D-3B9C0E4F5A61", "op": "delete", "hlc": "001791792060000-0002-9D04E6A2", "data": null }, "incoming": { "entity": "location", "id": "6F3D9A20-1C5B-4E77-8A2D-3B9C0E4F5A61", "op": "upsert", "hlc": "001791792070000-0000-3F2A9C1B", "data": { "code": "V-01", "name": "Van 1" } }, "status": "applied", "winner": "incoming" }, { "name": "a tombstone of an unknown record is stored", "stored": null, "incoming": { "entity": "recipient", "id": "5E2A8C14-9D3B-4A7F-B6C0-1E4D7F2A9B35", "op": "delete", "hlc": "001791792000123-0000-3F2A9C1B", "data": null }, "status": "applied", "winner": "incoming" }, { "name": "movements: the first write wins, even against a newer hlc", "stored": { "entity": "movement", "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "op": "upsert", "hlc": "001791792000123-0000-3F2A9C1B", "data": { "type": "receipt", "article_id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "to_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "quantity": 1 } }, "incoming": { "entity": "movement", "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "op": "upsert", "hlc": "001791792090000-0000-9D04E6A2", "data": { "type": "receipt", "article_id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "to_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "quantity": 9 } }, "status": "ignored", "winner": "stored" }, { "name": "movements cannot be deleted", "stored": { "entity": "movement", "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "op": "upsert", "hlc": "001791792000123-0000-3F2A9C1B", "data": { "type": "receipt", "article_id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "to_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "quantity": 1 } }, "incoming": { "entity": "movement", "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "op": "delete", "hlc": "001791792090000-0000-9D04E6A2", "data": null }, "status": "rejected", "reason": "immutable", "winner": "stored" }, { "name": "a locked template cannot be changed by devices", "stored": { "entity": "document_template", "id": "00000000-0000-4000-8000-000000000003", "op": "upsert", "hlc": "001791792000123-0000-00000000", "data": { "kind": "loan", "jurisdiction": "de", "locked": true } }, "incoming": { "entity": "document_template", "id": "00000000-0000-4000-8000-000000000003", "op": "upsert", "hlc": "001791792090000-0000-9D04E6A2", "data": { "kind": "loan", "jurisdiction": "at", "locked": false } }, "status": "rejected", "reason": "locked", "winner": "stored" }, { "name": "unknown entity types follow the mutable rule and are relayed unchanged", "stored": { "entity": "vehicle", "id": "B7D9F1A3-4C6E-4A80-9B2D-6F8A0C2E4D61", "op": "upsert", "hlc": "001791792000123-0000-3F2A9C1B", "data": { "plate": "B-LV 100" } }, "incoming": { "entity": "vehicle", "id": "B7D9F1A3-4C6E-4A80-9B2D-6F8A0C2E4D61", "op": "upsert", "hlc": "001791792000200-0000-9D04E6A2", "data": { "plate": "B-LV 101", "mileage": 12000.0 } }, "status": "applied", "winner": "incoming" } ]}push.json
Section titled “push.json”{ "description": "Changes as pushed to an empty server and the result the reference server answers for each, in request order. Every change is judged on its own; a rejected change does not affect the others. message texts are those of Accept-Language: en.", "request": { "device_id": "3F2A9C1B-7E4D-4B2A-9C1E-5D8F0A2B4C6E", "changes": [ { "entity": "location", "id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "op": "upsert", "hlc": "001791792000123-0000-3F2A9C1B", "data": { "code": "N-A01", "name": "Shelf A01" } }, { "entity": "article", "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "op": "upsert", "hlc": "001791792000123-0001-3F2A9C1B", "data": { "sku": "100101", "name": "Wireless access point AP-7", "unit": "Stk", "barcodes": [{ "code": "4012345678901", "symbology": "ean13" }] } }, { "entity": "movement", "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "op": "upsert", "hlc": "001791792000123-0002-3F2A9C1B", "data": { "type": "receipt", "article_id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "to_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "quantity": 20, "booked_at": "2026-10-12T08:00:00Z", "booked_by": "Erika Mustermann" } }, { "entity": "movement", "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "op": "upsert", "hlc": "001791792000123-0003-3F2A9C1B", "data": { "type": "receipt", "article_id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "to_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "quantity": 25 } }, { "entity": "movement", "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "op": "delete", "hlc": "001791792000123-0004-3F2A9C1B", "data": null }, { "entity": "article", "id": "19BE4A7C-6D2F-4B8E-9A31-7C5E3D1B0F22", "op": "upsert", "hlc": "001791792000123-0005-3F2A9C1B", "data": { "name": "Patch cable 2 m" } }, { "entity": "article", "id": "not-a-uuid", "op": "upsert", "hlc": "001791792000123-0006-3F2A9C1B", "data": { "sku": "1", "name": "x" } }, { "entity": "article", "id": "A1B2C3D4-E5F6-4A7B-8C9D-0E1F2A3B4C5D", "op": "delete", "hlc": "001791792000123-0007-3F2A9C1B", "data": { "sku": "1" } }, { "entity": "vehicle", "id": "B7D9F1A3-4C6E-4A80-9B2D-6F8A0C2E4D61", "op": "upsert", "hlc": "001791792000123-0008-3F2A9C1B", "data": { "plate": "B-LV 100", "nested": {} } } ] }, "response": { "results": [ { "id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "entity": "location", "status": "applied" }, { "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "entity": "article", "status": "applied" }, { "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "entity": "movement", "status": "applied" }, { "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "entity": "movement", "status": "ignored" }, { "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "entity": "movement", "status": "rejected", "reason": "immutable", "message": "Movements cannot be deleted. Correct a wrong booking with a counter-booking." }, { "id": "19BE4A7C-6D2F-4B8E-9A31-7C5E3D1B0F22", "entity": "article", "status": "rejected", "reason": "invalid_payload", "message": "The SKU field is required." }, { "id": "not-a-uuid", "entity": "article", "status": "rejected", "reason": "invalid_change", "message": "The ID field must be a valid UUID." }, { "id": "A1B2C3D4-E5F6-4A7B-8C9D-0E1F2A3B4C5D", "entity": "article", "status": "rejected", "reason": "invalid_change", "message": "A delete must not carry data." }, { "id": "B7D9F1A3-4C6E-4A80-9B2D-6F8A0C2E4D61", "entity": "vehicle", "status": "applied" } ] }}licence.json
Section titled “licence.json”{ "description": "Organisation licence verification. The token is the test token of the reference app's unit tests, signed with the publisher key that ships in the app. Each check gives the base URL the client is connected to and the current time; valid is the expected outcome of the verification steps.", "public_key_base64": "XUT6JSv2UP7Qs7lic7l/w46dOAV+8jRdwzmoevd5Yqs=", "public_key_hex": "5d44fa252bf650fed0b3b96273b97fc38e9d38057ef2345dc339a87af77962ab", "token": "eyJleHBpcmVzIjoiMjA5OS0xMi0zMSIsImxpY2Vuc2VlIjoiVGVzdCBHbWJIIiwicGxhbiI6InRlYW0iLCJzZXJ2ZXIiOiJodHRwczpcL1wvbGFnZXIuZXhhbXBsZS50ZXN0IiwidiI6MX0.SrjXLKu0cKaNjeUVqx_ilKVimb2wBD1j5Whg2SD8FgaGHktoIwllP2i8O0yjeq7UC5_nhtYc3fvJZk9WgrLfDw", "payload_bytes": "{\"expires\":\"2099-12-31\",\"licensee\":\"Test GmbH\",\"plan\":\"team\",\"server\":\"https:\\/\\/lager.example.test\",\"v\":1}", "signature_hex": "4ab8d72cabb470a68d8de515ab1fe294a56299bdb0043d63e56860d920fc1606861e4b682309653f68bc3b4ca37aaed40b9fe786d61cddfbc9664f5682b2df0f", "payload": { "v": 1, "licensee": "Test GmbH", "plan": "team", "server": "https://lager.example.test", "expires": "2099-12-31" }, "checks": [ { "server_url": "https://lager.example.test", "now": "2027-01-01T00:00:00Z", "valid": true, "why": "signature, host and date match" }, { "server_url": "https://lager.example.test/inventory", "now": "2027-01-01T00:00:00Z", "valid": true, "why": "only the host is compared; the path does not matter" }, { "server_url": "http://LAGER.example.test:8443", "now": "2027-01-01T00:00:00Z", "valid": true, "why": "host comparison ignores case, scheme and port" }, { "server_url": "https://other.example.test", "now": "2027-01-01T00:00:00Z", "valid": false, "why": "issued for another server" }, { "server_url": "https://lager.example.test", "now": "2099-12-31T23:59:59Z", "valid": true, "why": "valid through the whole expiry day (UTC)" }, { "server_url": "https://lager.example.test", "now": "2100-01-01T00:00:00Z", "valid": false, "why": "expired" }, { "server_url": "https://lager.example.test", "now": "2027-01-01T00:00:00Z", "token": "xyJleHBpcmVzIjoiMjA5OS0xMi0zMSIsImxpY2Vuc2VlIjoiVGVzdCBHbWJIIiwicGxhbiI6InRlYW0iLCJzZXJ2ZXIiOiJodHRwczpcL1wvbGFnZXIuZXhhbXBsZS50ZXN0IiwidiI6MX0.SrjXLKu0cKaNjeUVqx_ilKVimb2wBD1j5Whg2SD8FgaGHktoIwllP2i8O0yjeq7UC5_nhtYc3fvJZk9WgrLfDw", "valid": false, "why": "payload altered, signature no longer matches" } ]}numbering.json
Section titled “numbering.json”{ "description": "Document number formatting. device: how the reference app formats numbers on the device (the first {SEQ} is replaced; without {SEQ} the counter is appended). server: how the reference server formats reserved numbers ({DEV} and unknown placeholders stay as they are; without {SEQ} no counter is added). Dates are calendar dates in the time zone of the formatter.", "device": [ { "format": "LS-{YYYY}-{DEV}-{SEQ:4}", "date": "2026-03-09", "sequence": 42, "device_code": "B", "expected": "LS-2026-B-0042" }, { "format": "{YY}{MM}/{SEQ}", "date": "2026-03-09", "sequence": 7, "device_code": "B", "expected": "2603/7" }, { "format": "LS", "date": "2026-03-09", "sequence": 3, "device_code": "B", "expected": "LS3" }, { "format": "UE-{YYYY}-{DEV}-{SEQ:4}", "date": "2026-10-12", "sequence": 1, "device_code": "3F", "expected": "UE-2026-3F-0001" }, { "format": "LS-{SEQ:2}", "date": "2026-10-12", "sequence": 123, "device_code": "3F", "expected": "LS-123" } ], "server": [ { "format": "LS-{YYYY}-{SEQ:5}", "date": "2026-10-12", "sequence": 41, "expected": "LS-2026-00041" }, { "format": "{YY}{MM}-{SEQ:3}", "date": "2026-03-09", "sequence": 5, "expected": "2603-005" }, { "format": "LS-{DEV}-{SEQ:3}", "date": "2026-10-12", "sequence": 41, "expected": "LS-{DEV}-041" }, { "format": "LS", "date": "2026-10-12", "sequence": 41, "expected": "LS" } ]}penalty.json
Section titled “penalty.json”{ "description": "Late-return penalty. overdue_days are full calendar days after the due date, total_value the sum of replacement_value times quantity over all items of the loan. Amounts are in the inventory currency; compare with a tolerance of 1e-9.", "cases": [ { "rule": { "tiers": [{ "after_days": 2, "per_day": 5 }, { "after_days": 14, "per_day": 10 }], "cap_amount": null, "cap_percent": 30 }, "total_value": 189.9, "amounts": { "0": 0, "1": 0, "2": 0, "3": 5, "10": 40, "15": 56.97, "20": 56.97 } }, { "rule": { "tiers": [{ "after_days": 2, "per_day": 5 }, { "after_days": 14, "per_day": 10 }], "cap_amount": null, "cap_percent": null }, "total_value": 189.9, "amounts": { "0": 0, "3": 5, "15": 70, "20": 120 } }, { "rule": { "tiers": [{ "after_days": 14, "per_day": 10 }, { "after_days": 2, "per_day": 5 }], "cap_amount": 50, "cap_percent": 30 }, "total_value": 1000, "amounts": { "3": 5, "10": 40, "15": 50, "20": 50 } }, { "rule": { "tiers": [{ "after_days": 0, "per_day": 2.5 }], "cap_amount": null, "cap_percent": 10 }, "total_value": 0, "amounts": { "1": 2.5, "5": 12.5 } }, { "rule": { "tiers": [{ "after_days": 0, "per_day": 2.5 }], "cap_amount": null, "cap_percent": null }, "total_value": 0, "amounts": { "1": 2.5, "5": 12.5 } } ]}loan-state.json
Section titled “loan-state.json”{ "description": "What is still out on a loan, derived from its items and returns. Each state applies the first after_returns returns of the loan and evaluates at now in time_zone. status is open, overdue or closed; overdue_days are full calendar days after due_at; accrued_penalty is null when the penalty is zero or nothing is out. Only the fields the derivation reads are given.", "loans": [ { "name": "partial return, accessories later, remaining unit lost", "loan": { "due_at": "2026-10-18T22:00:00Z", "penalty": { "tiers": [{ "after_days": 2, "per_day": 5 }, { "after_days": 14, "per_day": 10 }], "cap_amount": null, "cap_percent": 30 }, "items": [ { "position": 1, "quantity": 1, "serials": ["DR-1001"], "replacement_value": 189.9, "accessories": ["Charger", "Battery 4 Ah", "Case"] }, { "position": 2, "quantity": 3, "serials": [], "replacement_value": 40, "accessories": [] } ], "returns": [ { "id": "F1A3C5E7-9B2D-4F46-8A0C-2E4B6D8F0A17", "returned_at": "2026-10-20T07:30:00Z", "items": [ { "position": 1, "quantity": 1, "serials": ["DR-1001"], "condition": "damaged", "accessories": ["charger", "Case"], "lost_accessories": [] }, { "position": 2, "quantity": 2, "serials": [], "condition": "ok", "accessories": [], "lost_accessories": [] } ] }, { "id": "0B2D4F61-8A3C-4E5F-9B7D-1C3E5A7B9D02", "returned_at": "2026-10-25T10:00:00Z", "items": [ { "position": 1, "quantity": 0, "serials": [], "condition": "ok", "accessories": [], "lost_accessories": ["Battery 4 Ah"] }, { "position": 2, "quantity": 1, "serials": [], "condition": "lost", "accessories": [], "lost_accessories": [] } ] } ] }, "states": [ { "after_returns": 0, "now": "2026-10-19T21:59:00Z", "time_zone": "Europe/Berlin", "expected": { "status": "open", "overdue_days": 0, "accrued_penalty": null, "total_value": 309.9, "items": [ { "position": 1, "outstanding_quantity": 1, "outstanding_serials": ["DR-1001"], "outstanding_accessories": ["Charger", "Battery 4 Ah", "Case"] }, { "position": 2, "outstanding_quantity": 3, "outstanding_serials": [], "outstanding_accessories": [] } ] } }, { "after_returns": 1, "now": "2026-10-20T08:00:00Z", "time_zone": "Europe/Berlin", "expected": { "status": "overdue", "overdue_days": 1, "accrued_penalty": null, "total_value": 309.9, "items": [ { "position": 1, "outstanding_quantity": 0, "outstanding_serials": [], "outstanding_accessories": ["Battery 4 Ah"] }, { "position": 2, "outstanding_quantity": 1, "outstanding_serials": [], "outstanding_accessories": [] } ] } }, { "after_returns": 1, "now": "2026-10-25T09:00:00Z", "time_zone": "Europe/Berlin", "expected": { "status": "overdue", "overdue_days": 6, "accrued_penalty": 20, "total_value": 309.9, "items": [ { "position": 1, "outstanding_quantity": 0, "outstanding_serials": [], "outstanding_accessories": ["Battery 4 Ah"] }, { "position": 2, "outstanding_quantity": 1, "outstanding_serials": [], "outstanding_accessories": [] } ] } }, { "after_returns": 2, "now": "2026-10-25T10:05:00Z", "time_zone": "Europe/Berlin", "expected": { "status": "closed", "overdue_days": 6, "accrued_penalty": null, "total_value": 309.9, "items": [ { "position": 1, "outstanding_quantity": 0, "outstanding_serials": [], "outstanding_accessories": [] }, { "position": 2, "outstanding_quantity": 0, "outstanding_serials": [], "outstanding_accessories": [] } ] } } ] }, { "name": "an item reported lost as a whole takes its accessories with it", "loan": { "due_at": null, "penalty": null, "items": [ { "position": 1, "quantity": 1, "serials": ["LP-77"], "replacement_value": null, "accessories": ["Power supply", "Bag"] } ], "returns": [ { "id": "2C4E6A80-1B3D-4F5A-8C7E-9D0F2B4A6C83", "returned_at": "2026-11-02T09:00:00Z", "items": [ { "position": 1, "quantity": 1, "serials": ["LP-77"], "condition": "lost", "accessories": [], "lost_accessories": [] } ] } ] }, "states": [ { "after_returns": 0, "now": "2027-06-01T12:00:00Z", "time_zone": "Europe/Berlin", "expected": { "status": "open", "overdue_days": 0, "accrued_penalty": null, "total_value": 0, "items": [{ "position": 1, "outstanding_quantity": 1, "outstanding_serials": ["LP-77"], "outstanding_accessories": ["Power supply", "Bag"] }] } }, { "after_returns": 1, "now": "2027-06-01T12:00:00Z", "time_zone": "Europe/Berlin", "expected": { "status": "closed", "overdue_days": 0, "accrued_penalty": null, "total_value": 0, "items": [{ "position": 1, "outstanding_quantity": 0, "outstanding_serials": [], "outstanding_accessories": [] }] } } ] } ]}