# M09 — Communication & Notifications

Phase 4. Firm-wide announcements, internal messaging, matter discussions, a
proper notification centre, and an SMS channel — all events fired by earlier
modules (hearing reminders, deadline alerts, invoice sent, fee approvals) land
here.

## Dependencies
M01 (users), M03 (matter_comments already scaffolded in M03 — this module adds
the delivery/notification layer over them).

## Part A — Announcements

### `announcements` / `announcement_reads`
```php
// announcements
$table->id(); $table->ulid('ulid')->unique();
$table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->string('title'); $table->text('body');
$table->string('audience')->default('all');  // all / role:{name} / office:{id}
$table->boolean('requires_acknowledgement')->default(false);
$table->boolean('pinned')->default(false);
$table->dateTime('publish_at')->nullable();
$table->dateTime('expires_at')->nullable();
$table->string('status')->default('draft');  // draft/published
$table->foreignId('created_by')->constrained('users');
$table->timestamps();

// announcement_reads
$table->foreignId('announcement_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->dateTime('read_at'); $table->dateTime('acknowledged_at')->nullable();
$table->unique(['announcement_id', 'user_id']);
```
Publish → resolves audience to a user list → creates `Notification` records +
optionally email. Pinned + unexpired → dismissible banner on dashboard.
Acknowledgement-required → blocking modal until acked. Creator sees a
read/ack matrix (`announcements/{id}/readers`).

## Part B — Internal Messaging

### `message_threads` / `message_thread_participants` / `messages`
```php
// message_threads
$table->id(); $table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->string('subject')->nullable();
$table->foreignId('created_by')->constrained('users');
$table->timestamps();

// message_thread_participants
$table->foreignId('thread_id')->constrained('message_threads')->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->dateTime('last_read_at')->nullable();
$table->primary(['thread_id', 'user_id']);

// messages
$table->id();
$table->foreignId('thread_id')->constrained('message_threads')->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->text('body'); $table->string('attachment_path')->nullable();
$table->timestamps();
```
Simple threaded inbox (no websockets in v1) — unread badge via a lightweight
polling endpoint (60s interval). New message → in-app notification always;
email if the recipient is flagged "offline" (no activity in 30 min — a cheap
heuristic, not presence tracking).

## Part C — Notification Centre

Laravel's native `Notification` system, database + mail + (optional) SMS
channels. Every module fires domain events; a thin `Notifications/` class per
event type renders the message for each channel. Centre UI groups by type
(hearing, deadline, task, fee approval, message, announcement) with icons,
mark-all-read, unread filter.

### SMS channel (feature-flagged: `feature('sms')`)
`app/Notifications/Channels/SmsChannel.php` calling a provider adapter
(`TermiiAdapter` primary — local NGN billing; `TwilioAdapter` as an
alternative, selected via config). Used for: hearing reminders to lawyers
(optional per-user preference), client-facing hearing-date notices (opt-in per
client), invoice due reminders. Every SMS send logged (cost tracking + audit).

## Part D — Daily Digest

`legal:daily-digest` (07:00 scheduled command, queued per-user job): for each
user with any of {hearings today, tasks due today, pending approvals}, send one
consolidated email — never an empty digest, and never another user's items
(explicit test).

## Routes

```
RESTful /announcements
POST /announcements/{a}/acknowledge      announcements.acknowledge
GET  /announcements/{a}/readers          announcements.readers
GET  /messages                            messages.index
POST /messages                            messages.store  (new thread)
GET  /messages/{thread}                   messages.show
POST /messages/{thread}/reply             messages.reply
GET  /messages/unread-count               messages.unreadCount (JSON, polled)
GET  /notifications                       notifications.index
POST /notifications/mark-all-read         notifications.markAllRead
```
(`matter_comments` routes were scaffolded in M03; this module wires their
notification delivery.)

## Acceptance criteria

- [ ] Announcement publish resolves audience correctly and creates
      notifications; acknowledgement-required blocks via modal until acked;
      read/ack matrix accurate
- [ ] Messaging: unread badge accurate; `last_read_at` updates correctly on
      thread view
- [ ] Matter comment creates a notification to matter team members (excluding
      the commenter)
- [ ] SMS channel sends only when `feature('sms')` is on; every send logged
- [ ] Daily digest contains only the recipient's own items; no digest sent to
      users with nothing pending (test)
- [ ] Cross-firm isolation on threads/announcements

## Claude Code kickoff prompt

> Read CLAUDE.md and this file, plus the domain events already fired by M03/
> M04/M06/M07 (HearingHeld, MatterRegistered, InvoiceSent, deadline reminders,
> fee-claim state changes). Implement Part A → B → C → D in order, wiring
> Notification classes for each existing event as you go rather than
> retrofitting later. Keep the SMS adapter behind an interface so Termii/Twilio
> are swappable without touching call sites.
