# Notification Centre API

In-app notifications for the **Customer App**, the **Astrologer App** and the **Web App**
(`https://astroeye.in/notifications`).

Both stacks expose the **same seven endpoints with the same payloads**, under their own prefix:

| Audience | Base |
|---|---|
| Customer app + web app (customer) | `/api/user/notifications` |
| Astrologer app + web app (astrologer) | `/api/astrologer/notifications` |

The web app picks the prefix from whichever account is signed in and renders one screen for both.

---

## Authentication

Every endpoint requires the account's access token:

```
Authorization: Bearer <token>
```

- `/api/user/*` → the token from `verify-login-otp` / `register`, validated by `JWTAuthMiddleware`
  (signature **and** a match against `users.jwt_token`, so logout invalidates it).
- `/api/astrologer/*` → the token from `verify-login` / `verify-email`, validated by
  `AstroAuthMiddleware` (signature only).

Missing or invalid token → **401**.

Notifications are keyed on `users.id`, which is what both middlewares put in the context — so an
astrologer reads their own rows through the astrologer prefix with no extra lookup.

---

## Response envelope

Every response uses the standard `helpers` envelope:

```json
{ "status": true, "message": "…", "data": { … } }
```

On failure:

```json
{ "status": false, "message": "select at least one notification to remove" }
```

| HTTP | When |
|---|---|
| `200` | Success |
| `400` | Bad request body, empty `ids`, invalid path id |
| `401` | Missing / invalid / logged-out token |

---

## The three states

The apps asked for `seen`, `unseen` and a `new` flag. They are three different things:

| Field | Meaning | Cleared by |
|---|---|---|
| `is_read` / `is_seen` | The person opened **this notification**. `false` = unseen. | `POST /notifications/read` |
| `is_new` | This notification **arrived since the screen was last opened**. | `POST /notifications/seen` |

`is_seen` is an alias of `is_read` — both are returned so a client can use whichever name its
screen is written against.

**The badge** (`counts.unseen`) counts unread notifications.
**The tab dot** (`counts.new`) counts ones that arrived since the list was last opened.

Opening the notification screen should call `POST /notifications/seen` — it clears the dot
**without** marking anything read, so entries still render as unread until the person taps one.
An account that has never opened the screen has `new == total`.

---

## 1. List notifications

```
GET /api/user/notifications
GET /api/astrologer/notifications
```

Newest first (`created_at DESC, id DESC`).

### Query parameters

| Param | Type | Default | Notes |
|---|---|---|---|
| `page` | int | `1` | 1-based |
| `limit` | int | `20` | Capped at `100` |
| `status` | string | `ALL` | `ALL` \| `READ` \| `UNREAD` |
| `search` | string | `""` | Matches `title` or `description` |

### Example

```
GET /api/user/notifications?page=1&limit=3&status=ALL
Authorization: Bearer <token>
```

```json
{
  "status": true,
  "message": "Notifications fetched successfully",
  "data": {
    "list": [
      {
        "id": 15,
        "title": "Wallet recharged",
        "description": "Rs 500 added to your wallet",
        "notification_type": 1,
        "is_read": false,
        "is_seen": false,
        "is_new": true,
        "chat_request_id": null,
        "call_request_id": null,
        "read_at": null,
        "created_at": "2026-09-23T10:52:33+05:30",
        "time_ago": "5 minutes ago"
      },
      {
        "id": 16,
        "title": "Astrologer online",
        "description": "Pandit Sharma is now online",
        "notification_type": 2,
        "is_read": false,
        "is_seen": false,
        "is_new": true,
        "chat_request_id": null,
        "call_request_id": null,
        "read_at": null,
        "created_at": "2026-09-23T08:57:33+05:30",
        "time_ago": "2 hours ago"
      }
    ],
    "counts": { "total": 4, "unseen": 3, "seen": 1, "new": 4 },
    "pagination": { "page": 1, "limit": 3, "totalRecords": 4, "totalPages": 2 }
  }
}
```

`time_ago` is rendered server-side (`Just now`, `5 minutes ago`, `2 hours ago`, `3 days ago`,
`2 months ago`, `1 year ago`) so every client agrees on the wording.

---

## 2. Unseen count (badge)

```
GET /api/user/notifications/count
GET /api/astrologer/notifications/count
```

No parameters. Cheap enough to poll or to call on app resume.

```json
{
  "status": true,
  "message": "Notification count fetched successfully",
  "data": { "total": 4, "unseen": 3, "seen": 1, "new": 4 }
}
```

---

## 3. Mark the screen seen

```
POST /api/user/notifications/seen
POST /api/astrologer/notifications/seen
```

No body. Call when the notification screen opens. Clears `is_new` / `counts.new`;
does **not** change `is_read`.

```json
{
  "status": true,
  "message": "Notifications marked as seen",
  "data": { "affected": 0, "counts": { "total": 4, "unseen": 3, "seen": 1, "new": 0 } }
}
```

---

## 4. Mark read

```
POST /api/user/notifications/read
POST /api/astrologer/notifications/read
```

### Request

```json
{ "ids": [15, 16] }
```

or, to clear the whole badge in one call:

```json
{ "all": true }
```

| Field | Type | Required | Notes |
|---|---|---|---|
| `ids` | array of int | required unless `all` | One id or many |
| `all` | bool | optional | `true` marks every unread notification read |

### Response

```json
{
  "status": true,
  "message": "Notifications marked as read",
  "data": { "affected": 1, "counts": { "total": 4, "unseen": 2, "seen": 2, "new": 0 } }
}
```

`affected` counts rows that actually changed, so re-sending an already-read id reports `0` and
does not push `read_at` forward.

Sending neither `ids` nor `all` → **400** `"send ids, or all = true"`.

---

## 5. Remove notifications (one or several)

```
POST /api/user/notifications/delete
POST /api/astrologer/notifications/delete
```

The same endpoint serves swipe-to-delete (one id) and multi-select (many ids).

### Request

```json
{ "ids": [16, 17] }
```

### Response

```json
{
  "status": true,
  "message": "Notifications removed successfully",
  "data": { "affected": 2, "counts": { "total": 2, "unseen": 1, "seen": 1, "new": 0 } }
}
```

Empty `ids` → **400** `"select at least one notification to remove"`.

### Single delete without a body

```
DELETE /api/user/notifications/:id
DELETE /api/astrologer/notifications/:id
```

For clients that will not send a body on a `DELETE`. Same response shape.

```
DELETE /api/user/notifications/15
```

---

## 6. Clear all

```
POST /api/user/notifications/clear
POST /api/astrologer/notifications/clear
```

No body. Removes every notification on the account.

```json
{
  "status": true,
  "message": "All notifications cleared successfully",
  "data": { "affected": 1, "counts": { "total": 0, "unseen": 0, "seen": 0, "new": 0 } }
}
```

---

## Every mutating endpoint returns the fresh counts

`read`, `seen`, `delete` and `clear` all answer with `counts`, so the app updates its badge from
the same response instead of making a second call to `/count`.

---

## Ownership

Ids in a request body are **never trusted on their own**. Every query is also filtered by the
`userId` taken from the token, so posting another account's notification id affects nothing and
returns `affected: 0` rather than an error — the caller learns nothing about whether that id exists.

---

## `notification_type` codes

Routes a tap to the right screen. Defined in `notifications/repository.go`.

| Code | Event | Audience |
|---|---|---|
| `0` | General | both |
| `1` | Wallet recharged | customer |
| `2` | Favourite astrologer online | customer |
| `3` | Consultation request accepted | customer |
| `4` | Astrologer free (from waitlist) | customer |
| `5` | Low balance | customer |
| `6` | Kundali ready | customer |
| `7` | Admin message | customer |
| `11` | Profile approved / rejected | astrologer |
| `12` | Consultation request received | astrologer |
| `13` | Customer waiting | astrologer |
| `14` | Settlement status / credited | astrologer |
| `15` | Withdraw approved | astrologer |
| `16` | Withdraw rejected | astrologer |
| `20` | Consultation request rejected | customer |
| `21` | Consultation missed | both |
| `22` | Consultation cancelled | both |
| `23` | Consultation ended | both |
| `24` | Free chat ending | customer |
| `25` | Session ending soon | astrologer |
| `26` | Request not joined | both |

---

## Database changes

`migrations/2026_09_23_notification_center.sql` — idempotent, applied by hand.

| Table | Column | Purpose |
|---|---|---|
| `user_notifications` | `read_at` TIMESTAMP NULL | When the notification was opened |
| `user_notifications` | `deleted_at` TIMESTAMP NULL | When it was removed (alongside `isDelete`) |
| `users` | `notifications_seen_at` TIMESTAMP NULL | When the screen was last opened — drives `is_new` |

Plus two indexes on `user_notifications`:

- `idx_user_notifications_list (userId, isDelete, created_at)` — the list query
- `idx_user_notifications_unread (userId, isDelete, is_read)` — the badge count

Existing rows are backfilled: an already-read row gets `read_at = created_at` (not `NOW()`, so the
history does not claim every old notification was read on migration day), and an already-removed
row gets `deleted_at = updated_at`.

Removal is a **soft delete** — `isDelete = 1`, `isActive = 0`, `deleted_at` stamped. Nothing is
hard-deleted, so a mistaken "clear all" is recoverable in the database and the push history stays
auditable.

---

## Quick reference

| # | Method | Path (prefix `/api/user` or `/api/astrologer`) | Body |
|---|---|---|---|
| 1 | `GET` | `/notifications?page=&limit=&status=&search=` | — |
| 2 | `GET` | `/notifications/count` | — |
| 3 | `POST` | `/notifications/seen` | — |
| 4 | `POST` | `/notifications/read` | `{"ids":[1,2]}` or `{"all":true}` |
| 5 | `POST` | `/notifications/delete` | `{"ids":[1,2]}` |
| 6 | `DELETE` | `/notifications/:id` | — |
| 7 | `POST` | `/notifications/clear` | — |
