Skip to content

Commit f9bf6a1

Browse files
committed
feat(OPS-2280): Award approval review page, BLI transitions, and validation rules
1 parent 1f42156 commit f9bf6a1

10 files changed

Lines changed: 1337 additions & 2 deletions

File tree

backend/ops_api/ops/validation/procurement_tracker_steps_validator.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def _get_validators(
7777
from ops_api.ops.validation.rules.procurement_tracker_step import (
7878
AcquisitionPlanningRequiredFieldsRule,
7979
AwardAgreementDataRequiredRule,
80+
AwardApprovalResponseAuthorizationRule,
81+
AwardApprovalResponseValidationRule,
8082
AwardCompletionRequiredFieldsRule,
8183
CompletedByUpdateAuthorizationRule,
8284
EvaluationCompletionRequiredFieldsRule,
@@ -154,6 +156,8 @@ def _get_validators(
154156
ResourceExistsRule(),
155157
UserAssociationRule(),
156158
AwardAgreementDataRequiredRule(),
159+
AwardApprovalResponseAuthorizationRule(),
160+
AwardApprovalResponseValidationRule(),
157161
AwardCompletionRequiredFieldsRule(),
158162
CompletedByUpdateAuthorizationRule(),
159163
NoUpdatingCompletedProcurementStepRule(),

backend/ops_api/ops/validation/rules/procurement_tracker_step.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,3 +775,62 @@ def validate(self, procurement_tracker_step: ProcurementTrackerStep, context: Va
775775
raise ValidationError(
776776
{"reviewer_notes": "Reviewer notes are required when declining an approval request."}
777777
)
778+
779+
780+
class AwardApprovalResponseAuthorizationRule(ValidationRule):
781+
"""
782+
Validates that the user responding to an award approval request is authorized.
783+
Only BUDGET_TEAM or SYSTEM_OWNER may respond to AWARD step approvals.
784+
"""
785+
786+
@property
787+
def name(self) -> str:
788+
return "AWARD Approval Response Authorization"
789+
790+
def validate(self, procurement_tracker_step: ProcurementTrackerStep, context: ValidationContext) -> None:
791+
if procurement_tracker_step.step_type != ProcurementTrackerStepType.AWARD:
792+
return
793+
if "approval_status" not in context.updated_fields:
794+
return
795+
796+
user_id = context.user.id
797+
user = context.db_session.get(User, user_id)
798+
if user:
799+
user_role_names = {role.name for role in user.roles}
800+
if "BUDGET_TEAM" in user_role_names or "SYSTEM_OWNER" in user_role_names:
801+
return
802+
803+
raise AuthorizationError(
804+
f"User {user_id} is not authorized to respond to award approval requests. "
805+
"Only BUDGET_TEAM or SYSTEM_OWNER members may approve award requests.",
806+
"ProcurementTrackerStep",
807+
)
808+
809+
810+
class AwardApprovalResponseValidationRule(ValidationRule):
811+
"""
812+
Validates that award approval responses are valid:
813+
- Approval must have been requested first
814+
- Cannot respond if already responded (APPROVED/DECLINED)
815+
"""
816+
817+
@property
818+
def name(self) -> str:
819+
return "AWARD Approval Response Validation"
820+
821+
def validate(self, procurement_tracker_step: ProcurementTrackerStep, context: ValidationContext) -> None:
822+
if procurement_tracker_step.step_type != ProcurementTrackerStepType.AWARD:
823+
return
824+
if "approval_status" not in context.updated_fields:
825+
return
826+
827+
if not procurement_tracker_step.award_approval_requested:
828+
raise ValidationError(
829+
{"approval_status": "Cannot respond to an award approval request that has not been submitted."}
830+
)
831+
832+
current_status = procurement_tracker_step.award_approval_status
833+
if current_status in ["APPROVED", "DECLINED"]:
834+
raise ValidationError(
835+
{"approval_status": f"This award approval request has already been {current_status.lower()}."}
836+
)

0 commit comments

Comments
 (0)