# Block Community Removal Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** When a user blocks someone, remove them from communities the blocker created, prevent rejoin while blocked, and hide blocked users from the blocker’s community member/post views everywhere.

**Architecture:** Keep `BlockUser` as source of truth. Add a small helper module for blocked-ID resolution, creator-community membership cleanup, and block-between checks. Wire cleanup into `blockUserToggle`, gates into join/accept, and filters into member/post reads.

**Tech Stack:** Node.js, Express, TypeScript, Mongoose, Jest

## Global Constraints

- No new community schema fields (`bannedMembers`, etc.).
- Unblock must not restore membership or join requests.
- Membership mutation only for communities where `createdBy === blocker`.
- Visibility filtering uses block relationship in either direction.
- Leave blocked users’ posts in the DB; hide from viewer queries/responses.

## File map

| File | Responsibility |
|------|----------------|
| `src/utils/Block/blockCommunityHelpers.ts` | Shared helpers: blocked IDs, block-between check, remove from creator communities + pending requests |
| `src/tests/utils/blockCommunityHelpers.test.ts` | Unit tests for helpers |
| `src/constants/messages.ts` | Join-blocked error message |
| `src/controllers/auth.controller.ts` | Call cleanup on block create path |
| `src/controllers/community.controller.ts` | Join/accept gates + member/post visibility filters |

---

### Task 1: Block community helpers + unit tests

**Files:**
- Create: `src/utils/Block/blockCommunityHelpers.ts`
- Create: `src/tests/utils/blockCommunityHelpers.test.ts`
- Modify: `src/constants/messages.ts` (add `UNABLE_TO_JOIN_BLOCKED`)

**Interfaces:**
- Produces:
  - `getBlockedProfileIds(viewerId: string): Promise<string[]>`
  - `isBlockedBetween(profileA: string, profileB: string): Promise<boolean>`
  - `removeBlockedUserFromCreatorCommunities(blockerId: string, blockedUserId: string): Promise<void>`

- [x] **Step 1: Write failing tests**

```typescript
jest.mock('../../models/BlockUser.model', () => ({
  __esModule: true,
  default: { find: jest.fn(), findOne: jest.fn() },
}));
jest.mock('../../models/community.model', () => ({
  __esModule: true,
  default: { updateMany: jest.fn(), find: jest.fn() },
}));
jest.mock('../../models/communityRequest.model', () => ({
  __esModule: true,
  default: { deleteMany: jest.fn() },
}));

import BlockUserModel from '../../models/BlockUser.model';
import communityModel from '../../models/community.model';
import communityRequestModel from '../../models/communityRequest.model';
import {
  getBlockedProfileIds,
  isBlockedBetween,
  removeBlockedUserFromCreatorCommunities,
} from '../../utils/Block/blockCommunityHelpers';

describe('blockCommunityHelpers', () => {
  beforeEach(() => jest.clearAllMocks());

  it('getBlockedProfileIds returns the other party for both directions', async () => {
    (BlockUserModel.find as jest.Mock).mockResolvedValue([
      { blockedBy: 'viewer', blockedUser: 'u1' },
      { blockedBy: 'u2', blockedUser: 'viewer' },
    ]);
    await expect(getBlockedProfileIds('viewer')).resolves.toEqual(['u1', 'u2']);
  });

  it('isBlockedBetween is true when either direction exists', async () => {
    (BlockUserModel.findOne as jest.Mock).mockResolvedValue({ _id: 'b1' });
    await expect(isBlockedBetween('a', 'b')).resolves.toBe(true);
  });

  it('removeBlockedUserFromCreatorCommunities pulls member and deletes pending requests', async () => {
    (communityModel.find as jest.Mock).mockReturnValue({
      select: jest.fn().mockResolvedValue([{ _id: 'c1' }, { _id: 'c2' }]),
    });
    (communityModel.updateMany as jest.Mock).mockResolvedValue({});
    (communityRequestModel.deleteMany as jest.Mock).mockResolvedValue({});

    await removeBlockedUserFromCreatorCommunities('blocker', 'blocked');

    expect(communityModel.updateMany).toHaveBeenCalledWith(
      { createdBy: 'blocker', members: 'blocked' },
      { $pull: { members: 'blocked' }, $inc: { membersCount: -1 } }
    );
    expect(communityRequestModel.deleteMany).toHaveBeenCalledWith({
      profileId: 'blocked',
      communityId: { $in: ['c1', 'c2'] },
    });
  });
});
```

- [ ] **Step 2: Run tests — expect FAIL (module missing)**

Run: `npx jest src/tests/utils/blockCommunityHelpers.test.ts -v`  
Expected: FAIL cannot find module

- [ ] **Step 3: Implement helpers + message constant**

`src/constants/messages.ts` — add to `COMMUNITY_CONSTANTS`:

```typescript
UNABLE_TO_JOIN_BLOCKED: "Unable to join this community",
```

`src/utils/Block/blockCommunityHelpers.ts`:

```typescript
import BlockUserModel from '../../models/BlockUser.model';
import communityModel from '../../models/community.model';
import communityRequestModel from '../../models/communityRequest.model';

export async function getBlockedProfileIds(viewerId: string): Promise<string[]> {
  const blocks = await BlockUserModel.find({
    $or: [{ blockedBy: viewerId }, { blockedUser: viewerId }],
  }).select('blockedBy blockedUser');

  return blocks.map((block: any) =>
    block.blockedBy.toString() === viewerId.toString()
      ? block.blockedUser.toString()
      : block.blockedBy.toString()
  );
}

export async function isBlockedBetween(profileA: string, profileB: string): Promise<boolean> {
  const block = await BlockUserModel.findOne({
    $or: [
      { blockedBy: profileA, blockedUser: profileB },
      { blockedBy: profileB, blockedUser: profileA },
    ],
  });
  return Boolean(block);
}

export async function removeBlockedUserFromCreatorCommunities(
  blockerId: string,
  blockedUserId: string
): Promise<void> {
  const ownedCommunities = await communityModel
    .find({ createdBy: blockerId })
    .select('_id');
  const communityIds = ownedCommunities.map((c: any) => c._id);

  await Promise.all([
    communityModel.updateMany(
      { createdBy: blockerId, members: blockedUserId },
      { $pull: { members: blockedUserId }, $inc: { membersCount: -1 } }
    ),
    communityIds.length > 0
      ? communityRequestModel.deleteMany({
          profileId: blockedUserId,
          communityId: { $in: communityIds },
        })
      : Promise.resolve(),
  ]);
}
```

- [ ] **Step 4: Run tests — expect PASS**

Run: `npx jest src/tests/utils/blockCommunityHelpers.test.ts -v`  
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/utils/Block/blockCommunityHelpers.ts src/tests/utils/blockCommunityHelpers.test.ts src/constants/messages.ts
git commit -m "feat: add block community helpers for membership cleanup"
```

---

### Task 2: Cleanup on block in `blockUserToggle`

**Files:**
- Modify: `src/controllers/auth.controller.ts` (`blockUserToggle` create path, after `BlockUserModel.create` / follow cleanup)

**Interfaces:**
- Consumes: `removeBlockedUserFromCreatorCommunities(blockerId, blockedUserId)`

- [ ] **Step 1: Wire cleanup into create-block path**

After creating the block and follow deletions (around the existing `Promise.all` for follows), call:

```typescript
await removeBlockedUserFromCreatorCommunities(profileId, user._id.toString());
```

Import:

```typescript
import { removeBlockedUserFromCreatorCommunities } from "../utils/Block/blockCommunityHelpers";
```

Do **not** call this on the unblock path.

- [ ] **Step 2: Smoke-check TypeScript compile for auth controller**

Run: `npx tsc --noEmit --pretty false 2>&1 | Select-String -Pattern "blockCommunityHelpers|auth.controller" | Select-Object -First 20`  
Expected: no errors referencing these files (project may have unrelated tsc noise)

- [ ] **Step 3: Commit**

```bash
git add src/controllers/auth.controller.ts
git commit -m "feat: remove blocked user from creator communities on block"
```

---

### Task 3: Join / accept gates

**Files:**
- Modify: `src/controllers/community.controller.ts` (`toggleJoinCommunity`, `acceptCommunityRequest`)

**Interfaces:**
- Consumes: `isBlockedBetween`, `COMMUNITY_CONSTANTS.UNABLE_TO_JOIN_BLOCKED`

- [ ] **Step 1: Gate `toggleJoinCommunity`**

After loading `community`, before leave/join/request logic, resolve creator id and if joining (not leaving):

```typescript
import { isBlockedBetween, getBlockedProfileIds } from '../utils/Block/blockCommunityHelpers';
import { COMMUNITY_CONSTANTS } from '../constants/messages';

const creatorId =
  typeof community.createdBy === 'object' && community.createdBy !== null && '_id' in (community.createdBy as any)
    ? (community.createdBy as any)._id.toString()
    : community.createdBy?.toString();

const isMember = (community.members || []).some((m: any) => m.toString() === profileId.toString());
if (!isMember && creatorId && await isBlockedBetween(profileId, creatorId)) {
  return ResponseUtil.errorResponse(
    res,
    STATUS_CODES.FORBIDDEN,
    COMMUNITY_CONSTANTS.UNABLE_TO_JOIN_BLOCKED
  );
}
```

Note: allow leaving even if blocked (`isMember === true` path continues).

- [ ] **Step 2: Gate `acceptCommunityRequest`**

After loading community and verifying creator, before `$push` members:

```typescript
const requesterId = request.profileId?.toString();
if (requesterId && await isBlockedBetween(profileId, requesterId)) {
  await communityRequestModel.findByIdAndDelete(id);
  return ResponseUtil.errorResponse(
    res,
    STATUS_CODES.FORBIDDEN,
    COMMUNITY_CONSTANTS.UNABLE_TO_JOIN_BLOCKED
  );
}
```

- [ ] **Step 3: Commit**

```bash
git add src/controllers/community.controller.ts
git commit -m "feat: block join and accept when users are blocked"
```

---

### Task 4: Visibility filters for members and posts

**Files:**
- Modify: `src/controllers/community.controller.ts` (`getCommunityMembers`, `getCommunityPosts`)

**Interfaces:**
- Consumes: `getBlockedProfileIds(viewerId)`

- [ ] **Step 1: Filter `getCommunityMembers`**

After fetching members / before success response:

```typescript
const blockedIds = await getBlockedProfileIds(profileId);
const blockedSet = new Set(blockedIds.map(String));
const visibleMembers = activeMembers.filter(
  (member: any) => !blockedSet.has(member._id.toString())
);
return ResponseUtil.successResponse(res, STATUS_CODES.SUCCESS, visibleMembers, 'Community members retrieved successfully');
```

- [ ] **Step 2: Filter `getCommunityPosts`**

1. Before pagination query, load blocked IDs once and add to query:

```typescript
const blockedUserIds = await getBlockedProfileIds(profileId);
if (blockedUserIds.length > 0) {
  query.user = { $nin: blockedUserIds };
}
```

2. Replace the per-post `BlockUserModel.find({ blockedBy: profileId })` with the already-loaded `blockedUserIds` for reply count adjustment (both-directions list is fine / stricter).

- [ ] **Step 3: Commit**

```bash
git add src/controllers/community.controller.ts
git commit -m "feat: hide blocked users from community members and posts"
```

---

### Task 5: Verification

- [ ] **Step 1: Run helper tests**

Run: `npx jest src/tests/utils/blockCommunityHelpers.test.ts -v`  
Expected: all PASS

- [ ] **Step 2: Manual checklist (for QA)**

1. A blocks B who is in A’s community → B removed from members, pending request gone.  
2. B cannot join / request A’s community while blocked.  
3. A unblocks B → B still not a member.  
4. A and B both in C’s community → A’s member list hides B; B’s membership unchanged.  
5. B’s posts in A’s community hidden from A, still in DB.

---

## Spec coverage

| Spec requirement | Task |
|------------------|------|
| Remove from creator communities on block | 1, 2 |
| Cancel pending requests | 1, 2 |
| Prevent join/request while blocked | 3 |
| Accept rejects if blocked | 3 |
| Unblock does not restore | 2 (no restore code) |
| Hide posts for viewer | 4 |
| Hide in shared community member lists | 4 |
| No new schema | Global / all tasks |
