# Solution Design Document

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] All context sources are listed with relevance ratings
- [x] Project commands are discovered from actual project files
- [x] Constraints → Strategy → Design → Implementation path is logical
- [x] Architecture pattern is clearly stated with rationale
- [x] Every component in diagram has directory mapping
- [x] Every interface has specification
- [x] Error handling covers all error types
- [x] Quality requirements are specific and measurable
- [x] Every quality requirement has test coverage
- [ ] **All architecture decisions confirmed by user** (ADRs pending user confirmation)
- [x] Component names consistent across diagrams
- [x] A developer could implement from this design

---

## Constraints

CON-1 **Framework**: Flutter 3.38.3 / Dart 3.10.1, Riverpod 3.x (AsyncNotifier + Notifier patterns), Freezed 3.x (abstract class required), GoRouter 17.x
CON-2 **Shared Package**: The `buyerkiosk_chat` package (from `../buyerkiosk-team/packages/buyerkiosk_chat/`) provides all models, entities, mappers, services, repository interface and implementation, constants, and exceptions. The Live app must consume this package — NOT duplicate its code.
CON-3 **Auth**: The Live app uses unified JWT authentication (Spec 003). Chat API uses the same JWT Bearer token — no separate auth needed. The shared `dioProvider` handles all token injection and refresh automatically.
CON-4 **Real-time**: Ably Flutter (`^1.2.42` in Live, `^1.2.35` in package) for WebSocket messaging. Chat-specific Ably tokens obtained via `/ably-token` endpoint. The package's `AblyRealtimeService` manages all real-time subscriptions.
CON-5 **API Base URL**: Staff Chat API at `{baseUrl}/mobile/staff-chat/{typeNum}/...`. The `ChatRepositoryImpl` needs a Dio instance with `basePath` pointing to the staff-chat prefix.
CON-6 **Content Type**: The Live app's default Dio content type is `application/x-www-form-urlencoded`, but the Chat API expects `application/json`. The chat Dio instance must override this.
CON-7 **Store-Scoped**: All chat operations require `typeNum`. Chat channels change when the user switches stores.

## Implementation Context

### Required Context Sources

- ICO-1 Shared Chat Package
  ```yaml
  - file: ../buyerkiosk-team/packages/buyerkiosk_chat/lib/buyerkiosk_chat.dart
    relevance: CRITICAL
    why: "Public API surface of the shared package — all models, entities, services, repository"

  - file: ../buyerkiosk-team/packages/buyerkiosk_chat/pubspec.yaml
    relevance: HIGH
    why: "Dependency versions that must be compatible with Live app"
  ```

- ICO-2 Live App Infrastructure
  ```yaml
  - file: lib/presentation/providers/providers.dart
    relevance: CRITICAL
    why: "Provider wiring pattern — dioProvider, repository providers, secure storage"

  - file: lib/core/network/api_interceptors.dart
    relevance: HIGH
    why: "UnifiedAuthInterceptor handles JWT for all API calls"

  - file: lib/core/constants/api_constants.dart
    relevance: HIGH
    why: "API base URL configuration"

  - file: lib/router/app_router.dart
    relevance: HIGH
    why: "Existing /chat route, navigation shell, auth redirect guards"

  - file: lib/presentation/screens/chat/chat_tab.dart
    relevance: MEDIUM
    why: "Current placeholder to be replaced"
  ```

- ICO-3 Team App Reference Implementation
  ```yaml
  - file: ../buyerkiosk-team/lib/presentation/providers/chat_notifier.dart
    relevance: HIGH
    why: "Reference ChatNotifier implementation (1,850 lines) to replicate"

  - file: ../buyerkiosk-team/lib/presentation/providers/chat_providers.dart
    relevance: HIGH
    why: "Derived provider pattern with .select() for efficient rebuilds"

  - file: ../buyerkiosk-team/lib/presentation/screens/chat/
    relevance: MEDIUM
    why: "Screen implementations to adapt for Live app navigation"
  ```

- ICO-4 API Specification
  ```yaml
  - file: docs/api/staff-chat-mobile-openapi.yaml
    relevance: CRITICAL
    why: "Complete API contract for all 19 chat endpoints"
  ```

### Implementation Boundaries

- **Must Preserve**: Existing unified JWT auth flow, existing bottom navigation shell with Chat as Tab 4, existing permission system, all other app functionality unchanged
- **Can Modify**: `lib/presentation/screens/chat/chat_tab.dart` (replace placeholder), `lib/presentation/providers/providers.dart` (add chat exports), `lib/router/app_router.dart` (add sub-routes), `pubspec.yaml` (add shared package dependency), `lib/core/constants/api_constants.dart` (add chat endpoints)
- **Must Not Touch**: `buyerkiosk_chat` shared package (read-only dependency), backend API endpoints, existing auth interceptors, existing Ably push notification setup

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    User[Store Team Member] --> LiveApp[BuyerKiosk Live App]

    LiveApp --> ChatPkg[buyerkiosk_chat Package]
    ChatPkg --> ChatAPI[Staff Chat REST API]
    ChatPkg --> AblyRT[Ably Real-time Service]

    LiveApp --> MainAPI[Main Mobile API]
    LiveApp --> AblyPush[Ably Push Notifications]

    ChatAPI --> DB[(Chat Database)]
    AblyRT --> AblyCloud[Ably Cloud]

    subgraph "Shared Package"
        ChatPkg
    end

    subgraph "Backend Services"
        ChatAPI
        MainAPI
        DB
    end

    subgraph "Real-time Infrastructure"
        AblyRT
        AblyCloud
        AblyPush
    end
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "User Chat Interaction"
    type: Flutter UI
    format: Riverpod state management
    authentication: JWT (via unified auth)
    data_flow: "User actions → ChatNotifier → API/Ably"

# Outbound Interfaces
outbound:
  - name: "Staff Chat REST API"
    type: HTTPS
    format: REST (JSON)
    authentication: JWT Bearer token
    doc: docs/api/staff-chat-mobile-openapi.yaml
    data_flow: "CRUD for channels, messages, reactions, mentions, members"
    criticality: HIGH
    base_url: "{baseUrl}/mobile/staff-chat/{typeNum}"

  - name: "Ably Real-time"
    type: WebSocket
    format: JSON events
    authentication: Ably token (from /ably-token endpoint)
    data_flow: "Real-time message delivery, typing indicators, presence"
    criticality: HIGH
    channels: "chat:{typeNum}:{channelId}, chat:{typeNum}:{channelId}:presence"

# Data Interfaces
data:
  - name: "SharedPreferences"
    type: Local Key-Value Store
    connection: shared_preferences plugin
    data_flow: "Channel pin state per store"

  - name: "Secure Storage"
    type: Encrypted Local Storage
    connection: flutter_secure_storage plugin
    data_flow: "JWT tokens (already stored by auth system)"
```

### Project Commands

```bash
# Component: BuyerKiosk Live Flutter App
Location: /Users/rvanvuren/Projects/buyerkiosk-live-flutter

## Environment Setup
Install Dependencies: flutter pub get
Environment Variables: lib/core/constants/app_config.dart (baseUrl, bypassSSL, enableApiLogging)

## Code Generation (after adding/modifying Freezed models)
Build Generated Code: dart run build_runner build --delete-conflicting-outputs

## Testing Commands
Unit/Widget Tests: flutter test
Specific Test File: flutter test test/path/to/test_file.dart
Dart MCP Tests: Use mcp__dart-mcp__run_tests tool

## Code Quality Commands
Analyze: flutter analyze
Format: dart format .
Dart MCP Analyze: Use mcp__dart-mcp__analyze_files tool
Dart MCP Format: Use mcp__dart-mcp__dart_format tool

## Build
Run App: flutter run
Run on Device: flutter run -d iphone
Build APK: flutter build apk --debug
Build iOS: flutter build ios --debug --no-codesign

## Local Package Linking
Add shared package: Add path dependency in pubspec.yaml
  buyerkiosk_chat:
    path: ../buyerkiosk-team/packages/buyerkiosk_chat
```

## Solution Strategy

- **Architecture Pattern**: Clean Architecture with Riverpod, consuming the shared `buyerkiosk_chat` package as a local path dependency. The package provides the data + domain layers (models, entities, repository, services). The Live app builds only the presentation layer (providers, screens, widgets) adapted to its navigation and auth systems.

- **Integration Approach**: Wire the shared package's `ChatRepositoryImpl` into the Live app's provider system using a dedicated Dio instance with JSON content type (different from the main form-encoded Dio). Create a `ChatNotifier` (Notifier, not AsyncNotifier) following the Team app's composite state pattern. Build screens/widgets adapted to Live app's navigation scaffold.

- **Justification**: The shared package already contains 100% of the data layer, tested and production-validated. Rebuilding it would be wasteful, error-prone, and create maintenance burden (two copies to keep in sync). The presentation layer must differ because the Live and Team apps have different navigation structures, auth systems, and provider wiring.

- **Key Decisions**:
  1. **Path dependency** over copy — single source of truth for models/services
  2. **Dedicated chat Dio** instance with JSON content type — avoids breaking existing form-encoded API calls
  3. **Single ChatNotifier with composite state** — matches Team app pattern for coordinated state management
  4. **Derived providers with `.select()`** — efficient widget rebuilds

## Building Block View

### Components

```mermaid
graph LR
    subgraph "Live App - Presentation Layer (NEW)"
        ChatScreen[ChatScreen]
        ChannelScreen[ChannelScreen]
        Widgets[Chat Widgets]
        ChatNotifier[ChatNotifier]
        DerivedProviders[Derived Providers]
    end

    subgraph "Live App - Infrastructure (EXISTING)"
        DioProvider[dioProvider]
        AuthInterceptor[UnifiedAuthInterceptor]
        Router[GoRouter]
        Permissions[Permission System]
        SecureStorage[Secure Storage]
        SharedPrefs[SharedPreferences]
    end

    subgraph "buyerkiosk_chat Package (EXISTING - READ ONLY)"
        ChatRepo[ChatRepository / ChatRepositoryImpl]
        AblyService[AblyRealtimeService]
        AblyClientImpl[AblyClientImpl]
        Models[Freezed Models]
        Entities[Equatable Entities]
        Mappers[Chat Mappers]
        Exceptions[Chat Exceptions]
        Constants[Chat Constants]
    end

    ChatScreen --> ChatNotifier
    ChannelScreen --> ChatNotifier
    Widgets --> DerivedProviders
    DerivedProviders --> ChatNotifier
    ChatNotifier --> ChatRepo
    ChatNotifier --> AblyService
    ChatNotifier --> ChatPrefs[ChatPreferencesService]
    ChatNotifier --> ChatSound[ChatSoundService]

    ChatRepo --> DioProvider
    DioProvider --> AuthInterceptor
    AblyService --> AblyClientImpl

    Router --> ChatScreen
    Router --> ChannelScreen
    Permissions --> Router

    ChatPrefs --> SharedPrefs
```

### Directory Map

**Component**: Live App - New Chat Files
```
lib/
├── core/
│   ├── constants/
│   │   └── chat_constants.dart                    # NEW: Chat API endpoint paths, storage keys
│   └── services/
│       ├── chat_preferences_service.dart          # NEW: Pin state (SharedPreferences)
│       └── chat_sound_service.dart                # NEW: Audio feedback (singleton)
│
├── presentation/
│   ├── providers/
│   │   └── chat/
│   │       ├── chat_notifier.dart                 # NEW: Main ChatNotifier (composite state)
│   │       ├── chat_providers.dart                # NEW: Derived providers + wiring
│   │       └── chat_composite_state.dart          # NEW: ChatCompositeState class
│   │
│   ├── screens/
│   │   └── chat/
│   │       ├── chat_screen.dart                   # REPLACE: Channels | Mentions tabs
│   │       ├── channel_screen.dart                # NEW: Message list + composer
│   │       ├── channel_search_screen.dart         # NEW: Search within channel
│   │       ├── channel_settings_screen.dart       # NEW: Channel settings (manager+)
│   │       └── create_channel_screen.dart         # NEW: Create channel form
│   │
│   └── widgets/
│       └── chat/
│           ├── channel_list_widget.dart            # NEW: Channel list with sections
│           ├── channel_tile.dart                   # NEW: Single channel row
│           ├── channel_context_menu.dart           # NEW: Long-press menu (pin/mute)
│           ├── message_list_widget.dart            # NEW: Scrollable message list
│           ├── message_bubble.dart                 # NEW: Individual message with swipe
│           ├── message_composer.dart               # NEW: Text input + mentions + emoji
│           ├── message_group.dart                  # NEW: Group messages by sender/time
│           ├── message_actions_menu.dart           # NEW: Edit/delete/copy/react
│           ├── typing_indicator.dart               # NEW: "X is typing..." display
│           ├── mentions_list_widget.dart           # NEW: Mentions tab content
│           ├── mention_tile.dart                   # NEW: Single mention row
│           ├── member_list_widget.dart             # NEW: Channel member list
│           ├── reaction_picker.dart                # NEW: Quick react + emoji keyboard
│           ├── reaction_chips.dart                 # NEW: Reaction display below message
│           ├── emoji_keyboard.dart                 # NEW: Full emoji picker
│           ├── read_receipts.dart                  # NEW: Read status indicators
│           ├── attachment_display.dart             # NEW: Inline image/document display
│           ├── connection_banner.dart              # NEW: Degraded/disconnected banner
│           ├── delete_confirmation_dialog.dart     # NEW: Delete message confirmation
│           ├── empty_state.dart                    # NEW: Empty channel/mentions state
│           ├── system_message.dart                 # NEW: System message display
│           └── image_viewer_screen.dart            # NEW: Full-screen image viewer
│
└── router/
    └── app_router.dart                            # MODIFY: Add chat sub-routes
```

**Files to Modify (Existing)**:
```
lib/
├── core/constants/
│   └── api_constants.dart                         # MODIFY: Add chat API base path
├── presentation/providers/
│   └── providers.dart                             # MODIFY: Add chat exports + chatDioProvider
├── router/
│   └── app_router.dart                            # MODIFY: Add /chat sub-routes
└── pubspec.yaml                                   # MODIFY: Add buyerkiosk_chat path dependency

assets/sounds/                                     # NEW: Chat sound files
├── slick-notification.mp3
└── confident-543.mp3
```

### Interface Specifications

#### Interface Documentation References

```yaml
interfaces:
  - name: "Staff Chat REST API"
    doc: docs/api/staff-chat-mobile-openapi.yaml
    relevance: CRITICAL
    sections: [channels, messages, reactions, mentions, members, attachments, ably-token]
    why: "Complete contract for all 19 API endpoints consumed by ChatRepositoryImpl"

  - name: "Ably Real-time Events"
    doc: (defined in buyerkiosk_chat/src/constants/chat_constants.dart)
    relevance: HIGH
    sections: [ChatAblyEvents, ChatAblyChannels]
    why: "Real-time event types and channel naming for WebSocket subscriptions"

  - name: "Chat Exception Hierarchy"
    doc: (defined in buyerkiosk_chat/src/exceptions/chat_exception.dart)
    relevance: HIGH
    why: "Typed error handling for all chat-specific error conditions"
```

#### Data Storage Changes

```yaml
# No database changes required — chat data is server-side only

# Local Storage (SharedPreferences)
Key: "pinned_channels_{typeNum}"
  Type: JSON list of int (channel IDs)
  Purpose: Per-store channel pin ordering
  Managed by: ChatPreferencesService

# No new Secure Storage keys needed — JWT tokens already stored by auth system
```

#### Application Data Models

All data models are provided by the `buyerkiosk_chat` package. No new models are created in the Live app.

```pseudocode
# FROM SHARED PACKAGE (read-only, not duplicated):
ENTITY: Channel (id, name, description, accessLevel, isDefault, unreadCount, ...)
ENTITY: Message (id, channelId, content, sender, senderType, createdAt, reactions, ...)
ENTITY: Reaction (emoji, count, users, hasReacted)
ENTITY: Member (userId, name, avatarUrl, isOnline, membershipType)
ENTITY: MentionItem (messageId, channelId, channelName, content, senderName, createdAt)
ENTITY: Attachment (id, fileName, mimeType, fileSize, thumbnailUrl, downloadUrl)

# NEW IN LIVE APP (presentation layer only):
CLASS: ChatCompositeState
  FIELDS:
    channelsState: ChannelsState (from package sealed classes)
    activeChannelId: int?
    activeChannelMembers: List<Member>
    messagesState: ChannelMessagesState (from package)
    mentionsState: MentionsState (from package)
    connectionState: ChatConnectionState (from package)
    editingMessageId: int?
    lastError: String?
    isLoadingMore: bool

  BEHAVIORS:
    copyWith({...}): ChatCompositeState
```

#### Integration Points

```yaml
# Live App → Shared Package
- from: Live App Providers
  to: buyerkiosk_chat package
  protocol: Dart method calls (in-process)
  data_flow: "ChatNotifier calls ChatRepository methods and AblyRealtimeService"

# Shared Package → Staff Chat API
- from: ChatRepositoryImpl
  to: Staff Chat REST API
  protocol: HTTPS (Dio)
  doc: docs/api/staff-chat-mobile-openapi.yaml
  data_flow: "JSON request/response for all CRUD operations"

# Shared Package → Ably Cloud
- from: AblyRealtimeService → AblyClientImpl
  to: Ably Cloud (WebSocket)
  protocol: WebSocket (ably_flutter SDK)
  data_flow: "Real-time chat events (message.created, message.updated, etc.)"

# Live App Auth → Shared Package
- from: dioProvider (with UnifiedAuthInterceptor)
  to: ChatRepositoryImpl (via Dio instance)
  protocol: HTTP header injection
  data_flow: "Authorization: Bearer <jwt_token> on all requests"
```

### Implementation Examples

#### Example: Chat Dio Provider (Critical — Content Type Override)

**Why this example**: The Live app's default Dio uses `application/x-www-form-urlencoded` but the Chat API requires `application/json`. This is the most critical integration detail.

```dart
/// Dedicated Dio instance for Staff Chat API.
/// Uses JSON content type (different from main app's form-encoded).
/// Shares the same auth interceptor for JWT token handling.
final chatDioProvider = Provider<Dio>((ref) {
  final mainDio = ref.watch(dioProvider);

  // Create a new Dio with JSON content type but same base config
  final chatDio = Dio(
    BaseOptions(
      baseUrl: '${ApiConstants.baseUrl}mobile/staff-chat',
      connectTimeout: ApiConstants.timeout,
      receiveTimeout: ApiConstants.timeout,
      headers: {
        'Content-Type': 'application/json',  // Override!
        'Accept': 'application/json',
      },
    ),
  );

  // Copy interceptors from main Dio (auth + logging)
  chatDio.interceptors.addAll(mainDio.interceptors);

  // Copy SSL bypass if configured
  if (AppConfig.bypassSSL) {
    (chatDio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
      final client = HttpClient();
      client.badCertificateCallback = (cert, host, port) => true;
      return client;
    };
  }

  return chatDio;
});

/// Chat repository wired to chat-specific Dio
final chatRepositoryProvider = Provider<ChatRepository>((ref) {
  return ChatRepositoryImpl(
    dio: ref.watch(chatDioProvider),
    basePath: '',  // Already set in baseUrl
  );
});
```

#### Example: ChatNotifier Initialization Pattern

**Why this example**: Shows how the ChatNotifier integrates with Live app's existing auth and store state.

```dart
class ChatNotifier extends Notifier<ChatCompositeState> {
  late final ChatRepository _repository;
  late final AblyRealtimeService _ablyService;
  late final ChatPreferencesService _preferences;

  @override
  ChatCompositeState build() {
    _repository = ref.read(chatRepositoryProvider);
    _ablyService = ref.read(ablyRealtimeServiceProvider);
    _preferences = ref.read(chatPreferencesServiceProvider);

    // Listen to Ably connection state changes
    _ablyService.connectionState.listen(_handleConnectionChange);

    return ChatCompositeState.initial();
  }

  /// Load channels for the given store.
  /// Called when chat tab is opened or store changes.
  Future<void> loadChannels(String typeNum) async {
    state = state.copyWith(
      channelsState: const ChannelsLoading(),
    );

    try {
      final channels = await _repository.getChannels(typeNum);

      // Apply local pin state
      final pinnedIds = _preferences.getPinnedChannelIds(typeNum);
      final withPins = channels.map((c) {
        final pinIndex = pinnedIds.indexOf(c.id);
        return c.copyWith(
          isPinned: pinIndex >= 0,
          pinOrder: pinIndex >= 0 ? pinIndex : null,
        );
      }).toList();

      state = state.copyWith(
        channelsState: ChannelsLoaded(channels: withPins),
      );

      // Connect to Ably for real-time updates
      await _ablyService.connect(typeNum);
    } on ChatException catch (e) {
      state = state.copyWith(
        channelsState: ChannelsError(
          message: e.message,
          canRetry: e is! ChatAccessDeniedException,
        ),
      );
    }
  }
}
```

#### Example: Store-Scoped TypeNum Resolution

**Why this example**: The Live app navigates by typeNum in routes. Chat needs to resolve typeNum from the current route or selected store.

```dart
/// Provider that resolves the current store's typeNum for chat.
/// In the Live app, typeNum comes from the selected store or route parameter.
final chatTypeNumProvider = Provider<String?>((ref) {
  // Watch the selected store provider (already exists in Live app)
  final selectedStore = ref.watch(selectedStoreProvider);
  return selectedStore?.typeNum;
});

/// Convenience provider: is chat available (requires a selected store)
final isChatAvailableProvider = Provider<bool>((ref) {
  return ref.watch(chatTypeNumProvider) != null;
});
```

## Runtime View

### Primary Flow: Opening Chat and Sending a Message

1. User taps Chat tab in bottom navigation
2. ChatScreen loads, reads `chatTypeNumProvider` for current store
3. ChatNotifier.loadChannels(typeNum) fires → fetches channels from API
4. Channels displayed with unread badges; Ably connection established
5. User taps a channel → navigates to ChannelScreen
6. ChatNotifier.enterChannel(channelId) → loads messages, subscribes to real-time
7. User types message in composer, presses send
8. ChatNotifier.sendMessage() → optimistic update (message appears immediately)
9. API confirms → optimistic message replaced with server message
10. Other users see the message in real-time via Ably event

```mermaid
sequenceDiagram
    actor User
    participant ChatScreen
    participant ChatNotifier
    participant ChatRepo as ChatRepositoryImpl
    participant AblyService as AblyRealtimeService
    participant API as Staff Chat API
    participant Ably as Ably Cloud

    User->>ChatScreen: Tap Chat tab
    ChatScreen->>ChatNotifier: loadChannels(typeNum)
    ChatNotifier->>ChatRepo: getChannels(typeNum)
    ChatRepo->>API: GET /{typeNum}/channels
    API-->>ChatRepo: List<ChannelSummaryModel>
    ChatRepo-->>ChatNotifier: List<Channel>
    ChatNotifier->>AblyService: connect(typeNum)
    AblyService->>API: POST /{typeNum}/ably-token
    API-->>AblyService: Ably token
    AblyService->>Ably: WebSocket connect
    ChatNotifier-->>ChatScreen: ChannelsLoaded state

    User->>ChatScreen: Tap channel
    ChatScreen->>ChatNotifier: enterChannel(channelId)
    ChatNotifier->>ChatRepo: getMessages(typeNum, channelId)
    ChatRepo->>API: GET /{typeNum}/channels/{channelId}/messages
    API-->>ChatNotifier: Messages
    ChatNotifier->>AblyService: subscribeToChannel(channelId)
    ChatNotifier-->>ChatScreen: ChannelMessagesLoaded

    User->>ChatScreen: Type & send message
    ChatScreen->>ChatNotifier: sendMessage(content)
    Note over ChatNotifier: Optimistic update (isPending: true)
    ChatNotifier->>ChatRepo: sendMessage(typeNum, channelId, content)
    ChatRepo->>API: POST /{typeNum}/channels/{channelId}/messages
    API-->>ChatNotifier: Confirmed message
    Note over ChatNotifier: Replace optimistic with confirmed

    Ably-->>AblyService: message.created event (for other users)
    AblyService-->>ChatNotifier: ChatMessageCreatedEvent
    Note over ChatNotifier: Add to message list
```

### Error Handling

- **Network failure during send**: Message marked as `sendFailed: true` with retry button. User taps retry → resends with same `clientMessageId` for dedup.
- **401 Unauthorized**: Handled by `UnifiedAuthInterceptor` — automatic token refresh + retry. If refresh fails → force logout to login screen.
- **403 Access Denied**: `ChatAccessDeniedException` → "You no longer have access to this channel" → navigate back to channel list.
- **404 Channel/Message Not Found**: Remove from UI, show toast notification.
- **429 Rate Limited**: `ChatRateLimitException` → "Slow down! Please wait a moment before sending." with `Retry-After` countdown.
- **422 Edit Window Expired**: `ChatEditWindowExpiredException` → "This message can no longer be edited (24 hour limit)."
- **Ably Disconnection**: Banner shown "Connection degraded" → automatic polling fallback (30s). Reconnection triggers resync after 5+ minute gap.

### Complex Logic: Real-time Event Buffering

```
ALGORITHM: Handle Real-time Events with Buffering
INPUT: ChatEvent from Ably subscription
OUTPUT: Updated composite state

1. IF messagesState is ChannelMessagesLoading:
   a. BUFFER: Add event to _eventBuffer list
   b. RETURN (don't process until load completes)

2. IF messagesState is ChannelMessagesLoaded:
   a. MATCH event type:
      - ChatMessageCreatedEvent:
        i. CHECK deduplication (by id AND clientMessageId)
        ii. ENRICH sender name (from members cache → existing messages → "User #id")
        iii. ADD to messages list
        iv. PLAY sound if from another user
      - ChatMessageUpdatedEvent:
        i. FIND message by id in list
        ii. UPDATE content and editedAt
      - ChatMessageDeletedEvent:
        i. FIND message by id
        ii. SET isDeleted: true, content: "[deleted]"
      - ChatReactionAddedEvent:
        i. FIND message, CHECK idempotency
        ii. ADD reaction or increment count
      - ChatReactionRemovedEvent:
        i. FIND message, decrement count or remove
      - ChatReadUpdatedEvent:
        i. UPDATE channel unread count to 0
      - ChatTypingEvent:
        i. UPDATE typingUsers in current state
      - ChatMemberRemovedEvent:
        i. IF current user → show error, navigate back
        ii. IF other user → remove from members list

3. AFTER message load completes (transition to Loaded):
   a. REPLAY all buffered events in order
   b. CLEAR buffer
```

## Deployment View

### Single Application Deployment

- **Environment**: iOS and Android mobile apps (client-side only)
- **Configuration**: No new environment variables. Chat API base path added to `ApiConstants`. Ably token obtained dynamically per session.
- **Dependencies**:
  - Staff Chat API (already running for Team app — no backend deployment needed)
  - Ably Cloud (already provisioned — may need capacity check)
  - `buyerkiosk_chat` shared package (local path reference)
- **Performance**:
  - Initial channel load: < 2s target
  - Message send to display: < 500ms (optimistic)
  - Real-time event to display: < 2s via WebSocket
  - Polling fallback interval: 30s

### Feature Flag Consideration

No feature flag needed — the Chat tab already exists as a placeholder. Replacing the placeholder with real functionality is the entire scope. If a gradual rollout is desired, backend can gate the `/channels` endpoint by store.

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Clean Architecture (domain/data/presentation layers)
  relevance: CRITICAL
  why: "All new code follows existing layer separation"

- pattern: Riverpod Notifier + Composite State
  relevance: CRITICAL
  why: "ChatNotifier uses single-notifier composite state (proven in Team app)"

- pattern: Derived providers with .select()
  relevance: HIGH
  why: "Performance optimization — widgets only rebuild when relevant state changes"

- pattern: Optimistic updates with clientMessageId
  relevance: HIGH
  why: "Instant message feedback with deduplication on server confirmation"

# New patterns introduced
- pattern: Shared package integration via path dependency
  relevance: HIGH
  why: "First cross-app shared package in the BuyerKiosk Flutter ecosystem"

- pattern: Dedicated Dio instance per API module
  relevance: MEDIUM
  why: "Chat API requires JSON content type vs main app's form-encoded"
```

### System-Wide Patterns

- **Security**: JWT authentication via existing `UnifiedAuthInterceptor`. Ably tokens scoped per user capabilities. No credentials stored beyond existing JWT flow.
- **Error Handling**: Chat-specific exceptions (`ChatException` hierarchy) caught in `ChatNotifier` and mapped to user-friendly messages. Network errors show retry options. Auth errors trigger force logout.
- **Performance**: In-memory message cache (100 messages/channel). Derived providers with `.select()` minimize rebuilds. Ably WebSocket for real-time (30s polling fallback). Debounced operations (typing 300ms, read receipts 500ms).
- **Logging/Auditing**: Debug-only logging via existing `LogInterceptor`. Chat events tracked via analytics providers (see Analytics Event Mapping section below).

### Implementation Patterns

#### Code Patterns and Conventions

- **File naming**: `snake_case.dart` matching existing Live app convention
- **Class naming**: `PascalCase` with descriptive suffixes (`ChatNotifier`, `ChannelScreen`, `MessageBubble`)
- **Provider naming**: `camelCaseProvider` with descriptive name (`chatProvider`, `sortedChannelsProvider`)
- **Import pattern**: Barrel files for each feature directory (`chat_providers.dart` re-exports all chat providers)
- **Widget pattern**: Extract sub-widgets as private classes (`_ChannelAppBarTitle`, `_MessageList`) for focused rebuilds

#### State Management Patterns

- **ChatNotifier** (extends `Notifier<ChatCompositeState>`): Synchronous notifier managing composite state. NOT `AsyncNotifier` because chat is always-connected, not request-response.
- **MessageSubmissionNotifier** (extends `Notifier<MessageSubmissionState>`): Separate notifier for message send lifecycle (idle → sending → success/error).
- **Derived providers**: Use `.select()` to watch only the specific part of composite state needed.
- **Store scoping**: `typeNum` passed through providers. When user switches stores, `ChatNotifier.loadChannels(newTypeNum)` resets all chat state.
- **Mentions scope**: Mentions are **store-scoped** — the `/mentions` API endpoint requires `typeNum` and returns only mentions for that store. The PRD's cross-store Mentions journey (Tertiary User Journey) is addressed by the Mentions tab refreshing when the user switches stores via the store switcher. True cross-store aggregation is a future enhancement requiring a new API endpoint (`/mentions/all`). **Decision**: Store-scoped mentions for this phase; PRD updated to clarify.

#### Performance Characteristics

- **Widget rebuild minimization**: Every derived provider uses `.select()` to prevent unnecessary rebuilds
- **Message list**: `ListView.builder` with item extent for consistent scroll performance
- **Image loading**: Lazy loading with `CachedNetworkImage` for thumbnails
- **Sound playback**: `AudioPlayer` singleton, pre-loaded assets
- **Typing debounce**: 300ms debounce on typing indicator API calls
- **Read receipt debounce**: 500ms debounce on mark-as-read calls
- **Channel refresh**: 5-minute staleness threshold before re-fetching

#### Integration Patterns

- **Package dependency**: Local path reference in `pubspec.yaml` → `buyerkiosk_chat: path: ../buyerkiosk-team/packages/buyerkiosk_chat`
- **Dio wiring**: `chatDioProvider` creates dedicated Dio with JSON content type, copies auth interceptors from `dioProvider`
- **Ably wiring**: `ablyRealtimeServiceProvider` creates `AblyRealtimeService` with `AblyClientImpl` and token provider from `chatRepositoryProvider`
- **Store context**: `chatTypeNumProvider` resolves from `selectedStoreProvider` (existing)

#### Channel List Sorting and Update Strategy

```pseudocode
ALGORITHM: Sort Channels for Display
INPUT: List<Channel> channels, List<int> pinnedIds (from ChatPreferencesService)
OUTPUT: Sorted list with sections

1. SEPARATE into two groups:
   a. pinnedChannels: channels where id is in pinnedIds
   b. unpinnedChannels: all others

2. SORT pinnedChannels:
   a. By position in pinnedIds list (user-defined order, maintained via drag-reorder)

3. SORT unpinnedChannels:
   a. By lastMessageAt descending (most recent activity first)
   b. Channels with no messages sorted by createdAt descending

4. RETURN: pinnedChannels + unpinnedChannels (pinned section always on top)

DISPLAY: Each channel tile shows:
  - Channel name (bold if unread > 0)
  - Last message preview (sender name + truncated content, max 80 chars)
  - Last message timestamp (relative: "2m", "1h", "Yesterday", date)
  - Unread count badge (right side, AppColors.primary background)
  - Muted indicator: dimmed text opacity (0.5) + muted icon, no badge
  - Pin indicator: small pin icon before channel name

REFRESH:
  - Pull-to-refresh: Calls ChatNotifier.loadChannels(typeNum) to re-fetch from API
  - Real-time updates: Ably events update channel summaries in-place:
    - message.created → update lastMessage preview, timestamp, increment unreadCount (if not active channel)
    - channel.updated → update channel name/description
    - read.updated → reset unreadCount to 0 for that channel
  - Re-sort after any update that changes lastMessageAt
```

#### Component Structure Pattern

```pseudocode
SCREEN: ChatScreen(typeNum)
  INITIALIZE:
    watch chatProvider for composite state
    call loadChannels on first build

  RENDER:
    IF channelsState is ChannelsLoading: loading indicator
    IF channelsState is ChannelsError: ErrorDisplay with retry
    IF channelsState is ChannelsLoaded:
      TabBarView:
        Tab 1 (Channels): ChannelListWidget
        Tab 2 (Mentions): MentionsListWidget
      FAB: CreateChannel (if manager+)

SCREEN: ChannelScreen(channelId, typeNum)
  INITIALIZE:
    call enterChannel(channelId) on mount
    call leaveChannel() on dispose

  RENDER:
    AppBar with channel name, member count, settings gear
    ConnectionBanner (if degraded)
    MessageListWidget (infinite scroll)
    TypingIndicator
    MessageComposer (text + mentions + emoji)
```

#### Error Handling Pattern

```pseudocode
FUNCTION: handle_chat_operation(operation)
  TRY:
    result = await operation()
    UPDATE state with success
  CATCH ChatAccessDeniedException:
    SHOW "Access denied" message
    NAVIGATE back to channels
  CATCH ChatRateLimitException:
    SHOW "Slow down" with retry-after countdown
  CATCH ChatEditWindowExpiredException:
    SHOW "Can no longer edit" message
    HIDE edit option for this message
  CATCH ChatNetworkException:
    IF can_retry: SHOW retry button
    ELSE: SHOW "Check your connection" message
  CATCH ChatException (generic):
    LOG error in debug mode
    SHOW "Something went wrong" with retry option
```

#### Read Receipts Design

```pseudocode
SCOPE: Per-channel read status (not per-message "seen by" lists)

API ENDPOINTS:
  - POST /{typeNum}/channels/{channelId}/read  → Marks channel as read (sets lastReadMessageId)
  - GET /{typeNum}/channels                     → Returns unreadCount per channel

UI BEHAVIOR:
  1. When user opens a channel (enterChannel):
     a. Start 500ms debounce timer
     b. After debounce: POST mark-as-read with latest message ID
     c. Update channelsState: set unreadCount = 0 for this channel
  2. While channel is open and new messages arrive:
     a. Reset debounce timer on each new message
     b. After 500ms idle: POST mark-as-read with latest message ID
  3. Chat tab badge:
     a. Sum of all channel unreadCounts from channelsState
     b. Updated reactively via derived provider: totalUnreadProvider
  4. Channel list badges:
     a. Each channel tile shows unreadCount badge (from ChannelSummary entity)
     b. Badge hidden when unreadCount == 0

MANAGER "WHO HAS READ" FEATURE:
  - **Deferred**: PRD Feature 6 acceptance criteria only specifies unread counts and badges.
    The "seen by" per-message read receipts (showing which specific users read a message)
    is NOT exposed by the current API. Per-message read receipt display is a future enhancement
    requiring a new API endpoint (e.g., GET /{channelId}/messages/{messageId}/readers).
  - Current scope: channel-level unread tracking only (matching API capabilities).
```

#### Feature Detail: Channel Management (Feature 9)

```pseudocode
CREATE CHANNEL (Manager+ only):
  UI: CreateChannelScreen
  FIELDS:
    - name: required, 1-50 chars
    - description: optional, max 200 chars
    - accessLevel: enum (public | manager | owner) — dropdown
    - retentionDays: optional, integer (0 = forever) — "Message retention" field
  API: POST /{typeNum}/channels
  VALIDATION: Name uniqueness checked server-side (409 Conflict)

CHANNEL SETTINGS (Manager+ only):
  UI: ChannelSettingsScreen
  SECTIONS:
    - Channel info (name, description, access level)
    - Members list (MemberListWidget)
    - Add/Remove members (for non-default channels)
    - Retention period display
  RULES:
    - Default channels (isDefault: true): name/delete disabled, settings limited
    - Only creator or owner can modify channel settings

MEMBER MANAGEMENT:
  - Add: POST /{typeNum}/channels/{channelId}/members with userId
  - Remove: DELETE /{typeNum}/channels/{channelId}/members/{userId}
  - Members list: GET /{typeNum}/channels/{channelId}/members
  - Default channels: all store employees auto-added, no manual management
```

#### Feature Detail: Message Search (Feature 10)

```pseudocode
SEARCH WITHIN CHANNEL:
  UI: ChannelSearchScreen (accessible from channel AppBar search icon)
  FLOW:
    1. User taps search icon in channel AppBar
    2. Search overlay/screen opens with text field (auto-focused)
    3. Minimum 2 characters before API call (enforced client-side)
    4. Debounced search (500ms) to avoid excessive API calls
    5. Results show: sender name, message preview, timestamp
    6. Tapping result: navigates back to ChannelScreen, scrolls to message
       - Use ScrollController.jumpTo() with message index calculation
       - If message is outside cached 100-message window, reload from that point

  API: GET /{typeNum}/channels/{channelId}/search?q={query}&page=1&limit=20
  PAGINATION: Standard offset/limit, load more on scroll
```

#### Feature Detail: Sound Feedback (Feature 12)

```pseudocode
SOUND PREFERENCES:
  STORAGE: SharedPreferences key "chat_sound_enabled" (default: true)
  MANAGED BY: ChatPreferencesService (same service as pin state)

TRIGGERS:
  - Message received from another user (while in active channel): slick-notification.mp3
  - Own message sent successfully: confident-543.mp3

TOGGLE:
  - Accessible from ChatScreen AppBar overflow menu: "Sound effects" switch
  - Persists immediately to SharedPreferences

IMPLEMENTATION:
  - ChatSoundService (singleton) pre-loads assets on init
  - play() checks preference before playback
  - AudioPlayer from audioplayers package (already in Team app deps)
```

#### Feature Detail: Attachment Display (Feature 13)

```pseudocode
ATTACHMENT TYPES:
  - Images (jpg, png, gif, webp): Inline thumbnail (max 200px width), tap for full-screen
  - Documents (pdf, doc, xlsx): File icon + filename + size, tap to download via URL launcher
  - Other: Generic file icon + filename

SIGNED URL HANDLING:
  - Attachment downloadUrl is a signed URL with 15-minute expiry
  - On tap (download/view): check URL age from attachment.signedAt timestamp
  - If expired (> 14 min): call GET /{typeNum}/channels/{channelId}/messages/{messageId}/attachments/{attachmentId}
    to get fresh signed URL, then proceed with download/view
  - Logic lives in ChatNotifier.refreshAttachmentUrl() method (not UI layer)

FULL-SCREEN IMAGE VIEWER:
  - ImageViewerScreen: Hero animation from thumbnail
  - Pinch-to-zoom, swipe to dismiss
  - Share button (share URL via platform share sheet)
  - Uses CachedNetworkImage for efficient loading
```

#### Feature Detail: Reaction Limits (Features 5)

```pseudocode
REACTION ENFORCEMENT:
  - Maximum 20 unique emoji per message:
    - Checked client-side before API call
    - If at limit: show toast "This message has the maximum number of reactions"
    - Server enforces independently (returns 422)

  - Rate limit: 20 reactions per minute per user:
    - Tracked in ChatNotifier._reactionTimestamps (List<DateTime>)
    - Before each reaction: check count in last 60 seconds
    - If exceeded: show "Slow down!" toast, don't call API
    - Server enforces independently (returns 429)
```

#### Analytics Event Mapping

```pseudocode
EVENT TRIGGER MAPPING (from PRD tracking requirements):

| Event                    | Trigger Location                        | Payload Fields                                             |
|--------------------------|-----------------------------------------|-----------------------------------------------------------|
| chat_tab_opened          | ChatScreen.initState                    | typeNum, channelCount (from state), totalUnread            |
| chat_channel_opened      | ChatNotifier.enterChannel()             | typeNum, channelId, unreadCount                            |
| chat_message_sent        | ChatNotifier.sendMessage() on success   | typeNum, channelId, hasAttachment, hasMention, msgLength   |
| chat_message_edited      | ChatNotifier.editMessage() on success   | typeNum, channelId, messageId                              |
| chat_message_deleted     | ChatNotifier.deleteMessage() on success | typeNum, channelId, messageId                              |
| chat_reaction_added      | ChatNotifier.addReaction() on success   | typeNum, channelId, emoji                                  |
| chat_mention_tapped      | MentionTile.onTap                       | typeNum, channelId, mentionedUserId                        |
| chat_channel_created     | ChatNotifier.createChannel() on success | typeNum, channelName, accessLevel                          |
| chat_search_performed    | ChannelSearchScreen on results loaded   | typeNum, channelId, queryLength, resultCount               |
| chat_connection_degraded | ChatNotifier._handleConnectionChange()  | typeNum, durationMs, fallbackMode ("polling"/"disconnected")|
| chat_message_failed      | ChatNotifier.sendMessage() on error     | typeNum, channelId, errorType                              |
| chat_channel_pinned      | ChatNotifier.togglePin()                | typeNum, channelId, pinned (bool)                          |
| chat_channel_muted       | ChatNotifier.toggleMute()               | typeNum, channelId, muted (bool)                           |

IMPLEMENTATION:
  - Analytics calls made via existing NavigationAnalytics service
  - Fire-and-forget (never block user actions for analytics)
  - Debug logging of events in debug mode only
```

#### Test Pattern

```pseudocode
TEST_SCENARIO: "Channel list loads and displays correctly"
  SETUP:
    mockChatRepository that returns [Channel A, Channel B]
    override chatRepositoryProvider with mock
    override chatPreferencesServiceProvider with mock (pin state)
    build ChatScreen widget with ProviderScope
  EXECUTE:
    pump widget
    wait for async load
  VERIFY:
    Channel A name visible
    Channel B name visible
    Unread badge shown for Channel A (unreadCount > 0)
    Pinned channel appears first

TEST_SCENARIO: "Message send with optimistic update"
  SETUP:
    mockChatRepository
    build ChannelScreen in loaded state with existing messages
  EXECUTE:
    enter text in composer
    tap send button
  VERIFY:
    Optimistic message appears immediately (isPending: true)
    API called with correct content and clientMessageId
    On API success: message updated with server ID, isPending: false

TEST_SCENARIO: "Real-time message from another user"
  SETUP:
    FakeAblyClient from package
    build ChannelScreen in loaded state
  EXECUTE:
    fakeAbly.simulateMessageCreated(newMessage)
  VERIFY:
    New message appears in list
    Sound plays (if from other user)
    Unread count not incremented (channel is active)
```

### Integration Points

- **Auth Provider**: `ChatNotifier` reads `authProvider` for current user info (userId, displayName) needed for mentions, sender attribution, and edit/delete permissions.
- **Selected Store Provider**: `chatTypeNumProvider` derives typeNum from `selectedStoreProvider`. Store switch triggers channel reload.
- **Permission Provider**: Route guards check `permissionProvider.canAccess(AppPage.chat)`. Channel creation gated by manager+ check.
- **Ably Provider**: Chat's Ably connection is separate from push notification Ably connection (different token, different channels).
- **Navigation**: GoRouter sub-routes under `/chat` for channel detail, search, settings, create.

## Architecture Decisions

- [x] ADR-1 **Path Dependency over Code Copy**: Import `buyerkiosk_chat` as `path: ../buyerkiosk-team/packages/buyerkiosk_chat`
  - Rationale: Single source of truth. Changes to models/services propagate to both apps automatically. Package is already tested (40 dedicated tests, 904 total in Team app).
  - Trade-offs: Requires both repos cloned side-by-side. CI/CD must clone Team repo. Version pinning via git ref recommended for production.
  - User confirmed: _Pending_

- [x] ADR-2 **Dedicated Chat Dio Instance**: Create `chatDioProvider` with `Content-Type: application/json` separate from the main `dioProvider`
  - Rationale: Main app uses `application/x-www-form-urlencoded` for legacy API. Chat API requires JSON. Sharing one Dio would break one or the other.
  - Trade-offs: Two Dio instances to maintain. Auth interceptors copied (not duplicated in code — same instances).
  - User confirmed: _Pending_

- [x] ADR-3 **Single ChatNotifier with Composite State**: Use one `Notifier<ChatCompositeState>` (not `AsyncNotifier`, not multiple notifiers)
  - Rationale: Proven in Team app. Coordinated state changes (e.g., entering a channel updates both active channel ID and message state atomically). Prevents race conditions.
  - Trade-offs: Single large notifier (~1,800 lines). Mitigated by clear method organization and derived providers for focused consumption.
  - User confirmed: _Pending_

- [x] ADR-4 **Adapt Screens, Don't Copy**: Build Live app screens from scratch following Team app patterns but adapted to Live app navigation
  - Rationale: Live app has different navigation (store navigation scaffold vs Team app's simple bottom nav). Different auth provider shape. Different route structure. Copy-paste would create maintenance burden and UI inconsistencies.
  - Trade-offs: More implementation effort than copy-paste. But result is properly integrated with Live app's design system and navigation.
  - User confirmed: _Pending_

- [x] ADR-5 **Separate Ably Connection for Chat**: Chat Ably connection independent from push notification Ably connection
  - Rationale: Different token scopes (chat channels vs push channels). Different lifecycle (chat connects on tab open, push connects on app start). Package manages its own connection.
  - Trade-offs: Two Ably connections consuming bandwidth. Acceptable — Ably SDK handles connection multiplexing internally when same API key is used.
  - **Note**: PRD risk table (Risk 6) incorrectly states "Single Ably connection shared with push notifications". This is corrected here: the connections are logically separate but the Ably SDK internally multiplexes them over a single TCP connection when sharing the same API key. Net effect: minimal bandwidth overhead. PRD risk mitigation updated accordingly.
  - User confirmed: _Pending_

## Quality Requirements

- **Performance**:
  - Channel list load: < 2 seconds on 4G connection
  - Message send to optimistic display: < 100ms (local operation)
  - Real-time message delivery: < 2 seconds end-to-end
  - Infinite scroll: No perceptible jank (60fps maintained)
  - Memory: < 50MB additional for chat state (100 messages/channel × estimated 200 bytes/message)

- **Usability**:
  - Chat tab accessible in 1 tap from any screen
  - Message send in 2 taps (open channel + tap send)
  - Mention autocomplete in 2 keystrokes (@ + first letter)
  - Consistent with Live app's Material 3 theme and design tokens

- **Security**:
  - JWT Bearer token on all API calls (handled by existing interceptor)
  - Ably tokens scoped to user's accessible channels only
  - No chat content stored locally (in-memory cache only, cleared on app close)
  - Channel access levels enforced server-side (public/manager/owner)

- **Reliability**:
  - Polling fallback when WebSocket unavailable (30-second interval)
  - Automatic reconnection with message resync after disconnection
  - Optimistic updates with retry on failure
  - No data loss — failed messages preserved in local state with retry option

## Risks and Technical Debt

### Known Technical Issues

- Live app's default Dio uses form-encoded content type — requires dedicated chat Dio to avoid breaking existing endpoints
- The `ably_flutter` package version differs slightly between Live app (^1.2.42) and chat package (^1.2.35) — compatible range but should verify

### Technical Debt

- Sound assets (mp3 files) need to be added to Live app's assets directory — currently only exist in Team app
- `emoji_picker_flutter` package needs to be added to Live app's dependencies
- `shared_preferences` needs to be added if not already present (for pin state)

### Implementation Gotchas

- **Content-Type override**: The `chatDioProvider` MUST set JSON content type. Forgetting this will cause 400 errors from the Chat API.
- **Interceptor copying**: When creating `chatDioProvider`, interceptors must be copied from the main Dio instance (same objects, not new instances) to preserve auth state.
- **TypeNum availability**: Chat requires a selected store. If no store is selected (edge case on first login), the Chat tab should show an empty state prompting store selection.
- **Ably token lifecycle**: Chat Ably tokens expire independently of JWT tokens. The `AblyRealtimeService` handles token refresh via its `tokenProvider` callback.
- **Memory pattern**: The Team app's ChatNotifier is ~1,850 lines. The Live app version will be similar in size. This is intentional (composite state coordination) not accidental complexity.
- **Build runner**: After adding the `buyerkiosk_chat` path dependency, `dart run build_runner build` must be run to generate code for any new Freezed models in the Live app (though the package's generated code is already built).

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Channel List Loads Successfully**
```gherkin
Given: User is authenticated and has a selected store
And: Chat API returns 3 channels (General, Managers, Announcements)
And: User has pinned "Managers" channel locally
When: User navigates to Chat tab
Then: Channels displayed with "Managers" at top (pinned)
And: Unread badges shown for channels with unreadCount > 0
And: Ably connection established for real-time updates
```

**Scenario 2: Send Message with Optimistic Update**
```gherkin
Given: User is viewing a channel with existing messages
When: User types "Hello team!" and taps send
Then: Message appears immediately at bottom (pending state)
And: API receives POST with content and clientMessageId
And: On API success, message transitions from pending to confirmed
And: Other users receive message via real-time event
```

**Scenario 3: Network Error During Message Send**
```gherkin
Given: User is viewing a channel
And: Network connection is lost
When: User types a message and taps send
Then: Message appears with "failed to send" indicator
And: Retry button is visible on the failed message
When: Network restored and user taps retry
Then: Message resent with same clientMessageId (dedup)
And: Message transitions to confirmed state
```

**Scenario 4: Real-time Event While Loading**
```gherkin
Given: User opens a channel (messages loading)
And: Another user sends a message during load
When: Channel messages finish loading
Then: Buffered real-time event replayed
And: New message appears in list (not duplicated)
```

**Scenario 5: Rate Limit Exceeded**
```gherkin
Given: User has sent 30 messages in the last minute
When: User attempts to send another message
Then: API returns 429 with Retry-After header
And: User sees "Slow down! Please wait a moment"
And: Message preserved for retry after cooldown
```

**Scenario 6: Store Switch Resets Chat**
```gherkin
Given: User is viewing chat for store "bk01"
And: Channels and messages are loaded
When: User switches to store "pa00" via store switcher
Then: Chat state resets completely
And: Channels reload for store "pa00"
And: Ably reconnects with "pa00" scope
```

### Test Coverage Requirements

- **Business Logic**: ChatNotifier state transitions (all channel/message/mention states), optimistic updates, event buffering, deduplication, rate limit handling
- **User Interface**: Channel list rendering, message bubble layout, composer interaction, reaction picker, mentions autocomplete, connection banner states
- **Integration Points**: ChatRepositoryImpl ↔ Dio mock, AblyRealtimeService ↔ FakeAblyClient (provided by package), ChatNotifier ↔ mocked repository
- **Edge Cases**: Empty channels, single message, max-length message (4000 chars), rapid-fire sends, disconnect/reconnect, access revocation mid-conversation
- **Security**: Verify no chat content persisted to disk, verify Ably token scope enforcement

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Channel | A named conversation space within a store (e.g., "General", "Managers Only") | Channels scope conversations by topic and access level |
| typeNum | Store identifier code (e.g., "bk01", "pa00") | All chat operations are scoped to a store via typeNum |
| Access Level | Channel visibility tier: public (all employees), manager (managers+), owner (owners only) | Controls who can see and post in a channel |
| Mention | @reference to a specific user in a message | Triggers notification in recipient's Mentions tab |
| Read Receipt | Server-side tracking of the last message ID a user has seen | Powers unread count badges |
| Pin | Local user preference to keep a channel at the top of the list | Stored in SharedPreferences per store |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| Composite State | Single state object containing all chat sub-states (channels, messages, mentions, connection) | ChatNotifier manages one composite state for atomic updates |
| Optimistic Update | Displaying a change in the UI before server confirmation | Messages appear instantly; confirmed asynchronously |
| clientMessageId | UUID generated client-side, sent with message, used for deduplication | Prevents duplicate messages when retrying or when real-time event races with API response |
| Event Buffering | Queuing real-time events that arrive during an ongoing API load | Prevents lost events during channel message loading |
| Derived Provider | Riverpod provider that selects a subset of another provider's state | `.select()` pattern for efficient widget rebuilds |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| Staff Chat API | REST API at `/api/mobile/staff-chat/{typeNum}/` | 19 endpoints for channels, messages, reactions, mentions |
| Ably Token | Time-limited credential for WebSocket real-time subscriptions | Obtained via `/ably-token` endpoint, scoped to user's channels |
| ChatEvent | Sealed class hierarchy representing real-time events from Ably | 8 event types (message.created, .updated, .deleted, reaction.added/removed, etc.) |
| ChatException | Typed exception hierarchy for chat-specific errors | 13 exception types mapping to specific HTTP status codes and business rules |
