Skip to main content

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:

ScenarioWho c.invitedBy isWhat displayedWhat should display
User invited someone (outbound)UserUser's nameThe other party (c.user)
User was invited (inbound)Other partyOther partyOther 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:

#userIdinvitedByIdCompanyTypeActual personcanLeave
1rezdev97rezdev97TestCOMPANY_PARTNERselftrue
2UI Testerrezdev97coaiCOMPANY_PARTNERUI Testerfalse
3Tester NoCompanyrezdev97coaiCOMPANY_PARTNERTester NoCompanyfalse

All 3 displayed "Reza Zeerat" (c.invitedBy) instead of the actual collaborator.


Changes made

Backend (aiqlick-backend — commit fd236cf0)

FileChange
services/collaboration-invitation-core.service.tsAdded self-collaboration guard: look up the target user by email for BOTH types, reject if targetUser.id === invitedById
services/collaboration-invitation-core.service.tsExtended existing-collaboration dedup to COMPANY_PARTNER, scoped by Invitation.invitationType
services/collaborator-management.service.tsAdded invitationType filtering for listMyCollaborators and excludes invalid self-collaboration rows from normal lists
dto/collaborator.dto.tsAdded invitationType?: CollaborationInvitationType field to CollaboratorOutput and ListCollaboratorsInput
resolvers/collaboration.resolver.tsAdded @ResolveField('invitationType') backed by existing DataLoader.loadInvitation()
schema.gqlRegenerated GraphQL schema with CollaboratorOutput.invitationType and ListCollaboratorsInput.invitationType

Frontend (aiqlick-frontend — commit 475f3df8)

FileChange
jobseeker/.../page.tsxDirection-aware otherParty — inbound → show c.invitedBy, outbound → show c.user
company/.../page.tsxSame fix for the invitedBy display field
company/.../[id]/page.tsxSuccess toast shows otherParty instead of collaborator.user
company/.../SharedTalentsTab.tsxsharedBy label uses direction-aware shareParty
company/.../CollaboratingPartnerListCard.tsxDelete confirmation dialog uses direction-aware displayUser
graphql/.../queries.tsAdded invitationType and invitedBy.title to collaborator queries
jobseeker/.../page.tsxFilters active collaborations to JOBSEEKER and ignores invalid self rows
company/.../page.tsxRequests COMPANY_PARTNER rows explicitly
company/.../SentJobseekerInvitations.tsxRequests JOBSEEKER rows explicitly

Company page tab architecture (unaffected)

The company page already properly separates collaborations into two tabs:

TabData sourceFilter
Company PartnerslistMyCollaboratorsExcludes records without roleId, pipelineShares, or talentShares
Jobseeker ConnectionsSeparate SentJobseekerInvitations / ReceivedJobSeekerInvitations componentsIndependent 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.ts
    • npm test -- --runInBand src/connections/collaboration-management/services/collaborator-management.service.spec.ts
    • npm 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