Files
nick-doc/02 - Data Models/Notification.md
Siavash Sameni 7a616744f4 docs: complete code-reality alignment for remaining docs + reconcile issue set
Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
  Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
  added undocumented endpoints (ton-proof challenge, profile email verify,
  GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
  enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
  90-day notification TTL, soft-delete semantics, wallet fields

Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
  page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
  TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation

Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
  ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:15:02 +04:00

115 lines
5.5 KiB
Markdown

---
title: Notification
tags: [data-model, mongoose]
aliases: [User Notification, INotification]
---
# Notification
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
Per-user notification entry. Each row binds to one `userId` (stored as a string rather than ObjectId), carries a typed severity (`info` / `success` / `warning` / `error`) and a domain category, optionally references another entity via `relatedId`, and supports an `actionUrl` for deep-linking. Old notifications are auto-purged by a 90-day TTL index (`createdAt` with `expireAfterSeconds = 7,776,000`).
> [!note] Source
> `backend/src/models/Notification.ts:18` — schema definition
> `backend/src/models/Notification.ts:79` — model export
> [!warning] String userId
> `userId` is a String, not an ObjectId, and is not declared with `ref`. Consumers must cast to `ObjectId` if they want to `populate()` it as a [[User]].
> [!warning] `category` enum vs reality
> The schema enum is `purchase_request` / `offer` / `payment` / `delivery` / `system`, but in practice:
> - `notificationController.createNotification` defaults the category to **`'general'`** (`category = 'general'`) when the caller omits it. `'general'` is **not** in the schema enum — Mongoose enum validation will reject it on a strict save, so callers must supply a valid value or the write fails. Treat `'general'` as a value you may encounter in payloads even though it is not an enum member.
> - The frontend socket hook `use-notifications.ts` hardcodes `category: 'system'` for every realtime-injected notification, so most client-side notifications surface as `'system'` regardless of their true domain.
> - `NotificationService.notifyRequestStatusChanged` always writes `category: 'system'` for purchase-request status changes.
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `userId` | String | yes | — | — | yes (single + compound) | Owner of the notification. |
| `title` | String | yes | — | maxlength 200 | — | Headline. |
| `message` | String | yes | — | maxlength 1000 | — | Body. |
| `type` | String | yes | `info` | enum: `info` / `success` / `warning` / `error` | — | Severity. |
| `category` | String | yes | — | enum: `purchase_request` / `offer` / `payment` / `delivery` / `system` | yes (compound) | Domain bucket. ⚠️ `notificationController` defaults to `'general'` (not in the enum) and the realtime socket hook + `notifyRequestStatusChanged` hardcode `'system'`. See warning above. |
| `relatedId` | String | no | — | — | yes | Id of the related entity (e.g. [[PurchaseRequest]]). |
| `metadata` | Mixed | no | — | — | — | Arbitrary payload. |
| `actionUrl` | String | no | — | maxlength 500 | — | Deep link. |
| `isRead` | Boolean | no | `false` | — | yes (compound) | Read flag. |
| `readAt` | Date | no | — | — | — | When read. |
| `createdAt` | Date | auto | — | — | yes (compound + TTL) | Mongoose timestamp. Auto-deleted after 90 days by TTL index. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
The collection name is overridden to `notifications` via `collection: 'notifications'`.
## Virtuals
None defined.
## Indexes
Defined at `backend/src/models/Notification.ts:71-77`:
- `{ userId: 1, createdAt: -1 }` — user feed.
- `{ userId: 1, isRead: 1 }` — unread badge.
- `{ userId: 1, category: 1 }` — category filter.
- `{ relatedId: 1 }` — lookup by linked entity.
- `{ createdAt: 1 }` with `expireAfterSeconds: 60 * 60 * 24 * 90` (7,776,000 s) — MongoDB TTL index; the database hard-deletes documents automatically after 90 days.
Plus the implicit index from `userId` having `index: true` at the field level.
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Status-change notification coverage
`NotificationService.notifyRequestStatusChanged` maps a [[PurchaseRequest]] status to a human label via an internal `statusMessages` table. That table covers `pending`, `active`, `received_offers`, `in_negotiation`, `payment`, `processing`, `delivery`, `delivered`, `confirming`, `completed`, and `cancelled`.
> [!warning] Missing status templates
> The `pending_payment` and `seller_paid` [[PurchaseRequest]] statuses have **no entry** in the `statusMessages` table and no dedicated notification template. Transitions into these states do not produce a meaningful status-change notification (the label falls back to the raw status string, and several flows skip notification entirely). If you rely on notifications for `pending_payment` / `seller_paid`, they will not arrive as expected.
## Relationships
- **References**: [[User]] indirectly through `userId` (string); arbitrary entity via `relatedId`.
- **Referenced by**: none.
## State Transitions
```mermaid
stateDiagram-v2
[*] --> unread
unread --> read : user opens
read --> [*] : TTL purge (90d)
unread --> [*] : TTL purge (90d)
```
## Common Queries
```ts
// User feed
Notification.find({ userId }).sort({ createdAt: -1 }).limit(50);
// Unread badge count
Notification.countDocuments({ userId, isRead: false });
// Mark all read
Notification.updateMany(
{ userId, isRead: false },
{ $set: { isRead: true, readAt: new Date() } }
);
// All notifications about a request
Notification.find({ relatedId: purchaseRequestId.toString() });
```
Related: [[User]], [[PurchaseRequest]], [[SellerOffer]], [[Payment]].