Collaboration partner: direction & self-invite bug report
Date: 2026-06-28
Author: AI
Scope: aiqlick-backend + aiqlick-frontend — collaboration partner display and invitation logic
Problem
A jobseeker viewing /jobseeker/connections/collaboratingpartner saw their own name on every collaborator card instead of the actual other party. The same bug existed on the company's /company/connections/collaboratingpartner page.
Additionally, a user could invite themselves as a collaborator (both userId and invitedById set to the same person), with no guard preventing it.
Root cause analysis
1. Wrong direction mapping (frontend)
listMyCollaborators returns all Collaborator records where the current user is either userId (the invitee) OR invitedById (the inviter). The frontend was always rendering c.invitedBy as the card's name — but for records where the user sent the invitation, c.invitedBy IS the user. This meant:
| Scenario | Who c.invitedBy is | What displayed | What should display |
|---|---|---|---|
| User invited someone (outbound) | User | User's name | The other party (c.user) |
| User was invited (inbound) | Other party | Other party | Other party (coincidentally correct) |
2. No self-invite guard (backend)
createCollaborationInvitation had no check for invitedById === targetUser.id. The existing dedup for JOBSEEKER type (checking { userId: targetUser.id, companyId }) only ran inside if (invitationType === JOBSEEKER) — COMPANY_PARTNER type had no existing-collaboration dedup at all. Additionally, the user-by-email lookup was inside the JOBSEEKER block, so COMPANY_PARTNER couldn't even check.
3. Missing invitationType on GraphQL CollaboratorOutput
The CollaboratorOutput type did not expose invitationType, so the frontend couldn't distinguish JOBSEEKER from COMPANY_PARTNER collaborations in listMyCollaborators.
4. Mixed collaboration types in active lists
After exposing invitationType, listMyCollaborators still needed a type filter. Without that filter, jobseeker pages could still receive COMPANY_PARTNER rows and company jobseeker tabs could still infer intent from role/share fields instead of the explicit invitation type.
Database evidence (dev RDS)
User rezdev97@gmail.com (id 2449ca19) had 3 ACTIVE collaborator records:
| # | userId | invitedById | Company | Type | Actual person | canLeave |
|---|---|---|---|---|---|---|
| 1 | rezdev97 | rezdev97 | Test | COMPANY_PARTNER | self | true |
| 2 | UI Tester | rezdev97 | coai | COMPANY_PARTNER | UI Tester | false |
| 3 | Tester NoCompany | rezdev97 | coai | COMPANY_PARTNER | Tester NoCompany | false |
All 3 displayed "Reza Zeerat" (c.invitedBy) instead of the actual collaborator.
Changes made
Backend (aiqlick-backend — commit fd236cf0)
| File | Change |
|---|---|
services/collaboration-invitation-core.service.ts | Added self-collaboration guard: look up the target user by email for BOTH types, reject if targetUser.id === invitedById |
services/collaboration-invitation-core.service.ts | Extended existing-collaboration dedup to COMPANY_PARTNER, scoped by Invitation.invitationType |
services/collaborator-management.service.ts | Added invitationType filtering for listMyCollaborators and excludes invalid self-collaboration rows from normal lists |
dto/collaborator.dto.ts | Added invitationType?: CollaborationInvitationType field to CollaboratorOutput and ListCollaboratorsInput |
resolvers/collaboration.resolver.ts | Added @ResolveField('invitationType') backed by existing DataLoader.loadInvitation() |
schema.gql | Regenerated GraphQL schema with CollaboratorOutput.invitationType and ListCollaboratorsInput.invitationType |
Frontend (aiqlick-frontend — commit 475f3df8)
| File | Change |
|---|---|
jobseeker/.../page.tsx | Direction-aware otherParty — inbound → show c.invitedBy, outbound → show c.user |
company/.../page.tsx | Same fix for the invitedBy display field |
company/.../[id]/page.tsx | Success toast shows otherParty instead of collaborator.user |
company/.../SharedTalentsTab.tsx | sharedBy label uses direction-aware shareParty |
company/.../CollaboratingPartnerListCard.tsx | Delete confirmation dialog uses direction-aware displayUser |
graphql/.../queries.ts | Added invitationType and invitedBy.title to collaborator queries |
jobseeker/.../page.tsx | Filters active collaborations to JOBSEEKER and ignores invalid self rows |
company/.../page.tsx | Requests COMPANY_PARTNER rows explicitly |
company/.../SentJobseekerInvitations.tsx | Requests JOBSEEKER rows explicitly |
Company page tab architecture (unaffected)
The company page already properly separates collaborations into two tabs:
| Tab | Data source | Filter |
|---|---|---|
| Company Partners | listMyCollaborators | Excludes records without roleId, pipelineShares, or talentShares |
| Jobseeker Connections | Separate SentJobseekerInvitations / ReceivedJobSeekerInvitations components | Independent queries for job seeker invitations |
The tabs now use explicit invitationType filtering instead of relying only on role/share presence.
Verification
- Backend targeted tests:
npm test -- --runInBand src/connections/collaboration-management/services/collaboration-invitation-core.service.spec.tsnpm test -- --runInBand src/connections/collaboration-management/services/collaborator-management.service.spec.tsnpm test -- --runInBand src/connections/collaboration-management/resolvers/collaboration.resolver.spec.ts
- Backend schema generation:
npm run schema:generate - Frontend production build:
npm run build - Documentation build:
npm run build