Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
82bd02a
feat(case-migration): case migration plans with triggers, conditions …
Klaas-Ritense Jul 7, 2026
59ec674
WIP
Klaas-Ritense Jul 9, 2026
92c67cb
WIP
Klaas-Ritense Jul 13, 2026
6186544
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Jul 13, 2026
1f1cd77
WIP
Klaas-Ritense Jul 13, 2026
2ad4b6d
fixes
Klaas-Ritense Jul 20, 2026
f6e47ec
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Jul 20, 2026
f23b281
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Jul 21, 2026
47b5357
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Jul 23, 2026
2cf4a87
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Jul 23, 2026
c37384a
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Jul 24, 2026
fa93e4b
Add dry run + bug fixes
Klaas-Ritense Jul 28, 2026
0923a6e
Add support for doc: null handling
Klaas-Ritense Jul 29, 2026
e9203a0
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Aug 5, 2026
b9f9de0
Fix
Klaas-Ritense Aug 5, 2026
bb72b98
Fix
Klaas-Ritense Aug 5, 2026
e8fbf73
Add default values
Klaas-Ritense Aug 5, 2026
59d072f
Fixes
Klaas-Ritense Aug 7, 2026
f5cb014
Fixes
Klaas-Ritense Aug 11, 2026
3a3cb61
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Aug 11, 2026
49d4fb2
Fixes
Klaas-Ritense Aug 11, 2026
d517aff
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Aug 11, 2026
31e7869
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Aug 11, 2026
fd7d6d7
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Aug 12, 2026
57e515e
Add fixtures
Klaas-Ritense Aug 13, 2026
023cbfd
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Aug 14, 2026
5dba64a
Merge remote-tracking branch 'origin/next-minor' into poc/case-migration
Klaas-Ritense Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
135 changes: 135 additions & 0 deletions backend/apps/dev/src/main/kotlin/com/ritense/gzac/DemoDataService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.gzac

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.readValue
import com.ritense.authorization.AuthorizationContext.Companion.runWithoutAuthorization
import com.ritense.document.domain.impl.request.NewDocumentRequest
import com.ritense.document.repository.impl.JsonSchemaDocumentRepository
import com.ritense.document.repository.impl.specification.JsonSchemaDocumentSpecificationHelper.Companion.byDocumentDefinitionIdCaseDefinitionId
import com.ritense.processdocument.domain.impl.request.NewDocumentAndStartProcessRequest
import com.ritense.processdocument.service.ProcessDocumentService
import com.ritense.valtimo.contract.case_.CaseDefinitionId
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.core.io.Resource
import org.springframework.core.io.ResourceLoader
import org.springframework.core.io.support.ResourcePatternUtils
import org.springframework.stereotype.Component

/**
* Generic dev-only seeder for demo cases. Everything is derived from the folder layout, mirroring
* the `resources/config/` convention (e.g. `config/building-block/verhuizing-inspectie/1-0-1/bpmn`):
*
* ```
* data/<caseKey>/<version>/documents/<name>.json
* ```
*
* - `<caseKey>` — used as the case definition key, process definition key and document definition
* name (matching the shipped case definitions, where all three are identical).
* - `<version>` — the case definition version to seed the documents on, written dash-separated like
* the config folders (`1-0-0` becomes version tag `1.0.0`). A document can be created directly on
* any deployed version, so every version folder is seeded independently.
*
* Adding a new demo case or version therefore requires no code change — just drop in a folder.
*
* Idempotent per case version: only seeds a `<caseKey>/<version>` when no document exists yet for
* that exact case definition version, and never fails startup.
*/
@Component
class DemoDataService(
private val objectMapper: ObjectMapper,
private val processDocumentService: ProcessDocumentService,
private val resourceLoader: ResourceLoader,
private val jsonSchemaDocumentRepository: JsonSchemaDocumentRepository,
) {
/** Discovers and seeds every demo case folder under `data/`. Invoked once at startup. */
fun deployDemoData() {
discoverDemoCases().forEach { seedCase(it) }
}

/** Groups every demo document resource by the case it belongs to, deriving key and version from the path. */
private fun discoverDemoCases(): List<DemoCase> =
ResourcePatternUtils
.getResourcePatternResolver(resourceLoader)
.getResources("classpath*:data/*/*/documents/*.json")
.mapNotNull { resource -> demoCaseKeyOf(resource)?.let { it to resource } }
.groupBy({ it.first }, { it.second })
.map { (key, documents) -> DemoCase(key.caseKey, key.versionTag, documents) }

private fun seedCase(demoCase: DemoCase) {
try {
runWithoutAuthorization {
val caseDefinitionId = CaseDefinitionId.of(demoCase.caseKey, demoCase.versionTag)
val alreadySeeded = jsonSchemaDocumentRepository
.count(byDocumentDefinitionIdCaseDefinitionId(caseDefinitionId)) > 0
if (alreadySeeded) {
return@runWithoutAuthorization
}

demoCase.documents.forEach { resource ->
try {
val content = resource.inputStream.bufferedReader().use { it.readText() }
val documentData = objectMapper.readValue<ObjectNode>(content)

val newDocumentRequest = NewDocumentRequest(
demoCase.caseKey,
demoCase.caseKey,
demoCase.versionTag,
documentData,
)
processDocumentService.newDocumentAndStartProcess(
NewDocumentAndStartProcessRequest(demoCase.caseKey, newDocumentRequest),
)
} catch (e: Exception) {
logger.warn(e) { "Failed to seed ${demoCase.caseKey} ${demoCase.versionTag} document '${resource.filename}'" }
}
}
logger.info { "Seeded ${demoCase.documents.size} ${demoCase.caseKey} demo cases on version ${demoCase.versionTag}" }
}
} catch (e: Exception) {
logger.warn(e) { "Failed to seed ${demoCase.caseKey} ${demoCase.versionTag} demo data" }
}
}

/**
* Extracts the case key and version from a `.../data/<caseKey>/<version>/documents/<file>.json`
* resource URI, converting the dash-separated version folder to a dotted version tag.
*/
private fun demoCaseKeyOf(resource: Resource): DemoCaseKey? =
DATA_FOLDER_REGEX.find(resource.uri.toString())?.let { match ->
val (caseKey, versionFolder) = match.destructured
DemoCaseKey(caseKey, versionFolder.replace('-', '.'))
}

private data class DemoCaseKey(
val caseKey: String,
val versionTag: String,
)

private data class DemoCase(
val caseKey: String,
val versionTag: String,
val documents: List<Resource>,
)

companion object {
private val logger = KotlinLogging.logger {}
private val DATA_FOLDER_REGEX = Regex("""/data/([^/]+)/([^/]+)/documents/""")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.springframework.boot.runApplication
import org.springframework.core.env.Environment
import org.springframework.scheduling.annotation.EnableScheduling
import java.net.InetAddress
import org.springframework.beans.factory.getBean

@SpringBootApplication
@EnableScheduling
Expand All @@ -49,9 +50,13 @@ fun main(args: Array<String>) {
""".trimIndent()
}

// Development-only demo data for the "Present images as thumbnails" (GH-289) feature.
// Runs after the context is fully initialised so the bezwaar case definition is deployed.
// Development-only demo data. Runs after the context is fully initialised so the required case
// definitions are deployed first:
// - BezwaarImageDemoDataService: "Present images as thumbnails" (GH-289) feature.
// - DemoDataService: generically seeds every demo case folder under resources/data/ on its
// source version so cases can be migrated via the shipped migration plans.
if (environment.activeProfiles.contains("dev")) {
applicationContext.getBean(BezwaarImageDemoDataService::class.java).deployBezwaarWithImages()
applicationContext.getBean<BezwaarImageDemoDataService>().deployBezwaarWithImages()
applicationContext.getBean<DemoDataService>().deployDemoData()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_InspectieDossier" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.28.0">
<bpmn:collaboration id="Collaboration_InspectieDossier">
<bpmn:participant id="InspectieDossierParticipant" name="Inspectiedossier" processRef="inspectie-dossier-process" />
</bpmn:collaboration>
<bpmn:process id="inspectie-dossier-process" name="Inspectiedossier proces" isExecutable="true" camunda:historyTimeToLive="180">
<bpmn:startEvent id="StartEvent" name="Start dossier">
<bpmn:outgoing>Flow_Start_To_Vaststellen</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="DossierVaststellenTask" name="Dossier vaststellen">
<bpmn:incoming>Flow_Start_To_Vaststellen</bpmn:incoming>
<bpmn:outgoing>Flow_Vaststellen_To_Kwaliteit</bpmn:outgoing>
</bpmn:userTask>
<bpmn:serviceTask id="RegistreerDossierstatusTask" name="Registreer dossierstatus" camunda:expression="${valueResolverDelegateService.handleValue(execution, 'doc:/dossierStatus', 'vastgesteld')}">
<bpmn:incoming>Flow_Vaststellen_To_Kwaliteit</bpmn:incoming>
<bpmn:outgoing>Flow_Kwaliteit_To_End</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:endEvent id="EndEvent" name="Dossier afgerond">
<bpmn:incoming>Flow_Kwaliteit_To_End</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_Start_To_Vaststellen" sourceRef="StartEvent" targetRef="DossierVaststellenTask" />
<bpmn:sequenceFlow id="Flow_Vaststellen_To_Kwaliteit" sourceRef="DossierVaststellenTask" targetRef="RegistreerDossierstatusTask" />
<bpmn:sequenceFlow id="Flow_Kwaliteit_To_End" sourceRef="RegistreerDossierstatusTask" targetRef="EndEvent" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_InspectieDossier">
<bpmndi:BPMNShape id="InspectieDossierParticipant_di" bpmnElement="InspectieDossierParticipant" isHorizontal="true">
<dc:Bounds x="160" y="60" width="620" height="200" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_di" bpmnElement="StartEvent">
<dc:Bounds x="212" y="132" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="203" y="175" width="55" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="DossierVaststellenTask_di" bpmnElement="DossierVaststellenTask">
<dc:Bounds x="300" y="110" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="RegistreerDossierstatusTask_di" bpmnElement="RegistreerDossierstatusTask">
<dc:Bounds x="460" y="110" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_di" bpmnElement="EndEvent">
<dc:Bounds x="622" y="132" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="603" y="175" width="75" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_Start_To_Vaststellen_di" bpmnElement="Flow_Start_To_Vaststellen">
<di:waypoint x="248" y="150" />
<di:waypoint x="300" y="150" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_Vaststellen_To_Kwaliteit_di" bpmnElement="Flow_Vaststellen_To_Kwaliteit">
<di:waypoint x="400" y="150" />
<di:waypoint x="460" y="150" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_Kwaliteit_To_End_di" bpmnElement="Flow_Kwaliteit_To_End">
<di:waypoint x="560" y="150" />
<di:waypoint x="622" y="150" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"title": "Fotobouwsteen wordt dossierbouwsteen",
"key": "inspectie-dossier-uit-fotos",
"source": {
"key": "inspectie-fotos",
"versionTag": "1.0.1"
},
"dataMigration": [
{
"target": "doc:/aanvragerNaam",
"source": "doc:/aanvragerNaam"
},
{
"target": "doc:/dossierStatus",
"source": "doc:/fotoStatus"
},
{
"target": "doc:/aantalBijlagen",
"source": "doc:/aantalFotos",
"targetType": "integer"
},
{
"target": "doc:/toelichting",
"source": "doc:/toelichting"
},
{
"target": "doc:/dossierKwaliteit",
"source": "doc:/fotoKwaliteit"
},
{
"target": "doc:/gemigreerdVan",
"value": "inspectie-fotos:1.0.1",
"targetType": "string"
}
],
"processMigration": [
{
"sourceProcessDefinitionKey": "inspectie-fotos-process",
"targetProcessDefinitionKey": "inspectie-dossier-process",
"mapActivities": {
"FotosControlerenTask": "DossierVaststellenTask",
"RegistreerKwaliteitTask": "RegistreerDossierstatusTask"
},
"setProcessVariables": [
{
"target": "pv:dossierGemigreerdVan",
"value": "inspectie-fotos:1.0.1",
"targetType": "string"
}
],
"skipCustomListeners": false,
"skipIoMappings": false
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"processDefinitionKey": "inspectie-dossier-process"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"key": "inspectie-dossier",
"name": "Inspectiedossier",
"description": "Vervangt de bouwsteen inspectie-fotos. Let op: deze bouwsteen heeft GEEN basedOnVersionTag — hij is geen nieuwe versie van inspectie-fotos maar een andere bouwsteen. Alleen het migratieplan op deze versie (met source inspectie-fotos:1.0.1) verbindt de twee, en dat plan is dus de enige manier waarop een lopende foto-bouwsteen hier terechtkomt.",
"versionTag": "1.0.0",
"final": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$id": "inspectie-dossier.schema",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Inspectiedossier",
"type": "object",
"properties": {
"aanvragerNaam": {
"type": "string",
"description": "Naam van de aanvrager, overgenomen uit de inspectiebouwsteen"
},
"dossierStatus": {
"type": "string",
"description": "Status van het inspectiedossier (opvolger van fotoStatus)"
},
"aantalBijlagen": {
"type": "integer",
"description": "Aantal bijlagen in het dossier (opvolger van aantalFotos)"
},
"toelichting": {
"type": ["string", "null"],
"description": "Toelichting bij het dossier"
},
"dossierKwaliteit": {
"type": "string",
"description": "Beoordeling van de dossierkwaliteit"
},
"gemigreerdVan": {
"type": "string",
"description": "Bouwsteen en versie waarvandaan dit dossier is gemigreerd"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"display": "form",
"components": [
{
"key": "html",
"type": "htmlelement",
"input": false,
"label": "Dossier vaststellen",
"content": "Controleer het inspectiedossier en stel het vast.",
"tableView": false,
"refreshOnChange": false
},
{
"key": "aantalBijlagen",
"type": "number",
"input": true,
"label": "Aantal bijlagen",
"tableView": true,
"defaultValue": 3
},
{
"key": "dossierKwaliteit",
"type": "select",
"input": true,
"label": "Dossierkwaliteit",
"tableView": true,
"data": {
"values": [
{
"label": "Goed",
"value": "goed"
},
{
"label": "Onvoldoende",
"value": "onvoldoende"
}
]
},
"dataSrc": "values",
"defaultValue": "goed"
},
{
"key": "toelichting",
"type": "textfield",
"input": true,
"label": "Toelichting",
"tableView": true,
"applyMaskOn": "change"
},
{
"key": "submit",
"type": "button",
"input": true,
"label": "Dossier vaststellen",
"tableView": false,
"disableOnInvalid": true
}
]
}
Loading
Loading