From ea99d33cd161be5c79b56e10563a88e7afe880e3 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 31 Mar 2026 09:59:01 +0200 Subject: [PATCH 1/4] Add `MedicalRecordController` for managing medical record operations - Introduced endpoints for creating, updating, viewing, assigning, and closing medical records. - Enforced role- and policy-based validations via `MedicalRecordPolicy`. - Updated `MedicalRecordService` to support controller operations, including ownership and clinic-based constraints. Closes #8 --- .../controllers/MedicalRecordController.java | 218 ++++++++++++++++++ .../services/MedicalRecordService.java | 39 +++- 2 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/example/vet1177/controllers/MedicalRecordController.java diff --git a/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java b/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java new file mode 100644 index 00000000..02826af0 --- /dev/null +++ b/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java @@ -0,0 +1,218 @@ +package org.example.vet1177.controllers; + +import jakarta.validation.Valid; +import org.example.vet1177.entities.*; +import org.example.vet1177.policy.MedicalRecordPolicy; +import org.example.vet1177.services.MedicalRecordService; +import org.example.vet1177.services.UserService; +import org.springframework.http.ResponseEntity; +import org.example.vet1177.dto.request.medicalrecord.*; +import org.example.vet1177.dto.response.medicalrecord.*; +import org.example.vet1177.entities.*; +import org.example.vet1177.exception.ForbiddenException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/medical-records") +public class MedicalRecordController { + + private final MedicalRecordService medicalRecordService; + private final MedicalRecordPolicy medicalRecordPolicy; + private final UserService userService; + + public MedicalRecordController( + MedicalRecordService medicalRecordService, + MedicalRecordPolicy medicalRecordPolicy, + UserService userService) { + this.medicalRecordService = medicalRecordService; + this.medicalRecordPolicy = medicalRecordPolicy; + this.userService = userService; + } + + // POST /api/medical-records + @PostMapping + @Transactional + public ResponseEntity create( + @Valid @RequestBody CreateMedicalRecordRequest request, + @AuthenticationPrincipal User currentUser) { + + return ResponseEntity.ok( + MedicalRecordResponse.from( + medicalRecordService.create( + request.title(), + request.description(), + request.petId(), + request.clinicId(), + currentUser + ) + ) + ); + } + + // GET /api/medical-records/{id} + @GetMapping("/{id}") + @Transactional(readOnly = true) + public ResponseEntity getById( + @PathVariable UUID id, + @AuthenticationPrincipal User currentUser) { + + MedicalRecord record = medicalRecordService.getById(id); + medicalRecordPolicy.canView(currentUser, record); + return ResponseEntity.ok(MedicalRecordResponse.from(record)); + } + + // GET /api/medical-records/my-records (för OWNER) + @GetMapping("/my-records") + @Transactional(readOnly = true) + public ResponseEntity> getMyRecords( + @AuthenticationPrincipal User currentUser) { + + if (currentUser.getRole() != Role.OWNER) { + throw new ForbiddenException("Endast djurägare kan se sina egna ärenden"); + } + + return ResponseEntity.ok( + medicalRecordService.getByOwner(currentUser.getId()) + .stream() + .map(MedicalRecordSummaryResponse::from) + .toList() + ); + } + + // GET /api/medical-records/pet/{petId} + @GetMapping("/pet/{petId}") + @Transactional(readOnly = true) + public ResponseEntity> getByPet( + @PathVariable UUID petId, + @AuthenticationPrincipal User currentUser) { + + List records = medicalRecordService.getByPet(petId); + + if (!records.isEmpty()) { + medicalRecordPolicy.canView(currentUser, records.get(0)); + } + + return ResponseEntity.ok( + records.stream().map(MedicalRecordSummaryResponse::from).toList() + ); + } + + // GET /api/medical-records/clinic/{clinicId} + @GetMapping("/clinic/{clinicId}") + @Transactional(readOnly = true) + public ResponseEntity> getByClinic( + @PathVariable UUID clinicId, + @AuthenticationPrincipal User currentUser) { + + medicalRecordPolicy.canViewClinic(currentUser, clinicId); + return ResponseEntity.ok( + medicalRecordService.getByClinic(clinicId) + .stream() + .map(MedicalRecordSummaryResponse::from) + .toList() + ); + } + + // GET /api/medical-records/clinic/{clinicId}/status/{status} + @GetMapping("/clinic/{clinicId}/status/{status}") + @Transactional(readOnly = true) + public ResponseEntity> getByClinicAndStatus( + @PathVariable UUID clinicId, + @PathVariable RecordStatus status, + @AuthenticationPrincipal User currentUser) { + + medicalRecordPolicy.canViewClinic(currentUser, clinicId); + return ResponseEntity.ok( + medicalRecordService.getByClinicAndStatus(clinicId, status) + .stream() + .map(MedicalRecordSummaryResponse::from) + .toList() + ); + } + + // PUT /api/medical-records/{id} + @PutMapping("/{id}") + @Transactional + public ResponseEntity update( + @PathVariable UUID id, + @Valid @RequestBody UpdateMedicalRecordRequest request, + @AuthenticationPrincipal User currentUser) { + + MedicalRecord record = medicalRecordService.getById(id); + medicalRecordPolicy.canUpdate(currentUser, record); + + return ResponseEntity.ok( + MedicalRecordResponse.from( + medicalRecordService.update( + id, + request.title(), + request.description(), + currentUser + ) + ) + ); + } + + // PUT /api/medical-records/{id}/assign-vet + @PutMapping("/{id}/assign-vet") + @Transactional + public ResponseEntity assignVet( + @PathVariable UUID id, + @Valid @RequestBody AssignVetRequest request, + @AuthenticationPrincipal User currentUser) { + + MedicalRecord record = medicalRecordService.getById(id); + User vetToAssign = userService.getById(request.vetId()); + medicalRecordPolicy.canAssignVet(currentUser, record, vetToAssign); + + return ResponseEntity.ok( + MedicalRecordResponse.from( + medicalRecordService.assignVet(id, vetToAssign, currentUser) + ) + ); + } + + // PUT /api/medical-records/{id}/status + @PutMapping("/{id}/status") + @Transactional + public ResponseEntity updateStatus( + @PathVariable UUID id, + @Valid @RequestBody UpdateStatusRequest request, + @AuthenticationPrincipal User currentUser) { + + MedicalRecord record = medicalRecordService.getById(id); + medicalRecordPolicy.canUpdateStatus(currentUser, record, request.status()); + + return ResponseEntity.ok( + MedicalRecordResponse.from( + medicalRecordService.updateStatus( + id, + request.status(), + currentUser + ) + ) + ); + } + + // PUT /api/medical-records/{id}/close + @PutMapping("/{id}/close") + @Transactional + public ResponseEntity close( + @PathVariable UUID id, + @AuthenticationPrincipal User currentUser) { + + MedicalRecord record = medicalRecordService.getById(id); + medicalRecordPolicy.canClose(currentUser, record); + + return ResponseEntity.ok( + MedicalRecordResponse.from( + medicalRecordService.close(id, currentUser) + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/vet1177/services/MedicalRecordService.java b/src/main/java/org/example/vet1177/services/MedicalRecordService.java index 65b0aea0..db08192e 100644 --- a/src/main/java/org/example/vet1177/services/MedicalRecordService.java +++ b/src/main/java/org/example/vet1177/services/MedicalRecordService.java @@ -3,7 +3,10 @@ import org.example.vet1177.entities.*; import org.example.vet1177.exception.BusinessRuleException; import org.example.vet1177.exception.ResourceNotFoundException; +import org.example.vet1177.policy.MedicalRecordPolicy; +import org.example.vet1177.repository.ClinicRepository; import org.example.vet1177.repository.MedicalRecordRepository; +import org.example.vet1177.repository.PetRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,9 +19,18 @@ public class MedicalRecordService { private final MedicalRecordRepository medicalRecordRepository; - - public MedicalRecordService(MedicalRecordRepository medicalRecordRepository) { + private final PetRepository petRepository; + private final ClinicRepository clinicRepository; + private final MedicalRecordPolicy medicalRecordPolicy; + + public MedicalRecordService(MedicalRecordRepository medicalRecordRepository, + PetRepository petRepository, + ClinicRepository clinicRepository, + MedicalRecordPolicy medicalRecordPolicy) { this.medicalRecordRepository = medicalRecordRepository; + this.petRepository = petRepository; + this.clinicRepository = clinicRepository; + this.medicalRecordPolicy = medicalRecordPolicy; } // ── Skapa ──────────────────────────────────────────────── @@ -26,18 +38,25 @@ public MedicalRecordService(MedicalRecordRepository medicalRecordRepository) { public MedicalRecord create( String title, String description, - Pet pet, - User owner, - Clinic clinic, - User createdBy) { + UUID petId, + UUID clinicId, + User currentUser) { + + Pet pet = petRepository.findById(petId) + .orElseThrow(() -> new ResourceNotFoundException("Pet", petId)); + + Clinic clinic = clinicRepository.findById(clinicId) + .orElseThrow(() -> new ResourceNotFoundException("Clinic", clinicId)); + + medicalRecordPolicy.canCreate(currentUser, pet, clinic); MedicalRecord record = new MedicalRecord(); record.setTitle(title); record.setDescription(description); record.setPet(pet); - record.setOwner(owner); + record.setOwner(pet.getOwner()); // ← hämtas från pet record.setClinic(clinic); - record.setCreatedBy(createdBy); + record.setCreatedBy(currentUser); record.setStatus(RecordStatus.OPEN); return medicalRecordRepository.save(record); @@ -121,4 +140,8 @@ public MedicalRecord close(UUID recordId, User closedBy) { return medicalRecordRepository.save(record); } + + public MedicalRecord save(MedicalRecord record) { + return medicalRecordRepository.save(record); + } } \ No newline at end of file From 2509dd6545aaa2483ab7684ce7b909e352e82f8d Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 31 Mar 2026 13:34:48 +0200 Subject: [PATCH 2/4] Add role-based access control for medical records retrieval and assignment - Implement `isAllowed` method in `MedicalRecordPolicy` for user-role validation. - Introduce `getByOwner` endpoint in `MedicalRecordController` with ownership-specific constraints. - Update `getByPet` to filter records based on user permissions. - Enhance validation for vet assignment to finalized records. Closes #8 --- .../controllers/MedicalRecordController.java | 33 +++++++++++++++---- .../vet1177/policy/MedicalRecordPolicy.java | 10 ++++++ .../services/MedicalRecordService.java | 25 ++++++++++++-- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java b/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java index 02826af0..42030b6d 100644 --- a/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java +++ b/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java @@ -84,21 +84,40 @@ public ResponseEntity> getMyRecords( ); } + // Lägg till i MedicalRecordController + @GetMapping("/owner/{ownerId}") + @Transactional(readOnly = true) + public ResponseEntity> getByOwner( + @PathVariable UUID ownerId, + @AuthenticationPrincipal User currentUser) { + + // OWNER får bara hämta sina egna ärenden + if (currentUser.getRole() == Role.OWNER && + !currentUser.getId().equals(ownerId)) { + throw new ForbiddenException("Du kan bara se dina egna ärenden"); + } + + return ResponseEntity.ok( + medicalRecordService.getByOwner(ownerId) + .stream() + .map(MedicalRecordSummaryResponse::from) + .toList() + ); + } + // GET /api/medical-records/pet/{petId} + // I controllern — enklare @GetMapping("/pet/{petId}") @Transactional(readOnly = true) public ResponseEntity> getByPet( @PathVariable UUID petId, @AuthenticationPrincipal User currentUser) { - List records = medicalRecordService.getByPet(petId); - - if (!records.isEmpty()) { - medicalRecordPolicy.canView(currentUser, records.get(0)); - } - return ResponseEntity.ok( - records.stream().map(MedicalRecordSummaryResponse::from).toList() + medicalRecordService.getByPetAllowedForUser(petId, currentUser) + .stream() + .map(MedicalRecordSummaryResponse::from) + .toList() ); } diff --git a/src/main/java/org/example/vet1177/policy/MedicalRecordPolicy.java b/src/main/java/org/example/vet1177/policy/MedicalRecordPolicy.java index ea0b4d26..e623ae1c 100644 --- a/src/main/java/org/example/vet1177/policy/MedicalRecordPolicy.java +++ b/src/main/java/org/example/vet1177/policy/MedicalRecordPolicy.java @@ -70,6 +70,16 @@ public void canUpdateStatus(User user, MedicalRecord record, RecordStatus newSta } } + + public boolean isAllowed(User user, MedicalRecord record) { + return switch (user.getRole()) { + case OWNER -> user.getId().equals(record.getOwner().getId()); + case VET -> user.getClinic() != null && + user.getClinic().getId().equals(record.getClinic().getId()); + case ADMIN -> true; + }; + } + public void canAssignVet(User user, MedicalRecord record, User vetToAssign) { switch (user.getRole()) { case OWNER -> diff --git a/src/main/java/org/example/vet1177/services/MedicalRecordService.java b/src/main/java/org/example/vet1177/services/MedicalRecordService.java index db08192e..e28b7df3 100644 --- a/src/main/java/org/example/vet1177/services/MedicalRecordService.java +++ b/src/main/java/org/example/vet1177/services/MedicalRecordService.java @@ -90,12 +90,21 @@ public List getByClinicAndStatus(UUID clinicId, RecordStatus stat return medicalRecordRepository.findByClinicIdAndStatus(clinicId, status); } + @Transactional(readOnly = true) + public List getByPetAllowedForUser(UUID petId, User currentUser) { + List all = medicalRecordRepository.findByPetId(petId); + + return all.stream() + .filter(record -> medicalRecordPolicy.isAllowed(currentUser, record)) + .toList(); + } + // ── Uppdatera ───────────────────────────────────────────── public MedicalRecord update(UUID id, String title, String description, User updatedBy) { MedicalRecord record = getById(id); - if (record.getStatus().isFinal()) { // ← mellan rad 75-76 + if (record.getStatus().isFinal()) { throw new BusinessRuleException("Stängda ärenden kan inte uppdateras"); } @@ -105,9 +114,19 @@ public MedicalRecord update(UUID id, String title, String description, User upda return medicalRecordRepository.save(record); } - public MedicalRecord assignVet(UUID recordId, User vet, User updatedBy) { + public MedicalRecord assignVet(UUID recordId, User vetToAssign, User updatedBy) { MedicalRecord record = getById(recordId); - record.setAssignedVet(vet); + if (record.getStatus().isFinal()) { + throw new BusinessRuleException( + "Kan inte tilldela handläggare till ett stängt ärende"); + } + + if (vetToAssign.getRole() != Role.VET) { + throw new BusinessRuleException( + "Endast veterinärer kan tilldelas som handläggare"); + } + + record.setAssignedVet(vetToAssign); record.setStatus(RecordStatus.IN_PROGRESS); record.setUpdatedBy(updatedBy); return medicalRecordRepository.save(record); From da727ba650738a339eb4c41293535bff7c53b774 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 31 Mar 2026 14:27:05 +0200 Subject: [PATCH 3/4] Add owner-based filtering in MedicalRecordService and update controller endpoint - Introduced `getByOwnerAllowedForUser` in `MedicalRecordService` for policy-based filtering. - Updated `MedicalRecordController` to use the new service method. --- .../vet1177/controllers/MedicalRecordController.java | 5 ++--- .../example/vet1177/services/MedicalRecordService.java | 9 +++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java b/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java index 42030b6d..dcfcfe4b 100644 --- a/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java +++ b/src/main/java/org/example/vet1177/controllers/MedicalRecordController.java @@ -84,21 +84,20 @@ public ResponseEntity> getMyRecords( ); } - // Lägg till i MedicalRecordController + @GetMapping("/owner/{ownerId}") @Transactional(readOnly = true) public ResponseEntity> getByOwner( @PathVariable UUID ownerId, @AuthenticationPrincipal User currentUser) { - // OWNER får bara hämta sina egna ärenden if (currentUser.getRole() == Role.OWNER && !currentUser.getId().equals(ownerId)) { throw new ForbiddenException("Du kan bara se dina egna ärenden"); } return ResponseEntity.ok( - medicalRecordService.getByOwner(ownerId) + medicalRecordService.getByOwnerAllowedForUser(ownerId, currentUser) .stream() .map(MedicalRecordSummaryResponse::from) .toList() diff --git a/src/main/java/org/example/vet1177/services/MedicalRecordService.java b/src/main/java/org/example/vet1177/services/MedicalRecordService.java index e28b7df3..07e4b21d 100644 --- a/src/main/java/org/example/vet1177/services/MedicalRecordService.java +++ b/src/main/java/org/example/vet1177/services/MedicalRecordService.java @@ -99,6 +99,15 @@ public List getByPetAllowedForUser(UUID petId, User currentUser) .toList(); } + @Transactional(readOnly = true) + public List getByOwnerAllowedForUser(UUID ownerId, User currentUser) { + List all = medicalRecordRepository.findByOwnerId(ownerId); + + return all.stream() + .filter(record -> medicalRecordPolicy.isAllowed(currentUser, record)) + .toList(); + } + // ── Uppdatera ───────────────────────────────────────────── public MedicalRecord update(UUID id, String title, String description, User updatedBy) { From 7a698e350b9001a8f3f09e0b27682275203cde00 Mon Sep 17 00:00:00 2001 From: Annika Holmqvist Date: Tue, 31 Mar 2026 14:35:47 +0200 Subject: [PATCH 4/4] Prevent updates to finalized medical records in `updateStatus` method --- .../org/example/vet1177/services/MedicalRecordService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/example/vet1177/services/MedicalRecordService.java b/src/main/java/org/example/vet1177/services/MedicalRecordService.java index 07e4b21d..377113d9 100644 --- a/src/main/java/org/example/vet1177/services/MedicalRecordService.java +++ b/src/main/java/org/example/vet1177/services/MedicalRecordService.java @@ -143,6 +143,9 @@ public MedicalRecord assignVet(UUID recordId, User vetToAssign, User updatedBy) public MedicalRecord updateStatus(UUID recordId, RecordStatus newStatus, User updatedBy) { MedicalRecord record = getById(recordId); + + if (record.getStatus().isFinal()) { + throw new BusinessRuleException("Stängda ärenden kan inte ändras");} record.setStatus(newStatus); record.setUpdatedBy(updatedBy);