{"record":{"id":"c72a033b97e8d8de","repo":"plandex-ai/plandex","slug":"error-creating-org-user-v","errorCode":null,"errorMessage":"error creating org user: %v","messagePattern":"error creating org user: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/invite_helpers.go","lineNumber":129,"sourceCode":"\t\treturn fmt.Errorf(\"error deleting invite: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc AcceptInvite(ctx context.Context, invite *Invite, inviteeId string) error {\n\terr := WithTx(ctx, \"accept invite\", func(tx *sqlx.Tx) error {\n\n\t\t_, err := tx.Exec(`UPDATE invites SET accepted_at = NOW(), invitee_id = $1 WHERE id = $2`, inviteeId, invite.Id)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error accepting invite: %v\", err)\n\t\t}\n\n\t\t// create org user\n\t\terr = CreateOrgUser(invite.OrgId, inviteeId, invite.OrgRoleId, tx)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating org user: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error accepting invite: %v\", err)\n\t}\n\n\tinvite.InviteeId = &inviteeId\n\n\treturn nil\n}\n","sourceCodeStart":111,"sourceCodeEnd":143,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/invite_helpers.go#L111-L143","documentation":"AcceptInvite runs inside a WithTx transaction: after marking the invite accepted, it calls CreateOrgUser to add the invitee to the org. If CreateOrgUser fails (e.g. role or org rows missing, DB constraint, duplicate org_users row), the error is wrapped as \"error creating org user: %v\" and the whole transaction rolls back, so the invite stays unaccepted.","triggerScenarios":"Calling AcceptInvite when invite.OrgId or invite.OrgRoleId references a deleted org/org_role, when the user already has an org_users row for that org (unique constraint), or when the org_users insert hits any DB error.","commonSituations":"Accepting a stale invite after the org was deleted; accepting the same invite twice concurrently so both transactions race to insert the org user; an org_role id captured on the invite no longer exists after role cleanup.","solutions":["Check the wrapped inner error for a uniqueness/duplicate-key violation — the user is likely already a member; treat as success or delete the org_users row and retry.","Verify invite.OrgId and invite.OrgRoleId still exist in orgs/org_roles before accepting; refresh or re-issue the invite if not.","Retry AcceptInvite — the transaction rolled back, so the invite is still pending and a transient DB error can simply be retried.","Inspect CreateOrgUser directly with the given orgId, userId, roleId to reproduce the underlying insert error."],"exampleFix":"// before\nerr = AcceptInvite(ctx, invite, userId)\nif err != nil { return err }\n// after\nerr = AcceptInvite(ctx, invite, userId)\nif err != nil {\n    if strings.Contains(err.Error(), \"duplicate key\") {\n        return nil // user already a member of the org\n    }\n    return err\n}","handlingStrategy":"try-catch","validationCode":"// before calling AcceptInvite\nvar orgExists, roleExists bool\nif err := db.Conn.Get(&orgExists, \"SELECT EXISTS(SELECT 1 FROM orgs WHERE id=$1)\", invite.OrgId); err != nil || !orgExists {\n    return fmt.Errorf(\"invite org %s no longer exists\", invite.OrgId)\n}\nif err := db.Conn.Get(&roleExists, \"SELECT EXISTS(SELECT 1 FROM org_roles WHERE id=$1)\", invite.OrgRoleId); err != nil || !roleExists {\n    return fmt.Errorf(\"invite role %s no longer exists\", invite.OrgRoleId)\n}","typeGuard":"func inviteValid(inv *db.Invite) bool {\n    return inv != nil && inv.Id != \"\" && inv.OrgId != \"\" && inv.OrgRoleId != \"\"\n}","tryCatchPattern":"err := AcceptInvite(ctx, invite, userId)\nif err != nil {\n    if strings.Contains(err.Error(), \"error creating org user\") && strings.Contains(err.Error(), \"duplicate key\") {\n        return nil // already a member\n    }\n    return fmt.Errorf(\"accept invite failed: %w\", err)\n}","preventionTips":["Check org_users for an existing membership before accepting to make the operation idempotent.","Re-fetch the invite immediately before accepting to avoid acting on stale/deleted rows.","Guard the accept endpoint against double-submits (idempotency key or UI disabling)."],"tags":["database","transaction","invite","org-user"],"backgroundTag":"transaction-rollback-on-insert","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}