# BulkUpdateNotificationPreferences

## Permission Scope

preferenceSelf

## Overview

bulkUpdateNotificationPreferences atomically upserts a full `(category, channel, allowed)` matrix for the calling user, supporting one-shot preference editing UIs that submit the entire grid in a single save. The operation runs in one transaction: any single entry that would violate the transactional invariant (`CATEGORY_NOT_OPT_OUT_ABLE`) rolls back **every** entry, so callers never observe a partially applied matrix. The caller must equal the target user, mirroring the single-row command.

## Business Rules

- `userId` is required; the caller's userId must equal `userId`
- `entries` is a non-empty array of `{ categoryId, channelId, allowed }`
- Each entry is validated under the same rules as `updateNotificationPreference`
- Atomicity: any single entry violating `CATEGORY_NOT_OPT_OUT_ABLE` rolls back the entire batch — zero rows are persisted
- Successful execution upserts every supplied row in a single transaction
- Categories / channels referenced in entries must exist
- Submitting the same matrix twice produces the same end state (idempotent)

## Process Flow

```mermaid
flowchart TD
    A[Receive bulk update request] --> B{Caller.userId == target userId?}
    B -->|No| C[Return FORBIDDEN]
    B -->|Yes| D{entries non-empty?}
    D -->|No| E[Return MISSING_REQUIRED_FIELD]
    D -->|Yes| F[Open transaction]
    F --> G[For each entry: resolve category and channel]
    G --> H{Any referenced category or channel missing?}
    H -->|Yes| I[Roll back; return CATEGORY_NOT_FOUND or CHANNEL_NOT_FOUND]
    H -->|No| J{Any entry violates optOutAllowed for transactional category?}
    J -->|Yes| K[Roll back; return CATEGORY_NOT_OPT_OUT_ABLE]
    J -->|No| L[Upsert every NotificationPreference row]
    L --> M[Commit transaction, return resulting rows]
```

## External Dependencies

- User-management context - The caller's userId is read from the authenticated context to enforce self-service editing

## Error Scenarios

- **FORBIDDEN**: Caller is not authorized to perform this operation in the target scope
- **CATEGORY_NOT_OPT_OUT_ABLE**: Target NotificationCategory has optOutAllowed = false and the requested change would persist an opt-out directive
- **CATEGORY_NOT_FOUND**: No NotificationCategory matches the supplied categoryId
- **CHANNEL_NOT_FOUND**: No NotificationChannel row matches the supplied channelId
- **MISSING_REQUIRED_FIELD**: One or more required input fields are missing or blank

## Test Cases

- atomically upserts every NotificationPreference row supplied in entries when all entries are valid
- rolls back the entire batch when one entry targets a transactional category with allowed=false
- rolls back the entire batch when one entry references a non-existent category
- rolls back the entire batch when one entry references a non-existent channel
- returns FORBIDDEN when the caller's userId does not equal the target userId
- returns MISSING_REQUIRED_FIELD when entries is empty
- supports mixing inserts and updates in the same call
- submitting the same matrix twice converges on the same end state
