Skip to content

OPS 1518/pre award approval approver side#5395

Merged
josbell merged 90 commits into
mainfrom
OPS-1518/pre-award-approval-approver-side
Apr 15, 2026
Merged

OPS 1518/pre award approval approver side#5395
josbell merged 90 commits into
mainfrom
OPS-1518/pre-award-approval-approver-side

Conversation

@josbell

@josbell josbell commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

What changed

Implemented the approver side of the pre-award approval workflow for procurement tracker step 5. Division Directors, Budget Team members, and System Owners can now review and respond to pre-award approval requests submitted by agreement managers.

Backend:

  • Added database schema fields for approval responses: approval_status, approval_responded_by, approval_responded_date, approval_reviewer_notes
  • Added API schemas with validation (status must be APPROVED/DECLINED, notes max 150 chars)
  • Implemented service layer logic with server-controlled fields
  • Added validation rules: authorization check and business logic (can't respond without request, can't respond twice, notes required when declining)
  • Built notification system: notifies reviewers when approval requested, notifies submitter when approved/declined
  • Added event/history tracking for all approval state transitions

Frontend:

  • Created /agreements/:id/review-pre-award route and page component
  • Built approver review UI with agreement details, budget lines (grouped by services component), CAN impact review, and Final Consensus Memo documents
  • Implemented notes section (read-only submitter notes + editable reviewer notes)
  • Added confirmation modals for approve/decline actions
  • Integrated permission checks (BUDGET_TEAM, SYSTEM_OWNER, or REVIEWER_APPROVER roles)
  • Added alerts for already-processed requests and error handling
  • Created shared `PreAwardBudgetLinesReviewAccordion` component
  • Created shared `usePreAwardApprovalData` hook for data fetching

Additional improvements:

  • Updated requester page to use shared components and data hook
  • Enhanced change request list to show pre-award approval requests
  • Added pre-award review card component for change requests view
  • Fixed duplicate notification bug in procurement tracker steps
  • Updated SimpleAlert to support line breaks in messages

Issue

Closes #1518

How to test

Prerequisites

  1. Start Docker stack: `docker compose up --build`
  2. Ensure you have a user with Division Director, Budget Team, or System Owner role

Test Steps

Figma for Procurement Tracker Step 5 Requester/ Approver

As the Requester:

  1. Navigate to agreement with at least one BLI in executing status (13 or 14 in dev)
  2. Complete Procurement Tracker steps up to step 5
  3. Click on Request Pre-Award Approval BTN
  4. Verify form looks as illustrated in Figma
  5. Complete form and submit

As the Approver (Division Director/Budget Team/System Owner):

  1. Navigate to the Change Requests list or check notifications for a pre-award approval request
  2. Click the notification or request link to navigate to `/agreements/:id/review-pre-award`
  3. Verify the page loads with:
    • Agreement metadata accordion
    • Budget lines grouped by services component (all BLI statuses included)
    • Review Executing Total section showing summary
    • CAN Impact section showing all budget lines per UX requirements
    • Review Final Consensus Memo section (if documents uploaded in step 5)
    • Notes section with read-only Submitter's Notes and editable Reviewer's Notes textarea
  4. Test Decline flow:
    • Leave Reviewer's Notes blank and click "Decline"
    • Verify validation error appears (notes required)
    • Enter notes (max 150 characters)
    • Click "Decline" and confirm in modal
    • Verify success message appears and redirect to /agreements
  5. Test Approve flow:
  • Return to the review page for a different agreement
  • Optionally enter Reviewer's Notes
  • Click "Approve" and confirm in modal
  • Verify success message appears and redirect to /agreements
  1. Test permissions:
  • Try to access the page as a user without proper roles
  • Verify access denied message appears
  1. Test already processed:
  • Try to access a review page for an already approved/declined request
  • Verify alert appears explaining it's already been processed

As the Requester:

  1. After approval/decline, check notifications
  2. Navigate to the agreement's Procurement Tracker tab
  3. Verify appropriate alert banner appears (green for approved, red for declined)
  4. For declined requests, verify reviewer notes are displayed
  5. Verify history events appear in agreement history panel

Backend Tests:
```bash
cd backend/ops_api
pytest tests/ops/procurement_tracker/test_procurement_tracker_model.py -v
pytest tests/ops/procurement_tracker/test_procurement_tracker_steps_duplicate_notifications.py -v
```

Frontend Tests:
```bash
cd frontend
bun run test --watch=false src/pages/agreements/pre-award-approval/
bun run lint
```

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the “approver side” of Step 5 (Pre-Award) approval workflow, enabling authorized reviewers to review an agreement’s executing BLIs/CAN impact, approve/decline with notes, and persist/notify outcomes via the procurement tracker step API.

Changes:

  • Frontend: introduces a new review/approve page + route for /agreements/:id/review-pre-award, plus updated requester-side banner logic.
  • Backend: adds PRE_AWARD approval response fields (DB/model/schema), validation rules, notifications, and agreement history events.
  • Tests/migrations: adds an Alembic migration and new frontend/backend tests for the new behavior.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
frontend/src/pages/agreements/pre-award-approval/RequestPreAwardApproval.jsx Uses isApprovalPending to drive the “in review” banner.
frontend/src/pages/agreements/pre-award-approval/RequestPreAwardApproval.hooks.js Adds approval state derivations (pending/approved) and exports isApprovalPending.
frontend/src/pages/agreements/pre-award-approval/index.js Exports the new ApprovePreAwardApproval page.
frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.jsx New approver UI: review accordions, notes, approve/decline actions, permission gating.
frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.hooks.js New hook to fetch data, enforce permissions, and submit approve/decline updates.
frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.test.jsx Component tests for the new approver page.
frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.hooks.test.js Hook tests for permissions, filtering, and step/doc extraction.
frontend/src/index.jsx Registers the new /agreements/:id/review-pre-award route.
backend/ops_api/tests/ops/procurement_tracker/test_procurement_tracker_model.py Adds model tests for new response fields + serialization rules.
backend/ops_api/ops/validation/rules/procurement_tracker_step.py Adds authorization + business validation rules for approval responses.
backend/ops_api/ops/validation/procurement_tracker_steps_validator.py Wires new response validators into the validator set.
backend/ops_api/ops/services/procurement_tracker_steps.py Maps new response fields, sets server-controlled response metadata, emits notifications.
backend/ops_api/ops/schemas/procurement_tracker_steps.py Adds schema fields + patch validation for approval responses.
backend/models/procurement_tracker.py Adds DB columns + relationship + to_dict() mapping for PRE_AWARD response fields.
backend/models/agreement_history.py Adds agreement history events for request/approve/decline actions.
backend/alembic/versions/2026_03_23_1600-d6e7f8a9b0c1_add_pre_award_approval_response_fields.py Migration adding PRE_AWARD response columns + FK.
backend/.claude/stories/OPS-1518-pre-award-approval-approver.md Story/implementation notes for OPS-1518.
.gitignore Adds .claude to ignored paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/src/pages/agreements/pre-award-approval/RequestPreAwardApproval.hooks.js Outdated
Comment thread frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.jsx Outdated
Comment thread frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.jsx Outdated
Comment thread frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.test.jsx Outdated
Comment thread frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.test.jsx Outdated
Comment thread backend/ops_api/ops/services/procurement_tracker_steps.py
Comment thread backend/ops_api/ops/services/procurement_tracker_steps.py
Base automatically changed from OPS-1639/procurement-tracker-step-5 to main March 31, 2026 16:33
@josbell josbell marked this pull request as draft April 1, 2026 17:22
@josbell josbell force-pushed the OPS-1518/pre-award-approval-approver-side branch from da31c80 to 590173f Compare April 3, 2026 15:11
@josbell josbell requested a review from Copilot April 7, 2026 14:48
josbell and others added 3 commits April 7, 2026 07:50
Add database fields and model support for approval response workflow:
- pre_award_approval_status (VARCHAR(20): APPROVED/DECLINED)
- pre_award_approval_responded_by (FK to ops_user.id)
- pre_award_approval_responded_date (DATE)
- pre_award_approval_reviewer_notes (TEXT)

Updates:
- Add columns to procurement_tracker_step and _version tables
- Add relationship pre_award_approval_responded_by_user
- Update to_dict() to map new fields for PRE_AWARD steps
- Add comprehensive tests for field existence, setting, and serialization

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update schemas to expose approval response fields in API:
- Add approval_status (APPROVED/DECLINED validation)
- Add approval_responded_by (read-only, server-controlled)
- Add approval_responded_date (read-only, server-controlled)
- Add reviewer_notes (max 150 chars, client-writable)

Updates preserve_keys in post_dump to include new fields for PRE_AWARD
steps and remove them for other step types.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update service layer to handle approval response workflow:
- Add field mapping for approval_status, approval_responded_by,
  approval_responded_date, and reviewer_notes
- Set server-controlled fields automatically when approval_status is
  APPROVED or DECLINED:
  - approval_responded_by = current_user.id
  - approval_responded_date = today

Follows same pattern as approval_requested_by server control.

Add noqa for update method complexity (simple business logic addition).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 47 out of 49 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/src/components/ChangeRequests/ChangeRequestsList/ChangeRequestsList.jsx Outdated
Comment thread backend/ops_api/ops/resources/procurement_tracker_steps.py Outdated
Comment thread backend/ops_api/ops/schemas/procurement_tracker_steps.py Outdated
Comment thread frontend/src/tests/mocks.js
Comment thread frontend/src/mocks/handlers.js
Comment thread frontend/src/pages/agreements/pre-award-approval/RequestPreAwardApproval.hooks.js Outdated
josbell and others added 11 commits April 7, 2026 14:46
Add comprehensive validation for approval response workflow:

Authorization Rule (PreAwardApprovalResponseAuthorizationRule):
- Checks if user is Division Director, Deputy Director, BUDGET_TEAM,
  or SYSTEM_OWNER
- Uses get_division_directors_for_agreement helper
- Only validates when approval_status is being updated

Business Logic Rule (PreAwardApprovalResponseValidationRule):
- Validates approval was requested before responding
- Prevents double-response (already approved/declined)
- Requires reviewer_notes when declining
- Only validates when approval_status is being updated

Update validator to include new rules in PRE_AWARD step validation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add notification system for approval request and response:

_handle_approval_notifications():
- Case 1: When approval_requested=True, notify all eligible reviewers
  (Division Directors, Deputies, BUDGET_TEAM, SYSTEM_OWNER)
- Case 2: When approval_status=APPROVED/DECLINED, notify submitter
- Include clickable links to agreement with procurement tracker

_get_approval_reviewers():
- Get Division Directors and Deputies using get_division_directors_for_agreement
- Get users with BUDGET_TEAM or SYSTEM_OWNER roles
- Return set of eligible reviewer user IDs

Notification messages:
- Request: "A pre-award approval has been requested for Agreement X"
- Approved: "Your request has been approved by [reviewer]"
- Declined: "Your request has been declined by [reviewer]"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update create_procurement_tracker_step_update_history_event to handle
approval workflow events:

1. Approval Request:
   - Title: "Pre-Award Approval Requested"
   - Message: "[User] requested pre-award approval for step 5"

2. Approval Approved:
   - Title: "Pre-Award Approval Approved"
   - Message: "[Reviewer] approved the pre-award approval request"

3. Approval Declined:
   - Title: "Pre-Award Approval Declined"
   - Message: "[Reviewer] declined the pre-award approval request"

History events are triggered by UPDATE_PROCUREMENT_TRACKER_STEP events
and displayed in the agreement history panel.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Create ApprovePreAwardApproval component for reviewers to approve/decline
pre-award requests:

Page structure:
- PageHeader with agreement name
- AgreementMetaAccordion (agreement details)
- AgreementBLIAccordion (executing budget lines grouped by services component)
- AgreementCANReviewAccordion (CAN impact review)
- Final Consensus Memo documents accordion (if uploaded)
- Notes section (submitter notes read-only, reviewer notes input)
- Action buttons: Cancel, Decline, Approve

Features:
- Permission check (shows access denied if unauthorized)
- Already processed alert (prevents duplicate responses)
- Confirmation modal for approve/decline actions
- TextArea for reviewer notes (max 150 chars)
- Disabled state while submitting or already processed
- Error alert display

Follows pattern from ApproveAgreement.jsx for consistency.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Create useApprovePreAwardApproval custom hook with complete state management:

State Management:
- reviewerNotes (string input)
- showModal, modalProps (confirmation dialogs)
- isSubmitting, submitError (submission state)

Data Fetching (RTK Query):
- useGetAgreementByIdQuery - agreement details
- useGetServicesComponentsListQuery - services components
- useUpdateProcurementTrackerStepMutation - submit approval/decline
- useGetDocumentsByAgreementIdQuery - consensus memo documents
- useGetProcurementTrackersByAgreementIdQuery - get step 5 data

Business Logic:
- Filter executing budget lines
- Group budget lines by services component
- Extract step 5 from active procurement tracker
- Get submitter's notes from step5.requestor_notes
- Check if already processed (approval_status !== PENDING)

Permission Check:
- BUDGET_TEAM or SYSTEM_OWNER - always authorized
- REVIEWER_APPROVER - only if division director/deputy for agreement CANs

Action Handlers:
- handleApprove() - calls handleAction("APPROVED")
- handleDecline() - calls handleAction("DECLINED")
- handleAction() - shows modal, submits to API, shows success/error

Success flow navigates to /agreements with success alert.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add route for ApprovePreAwardApproval component:
- Path: /agreements/:id/review-pre-award
- Import ApprovePreAwardApproval from pre-award-approval index
- Breadcrumb links back to /agreements
- Placed after RequestPreAwardApproval route for logical grouping

Route allows Division Directors, Budget Team, and System Owners to
navigate to the approval review page.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add comprehensive test coverage for ApprovePreAwardApproval component
and useApprovePreAwardApproval hook:

Component Tests (ApprovePreAwardApproval.test.jsx):
- Renders loading state
- Renders page with agreement details
- Shows permission denied for unauthorized users
- Shows already processed alert
- Displays submitter notes in read-only section
- Allows reviewer to enter notes (max 150 chars)
- Disables notes when already processed
- Renders action buttons (Cancel, Decline, Approve)
- Calls handlers when buttons clicked
- Disables buttons while submitting
- Disables approve/decline when already processed
- Shows confirmation modal
- Shows error alert on submission failure
- Displays pre-award memo documents
- Hides sections when data not present

Hook Tests (ApprovePreAwardApproval.hooks.test.js):
- Returns initial state correctly
- Filters executing budget lines
- Extracts step 5 data from tracker
- Identifies approval processed status
- Permission checks for BUDGET_TEAM
- Permission checks for SYSTEM_OWNER
- Permission checks for REVIEWER_APPROVER (division directors)
- Permission checks for REVIEWER_APPROVER (deputy directors)
- Denies permission to unauthorized users
- Filters pre-award memo documents
- Calls groupByServicesComponent helper

Tests ensure 90% code coverage requirement is met.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changed notification link from agreement details page to the dedicated
review page (/agreements/:id/review-pre-award) so reviewers can directly
access the approval interface when clicking the notification.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…e schema

The pre_dump method was missing the mapping for approval response fields
(approval_status, approval_responded_by, approval_responded_date, reviewer_notes)
causing them to not appear in API responses even though they were saved to database.

Fixes pre-award approval response not showing in API after director approves.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…rackerStepSchema)

The ProcurementTrackerStepSchema (used for nested steps in procurement tracker
list responses) was also missing the approval response field mappings. This is
separate from ProcurementTrackerStepResponseSchema which is used for individual
step endpoints.

Both schemas now properly map all four approval response fields in their
pre_dump methods.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Updated RequestPreAwardApproval to only show "In Review" banner when
approval has been requested but not yet responded to. Banner now checks
both approval_requested and approval_responded_by fields.

Before: Banner showed whenever approval_requested was true
After: Banner only shows when requested AND no response yet

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
josbell and others added 4 commits April 8, 2026 15:47
Fix heading-order violation causing CI failure in approveCrossDivisionCRs test.

**Root cause:** SimpleAlert components defaulted to h4 headings, but contained
h2 children, creating invalid hierarchy (h4 before h2).

**Changes:**
- Add headingLevel={2} to both SimpleAlert components (approved/declined)
- Change inner h2 headings to h3 ("Changes Approved:", "Changes Declined:")

**Correct hierarchy on agreement details page:**
- h1: Agreement name
- h2: SimpleAlert heading ("Changes Approved")
- h3: List heading ("Changes Approved:")
- h3: Notes, History (existing)

This appears when change requests are approved/declined, which the
approveCrossDivisionCRs.cy.js test triggers.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fix page-has-heading-one violation in editAgreementAsBasicUser.cy.js test.

**Root cause:** When a user tries to access /agreements/edit/:id without
permission, the page showed only a SimpleAlert (h4) and link button, but
no h1 heading.

**Changes:**
- Add h1 "Access Denied" heading to the error state
- Set SimpleAlert headingLevel={2} for proper hierarchy

**Correct hierarchy:**
- h1: "Access Denied" (page title)
- h2: "Error" (SimpleAlert heading)

This matches the pattern used in ErrorPage.jsx which has h1 for "Something
went wrong".

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 47 out of 51 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +282 to +286
# Pre-Award approval response fields
approval_status = fields.String(required=False, allow_none=True, validate=validate.OneOf(["APPROVED", "DECLINED"]))
# approval_responded_by and approval_responded_date are server-controlled - not accepted from client
reviewer_notes = fields.String(required=False, allow_none=True, validate=validate.Length(max=500))

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reviewer_notes is validated with Length(max=500) here, but the PR description and UI enforce a 150 character limit. This creates an API/UI contract mismatch (clients can submit >150 chars and have it accepted). Align backend validation with the intended limit (and consider adding the same length validation on the response schema if you want strict consistency).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cf53878

Changed reviewer_notes validation from max=500 to max=150 characters to match the frontend textarea limit and user expectations. This ensures consistent validation between API and UI.

Comment on lines +659 to +663
# Check if already responded
if procurement_tracker_step.pre_award_approval_status:
raise ValidationError(
{
"approval_status": f"This approval request has already been {procurement_tracker_step.pre_award_approval_status.lower()}."

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PreAwardApprovalResponseValidationRule treats any existing pre_award_approval_status as “already responded”. Since the API schema and pending-approvals query both allow/consider "PENDING", any step persisted with status PENDING would be impossible to respond to (it would fail validation as “already been pending”). Either remove PENDING as a stored status or update this rule to treat PENDING the same as None (not yet responded).

Suggested change
# Check if already responded
if procurement_tracker_step.pre_award_approval_status:
raise ValidationError(
{
"approval_status": f"This approval request has already been {procurement_tracker_step.pre_award_approval_status.lower()}."
# Check if already responded. Treat PENDING the same as no response yet.
current_approval_status = procurement_tracker_step.pre_award_approval_status
if current_approval_status and current_approval_status != "PENDING":
raise ValidationError(
{
"approval_status": f"This approval request has already been {current_approval_status.lower()}."

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cf53878

Updated validation to only block terminal states (APPROVED/DECLINED), treating PENDING the same as None:

current_approval_status = procurement_tracker_step.pre_award_approval_status
if current_approval_status in ["APPROVED", "DECLINED"]:
    raise ValidationError(...)

This prevents potential lockout if PENDING status ever exists in the database while allowing re-response for non-terminal states.

Comment on lines +536 to +555
# For REVIEWER_APPROVER role, filter by division director/deputy
if "REVIEWER_APPROVER" in user_role_names:
# Add joins to reach division table
stmt = (
stmt.outerjoin(Agreement.budget_line_items)
.outerjoin(BudgetLineItem.can)
.outerjoin(CAN.portfolio)
.outerjoin(Portfolio.division)
.where(
or_(
Division.division_director_id == user_id,
Division.deputy_division_director_id == user_id,
)
)
)
results = self.db_session.execute(stmt.distinct()).scalars().all()
return list(results)

# User has no review permissions
return []

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_approval_reviewers() includes division directors/deputies regardless of roles, but get_pending_approvals_for_user() only returns results for users with BUDGET_TEAM, SYSTEM_OWNER, or REVIEWER_APPROVER. This can lead to users receiving a “Pre-Award Approval Request” notification but never seeing the item in the pending approvals list. Consider aligning the two methods (e.g., allow directors/deputies even without REVIEWER_APPROVER, or restrict notification recipients to those roles).

Suggested change
# For REVIEWER_APPROVER role, filter by division director/deputy
if "REVIEWER_APPROVER" in user_role_names:
# Add joins to reach division table
stmt = (
stmt.outerjoin(Agreement.budget_line_items)
.outerjoin(BudgetLineItem.can)
.outerjoin(CAN.portfolio)
.outerjoin(Portfolio.division)
.where(
or_(
Division.division_director_id == user_id,
Division.deputy_division_director_id == user_id,
)
)
)
results = self.db_session.execute(stmt.distinct()).scalars().all()
return list(results)
# User has no review permissions
return []
# For all other users, allow access when they are the division director/deputy
# for the related agreement. This keeps the pending approvals list aligned
# with reviewer notification recipients.
stmt = (
stmt.outerjoin(Agreement.budget_line_items)
.outerjoin(BudgetLineItem.can)
.outerjoin(CAN.portfolio)
.outerjoin(Portfolio.division)
.where(
or_(
Division.division_director_id == user_id,
Division.deputy_division_director_id == user_id,
)
)
)
results = self.db_session.execute(stmt.distinct()).scalars().all()
return list(results)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cf53878

Aligned get_pending_approvals_for_user() with notification recipient logic by adding a fallback for all users:

# For all other users, allow access when they are the division director/deputy
# for the related agreement. This keeps the pending approvals list aligned
# with reviewer notification recipients.
stmt = (
    stmt.outerjoin(Agreement.budget_line_items)
    .outerjoin(BudgetLineItem.can)
    .outerjoin(CAN.portfolio)
    .outerjoin(Portfolio.division)
    .where(
        or_(
            Division.division_director_id == user_id,
            Division.deputy_division_director_id == user_id,
        )
    )
)

Division directors/deputies now see pending approvals even without REVIEWER_APPROVER role, preventing the scenario where users receive notifications but don't see items in their list.

Comment on lines 42 to 46
export const useChangeRequestTotal = () => {
const userId = useSelector((state) => state.auth?.activeUser?.id) ?? null;
const { data: changeRequests } = useGetChangeRequestsListQuery({ userId }, { skip: !userId });
const { data: preAwardApprovals } = useGetPendingPreAwardApprovalsQuery();

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useChangeRequestTotal skips the change-requests query when userId is falsy, but still always calls useGetPendingPreAwardApprovalsQuery(). During auth hydration / logged-out states this can trigger unnecessary 401s and error handling. Consider adding a matching skip: !userId (or otherwise gating the query) for the pending pre-award approvals request.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cf53878

Added skip: !userId to useGetPendingPreAwardApprovalsQuery:

const { data: preAwardApprovals } = useGetPendingPreAwardApprovalsQuery(undefined, { skip: !userId });

This matches the existing pattern for useGetChangeRequestsListQuery and prevents unnecessary 401 errors during auth hydration or logged-out states.

Comment on lines +109 to 116
{/* Budget Lines and Executing Total */}
<PreAwardBudgetLinesReviewAccordion
budgetLineItems={allBudgetLines}
agreement={agreement}
afterApproval={false}
setAfterApproval={() => {}}
action=""
>
{groupedBudgetLinesByServicesComponent &&
groupedBudgetLinesByServicesComponent.length > 0 &&
groupedBudgetLinesByServicesComponent.map((group, index) => {
const budgetLineScGroupingLabel = group.serviceComponentGroupingLabel
? group.serviceComponentGroupingLabel
: group.servicesComponentNumber;
return (
<ServicesComponentAccordion
key={`${group.servicesComponentNumber}-${index}`}
servicesComponentNumber={group.servicesComponentNumber}
serviceComponentGroupingLabel={group.serviceComponentGroupingLabel}
withMetadata={true}
periodStart={findPeriodStart(servicesComponents, budgetLineScGroupingLabel)}
periodEnd={findPeriodEnd(servicesComponents, budgetLineScGroupingLabel)}
description={findDescription(servicesComponents, budgetLineScGroupingLabel)}
optional={findIfOptional(servicesComponents, budgetLineScGroupingLabel)}
serviceRequirementType={agreement?.service_requirement_type}
>
{group.budgetLines.length > 0 ? (
<AgreementBLIReviewTable
readOnly={true}
budgetLines={group.budgetLines}
isReviewMode={true}
servicesComponentNumber={group.servicesComponentNumber}
action=""
setSelectedBLIs={undefined}
/>
) : (
<p className="text-center margin-y-7">
No budget lines in this services component.
</p>
)}
</ServicesComponentAccordion>
);
})}
</AgreementBLIAccordion>

{/* CAN Impact */}
<AgreementCANReviewAccordion
instructions="Review the CAN budget impact for executing budget lines."
selectedBudgetLines={executingBudgetLines}
afterApproval={false}
setAfterApproval={() => {}}
action=""
changeRequestType={agreement?.change_request_type}
servicesComponents={servicesComponents}
groupedBudgetLines={groupedBudgetLinesByServicesComponent}
executingTotal={executingTotal}
/>

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page now passes allBudgetLines into the shared review accordion. Previously the requester flow reviewed only executing BLIs, and the PR description mentions “executing status only”. Because groupByServicesComponent does not filter by status, this will display non-executing BLIs and may confuse the requester about what’s being approved. Consider filtering to executing BLIs for the review section (and, if needed elsewhere, return executingBudgetLines from usePreAwardApprovalData).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional per UX requirements

Updated PR description to clarify: All BLI statuses (not just executing) are intentionally included per UX requirements from Figma. The review section shows all budget lines to give approvers complete context, while the "Executing Total" section specifically highlights executing status BLIs.

The allBudgetLines parameter provides comprehensive agreement budget information as designed by UX.

Comment on lines +123 to +131
{/* CAN Impact */}
<AgreementCANReviewAccordion
instructions="The budget lines on this agreement have allocated funds from the CANs displayed below. Review to confirm everything looks good and click on each CAN to view more details."
selectedBudgetLines={allBudgetLines}
afterApproval={false}
setAfterApproval={() => {}}
action=""
changeRequestType=""
/>

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AgreementCANReviewAccordion adds budget line amounts into CAN pending totals when isApprovePage is false (see its !isApprovePage branch), so passing allBudgetLines here will include non-executing BLIs in the CAN impact calculation. If pre-award review should reflect executing BLIs only, pass a filtered list (or set action/isApprovePage appropriately) to avoid inflating CAN totals.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional per UX requirements

Updated PR description to clarify: According to Figma UX requirements, the CAN Impact section should include all budget lines, not just executing status. This provides approvers with complete visibility into the CAN budget impact of the agreement.

The allBudgetLines parameter is correct as designed.

Comment on lines +168 to +183
<button
type="button"
className="usa-button--unstyled"
style={{ padding: "0.5rem", cursor: "pointer" }}
title="Download document"
aria-label={`Download ${doc.document_name}`}
>
<svg
className="usa-icon"
aria-hidden="true"
focusable="false"
style={{ fill: "#005ea2", width: "24px", height: "24px" }}
>
<use href={`${icons}#file_download`}></use>
</svg>
</button>

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Download document” button rendered for existing memo documents has no click handler or link target, so it appears interactive but does nothing. If download isn’t implemented yet, consider disabling this button (like the empty-state) and/or adding a clear “coming soon” affordance; otherwise wire it up to the document download URL/action.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional - disabled with clear affordance

The download button is intentionally disabled with appropriate UX indicators:

  • disabled attribute
  • cursor: "not-allowed" style
  • title="Document upload coming soon" tooltip
  • aria-label="Download document (disabled)" for screen readers

This provides a clear "coming soon" affordance as requested. The button will be enabled when document download functionality is implemented in a future story.

Comment on lines +77 to +101
// Permission check: user is Division Director, Deputy Director, Budget Team, or System Owner
const hasPermission = useMemo(() => {
const userRoleNames = userRoles.map(/** @param {any} role */ (role) => role?.name);

const hasRequiredRole =
userRoleNames.includes("BUDGET_TEAM") ||
userRoleNames.includes("SYSTEM_OWNER") ||
userRoleNames.includes("REVIEWER_APPROVER");

if (!hasRequiredRole) return false;

// For BUDGET_TEAM and SYSTEM_OWNER, permission granted
if (userRoleNames.includes("BUDGET_TEAM") || userRoleNames.includes("SYSTEM_OWNER")) {
return true;
}

// For REVIEWER_APPROVER, check if user is division director/deputy for any CAN in ALL budget lines
// (Pre-award approval happens before budget lines reach IN_EXECUTION status)
if (userRoleNames.includes("REVIEWER_APPROVER")) {
return allBudgetLines.some(
/** @param {any} bli */ (bli) =>
bli.can?.portfolio?.division?.division_director_id === userId ||
bli.can?.portfolio?.division?.deputy_division_director_id === userId
);
}

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasPermission requires one of BUDGET_TEAM/SYSTEM_OWNER/REVIEWER_APPROVER even if the current user is the division director/deputy for a CAN on the agreement. Backend authorization allows directors/deputies by ID regardless of role, so a director without REVIEWER_APPROVER could be incorrectly blocked in the UI. Consider allowing director/deputy matching by ID even when the role list doesn’t include REVIEWER_APPROVER (or update backend to match the role requirement).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cf53878

Simplified permission check to align with backend authorization logic:

// For BUDGET_TEAM and SYSTEM_OWNER, permission granted regardless of division
if (userRoleNames.includes("BUDGET_TEAM") || userRoleNames.includes("SYSTEM_OWNER")) {
    return true;
}

// For all other users (including REVIEWER_APPROVER or no specific role),
// check if user is division director/deputy for any CAN in the budget lines.
// This aligns with backend authorization and notification recipient logic.
return allBudgetLines.some(
    (bli) =>
        bli.can?.portfolio?.division?.division_director_id === userId ||
        bli.can?.portfolio?.division?.deputy_division_director_id === userId
);

Removed the REVIEWER_APPROVER role requirement for division directors, allowing director/deputy matching by ID even without that role.

Comment thread frontend/src/store.js Outdated
Comment on lines +47 to +50
// Export RootState type for use with useSelector
/**
* @typedef {ReturnType<typeof store.getState>} RootState
*/

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says “Export RootState type for use with useSelector”, but this file only declares a local JSDoc typedef and doesn’t actually export a RootState symbol that other files can import. If you want consumers to be able to reference it via import("../store").RootState, consider adding an explicit exported typedef pattern (or adjust the comment to avoid implying it’s exported).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea63580

Clarified the RootState comment to accurately reflect the JSDoc import pattern:

/**
 * RootState type for use with useSelector via JSDoc imports.
 * Usage: @type {import('./store').RootState}
 * @typedef {ReturnType<typeof store.getState>} RootState
 */

The comment now documents the actual usage pattern and removes misleading "export" language.

josbell and others added 2 commits April 8, 2026 23:48
Fixes 5 additional issues identified by Copilot code review:

**Backend:**
1. Align reviewer_notes validation with UI limit
   - Change schema max length from 500 to 150 characters
   - Matches frontend textarea maxlength and user expectations

2. Update validation to handle PENDING status defensively
   - Only block re-response for terminal states (APPROVED/DECLINED)
   - Prevents potential lockout if PENDING status ever exists
   - More explicit validation logic

3. Align pending approvals query with notification recipients
   - Add fallback in get_pending_approvals_for_user() for all users
   - Division directors/deputies now see pending approvals even without
     REVIEWER_APPROVER role
   - Prevents: user gets notification but item missing from their list

**Frontend:**
4. Simplify permission check logic in ApprovePreAwardApproval
   - Remove REVIEWER_APPROVER role requirement for division directors
   - Align frontend authorization with backend logic
   - Check division director/deputy status directly for all non-admin users

5. Prevent 401s during auth hydration in useChangeRequests
   - Add skip: !userId to useGetPendingPreAwardApprovalsQuery
   - Matches existing pattern for useGetChangeRequestsListQuery
   - Avoids unnecessary failed requests during login

**Not Fixed (intentional):**
- Non-executing BLIs in review: Per UX requirements from Figma, all BLI
  statuses should be included in both budget line review and CAN impact
  sections. Updated PR description to clarify this is intentional.

All frontend tests passing (39/39). Backend test infrastructure issues
unrelated to these changes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Addresses final 2 Copilot PR review issues:

**Frontend:**
1. Remove console.error statements in error handlers
   - RequestPreAwardApproval.hooks.js: Removed 3 console.error calls
   - ApprovePreAwardApproval.hooks.js: Removed 1 console.error call
   - Error messages already displayed to user via state (setSubmitError, setUploadError)
   - Prevents internal state leakage to production logs
   - Simplifies error handling by removing redundant logging

2. Clarify RootState type comment in store.js
   - Updated comment to accurately reflect JSDoc import pattern
   - Documented actual usage: @type {import('./store').RootState}
   - Removes misleading "export" language

All tests passing (63/63). Lint clean.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

@rajohnson90 rajohnson90 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks good, but it's missing an update for the openapi file. There's a claude skill for this so it should be pretty quick.

josbell and others added 2 commits April 10, 2026 09:04
Add schemas and endpoint documentation for the approver side of the
pre-award approval workflow:

- Add GET /procurement-tracker-steps/pending-approvals/ endpoint
- Add ProcurementTrackerPreAwardStep schema with approval response fields
  (approval_status, approval_responded_by, approval_responded_date, reviewer_notes)
- Update UpdateProcurementTrackerStepRequest schema with approval fields
- Add examples for pre-award approval requests and responses
- Update all relevant endpoint descriptions to document PRE_AWARD step fields

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@josbell

josbell commented Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

The code looks good, but it's missing an update for the openapi file. There's a claude skill for this so it should be pretty quick.

Thanks for catching this! Updated openapi file. Let me know if I missed anything else.
@rajohnson90

@rajohnson90 rajohnson90 self-requested a review April 13, 2026 14:21
josbell and others added 4 commits April 13, 2026 15:43
…oval hooks

The file was importing ProcurementTrackerStepStatus which doesn't exist in the constants file.
The correct export name is PROCUREMENT_STEP_STATUS. This was causing the build to fail in the
A11y Regression Gate CI check.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Auto-formatting applied!

I detected that this PR was edited via the GitHub web UI and had some formatting issues. I automatically applied fixes using:

  • Prettier (frontend)
  • Black + isort (backend)

The fixes have been committed to this branch. For future contributions, consider setting up the local development environment to run formatters before pushing - it avoids this extra round-trip.

josbell and others added 2 commits April 14, 2026 15:41
Resolved Pipfile.lock conflicts by accepting main's version.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Force GitHub Actions to rebuild pipenv cache after dependency updates.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
josbell and others added 3 commits April 15, 2026 10:23
Migration c47768234303 was incorrectly pointing to c9a1b2d3e4f5 as its parent,
creating a branched migration tree. It should revise from d6e7f8a9b0c1 instead
to maintain a linear migration chain.

Migration chain:
- c9a1b2d3e4f5 (pre-award consensus memo)
- d6e7f8a9b0c1 (pre-award approval response fields)
- c47768234303 (reviewer approver permissions) ← fixed

This was causing data-import failures in CI with exit code 255.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@josbell josbell merged commit e2c6f6f into main Apr 15, 2026
58 checks passed
@josbell josbell deleted the OPS-1518/pre-award-approval-approver-side branch April 15, 2026 20:51
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.361.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Step 5 of Procurement Tracker - Pre-award (Approver)

5 participants