96 lines
3.6 KiB
Markdown
96 lines
3.6 KiB
Markdown
---
|
|
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]].
|