From bfa040859c960ed0f87b54f9be180d9d73f00924 Mon Sep 17 00:00:00 2001 From: Benard Simon <103938678+BigBen-7@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:39:52 +0000 Subject: [PATCH] feat: implement Project mongoose model with full schema and indexes Resolves #70 - Add all required fields: title, description, category, goalAmount, raisedAmount (default 0), currency (default XLM), owner (ref User), stellarAddress, status (draft|pending|active|rejected|completed), coverImage, documents[], startDate, endDate, createdAt, updatedAt - Add indexes on status and owner fields - Add unit tests covering defaults, required field validation, enum validation, optional fields, and index registration --- src/__tests__/project.model.test.js | 84 +++++++++++++++++++++++++++++ src/models/Project.model.js | 36 +++++++++---- 2 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/project.model.test.js diff --git a/src/__tests__/project.model.test.js b/src/__tests__/project.model.test.js new file mode 100644 index 0000000..1520b36 --- /dev/null +++ b/src/__tests__/project.model.test.js @@ -0,0 +1,84 @@ +const mongoose = require('mongoose'); +const Project = require('../models/Project.model'); + +describe('Project model schema', () => { + const ownerId = new mongoose.Types.ObjectId(); + + it('saves with required fields and correct defaults', () => { + const project = new Project({ title: 'Clean Water Fund', goalAmount: 5000, owner: ownerId }); + + expect(project.title).toBe('Clean Water Fund'); + expect(project.goalAmount).toBe(5000); + expect(project.raisedAmount).toBe(0); + expect(project.currency).toBe('XLM'); + expect(project.status).toBe('draft'); + expect(project.documents).toEqual([]); + }); + + it('rejects invalid status enum', () => { + const project = new Project({ title: 'Test', goalAmount: 100, owner: ownerId, status: 'unknown' }); + const err = project.validateSync(); + expect(err.errors.status).toBeDefined(); + }); + + it('requires title', () => { + const project = new Project({ goalAmount: 100, owner: ownerId }); + const err = project.validateSync(); + expect(err.errors.title).toBeDefined(); + }); + + it('requires owner', () => { + const project = new Project({ title: 'Test', goalAmount: 100 }); + const err = project.validateSync(); + expect(err.errors.owner).toBeDefined(); + }); + + it('requires goalAmount', () => { + const project = new Project({ title: 'Test', owner: ownerId }); + const err = project.validateSync(); + expect(err.errors.goalAmount).toBeDefined(); + }); + + it('accepts all status enum values', () => { + for (const status of ['draft', 'pending', 'active', 'rejected', 'completed']) { + const project = new Project({ title: 'T', goalAmount: 1, owner: ownerId, status }); + expect(project.validateSync()).toBeUndefined(); + } + }); + + it('accepts optional fields', () => { + const project = new Project({ + title: 'Solar Initiative', + goalAmount: 10000, + owner: ownerId, + category: 'Energy', + currency: 'USDC', + stellarAddress: 'GABC123', + coverImage: 'https://example.com/cover.jpg', + startDate: new Date('2026-01-01'), + endDate: new Date('2026-12-31'), + documents: [ + { + originalName: 'plan.pdf', + filename: 'plan-stored.pdf', + mimetype: 'application/pdf', + size: 1024, + url: '/uploads/plan-stored.pdf', + }, + ], + }); + + expect(project.category).toBe('Energy'); + expect(project.currency).toBe('USDC'); + expect(project.stellarAddress).toBe('GABC123'); + expect(project.documents).toHaveLength(1); + expect(project.documents[0].originalName).toBe('plan.pdf'); + }); + + it('has indexes on status and owner', () => { + const indexes = Project.schema.indexes(); + const keys = indexes.map(([fields]) => Object.keys(fields).join(',')); + expect(keys).toContain('status'); + expect(keys).toContain('owner'); + }); +}); diff --git a/src/models/Project.model.js b/src/models/Project.model.js index f6d70fe..0b3ab4b 100644 --- a/src/models/Project.model.js +++ b/src/models/Project.model.js @@ -3,25 +3,39 @@ const mongoose = require('mongoose'); const documentSchema = new mongoose.Schema( { originalName: { type: String, required: true }, - filename: { type: String, required: true }, // stored name on disk - mimetype: { type: String, required: true }, - size: { type: Number, required: true }, // bytes - url: { type: String, required: true }, // public URL path - uploadedAt: { type: Date, default: Date.now }, + filename: { type: String, required: true }, + mimetype: { type: String, required: true }, + size: { type: Number, required: true }, + url: { type: String, required: true }, + uploadedAt: { type: Date, default: Date.now }, }, { _id: true } ); const projectSchema = new mongoose.Schema( { - title: { type: String, required: true, trim: true }, + title: { type: String, required: true, trim: true }, description: { type: String, trim: true }, - owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, - documents: { type: [documentSchema], default: [] }, - status: { type: String, enum: ['active', 'inactive'], default: 'active' }, - // ... add your other project fields here + category: { type: String, trim: true }, + goalAmount: { type: Number, required: true, min: 0 }, + raisedAmount: { type: Number, default: 0, min: 0 }, + currency: { type: String, default: 'XLM', trim: true }, + owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + stellarAddress: { type: String, trim: true }, + status: { + type: String, + enum: ['draft', 'pending', 'active', 'rejected', 'completed'], + default: 'draft', + }, + coverImage: { type: String, trim: true }, + documents: { type: [documentSchema], default: [] }, + startDate: { type: Date }, + endDate: { type: Date }, }, { timestamps: true } ); -module.exports = mongoose.model('Project', projectSchema); \ No newline at end of file +projectSchema.index({ status: 1 }); +projectSchema.index({ owner: 1 }); + +module.exports = mongoose.model('Project', projectSchema);