diff --git a/models/DependencyModel.php b/models/DependencyModel.php index a7caf31..8f37899 100644 --- a/models/DependencyModel.php +++ b/models/DependencyModel.php @@ -172,6 +172,27 @@ class DependencyModel } $checkStmt->close(); + // Also check the semantic inverse: "A blocks B" and "B blocked_by A" + // describe the same relationship, so adding one from either ticket's + // page must be rejected as a duplicate of the other. relates_to is + // its own inverse (symmetric); duplicates has no defined inverse type. + $inverseTypes = ['blocks' => 'blocked_by', 'blocked_by' => 'blocks', 'relates_to' => 'relates_to']; + if (isset($inverseTypes[$type])) { + $inverseType = $inverseTypes[$type]; + $checkInverseSql = "SELECT dependency_id FROM ticket_dependencies + WHERE ticket_id = ? AND depends_on_id = ? AND dependency_type = ?"; + $checkInverseStmt = $this->conn->prepare($checkInverseSql); + $checkInverseStmt->bind_param("sss", $dependsOnId, $ticketId, $inverseType); + $checkInverseStmt->execute(); + $inverseResult = $checkInverseStmt->get_result(); + + if ($inverseResult->num_rows > 0) { + $checkInverseStmt->close(); + return ['success' => false, 'error' => 'This relationship already exists']; + } + $checkInverseStmt->close(); + } + // Check for circular dependency if ($this->wouldCreateCycle($ticketId, $dependsOnId, $type)) { return ['success' => false, 'error' => 'This would create a circular dependency'];