feat(disputed): Confirm My Validation button + discussion request modal
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Backend: POST /tests/{id}/request-discussion
- Only callable by the lead whose vote is 'approved' in a disputed test
- Sends notification to the rejecting lead: 'Lead X confirms their
approval and wants to discuss your rejection'
- Logs the action in audit trail
Frontend:
- 'Confirm My Validation' button (amber outline) alongside 'Change to Rejected'
- Opens a modal showing:
* Explanation: both leads must agree to finalise
* Other lead's rejection reason/notes
* What happens next (stays disputed, notification sent, either can change)
- 'Send Discussion Request' → calls the new endpoint → shows success state:
'Lead username has been notified...'
- Instruction to reach out via team channels to resolve offline
Flow summary for disputed tests:
Approving lead sees 2 options:
a) 'Confirm My Validation' → modal → send request → other lead notified
b) 'Change to Rejected' → validation modal → both agree to reject → rejected
This commit is contained in:
@@ -745,6 +745,97 @@ def sync_tempo(
|
||||
return {"results": results}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /tests/{id}/request-discussion — disputed: confirm vote + notify other lead
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/{test_id}/request-discussion")
|
||||
def request_discussion(
|
||||
test_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
|
||||
):
|
||||
"""Called when the approving lead confirms their vote in a disputed test.
|
||||
|
||||
Sends a notification to the other lead (who rejected) asking them to
|
||||
discuss and resolve the conflict. The test remains in 'disputed' state.
|
||||
"""
|
||||
from app.models.enums import TestState as ModelTestState
|
||||
from app.models.user import User as UserModel
|
||||
from app.services.notification_service import create_notification
|
||||
|
||||
test = crud_get_test_or_raise(db, test_id)
|
||||
|
||||
if test.state.value != "disputed":
|
||||
from app.domain.errors import BusinessRuleViolation
|
||||
raise BusinessRuleViolation("Test is not in disputed state")
|
||||
|
||||
role = current_user.role
|
||||
|
||||
# Identify who the "other lead" is (the one who rejected)
|
||||
if role == "red_lead" and test.red_validation_status == "approved":
|
||||
# Red approved, Blue rejected → notify Blue Lead who rejected
|
||||
rejector_id = test.blue_validated_by
|
||||
rejector_label = "Blue Lead"
|
||||
requester_label = "Red Lead"
|
||||
elif role == "blue_lead" and test.blue_validation_status == "approved":
|
||||
# Blue approved, Red rejected → notify Red Lead who rejected
|
||||
rejector_id = test.red_validated_by
|
||||
rejector_label = "Red Lead"
|
||||
requester_label = "Blue Lead"
|
||||
else:
|
||||
from app.domain.errors import BusinessRuleViolation
|
||||
raise BusinessRuleViolation(
|
||||
"You are not the approving lead in this conflict or the state is inconsistent"
|
||||
)
|
||||
|
||||
# Look up the rejecting lead's username for the message
|
||||
rejector = (
|
||||
db.query(UserModel).filter(UserModel.id == rejector_id).first()
|
||||
if rejector_id else None
|
||||
)
|
||||
rejector_name = rejector.username if rejector else rejector_label
|
||||
|
||||
# Notify the rejecting lead
|
||||
if rejector_id:
|
||||
try:
|
||||
create_notification(
|
||||
db,
|
||||
user_id=rejector_id,
|
||||
type="validation_conflict",
|
||||
title="Discussion requested on disputed test",
|
||||
message=(
|
||||
f"{requester_label} ({current_user.username}) is confirming their approval "
|
||||
f"of test '{test.name}' and wants to discuss your rejection with you. "
|
||||
f"Please reach out to resolve the disagreement."
|
||||
),
|
||||
entity_type="test",
|
||||
entity_id=str(test.id),
|
||||
)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(
|
||||
"Failed to send discussion notification: %s", e
|
||||
)
|
||||
|
||||
log_action(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
action="request_dispute_discussion",
|
||||
entity_type="test",
|
||||
entity_id=test.id,
|
||||
details={"test_name": test.name, "rejector": rejector_name},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "notification_sent",
|
||||
"message": f"Discussion request sent to {rejector_name}",
|
||||
"rejector_username": rejector_name,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /tests/import-rt — bulk import from a real Red Team engagement
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user