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
| Area | Change | Why |
|---|---|---|
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 state | Classification | Shown? |
|---|---|---|
| Matches an active member | synced | No |
| Matches a deactivated member | already_synced_then_off | No |
| Also has a pending invite | still_pending | No |
| Matches PRAMAAN internal domain | expected_staff | No |
| Matches no membership and is not internal | accepted_unsynced | Needs 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
- Done Scope stayed in
pramaan-app; no functions/admin edits and no new functions endpoint. - Done Members boundary still uses
membersGateway.listwith generatedsrc/genZod contract validation. - Done Internal-domain staff accepted invites are classified silent via
isInternalEmailDomain. - Done Reconcile tests cover active, deactivated, duplicate pending, internal-domain, and unsynced accepted invites.
- Done Storybook has
NeedsAttentionandZeroDriftNoBannerstates forTeamListContent. - Done New UI copy is present in both
messages/en.jsonandmessages/hi.json.
Verification
pnpm lint- passed with one unrelated existingno-img-elementwarning.pnpm test- 117 files passed, 741 tests passed.pnpm build- passed.pnpm storybook:build- passed.git diff --check- passed.
Files changed
20 file(s), +484 / -36 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +5 | -0 |
| messages/hi.json | Config | +5 | -0 |
| src/app/[locale]/(workspace)/settings/firm/team/page.tsx | App route | +12 | -2 |
| src/app/api/internal/team-invitations/reconcile/route.ts | App route | +24 | -0 |
| src/components/settings/team/presentation.ts | Component | +6 | -1 |
| src/components/settings/team/team-fixtures.ts | Component | +20 | -1 |
| src/components/settings/team/team-list-content.stories.tsx | Component | +11 | -0 |
| src/components/settings/team/team-list-content.tsx | Component | +48 | -4 |
| src/components/settings/team/team-list-page.stories.tsx | Component | +13 | -1 |
| src/components/settings/team/team-list-page.tsx | Component | +31 | -6 |
| src/components/settings/team/team-storybook-helpers.tsx | Component | +9 | -1 |
| src/components/settings/team/use-team-mutations.ts | Component | +13 | -1 |
| src/lib/team-invitations/hooks.ts | Library | +32 | -3 |
| src/lib/team-invitations/query.ts | Library | +3 | -0 |
| src/lib/team-invitations/reconcile.test.ts | Library | +98 | -0 |
| src/lib/team-invitations/reconcile.ts | Library | +63 | -0 |
| src/lib/team-invitations/schema.ts | Library | +31 | -1 |
| src/lib/team-invitations/server.test.ts | Library | +19 | -4 |
| src/lib/team-invitations/server.ts | Library | +36 | -9 |
| tests/unit/team-presentation.test.ts | Test | +5 | -2 |