# Push Notifications Architecture

This document describes how push notifications work in the Casera API.

## Overview

The Casera API sends push notifications directly to Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM) without any intermediate push server. This approach:

- Reduces infrastructure complexity (no Gorush or other push server needed)
- Provides direct control over notification payloads
- Supports actionable notifications with custom button types

## Architecture

```
┌─────────────────────────────────────────────────────────────────────────┐
│                           Casera API                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ┌──────────────┐    ┌─────────────────────┐    ┌───────────────────┐   │
│  │   Handlers   │───▶│ NotificationService │───▶│    PushClient     │   │
│  │  (HTTP API)  │    │                     │    │                   │   │
│  └──────────────┘    │  - Create record    │    │  ┌─────────────┐  │   │
│                      │  - Get button types │    │  │  APNsClient │──┼───┼──▶ APNs
│  ┌──────────────┐    │  - Send push        │    │  │  (apns2)    │  │   │
│  │   Worker     │───▶│                     │    │  └─────────────┘  │   │
│  │  (Asynq)     │    └─────────────────────┘    │                   │   │
│  └──────────────┘                               │  ┌─────────────┐  │   │
│                                                 │  │  FCMClient  │──┼───┼──▶ FCM
│                                                 │  │  (HTTP)     │  │   │
│                                                 │  └─────────────┘  │   │
│                                                 └───────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
```

## Components

### 1. Push Client (`internal/push/client.go`)

The unified push client that wraps both APNs and FCM clients:

```go
type Client struct {
    apns *APNsClient  // iOS
    fcm  *FCMClient   // Android
}
```

**Key Methods:**
- `SendToIOS()` - Send to iOS devices
- `SendToAndroid()` - Send to Android devices
- `SendToAll()` - Send to both platforms
- `SendActionableNotification()` - Send with iOS category for action buttons

### 2. APNs Client (`internal/push/apns.go`)

Uses the [sideshow/apns2](https://github.com/sideshow/apns2) library for direct APNs communication:

- **Token-based authentication** using .p8 key file
- **Production/Sandbox** modes based on configuration
- **Batch sending** with per-token error handling
- **Category support** for actionable notifications

### 3. FCM Client (`internal/push/fcm.go`)

Uses direct HTTP calls to the FCM legacy API (`https://fcm.googleapis.com/fcm/send`):

- **Server key authentication**
- **Batch sending** via `registration_ids`
- **Data payload** for Android action handling

### 4. Notification Service (`internal/services/notification_service.go`)

Orchestrates notification creation and delivery:

```go
func (s *NotificationService) CreateAndSendTaskNotification(
    ctx context.Context,
    userID uint,
    notifType models.NotificationType,
    task *models.Task,
) error
```

**Responsibilities:**
1. Create notification record in database
2. Determine button types based on task state
3. Map button types to iOS category
4. Get user's device tokens
5. Send via PushClient

### 5. Task Button Types (`internal/services/task_button_types.go`)

Determines which action buttons to show based on task state:

| Task State | Button Types |
|------------|--------------|
| Overdue | `edit`, `complete`, `cancel`, `mark_in_progress` |
| Due Soon | `edit`, `complete`, `cancel`, `mark_in_progress` |
| In Progress | `edit`, `complete`, `cancel` |
| Cancelled | `edit` |
| Completed | `edit` |

## Scheduled Notifications

The worker process (`cmd/worker/main.go`) sends scheduled notifications using Asynq:

### Schedule

| Job | Default Time (UTC) | Config Variable |
|-----|-------------------|-----------------|
| Task Reminders | 8:00 PM | `TASK_REMINDER_HOUR`, `TASK_REMINDER_MINUTE` |
| Overdue Alerts | 9:00 AM | `OVERDUE_REMINDER_HOUR` |
| Daily Digest | 11:00 AM | `DAILY_DIGEST_HOUR` |

### Job Handlers (`internal/worker/jobs/handler.go`)

1. **HandleTaskReminder** - Sends actionable notifications for tasks due today/tomorrow
2. **HandleOverdueReminder** - Sends actionable notifications for overdue tasks
3. **HandleDailyDigest** - Sends summary notification with task statistics

## iOS Categories

iOS actionable notifications use categories to define available actions. The API sends a `category` field that maps to categories registered in the iOS app:

| Category ID | Actions |
|-------------|---------|
| `TASK_ACTIONABLE` | Complete, Edit, Skip, In Progress |
| `TASK_IN_PROGRESS` | Complete, Edit, Cancel |
| `TASK_CANCELLED` | Edit |
| `TASK_COMPLETED` | Edit |

The iOS app must register these categories in `AppDelegate`:

```swift
let completeAction = UNNotificationAction(identifier: "COMPLETE", title: "Complete")
let editAction = UNNotificationAction(identifier: "EDIT", title: "Edit")
// ... more actions

let taskCategory = UNNotificationCategory(
    identifier: "TASK_ACTIONABLE",
    actions: [completeAction, editAction, skipAction, inProgressAction],
    intentIdentifiers: []
)
```

## Configuration

### Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `APNS_AUTH_KEY_PATH` | For iOS | Path to .p8 key file |
| `APNS_AUTH_KEY_ID` | For iOS | Key ID from Apple Developer |
| `APNS_TEAM_ID` | For iOS | Team ID from Apple Developer |
| `APNS_TOPIC` | For iOS | Bundle ID (e.g., `com.tt.casera.CaseraDev`) |
| `APNS_PRODUCTION` | No | `true` for production, `false` for sandbox |
| `APNS_USE_SANDBOX` | No | Deprecated, use `APNS_PRODUCTION` |
| `FCM_SERVER_KEY` | For Android | Firebase Cloud Messaging server key |

### Docker/Dokku Setup

Mount the .p8 key file:

```bash
# Docker
volumes:
  - ./push_certs:/certs:ro

# Dokku
dokku storage:mount casera-api /path/to/push_certs:/certs
```

## Data Flow

### Real-time Notification (e.g., task assigned)

```
1. User assigns task via API
2. Handler calls NotificationService.CreateAndSendTaskNotification()
3. NotificationService:
   a. Creates Notification record in database
   b. Calls GetButtonTypesForTask() to determine actions
   c. Maps buttons to iOS category
   d. Gets user's device tokens from APNSDevice/GCMDevice tables
   e. Calls PushClient.SendActionableNotification()
4. PushClient sends to APNs and/or FCM
5. Device receives notification with action buttons
```

### Scheduled Notification (e.g., daily overdue alert)

```
1. Asynq scheduler triggers job at configured time
2. Worker calls HandleOverdueReminder()
3. Handler:
   a. Queries all overdue tasks with Preloads
   b. Groups tasks by user (assigned_to or residence owner)
   c. Checks user's notification preferences
   d. For each user with enabled notifications:
      - Sends up to 5 individual actionable notifications
      - Sends summary if more than 5 tasks
4. NotificationService creates records and sends via PushClient
```

## Device Token Management

Device tokens are stored in two tables:

- `push_notifications_apnsdevice` - iOS device tokens
- `push_notifications_gcmdevice` - Android device tokens (FCM)

Fields:
- `registration_id` - The device token
- `user_id` - Associated user
- `active` - Whether to send to this device
- `device_id` - Unique device identifier

Tokens are registered via:
- `POST /api/push/apns/` - Register iOS device
- `POST /api/push/gcm/` - Register Android device

## Error Handling

### APNs Errors

The APNs client handles individual token failures:
- Logs each failure with token prefix and reason
- Continues sending to remaining tokens
- Returns error only if ALL tokens fail

Common APNs errors:
- `BadDeviceToken` - Invalid or expired token
- `Unregistered` - App uninstalled
- `TopicDisallowed` - Bundle ID mismatch

### FCM Errors

The FCM client parses the response to identify failed tokens:
- Logs failures with error type
- Returns error only if ALL tokens fail

Common FCM errors:
- `InvalidRegistration` - Invalid token
- `NotRegistered` - App uninstalled
- `MismatchSenderId` - Wrong server key

## Debugging

### Check Push Configuration

```go
log.Info().
    Bool("ios_enabled", pushClient.IsIOSEnabled()).
    Bool("android_enabled", pushClient.IsAndroidEnabled()).
    Msg("Push notification client initialized")
```

### View Worker Logs

```bash
# Docker
docker-compose logs -f worker

# Dokku
dokku logs casera-api -p worker
```

### Test Push Manually

The API has a test endpoint (debug mode only):

```bash
curl -X POST https://api.example.com/api/push/test/ \
  -H "Authorization: Token <token>" \
  -H "Content-Type: application/json" \
  -d '{"title": "Test", "message": "Hello"}'
```

## Migration from Gorush

The API previously used Gorush as an intermediate push server. The migration to direct APNs/FCM involved:

1. **Added** `internal/push/apns.go` - Direct APNs using apns2 library
2. **Added** `internal/push/fcm.go` - Direct FCM using HTTP
3. **Added** `internal/push/client.go` - Unified client wrapper
4. **Removed** Gorush service from docker-compose.yml
5. **Removed** `GORUSH_URL` configuration

Benefits of direct approach:
- No additional container/service to manage
- Lower latency (one less hop)
- Full control over notification payload
- Simplified debugging
