# RIMS / Fund Administration — Manual Testing Guide

Walkthrough script for verifying `apps/rims/` against **SRS §5.1 (RIMS/Fund Administration)** and **SRS §5.4 (Finance Management)**. Each case names the **seeder account** to log in with, its **role**, its **password**, and the SRS Functional Requirement (FR) the step validates.

Flows that are not yet implemented, are partial, or cannot be exercised with the current seeds are tracked separately in [`rims_gaps.md`](rims_gaps.md).

---

## 1. Preparation

All commands use the Docker stack defined in `docker-compose.yml`. The `web` image has the libmagic and dev dependencies the test flow needs.

```bash
# Bring up the stack
docker compose up -d web

# Bootstrap groups, demo users, and the RIMS demo dataset
docker compose exec web python manage.py migrate --noinput
docker compose exec web python manage.py seed_roles
docker compose exec web python manage.py seed_users --volume demo
docker compose exec web python manage.py seed_rims --reset --comprehensive
```

App URL: `http://localhost:${WEB_HOST_PORT:-8000}/` (this repo's `.env` may set `WEB_HOST_PORT=8090`).

> **Re-seed after destructive flows** (close-out, force close-out, withdraw, reject). The `--reset --comprehensive` pair restores all e2e calls, applications, awards, budgets, and projects.

> **Notifications:** Email/in-app dispatch goes through Celery + SMTP. Test cases that mention "notifies user X" are verified by checking the **Notification feed** in the topnav, **not** by inspecting an inbox unless SMTP is wired. See `rims_gaps.md` for details.

---

## 2. Seeded test accounts

After `seed_users` + `seed_rims --reset --comprehensive`, the following users exist. **Password for all of them is `password123`** (override `seed_rims` with `--password <pw>` if needed).

### 2.1 RIMS-specific personas (`seed_rims`)

| Email | Role | Used for |
|---|---|---|
| `system.admin@demo.local` | SYSTEM_ADMIN | Cross-module admin, audit-log read, perms sanity |
| `program.director@demo.local` | PROGRAM_DIRECTOR | Approve funding records, force award close-out |
| `finance.director@demo.local` | FINANCE_DIRECTOR | Approve funding records, approve disbursements, reverse payment batches |
| `program.manager@demo.local` | PROGRAM_MANAGER | Manage calls, screen applications, oversee projects |
| `grants.manager@demo.local` | GRANTS_MANAGER | Author funding records, run calls, shortlist, award, close-out |
| `reviewer@demo.local` | REVIEWER | Scoring queue (default reviewer for `e2e-reviewer-call`) |
| `reviewer2@demo.local` | REVIEWER | Second reviewer (used for conflict + blind review flows) |
| `finance@demo.local` | FINANCE_OFFICER | Budget setup, disbursement requests, expenditure, donor reports |
| `mel@demo.local` | MEL_OFFICER | Read-only dashboard view |
| `scholar@demo.local` | SCHOLAR | Applicant for `e2e-reviewer-call` |
| `award.recipient@demo.local` | SCHOLAR | Acknowledge an award (post-award) |
| `eligibility.applicant@demo.local` | SCHOLAR | PhD-only eligibility rejection flow (`e2e-eligibility-call`) |
| `interview.applicant@demo.local` | SCHOLAR | Interview-gate flow (`e2e-interview-call`) |
| `conflict.applicant@demo.local` | SCHOLAR | Conflicting reviewer recommendations (`e2e-conflict-call`) |
| `blind.applicant@demo.local` | SCHOLAR | Blind review flow (`e2e-blind-call`) |
| `scholar2/3/4/5@demo.local` | SCHOLAR | Cohort fixtures for the scholar list and graduation flag |
| `finance.manager@demo.local` | FINANCE_MANAGER | Two-eyes disbursement approval distinct from FINANCE_DIRECTOR; budget approval at line level |
| `program.coordinator@demo.local` | PROGRAM_COORDINATOR | Approve budget revisions per FRFM011/029 |
| `project.manager@demo.local` | PROJECT_MANAGER | Raise disbursement requests per SRS §5.4 (alternative to Finance Officer) |
| `programme.officer@demo.local` | PROGRAMME_OFFICER | Application screening per SRS §5.1.1.2/3 (distinct from Grants Manager) |

> The four personas above are added by `seed_rims` (lines 165–168 + 1489–1499 of `apps/rims/grants/management/commands/seed_rims.py`). They each get a populated dashboard on first login: Programme Officer is attached to `e2e-po-screening-call` (a call with `programme_officer_screening_enabled=True`), Project Manager owns at least one pending DisbursementRequest, and Finance Manager / Program Coordinator share queues with the existing approver tiers.

### 2.2 Named personas from `seed_users`

| Email | Role | Notes |
|---|---|---|
| `auditor.demo@iilmp.local` | AUDITOR | Read-only audit log access (NFRFA011) |
| `finance.demo@iilmp.local` | FINANCE_OFFICER | Alternative finance officer (no RIMS seed data attached) |
| `mel.demo@iilmp.local` | MEL_OFFICER | RIMS dashboard read access |
| `reviewer.demo@iilmp.local` | REVIEWER | Alternative reviewer (no RIMS seed assignments) |
| `scholar.demo@iilmp.local` | SCHOLAR | Alternative applicant (no seeded application) |

### 2.3 Seeded grant calls

| Slug | Purpose |
|---|---|
| `demo-seed-call` | Default published call with seeded award + budget + project/milestones + a pending disbursement |
| `e2e-pilot-call` | Pilot cohort with light document checklist |
| `e2e-reviewer-call` | Scholar application **under review** with a pending review for `reviewer@demo.local` |
| `e2e-eligibility-call` | PhD-only automated eligibility rule (used to assert rejection) |
| `e2e-interview-call` | Shortlisted application requiring interview before award |
| `e2e-conflict-call` | Two submitted reviews with diverging recommendations |
| `e2e-blind-call` | Blind review (reviewer sees code, not applicant identity) |
| `e2e-po-screening-call` | Submitted application sitting in `programme.officer@demo.local`'s queue (`programme_officer_screening_enabled=True`) |

---

## 3. SP1 — Grant Planning & Setup (SRS §5.1.1.1)

Covers funding record lifecycle and donor + strategic objective master data. PRD mapping: `apps/rims/grants/PRD_MAPPING.md` (GPS001–027).

### 3.1 Pre-seed master data — Donors (FRFA-GPS007–009)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` (GRANTS_MANAGER, `password123`) | `/rims/operations/donors/` | Three seeded donors are listed (Bilateral, Multilateral, Foundation) | FRFA-GPS007 |
| 2 | same | `+ New donor` → fill name, type, country, contact email | Created donor appears in list, audit row written | FRFA-GPS008, NFRFA011 |
| 3 | same | Try to create a second donor with identical name + country | Form returns duplicate-detection inline error | FRFA-GPS009 |

### 3.2 Strategic objectives (FRFA-GPS006)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/operations/strategic-objectives/` | Five seeded objectives (climate-resilient agriculture, etc.) | GPS006 |
| 2 | same | `+ New objective` → name, description, active=on | Objective is now selectable in funding-record form | GPS006 |

### 3.3 Funding record draft → submit → approve (FRFA-GPS001–027)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/funding/` | Seeded funding record "Seeded PRD funding record — flagship grants window" listed (status APPROVED) | GPS001 |
| 2 | same | `+ New funding record` → fill General Information, donor, strategic objectives, dates | Form saves as **Draft** with a system-generated draft ID | GPS003 |
| 3 | same | Open the draft → **Budget structure** → add three lines totalling the grant amount | Total budget pill matches grant amount; mismatch shows inline error | GPS010-GPS013 |
| 4 | same | **Staff assignment** → leave grant manager blank → **Submit for approval** | Submit blocked with "assignment gate" error | GPS017, GPS018 |
| 5 | same | Add grant manager / finance officer / programme lead → **Submit for approval** | Status moves to **PENDING_APPROVAL**, approver gets a notification | GPS019, GPS020, GPS021 |
| 6 | `program.director@demo.local` (PROGRAM_DIRECTOR, `password123`) | `/rims/grants/manage/funding/approvals/` | Pending record listed in approval queue | GPS021 |
| 7 | same | Open record → **Approve** | Status flips to **APPROVED**, Grant ID issued, audit row written, grants manager notified | GPS002, GPS023, GPS025 |
| 8 | same | Repeat with a fresh draft, choose **Reject** with comments | Status → **DRAFT_REVISION_REQUIRED**, rejection comment visible to grants manager | GPS024 |
| 9 | `grants.manager@demo.local` | Open a DRAFT_REVISION_REQUIRED record → **Edit** → **Submit for approval** | New version row added under `/rims/grants/manage/funding/<pk>/versions/`; routes back to approver | GPS026 |
| 10 | `grants.manager@demo.local` | Try to edit an APPROVED record's budget lines | Edit form is locked to non-destructive fields per status guard | GPS027 |

### 3.4 Audit trail (NFRFA011)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `auditor.demo@iilmp.local` (AUDITOR, `password123`) | `/rims/operations/audit-log/` | Read-only log of every funding-record + call + application transition above | NFRFA011, GPS025 |
| 2 | same | Try POST against any audit URL via dev tools | 403 PermissionDenied ("Audit log is read-only.") | NFRFA011 |

---

## 4. SP2 — Call Setup (SRS §5.1.1.2)

PRD mapping references: FRFA-CS006/008/013/019/021/030/031.

### 4.1 Create + publish a call (FRFA-CS001–021)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/calls/new/` | Form loads with funding-record selector (only APPROVED records with uncommitted balance) | FRFA-CS001, FRFA-CS002 |
| 2 | same | Pick a funding record, then enter title, description, opens_at, closes_at, scoring rubric | Form saves as **Draft** | FRFA-CS006, FRFA-CS008 |
| 3 | same | Open the draft → **Required documents** → add at least one guideline + checklist row | Documents appear on the call detail page | FRFA-CS013 |
| 4 | same | Try **Publish** without eligibility rule or guideline document | Pre-publish validation blocks with summary of missing items | FRFA-CS019, NFRFA007, NFRFA008 |
| 5 | same | Add `EligibilityRule` (e.g., nationality, blank list) + guideline → **Publish** | Status → **PUBLISHED**, call visible at `/rims/grants/calls/`, committed balance updates | FRFA-CS021 |

### 4.2 Withdraw a published call (FRFA-CS030)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/calls/<slug>/withdraw/` on the call you just published | Form requires a withdrawal reason | FRFA-CS030 |
| 2 | same | Submit with reason | Status → **WITHDRAWN**, committed balance released, audit row written | FRFA-CS030, FRFA-CS031 |

### 4.3 Close submissions + complete (FRFA-CS031)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/calls/demo-seed-call/close-submissions/` | Submission portal locked; new applications rejected | FRFA-CS031 |
| 2 | same | Once all awards are decided, `/rims/grants/manage/calls/demo-seed-call/complete/` | Status → **COMPLETED**, call removed from public list | FRFA-CS031 |

### 4.4 Type-configured form variations (FRFA-CS005, FRFA-CS009, FRFA-CS016)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/calls/new/` → pick **Grant**, then **Scholarship**, then **Fellowship**, then **Challenge** | Each `CallType` exposes its type-specific required admin docs at publish (e.g. Fellowship requires `fellowship_terms`; Challenge requires `innovation_brief`) | FRFA-CS005, FRFA-CS009, FRFA-CS016 |

Source: `apps/rims/grants/constants.py:61-69`, `apps/rims/grants/models.py:224-228`.

### 4.5 Eligibility-rule structured controls (FRFA-CS010)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | Open a draft call → **Eligibility rules** → add a typed rule (`NATIONALITY` / `DEGREE` / `INSTITUTION`) via JSON params | Rule persists with `RuleType` and is applied during automated screening (see §5.2) | FRFA-CS010 |

Source: `apps/rims/grants/models.py:698-708`.

### 4.6 Auto-close acknowledgement (FRFA-CS022) + auto-close beat (FRFA-CS031)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | Before publishing a call, untick "I acknowledge the call will auto-close on `<closes_at>`" and Submit | Form is rejected; `auto_close_confirmed_at` remains null | FRFA-CS022 |
| 2 | shell | `docker compose exec web python manage.py shell -c "from apps.rims.grants.tasks import close_expired_calls; close_expired_calls()"` against a call past its deadline | Status → **CLOSED**; in-flight draft applications transition to `EXPIRED_DRAFT` | FRFA-CS031 |

Source: `apps/rims/grants/models.py:382`, `apps/rims/grants/tasks.py:395`.

### 4.7 Notification config + reminder cadence (FRFA-CS023, FRFA-CS025)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/calls/<slug>/notifications/` → configure `reminder_intervals_days` JSON (e.g. `[14, 7, 1]`) | `GrantCallNotificationConfig` row persists; reminder dispatch fires at the configured offsets | FRFA-CS023, FRFA-CS025 |

Source: `GrantCallNotificationConfig` model at `apps/rims/grants/models.py:576-627`.

### 4.8 Inline-edit window for PUBLISHED calls (FRFA-CS028)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | For a PUBLISHED call, open `/rims/grants/manage/calls/<slug>/edit/` | Restricted form (`GrantCallActiveEditForm`) only exposes non-critical fields (description, contact, etc.); deadlines and scoring rubric are read-only | FRFA-CS028 |

Source: `apps/rims/grants/forms.py:333`.

### 4.9 Deadline extension (FRFA-CS029)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/calls/<slug>/extend-deadline/` → submit new `closes_at` + reason | Deadline updated; in-progress applicants notified; audit row written | FRFA-CS029 |

Source: `apps/rims/grants/services.py:3022`.

### 4.10 Withdraw audit fields (FRFA-CS030)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `auditor.demo@iilmp.local` | After running §4.2, navigate to `/rims/operations/audit-log/` and filter by the call slug | `withdrawal_reason`, `withdrawn_at`, `withdrawn_by` fields visible on the call row | FRFA-CS030, NFRFA011 |

Source: `apps/rims/grants/models.py:527`.

### 4.11 Version history snapshots (FRFA-CS033)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | After editing a published call via §4.8, navigate to `/rims/grants/manage/calls/<slug>/versions/` | New `GrantCallVersion` snapshot row appears with diff against the previous version | FRFA-CS033 |

Source: `apps/rims/grants/services.py:2978`, `apps/rims/grants/models.py:1937`.

### 4.12 Reporting calendar (FRFA-GPS011–016)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/funding/<pk>/` for a funding record with a reporting frequency configured | `ReportingCalendarEntry` rows render under the funding-record detail (4+ seeded entries on demo data) | FRFA-GPS011, FRFA-GPS012, FRFA-GPS013, FRFA-GPS014, FRFA-GPS015, FRFA-GPS016 |

Source: `apps/rims/grants/services.py:3141`, `apps/rims/grants/models.py:2020`.

### 4.13 Phase 2 gaps (not yet implemented)

> The submission-settings configuration UI (FRFA-CS014) and the clarifications board (FRFA-CS024/025) are not yet wired. See [`rims_gaps.md`](rims_gaps.md) §B.2 for the tracking entry.

---

## 5. SP3 — Application Management (SRS §5.1.1.3)

PRD mapping references: FRFA-AM004/007/008/010/013–018/019/020/022/024/025/027/028/030/031/032/037/038/040/041.

### 5.1 Applicant intake (FRFA-AM001–005)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `scholar.demo@iilmp.local` (SCHOLAR, `password123`) | `/rims/grants/calls/demo-seed-call/` → **Apply** | Draft application created, status `DRAFT` | FRFA-AM004 |
| 2 | same | Fill proposal, upload CV / proposal / reference letter, wait 5 s | Autosave POSTs to `/rims/grants/calls/<slug>/apply/autosave/` (FRFA-AM004) | FRFA-AM004 |
| 3 | same | Try **Submit** without a mandatory document | Blocked with field-level message | FRFA-AM007, FRFA-AM008 |
| 4 | same | Upload all required documents → **Submit** | Status → **SUBMITTED** (or **UNDER_REVIEW** after auto-screening) | FRFA-AM010 |
| 5 | same | Re-open the application → **Withdraw** with reason | Status → **WITHDRAWN**, removed from manager queue | FRFA-AM037 |
| 6 | same | Save the EICAR test string `X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*` as `eicar.txt` and attempt to upload it on a fresh draft at `/rims/grants/calls/demo-seed-call/apply/` | Upload rejected with malware-scan ValidationError; `validate_grant_upload()` short-circuits via `scan_stream_optional()` | FRFA-AM006 |

Source for step 6: `apps/rims/grants/services.py validate_grant_upload()` + ClamAV wiring (`CLAMD_ENABLED` / `CLAMD_TCP_HOST` settings). Regression test at `apps/rims/grants/tests/test_application_malware_scan.py`.

### 5.2 Automated eligibility screening (FRFA-AM010, FRFA-AM018)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `eligibility.applicant@demo.local` (SCHOLAR, `password123`) | `/rims/grants/applications/` | Seed application against `e2e-eligibility-call` already submitted with status **REJECTED_INELIGIBLE** | FRFA-AM010, FRFA-AM018 |
| 2 | same | Open the application | Failed rule (PhD required) listed in the rejection panel | FRFA-AM018 |
| 3 | same | Scroll to the **Eligibility evaluation** panel on the same page | `Application.eligibility_trace` JSON renders rule-by-rule pass/fail with `rule_id`, `rule_label`, `expected`, `actual`, `passed`, `reason`, `evaluated_at` | FRFA-AM009 |

Source for step 3: `Application.eligibility_trace` field + `partials/_eligibility_trace.html`. The trace is populated by `_build_eligibility_trace()` in `apps/rims/grants/services.py:493` on submit.

### 5.3 Programme officer screening (FRFA-AM013–017)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `programme.officer@demo.local` (PROGRAMME_OFFICER, `password123`) | `/rims/grants/manage/applications/<pk>/po-screen/` for the seeded application on `e2e-po-screening-call` | Form lets PO mark Eligible-Ready-for-Review **or** Rejected-Incomplete | FRFA-AM013, FRFA-AM014, FRFA-AM015 |
| 2 | same | Mark Eligible | Application enters reviewer-assignment queue; `Notification.Verb.REVIEW_INTAKE_MANAGER` fires to the reviewer-assignment actor | FRFA-AM014, FRFA-AM017 |
| 3 | same | Pick a different application and mark Rejected-Incomplete | Applicant is notified, status archived | FRFA-AM015, FRFA-AM016 |

Source for actor: `apps/rims/grants/management/commands/seed_rims.py:1407-1421` seeds the call with `programme_officer_screening_enabled=True`. `grants.manager@demo.local` also still resolves `manage_screening` and can act as a backstop.

### 5.4 Reviewer assignment + scoring (FRFA-AM019–028)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/applications/<pk>/auto-assign-reviewers/` | Service distributes assignments using `ReviewerProfile` workload + tags | FRFA-AM020, FRFA-AM022 |
| 2 | same | Or manually assign via `/rims/grants/manage/applications/<pk>/assign-reviewer/` | Reviewer added; review deadline set per call's `review_deadline_days` | FRFA-AM019 |
| 3 | `reviewer@demo.local` (REVIEWER, `password123`) | `/rims/grants/reviews/` | Pending row titled **E2E reviewer queue (scholar pending)** appears | FRFA-AM024 |
| 4 | same | Open the review → **Score** every criterion (must respect each max) + recommendation → **Submit** | Flash *Review submitted*; row leaves queue | FRFA-AM025 |
| 5 | `grants.manager@demo.local` | `/rims/grants/manage/applications/<pk>/` | Weighted-score panel (`partials/_score_panel.html`) renders `weighted_pct`, `raw_total_mean`, `max_total`, `reviewers_counted`, per-criterion breakdown, and application ranking | FRFA-AM026, FRFA-AM027, FRFA-AM028 |

Source for step 5: `aggregate_weighted_score()` in `apps/rims/grants/services.py:897` reads `Review.scores` + `GrantCall.scoring_rubric` weights.

### 5.5 Blind review (FRFA-AM022)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `reviewer@demo.local` | `/rims/grants/reviews/` for the row backed by `e2e-blind-call` | Queue row shows an **anonymised code**, not the applicant's email/name | FRFA-AM022 |
| 2 | same | Open the review → application body | Applicant identity fields are masked | FRFA-AM022 |

### 5.6 Reviewer conflict of interest (FRFA-AM029, FRFA-AM030)

Reviewer-self-declared COI (new in Phase 3 — preferred path):

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `reviewer2@demo.local` (REVIEWER, `password123`) | `/rims/grants/reviews/<int:pk>/declare-coi/` for an assigned review | `ReviewerCoiDeclarationForm` requires `notes` (min 20 chars) + `confirm` checkbox | FRFA-AM029 |
| 2 | same | Submit | Application disappears from reviewer queue; `ConflictOfInterest.declared_by` + `declared_at` populated; audit row written; PO notified | FRFA-AM029, FRFA-AM030 |

Manager-declared COI (still supported as a backstop):

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | Assign `reviewer2@demo.local` to an application via `assign-reviewer` with **Declare COI = on** | Reviewer removed; PO notified; audit row written | FRFA-AM030 |

Source: `ReviewerCoiDeclarationForm` at `apps/rims/grants/forms.py:558`; `ReviewerDeclareCoiView` at `apps/rims/grants/views.py:1112`.

### 5.7 Conflicting reviewer recommendations (FRFA-AM031, FRFA-AM033–036)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 0 | `grants.manager@demo.local` | `/rims/grants/manage/calls/e2e-conflict-call/edit/` → set `conflict_resolution` to one of `majority_recommendation` / `average_score` / `escalation` and save | Field persists on `GrantCall`; protocol governs resolution outcome | FRFA-AM033, FRFA-AM034, FRFA-AM035, FRFA-AM036 |
| 1 | `grants.manager@demo.local` | Open the application for `e2e-conflict-call` | Two reviews with **diverging** recommendations are flagged in the conflict panel | FRFA-AM031 |
| 2 | same | `/rims/grants/manage/applications/<pk>/ack-review-conflict/` (POST) | Conflict acknowledged + resolved per the configured protocol; outcome saved to audit | FRFA-AM031 |

Source for conflict_resolution: `apps/rims/grants/models.py:249`.

### 5.8 Interview & verification (FRFA-AM032)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/applications/<pk>/interview/schedule/` for `e2e-interview-call` | Interview scheduled, ICS feed at `/rims/grants/interviews/<pk>/calendar.ics` | FRFA-AM032 |
| 2 | same | `/rims/grants/manage/interviews/<pk>/outcome/` → record attendance + score + recommendation | Outcome appears on application detail | FRFA-AM032 |
| 3 | same | `/rims/grants/manage/applications/<pk>/verification/` | Verification record persisted; contributes to ranking | FRFA-AM032 |

### 5.9 Revision request (FRFA-AM038)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/applications/<pk>/request-revision/` with instructions | Status → **REVISION_REQUESTED**, applicant notified | FRFA-AM038 |
| 2 | `scholar.demo@iilmp.local` | `/rims/grants/applications/<pk>/revise/` | Form re-opens application for edits | FRFA-AM038 |

### 5.10 Shortlist + decision + archive (FRFA-AM027, FRFA-AM040)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/calls/e2e-reviewer-call/shortlist/` (POST) | Shortlist generated, tied applications flagged | FRFA-AM027 |
| 2 | same | `/rims/grants/manage/applications/<pk>/shortlist/` (per-application) | Application marked **SHORTLISTED** | FRFA-AM027 |
| 3 | same | `/rims/grants/manage/applications/<pk>/reject/` with reason | Application archived; applicant notified | FRFA-AM041 |
| 4 | same | `/rims/grants/manage/applications/archive/` | Rejected / ineligible / withdrawn applications listed | FRFA-AM040 |
| 5 | `auditor.demo@iilmp.local` | Same URL `/rims/grants/manage/applications/archive/` | Read-only view renders; CSV export works; rows audit-traceable | FRFA-AM039, FRFA-AM040, NFRFA011 |

---

## 6. SP4 — Award & Agreement Management (SRS §5.1.1.4)

PRD mapping references: FRFA-AA002–025, FRFA-AW.

### 6.1 Create an award (FRFA-AA001, FRFA-AA002, FRFA-AA004)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 0 | `grants.manager@demo.local` | Navigate to `/rims/grants/manage/applications/<pk>/award/` for a shortlisted app | The form surfaces an `award_preconditions` panel covering: parent funding uncommitted balance ≥ amount, Grants Manager assigned to funding, Finance Officer assigned to funding, award letter template configured. Any failure blocks submit with an "FRFA-AA001" ValidationError | FRFA-AA001 |
| 1 | same | With preconditions green, form requires amount + project end date | FRFA-AA002 |
| 2 | same | Submit | Award created with status **AGREEMENT_PENDING**; redirect to `/rims/grants/manage/awards/`; `generate_award_letter_pdf()` fires and writes a `AwardLetterVersion` row + `Award.letter_pdf_file` | FRFA-AA002, FRFA-AA004, FRFA-AA006 |
| 3 | same | Open `/rims/grants/manage/awards/<pk>/` → scroll to **Award letters** section | First `AwardLetterVersion` row visible with `version_number=1` + `generated_at` + downloadable PDF | FRFA-AA004 |
| 4 | same | After running §6.4 amendment that re-issues the letter, return to the same panel | Second `AwardLetterVersion` row appears with `triggering_amendment` link populated | FRFA-AA004 |

Source: `check_award_preconditions()` in `apps/rims/grants/services.py:1313`; `generate_award_letter_pdf()` in `services.py:2244`; `AwardLetterVersion` model (migration `0041`); letters render inline on `templates/grants/award_detail.html:311-314`.

### 6.2 Agreements + tranches (FRFA-AA005, FRFA-AA007, FRFA-AA008, FRFA-AA012)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/awards/<pk>/agreements/new/` | Adds a versioned agreement record | FRFA-AA005 |
| 2 | same | `/rims/grants/manage/awards/<pk>/tranches/new/` (repeat) | Tranches must sum ≤ award amount; `generate_tranche_schedule()` creates quarterly default rows | FRFA-AA007, FRFA-AA008 |
| 3 | same | `/rims/grants/manage/awards/<pk>/tranche-reconciliation/` | `AwardTrancheReconciliationView` lists each `AwardTranche` matched to `TrancheBudgetAllocation` rows with allocation amount, matched budget lines, and variance | FRFA-AA007, FRFA-AA012 |

Source: `generate_tranche_schedule()` at `apps/rims/grants/services.py:2342`, `reconcile_award_tranches()` at `services.py:2469`; `TrancheBudgetAllocation` bridge (migration `0044`).

### 6.3 Awardee acknowledgement (FRFA-AA011)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `award.recipient@demo.local` (SCHOLAR, `password123`) | `/rims/grants/awards/<pk>/acknowledge/` | Award status → **ACTIVE**, acknowledgement timestamp set | FRFA-AA011 |

### 6.4 Amendments (FRFA-AA019, FRFA-AA021–022)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/awards/<pk>/amendments/new/` | Amendment created, status PENDING | FRFA-AA019 |
| 2 | `program.director@demo.local` | `/rims/grants/manage/amendments/<pk>/decision/` → **Approve** or **Reject** | Status reflects decision; audit row written | FRFA-AA021, FRFA-AA022 |
| 3 | `grants.manager@demo.local` | If rejected, `/rims/grants/manage/amendments/<pk>/resubmit/` | New version created, returns to approver | FRFA-AA022 |

### 6.5 Cancellation cascade (FRFA-AA015, FRFA-AA016, FRFA-AA017)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/awards/<pk>/cancellation/new/` with reason | `AwardCancellationRequest` row created in PENDING | FRFA-AA015, FRFA-AA016 |
| 2 | same | `/rims/grants/manage/cancellations/<pk>/` | Cascade preview shows undisbursed tranches that will be cancelled + already-disbursed amounts that will create `RecoveryRequest` rows | FRFA-AA016 |
| 3 | same | `/rims/grants/manage/cancellations/<pk>/submit/` | Status → DIRECTOR_REVIEW; Programme Director notified | FRFA-AA016 |
| 4 | `program.director@demo.local` | `/rims/grants/manage/cancellations/<pk>/approve/` | `approve_cancellation_and_cascade()` runs atomically: `AwardTranche.cancelled_at` set on undisbursed rows, undisbursed balance returned to parent funding's uncommitted, `RecoveryRequest` rows written for disbursed amounts, `cascade_summary` JSON populated, audit cascade emitted | FRFA-AA016, FRFA-AA017 |
| 5 | `program.director@demo.local` | Reject path on a different request: `/rims/grants/manage/cancellations/<pk>/reject/` | Status → REJECTED with decision_notes; no cascade fires | FRFA-AA017 |
| 6 | `grants.manager@demo.local` | Withdraw a PENDING request: `/rims/grants/manage/cancellations/<pk>/withdraw/` | Status → WITHDRAWN | FRFA-AA017 |

Source: `create_award_cancellation()` + `approve_cancellation_and_cascade()` in `apps/rims/grants/services.py:1972`; `ProgramDirectorMixin`; migration `0042` adds `cascade_summary`, `AwardTranche.cancelled_at`, `RecoveryRequest.{award,cancellation,tranche}`.

### 6.6 Agreement chase beat (FRFA-AA009)

This is a Celery beat (`apps.rims.grants.tasks.chase_agreements`). To exercise it manually:

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | shell | `docker compose exec web python manage.py shell -c "from apps.rims.grants.tasks import chase_agreements; chase_agreements()"` | AGREEMENT_PENDING awards past the configured grace receive a chase notification | FRFA-AA009 |

### 6.7 Cohort addenda (FRFA-AA023, FRFA-AA024, FRFA-AA026–030)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/awards/<pk>/cohort-addenda/new/` | Form shows funding uncommitted balance + existing `AwardCohort` rows | FRFA-AA023 |
| 2 | same | Submit with `label` / `target_participants` / `budget_allocation` / `start_date` / `end_date` / `reason` | `CohortAddendum` row in REQUESTED with `created_by=request.user` | FRFA-AA024 |
| 3 | same | `/rims/grants/manage/cohort-addenda/<pk>/submit/` | Status → DIRECTOR_REVIEW; Programme Director notified | FRFA-AA026 |
| 4 | `program.director@demo.local` | `/rims/grants/manage/cohort-addenda/<pk>/approve/` | Status → APPROVED; new `AwardCohort` row activated; `decided_by=request.user` | FRFA-AA027, FRFA-AA028 |
| 5 | `program.director@demo.local` | Reject path on a different addendum: `/rims/grants/manage/cohort-addenda/<pk>/reject/` | Status → REJECTED with `decision_notes` populated | FRFA-AA029 |
| 6 | `grants.manager@demo.local` | Withdraw a REQUESTED row: `/rims/grants/manage/cohort-addenda/<pk>/withdraw/` | Status → WITHDRAWN | FRFA-AA030 |

Source: `request_cohort_addendum()` / `submit_cohort_addendum_for_review()` / `approve_cohort_addendum()` / `reject_cohort_addendum()` / `withdraw_cohort_addendum()` in `apps/rims/grants/services.py:2510`+; migration `0043` adds `AwardCohort` + `CohortAddendum`; `ProgramDirectorMixin` guards approve/reject.

---

## 7. SP5 — Close-out (SRS §5.1.1.5)

PRD mapping references: FRFA-CO002–026.

### 7.1 Final report decision + awardee resubmit loop (FRFA-CO005, FRFA-CO006, FRFA-CO007)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/final-reports/<pk>/decision/` | Approve / reject final report; status updates | FRFA-CO005, FRFA-CO007 |
| 2 | `award.recipient@demo.local` (SCHOLAR) | After a REVISION_REQUIRED narrative decision, `/rims/grants/final-narrative/<int:pk>/resubmit/` | Form accepts `response_note`; FSM REVISION_REQUIRED → SUBMITTED; manager notified | FRFA-CO006 |
| 3 | same | After a QUERIES_RAISED financial decision, `/rims/grants/final-financial/<int:pk>/resubmit/` | Same loop: QUERIES_RAISED/REVISION_REQUIRED → SUBMITTED; manager notified | FRFA-CO006 |

Source: `FinalNarrativeResubmitView` + `FinalFinancialResubmitView` at `apps/rims/grants/views.py:2185+`/`2215+`; `resubmit_narrative_report()` + `resubmit_financial_report()` in `closeout_helpers`. Routes use applicant-facing prefixes (`/rims/grants/final-*`), not manager URLs (see [[feedback_awardee_url_rule]]).

### 7.2 Award close-out (FRFA-CO001, FRFA-CO008–013, FRFA-CO014, FRFA-CO018)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/grants/manage/awards/<pk>/closeout/` (use seeded `demo-seed-call` award) | `closeout_preconditions()` panel surfaces all seven gates with pass/fail: `narrative_present`, `mandatory_checklist_done`, `disbursement_within_budget`, `no_pending_finance`, `milestones_resolved`, `reports_accepted`, `assets_resolved`. Submit blocked until all pass | FRFA-CO001, FRFA-CO008, FRFA-CO009 |
| 2 | same | Submit with all gates green + narrative | Award status → **CLOSED**, audit row written; `record_closeout_residual()` writes residual to `AwardCloseoutRecord.financial_snapshot['residual_returned']`. If total disbursed < approved, residual moves back to parent funding's uncommitted balance with audit action `CLOSEOUT_RESIDUAL_RECONCILED` | FRFA-CO010, FRFA-CO011, FRFA-CO012, FRFA-CO013 |

Source: `closeout_preconditions()` + `record_closeout_residual()` in `closeout_helpers`; `AwardCloseoutView` at `/rims/grants/manage/awards/<pk>/closeout/`.

### 7.3 Forced close-out (FRFA-CO023)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `program.director@demo.local` (only Programme Director allowed) | `/rims/grants/manage/awards/<pk>/force-closeout/` | Mandatory reason field; status → **CLOSED** with `forced=True` | FRFA-CO023 |

### 7.4 Archive + retention beat (FRFA-CO015, FRFA-CO016, FRFA-CO017)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | shell | `docker compose exec web python manage.py shell -c "from apps.rims.grants.tasks import archive_closed_awards; archive_closed_awards()"` against a CLOSED award past its `RetentionPolicy.archive_after_days` window | `Award.archived_at` set; audit action `AWARD_ARCHIVED` emitted | FRFA-CO015, FRFA-CO016 |
| 2 | `auditor.demo@iilmp.local` | `/rims/operations/archived-awards/` | `ArchivedAwardsListView` lists archived award; read-only access via `AuditorReadOnlyMixin` | FRFA-CO017 |

Source: `archive_closed_awards()` in `apps/rims/grants/tasks.py:448`; `ArchivedAwardsListView` + `operations/archived_awards.html`; gated by `RetentionPolicy` (4 rows seeded).

### 7.5 Donor close-out report dispatch (FRFA-CO019)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | After the §7.2 close-out commit | `dispatch_donor_closeout_report(funding_record_id)` queues via `transaction.on_commit()`; PDF generated from `finance/donor_closeout_report.pdf.html`; emailed to `Donor.partner.contact_email` | FRFA-CO019 |
| 2 | shell | Re-run manually: `docker compose exec web python manage.py shell -c "from apps.rims.finance.tasks import dispatch_donor_closeout_report; dispatch_donor_closeout_report(<funding_pk>)"` | Idempotent — guarded by a recent-AWARDED-action check; reuses `apps/rims/finance/services_donor_closeout.py` | FRFA-CO019 |

---

## 8. FM-1 — Budget & Financial Management (SRS §5.4)

PRD mapping references: FRFM002–005, FRFM014, FRFM017–025, FRFM028–045.

### 8.1 Create a project budget (FRFM002–005)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance@demo.local` (FINANCE_OFFICER, `password123`) | `/rims/finance/budgets/new/` | Form binds to an Award + funding source | FRFM002 |
| 2 | same | Add 3+ budget lines with multi-line totals; each line can pick its own `funding_source` (falls back to `Budget.funding_source`) | Total budget displayed; saves as DRAFT | FRFM002, FRFM003, FRFM005, FRFM006 |
| 3 | same | Submit and approve via the inline workflow | Status → APPROVED; locked from line-level edits | FRFM004, FRFM005 |
| 4 | same | For a multi-year budget, `/rims/finance/budgets/<int:pk>/year-allocations/` | `BudgetYearAllocationsView` shows annual buckets; add a `BudgetYearAllocation` row per fiscal year with `amount` + `currency` | FRFM001 |

Source for step 4: `Budget.is_multi_year`, `BudgetYearAllocation` model, `BudgetYearAllocationFormSet`.

### 8.2 Budget revision (FRFM011/029)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance@demo.local` | Open an APPROVED budget → request revision (creates DRAFT `BudgetRevision`) | Revision in pending state | FRFM010 |
| 2 | `program.coordinator@demo.local` (PROGRAM_COORDINATOR, `password123`) | `/rims/finance/budgets/<pk>/edit/` (requires `ProgramCoordinatorMixin`) | Approve revision; versioned `BudgetRevisionLine` rows written | FRFM011 |

### 8.3 Expenditure tracking (FRFM003, FRFM014)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance@demo.local` | `/rims/finance/budgets/<pk>/expenditures/new/` | Pick budget line, amount, date, attach receipt | FRFM003 |
| 2 | same | `/rims/finance/expenditures/` | Filter by date / line; CSV export available | FRFM014 |
| 3 | same | `/rims/finance/budgets/<pk>/cashflow.json` | JSON returns time-series for the chart on budget detail | FRFM018, FRFM019 |

### 8.4 Variance + analytics dashboard (FRFM014/018–025)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance@demo.local` | `/rims/finance/analytics/` | Budget vs actual, cost categories, variance ribbon | FRFM014, FRFM022, FRFM024 |
| 2 | shell | `from apps.rims.operations.models import VarianceAlertConfig; VarianceAlertConfig.objects.first()` | Singleton enabled; threshold 20%, window 30 d | FRFM024 |

### 8.5 Exchange rates admin (FRFM008, FRFM009)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance.manager@demo.local` (FINANCE_MANAGER, `password123`) | `/rims/finance/exchange-rates/` | `ExchangeRateListView` lists seeded rates (5 rows on demo data) | FRFM008 |
| 2 | same | `/rims/finance/exchange-rates/new/` → fill source/target currency, rate, `effective_date` | New `ExchangeRate` row persisted | FRFM008 |
| 3 | same | `/rims/finance/exchange-rates/<int:pk>/edit/` | Updates flow through `services.budget_ceiling()` + `total_disbursed()` consolidation for multi-currency budgets | FRFM009 |

Source: `ExchangeRate` model; views guarded by `FinanceApprovalMixin`.

### 8.6 Approval workflow config + telemetry (FRFM012, FRFM013, FRFM015, FRFM036–038)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance.manager@demo.local` | `/rims/finance/approval-workflows/` | `BudgetApprovalWorkflowListView` shows existing workflows | FRFM012 |
| 2 | same | `/rims/finance/approval-workflows/new/` → `name`, `approval_mode` (TOTAL / line), `required_approver_count` | Workflow persisted | FRFM013, FRFM015 |
| 3 | `finance@demo.local` | Submit a budget; `finance.manager@demo.local` approves at line level via `ApproveBudgetLineView`; `finance.director@demo.local` approves at total via `ApproveBudgetTotalView` | Two-eyes enforced: same-actor approval rejected; `FinanceApproval` rows recorded for each step | FRFM036, FRFM037 |
| 4 | `finance.manager@demo.local` | `/rims/finance/approval-telemetry/` | `ApprovalTelemetryView` (`finance/approval_telemetry.html`) shows who-approved-which-row + two-eyes matrix | FRFM038, NFRFM005 |

Source: `BudgetApprovalWorkflow` + `FinanceApproval` models; `ApproveBudgetLineView` at `apps/rims/finance/views.py:1619`, `ApproveBudgetTotalView` at line 1632.

### 8.7 Reforecast / rolling budget (FRFM027)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance.manager@demo.local` | `/rims/finance/budgets/<int:pk>/reforecasts/` | `ReforecastListView` lists snapshots (immutable; new generations append) | FRFM027 |
| 2 | same | `/rims/finance/budgets/<int:pk>/reforecasts/generate/` → pick `methodology` (`burn_rate` / `tranche_schedule` / `blended`) | `Reforecast` row + per-line `ReforecastLine` rows with `projected_end_date` + `remaining_balance` | FRFM027 |
| 3 | same | `/rims/finance/reforecasts/<int:pk>/chart.json` | Time-series JSON returned for the dashboard chart | FRFM027 |
| 4 | shell | `docker compose exec web python manage.py shell -c "from apps.rims.finance.tasks import recompute_reforecasts; recompute_reforecasts()"` | Monthly beat runs against all APPROVED/FINALIZED budgets; appends a fresh snapshot per budget | FRFM027 |

Source: `Reforecast` + `ReforecastLine` models; `services_reforecast.generate_reforecast()`.

---

## 9. FM-2 — Disbursement & Payment (SRS §5.4)

PRD mapping references: FRFM028–050.

### 9.1 Disbursement request → approval → execute → reconcile

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance@demo.local` (FINANCE_OFFICER) or `project.manager@demo.local` (PROJECT_MANAGER — allowed by `DisbursementCreateMixin`) | `/rims/finance/budgets/<pk>/disbursements/new/` | Form binds to budget line; status DRAFT → SUBMITTED on submit | FRFM028, FRFM030 |
| 2 | same | `/rims/finance/disbursements/<pk>/` | Submitted request visible with documentation | FRFM031 |
| 3 | `finance.manager@demo.local` (FINANCE_MANAGER, `password123`) | `/rims/finance/disbursements/approvals/` | Approval queue lists submitted requests; both FINANCE_MANAGER and FINANCE_DIRECTOR resolve `FinanceApprovalMixin` | FRFM033 |
| 3b | `finance.director@demo.local` | Re-approve the same row at the next tier | Confirms Finance Manager and Finance Director tiers are wired distinct per the two-eyes ladder | FRFM033, NFRFM005 |
| 4 | same | Open a row → **Approve** | Status → APPROVED; finance officer notified | FRFM033, FRFM035 |
| 5 | same | Approve a different one with **Reject** + reason | Status → REJECTED; original requester notified | FRFM033 |
| 6 | `finance@demo.local` | `/rims/finance/disbursements/<pk>/resubmit/` after a rejection | Status moves back to SUBMITTED with revision history | FRFM035 |
| 7 | `finance@demo.local` | `/rims/finance/disbursements/<pk>/execution/` | Capture payment method, scheduled date, destination account | FRFM039 |
| 8 | same | Same screen → mark **Executed** with reference | Status → PAID; ExecutedAt timestamp set | FRFM039, FRFM044 |
| 9 | `finance@demo.local` | `/rims/finance/disbursements/<pk>/reconciliation/` | Reconciliation form persists matched/unmatched state | FRFM045 |

### 9.2 Payment batching (FRFM040)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance@demo.local` | `/rims/finance/payment-batches/new/` → select APPROVED requests | New batch in DRAFT | FRFM040 |
| 2 | same | `/rims/finance/payment-batches/<pk>/lock/` | Locked; further request additions blocked | FRFM040 |
| 3 | same | `/rims/finance/payment-batches/<pk>/execute/` | Underlying requests flipped to PAID; transaction references stored | FRFM044 |
| 4 | `finance.director@demo.local` | `/rims/finance/payment-batches/<pk>/reverse/` (only `FinanceApprovalMixin`) | Two-eyes reverse; audit + notifications | FRFM044, NFRFM005 |
| 5 | `finance@demo.local` | `/rims/finance/payment-batches/<pk>/cancel/` (only while DRAFT/LOCKED) | Cancelled before any execution | FRFM040 |

### 9.3 Donor reporting (FRFM031–039)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance@demo.local` | `/rims/finance/donor-reports/` | Lists donor reports derived from disbursements + budget actuals | FRFM031, FRFM033 |
| 2 | same | Open a report → **Export** at `/rims/finance/donor-reports/<pk>/export/` | Downloads spreadsheet/PDF with budgeted vs actual per line | FRFM035, FRFM039 |

### 9.4 Payment failure handling + retry queue (FRFM042, FRFM043)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance@demo.local` | `/rims/finance/disbursements/<pk>/execution/mark-failed/` with `failure_reason` + `failure_category` (NSF / WRONG_ACCOUNT / BANK_REJECTION / OTHER) | `MarkExecutionFailedView` captures failure; `PaymentRetry` row created in QUEUED | FRFM042 |
| 2 | `finance.manager@demo.local` | `/rims/finance/payment-retries/` | `PaymentRetryQueueView` lists QUEUED + RETRYING rows | FRFM042 |
| 3 | same | `/rims/finance/payment-retries/<int:pk>/` | Detail view shows `attempts`, `last_attempt_at`, `next_retry_at`, `last_error` | FRFM042 |
| 4 | same | `/rims/finance/payment-retries/<int:pk>/retry/` | Status → RETRYING; on success → PAID; on failure → QUEUED with incremented attempts | FRFM043 |
| 5 | same | `/rims/finance/payment-retries/<int:pk>/abandon/` | Status → ABANDONED; finance officer notified | FRFM043 |
| 6 | shell | `docker compose exec web python manage.py shell -c "from apps.rims.finance.tasks import process_payment_retry_queue; process_payment_retry_queue()"` | Beat advances QUEUED rows past `next_retry_at` into RETRYING | FRFM042 |

Source: `PaymentRetry` model; `DisbursementExecution.{failure_reason, failure_category}`.

### 9.5 GL posting + register (FRFM046, FRFM047)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance@demo.local` | Execute a disbursement per §9.1 step 7-8 | `services_gl.post_gl_entries(execution)` writes a debit + credit pair (debit: `DisbursementRequest.account_reference` or `Budget.account_reference` or `6000-DISB`; credit: `1000-CASH`); audit action `GL_POSTED` emitted | FRFM046 |
| 2 | `finance@demo.local` | `/rims/finance/gl/` | `GLEntryRegisterView` lists entries with `validation_status` (POSTED / FLAGGED / VOIDED); CSV export available | FRFM047 |
| 3 | `finance.manager@demo.local` | `/rims/finance/gl/<int:pk>/void/` on a posted entry | Status → VOIDED; audit row written | FRFM047 |
| 4 | shell | `docker compose exec web python manage.py shell -c "from apps.rims.finance.tasks import revalidate_gl_entries; revalidate_gl_entries()"` | Daily beat re-checks recent GL rows and posts any pending executions | FRFM047 |

Source: `GLEntry` model; `services_gl.post_gl_entries()`.

### 9.6 Bank reconciliation (FRFM048, FRFM049)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `finance.manager@demo.local` | `/rims/finance/bank-statements/upload/` → upload a CSV bank statement with `period_start`, `period_end`, `account_reference`, `currency` | `BankStatement` + `BankStatementLine` rows created via `import_bank_statement()`; `parse_bank_statement_lines()` populates `value_date` / `description` / `amount` | FRFM048 |
| 2 | same | `/rims/finance/bank-statements/<int:pk>/` | Detail view shows matched / unmatched lines; auto-matcher uses ±3-day value-date window + exact amount against `DisbursementExecution` | FRFM048, FRFM049 |
| 3 | same | `/rims/finance/bank-statements/lines/<int:pk>/match/` on an unmatched line | `ConfirmBankMatchView` lets the reviewer confirm a fuzzy match; `match_status` → `matched` | FRFM049 |
| 4 | same | `/rims/finance/bank-statements/` | List view shows all statements; CSV export available | FRFM049 |

Source: `BankStatement` + `BankStatementLine` models; `services_reconciliation.import_bank_statement()`.

---

## 10. Projects (SRS §5.1, milestones + deliverables)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/projects/` | Seeded project for `demo-seed-call` award is listed | FRFA-AA014 |
| 2 | same | `+ New project` → bind to award + budget | Project created with summary, lead, start/end | FRFA-AA014 |
| 3 | same | `/rims/projects/<pk>/milestones/new/` | Create milestone; appears on detail page | FRFA-AA014 |
| 4 | same | `/rims/projects/<pk>/milestones/<m>/submit/` | Status → SUBMITTED, capture evidence | FRFA-CO002 |
| 5 | `program.manager@demo.local` (PROGRAM_MANAGER, `password123`) | `/rims/projects/<pk>/milestones/<m>/review/` | Approve / reject milestone evidence; audit row written | FRFA-CO003 |
| 6 | same | `/rims/projects/milestone-compliance/` | Compliance dashboard lists overdue / pending milestones (sortable, CSV export) | FRFA-CO004 |

---

## 11. Scholarships (SRS §5.1, scholar lifecycle)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/scholarships/programmes/` | Seeded programmes listed | FRFA-AA025 |
| 2 | same | `/rims/scholarships/programmes/new/` | New programme form saves and lists | — |
| 3 | same | `/rims/scholarships/scholars/enrol/` | Enrol scholar against an APPROVED award; appears in scholar list | FRFA-AA025 |
| 4 | `scholar@demo.local` (SCHOLAR, `password123`) | `/rims/scholarships/my/` | Scholar sees their own enrolment with stipend history (4 seeded rows) | — |
| 5 | same | `/rims/scholarships/scholars/<pk>/progress-reports/new/` | Submit a progress report PDF; appears on detail timeline | — |
| 6 | `finance@demo.local` | `/rims/scholarships/scholars/<pk>/stipends/new/` | Record a stipend payment with reference | — |
| 7 | `grants.manager@demo.local` | `/rims/scholarships/officer/` | Officer dashboard summarises active / graduated / on-hold | — |
| 8 | same | `/rims/scholarships/scholars/<pk>/status/` | Lifecycle transition (active → on-hold → graduated) writes audit row | — |

---

## 12. Operations (SRS §5.1 master data)

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `grants.manager@demo.local` | `/rims/operations/partners/` | Partner list with 6 seeded rows | — |
| 2 | same | `+ New partner` → name, type, country, contact | New partner persisted, audit row written | NFRFA011 |
| 3 | same | `/rims/operations/institutions/` then `+ New` | Institution created | — |
| 4 | same | `/rims/operations/mous/` → `+ New MOU` | MOU bound to partner + institution + status | — |
| 5 | `auditor.demo@iilmp.local` | `/rims/operations/audit-log/` | Read-only listing of every CRUD across SP1–SP5; CSV export available | NFRFA011 |

---

## 13. Cross-cutting / RBAC sanity

| Step | Account | Action | Expected | FR |
|---|---|---|---|---|
| 1 | `scholar@demo.local` | Try `/rims/grants/manage/calls/` | 403 (no `manage_calls` perm) | NFRFA001 |
| 2 | `reviewer@demo.local` | Try `/rims/finance/disbursements/approvals/` | 403 (REVIEWER not in `FinanceApprovalMixin`) | NFRFA001 |
| 3 | `auditor.demo@iilmp.local` | POST to `/rims/operations/audit-log/` | 403 "Audit log is read-only." | NFRFA011 |
| 4 | `mel@demo.local` (MEL_OFFICER) | `/rims/grants/manage/funding/` | 403 (only `view_dashboard` granted) | NFRFA001 |
| 5 | `finance@demo.local` | POST `/rims/finance/disbursements/<pk>/approve/` | 403 (FINANCE_OFFICER can submit but not approve; needs FINANCE_MANAGER+) | FRFM033, NFRFM005 |
| 6 | `finance@demo.local` | POST `/rims/finance/exchange-rates/new/` | 403 (FINANCE_OFFICER not in `FinanceApprovalMixin`; FINANCE_MANAGER+ required) | FRFM008, NFRFA001 |
| 7 | `program.coordinator@demo.local` | GET `/rims/finance/disbursements/approvals/` | 403 (PROGRAM_COORDINATOR not in `FinanceApprovalMixin`) | NFRFA001 |
| 8 | `grants.manager@demo.local` | POST `/rims/grants/manage/cancellations/<pk>/approve/` | 403 (`ProgramDirectorMixin` required for the cascade approve step) | FRFA-AA017, NFRFA001 |
| 9 | `reviewer@demo.local` | GET `/rims/grants/manage/applications/<pk>/po-screen/` | 403 (REVIEWER lacks `manage_screening`); confirms `programme.officer@demo.local` is the correct actor for SP3 | FRFA-AM013, NFRFA001 |

---

## 14. Re-seeding between flows

After running destructive flows (withdraw call, reject application, close-out, force close-out, reverse batch), restore the e2e fixtures with:

```bash
docker compose exec web python manage.py seed_rims --reset --comprehensive
```

To resolve specific PKs on the freshly-seeded data (review row, application row, award row) use the shell snippets in `AGENTS.md` §RIMS browser testing.

---

## 15. Reading the audit trail during tests

Every state transition above writes to the `audit_rims` log surfaced at `/rims/operations/audit-log/`. When a flow seems to "succeed silently" but the UI doesn't refresh (HTMX, redirect, …), open the audit log as `auditor.demo@iilmp.local` and confirm the row was written — that disambiguates "wired but UI stale" from "actually missed".

### Audit actions added in Phases 3–6 (2026-05-18)

When filtering the audit log to confirm a new flow fired, look for these action names:

| Action | Emitted by | Phase |
|---|---|---|
| `CLOSEOUT_RESIDUAL_RECONCILED` | `record_closeout_residual()` on §7.2 close-out commit | 5 |
| `AWARD_ARCHIVED` | `archive_closed_awards` beat per §7.4 | 5 |
| `GL_POSTED` | `services_gl.post_gl_entries()` per §9.5 | 6 |
| `COHORT_ADDENDUM_REQUESTED` / `_SUBMITTED` / `_APPROVED` / `_REJECTED` / `_WITHDRAWN` | Cohort-addendum FSM transitions per §6.7 | 4 |
| `AWARD_CANCELLATION_*` | Cancellation cascade FSM per §6.5 | 4 |

Known incomplete or unimplemented flows referenced above are captured in [`rims_gaps.md`](rims_gaps.md).
