Docs / Admin
Investment facilitation (Phase 4)
Summary — The Facilitation section (/facilitation/*) is a read-oriented reporting layer over the matching module: which investor profiles have been qualified, which B2B meetings have happened, which follow-ups are outstanding, and a metrics snapshot tying qualification through to deal flow.
Purpose — Understand the investor qualification workflow that feeds the Qualified tab, what each facilitation tab shows, and where the underlying data comes from.
Audience — Staff, reviewer, lead, and admin roles working Phase 4 (investment facilitation) of the program.
Prerequisites — Investor profiles and matches already exist — see Investor-opportunity matching and B2B meetings.
Overview
Facilitation has four tabs, all under src/app/(authenticated)/facilitation/:
| Route | Purpose |
|---|---|
/facilitation/qualified | Investor profiles that have reached a qualified state in the investor-qualification workflow. |
/facilitation/follow-up | Follow-up actions tracked from B2B meetings (backed by the generic Issues module). |
/facilitation/meetings-log | Full B2B meeting log across all opportunities, filterable by status. |
/facilitation/metrics | A five-card snapshot: qualified investors, matched, meetings completed, promoted to deal flow, successful matches. |
Key concepts:
- Investor qualification workflow — A
workflow-statesmodule workflow (sluginvestor-qualification, entity typeinvestor_profile) with its own states and transition rules, separate from match status. - Qualified states —
qualified,qualified_with_conditions, andmatchedare the states this reporting surface treats as "qualified" (see below — the two lists differ slightly). - Event follow-up — Not a new table; it is an
Issueof typeevent-follow-up, linked to the originating B2B meeting.
Screenshot: Facilitation Metrics page showing five stat cards — Qualified Investors, Matched, Meetings Completed, Promoted to Deal Flow, Successful Matches.
The investor qualification workflow
Seeded per-tenant by migration 060_phase4_investor_facilitation.sql as workflow slug investor-qualification, entity type investor_profile.
States:
| State | Label | Final? |
|---|---|---|
submitted | Submitted | No |
incomplete | Incomplete | No |
validated | Validated | No |
qualified | Qualified | No |
qualified_with_conditions | Qualified (Conditions) | No |
not_qualified | Not Qualified | Yes |
matched | Matched | Yes |
Transitions and required role:
| From | To | Label | Required role |
|---|---|---|---|
submitted | validated | Validate | staff |
submitted | incomplete | Mark Incomplete | staff |
incomplete | validated | Validate | staff |
validated | qualified | Qualify | reviewer |
validated | qualified_with_conditions | Qualify with Conditions | reviewer |
validated | not_qualified | Disqualify | reviewer |
qualified_with_conditions | qualified | Clear Conditions | lead |
* (any state) | not_qualified | Disqualify | reviewer |
qualified | matched | Mark Matched | reviewer |
One extra rule not expressible in the generic state machine: a profile cannot move into qualified or qualified_with_conditions unless it has at least one entry in sectorsOfInterest and a non-null counterpartyType. This is enforced by assertQualifiable() in src/lib/modules/matching/qualification.ts, with these verbatim errors:
Cannot qualify: profile has no sectors of interest.
Cannot qualify: profile is missing a counterparty type.
This check runs before every call to transitionQualification(), in addition to (not instead of) the workflow engine's own transition and role validation and its own audit logging (no duplicate audit call is made in the matching module wrapper).
There is no dedicated action route reviewed for triggering these transitions from the UI other than transitionQualificationAction (a server action taking organizationId + toState + optional notes) — the coarse gate on the action itself only requires staff; the workflow engine enforces the finer per-transition role shown above and throws if unmet.
Qualified Investors tab
Path: /facilitation/qualified
Calls listQualifiedProfiles(), which reads the latest state per investor_profile entity from state_transitions directly (there is no generic "latest state per entity" helper on the workflow-states module, so this reads the table with the same tenant-scoped pattern module repositories use) and filters to profiles whose latest state is one of:
qualifiedqualified_with_conditionsmatched
Each row links to the organization's stakeholder page and shows a StatusBadge for the qualification state. Empty state: "No qualified investors yet — Move investor profiles through qualification on their organization page."
Note: this "report" set of states (qualified, qualified_with_conditions, matched) is the same as QUALIFIED_STATES ∪ {matched} used internally — QUALIFIED_STATES (used only by the assertQualifiable gate) is {qualified, qualified_with_conditions}, while the reporting set additionally includes matched.
B2B Meetings Log tab
Path: /facilitation/meetings-log
Lists all B2B meetings (listB2bMeetings), filterable by status via the FilterBar (scheduled, completed, cancelled, no_show — same statuses as on /matching/meetings). Each row links through to /matching/meetings/[id] for the full meeting detail (status/outcome editing, participants, promote outcome) — see Investor-opportunity matching and B2B meetings for that detail page's full behavior. This tab is a cross-opportunity view; the matching module's own /matching/meetings page shows the same data split into Upcoming/Past sections.
Follow-up tab
Path: /facilitation/follow-up
Follow-ups are not a dedicated table — a follow-up is an Issue of type event-follow-up (seeded per-tenant by migration 060), created via createEventFollowUp() from a B2B meeting's detail page ("Create Follow-up" form). The follow-up issue carries:
title:"Follow-up: {meeting.title}"description: the free-text note entered- optional
assignedTo,dueDate fieldData:{ meetingId, organizationId }linkedEntityType: "b2b_meeting",linkedEntityId: meeting.id
The issue type is seeded with default_status: "open" and allowed_statuses: [open, in_progress, resolved, closed].
The Facilitation Follow-up tab calls listIssues({ typeSlug: "event-follow-up" }) and lists title, due date, and status, with each row linking to the full issue detail at /issues/{id} and a top-level link to "View in Issues" for the generic issue tracker.
b2b_meetings.follow_up_action (a free-text field editable from the meeting's Status & Outcome form) is a separate, simpler note field on the meeting itself — it is not synced with the Issue created here.
Metrics tab
Path: /facilitation/metrics
Five cards, computed by:
| Card | Source |
|---|---|
| Qualified Investors | listQualifiedProfiles().length (same set as the Qualified tab: qualified, qualified_with_conditions, matched) |
| Matched | Of the qualified rows, count where state === "matched" |
| Meetings Completed | listB2bMeetings({ status: "completed" }).length |
| Promoted to Deal Flow | Of completed meetings, count where metadata.promotedOpportunityId is set (i.e., outcome was promoted via the deal_flow decision) |
| Successful Matches | listMatches({ status: "successful" }).length |
This is a point-in-time snapshot with no date-range filter, export, or drill-down in the code reviewed.
Roles and permissions
| Action | Minimum role |
|---|---|
| View any facilitation tab | staff (inherits the authenticated layout's default gate — no facilitation-specific role check found in the four page.tsx files themselves) |
| Transition investor qualification (coarse gate) | staff |
| Qualify / Qualify with Conditions / Disqualify | reviewer (per workflow transition requiredRole) |
Clear Conditions (qualified_with_conditions → qualified) | lead |
| Create event follow-up | staff |
Best practices
- Qualify profiles before they show up here. The Qualified tab and the Matched metric are both driven entirely by the investor-qualification workflow state — a profile with a great match score but no qualification transition applied will not appear.
- Use Promote Outcome (deal_flow) consistently on completed B2B meetings you want reflected in the "Promoted to Deal Flow" metric — the count only increments when
promotedOpportunityIdis set via that path. - Route ad-hoc action items through Create Follow-up, not the meeting's
followUpActionfree-text field, if you want them tracked and assignable in Issues.
Warnings
not_qualified and matched are terminal in the investor-qualification workflow — once reached, no further transition is defined except the wildcard * → not_qualified disqualify path (which does not apply once already not_qualified, since that would be a self-transition).
The Qualified tab and QUALIFIED_STATES guard use different state sets. The tab's report set includes matched; the qualification gate (assertQualifiable) does not check matched at all since matched profiles are already past the gate.
Troubleshooting
"Cannot qualify: profile has no sectors of interest." / "...missing a counterparty type."
Symptom: A qualification transition to qualified or qualified_with_conditions fails.
Cause: The investor profile is missing sectorsOfInterest or counterpartyType.
Fix: Update the investor profile (Upsert Investor Profile form on the organization page) to add at least one sector of interest and a counterparty type, then retry the transition.
A meeting doesn't count toward "Promoted to Deal Flow"
Symptom: A completed B2B meeting isn't reflected in the metrics card.
Cause: The metric only counts completed meetings with metadata.promotedOpportunityId set — which only happens via the "Deal flow — source an opportunity" decision on Promote Outcome, not by manually setting status to completed.
Fix: Use the Promote Outcome form with decision deal_flow on the meeting detail page.
FAQ
Is "Qualified" on this page the same as a match status?
No. Match status (suggested/approved/etc.) lives on the Match record between an opportunity and organization. Qualification status lives on the investor profile itself, independent of any specific opportunity match.
Can a profile be qualified without ever being matched?
Yes — qualified and qualified_with_conditions are non-final states; matched is a separate, later transition (qualified → matched, requires reviewer) typically applied once matchmaking has actually produced a result.
Where do event follow-ups show up outside of Facilitation?
They are ordinary Issues of type event-follow-up and also appear in the generic /issues list and detail pages.
Related articles
- Investor-opportunity matching and B2B meetings — the matches and meetings this reporting layer summarizes.
- The investment pipeline lifecycle — how opportunities reach
matchmaking/facilitationin the first place. - Matching and scoring configuration — scoring dimension setup for matches.