PR: #409: feat(team): surface accepted invite reconciliation [Story-1681] by @ayush-pramaan
View PR →
Story 1681 · pramaan-app · Team page reconciliation

Accepted invitations no longer vanish between Pending and Members

The Team page now asks the app server for Clerk accepted invitations, compares them with the validated firm-members response, and shows a needs-attention banner only for accepted invitees who are not present in any membership row and are not PRAMAAN internal-domain staff.

Problem

Before this change, the Team page rendered two separate lists: PostgreSQL members from GET /app/firm/members and Clerk invitations with status pending. Once a person accepted the invite, Clerk moved them out of Pending. If the webhook did not create a member row, they disappeared from both lists.

What changed

AreaChangeWhy
team-invitations/server.ts Fetches Clerk pending and accepted invitations; preserves accepted in the adapter. The app cannot detect accepted-but-unsynced invitees unless it keeps accepted invites visible to the server model.
team-invitations/reconcile.ts Normalizes email and classifies accepted invites as synced, already offboarded, still pending, expected staff, or unsynced. The warning should fire only on real drift, not on deactivated members or PRAMAAN staff identity-hat invites.
/api/internal/team-invitations/reconcile Adds an app-only BFF read route backed by existing Clerk and members reads. The browser receives a single hydrated reconcile dataset without adding a functions endpoint.
TeamListContent Renders a token-backed attention banner when unsynced accepted invites exist. Firm admins can see the gap instead of mistaking disappearance for completion.

Classification rules

Accepted invite email stateClassificationShown?
Matches an active membersyncedNo
Matches a deactivated memberalready_synced_then_offNo
Also has a pending invitestill_pendingNo
Matches PRAMAAN internal domainexpected_staffNo
Matches no membership and is not internalaccepted_unsyncedNeeds attention

User-facing state

The zero-drift state has no banner. The Pending tab and seat count only include live pending invitations, so accepted records do not inflate pending work.

Acceptance evidence

Verification

Files changed

20 file(s), +484 / -36 lines

Path Role + -
messages/en.jsonConfig+5-0
messages/hi.jsonConfig+5-0
src/app/[locale]/(workspace)/settings/firm/team/page.tsxApp route+12-2
src/app/api/internal/team-invitations/reconcile/route.tsApp route+24-0
src/components/settings/team/presentation.tsComponent+6-1
src/components/settings/team/team-fixtures.tsComponent+20-1
src/components/settings/team/team-list-content.stories.tsxComponent+11-0
src/components/settings/team/team-list-content.tsxComponent+48-4
src/components/settings/team/team-list-page.stories.tsxComponent+13-1
src/components/settings/team/team-list-page.tsxComponent+31-6
src/components/settings/team/team-storybook-helpers.tsxComponent+9-1
src/components/settings/team/use-team-mutations.tsComponent+13-1
src/lib/team-invitations/hooks.tsLibrary+32-3
src/lib/team-invitations/query.tsLibrary+3-0
src/lib/team-invitations/reconcile.test.tsLibrary+98-0
src/lib/team-invitations/reconcile.tsLibrary+63-0
src/lib/team-invitations/schema.tsLibrary+31-1
src/lib/team-invitations/server.test.tsLibrary+19-4
src/lib/team-invitations/server.tsLibrary+36-9
tests/unit/team-presentation.test.tsTest+5-2