Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { BuyerRequestsModule } from './modules/buyer-requests/buyer-requests.mod
import { OffersModule } from './modules/offers/offers.module';
import { SupabaseModule } from './modules/supabase/supabase.module';
import { StoresModule } from './modules/stores/stores.module';
import { ReviewsModule } from './modules/reviews/reviews.module';
import { HealthModule } from './health/health.module';

// Entities
Expand All @@ -39,6 +40,8 @@ import { BuyerRequest } from './modules/buyer-requests/entities/buyer-request.en
import { Offer } from './modules/offers/entities/offer.entity';
import { OfferAttachment } from './modules/offers/entities/offer-attachment.entity';
import { Store } from './modules/stores/entities/store.entity';
import { Review } from './modules/reviews/entities/review.entity';
import { SellerReview } from './modules/reviews/entities/seller-review.entity';

@Module({
imports: [
Expand Down Expand Up @@ -67,6 +70,8 @@ import { Store } from './modules/stores/entities/store.entity';
Offer,
OfferAttachment,
Store,
Review,
SellerReview,
],
synchronize: false,
logging: process.env.NODE_ENV === 'development',
Expand All @@ -86,6 +91,7 @@ import { Store } from './modules/stores/entities/store.entity';
OffersModule,
SupabaseModule,
StoresModule,
ReviewsModule,
HealthModule,
],
})
Expand Down
109 changes: 109 additions & 0 deletions src/migrations/1756000000000-CreateSellerReviewsTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { MigrationInterface, QueryRunner, Table, Index, Check } from 'typeorm';

export class CreateSellerReviewsTable1756000000000 implements MigrationInterface {
name = 'CreateSellerReviewsTable1756000000000';

public async up(queryRunner: QueryRunner): Promise<void> {
// Create seller_reviews table
await queryRunner.createTable(
new Table({
name: 'seller_reviews',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
generationStrategy: 'uuid',
default: 'uuid_generate_v4()',
},
{
name: 'offer_id',
type: 'uuid',
isNullable: false,
},
{
name: 'buyer_id',
type: 'uuid',
isNullable: false,
},
{
name: 'rating',
type: 'int',
isNullable: false,
},
{
name: 'comment',
type: 'text',
isNullable: true,
},
{
name: 'created_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
],
foreignKeys: [
{
columnNames: ['offer_id'],
referencedTableName: 'offers',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
},
{
columnNames: ['buyer_id'],
referencedTableName: 'users',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
},
],
indices: [
new Index('IDX_seller_reviews_offer_id', ['offer_id']),
new Index('IDX_seller_reviews_buyer_id', ['buyer_id']),
new Index('IDX_seller_reviews_created_at', ['created_at']),
],
checks: [
new Check('CHK_seller_reviews_rating_range', '"rating" >= 1 AND "rating" <= 5'),
],
uniques: [
{
name: 'UQ_seller_reviews_offer_buyer',
columnNames: ['offer_id', 'buyer_id'],
},
],
}),
true
);

// Add seller rating columns to users table
await queryRunner.addColumn(
'users',
{
name: 'average_seller_rating',
type: 'decimal',
precision: 3,
scale: 2,
isNullable: true,
}
);

await queryRunner.addColumn(
'users',
{
name: 'total_seller_reviews',
type: 'int',
default: 0,
isNullable: false,
}
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Remove seller rating columns from users table
await queryRunner.dropColumn('users', 'total_seller_reviews');
await queryRunner.dropColumn('users', 'average_seller_rating');

// Drop seller_reviews table
await queryRunner.dropTable('seller_reviews');
}
}
236 changes: 236 additions & 0 deletions src/modules/reviews/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# Seller Review System

This module implements a comprehensive review and rating system for sellers based on completed offers. Buyers can rate and review sellers after confirming a purchase, which helps build trust and assists future buyers in making informed decisions.

## Features

- βœ… **One review per offer**: Ensures each offer can only be reviewed once
- βœ… **Buyer validation**: Only the buyer who confirmed the purchase can review
- βœ… **Rating validation**: Ratings must be between 1-5 stars
- βœ… **Seller rating aggregation**: Automatic calculation of average ratings
- βœ… **Comprehensive API**: Full CRUD operations for reviews
- βœ… **Data validation**: Robust input validation and error handling
- βœ… **Test coverage**: Unit and integration tests included

## Database Schema

### SellerReviews Table
```sql
CREATE TABLE seller_reviews (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
offer_id UUID NOT NULL REFERENCES offers(id) ON DELETE CASCADE,
buyer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
rating INT NOT NULL CHECK (rating >= 1 AND rating <= 5),
comment TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(offer_id, buyer_id)
);
```

### User Table Updates
```sql
ALTER TABLE users ADD COLUMN average_seller_rating DECIMAL(3,2);
ALTER TABLE users ADD COLUMN total_seller_reviews INT DEFAULT 0;
```

## API Endpoints

### POST /api/v1/reviews
Create a new review for a seller based on an offer.

**Authentication**: Required (JWT)

**Request Body**:
```json
{
"offerId": "uuid",
"rating": 5,
"comment": "Great seller! Very responsive and professional."
}
```

**Response**:
```json
{
"success": true,
"data": {
"id": "review-uuid",
"offerId": "offer-uuid",
"buyerId": "buyer-uuid",
"rating": 5,
"comment": "Great seller!",
"createdAt": "2024-01-15T10:30:00Z",
"buyer": {
"id": "buyer-uuid",
"name": "John Doe",
"walletAddress": "buyer-wallet-address"
},
"offer": {
"id": "offer-uuid",
"title": "Custom Product",
"price": 100.00,
"sellerId": "seller-uuid"
}
}
}
```

### GET /api/v1/users/:id/reviews
Get all reviews for a specific seller.

**Authentication**: Not required (Public endpoint)

**Response**:
```json
{
"success": true,
"data": {
"reviews": [
{
"id": "review-uuid",
"offerId": "offer-uuid",
"buyerId": "buyer-uuid",
"rating": 5,
"comment": "Great seller!",
"createdAt": "2024-01-15T10:30:00Z",
"buyer": {
"id": "buyer-uuid",
"name": "John Doe",
"walletAddress": "buyer-wallet-address"
},
"offer": {
"id": "offer-uuid",
"title": "Custom Product",
"price": 100.00,
"sellerId": "seller-uuid"
}
}
],
"averageRating": 4.5,
"totalReviews": 10,
"seller": {
"id": "seller-uuid",
"name": "Jane Smith",
"walletAddress": "seller-wallet-address",
"averageSellerRating": 4.5,
"totalSellerReviews": 10
}
}
}
```

### PUT /api/v1/reviews/:id
Update an existing review.

**Authentication**: Required (JWT)

**Request Body**:
```json
{
"rating": 4,
"comment": "Updated comment"
}
```

### DELETE /api/v1/reviews/:id
Delete a review.

**Authentication**: Required (JWT)

## Business Rules

1. **One Review Per Offer**: Each offer can only be reviewed once by the buyer
2. **Buyer Validation**: Only the buyer who confirmed the purchase can review the offer
3. **Purchase Confirmation**: Reviews can only be created for offers that have been purchased (`wasPurchased = true`)
4. **Rating Range**: Ratings must be between 1 and 5 stars
5. **Ownership**: Users can only update/delete their own reviews
6. **Automatic Aggregation**: Seller ratings are automatically calculated and updated

## Validation Rules

- `offerId`: Must be a valid UUID and reference an existing offer
- `rating`: Must be an integer between 1 and 5
- `comment`: Optional text field (max length handled by database)
- `buyerId`: Automatically set from authenticated user

## Error Handling

The system provides comprehensive error handling for various scenarios:

- **404 Not Found**: When offer, user, or review doesn't exist
- **400 Bad Request**: Invalid input data, rating out of range, offer not purchased
- **403 Forbidden**: User trying to review offer they didn't purchase
- **409 Conflict**: Attempting to create duplicate review

## Testing

The module includes comprehensive test coverage:

- **Unit Tests**: Service and controller logic
- **Integration Tests**: End-to-end API testing
- **Validation Tests**: Input validation and error handling

Run tests with:
```bash
npm test -- --testPathPattern=seller-review
```

## Usage Examples

### Creating a Review
```typescript
const reviewData = {
offerId: 'offer-uuid',
rating: 5,
comment: 'Excellent service and fast delivery!'
};

const review = await sellerReviewService.createReview(buyerId, reviewData);
```

### Getting Seller Reviews
```typescript
const sellerReviews = await sellerReviewService.getSellerReviews(sellerId);
console.log(`Average rating: ${sellerReviews.averageRating}`);
console.log(`Total reviews: ${sellerReviews.totalReviews}`);
```

### Updating a Review
```typescript
const updateData = {
rating: 4,
comment: 'Updated my rating after further consideration'
};

const updatedReview = await sellerReviewService.updateReview(
reviewId,
buyerId,
updateData
);
```

## Database Migration

To apply the database changes, run the migration:

```bash
npm run migration:run
```

The migration will:
1. Create the `seller_reviews` table with proper constraints
2. Add seller rating columns to the `users` table
3. Create necessary indexes for performance

## Performance Considerations

- Indexes are created on frequently queried columns (`offer_id`, `buyer_id`, `created_at`)
- Seller rating aggregation is cached in the `users` table
- Queries use proper joins to minimize database round trips

## Security

- JWT authentication required for creating, updating, and deleting reviews
- Input validation prevents malicious data
- Foreign key constraints ensure data integrity
- Users can only modify their own reviews
Loading