ArchitectureProject.java
package com.taxonomy.portfolio.model;
import com.taxonomy.portfolio.model.PortfolioTypes.ProjectStatus;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
/**
* Project boundary for requirements, analyses, solution decisions and products.
*
* <p>{@code scopeKey} is the reversible repository/workspace/branch identity.
* Explicit tenant columns are synchronized from it before every write so
* operational queries and database evidence never have to infer tenancy from a
* user name or nullable workspace alone.</p>
*/
@Entity
@Table(name = "arch_project", indexes = {
@Index(name = "idx_proj_scope", columnList = "scope_key"),
@Index(name = "idx_proj_status", columnList = "scope_key,status"),
@Index(name = "idx_proj_tenant", columnList = "repository_id,workspace_scope,branch_name")
}, uniqueConstraints = {
@UniqueConstraint(name = "uq_proj_scope_key", columnNames = {"scope_key", "project_key"}),
@UniqueConstraint(name = "uq_proj_id_scope", columnNames = {"id", "scope_key"})
})
public class ArchitectureProject {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "scope_key", nullable = false, length = PortfolioTenantIdentity.MAX_SCOPE_KEY_LENGTH)
private String scopeKey;
@Column(name = "repository_id", nullable = false, length = 255)
private String repositoryId;
@Column(name = "workspace_scope", nullable = false, length = 320)
private String workspaceScope;
@Column(name = "branch_name", nullable = false, length = 255)
private String branchName;
@Column(name = "workspace_id", length = 120)
private String workspaceId;
@Column(name = "owner_username", nullable = false, length = 160)
private String ownerUsername;
@Column(name = "project_key", nullable = false, length = 64)
private String projectKey;
@Column(nullable = false, length = 240)
private String title;
@Column(length = 4000)
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private ProjectStatus status = ProjectStatus.PLANNING;
@Column(name = "target_architecture", length = 4000)
private String targetArchitecture;
@Column(name = "target_date")
private LocalDate targetDate;
@Column(name = "budget_amount", precision = 19, scale = 2)
private BigDecimal budgetAmount;
@Column(name = "budget_currency", length = 3)
private String budgetCurrency;
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
@Column(name = "row_version", nullable = false)
private long rowVersion;
protected ArchitectureProject() {
}
public ArchitectureProject(String scopeKey,
String workspaceId,
String ownerUsername,
String projectKey,
String title,
String description,
ProjectStatus status,
Instant now) {
this.scopeKey = scopeKey;
this.workspaceId = workspaceId;
this.ownerUsername = ownerUsername;
this.projectKey = projectKey;
this.title = title;
this.description = description;
this.status = status != null ? status : ProjectStatus.PLANNING;
this.createdAt = now;
this.updatedAt = now;
synchronizeTenantIdentityIfEncoded();
}
public void update(String title,
String description,
ProjectStatus status,
String targetArchitecture,
LocalDate targetDate,
BigDecimal budgetAmount,
String budgetCurrency,
Instant now) {
if (title != null) this.title = title;
if (description != null) this.description = description;
if (status != null) this.status = status;
if (targetArchitecture != null) this.targetArchitecture = targetArchitecture;
if (targetDate != null) this.targetDate = targetDate;
if (budgetAmount != null) this.budgetAmount = budgetAmount;
if (budgetCurrency != null) this.budgetCurrency = budgetCurrency;
this.updatedAt = now;
}
@PrePersist
@PreUpdate
private void synchronizeTenantIdentity() {
PortfolioTenantIdentity identity = PortfolioTenantIdentity.parse(scopeKey);
String expectedWorkspaceScope = workspaceId == null || workspaceId.isBlank()
? PortfolioTenantIdentity.CENTRAL_SCOPE
: PortfolioTenantIdentity.WORKSPACE_SCOPE_PREFIX + workspaceId.strip();
if (!expectedWorkspaceScope.equals(identity.workspaceScope())) {
throw new IllegalArgumentException(
"Portfolio scope key does not match workspaceId");
}
repositoryId = identity.repositoryId();
workspaceScope = identity.workspaceScope();
branchName = identity.branch();
}
private void synchronizeTenantIdentityIfEncoded() {
if (PortfolioTenantIdentity.isEncoded(scopeKey)) {
synchronizeTenantIdentity();
}
}
public Long getId() { return id; }
public String getScopeKey() { return scopeKey; }
public String getRepositoryId() { return repositoryId; }
public String getWorkspaceScope() { return workspaceScope; }
public String getBranchName() { return branchName; }
public String getWorkspaceId() { return workspaceId; }
public String getOwnerUsername() { return ownerUsername; }
public String getProjectKey() { return projectKey; }
public String getTitle() { return title; }
public String getDescription() { return description; }
public ProjectStatus getStatus() { return status; }
public String getTargetArchitecture() { return targetArchitecture; }
public LocalDate getTargetDate() { return targetDate; }
public BigDecimal getBudgetAmount() { return budgetAmount; }
public String getBudgetCurrency() { return budgetCurrency; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public long getRowVersion() { return rowVersion; }
}