> ## Documentation Index
> Fetch the complete documentation index at: https://internal.mechzie.in/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Notifications

> FCM push notifications, in-app notification management, and notification triggers

## Overview

MechZie uses **Firebase Cloud Messaging (FCM)** for push notifications with **Fast2SMS** as an SMS fallback. All notifications are also stored in-app for retrieval via the API.

## Setting Up Push Notifications

### Register FCM Token

On app startup and whenever the FCM token refreshes, register it with the API:

```dart theme={null}
final fcmToken = await FirebaseMessaging.instance.getToken();

await http.put(
  Uri.parse('$baseUrl/api/v1/notifications/fcm-token'),
  headers: {
    'Authorization': 'Bearer $accessToken',
    'Content-Type': 'application/json',
  },
  body: jsonEncode({
    'token': fcmToken,
    'device_info': 'Flutter Android 14',  // optional
  }),
);
```

```bash theme={null}
curl -X PUT https://mechzie-api-production.run.app/api/v1/notifications/fcm-token \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "fcm-device-token-string",
    "device_info": "Flutter Android 14"
  }'
```

<Note>
  Call this on every app launch. The server tracks `last_used_at` and automatically prunes stale tokens. If you don't re-register, the token may be cleaned up and the user will stop receiving push notifications.
</Note>

### Listen for Token Refresh

```dart theme={null}
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async {
  await registerFcmToken(newToken);
});
```

## Listing Notifications

Fetch in-app notifications (paginated):

```bash theme={null}
GET /api/v1/notifications?page=1&limit=20
Authorization: Bearer ACCESS_TOKEN
```

**Response:**

```json theme={null}
{
  "data": [
    {
      "id": "uuid",
      "user_id": "uuid",
      "title": "Mechanic on the way",
      "body": "Ravi is heading to your location. ETA: 8 minutes.",
      "data": {
        "jobId": "uuid",
        "type": "job_status_change",
        "status": "en_route"
      },
      "is_read": false,
      "created_at": "2026-05-21T10:30:00.000Z"
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 15
}
```

## Marking as Read

### Mark a single notification

```bash theme={null}
PATCH /api/v1/notifications/{id}/read
Authorization: Bearer ACCESS_TOKEN
```

### Mark all as read

```bash theme={null}
PATCH /api/v1/notifications/read-all
Authorization: Bearer ACCESS_TOKEN
```

## Notification Triggers

These events generate notifications for the relevant user:

### Customer Notifications

| Trigger               | Title Example            | When                                 |
| --------------------- | ------------------------ | ------------------------------------ |
| Mechanic accepted job | "Mechanic assigned"      | Job status → `accepted`              |
| Mechanic en route     | "Mechanic on the way"    | Job status → `en_route`              |
| Mechanic arrived      | "Mechanic has arrived"   | Job status → `arrived`               |
| Job completed         | "Service completed"      | Job status → `completed`             |
| Payment failed        | "Payment failed"         | Razorpay `payment.failed` webhook    |
| Refund processed      | "Refund issued"          | Admin initiates refund (FCM + email) |
| No mechanic found     | "No mechanics available" | All dispatch tiers exhausted         |

### Mechanic Notifications

| Trigger                   | Title Example         | When                            |
| ------------------------- | --------------------- | ------------------------------- |
| New job offer             | "New job nearby"      | Dispatch sends `job:offer`      |
| Job cancelled by customer | "Job cancelled"       | Customer cancels                |
| Verification approved     | "Profile verified"    | Admin approves mechanic         |
| Verification rejected     | "Verification update" | Admin rejects mechanic          |
| Document review           | "Document reviewed"   | Admin approves/rejects document |

## Notification Data Payload

The `data` field contains structured data for deep-linking. Use it to navigate the user to the relevant screen:

```dart theme={null}
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  final data = message.data;

  switch (data['type']) {
    case 'job_status_change':
      navigateToJob(data['jobId']);
      break;
    case 'job_offer':
      navigateToJobOffer(data['jobId']);
      break;
    case 'payment_failed':
      navigateToPayment(data['jobId']);
      break;
    case 'mechanic_verified':
      navigateToProfile();
      break;
    case 'refund_processed':
      navigateToPaymentHistory(data['jobId']);
      break;
  }
});
```

## Delivery Channels

| Channel            | Priority        | Notes                                                                    |
| ------------------ | --------------- | ------------------------------------------------------------------------ |
| **FCM Push**       | Primary         | Requires registered token                                                |
| **Fast2SMS**       | Fallback        | Used when FCM delivery fails                                             |
| **Email (Resend)** | Specific events | Admin invites, refund confirmations                                      |
| **Socket.io**      | Real-time       | `job:status`, `payment:failed`, etc. (see [Real-time Events](/realtime)) |
| **In-App**         | Persistent      | Always stored in `notifications` table                                   |

<Note>
  Push notifications and Socket.io events often carry the same information. Socket.io is for real-time UI updates while the user is active. Push notifications reach users when the app is in the background. In-app notifications serve as a persistent history.
</Note>

## Unread Badge Count

The API doesn't have a dedicated unread count endpoint, but you can calculate it from the notification list:

```dart theme={null}
final response = await api.get('/notifications?page=1&limit=1');
// Use the total count of unread notifications
// Or filter client-side: data.where((n) => !n.is_read).length
```

<Note>
  For a proper unread count, fetch the first page and count items where `is_read == false`. Consider caching this and updating it when you mark notifications as read.
</Note>
