diff --git a/lib/Settings/example-sets/zuiddrecht.json b/lib/Settings/example-sets/zuiddrecht.json index 8f0be19183..669c687be8 100644 --- a/lib/Settings/example-sets/zuiddrecht.json +++ b/lib/Settings/example-sets/zuiddrecht.json @@ -1,7 +1,7 @@ { "set": "zuiddrecht", "title": "Gemeente Zuiddrecht", - "$comment": "The cast of the Zuiddrecht example set, and OpenRegister's own fragment: a BRP row per cast person with a BSN and a KVK row and branch per business. Installed with `occ openregister:example-set:install zuiddrecht`; every other app adds its part in its own lib/Settings/example-sets/zuiddrecht.json and refers to cast members with {{cast:key.field}} and to other apps' objects only through a lookup on a business key (BSN, KVK number). Zuiddrecht is fictitious. BSNs are 11-proof numbers from the BRP test block 999990000-999999999 and appear nowhere else in the fleet's registers; KVK numbers are fictitious; municipality code 9999 belongs to no municipality.", + "$comment": "The cast of the Zuiddrecht example set, and OpenRegister's own fragment: a BRP row per cast person with a BSN and a KVK row and branch per business. It also fills the public-space register (openbare ruimte) with the trees, street lights, play equipment, market pitches and road sections the cases link to; other fragments find them by `identification`. Installed with `occ openregister:example-set:install zuiddrecht`; every other app adds its part in its own lib/Settings/example-sets/zuiddrecht.json and refers to cast members with {{cast:key.field}} and to other apps' objects only through a lookup on a business key (BSN, KVK number). Zuiddrecht is fictitious. BSNs are 11-proof numbers from the BRP test block 999990000-999999999 and appear nowhere else in the fleet's registers; KVK numbers are fictitious; municipality code 9999 belongs to no municipality.", "timezone": "Europe/Amsterdam", "requires": [ { @@ -14,6 +14,10 @@ }, { "register": "destruction-lists" + }, + { + "register": "public-space", + "mock": true } ], "apps": [ @@ -2198,6 +2202,265 @@ "dpiaRequired": false, "escalationTier": "on-track" } + }, + { + "key": "public-space-kastanje-parkstraat", + "register": "public-space", + "schema": "tree", + "data": { + "identification": "BM-0412", + "name": "Kastanje Parkstraat", + "status": "Te kappen", + "species": "Paardenkastanje (Aesculus hippocastanum)", + "plantingYear": 1958, + "heightClass": "15-18 m", + "trunkDiameter": 74, + "monumental": false, + "location": { + "type": "Point", + "coordinates": [ + 4.6331, + 51.8164 + ] + } + } + }, + { + "key": "public-space-es-parkweg-1", + "register": "public-space", + "schema": "tree", + "data": { + "identification": "BM-1031", + "name": "Es Parkweg 1", + "status": "Te kappen", + "species": "Gewone es (Fraxinus excelsior)", + "plantingYear": 1975, + "heightClass": "12-15 m", + "trunkDiameter": 46, + "monumental": false, + "location": { + "type": "Point", + "coordinates": [ + 4.6352, + 51.8181 + ] + } + } + }, + { + "key": "public-space-es-parkweg-2", + "register": "public-space", + "schema": "tree", + "data": { + "identification": "BM-1032", + "name": "Es Parkweg 2", + "status": "Te kappen", + "species": "Gewone es (Fraxinus excelsior)", + "plantingYear": 1975, + "heightClass": "12-15 m", + "trunkDiameter": 52, + "monumental": false, + "location": { + "type": "Point", + "coordinates": [ + 4.6355, + 51.8182 + ] + } + } + }, + { + "key": "public-space-es-parkweg-3", + "register": "public-space", + "schema": "tree", + "data": { + "identification": "BM-1033", + "name": "Es Parkweg 3", + "status": "Te kappen", + "species": "Gewone es (Fraxinus excelsior)", + "plantingYear": 1975, + "heightClass": "12-15 m", + "trunkDiameter": 41, + "monumental": false, + "location": { + "type": "Point", + "coordinates": [ + 4.6358, + 51.8183 + ] + } + } + }, + { + "key": "public-space-linde-oosthavenplein", + "register": "public-space", + "schema": "tree", + "data": { + "identification": "BM-0207", + "name": "Linde Oosthavenplein", + "status": "Gezond", + "species": "Zilverlinde (Tilia tomentosa)", + "plantingYear": 1932, + "heightClass": "18-24 m", + "trunkDiameter": 88, + "monumental": true, + "location": { + "type": "Point", + "coordinates": [ + 4.6412, + 51.8129 + ] + } + } + }, + { + "key": "public-space-lantaarnpaal-kerkplein", + "register": "public-space", + "schema": "street-light", + "data": { + "identification": "LP-0703", + "name": "Lantaarnpaal Kerkplein", + "status": "In orde", + "address": "Kerkplein, bij de ingang", + "lampType": "LED", + "mastHeight": 6, + "location": { + "type": "Point", + "coordinates": [ + 4.6298, + 51.815 + ] + } + } + }, + { + "key": "public-space-lantaarnpaal-lindelaan-14", + "register": "public-space", + "schema": "street-light", + "data": { + "identification": "LP-1408", + "name": "Lantaarnpaal Lindelaan 14", + "status": "Defect", + "address": "Lindelaan 14", + "lampType": "LED", + "mastHeight": 6, + "location": { + "type": "Point", + "coordinates": [ + 4.627, + 51.8201 + ] + } + } + }, + { + "key": "public-space-lichtmast-14", + "register": "public-space", + "schema": "street-light", + "data": { + "identification": "LM-0014", + "name": "Lichtmast 14", + "status": "Gepland", + "address": "Fietspad Lindelaan, bij de brug", + "lampType": "LED", + "mastHeight": 5, + "location": { + "type": "Point", + "coordinates": [ + 4.6259, + 51.8213 + ] + } + } + }, + { + "key": "public-space-lichtmast-15", + "register": "public-space", + "schema": "street-light", + "data": { + "identification": "LM-0015", + "name": "Lichtmast 15", + "status": "Gepland", + "address": "Fietspad Lindelaan, bij de bushalte", + "lampType": "LED", + "mastHeight": 5, + "location": { + "type": "Point", + "coordinates": [ + 4.6251, + 51.8219 + ] + } + } + }, + { + "key": "public-space-wipkip-oosthavenplein", + "register": "public-space", + "schema": "play-equipment", + "data": { + "identification": "SP-0117", + "name": "Wipkip Oosthavenplein", + "status": "Defect", + "playground": "Speelplek Oosthavenplein", + "equipmentType": "Veerwipper", + "manufacturer": "Speelwerk Zuid", + "installationYear": 2014, + "ageGroup": "2 tot 6 jaar", + "safetySurface": "Rubbertegels", + "lastInspection": "{{date:-40}}", + "location": { + "type": "Point", + "coordinates": [ + 4.6415, + 51.8127 + ] + } + } + }, + { + "key": "public-space-standplaats-markt-7", + "register": "public-space", + "schema": "market-pitch", + "data": { + "identification": "ST-0007", + "name": "Standplaats Markt 7", + "status": "Vrij", + "market": "Weekmarkt Markt", + "marketDay": "Zaterdag", + "area": 12 + } + }, + { + "key": "public-space-standplaats-oosthavenplein", + "register": "public-space", + "schema": "market-pitch", + "data": { + "identification": "ST-0031", + "name": "Standplaats Oosthavenplein", + "status": "Vergeven", + "market": "Oosthavenplein", + "marketDay": "Evenement", + "area": 9 + } + }, + { + "key": "public-space-fietspad-lindelaan", + "register": "public-space", + "schema": "road-section", + "data": { + "identification": "BGT wgl.0505.1234", + "name": "Fietspad Lindelaan", + "status": "In beheer", + "function": "Fietspad", + "surface": "Rood asfalt", + "location": { + "type": "Point", + "coordinates": [ + 4.6262, + 51.8209 + ] + } + } } ], "archival": [ diff --git a/lib/Settings/public_space_register.json b/lib/Settings/public_space_register.json new file mode 100644 index 0000000000..468546dfe7 --- /dev/null +++ b/lib/Settings/public_space_register.json @@ -0,0 +1,381 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Public Space Mock Register", + "description": "Mock register of objects in public space (openbare ruimte): trees, street lights, play equipment, market pitches and road sections. Example data a case can link an object to; the Zuiddrecht example set imports it and fills it.", + "version": "1.0.0" + }, + "x-openregister": { + "type": "mock", + "app": "openregister", + "openregister": "^v0.2.10", + "description": "Public space mock register for development and demos, so a case can link a real object with a status of its own" + }, + "paths": {}, + "components": { + "registers": { + "public-space": { + "slug": "public-space", + "title": "Openbare ruimte", + "description": "Objects in public space that the municipality manages and that cases are about: trees, street lights, play equipment, market pitches and road sections.", + "schemas": [ + "tree", + "street-light", + "play-equipment", + "market-pitch", + "road-section" + ], + "folder": "Open Registers/Openbare ruimte" + } + }, + "schemas": { + "tree": { + "slug": "tree", + "title": "Bomenregister", + "version": "1.0.0", + "summary": "A tree the municipality manages in public space.", + "description": "A tree the municipality manages in public space.", + "type": "object", + "required": [ + "identification", + "name", + "status" + ], + "hardValidation": false, + "searchable": true, + "configuration": { + "objectNameField": "name", + "objectDescriptionField": "identification" + }, + "properties": { + "identification": { + "type": "string", + "title": "Identification", + "description": "The tree number on its tag, such as BM-0412.", + "facetable": true + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name a reader knows the object by." + }, + "status": { + "type": "string", + "title": "Status", + "description": "The tree's condition as the last inspection found it.", + "enum": [ + "Gezond", + "Aandacht nodig", + "Te kappen", + "Gekapt" + ], + "facetable": true + }, + "species": { + "type": "string", + "title": "Species", + "description": "Common and botanical name.", + "facetable": true + }, + "plantingYear": { + "type": "integer", + "title": "Planting year", + "description": "The year the tree was planted." + }, + "heightClass": { + "type": "string", + "title": "Height class", + "description": "Height class in metres, such as 15-18 m.", + "facetable": true + }, + "trunkDiameter": { + "type": "integer", + "title": "Trunk diameter", + "description": "Diameter at chest height, in centimetres." + }, + "monumental": { + "type": "boolean", + "title": "Monumental", + "description": "Whether the tree is on the municipal list of monumental trees." + }, + "location": { + "type": "geo", + "title": "Location", + "description": "Where the object is, as a GeoJSON point in WGS84." + } + } + }, + "street-light": { + "slug": "street-light", + "title": "Openbare verlichting", + "version": "1.0.0", + "summary": "A street light or light mast the municipality manages.", + "description": "A street light or light mast the municipality manages.", + "type": "object", + "required": [ + "identification", + "name", + "status" + ], + "hardValidation": false, + "searchable": true, + "configuration": { + "objectNameField": "name", + "objectDescriptionField": "identification" + }, + "properties": { + "identification": { + "type": "string", + "title": "Identification", + "description": "The mast number, such as LP-1408 or LM-0014.", + "facetable": true + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name a reader knows the object by." + }, + "status": { + "type": "string", + "title": "Status", + "description": "Whether the light works, is broken or is still to be placed.", + "enum": [ + "In orde", + "Defect", + "Gepland", + "Verwijderd" + ], + "facetable": true + }, + "address": { + "type": "string", + "title": "Address", + "description": "The address or spot the mast stands at." + }, + "lampType": { + "type": "string", + "title": "Lamp type", + "description": "The kind of lamp, such as LED.", + "facetable": true + }, + "mastHeight": { + "type": "number", + "title": "Mast height", + "description": "Height of the mast in metres." + }, + "location": { + "type": "geo", + "title": "Location", + "description": "Where the object is, as a GeoJSON point in WGS84." + } + } + }, + "play-equipment": { + "slug": "play-equipment", + "title": "Buitenruimte", + "version": "1.0.0", + "summary": "A piece of play equipment on a public playground.", + "description": "A piece of play equipment on a public playground.", + "type": "object", + "required": [ + "identification", + "name", + "status" + ], + "hardValidation": false, + "searchable": true, + "configuration": { + "objectNameField": "name", + "objectDescriptionField": "identification" + }, + "properties": { + "identification": { + "type": "string", + "title": "Identification", + "description": "The equipment number, such as SP-0117.", + "facetable": true + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name a reader knows the object by." + }, + "status": { + "type": "string", + "title": "Status", + "description": "Whether children can use it.", + "enum": [ + "In orde", + "Defect", + "Buiten gebruik" + ], + "facetable": true + }, + "playground": { + "type": "string", + "title": "Playground", + "description": "The playground it stands on.", + "facetable": true + }, + "equipmentType": { + "type": "string", + "title": "Equipment type", + "description": "The kind of equipment, such as a spring rider.", + "facetable": true + }, + "manufacturer": { + "type": "string", + "title": "Manufacturer", + "description": "Who made it." + }, + "installationYear": { + "type": "integer", + "title": "Installation year", + "description": "The year it was placed." + }, + "ageGroup": { + "type": "string", + "title": "Age group", + "description": "The ages it is meant for." + }, + "safetySurface": { + "type": "string", + "title": "Safety surface", + "description": "The impact-absorbing surface under it." + }, + "lastInspection": { + "type": "string", + "title": "Last inspection", + "description": "The date of the last safety inspection.", + "format": "date" + }, + "location": { + "type": "geo", + "title": "Location", + "description": "Where the object is, as a GeoJSON point in WGS84." + } + } + }, + "market-pitch": { + "slug": "market-pitch", + "title": "Markt", + "version": "1.0.0", + "summary": "A pitch on a market or event square that the municipality lets.", + "description": "A pitch on a market or event square that the municipality lets.", + "type": "object", + "required": [ + "identification", + "name", + "status" + ], + "hardValidation": false, + "searchable": true, + "configuration": { + "objectNameField": "name", + "objectDescriptionField": "identification" + }, + "properties": { + "identification": { + "type": "string", + "title": "Identification", + "description": "The pitch number, such as ST-0007.", + "facetable": true + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name a reader knows the object by." + }, + "status": { + "type": "string", + "title": "Status", + "description": "Whether the pitch is let.", + "enum": [ + "Vergeven", + "Vrij", + "Ingetrokken" + ], + "facetable": true + }, + "market": { + "type": "string", + "title": "Market", + "description": "The market or square the pitch is on.", + "facetable": true + }, + "marketDay": { + "type": "string", + "title": "Market day", + "description": "The day the pitch is used.", + "facetable": true + }, + "area": { + "type": "number", + "title": "Area", + "description": "Size of the pitch in square metres." + } + } + }, + "road-section": { + "slug": "road-section", + "title": "Wegen en paden", + "version": "1.0.0", + "summary": "A road, cycle path or footpath section the municipality manages.", + "description": "A road, cycle path or footpath section the municipality manages.", + "type": "object", + "required": [ + "identification", + "name", + "status" + ], + "hardValidation": false, + "searchable": true, + "configuration": { + "objectNameField": "name", + "objectDescriptionField": "identification" + }, + "properties": { + "identification": { + "type": "string", + "title": "Identification", + "description": "The BGT identification of the section.", + "facetable": true + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name a reader knows the object by." + }, + "status": { + "type": "string", + "title": "Status", + "description": "Where the section is in its life.", + "enum": [ + "Gepland", + "In aanleg", + "In beheer" + ], + "facetable": true + }, + "function": { + "type": "string", + "title": "Function", + "description": "What the section is for, such as a cycle path.", + "facetable": true + }, + "surface": { + "type": "string", + "title": "Surface", + "description": "The paving, such as asphalt." + }, + "location": { + "type": "geo", + "title": "Location", + "description": "Where the object is, as a GeoJSON point in WGS84." + } + } + } + }, + "objects": [] + } +} diff --git a/tests/Unit/Service/ExampleSet/ExampleSetCatalogueTest.php b/tests/Unit/Service/ExampleSet/ExampleSetCatalogueTest.php index 2210e43ad3..534a0f99f8 100644 --- a/tests/Unit/Service/ExampleSet/ExampleSetCatalogueTest.php +++ b/tests/Unit/Service/ExampleSet/ExampleSetCatalogueTest.php @@ -147,7 +147,10 @@ public function testTheShippedZuiddrechtSetIsWhole(): void { ); // The jeugdteam's regisseur of the jeugdwet example case. $this->assertSame(['fatma.kaya', ['sociaal-domein']], [$plan['cast']['people']['fatma']['uid'], array_values(array_diff($plan['cast']['people']['fatma']['groups'], ['medewerkers']))]); - $this->assertSame(['brp' => ['mock' => true], 'kvk' => ['mock' => true], 'destruction-lists' => ['mock' => false]], $plan['requires']); + $this->assertSame( + expected: ['brp' => ['mock' => true], 'kvk' => ['mock' => true], 'destruction-lists' => ['mock' => false], 'public-space' => ['mock' => true]], + actual: $plan['requires'] + ); // Dossiq's archival review widget reads the destruction lists in the register the archival setting names. $this->assertSame( expected: [['key' => 'destruction-lists', 'data' => ['destructionListRegister' => '{{register:destruction-lists}}', 'destructionListSchema' => '{{schema:destruction-lists/destructionList}}']]], diff --git a/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php b/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php index 4125628ca5..662db75778 100644 --- a/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php +++ b/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php @@ -153,8 +153,8 @@ public function testEveryRegisterTheShippedZuiddrechtSetRequiresImportsOnAFreshI $plan = $this->catalogue->gather(set: 'zuiddrecht'); $registers = $this->realRegisters(); - $this->assertSame(['brp', 'kvk', 'destruction-lists'], array_keys($plan['requires'])); - $this->assertSame([true, true, false], array_column($plan['requires'], 'mock')); + $this->assertSame(expected: ['brp', 'kvk', 'destruction-lists', 'public-space'], actual: array_keys($plan['requires'])); + $this->assertSame(expected: [true, true, false, true], actual: array_column($plan['requires'], 'mock')); foreach ($plan['requires'] as $slug => $required) { $import = match ($required['mock']) { true => $registers->importMock(slug: $slug), @@ -165,7 +165,10 @@ public function testEveryRegisterTheShippedZuiddrechtSetRequiresImportsOnAFreshI $this->assertArrayHasKey($slug, $this->instance); } - $this->assertSame(['openregister.mock', 'openregister.mock', 'openregister.destruction-lists'], $this->importedAs); + $this->assertSame( + expected: ['openregister.mock', 'openregister.mock', 'openregister.destruction-lists', 'openregister.mock'], + actual: $this->importedAs + ); } public function testARegisterShippedOnlyAsAMockIsNotImportedAsTheRealConfiguration(): void { diff --git a/tests/Unit/Service/ExampleSet/ZuiddrechtPublicSpaceTest.php b/tests/Unit/Service/ExampleSet/ZuiddrechtPublicSpaceTest.php new file mode 100644 index 0000000000..5cd44342e2 --- /dev/null +++ b/tests/Unit/Service/ExampleSet/ZuiddrechtPublicSpaceTest.php @@ -0,0 +1,142 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://www.OpenRegister.nl + * + * @spec exclude example-set data integrity; no requirement covers the demo fragment + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Tests\Unit\Service\ExampleSet; + +use PHPUnit\Framework\TestCase; + +/** + * Reads the shipped descriptor and cast file from disk. + */ +class ZuiddrechtPublicSpaceTest extends TestCase { + + private const CAST_FILE = __DIR__.'/../../../../lib/Settings/example-sets/zuiddrecht.json'; + + private const DESCRIPTOR_FILE = __DIR__.'/../../../../lib/Settings/public_space_register.json'; + + /** + * Decode one shipped JSON file. + * + * @param string $path The file. + * + * @return array + */ + private static function read(string $path): array { + return (array) json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); + }//end read() + + /** + * The descriptor is a mock, so no instance imports it unless a set asks. + * + * @return void + */ + public function testTheRegisterIsAMockTheSetRequires(): void { + $descriptor = self::read(path: self::DESCRIPTOR_FILE); + $set = self::read(path: self::CAST_FILE); + + $this->assertSame(expected: 'mock', actual: $descriptor['x-openregister']['type']); + $this->assertSame(expected: 'Openbare ruimte', actual: $descriptor['components']['registers']['public-space']['title']); + $this->assertContains(needle: ['register' => 'public-space', 'mock' => true], haystack: $set['requires']); + }//end testTheRegisterIsAMockTheSetRequires() + + /** + * Each schema's title, property count and whether it has a location, + * which is what dossiq's object type catalogue shows per type. + * + * @return void + */ + public function testTheSchemasCarryTheCatalogueColumns(): void { + $schemas = self::read(path: self::DESCRIPTOR_FILE)['components']['schemas']; + + $shape = []; + foreach ($schemas as $slug => $schema) { + $geo = array_filter($schema['properties'], static fn (array $property): bool => $property['type'] === 'geo'); + $shape[$slug] = [$schema['title'], count($schema['properties']), $geo !== []]; + } + + $this->assertSame( + expected: [ + 'tree' => ['Bomenregister', 9, true], + 'street-light' => ['Openbare verlichting', 7, true], + 'play-equipment' => ['Buitenruimte', 11, true], + 'market-pitch' => ['Markt', 6, false], + 'road-section' => ['Wegen en paden', 6, true], + ], + actual: $shape + ); + $this->assertSame( + expected: array_keys($schemas), + actual: self::read(path: self::DESCRIPTOR_FILE)['components']['registers']['public-space']['schemas'] + ); + }//end testTheSchemasCarryTheCatalogueColumns() + + /** + * Every object is in a declared schema, fills its required fields, has a + * status the schema allows and an identification no other object uses. + * + * @return void + */ + public function testEveryObjectFitsItsSchema(): void { + $schemas = self::read(path: self::DESCRIPTOR_FILE)['components']['schemas']; + $objects = array_values( + array_filter( + self::read(path: self::CAST_FILE)['objects'], + static fn (array $object): bool => $object['register'] === 'public-space' + ) + ); + + $this->assertCount(expectedCount: 13, haystack: $objects); + foreach ($objects as $object) { + $schema = ($schemas[$object['schema']] ?? null); + $this->assertNotNull(actual: $schema, message: $object['key']); + $this->assertSame(expected: [], actual: array_diff($schema['required'], array_keys($object['data'])), message: $object['key']); + $this->assertSame(expected: [], actual: array_diff(array_keys($object['data']), array_keys($schema['properties'])), message: $object['key']); + $this->assertContains(needle: $object['data']['status'], haystack: $schema['properties']['status']['enum'], message: $object['key']); + } + + $identifications = array_map(static fn (array $object): string => $object['data']['identification'], $objects); + $this->assertSame(expected: $identifications, actual: array_values(array_unique($identifications))); + }//end testEveryObjectFitsItsSchema() + + /** + * The three objects the Lindelaan Woo case links: the cycle path in + * management and two masts still to be placed. + * + * @return void + */ + public function testTheLindelaanObjectsCarryTheirStatus(): void { + $byIdentification = []; + foreach (self::read(path: self::CAST_FILE)['objects'] as $object) { + if ($object['register'] === 'public-space') { + $byIdentification[$object['data']['identification']] = [$object['data']['name'], $object['data']['status']]; + } + } + + $this->assertSame( + expected: [['Fietspad Lindelaan', 'In beheer'], ['Lichtmast 14', 'Gepland'], ['Lichtmast 15', 'Gepland']], + actual: [$byIdentification['BGT wgl.0505.1234'], $byIdentification['LM-0014'], $byIdentification['LM-0015']] + ); + }//end testTheLindelaanObjectsCarryTheirStatus() +}//end class