Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public AuditEventDTO toDTO(AuditEventEntity entity) {
.responseStatus(entity.getResponseStatus())
.errorType(entity.getErrorType())
.caseId(entity.getCaseId())
.statusChange(entity.getStatusChange())
.clientIp(entity.getClientIp())
.userAgent(entity.getUserAgent())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.example.projektarendehantering.common.Actor;
import org.example.projektarendehantering.common.BadRequestException;
import org.example.projektarendehantering.common.CaseStatus;
import org.example.projektarendehantering.common.NotAuthorizedException;
import org.example.projektarendehantering.common.Role;
import org.example.projektarendehantering.infrastructure.persistence.AuditEventEntity;
import org.example.projektarendehantering.infrastructure.persistence.CaseEntity;
import org.example.projektarendehantering.infrastructure.persistence.CaseNoteEntity;
import org.example.projektarendehantering.infrastructure.persistence.CaseNoteRepository;
Expand Down Expand Up @@ -37,6 +39,7 @@ public class CaseService {
private final PatientRepository patientRepository;
private final CaseNoteRepository caseNoteRepository;
private final EmployeeRepository employeeRepository;
private final AuditService auditService;

@Transactional
public void addNote(UUID caseId, String content, Actor actor) {
Expand All @@ -45,13 +48,21 @@ public void addNote(UUID caseId, String content, Actor actor) {
}
CaseEntity caseEntity = caseRepository.findById(caseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found"));
if (caseEntity.getStatus() == CaseStatus.CLOSED) {
throw new BadRequestException("Case is closed");
}
requireCanRead(actor, caseEntity);

CaseNoteEntity note = caseNoteMapper.toEntity(actor, content);
note.setId(UUID.randomUUID());
note.setCaseEntity(caseEntity);

caseNoteRepository.save(note);

CaseStatus previousStatus = caseEntity.getStatus();
caseEntity.setStatus(CaseStatus.COMMUNICATION);
caseRepository.save(caseEntity);
recordStatusChange(actor, caseEntity.getId(), previousStatus, CaseStatus.COMMUNICATION);
}

@Transactional
Expand All @@ -70,13 +81,13 @@ public CaseDTO createCase(Actor actor, CaseDTO caseDTO) {
if (isDoctor(actor) || isManager(actor)) {
entity.setOwnerId(actor.userId());
}
if (entity.getStatus() == null) {
entity.setStatus("OPEN");
}
entity.setStatus(CaseStatus.CREATED);

if (entity.getCreatedAt() == null) {
entity.setCreatedAt(Instant.now());
}
CaseEntity savedEntity = caseRepository.save(entity);
recordStatusChange(actor, savedEntity.getId(), null, CaseStatus.CREATED);
return caseMapper.toDTO(savedEntity);
}

Expand All @@ -100,48 +111,73 @@ public CaseDTO updateCase(Actor actor, UUID caseId, CaseDTO caseDTO) {

CaseEntity entity = caseRepository.findById(caseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found"));
if (entity.getStatus() == CaseStatus.CLOSED) {
throw new BadRequestException("Case is closed");
}

requireCanEdit(actor, entity);

CaseStatus previousStatus = entity.getStatus();
entity.setTitle(title);
entity.setDescription(description);
entity.setStatus(CaseStatus.UPDATED);

CaseEntity savedEntity = caseRepository.save(entity);
recordStatusChange(actor, savedEntity.getId(), previousStatus, CaseStatus.UPDATED);
return caseMapper.toDTO(savedEntity);
}

@Transactional
public void deleteCase(Actor actor, UUID caseId) {
CaseEntity entity = caseRepository.findById(caseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found"));
if (entity.getStatus() == CaseStatus.CLOSED) {
throw new BadRequestException("Case is closed");
}

requireCanDelete(actor, entity);
caseRepository.delete(entity);
CaseStatus previousStatus = entity.getStatus();
entity.setStatus(CaseStatus.CLOSED);
caseRepository.save(entity);
recordStatusChange(actor, entity.getId(), previousStatus, CaseStatus.CLOSED);
}

@Transactional(readOnly = true)
public Optional<CaseDTO> getCase(Actor actor, UUID id) {
return caseRepository.findById(id)
.map(entity -> {
Optional<CaseEntity> entityOpt = caseRepository.findById(id);
if (entityOpt.isPresent() && entityOpt.get().getStatus() == CaseStatus.CLOSED && !isManager(actor)) {
throw new BadRequestException("Case is closed");
}
return entityOpt.map(entity -> {
requireCanRead(actor, entity);
return caseMapper.toDTO(entity);
});
}

@Transactional(readOnly = true)
public List<CaseDTO> getClosedCases(Actor actor) {
if (!isManager(actor)) {
throw new NotAuthorizedException("Not allowed to view closed cases");
}
return caseRepository.findAllByStatus(CaseStatus.CLOSED).stream()
.map(caseMapper::toDTO)
.collect(Collectors.toList());
}

@Transactional(readOnly = true)
public List<CaseDTO> getAllCases(Actor actor) {
if (isManager(actor)) {
return caseRepository.findAll().stream()
return caseRepository.findAllByStatusNot(CaseStatus.CLOSED).stream()
.map(caseMapper::toDTO)
.collect(Collectors.toList());
}
if (isDoctor(actor)) {
return caseRepository.findAllByOwnerId(actor.userId()).stream()
return caseRepository.findAllByOwnerIdAndStatusNot(actor.userId(), CaseStatus.CLOSED).stream()
.map(caseMapper::toDTO)
.collect(Collectors.toList());
}
if (isNurse(actor)) {
return caseRepository.findAllByHandlerId(actor.userId()).stream()
return caseRepository.findAllByHandlerIdAndStatusNot(actor.userId(), CaseStatus.CLOSED).stream()
.map(caseMapper::toDTO)
.collect(Collectors.toList());
}
Expand All @@ -150,8 +186,8 @@ public List<CaseDTO> getAllCases(Actor actor) {

@Transactional(readOnly = true)
public List<CaseDTO> getCasesForPatient(Actor actor, UUID patientId) {
return caseRepository.findAllByPatient_Id(patientId).stream()
.peek(entity -> requireCanRead(actor, entity))
return caseRepository.findAllByPatient_IdAndStatusNot(patientId, CaseStatus.CLOSED).stream()
.filter(entity -> canRead(actor, entity))
.map(caseMapper::toDTO)
.collect(Collectors.toList());
}
Expand All @@ -163,6 +199,9 @@ public CaseDTO assignUsers(Actor actor, UUID caseId, CaseAssignmentDTO dto) {
}
CaseEntity entity = caseRepository.findById(caseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found"));
if (entity.getStatus() == CaseStatus.CLOSED) {
throw new BadRequestException("Case is closed");
}
if (isDoctor(actor)) {
if (entity.getOwnerId() == null || !entity.getOwnerId().equals(actor.userId())) {
throw new NotAuthorizedException("Not allowed to modify assignments for this case");
Expand All @@ -180,7 +219,12 @@ public CaseDTO assignUsers(Actor actor, UUID caseId, CaseAssignmentDTO dto) {
UUID handlerId = requireEmployeeWithRole(dto.getHandlerId(), Set.of(Role.NURSE), "handlerId");
entity.setHandlerId(handlerId);
}
return caseMapper.toDTO(caseRepository.save(entity));

CaseStatus previousStatus = entity.getStatus();
entity.setStatus(CaseStatus.HANDLER_ASSIGNED);
CaseEntity savedEntity = caseRepository.save(entity);
recordStatusChange(actor, savedEntity.getId(), previousStatus, CaseStatus.HANDLER_ASSIGNED);
return caseMapper.toDTO(savedEntity);
}

private UUID requireEmployeeWithRole(UUID id, Set<Role> allowedRoles, String fieldName) {
Expand Down Expand Up @@ -227,6 +271,13 @@ private boolean canCreate(Actor actor) {
return isManager(actor) || isDoctor(actor);
}

private boolean canRead(Actor actor, CaseEntity entity) {
if (isManager(actor)) return true;
if (isDoctor(actor) && actor.userId().equals(entity.getOwnerId())) return true;
if (isNurse(actor) && actor.userId().equals(entity.getHandlerId())) return true;
return false;
}

private boolean isManager(Actor actor) {
return actor.role() == Role.MANAGER;
}
Expand All @@ -238,4 +289,15 @@ private boolean isDoctor(Actor actor) {
private boolean isNurse(Actor actor) {
return actor.role() == Role.NURSE;
}

private void recordStatusChange(Actor actor, UUID caseId, CaseStatus from, CaseStatus to) {
String statusChange = (from != null ? from.name() : "NEW") + " -> " + to.name();
AuditEventEntity event = AuditEventEntity.builder()
.caseId(caseId)
.statusChange(statusChange)
.actorId(actor != null ? actor.userId() : null)
.actorRole(actor != null && actor.role() != null ? actor.role().name() : null)
.build();
auditService.record(event);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,8 @@
import io.awspring.cloud.s3.S3Template;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.projektarendehantering.common.Actor;
import org.example.projektarendehantering.common.AppException;
import org.example.projektarendehantering.common.BadRequestException;
import org.example.projektarendehantering.common.NotAuthorizedException;
import org.example.projektarendehantering.infrastructure.persistence.CaseEntity;
import org.example.projektarendehantering.infrastructure.persistence.CaseRepository;
import org.example.projektarendehantering.infrastructure.persistence.DocumentEntity;
import org.example.projektarendehantering.infrastructure.persistence.DocumentRepository;
import org.example.projektarendehantering.common.*;
import org.example.projektarendehantering.infrastructure.persistence.*;
import org.example.projektarendehantering.presentation.dto.DocumentDTO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
Expand All @@ -36,6 +30,7 @@ public class DocumentService {
private final CaseRepository caseRepository;
private final S3Template s3Template;
private final DocumentMapper documentMapper;
private final AuditService auditService;

@Value("${app.s3.bucket}")
private String bucket;
Expand All @@ -57,6 +52,10 @@ public DocumentDTO uploadDocument(Actor actor, UUID caseId, MultipartFile file)
CaseEntity caseEntity = caseRepository.findById(caseId)
.orElseThrow(() -> new BadRequestException("Case not found"));

if (caseEntity.getStatus() == CaseStatus.CLOSED) {
throw new BadRequestException("Case is closed");
}

validateAccess(actor, caseEntity);

String s3Key = UUID.randomUUID().toString() + "-" + originalFilename;
Expand Down Expand Up @@ -100,6 +99,20 @@ public void afterCompletion(int status) {
// --- SAFE DB SAVE WITH COMPENSATION IF IT FAILS ---
try {
DocumentEntity saved = documentRepository.save(entity);
CaseStatus previousStatus = caseEntity.getStatus();
if (previousStatus != CaseStatus.COMMUNICATION) {
caseEntity.setStatus(CaseStatus.COMMUNICATION);
caseRepository.save(caseEntity);

String statusChange = (previousStatus != null ? previousStatus.name() : "NEW")
+ " -> " + CaseStatus.COMMUNICATION.name();
auditService.record(AuditEventEntity.builder()
.caseId(caseEntity.getId())
.statusChange(statusChange)
.actorId(actor.userId())
.actorRole(actor.role() != null ? actor.role().name() : null)
.build());
}
return documentMapper.toDTO(saved);
} catch (RuntimeException ex) {
if (!syncActive) {
Expand All @@ -117,6 +130,10 @@ public List<DocumentDTO> listDocuments(Actor actor, UUID caseId) {
CaseEntity caseEntity = caseRepository.findById(caseId)
.orElseThrow(() -> new BadRequestException("Case not found"));

if (caseEntity.getStatus() == CaseStatus.CLOSED) {
throw new BadRequestException("Case is closed");
}

validateAccess(actor, caseEntity);

return documentRepository.findAllByCaseEntityId(caseId).stream()
Expand Down Expand Up @@ -180,6 +197,9 @@ public DocumentEntity getEntity(Actor actor, UUID documentId) {
}

private void validateAccess(Actor actor, CaseEntity caseEntity) {
if (caseEntity.getStatus() == CaseStatus.CLOSED) {
throw new BadRequestException("Case is closed");
}
if (actor.isManager()) return;
if (actor.isDoctor() && actor.userId().equals(caseEntity.getOwnerId())) return;
if (actor.isNurse() && actor.userId().equals(caseEntity.getHandlerId())) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.example.projektarendehantering.common;

public enum CaseStatus {
CREATED,
HANDLER_ASSIGNED,
COMMUNICATION,
UPDATED,
CLOSED
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public class AuditEventEntity {

private UUID caseId;

private String statusChange;

private String clientIp;
private String userAgent;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.example.projektarendehantering.common.CaseStatus;

import java.time.Instant;
import java.util.ArrayList;
Expand All @@ -23,7 +24,10 @@ public class CaseEntity {

@Id
private UUID id;
private String status;

@Enumerated(EnumType.STRING)
private CaseStatus status;

private UUID ownerId;
private String title;
private String description;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package org.example.projektarendehantering.infrastructure.persistence;

import org.example.projektarendehantering.common.CaseStatus;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.UUID;

public interface CaseRepository extends JpaRepository<CaseEntity, UUID> {
List<CaseEntity> findAllByStatus(CaseStatus status);
List<CaseEntity> findAllByStatusNot(CaseStatus status);
List<CaseEntity> findAllByPatient_Id(UUID patientId);
List<CaseEntity> findAllByPatient_IdAndStatusNot(UUID patientId, CaseStatus status);
List<CaseEntity> findAllByOwnerId(UUID ownerId);
List<CaseEntity> findAllByOwnerIdAndStatusNot(UUID ownerId, CaseStatus status);
List<CaseEntity> findAllByHandlerId(UUID handlerId);
List<CaseEntity> findAllByHandlerIdAndStatusNot(UUID handlerId, CaseStatus status);
List<CaseEntity> findAllByOtherId(UUID otherId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class AuditEventDTO {

private UUID caseId;

private String statusChange;

private String clientIp;
private String userAgent;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.example.projektarendehantering.common.CaseStatus;

import java.time.Instant;
import java.util.ArrayList;
Expand All @@ -17,7 +18,7 @@
public class CaseDTO {

private UUID id;
private String status;
private CaseStatus status;
private String title;
private String description;
private Instant createdAt;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public ResponseEntity<List<CaseDTO>> getAllCases() {
return ResponseEntity.ok(caseService.getAllCases(securityActorAdapter.currentUser()));
}

@GetMapping("/closed")
public ResponseEntity<List<CaseDTO>> getClosedCases() {
return ResponseEntity.ok(caseService.getClosedCases(securityActorAdapter.currentUser()));
}

@PutMapping("/{id}")
public ResponseEntity<CaseDTO> updateCase(@PathVariable UUID id, @RequestBody @Valid UpdateCaseRequest request) {
CaseDTO caseDTO = new CaseDTO();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ public String listCases(Model model) {
return "cases/list";
}

@GetMapping("/ui/cases/closed")
public String listClosedCases(Model model) {
model.addAttribute("cases", caseService.getClosedCases(securityActorAdapter.currentUser()));
return "cases/closed";
}

@GetMapping("/ui/cases/new")
public String newCase(Model model) {
model.addAttribute("createCaseForm", new CreateCaseForm());
Expand Down
Loading