Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cb284b1
Adeed entity CaseRecord
MartinStenhagen Apr 2, 2026
2b61c2e
Adeed entity CaseNumberSequence
MartinStenhagen Apr 2, 2026
b5395ee
Added Flyway Migration for CaseRecord
MartinStenhagen Apr 2, 2026
103109a
Added Flyway Migration for CaseNumberSequence
MartinStenhagen Apr 2, 2026
0d65095
changed from year to sequence_year
MartinStenhagen Apr 2, 2026
67450df
Fixed nullable mismatch
MartinStenhagen Apr 2, 2026
a54ae8e
Added optional = false to owner and assignedUser
MartinStenhagen Apr 7, 2026
5722af8
improvements after coderabbit-feedback
MartinStenhagen Apr 7, 2026
6843af7
improvements after coderabbit-feedback
MartinStenhagen Apr 7, 2026
df9cbde
Merge remote-tracking branch 'origin/feature/db-case-record' into fea…
MartinStenhagen Apr 7, 2026
fe385be
Merge branch 'main' into feature/db-case-record
MartinStenhagen Apr 7, 2026
1303800
Changed import of userEntity to correspond to updated main branch and…
MartinStenhagen Apr 7, 2026
3b52ea6
Removed duplicate dependencies after merge.
MartinStenhagen Apr 7, 2026
4fe6a03
Removed missplaced code in CaseRecord and added Repo and Service
MartinStenhagen Apr 8, 2026
92e17d9
Added Non-null validation to CaseNumberSequence constructor
MartinStenhagen Apr 8, 2026
d5a6168
Changes to support Registry
MartinStenhagen Apr 8, 2026
65d1c64
Added validation for code
MartinStenhagen Apr 8, 2026
58ade39
improvements to CaseRecord
MartinStenhagen Apr 8, 2026
ee1a5c0
Added CaseRecordRequestDto
MartinStenhagen Apr 9, 2026
007c140
Added CaseRecordResponseDto, repositories etc
MartinStenhagen Apr 9, 2026
be1d8fa
Merge branch 'main' into feature/db-case-record
MartinStenhagen Apr 9, 2026
f09d772
Rename Flyway migration files after main merge
MartinStenhagen Apr 9, 2026
95c82e7
Added getIdAsString metod to UserEntity.
MartinStenhagen Apr 9, 2026
e531cd4
Added exception handling
MartinStenhagen Apr 9, 2026
c1ec293
Added ApiExceptionHandler
MartinStenhagen Apr 9, 2026
fe6e029
Moved UserRepository to user-package
MartinStenhagen Apr 9, 2026
cdf2a85
Changed flyway migration from app_user to user_entities
MartinStenhagen Apr 9, 2026
4b670d8
Added CaseRecordServiceTest
MartinStenhagen Apr 9, 2026
6f519a8
Changed Exception handling to match current code in main
MartinStenhagen Apr 9, 2026
4f758a1
Added CaseRecordTest
MartinStenhagen Apr 9, 2026
f15372d
Added CaseRecordControllerTest
MartinStenhagen Apr 9, 2026
8c062de
Changed from @NotNull to @NotBlank after coderabbit feedback
MartinStenhagen Apr 9, 2026
4a7940b
Changed to if (code == null || !code.matches("[A-Z]{2}")) after coder…
MartinStenhagen Apr 9, 2026
c9e52b4
Changed allocateNextCaseNumber to prevent race condition after codera…
MartinStenhagen Apr 9, 2026
7220c77
Added tryAllocateNextCaseNumber-method after coderabbit feedback
MartinStenhagen Apr 9, 2026
638c2f8
Added new tests after changed to CaseRecordService
MartinStenhagen Apr 9, 2026
9cb3ddc
Added logging after coderabbit feedback
MartinStenhagen Apr 9, 2026
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
42 changes: 4 additions & 38 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,16 @@
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
Expand Down Expand Up @@ -76,42 +74,10 @@
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-webauthn</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-webauthn</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package backendlab.team4you.caserecord;

import backendlab.team4you.registry.Registry;
import jakarta.persistence.*;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

import java.util.Objects;

@Entity
@Table(
name = "case_number_sequence",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_case_number_sequence_registry_year",
columnNames = {"registry_id", "sequence_year"}
)
}
)
public class CaseNumberSequence {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "registry_id", nullable = false)
private Registry registry;

@Column(name = "sequence_year",nullable = false)
private Integer year;

@Column(name = "last_value", nullable = false)
@NotNull
@Min(0)
private Long lastValue;

protected CaseNumberSequence() {}

public CaseNumberSequence(Registry registry, Integer year, Long lastValue) {
this.registry = Objects.requireNonNull(registry, "registry is required");
this.year = Objects.requireNonNull(year, "year is required");
setLastValue(lastValue);
}

public Long getId() {
return id;
}

public Registry getRegistry() {
return registry;
}

public Integer getYear() {
return year;
}

public Long getLastValue() {
return lastValue;
}

public void setLastValue(Long lastValue) {
if (lastValue == null) {
throw new IllegalArgumentException("lastValue is required");
}
if (lastValue < 0) {
throw new IllegalArgumentException("lastValue must be >= 0");
}
this.lastValue = lastValue;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package backendlab.team4you.caserecord;

import backendlab.team4you.registry.Registry;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;

import java.util.Optional;

public interface CaseNumberSequenceRepository extends JpaRepository<CaseNumberSequence, Long> {

Optional<CaseNumberSequence> findByRegistryAndYear(Registry registry, Integer year);

@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<CaseNumberSequence> findWithLockByRegistryAndYear(Registry registry, Integer year);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
173 changes: 173 additions & 0 deletions src/main/java/backendlab/team4you/caserecord/CaseRecord.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package backendlab.team4you.caserecord;

import backendlab.team4you.registry.Registry;
import backendlab.team4you.user.UserEntity;
import jakarta.persistence.*;

import java.time.LocalDateTime;
import java.util.Objects;

@Entity
@Table(
name = "case_record",
uniqueConstraints = {
@UniqueConstraint(name = "uk_case_record_case_number", columnNames = "case_number")
}
)
public class CaseRecord {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "case_number", nullable = false, length = 50, updatable = false)
private String caseNumber;

@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "registry_id", nullable = false)
private Registry registry;

@Column(nullable = false, length = 255)
private String title;

@Column(columnDefinition = "text")
private String description;

@Column(nullable = false, length = 50)
private String status;

@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "owner_user_id", nullable = false)
private UserEntity owner;

@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "assigned_user_id", nullable = false)
private UserEntity assignedUser;

@Column(name = "confidentiality_level", nullable = false, length = 50)
private String confidentialityLevel = "OPEN";

@Column(name = "opened_at", nullable = false)
private LocalDateTime openedAt;

@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

@Column(name = "updated_at")
private LocalDateTime updatedAt;

@Column(name = "closed_at")
private LocalDateTime closedAt;

protected CaseRecord() {
}

public CaseRecord(
Registry registry,
String title,
String description,
String status,
UserEntity owner,
UserEntity assignedUser,
String confidentialityLevel,
LocalDateTime openedAt
) {
this.registry = Objects.requireNonNull(registry, "registry is required");
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("title is required");
}
this.title = title.trim();
this.description = description;
this.status = (status == null || status.isBlank()) ? "OPEN" : status.trim();
this.owner = Objects.requireNonNull(owner, "owner is required");
this.assignedUser = Objects.requireNonNull(assignedUser, "assignedUser is required");
this.confidentialityLevel =
(confidentialityLevel == null || confidentialityLevel.isBlank())
? "OPEN"
: confidentialityLevel.trim();

this.openedAt = openedAt;
Comment thread
MartinStenhagen marked this conversation as resolved.
}
Comment thread
MartinStenhagen marked this conversation as resolved.

@PrePersist
void onCreate() {
LocalDateTime now = LocalDateTime.now();
if (this.caseNumber == null || this.caseNumber.isBlank()) {
throw new IllegalStateException("caseNumber must be set before persisting");
}
this.createdAt = now;
if (this.openedAt == null) {
this.openedAt = now;
}
}

@PreUpdate
void onUpdate() {
this.updatedAt = LocalDateTime.now();
}

public Long getId() {
return id;
}

public String getCaseNumber() {
return caseNumber;
}

public void setCaseNumber(String caseNumber) {
if (this.caseNumber != null) {
throw new IllegalStateException("caseNumber is immutable once set");
}
if (caseNumber == null || caseNumber.isBlank()) {
throw new IllegalArgumentException("caseNumber is required");
}
if (caseNumber.length() > 50) {
throw new IllegalArgumentException("caseNumber length must be <= 50");
}
this.caseNumber = caseNumber;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public Registry getRegistry() {
return registry;
}

public String getTitle() {
return title;
}

public String getDescription() {
return description;
}

public String getStatus() {
return status;
}

public UserEntity getOwner() {
return owner;
}

public UserEntity getAssignedUser() {
return assignedUser;
}

public String getConfidentialityLevel() {
return confidentialityLevel;
}

public LocalDateTime getOpenedAt() {
return openedAt;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}

public LocalDateTime getUpdatedAt() {
return updatedAt;
}

public LocalDateTime getClosedAt() {
return closedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package backendlab.team4you.caserecord;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.net.URI;

@RestController
@RequestMapping("/api/case-records")

public class CaseRecordController {
private final CaseRecordService caseRecordService;

public CaseRecordController(CaseRecordService caseRecordService) {
this.caseRecordService = caseRecordService;
}

@PostMapping
public ResponseEntity<CaseRecordResponseDto> createCaseRecord(
@Valid @RequestBody CaseRecordRequestDto requestDto) {
CaseRecordResponseDto responseDto = caseRecordService.createCaseRecord(requestDto);

URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(responseDto.id())
.toUri();
return ResponseEntity.created(location).body(responseDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package backendlab.team4you.caserecord;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface CaseRecordRepository extends JpaRepository<CaseRecord, Long> {

Optional<CaseRecord> findByCaseNumber(String caseNumber);

boolean existsByCaseNumber(String caseNumber);
}
Loading