Repository: infonl/dimpact-zaakafhandelcomponent
Pull request #3392: chore: convert notitie backend classes to Kotlin
Author: login: edgarvonk
Created: 2025-05-19T10:34:04Z | Merged: 2025-05-19T11:49:36Z

Converted notitie backend classes to Kotlin.

Solves PZ-6850


Diff:
diff --git a/src/itest/kotlin/nl/info/zac/itest/NoteServiceTest.kt b/src/itest/kotlin/nl/info/zac/itest/NoteServiceTest.kt
new file mode 100644
index 0000000000..c551282840
--- /dev/null
+++ b/src/itest/kotlin/nl/info/zac/itest/NoteServiceTest.kt
@@ -0,0 +1,79 @@
+/*
+ * SPDX-FileCopyrightText: 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.itest
+
+import io.kotest.core.spec.Order
+import io.kotest.core.spec.style.BehaviorSpec
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+import nl.info.zac.itest.client.ItestHttpClient
+import nl.info.zac.itest.config.ItestConfiguration.TEST_SPEC_ORDER_AFTER_ZAAK_CREATED
+import nl.info.zac.itest.config.ItestConfiguration.TEST_USER_1_NAME
+import nl.info.zac.itest.config.ItestConfiguration.TEST_USER_1_USERNAME
+import nl.info.zac.itest.config.ItestConfiguration.ZAC_API_URI
+import nl.info.zac.itest.config.ItestConfiguration.zaakProductaanvraag1Uuid
+import nl.info.zac.itest.util.shouldEqualJsonIgnoringExtraneousFields
+import org.json.JSONObject
+
+@Order(TEST_SPEC_ORDER_AFTER_ZAAK_CREATED)
+class NoteServiceTest : BehaviorSpec({
+    val itestHttpClient = ItestHttpClient()
+
+    Given("An existing zaak") {
+        When("the 'create note' endpoint is called") {
+            val response = itestHttpClient.performJSONPostRequest(
+                url = "$ZAC_API_URI/notities",
+                requestBodyAsString = """
+                    {
+                        "zaakUUID": "$zaakProductaanvraag1Uuid",
+                        "tekst": "fakeNoteText",
+                        "gebruikersnaamMedewerker": "$TEST_USER_1_USERNAME"
+                    }
+                """.trimIndent()
+            )
+            Then(
+                "the created note should be returned"
+            ) {
+                response.isSuccessful shouldBe true
+                val responseBody = response.body!!.string()
+                responseBody shouldEqualJsonIgnoringExtraneousFields """
+                    {
+                        "zaakUUID": "$zaakProductaanvraag1Uuid",
+                        "tekst": "fakeNoteText",
+                        "gebruikersnaamMedewerker": "$TEST_USER_1_USERNAME",
+                        "voornaamAchternaamMedewerker": "$TEST_USER_1_NAME",
+                        "bewerkenToegestaan": true
+                    }
+                """.trimIndent()
+                with(JSONObject(responseBody)) {
+                    get("id") shouldNotBe null
+                    get("tijdstipLaatsteWijziging") shouldNotBe null
+                }
+            }
+        }
+        When("the 'get notes' endpoint is called") {
+            val response = itestHttpClient.performGetRequest(
+                url = "$ZAC_API_URI/notities/ZAAK/$zaakProductaanvraag1Uuid"
+            )
+            Then(
+                "the just created note should be returned"
+            ) {
+                response.isSuccessful shouldBe true
+                val responseBody = response.body!!.string()
+                responseBody shouldEqualJsonIgnoringExtraneousFields """
+                    [
+                        {
+                            "zaakUUID": "$zaakProductaanvraag1Uuid",
+                            "tekst": "fakeNoteText",
+                            "gebruikersnaamMedewerker": "$TEST_USER_1_USERNAME",
+                            "voornaamAchternaamMedewerker": "$TEST_USER_1_NAME",
+                            "bewerkenToegestaan": true
+                        }
+                    ]
+                """.trimIndent()
+            }
+        }
+    }
+})
diff --git a/src/main/java/net/atos/zac/app/notities/NotitiesRESTService.java b/src/main/java/net/atos/zac/app/notities/NotitiesRESTService.java
deleted file mode 100644
index 156a085b2b..0000000000
--- a/src/main/java/net/atos/zac/app/notities/NotitiesRESTService.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2021 Atos
- * SPDX-License-Identifier: EUPL-1.2+
- */
-
-package net.atos.zac.app.notities;
-
-import java.util.List;
-import java.util.UUID;
-import java.util.stream.Collectors;
-
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.DELETE;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.PATCH;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.core.MediaType;
-
-import net.atos.zac.app.notities.converter.NotitieConverter;
-import net.atos.zac.app.notities.model.RESTNotitie;
-import net.atos.zac.notities.NotitieService;
-import net.atos.zac.notities.model.Notitie;
-
-@Singleton
-@Path("notities")
-@Consumes(MediaType.APPLICATION_JSON)
-@Produces(MediaType.APPLICATION_JSON)
-public class NotitiesRESTService {
-
-    @Inject
-    private NotitieService notitieService;
-
-    @Inject
-    private NotitieConverter notitieConverter;
-
-    @GET
-    @Path("{type}/{uuid}")
-    public List<RESTNotitie> listNotities(@PathParam("type") final String type, @PathParam("uuid") final String uuid) {
-        final UUID notitieUUID = UUID.fromString(uuid);
-        return notitieService.listNotitiesForZaak(notitieUUID).stream()
-                .map(notitieConverter::convertToRESTNotitie)
-                .collect(Collectors.toList());
-    }
-
-    @POST
-    public RESTNotitie createNotitie(final RESTNotitie restNotitie) {
-        final Notitie notitie = notitieConverter.convertToNotitie(restNotitie);
-        return notitieConverter.convertToRESTNotitie(notitieService.createNotitie(notitie));
-    }
-
-    @PATCH
-    public RESTNotitie updateNotitie(final RESTNotitie restNotitie) {
-        final Notitie notitie = notitieConverter.convertToNotitie(restNotitie);
-        return notitieConverter.convertToRESTNotitie(notitieService.updateNotitie(notitie));
-    }
-
-    @DELETE
-    @Path("{id}")
-    public void deleteNotitie(@PathParam("id") final Long id) {
-        notitieService.deleteNotitie(id);
-    }
-}
diff --git a/src/main/java/net/atos/zac/app/notities/converter/NotitieConverter.java b/src/main/java/net/atos/zac/app/notities/converter/NotitieConverter.java
deleted file mode 100644
index e29f9be493..0000000000
--- a/src/main/java/net/atos/zac/app/notities/converter/NotitieConverter.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2021 Atos
- * SPDX-License-Identifier: EUPL-1.2+
- */
-
-package net.atos.zac.app.notities.converter;
-
-import java.time.ZonedDateTime;
-
-import jakarta.enterprise.inject.Instance;
-import jakarta.inject.Inject;
-
-import net.atos.zac.app.notities.model.RESTNotitie;
-import net.atos.zac.notities.model.Notitie;
-import nl.info.zac.authentication.LoggedInUser;
-import nl.info.zac.identity.IdentityService;
-import nl.info.zac.identity.model.User;
-
-public class NotitieConverter {
-
-    @Inject
-    private IdentityService identityService;
-
-    @Inject
-    private Instance<LoggedInUser> loggedInUserInstance;
-
-    public RESTNotitie convertToRESTNotitie(final Notitie notitie) {
-        final RESTNotitie restNotitie = new RESTNotitie();
-        restNotitie.id = notitie.getId();
-        restNotitie.zaakUUID = notitie.getZaakUUID();
-        restNotitie.tekst = notitie.getTekst();
-        restNotitie.tijdstipLaatsteWijziging = notitie.getTijdstipLaatsteWijziging();
-        restNotitie.gebruikersnaamMedewerker = notitie.getGebruikersnaamMedewerker();
-        final User medewerker = identityService.readUser(notitie.getGebruikersnaamMedewerker());
-        restNotitie.voornaamAchternaamMedewerker = String.format("%s %s", medewerker.getFirstName(), medewerker.getLastName());
-        restNotitie.bewerkenToegestaan = loggedInUserInstance.get().getId().equals(notitie.getGebruikersnaamMedewerker());
-        return restNotitie;
-    }
-
-    public static Notitie convertToNotitie(final RESTNotitie restNotitie) {
-        final Notitie notitie = new Notitie();
-        notitie.setId(restNotitie.id);
-        notitie.setZaakUUID(restNotitie.zaakUUID);
-        notitie.setTekst(restNotitie.tekst);
-        notitie.setTijdstipLaatsteWijziging(ZonedDateTime.now());
-        notitie.setGebruikersnaamMedewerker(restNotitie.gebruikersnaamMedewerker);
-        return notitie;
-    }
-}
diff --git a/src/main/java/net/atos/zac/app/notities/model/RESTNotitie.java b/src/main/java/net/atos/zac/app/notities/model/RESTNotitie.java
deleted file mode 100644
index bc82a761c1..0000000000
--- a/src/main/java/net/atos/zac/app/notities/model/RESTNotitie.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2021 Atos
- * SPDX-License-Identifier: EUPL-1.2+
- */
-
-package net.atos.zac.app.notities.model;
-
-import java.time.ZonedDateTime;
-import java.util.UUID;
-
-public class RESTNotitie {
-
-    public Long id;
-
-    public UUID zaakUUID;
-
-    public String tekst;
-
-    public ZonedDateTime tijdstipLaatsteWijziging;
-
-    public String gebruikersnaamMedewerker;
-
-    public String voornaamAchternaamMedewerker;
-
-    public boolean bewerkenToegestaan;
-
-}
diff --git a/src/main/java/net/atos/zac/notities/NotitieService.java b/src/main/java/net/atos/zac/notities/NotitieService.java
deleted file mode 100644
index ce778c6290..0000000000
--- a/src/main/java/net/atos/zac/notities/NotitieService.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2021 Atos
- * SPDX-License-Identifier: EUPL-1.2+
- */
-
-package net.atos.zac.notities;
-
-import static net.atos.zac.notities.model.Notitie.ZAAK_UUID;
-import static net.atos.zac.util.ValidationUtil.valideerObject;
-
-import java.util.List;
-import java.util.UUID;
-
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.PersistenceContext;
-import jakarta.persistence.criteria.CriteriaBuilder;
-import jakarta.persistence.criteria.CriteriaQuery;
-import jakarta.persistence.criteria.Root;
-import jakarta.transaction.Transactional;
-
-import net.atos.zac.notities.model.Notitie;
-
-@ApplicationScoped
-@Transactional
-public class NotitieService {
-
-    @PersistenceContext(unitName = "ZaakafhandelcomponentPU")
-    private EntityManager entityManager;
-
-    public Notitie createNotitie(final Notitie notitie) {
-        valideerObject(notitie);
-        entityManager.persist(notitie);
-        return notitie;
-    }
-
-    public List<Notitie> listNotitiesForZaak(final UUID zaakUUID) {
-        final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
-        final CriteriaQuery<Notitie> query = builder.createQuery(Notitie.class);
-        final Root<Notitie> root = query.from(Notitie.class);
-        query.select(root).where(builder.equal(root.get(ZAAK_UUID), zaakUUID));
-        return entityManager.createQuery(query).getResultList();
-    }
-
-    public Notitie updateNotitie(final Notitie notitie) {
-        valideerObject(notitie);
-        return entityManager.merge(notitie);
-    }
-
-    public void deleteNotitie(final Long notitieId) {
-        final Notitie notitie = entityManager.find(Notitie.class, notitieId);
-        entityManager.remove(notitie);
-    }
-}
diff --git a/src/main/java/net/atos/zac/notities/model/Notitie.java b/src/main/java/net/atos/zac/notities/model/Notitie.java
deleted file mode 100644
index 38cfbea462..0000000000
--- a/src/main/java/net/atos/zac/notities/model/Notitie.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2021 Atos
- * SPDX-License-Identifier: EUPL-1.2+
- */
-
-package net.atos.zac.notities.model;
-
-import static net.atos.zac.util.FlywayIntegrator.SCHEMA;
-
-import java.time.ZonedDateTime;
-import java.util.UUID;
-
-import jakarta.persistence.Basic;
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.SequenceGenerator;
-import jakarta.persistence.Table;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
-
-/**
- * Een Notitie kan worden gekoppeld aan een zaak.
- */
-@Entity
-@Table(schema = SCHEMA, name = "notitie")
-@SequenceGenerator(schema = SCHEMA, name = "notitie_sq", sequenceName = "notitie_sq", allocationSize = 1)
-public class Notitie {
-
-    /**
-     * Naam van property: {@link Notitie#zaakUUID}
-     */
-    public static final String ZAAK_UUID = "zaakUUID";
-
-    @Id
-    @GeneratedValue(generator = "notitie_sq", strategy = GenerationType.SEQUENCE)
-    private Long id;
-
-    @Basic
-    @Column(name = "zaak_uuid", updatable = false)
-    private UUID zaakUUID;
-
-    @NotBlank @Column(nullable = false)
-    private String tekst;
-
-    @NotNull @Column(name = "tijdstip_laatste_wijziging", nullable = false)
-    private ZonedDateTime tijdstipLaatsteWijziging;
-
-    @NotBlank @Column(name = "gebruikersnaam_medewerker", nullable = false, updatable = true)
-    private String gebruikersnaamMedewerker;
-
-    public Long getId() {
-        return id;
-    }
-
-    public void setId(final Long id) {
-        this.id = id;
-    }
-
-    public UUID getZaakUUID() {
-        return zaakUUID;
-    }
-
-    public void setZaakUUID(final UUID zaakUUID) {
-        this.zaakUUID = zaakUUID;
-    }
-
-    public String getTekst() {
-        return tekst;
-    }
-
-    public void setTekst(final String text) {
-        this.tekst = text;
-    }
-
-    public String getGebruikersnaamMedewerker() {
-        return gebruikersnaamMedewerker;
-    }
-
-    public void setGebruikersnaamMedewerker(final String gebruikersnaamMedewerker) {
-        this.gebruikersnaamMedewerker = gebruikersnaamMedewerker;
-    }
-
-    public void setTijdstipLaatsteWijziging(final ZonedDateTime tijdstipLaatsteWijziging) {
-        this.tijdstipLaatsteWijziging = tijdstipLaatsteWijziging;
-    }
-
-    public ZonedDateTime getTijdstipLaatsteWijziging() {
-        return tijdstipLaatsteWijziging;
-    }
-
-}
diff --git a/src/main/kotlin/nl/info/zac/app/note/NoteRestService.kt b/src/main/kotlin/nl/info/zac/app/note/NoteRestService.kt
new file mode 100644
index 0000000000..e0daeeafcf
--- /dev/null
+++ b/src/main/kotlin/nl/info/zac/app/note/NoteRestService.kt
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Atos, 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.app.note
+
+import jakarta.inject.Inject
+import jakarta.inject.Singleton
+import jakarta.ws.rs.Consumes
+import jakarta.ws.rs.DELETE
+import jakarta.ws.rs.GET
+import jakarta.ws.rs.PATCH
+import jakarta.ws.rs.POST
+import jakarta.ws.rs.Path
+import jakarta.ws.rs.PathParam
+import jakarta.ws.rs.Produces
+import jakarta.ws.rs.core.MediaType
+import nl.info.zac.app.note.converter.NoteConverter
+import nl.info.zac.app.note.model.RestNote
+import nl.info.zac.app.note.model.toNote
+import nl.info.zac.note.NoteService
+import nl.info.zac.util.AllOpen
+import nl.info.zac.util.NoArgConstructor
+import java.util.UUID
+
+@Singleton
+@Path("notities")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+@NoArgConstructor
+@AllOpen
+class NoteRestService @Inject constructor(
+    private val noteService: NoteService,
+    private val noteConverter: NoteConverter
+) {
+    /**
+     * List notes for a given case UUID.
+     * The `type' path parameter is not used currently.
+     */
+    @GET
+    @Path("{type}/{uuid}")
+    fun listNotes(@PathParam("uuid") zaakUUID: UUID): List<RestNote> =
+        noteService.listNotesForZaak(zaakUUID)
+            .map(noteConverter::toRestNote)
+
+    @POST
+    fun createNote(restNote: RestNote): RestNote {
+        val notitie = noteService.createNote(restNote.toNote())
+        return noteConverter.toRestNote(notitie)
+    }
+
+    @PATCH
+    fun updateNote(restNote: RestNote): RestNote {
+        val updatedNotitie = noteService.updateNote(restNote.toNote())
+        return noteConverter.toRestNote(updatedNotitie)
+    }
+
+    @DELETE
+    @Path("{id}")
+    fun deleteNote(@PathParam("id") id: Long) =
+        noteService.deleteNote(id)
+}
diff --git a/src/main/kotlin/nl/info/zac/app/note/converter/NoteConverter.kt b/src/main/kotlin/nl/info/zac/app/note/converter/NoteConverter.kt
new file mode 100644
index 0000000000..6ffbb29c53
--- /dev/null
+++ b/src/main/kotlin/nl/info/zac/app/note/converter/NoteConverter.kt
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Atos, 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.app.note.converter
+
+import jakarta.enterprise.inject.Instance
+import jakarta.inject.Inject
+import nl.info.zac.app.note.model.RestNote
+import nl.info.zac.authentication.LoggedInUser
+import nl.info.zac.identity.IdentityService
+import nl.info.zac.note.model.Note
+
+class NoteConverter @Inject constructor(
+    private val identityService: IdentityService,
+    private val loggedInUserInstance: Instance<LoggedInUser>
+) {
+    fun toRestNote(note: Note): RestNote {
+        val medewerker = identityService.readUser(note.employeeUsername)
+        return RestNote(
+            id = note.id,
+            zaakUUID = note.zaakUUID,
+            text = note.text,
+            dateTimeLastModified = note.dateTimeLastModified,
+            employeeUsername = note.employeeUsername,
+            employeeFullname = "${medewerker.firstName} ${medewerker.lastName}",
+            // updating a note is only allowed if the logged-in user is the same as the employee who created the note
+            updatingAllowed = loggedInUserInstance.get().id == note.employeeUsername
+        )
+    }
+}
diff --git a/src/main/kotlin/nl/info/zac/app/note/model/RestNote.kt b/src/main/kotlin/nl/info/zac/app/note/model/RestNote.kt
new file mode 100644
index 0000000000..86ef5083cf
--- /dev/null
+++ b/src/main/kotlin/nl/info/zac/app/note/model/RestNote.kt
@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Atos, 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.app.note.model
+
+import jakarta.json.bind.annotation.JsonbProperty
+import nl.info.zac.note.model.Note
+import nl.info.zac.util.AllOpen
+import nl.info.zac.util.NoArgConstructor
+import java.time.ZonedDateTime
+import java.util.UUID
+
+@NoArgConstructor
+@AllOpen
+data class RestNote(
+    var id: Long?,
+    var zaakUUID: UUID,
+
+    @field:JsonbProperty("tekst")
+    var text: String,
+
+    /**
+     * Date-time when the note was last modified.
+     */
+    @field:JsonbProperty("tijdstipLaatsteWijziging")
+    var dateTimeLastModified: ZonedDateTime?,
+
+    @field:JsonbProperty("gebruikersnaamMedewerker")
+    var employeeUsername: String,
+
+    @field:JsonbProperty("voornaamAchternaamMedewerker")
+    var employeeFullname: String?,
+
+    @field:JsonbProperty("bewerkenToegestaan")
+    var updatingAllowed: Boolean = false
+)
+
+fun RestNote.toNote() = Note().apply {
+    id = this@toNote.id ?: 0
+    zaakUUID = this@toNote.zaakUUID
+    text = this@toNote.text
+    dateTimeLastModified = ZonedDateTime.now()
+    employeeUsername = this@toNote.employeeUsername
+}
diff --git a/src/main/kotlin/nl/info/zac/note/NoteService.kt b/src/main/kotlin/nl/info/zac/note/NoteService.kt
new file mode 100644
index 0000000000..0dc664acfa
--- /dev/null
+++ b/src/main/kotlin/nl/info/zac/note/NoteService.kt
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Atos, 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.note
+
+import jakarta.enterprise.context.ApplicationScoped
+import jakarta.inject.Inject
+import jakarta.persistence.EntityManager
+import jakarta.transaction.Transactional
+import jakarta.transaction.Transactional.TxType.REQUIRED
+import jakarta.transaction.Transactional.TxType.SUPPORTS
+import net.atos.zac.util.ValidationUtil.valideerObject
+import nl.info.zac.note.model.Note
+import nl.info.zac.note.model.Note.Companion.ZAAK_UUID_FIELD
+import nl.info.zac.util.AllOpen
+import nl.info.zac.util.NoArgConstructor
+import java.util.UUID
+
+@ApplicationScoped
+@Transactional(SUPPORTS)
+@NoArgConstructor
+@AllOpen
+class NoteService @Inject constructor(
+    private val entityManager: EntityManager
+) {
+    @Transactional(REQUIRED)
+    fun createNote(note: Note): Note {
+        valideerObject(note)
+        entityManager.persist(note)
+        return note
+    }
+
+    fun listNotesForZaak(zaakUUID: UUID): List<Note> {
+        val builder = entityManager.criteriaBuilder
+        val query = builder.createQuery(Note::class.java)
+        val root = query.from(Note::class.java)
+        query.select(root).where(builder.equal(root.get<Any>(ZAAK_UUID_FIELD), zaakUUID))
+        return entityManager.createQuery(query).getResultList()
+    }
+
+    @Transactional(REQUIRED)
+    fun updateNote(note: Note): Note {
+        valideerObject(note)
+        return entityManager.merge(note)
+    }
+
+    @Transactional(REQUIRED)
+    fun deleteNote(notitieId: Long) {
+        val note = entityManager.find(Note::class.java, notitieId)
+        entityManager.remove(note)
+    }
+}
diff --git a/src/main/kotlin/nl/info/zac/note/model/Note.kt b/src/main/kotlin/nl/info/zac/note/model/Note.kt
new file mode 100644
index 0000000000..ee503cac0f
--- /dev/null
+++ b/src/main/kotlin/nl/info/zac/note/model/Note.kt
@@ -0,0 +1,56 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Atos, 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.note.model
+
+import jakarta.persistence.Basic
+import jakarta.persistence.Column
+import jakarta.persistence.Entity
+import jakarta.persistence.GeneratedValue
+import jakarta.persistence.GenerationType
+import jakarta.persistence.Id
+import jakarta.persistence.SequenceGenerator
+import jakarta.persistence.Table
+import jakarta.validation.constraints.NotBlank
+import net.atos.zac.util.FlywayIntegrator
+import nl.info.zac.util.AllOpen
+import java.time.ZonedDateTime
+import java.util.UUID
+
+/**
+ * Een Notitie kan worden gekoppeld aan een zaak.
+ */
+@Entity
+@Table(schema = FlywayIntegrator.SCHEMA, name = "notitie")
+@SequenceGenerator(
+    schema = FlywayIntegrator.SCHEMA,
+    name = "notitie_sq",
+    sequenceName = "notitie_sq",
+    allocationSize = 1
+)
+@AllOpen
+class Note {
+    companion object {
+        const val ZAAK_UUID_FIELD: String = "zaakUUID"
+    }
+
+    @Id
+    @GeneratedValue(generator = "notitie_sq", strategy = GenerationType.SEQUENCE)
+    var id: Long = 0
+
+    @Basic
+    @Column(name = "zaak_uuid", updatable = false)
+    lateinit var zaakUUID: UUID
+
+    @Column(name = "tekst", nullable = false)
+    @NotBlank
+    lateinit var text: String
+
+    @Column(name = "tijdstip_laatste_wijziging", nullable = false)
+    lateinit var dateTimeLastModified: ZonedDateTime
+
+    @Column(name = "gebruikersnaam_medewerker", nullable = false, updatable = true)
+    @NotBlank
+    lateinit var employeeUsername: String
+}
diff --git a/src/test/kotlin/nl/info/zac/note/NoteFixtures.kt b/src/test/kotlin/nl/info/zac/note/NoteFixtures.kt
new file mode 100644
index 0000000000..b6a5d362ad
--- /dev/null
+++ b/src/test/kotlin/nl/info/zac/note/NoteFixtures.kt
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.note
+
+import nl.info.zac.note.model.Note
+import java.time.ZonedDateTime
+import java.util.UUID
+import java.util.UUID.randomUUID
+
+fun createNote(
+    id: Long = 1L,
+    zaakUUID: UUID = randomUUID(),
+    text: String = "fakeText",
+    dateTimeLastModified: ZonedDateTime = ZonedDateTime.now(),
+    employeeUsername: String = "fakeUsername",
+) = Note().apply {
+    this.id = id
+    this.zaakUUID = zaakUUID
+    this.text = text
+    this.dateTimeLastModified = dateTimeLastModified
+    this.employeeUsername = employeeUsername
+}
diff --git a/src/test/kotlin/nl/info/zac/note/NoteServiceTest.kt b/src/test/kotlin/nl/info/zac/note/NoteServiceTest.kt
new file mode 100644
index 0000000000..73d21d0d3d
--- /dev/null
+++ b/src/test/kotlin/nl/info/zac/note/NoteServiceTest.kt
@@ -0,0 +1,107 @@
+/*
+ * SPDX-FileCopyrightText: 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.note
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.core.spec.style.BehaviorSpec
+import io.kotest.matchers.shouldBe
+import io.mockk.Runs
+import io.mockk.checkUnnecessaryStub
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.verify
+import jakarta.persistence.EntityManager
+import jakarta.validation.ConstraintViolationException
+import nl.info.zac.note.model.Note
+
+class NoteServiceTest : BehaviorSpec({
+    val entityManager = mockk<EntityManager>()
+    val noteService = NoteService(entityManager)
+
+    beforeEach {
+        checkUnnecessaryStub()
+    }
+
+    Given("A new valid note") {
+        val note = createNote()
+        every { entityManager.persist(note) } returns Unit
+
+        When("createNote is called") {
+            val result = noteService.createNote(note)
+
+            Then("it should persist the note and return it") {
+                result shouldBe note
+                verify(exactly = 1) {
+                    entityManager.persist(note)
+                }
+            }
+        }
+    }
+
+    Given("A new note with a blank text field") {
+        val note = createNote(
+            text = ""
+        )
+
+        When("createNote is called") {
+            val exception = shouldThrow<ConstraintViolationException> {
+                noteService.createNote(note)
+            }
+            Then("it should throw an ConstraintViolationException") {
+                exception.message shouldBe "text: must not be blank"
+            }
+        }
+    }
+
+    Given("An existing valid note") {
+        val note = createNote()
+        val updatedNote = createNote()
+        every { entityManager.merge(note) } returns updatedNote
+
+        When("createNote is called") {
+            val result = noteService.updateNote(note)
+
+            Then("it should merge the updated note and return it") {
+                result shouldBe updatedNote
+                verify(exactly = 1) {
+                    entityManager.merge(note)
+                }
+            }
+        }
+    }
+
+    Given("An existing note with a blank text field") {
+        val note = createNote(
+            text = ""
+        )
+
+        When("createNote is called") {
+            val exception = shouldThrow<ConstraintViolationException> {
+                noteService.updateNote(note)
+            }
+            Then("it should throw an ConstraintViolationException") {
+                exception.message shouldBe "text: must not be blank"
+            }
+        }
+    }
+
+    Given("A note ID for an existing note") {
+        val noteId = 123L
+        val note = createNote(id = noteId)
+        every { entityManager.find(Note::class.java, noteId) } returns note
+        every { entityManager.remove(note) } just Runs
+
+        When("createNote is called") {
+            val result = noteService.deleteNote(noteId)
+
+            Then("it should merge the updated note and return it") {
+                verify(exactly = 1) {
+                    entityManager.remove(note)
+                }
+            }
+        }
+    }
+})


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR converts the backend Notitie classes from Java to Kotlin while updating tests to use Kotest and mockk. It also removes legacy Java classes in favor of the new Kotlin implementations.
- Converted and refactored backend classes to Kotlin.
- Updated test implementations using Kotest and mockk.
- Removed legacy Java classes for notities.

### Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| src/test/kotlin/nl/info/zac/note/NoteServiceTest.kt | Revised tests for creating, updating, and deleting notes |
| src/test/kotlin/nl/info/zac/note/NoteFixtures.kt | Provides test fixtures for notes |
| src/main/kotlin/nl/info/zac/note/model/Note.kt | New Kotlin entity model for Note |
| src/main/kotlin/nl/info/zac/note/NoteService.kt | New Kotlin service implementation for Note |
| src/main/kotlin/nl/info/zac/app/note/model/RestNote.kt | New Kotlin REST model for Note |
| src/main/kotlin/nl/info/zac/app/note/converter/NoteConverter.kt | Converter between domain and REST models |
| src/main/kotlin/nl/info/zac/app/note/NoteRestService.kt | REST endpoints for handling notes |
| src/main/java/net/atos/zac/notities/model/Notitie.java | Removed legacy Java entity |
| src/main/java/net/atos/zac/notities/NotitieService.java | Removed legacy Java service |
| src/main/java/net/atos/zac/app/notities/model/RESTNotitie.java | Removed legacy REST model |
| src/main/java/net/atos/zac/app/notities/converter/NotitieConverter.java | Removed legacy converter |
| src/main/java/net/atos/zac/app/notities/NotitiesRESTService.java | Removed legacy REST endpoints |
| src/itest/kotlin/nl/info/zac/itest/NoteServiceTest.kt | Integration tests for Notitie endpoints |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/test/kotlin/nl/info/zac/note/NoteServiceTest.kt:64**
* The description 'createNote is called' is misleading as the updateNote method is invoked. Consider renaming it to 'updateNote is called' for clarity.
```
When("createNote is called") {
```
**src/test/kotlin/nl/info/zac/note/NoteServiceTest.kt:81**
* The description 'createNote is called' is misleading in this test as well, where updateNote is invoked. Updating the description to 'updateNote is called' would improve consistency and clarity.
```
When("createNote is called") {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T10:34:19Z
user: login: hsiliev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T10:40:58Z
user: login: hsiliev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T10:41:12Z
user: login: edgarvonk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T10:45:17Z
user: login: edgarvonk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T10:46:11Z
user: login: bas-info-nl
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T10:47:59Z

Review comments:
diff_hunk: @@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Atos, 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.app.note.model
+
+import jakarta.json.bind.annotation.JsonbProperty
+import nl.info.zac.note.model.Note
+import nl.info.zac.util.AllOpen
+import nl.info.zac.util.NoArgConstructor
+import java.time.ZonedDateTime
+import java.util.UUID
+
+@NoArgConstructor
+@AllOpen
+data class RestNote(
+    var id: Long?,
+    var zaakUUID: UUID,
+
+    @field:JsonbProperty("tekst")
+    var text: String,
+
+    /**
+     * Date-time when the note was last modified.
+     */
+    @field:JsonbProperty("tijdstipLaatsteWijziging")
+    var dateTimeLastModified: ZonedDateTime?,
path: src/main/kotlin/nl/info/zac/app/note/model/RestNote.kt
user: login: hsiliev
body: Can this ever be null? If a note is created is should have a modified date-time I think
created_at: 2025-05-19T10:40:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Atos, 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.app.note.model
+
+import jakarta.json.bind.annotation.JsonbProperty
+import nl.info.zac.note.model.Note
+import nl.info.zac.util.AllOpen
+import nl.info.zac.util.NoArgConstructor
+import java.time.ZonedDateTime
+import java.util.UUID
+
+@NoArgConstructor
+@AllOpen
+data class RestNote(
+    var id: Long?,
+    var zaakUUID: UUID,
+
+    @field:JsonbProperty("tekst")
+    var text: String,
+
+    /**
+     * Date-time when the note was last modified.
+     */
+    @field:JsonbProperty("tijdstipLaatsteWijziging")
+    var dateTimeLastModified: ZonedDateTime?,
path: src/main/kotlin/nl/info/zac/app/note/model/RestNote.kt
user: login: edgarvonk
body: yes, it is null when a note is created by the frontend. There are loads of improvements that can be done here still but I first wanted to convert the code to Kotlin. :-)

created_at: 2025-05-19T10:45:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Atos, 2025 INFO.nl
+ * SPDX-License-Identifier: EUPL-1.2+
+ */
+package nl.info.zac.app.note.model
+
+import jakarta.json.bind.annotation.JsonbProperty
+import nl.info.zac.note.model.Note
+import nl.info.zac.util.AllOpen
+import nl.info.zac.util.NoArgConstructor
+import java.time.ZonedDateTime
+import java.util.UUID
+
+@NoArgConstructor
+@AllOpen
+data class RestNote(
+    var id: Long?,
+    var zaakUUID: UUID,
+
+    @field:JsonbProperty("tekst")
+    var text: String,
+
+    /**
+     * Date-time when the note was last modified.
+     */
+    @field:JsonbProperty("tijdstipLaatsteWijziging")
+    var dateTimeLastModified: ZonedDateTime?,
path: src/main/kotlin/nl/info/zac/app/note/model/RestNote.kt
user: login: edgarvonk
body: the issue with these REST data classes is that currently they are used both by the getters as well as the setters so to speak. Hence the nullable fields in case of the setters. We should separate these in two. Maybe a future PR..
created_at: 2025-05-19T10:46:11Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-19T11:00:44Z
body: ## unit-test-results
 98 files  +1  142 suites  +1   1m 58s ⏱️ +2s
591 tests +3  591 ✅ +3  0 💤 ±0  0 ❌ ±0 
743 runs  +5  743 ✅ +5  0 💤 ±0  0 ❌ ±0 

Results for commit 6ad100ae. ± Comparison against base commit b4fc979e.

[test-results]:data:application/gzip;base64,H4sIAFkPK2gC/12MQQ6DIBAAv2I498AiIvQzzQqYkKo0CKemfy8QbWmPM5PMk8xusTu5dkpeOrInFysBZxlNChid34qA0nOMJQ8KTrrtSet/dXePrOhHzOiWH2FD8OEwIW3lOfL+gHPZmu+xcjOs3P60X1cXMxCBBihFO+HEOWjJhB6okNIqkAx5PyrU1oAgrzcemuLpCAEAAA==

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-19T11:05:24Z
body: ## integration-test-results
 37 files  +1   37 suites  +1   41s ⏱️ ±0s
132 tests +2  132 ✅ +2  0 💤 ±0  0 ❌ ±0 
150 runs  +2  150 ✅ +2  0 💤 ±0  0 ❌ ±0 

Results for commit 6ad100ae. ± Comparison against base commit b4fc979e.

[test-results]:data:application/gzip;base64,H4sIAHIQK2gC/13MSw7CIBSF4a00jB1wgVLqZswt0ITYFsNjZNy7SGyLDr//JOdJZrfYSK4dHy4didmlUyYHTM5vxQKKy5Q+I3C26xaz1v/p7h4l0SPM6JafYEPw4VtC3upnv+O4bMr5WN0cVrd/2q+rSwVEogFK0U44CQFaMal7KpWyIyiGgg8jamtAktcbh9vRlwYBAAA=

author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-19T11:05:56Z
body: ## [Codecov](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl) Report
Attention: Patch coverage is `88.40580%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 41.40%. Comparing base [(`b4fc979`)](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/commit/b4fc979ea4896fad8a2c7c82d518bd49fe4bdd04?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl) to head [(`6ad100a`)](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/commit/6ad100aebab441c826c50688e9182a4379aced16?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl).
> Report is 2 commits behind head on main.

:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl) | Patch % | Lines |
|---|---|---|
| [...main/kotlin/nl/info/zac/app/note/model/RestNote.kt](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392?src=pr&el=tree&filepath=src%2Fmain%2Fkotlin%2Fnl%2Finfo%2Fzac%2Fapp%2Fnote%2Fmodel%2FRestNote.kt&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl#diff-c3JjL21haW4va290bGluL25sL2luZm8vemFjL2FwcC9ub3RlL21vZGVsL1Jlc3ROb3RlLmt0) | 76.19% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl) |
| [...ain/kotlin/nl/info/zac/app/note/NoteRestService.kt](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392?src=pr&el=tree&filepath=src%2Fmain%2Fkotlin%2Fnl%2Finfo%2Fzac%2Fapp%2Fnote%2FNoteRestService.kt&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl#diff-c3JjL21haW4va290bGluL25sL2luZm8vemFjL2FwcC9ub3RlL05vdGVSZXN0U2VydmljZS5rdA==) | 70.00% | [3 Missing :warning: ](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               main    #3392      +/-   ##
============================================
+ Coverage     41.24%   41.40%   +0.16%     
- Complexity     3759     3778      +19     
============================================
  Files          1395     1395              
  Lines         36182    36191       +9     
  Branches       3893     3963      +70     
============================================
+ Hits          14923    14985      +62     
+ Misses        20410    19975     -435     
- Partials        849     1231     +382     
```

| [Flag](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl) | Coverage Δ | |
|---|---|---|
| [integrationtests](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl) | `45.08% <81.15%> (+0.22%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl) | `29.02% <30.43%> (+0.05%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/infonl/dimpact-zaakafhandelcomponent/pull/3392?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=infonl).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE