RelationReviewService.java
package com.taxonomy.relations.service;
import com.taxonomy.dto.RelationProposalDto;
import com.taxonomy.dto.TaxonomyRelationDto;
import com.taxonomy.model.*;
import com.taxonomy.relations.repository.RelationProposalRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import com.taxonomy.catalog.model.TaxonomyRelation;
import com.taxonomy.catalog.service.TaxonomyRelationService;
import com.taxonomy.model.ProposalStatus;
import com.taxonomy.relations.model.RelationProposal;
/**
* Handles the human review workflow for relation proposals.
* Accepted proposals are converted into actual {@link TaxonomyRelation} entities.
*/
@Service
public class RelationReviewService {
private static final Logger log = LoggerFactory.getLogger(RelationReviewService.class);
private final RelationProposalRepository proposalRepository;
private final TaxonomyRelationService relationService;
private final RelationProposalService proposalService;
public RelationReviewService(RelationProposalRepository proposalRepository,
TaxonomyRelationService relationService,
RelationProposalService proposalService) {
this.proposalRepository = proposalRepository;
this.relationService = relationService;
this.proposalService = proposalService;
}
/**
* Accept a proposal: creates a real TaxonomyRelation and marks the proposal as ACCEPTED.
*
* <p>The relation is created in the proposal's workspace (using the proposal's stored
* {@code workspaceId}/{@code ownerUsername}). For legacy proposals with null workspace,
* the relation is created in the shared/legacy scope.
*/
@Transactional
public TaxonomyRelationDto acceptProposal(Long proposalId) {
RelationProposal proposal = proposalRepository.findById(proposalId)
.orElseThrow(() -> new IllegalArgumentException(
"Proposal not found: " + proposalId));
if (proposal.getStatus() != ProposalStatus.PENDING) {
throw new IllegalStateException(
"Proposal " + proposalId + " is already " + proposal.getStatus());
}
// Create the real relation in the proposal's workspace (not the current user's)
String workspaceId = proposal.getWorkspaceId();
String ownerUsername = proposal.getOwnerUsername();
TaxonomyRelationDto relation = relationService.createRelation(
proposal.getSourceNode().getCode(),
proposal.getTargetNode().getCode(),
proposal.getRelationType(),
proposal.getRationale(),
"proposal-pipeline",
workspaceId, ownerUsername);
// Mark proposal as accepted
proposal.setStatus(ProposalStatus.ACCEPTED);
proposal.setReviewedAt(Instant.now());
proposalRepository.save(proposal);
log.info("Accepted proposal {}: {} → {} [{}]",
proposalId,
proposal.getSourceNode().getCode(),
proposal.getTargetNode().getCode(),
proposal.getRelationType());
return relation;
}
/**
* Reject a proposal.
*/
@Transactional
public RelationProposalDto rejectProposal(Long proposalId) {
RelationProposal proposal = proposalRepository.findById(proposalId)
.orElseThrow(() -> new IllegalArgumentException(
"Proposal not found: " + proposalId));
if (proposal.getStatus() != ProposalStatus.PENDING) {
throw new IllegalStateException(
"Proposal " + proposalId + " is already " + proposal.getStatus());
}
proposal.setStatus(ProposalStatus.REJECTED);
proposal.setReviewedAt(Instant.now());
proposalRepository.save(proposal);
log.info("Rejected proposal {}: {} → {} [{}]",
proposalId,
proposal.getSourceNode().getCode(),
proposal.getTargetNode().getCode(),
proposal.getRelationType());
return proposalService.toDto(proposal);
}
/**
* Revert a previously accepted or rejected proposal back to PENDING status.
* If the proposal was accepted, the corresponding relation is deleted.
*/
@Transactional
public RelationProposalDto revertProposal(Long proposalId) {
RelationProposal proposal = proposalRepository.findById(proposalId)
.orElseThrow(() -> new IllegalArgumentException(
"Proposal not found: " + proposalId));
if (proposal.getStatus() == ProposalStatus.PENDING) {
throw new IllegalStateException(
"Proposal " + proposalId + " is already PENDING");
}
ProposalStatus oldStatus = proposal.getStatus();
// If accepted, remove the created relation
if (oldStatus == ProposalStatus.ACCEPTED) {
relationService.deleteRelationBySourceTargetType(
proposal.getSourceNode().getCode(),
proposal.getTargetNode().getCode(),
proposal.getRelationType());
}
proposal.setStatus(ProposalStatus.PENDING);
proposal.setReviewedAt(null);
proposalRepository.save(proposal);
log.info("Reverted proposal {} from {} to PENDING: {} → {} [{}]",
proposalId, oldStatus,
proposal.getSourceNode().getCode(),
proposal.getTargetNode().getCode(),
proposal.getRelationType());
return proposalService.toDto(proposal);
}
}