# Implementation prompt — SME-Hub deferred SRS gaps

You are implementing the remaining **deferred** SRS gaps in the Django SME-Hub module of the
RUFORUM IILMP platform. A full audit (2026-07-07) confirmed the module is production-grade: all
131 core FRs are IMPLEMENTED or PARTIAL, none MISSING. The 12 highest-value gaps were already
fixed and verified. **This prompt covers the 8 remaining deferred items** — the riskier or
lower-value partials. Implement them well, follow the house design system, and QA every one in a
real browser before declaring it done.

---

## 0. Environment & ground rules (read first)

- App runs in Docker: `http://localhost:8000/` (see `docker-compose.yml`). SME-Hub is mounted at
  `/smehub/`. Sub-apps: `apps/smehub/{onboarding,incubation,linkage,marketplace,showcasing,investment}`;
  shared code in `apps/smehub/_shared/`.
- **Auth is email-only** (`CustomUser` has no username). Login page `/accounts/login/`, fields
  `#id_email` / `#id_password`.
- **Seeded QA logins** (password `TestApply!2026` for all):
  - entrepreneur: `entrepreneur.demo@iilmp.local`
  - sme_admin: `smehub-admin@ruforum.org`
  - programme_officer: `alex.officer@smehub.example`
  - judge: `judge1@smehub.example`
  - investor: `karim.invest@smehub.example`
  - service_provider: `naomi.svc@smehub.example`
  - not-ready entrepreneur w/ matches: `gilleschristakakpo@gmail.com`
  - entrepreneur with a draft application (pk 1764): `daniel.otieno@smehub.example`
  If a login is missing, seed it in the Docker shell:
  ```
  docker compose exec -T web python manage.py shell -c "
  from django.contrib.auth import get_user_model; U=get_user_model()
  u=U.objects.get(email='someone@x'); u.set_password('TestApply!2026'); u.is_active=True; u.save()"
  ```
- **Browser QA uses the `agent-browser` CLI via Bash** (NOT the Chrome extension). Core commands:
  `agent-browser open <url>`, `fill <sel> "text"`, `upload <sel> <file>`, `select <sel> <val>`,
  `eval "<js>"` (returns JSON — the workhorse for assertions), `screenshot <path.png>`.
  Robust login pattern (refs go stale — use stable selectors + submit the form directly):
  ```
  agent-browser open "http://localhost:8000/accounts/logout/"
  agent-browser open "http://localhost:8000/accounts/login/"
  agent-browser fill "#id_email" "smehub-admin@ruforum.org"
  agent-browser fill "#id_password" "TestApply!2026"
  agent-browser eval "document.querySelector('form').requestSubmit()"
  ```
  Alpine (`x-model`) and Quill do NOT see programmatic `.value`/`fill` — dispatch events:
  `el.value=v; el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true}))`.
  Avoid triggering native `confirm()`/`alert()` dialogs — they block the browser. To exercise a
  POST behind a confirm dialog, POST via `fetch` with the CSRF token instead of clicking.
- **Design system is non-negotiable.** Every template you touch must ship production-grade and
  match surrounding markup: `page_header`, `card` / `card-section`, `stat-card`, `filter-form` /
  `filter-field__input`, `smehub_status_badge`, the shared `callout` / `ai_callout` partials,
  `btn-primary` / `btn-secondary` / `btn-sm`, `empty_state.html`, `tabular-nums`. Add empty and
  loading states. Grep a neighbouring template for the exact class vocabulary before writing.
- **Django template comments:** `{# … #}` is SINGLE-LINE ONLY (multi-line renders as visible
  text). Use `{% comment %}…{% endcomment %}` for multi-line notes.
- **Migrations:** if you add a model field, run
  `docker compose exec -T web python manage.py makemigrations <app_label>` then
  `... migrate <app_label>`. Confirm the app label from the sub-app's `apps.py` (namespaces are
  `smehub_onboarding`, `smehub_incubation`, `smehub_linkage`, `smehub_marketplace`,
  `smehub_showcasing`, `smehub_investment`).
- **Tests:** ONE pytest invocation at a time (concurrent runs collide on `test_iilmp`). Buffer
  output inside the container:
  ```
  docker compose exec -T web sh -c "python -m pytest apps/smehub -q > /tmp/res.txt 2>&1; echo EXIT $?; tail -20 /tmp/res.txt"
  ```
  Use `force_login(user)` in tests (django-axes rejects `Client.login()`).
- **Verify before building** — re-read the current code for each item; the audit line numbers may
  have drifted. Reuse existing services/helpers; don't duplicate logic. Work only inside the named
  sub-app dirs.

---

## Items to implement

### 1. INC033 — full programme-completion criteria (incubation)
**FR:** "…evaluate programme completion criteria defined **attendance threshold, E-Learning course
completions, and minimum mentorship sessions** and automatically set an entrepreneur's status to
Programme Complete when all criteria are satisfied."
**Current:** `evaluate_completion_criteria()` in `apps/smehub/incubation/services.py` checks
required-course completions ONLY; attendance threshold and minimum mentorship sessions are ignored,
and there is no per-programme config.
**Do:**
- Add per-programme completion config on `Programme` (e.g. `attendance_threshold_pct`,
  `min_mentorship_sessions`; course completions already tracked). Surface them in `ProgrammeForm` /
  `programme_form.html`. Migration required.
- Extend `evaluate_completion_criteria()` to require ALL configured criteria: required courses
  complete AND `progress.sessions_completed >= min_mentorship_sessions` AND attendance ≥ threshold
  (use whatever attendance data the cohort/session models already record; if attendance isn't
  tracked, gate only on the criteria that have data and note the limitation in an `ai_callout`).
- Show the criteria + the member's progress against each on `my_programme.html` and the cohort
  management view so both entrepreneur and officer can see what's outstanding.
**QA:**
1. As sme_admin, edit a programme → set `min_mentorship_sessions=2`, save; confirm it persists.
2. Pick a cohort member with 1 completed session (shell): confirm `evaluate_completion_criteria`
   returns False and status stays ACTIVE.
3. Add a 2nd completed session; re-run; confirm status flips to `programme_complete` and a
   certificate is generated.
4. As that entrepreneur, `/smehub/incubation/my/` shows the criteria breakdown and the cert.
5. `eval` assert: `location.pathname`, the criteria labels present, and the member status badge.

### 2. INC017 / INC021 — judge auto-assignment + reminder cadence (incubation)
**FR INC017:** "assign applications that pass first-level review to the **judges configured on the
programme record**, and notify each judge via dashboard and email."
**FR INC021:** "send automated reminders to judges who have not submitted scores by a
**configurable number of days before the scoring deadline**."
**Current:** no programme judge roster (officers assign manually per application); the reminder beat
is days-since-notified, not deadline-relative (no `scoring_deadline` field).
**Do:**
- Add a judge roster to `Programme` (M2M to judge users) + a `scoring_deadline` +
  `reminder_days_before` field. Surface in the programme management UI. Migration required.
- When an application transitions past first-level review, auto-create judge assignments for the
  roster and notify each judge (dashboard + email), reusing existing assignment/notification code.
- Update the reminder Celery task in `incubation/tasks.py` to fire `reminder_days_before` days
  before `scoring_deadline` for judges with unsubmitted scores.
**QA:**
1. As programme_officer, configure a programme roster with `judge1@smehub.example` + a deadline.
2. Move an application past first-level review; assert (shell) a JudgeAssignment row was created
   and a notification exists for judge1.
3. Log in as `judge1@smehub.example` → `/smehub/incubation/judge/` shows the assigned application.
4. Run the reminder task manually in the shell with a deadline inside the window; assert a reminder
   notification is created for the non-scoring judge.

### 3. MPL017 — advisory booking slot picker (linkage)
**FR:** "book an advisory session with a connected service provider directly from the service
provider's profile, **selecting from available time slots displayed by the system**."
**Current:** `AdvisorySessionForm.starts_at` is a free-form `datetime-local`; the provider's
`ServiceProviderEntry.available_slots` JSON is never surfaced or validated (double/off-hours
bookings possible; MPL018 "release slot" is moot).
**Do:**
- Render the provider's `available_slots` as selectable options in `advisory_booking.html`;
  validate the chosen slot server-side in `AdvisoryBookingView`/`AdvisorySessionForm`.
- Mark a slot consumed on booking and freed on cancel (`cancel_advisory_session`). Decide storage:
  either a small `AdvisorySlot` inventory model or a status flag inside the JSON — prefer a model
  if slots need per-slot state. Migration if you add a model.
**QA:**
1. Seed a service provider with 2 published slots + an accepted connection to `entrepreneur.demo`.
2. As entrepreneur.demo, open `/smehub/linkage/service-providers/<pk>/book/` — assert only the 2
   slots are offered (no free-text datetime), via `eval` on the option list.
3. Book slot A; assert (shell) the session is created and slot A is no longer offered on reload.
4. Cancel the session; assert slot A is offered again.

### 4. MPL004 — unified directory search + relevance (linkage + marketplace)
**FR:** "keyword and advanced filter search across **all three directories** Partner, Buyer, and
Service Provider filterable by sector, organisation type, geographic coverage, and **service
offering**."
**Current:** `_DirectoryListMixin` does keyword+sector+geo+org_type but has no `service_offering`
facet on Service Providers, no relevance ranking (alpha sort only), and the Buyer registry (in
`apps/smehub/marketplace/`) is searched separately.
**Do:**
- Add a `service_offering` filter to the Service Provider list.
- Add relevance ranking (Postgres FTS/trigram or a reused matchmaking score) instead of
  alphabetical.
- Provide a unified search entry that spans Partner + Service Provider + Buyer directories (a
  combined results view or a shared filter bar reaching all three).
**QA:**
1. As entrepreneur, `/smehub/linkage/service-providers/?service_offering=finance` returns only
   matching providers (`eval` row count vs an unfiltered baseline).
2. A keyword query returns most-relevant first (assert the top row contains the query term).
3. The unified search surface returns entries from all three directories for a broad keyword.

### 5. ISD004 — draft applications + autosave (showcasing)
**FR:** entrepreneurs apply to a Showcase Event / Innovation Challenge submitting profile,
innovation description, product details, traction data, pitch materials.
**Current:** applications post straight to SUBMITTED (`showcasing/models.py`); no DRAFT enum, no
autosave.
**Do:**
- Add a `DRAFT` status to the application status enum + a "Save draft" path that persists without
  submitting; "Submit" moves DRAFT→SUBMITTED. Migration required.
- Add localStorage autosave to the apply templates (mirror the incubation wizard / SME-Hub P1
  autosave partial already in the codebase — grep for the existing localStorage autosave pattern
  and reuse it). Show a "Draft saved" indicator.
**QA:**
1. As entrepreneur, start an event/challenge application, fill some fields, click "Save draft";
   assert (shell) the row is DRAFT and `location` returned to the entrepreneur's applications list.
2. Reopen the draft — fields are prefilled; the status badge reads Draft.
3. Reload mid-edit before saving — localStorage restores unsaved input (assert a field value via
   `eval`).
4. Submit the draft — assert status → SUBMITTED and it appears in the admin review queue.

### 6. ISD011 — generated video-conferencing links (showcasing)
**FR:** "provide **integrated video conferencing links** for virtual or hybrid Showcase Events,
**generated and distributed** to confirmed participants and evaluators through the system."
**Current:** just a manual `video_link` URL on the event; no generation, no distribution, no access
log.
**Do:**
- On go-live of a virtual/hybrid event, generate a conferencing link (integrate with the platform's
  existing BBB/meeting infra if present — grep for `bbb`/`meeting`/`video` integrations; otherwise
  provide a pluggable provider with a sensible default and a config point).
- Distribute the link to confirmed participants and evaluators (notification + email).
- Log access when a participant opens the link (an access-log row: user, event, timestamp).
**QA:**
1. As sme_admin, create a virtual event, confirm 1 participant + 1 evaluator, take it live; assert
   a link was generated and both parties received a notification with it.
2. As the confirmed participant, open the join URL; assert an access-log row is written (shell).
3. A non-participant hitting the join URL is blocked (403/redirect).

### 7. OBR034 — admin set/correct AIH affiliation (onboarding)
**FR:** "allow the administrator to **set or correct** an entrepreneur's AIH affiliation from the
backend, for example during bulk cohort onboarding or where the entrepreneur cannot complete
affiliation independently."
**Current:** service `by_admin` / `set_by_admin` flags exist but there is no admin URL/view/form;
`business_detail_admin.html` shows affiliation read-only.
**Do:**
- Add an admin view + form + URL to set/correct a business's AIH affiliation (reuse the existing
  `record_aih_affiliation(..., by_admin=True)` service path). Gate to SME-admin roles.
- Surface an "Edit affiliation" action on `business_detail_admin.html`; write an AuditLog entry
  noting the admin actor.
**QA:**
1. As sme_admin, open a verified business detail → "Edit affiliation", set an AIH; assert (shell)
   the affiliation is stored with `by_admin=True` and an AuditLog row exists.
2. Correct it to a different AIH; assert the change + a second audit entry.
3. Confirm a non-admin cannot reach the edit URL (403).

### 8. INV thresholds config + INV004 structured eligibility (investment)
**FR INV004:** publish Funding Call / Grant Listing records "…with **eligibility criteria**, funding
amount, application requirements, and deadline defined."
**Current:** `default_readiness_thresholds()` in `investment/services.py` is hard-coded (overrides
only via kwarg, no admin UI); INV004 eligibility is free-form JSON with no structured builder.
**Do:**
- Add a per-cohort/per-programme readiness-threshold config model (or fields) read by
  `compute_readiness()`, with an admin UI. Migration required.
- Replace the free-form eligibility JSON on funding calls with a structured builder (sector,
  business stage, min traction, geography, etc.), validated and used by the eligibility match on
  publish. Reuse the SP2 call-management infrastructure the FR references.
**QA:**
1. As sme_admin, set a custom readiness threshold; assert `compute_readiness` for an entrepreneur
   reflects the new threshold (shell before/after).
2. Publish a funding call with structured eligibility; assert only matching entrepreneurs are
   notified/eligible (shell + the entrepreneur's funding-calls list).
3. The matchmaking dashboard readiness banner reflects the configured thresholds.

---

## Final acceptance (run after ALL items)
1. `docker compose exec -T web python manage.py makemigrations --check --dry-run` — no unstaged
   model changes; every new migration is committed.
2. Full suite green: `docker compose exec -T web sh -c "python -m pytest apps/smehub -q > /tmp/res.txt 2>&1; echo EXIT $?; tail -20 /tmp/res.txt"` (expect EXIT 0; baseline was **492 passed**).
3. `docker compose exec -T web python manage.py check` clean.
4. Browser smoke every touched page across the relevant role (entrepreneur / sme_admin / judge /
   investor / service_provider) — assert no `Server Error / NoReverse / TemplateSyntax / FieldError`
   in `document.body.innerText` and `location.pathname` is correct. Screenshot each key state under
   the session scratchpad.
5. Report per item: files changed (path:line), migration filenames, the exact QA assertions you ran
   and their results, and anything you deferred with the reason.
