# Issue #204: Duplicate Sub-Entity Sync Methods

## Problem

`MemberService` contains three nearly identical methods for syncing sub-entities:

- `syncAddresses()`
- `syncPhones()`
- `syncEmails()`

They all follow the same pattern:
1. Fetch existing entities
2. Index by ID
3. Update kept IDs, create new ones
4. Delete removed IDs

This violates DRY (Don't Repeat Yourself) and makes maintenance harder.

## Current Code

```php
private function syncAddresses(int $memberId, array $incoming): void {
    $existing = $this->addressMapper->findByMemberId($memberId);
    $existingById = [];
    foreach ($existing as $addr) {
        $existingById[$addr->getId()] = $addr;
    }
    $keptIds = [];
    foreach ($incoming as $item) {
        $id = isset($item['id']) ? (int)$item['id'] : 0;
        if ($id > 0 && isset($existingById[$id])) {
            $this->updateAddress($id, $item);
            $keptIds[$id] = true;
        } else {
            $this->addAddress($memberId, $item);
        }
    }
    foreach ($existingById as $id => $addr) {
        if (!isset($keptIds[$id])) {
            $this->deleteAddress($id);
        }
    }
}
// syncPhones() and syncEmails() are identical except for Address → Phone/Email
```

## Solution

Extract the common sync logic into a single private generic method that accepts callables for the mapper-specific operations. Each of the three sync methods becomes a thin 10-line wrapper calling the generic method.

## Tasks

- [ ] Extract common sync logic into `syncSubEntities()` private generic method
- [ ] Refactor `syncAddresses()`, `syncPhones()`, `syncEmails()` to use the new pattern
- [ ] Ensure identical behavior is preserved (all existing tests must pass)
- [ ] Run `make test` — all must pass
- [ ] Commit, push, create PR via tea, merge it
- [ ] Move plan file from `.plans/open/` to `.plans/done/`
- [ ] Check out main again

## Labels

- refactoring
- backend
- priority:medium
