ProjectRequirementVersion.java

package com.taxonomy.portfolio.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

import java.time.Instant;
import java.util.Objects;

/** Immutable text/provenance version of a project requirement in one exact tenant. */
@Entity
@Table(name = "project_req_version", indexes = {
        @Index(name = "idx_reqver_req", columnList = "scope_key,requirement_id"),
        @Index(name = "idx_reqver_scope", columnList = "scope_key")
}, uniqueConstraints = {
        @UniqueConstraint(name = "uq_reqver_number",
                columnNames = {"scope_key", "requirement_id", "version_number"}),
        @UniqueConstraint(name = "uq_reqver_hash",
                columnNames = {"scope_key", "requirement_id", "content_hash"}),
        @UniqueConstraint(name = "uq_reqver_id_scope", columnNames = {"id", "scope_key"}),
        @UniqueConstraint(name = "uq_reqver_id_req_scope",
                columnNames = {"id", "requirement_id", "scope_key"})
})
public class ProjectRequirementVersion {

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

    @Column(name = "scope_key", nullable = false,
            length = PortfolioTenantIdentity.MAX_SCOPE_KEY_LENGTH)
    private String scopeKey;

    /** Read-only scalar parent identity for scoped queries without initializing the association. */
    @Column(name = "requirement_id", nullable = false, insertable = false, updatable = false)
    private Long requirementId;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "requirement_id", nullable = false)
    private ProjectRequirement requirement;

    @Column(name = "version_number", nullable = false)
    private int versionNumber;

    @Lob
    @Column(name = "requirement_text", nullable = false)
    private String text;

    @Column(name = "content_hash", nullable = false, length = 64)
    private String contentHash;

    @Column(name = "change_reason", length = 1000)
    private String changeReason;

    @Column(name = "created_by", nullable = false, length = 160)
    private String createdBy;

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

    @Column(name = "source_artifact_id")
    private Long sourceArtifactId;

    @Column(name = "source_version_id")
    private Long sourceVersionId;

    /** JSON array of source-fragment database IDs. */
    @Lob
    @Column(name = "source_fragment_ids")
    private String sourceFragmentIdsJson;

    @Column(name = "section_ref", length = 500)
    private String sectionReference;

    @Column(name = "page_number")
    private Integer pageNumber;

    @Lob
    @Column(name = "original_text")
    private String originalText;

    protected ProjectRequirementVersion() {
    }

    public ProjectRequirementVersion(ProjectRequirement requirement,
                                     int versionNumber,
                                     String text,
                                     String contentHash,
                                     String changeReason,
                                     String createdBy,
                                     Instant createdAt,
                                     Long sourceArtifactId,
                                     Long sourceVersionId,
                                     String sourceFragmentIdsJson,
                                     String sectionReference,
                                     Integer pageNumber,
                                     String originalText) {
        this.requirement = requirement;
        this.versionNumber = versionNumber;
        this.text = text;
        this.contentHash = contentHash;
        this.changeReason = changeReason;
        this.createdBy = createdBy;
        this.createdAt = createdAt;
        this.sourceArtifactId = sourceArtifactId;
        this.sourceVersionId = sourceVersionId;
        this.sourceFragmentIdsJson = sourceFragmentIdsJson;
        this.sectionReference = sectionReference;
        this.pageNumber = pageNumber;
        this.originalText = originalText;
        synchronizeTenantAuthority(false);
    }

    @PrePersist
    @PreUpdate
    private void synchronizeTenantAuthority() {
        synchronizeTenantAuthority(true);
    }

    private void synchronizeTenantAuthority(boolean requirePersistentParent) {
        if (requirement == null || requirement.getScopeKey() == null
                || requirement.getScopeKey().isBlank()) {
            throw new IllegalArgumentException(
                    "Requirement version must reference an exact requirement tenant");
        }
        String requirementScope = requirement.getScopeKey().strip();
        if (scopeKey != null && !scopeKey.isBlank()
                && !requirementScope.equals(scopeKey.strip())) {
            throw new IllegalArgumentException(
                    "Requirement version tenant scope does not match its requirement");
        }
        scopeKey = requirementScope;

        Long persistentRequirementId = requirement.getId();
        if (persistentRequirementId == null) {
            if (requirePersistentParent) {
                throw new IllegalArgumentException(
                        "Requirement must be persisted before its version");
            }
            return;
        }
        if (requirementId != null && !Objects.equals(requirementId, persistentRequirementId)) {
            throw new IllegalArgumentException(
                    "Requirement version parent ID does not match its association");
        }
        requirementId = persistentRequirementId;
    }

    public Long getId() { return id; }
    public String getScopeKey() { return scopeKey; }
    public Long getRequirementId() { return requirementId; }
    public ProjectRequirement getRequirement() { return requirement; }
    public int getVersionNumber() { return versionNumber; }
    public String getText() { return text; }
    public String getContentHash() { return contentHash; }
    public String getChangeReason() { return changeReason; }
    public String getCreatedBy() { return createdBy; }
    public Instant getCreatedAt() { return createdAt; }
    public Long getSourceArtifactId() { return sourceArtifactId; }
    public Long getSourceVersionId() { return sourceVersionId; }
    public String getSourceFragmentIdsJson() { return sourceFragmentIdsJson; }
    public String getSectionReference() { return sectionReference; }
    public Integer getPageNumber() { return pageNumber; }
    public String getOriginalText() { return originalText; }
}