---
title: Review
tags: [data-model, mongoose]
aliases: [Rating, IReview]
---

# Review

Polymorphic 1-5 star review. The `subjectType` discriminator (`seller` or `template`) plus `subjectId` identifies what is being reviewed. `sellerId` is always present so per-seller aggregations work regardless of subject. A compound unique index on `(subjectType, subjectId, reviewerId)` prevents a reviewer from posting two reviews for the same subject.

> [!note] Source
> `backend/src/models/Review.ts:19` — schema definition
> `backend/src/models/Review.ts:38` — model export

## Schema

| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `subjectType` | String | yes | — | enum: `seller` / `template` | yes (compound) | Discriminator. |
| `subjectId` | ObjectId | yes | — | — | yes (compound) | Id of the seller [[User]] or [[RequestTemplate]]. |
| `sellerId` | ObjectId → [[User]] | yes | — | — | — | Seller associated with the review (always populated). |
| `reviewerId` | ObjectId → [[User]] | yes | — | — | yes (compound + unique) | Author. |
| `rating` | Number | yes | — | min 1, max 5 | — | Star rating. |
| `comment` | String | no | `""` | maxlength 1000 | — | Free-form comment. |
| `isVerifiedBuyer` | Boolean | no | `false` | — | — | Whether the reviewer actually bought from this seller. |
| `purchaseRequestId` | ObjectId → [[PurchaseRequest]] | no | `null` | — | — | Source request (if any). |
| `status` | String | no | `published` | enum: `published` / `pending` / `rejected` | — | Moderation status. |
| `createdAt` | Date | auto | — | — | yes (compound, desc) | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |

## Virtuals

None defined.

## Indexes

Defined at `backend/src/models/Review.ts:34-36`:

- `{ subjectType: 1, subjectId: 1, createdAt: -1 }` — listing for a subject.
- `{ reviewerId: 1, subjectType: 1 }` — reviewer history.
- `{ subjectType: 1, subjectId: 1, reviewerId: 1 }` — **unique**, one review per reviewer per subject.

## Pre/Post Hooks

None declared.

## Instance Methods

None defined.

## Static Methods

None defined.

## Relationships

- **References**: [[User]] (`sellerId`, `reviewerId`, and `subjectId` when `subjectType === 'seller'`), [[RequestTemplate]] (`subjectId` when `subjectType === 'template'`), [[PurchaseRequest]] (`purchaseRequestId`).
- **Referenced by**: none.

## State Transitions

```mermaid
stateDiagram-v2
    [*] --> published : default
    [*] --> pending : moderation required
    pending --> published : approved
    pending --> rejected : rejected
    published --> rejected : flagged
    rejected --> [*]
```

## Common Queries

```ts
// All reviews for a seller
Review.find({ subjectType: 'seller', subjectId: sellerUserId, status: 'published' })
      .sort({ createdAt: -1 });

// Average rating per seller
Review.aggregate([
  { $match: { subjectType: 'seller', subjectId: sellerUserId, status: 'published' } },
  { $group: { _id: null, avg: { $avg: '$rating' }, count: { $sum: 1 } } }
]);

// Reviews written by a user
Review.find({ reviewerId: userId }).sort({ createdAt: -1 });

// Reviews for a template
Review.find({ subjectType: 'template', subjectId: templateId, status: 'published' });
```

> [!warning] Duplicate prevention
> Attempting to insert a second review for the same `(subjectType, subjectId, reviewerId)` will fail with a `E11000 duplicate key` error from MongoDB. Application code should treat that as "already reviewed".

Related: [[User]], [[RequestTemplate]], [[PurchaseRequest]].
